Skip to content

Commit ab7bd48

Browse files
authored
[refactor] アップデート通知ダイアログ周りをEditorHome.vueから分離 (#1717)
* e2eテスト追加 * 色々実装しちゃったやつ * バグフィックス * assertNonNullable * UrlString
1 parent 27529f3 commit ab7bd48

File tree

11 files changed

+203
-65
lines changed

11 files changed

+203
-65
lines changed

.eslintrc.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ module.exports = {
5555
order: ["template", "script", "style"],
5656
},
5757
],
58+
"vue/multi-word-component-names": [
59+
"error",
60+
{
61+
ignores: ["Container", "Presentation"],
62+
},
63+
],
5864
"import/order": "error",
5965
"no-restricted-syntax": [
6066
"warn",
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<!--
2+
アップデート通知ダイアログのコンテナ。
3+
スキップしたバージョンより新しいバージョンがあれば、ダイアログを表示する。
4+
-->
5+
6+
<template>
7+
<update-notification-dialog
8+
v-if="newUpdateResult.status == 'updateAvailable'"
9+
v-model="isDialogOpenComputed"
10+
:latest-version="newUpdateResult.latestVersion"
11+
:new-update-infos="newUpdateResult.newUpdateInfos"
12+
@skip-this-version-click="handleSkipThisVersionClick"
13+
/>
14+
</template>
15+
16+
<script setup lang="ts">
17+
import semver from "semver";
18+
import { computed, watch } from "vue";
19+
import UpdateNotificationDialog from "./Presentation.vue";
20+
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";
21+
import { useStore } from "@/store";
22+
import { UrlString } from "@/type/preload";
23+
24+
const props =
25+
defineProps<{
26+
canOpenDialog: boolean; // ダイアログを開いても良いかどうか
27+
}>();
28+
29+
const store = useStore();
30+
31+
const isDialogOpenComputed = computed({
32+
get: () => store.state.isUpdateNotificationDialogOpen,
33+
set: (val) =>
34+
store.dispatch("SET_DIALOG_OPEN", {
35+
isUpdateNotificationDialogOpen: val,
36+
}),
37+
});
38+
39+
// エディタのアップデート確認
40+
if (!import.meta.env.VITE_LATEST_UPDATE_INFOS_URL) {
41+
throw new Error(
42+
"環境変数VITE_LATEST_UPDATE_INFOS_URLが設定されていません。.envに記載してください。"
43+
);
44+
}
45+
46+
// アプリのバージョンとスキップしたバージョンのうち、新しい方を返す
47+
const currentVersionGetter = async () => {
48+
const appVersion = await window.electron
49+
.getAppInfos()
50+
.then((obj) => obj.version);
51+
52+
await store.dispatch("WAIT_VUEX_READY", { timeout: 15000 });
53+
const skipUpdateVersion = store.state.skipUpdateVersion ?? "0.0.0";
54+
if (semver.valid(skipUpdateVersion) == undefined) {
55+
throw new Error(`skipUpdateVersionが不正です: ${skipUpdateVersion}`);
56+
}
57+
58+
return semver.gt(appVersion, skipUpdateVersion)
59+
? appVersion
60+
: skipUpdateVersion;
61+
};
62+
63+
// 新しいバージョンがあれば取得
64+
const newUpdateResult = useFetchNewUpdateInfos(
65+
currentVersionGetter,
66+
UrlString(import.meta.env.VITE_LATEST_UPDATE_INFOS_URL)
67+
);
68+
69+
// 新しいバージョンのアップデートがスキップされたときの処理
70+
const handleSkipThisVersionClick = (version: string) => {
71+
store.dispatch("SET_ROOT_MISC_SETTING", {
72+
key: "skipUpdateVersion",
73+
value: version,
74+
});
75+
};
76+
77+
// ダイアログを開くかどうか
78+
watch(
79+
() => [props.canOpenDialog, newUpdateResult],
80+
() => {
81+
if (
82+
props.canOpenDialog &&
83+
newUpdateResult.value.status == "updateAvailable"
84+
) {
85+
isDialogOpenComputed.value = true;
86+
}
87+
}
88+
);
89+
</script>

src/components/help/HelpDialog.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ import UpdateInfo from "./UpdateInfo.vue";
9595
import OssCommunityInfo from "./OssCommunityInfo.vue";
9696
import QAndA from "./QAndA.vue";
9797
import ContactInfo from "./ContactInfo.vue";
98-
import { UpdateInfo as UpdateInfoObject } from "@/type/preload";
98+
import { UpdateInfo as UpdateInfoObject, UrlString } from "@/type/preload";
9999
import { useStore } from "@/store";
100100
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";
101101
@@ -139,7 +139,7 @@ if (!import.meta.env.VITE_LATEST_UPDATE_INFOS_URL) {
139139
}
140140
const newUpdateResult = useFetchNewUpdateInfos(
141141
() => window.electron.getAppInfos().then((obj) => obj.version), // アプリのバージョン
142-
import.meta.env.VITE_LATEST_UPDATE_INFOS_URL
142+
UrlString(import.meta.env.VITE_LATEST_UPDATE_INFOS_URL)
143143
);
144144
145145
// エディタのOSSライセンス取得

src/composables/useFetchNewUpdateInfos.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { ref } from "vue";
22
import semver from "semver";
33
import { z } from "zod";
4-
import { UpdateInfo, updateInfoSchema } from "@/type/preload";
4+
import { UpdateInfo, UrlString, updateInfoSchema } from "@/type/preload";
55

66
/**
77
* 現在のバージョンより新しいバージョンがリリースされているか調べる。
88
* あれば最新バージョンと、現在より新しいバージョンの情報を返す。
99
*/
1010
export const useFetchNewUpdateInfos = (
1111
currentVersionGetter: () => Promise<string>,
12-
newUpdateInfosUrl: string
12+
newUpdateInfosUrl: UrlString
1313
) => {
1414
const result = ref<
1515
| {

src/store/type.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,6 +1266,10 @@ export type UiStoreTypes = {
12661266
action(): void;
12671267
};
12681268

1269+
WAIT_VUEX_READY: {
1270+
action(palyoad: { timeout: number }): Promise<void>;
1271+
};
1272+
12691273
HYDRATE_UI_STORE: {
12701274
action(): void;
12711275
};

src/store/ui.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,22 @@ export const uiStore = createPartialStore<UiStoreTypes>({
269269
},
270270
},
271271

272+
// Vuexが準備できるまで待つ
273+
WAIT_VUEX_READY: {
274+
async action({ state }, { timeout }) {
275+
if (state.isVuexReady) return;
276+
277+
let vuexReadyTimeout = 0;
278+
while (!state.isVuexReady) {
279+
if (vuexReadyTimeout >= timeout) {
280+
throw new Error("Vuexが準備できませんでした");
281+
}
282+
await new Promise((resolve) => setTimeout(resolve, 300));
283+
vuexReadyTimeout += 300;
284+
}
285+
},
286+
},
287+
272288
SET_INHERIT_AUDIOINFO: {
273289
mutation(state, { inheritAudioInfo }: { inheritAudioInfo: boolean }) {
274290
state.inheritAudioInfo = inheritAudioInfo;

src/type/preload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ function checkIsMac(): boolean {
2727
}
2828
export const isMac = checkIsMac();
2929

30+
const urlStringSchema = z.string().url().brand("URL");
31+
export type UrlString = z.infer<typeof urlStringSchema>;
32+
export const UrlString = (url: string): UrlString => urlStringSchema.parse(url);
33+
3034
export const engineIdSchema = z.string().brand<"EngineId">();
3135
export type EngineId = z.infer<typeof engineIdSchema>;
3236
export const EngineId = (id: string): EngineId => engineIdSchema.parse(id);

src/views/EditorHome.vue

Lines changed: 9 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,8 @@
177177
v-model="isAcceptRetrieveTelemetryDialogOpenComputed"
178178
/>
179179
<accept-terms-dialog v-model="isAcceptTermsDialogOpenComputed" />
180-
<update-notification-dialog
181-
v-if="newUpdateResult.status == 'updateAvailable'"
182-
v-model="isUpdateNotificationDialogOpenComputed"
183-
:latest-version="newUpdateResult.latestVersion"
184-
:new-update-infos="newUpdateResult.newUpdateInfos"
185-
@skip-this-version-click="handleSkipThisVersionClick"
180+
<update-notification-dialog-container
181+
:can-open-dialog="canOpenNotificationDialog"
186182
/>
187183
</template>
188184

@@ -193,7 +189,6 @@ import draggable from "vuedraggable";
193189
import { QResizeObserver } from "quasar";
194190
import cloneDeep from "clone-deep";
195191
import Mousetrap from "mousetrap";
196-
import semver from "semver";
197192
import { useStore } from "@/store";
198193
import HeaderBar from "@/components/HeaderBar.vue";
199194
import AudioCell from "@/components/AudioCell.vue";
@@ -212,8 +207,7 @@ import AcceptTermsDialog from "@/components/AcceptTermsDialog.vue";
212207
import DictionaryManageDialog from "@/components/DictionaryManageDialog.vue";
213208
import EngineManageDialog from "@/components/EngineManageDialog.vue";
214209
import ProgressDialog from "@/components/ProgressDialog.vue";
215-
import UpdateNotificationDialog from "@/components/UpdateNotificationDialog.vue";
216-
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";
210+
import UpdateNotificationDialogContainer from "@/components/UpdateNotificationDialog/Container.vue";
217211
import { AudioItem, EngineState } from "@/store/type";
218212
import {
219213
AudioKey,
@@ -553,23 +547,6 @@ watch(userOrderedCharacterInfos, (userOrderedCharacterInfos) => {
553547
}
554548
});
555549
556-
// エディタのアップデート確認
557-
if (!import.meta.env.VITE_LATEST_UPDATE_INFOS_URL) {
558-
throw new Error(
559-
"環境変数VITE_LATEST_UPDATE_INFOS_URLが設定されていません。.envに記載してください。"
560-
);
561-
}
562-
const newUpdateResult = useFetchNewUpdateInfos(
563-
() => window.electron.getAppInfos().then((obj) => obj.version), // アプリのバージョン
564-
import.meta.env.VITE_LATEST_UPDATE_INFOS_URL
565-
);
566-
const handleSkipThisVersionClick = (version: string) => {
567-
store.dispatch("SET_ROOT_MISC_SETTING", {
568-
key: "skipUpdateVersion",
569-
value: version,
570-
});
571-
};
572-
573550
// ソフトウェアを初期化
574551
const isCompletedInitialStartup = ref(false);
575552
onMounted(async () => {
@@ -647,14 +624,7 @@ onMounted(async () => {
647624
648625
// 設定の読み込みを待機する
649626
// FIXME: 設定が必要な処理はINIT_VUEXを実行しているApp.vueで行うべき
650-
let vuexReadyTimeout = 0;
651-
while (!store.state.isVuexReady) {
652-
if (vuexReadyTimeout >= 15000) {
653-
throw new Error("Vuexが準備できませんでした");
654-
}
655-
await new Promise((resolve) => setTimeout(resolve, 300));
656-
vuexReadyTimeout += 300;
657-
}
627+
await store.dispatch("WAIT_VUEX_READY", { timeout: 15000 });
658628
659629
isAcceptRetrieveTelemetryDialogOpenComputed.value =
660630
store.state.acceptRetrieveTelemetry === "Unconfirmed";
@@ -663,22 +633,6 @@ onMounted(async () => {
663633
import.meta.env.MODE !== "development" &&
664634
store.state.acceptTerms !== "Accepted";
665635
666-
// アップデート通知ダイアログ
667-
if (newUpdateResult.value.status === "updateAvailable") {
668-
const skipUpdateVersion = store.state.skipUpdateVersion ?? "0.0.0";
669-
if (semver.valid(skipUpdateVersion) == undefined) {
670-
// 処理を止めるほどではないので警告だけ
671-
store.dispatch(
672-
"LOG_WARN",
673-
`skipUpdateVersionが不正です: ${skipUpdateVersion}`
674-
);
675-
} else if (
676-
semver.gt(newUpdateResult.value.latestVersion, skipUpdateVersion)
677-
) {
678-
isUpdateNotificationDialogOpenComputed.value = true;
679-
}
680-
}
681-
682636
isCompletedInitialStartup.value = true;
683637
});
684638
@@ -854,18 +808,15 @@ const isAcceptRetrieveTelemetryDialogOpenComputed = computed({
854808
}),
855809
});
856810
857-
// アップデート通知
858-
const isUpdateNotificationDialogOpenComputed = computed({
859-
get: () =>
811+
// エディタのアップデート確認ダイアログ
812+
const canOpenNotificationDialog = computed(() => {
813+
return (
860814
!store.state.isAcceptTermsDialogOpen &&
861815
!store.state.isCharacterOrderDialogOpen &&
862816
!store.state.isDefaultStyleSelectDialogOpen &&
863817
!store.state.isAcceptRetrieveTelemetryDialogOpen &&
864-
store.state.isUpdateNotificationDialogOpen,
865-
set: (val) =>
866-
store.dispatch("SET_DIALOG_OPEN", {
867-
isUpdateNotificationDialogOpen: val,
868-
}),
818+
isCompletedInitialStartup.value
819+
);
869820
});
870821
871822
// ドラッグ&ドロップ
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import {
2+
mount,
3+
flushPromises,
4+
DOMWrapper,
5+
enableAutoUnmount,
6+
} from "@vue/test-utils";
7+
import { describe, it } from "vitest";
8+
import { Quasar } from "quasar";
9+
10+
import UpdateNotificationDialogPresentation from "@/components/UpdateNotificationDialog/Presentation.vue";
11+
import { assertNonNullable } from "@/type/utility";
12+
13+
const mountUpdateNotificationDialogPresentation = async (context?: {
14+
latestVersion?: string;
15+
onSkipThisVersionClick?: (version: string) => void;
16+
}) => {
17+
const latestVersion = context?.latestVersion ?? "1.0.0";
18+
const onSkipThisVersionClick =
19+
context?.onSkipThisVersionClick ?? (() => undefined);
20+
21+
const wrapper = mount(UpdateNotificationDialogPresentation, {
22+
props: {
23+
modelValue: true,
24+
latestVersion,
25+
newUpdateInfos: [],
26+
onSkipThisVersionClick,
27+
},
28+
global: {
29+
plugins: [Quasar],
30+
},
31+
});
32+
await flushPromises();
33+
const domWrapper = new DOMWrapper(document.body); // QDialogを取得するワークアラウンド
34+
35+
const buttons = domWrapper.findAll("button");
36+
37+
const skipButton = buttons.find((button) => button.text().match(//));
38+
assertNonNullable(skipButton);
39+
40+
const exitButton = buttons.find((button) => button.text().match(//));
41+
assertNonNullable(exitButton);
42+
43+
return { wrapper, skipButton, exitButton };
44+
};
45+
46+
describe("Presentation", () => {
47+
enableAutoUnmount(afterEach);
48+
49+
it("マウントできる", async () => {
50+
mountUpdateNotificationDialogPresentation();
51+
});
52+
53+
it("閉じるボタンを押すと閉じられる", async () => {
54+
const { wrapper, exitButton } =
55+
await mountUpdateNotificationDialogPresentation();
56+
await exitButton.trigger("click");
57+
expect(wrapper.emitted("update:modelValue")).toEqual([[false]]);
58+
});
59+
60+
it("スキップボタンを押すとコールバックが実行される", async () => {
61+
const onSkipThisVersionClick = vi.fn();
62+
const { skipButton } = await mountUpdateNotificationDialogPresentation({
63+
onSkipThisVersionClick,
64+
});
65+
await skipButton.trigger("click");
66+
expect(onSkipThisVersionClick).toHaveBeenCalled();
67+
});
68+
});

0 commit comments

Comments
 (0)