Skip to content

Commit

Permalink
[refactor] アップデート通知ダイアログ周りをEditorHome.vueから分離 (#1717)
Browse files Browse the repository at this point in the history
* e2eテスト追加

* 色々実装しちゃったやつ

* バグフィックス

* assertNonNullable

* UrlString
  • Loading branch information
Hiroshiba authored Jan 20, 2024
1 parent 27529f3 commit ab7bd48
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 65 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ module.exports = {
order: ["template", "script", "style"],
},
],
"vue/multi-word-component-names": [
"error",
{
ignores: ["Container", "Presentation"],
},
],
"import/order": "error",
"no-restricted-syntax": [
"warn",
Expand Down
89 changes: 89 additions & 0 deletions src/components/UpdateNotificationDialog/Container.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<!--
アップデート通知ダイアログのコンテナ。
スキップしたバージョンより新しいバージョンがあれば、ダイアログを表示する。
-->

<template>
<update-notification-dialog
v-if="newUpdateResult.status == 'updateAvailable'"
v-model="isDialogOpenComputed"
:latest-version="newUpdateResult.latestVersion"
:new-update-infos="newUpdateResult.newUpdateInfos"
@skip-this-version-click="handleSkipThisVersionClick"
/>
</template>

<script setup lang="ts">
import semver from "semver";
import { computed, watch } from "vue";
import UpdateNotificationDialog from "./Presentation.vue";
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";
import { useStore } from "@/store";
import { UrlString } from "@/type/preload";

const props =
defineProps<{
canOpenDialog: boolean; // ダイアログを開いても良いかどうか
}>();

const store = useStore();

const isDialogOpenComputed = computed({
get: () => store.state.isUpdateNotificationDialogOpen,
set: (val) =>
store.dispatch("SET_DIALOG_OPEN", {
isUpdateNotificationDialogOpen: val,
}),
});

// エディタのアップデート確認
if (!import.meta.env.VITE_LATEST_UPDATE_INFOS_URL) {
throw new Error(
"環境変数VITE_LATEST_UPDATE_INFOS_URLが設定されていません。.envに記載してください。"
);
}

// アプリのバージョンとスキップしたバージョンのうち、新しい方を返す
const currentVersionGetter = async () => {
const appVersion = await window.electron
.getAppInfos()
.then((obj) => obj.version);

await store.dispatch("WAIT_VUEX_READY", { timeout: 15000 });
const skipUpdateVersion = store.state.skipUpdateVersion ?? "0.0.0";
if (semver.valid(skipUpdateVersion) == undefined) {
throw new Error(`skipUpdateVersionが不正です: ${skipUpdateVersion}`);
}

return semver.gt(appVersion, skipUpdateVersion)
? appVersion
: skipUpdateVersion;
};

// 新しいバージョンがあれば取得
const newUpdateResult = useFetchNewUpdateInfos(
currentVersionGetter,
UrlString(import.meta.env.VITE_LATEST_UPDATE_INFOS_URL)
);

// 新しいバージョンのアップデートがスキップされたときの処理
const handleSkipThisVersionClick = (version: string) => {
store.dispatch("SET_ROOT_MISC_SETTING", {
key: "skipUpdateVersion",
value: version,
});
};

// ダイアログを開くかどうか
watch(
() => [props.canOpenDialog, newUpdateResult],
() => {
if (
props.canOpenDialog &&
newUpdateResult.value.status == "updateAvailable"
) {
isDialogOpenComputed.value = true;
}
}
);
</script>
File renamed without changes.
4 changes: 2 additions & 2 deletions src/components/help/HelpDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ import UpdateInfo from "./UpdateInfo.vue";
import OssCommunityInfo from "./OssCommunityInfo.vue";
import QAndA from "./QAndA.vue";
import ContactInfo from "./ContactInfo.vue";
import { UpdateInfo as UpdateInfoObject } from "@/type/preload";
import { UpdateInfo as UpdateInfoObject, UrlString } from "@/type/preload";
import { useStore } from "@/store";
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";
Expand Down Expand Up @@ -139,7 +139,7 @@ if (!import.meta.env.VITE_LATEST_UPDATE_INFOS_URL) {
}
const newUpdateResult = useFetchNewUpdateInfos(
() => window.electron.getAppInfos().then((obj) => obj.version), // アプリのバージョン
import.meta.env.VITE_LATEST_UPDATE_INFOS_URL
UrlString(import.meta.env.VITE_LATEST_UPDATE_INFOS_URL)
);
// エディタのOSSライセンス取得
Expand Down
4 changes: 2 additions & 2 deletions src/composables/useFetchNewUpdateInfos.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ref } from "vue";
import semver from "semver";
import { z } from "zod";
import { UpdateInfo, updateInfoSchema } from "@/type/preload";
import { UpdateInfo, UrlString, updateInfoSchema } from "@/type/preload";

/**
* 現在のバージョンより新しいバージョンがリリースされているか調べる。
* あれば最新バージョンと、現在より新しいバージョンの情報を返す。
*/
export const useFetchNewUpdateInfos = (
currentVersionGetter: () => Promise<string>,
newUpdateInfosUrl: string
newUpdateInfosUrl: UrlString
) => {
const result = ref<
| {
Expand Down
4 changes: 4 additions & 0 deletions src/store/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,6 +1266,10 @@ export type UiStoreTypes = {
action(): void;
};

WAIT_VUEX_READY: {
action(palyoad: { timeout: number }): Promise<void>;
};

HYDRATE_UI_STORE: {
action(): void;
};
Expand Down
16 changes: 16 additions & 0 deletions src/store/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,22 @@ export const uiStore = createPartialStore<UiStoreTypes>({
},
},

// Vuexが準備できるまで待つ
WAIT_VUEX_READY: {
async action({ state }, { timeout }) {
if (state.isVuexReady) return;

let vuexReadyTimeout = 0;
while (!state.isVuexReady) {
if (vuexReadyTimeout >= timeout) {
throw new Error("Vuexが準備できませんでした");
}
await new Promise((resolve) => setTimeout(resolve, 300));
vuexReadyTimeout += 300;
}
},
},

SET_INHERIT_AUDIOINFO: {
mutation(state, { inheritAudioInfo }: { inheritAudioInfo: boolean }) {
state.inheritAudioInfo = inheritAudioInfo;
Expand Down
4 changes: 4 additions & 0 deletions src/type/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ function checkIsMac(): boolean {
}
export const isMac = checkIsMac();

const urlStringSchema = z.string().url().brand("URL");
export type UrlString = z.infer<typeof urlStringSchema>;
export const UrlString = (url: string): UrlString => urlStringSchema.parse(url);

export const engineIdSchema = z.string().brand<"EngineId">();
export type EngineId = z.infer<typeof engineIdSchema>;
export const EngineId = (id: string): EngineId => engineIdSchema.parse(id);
Expand Down
67 changes: 9 additions & 58 deletions src/views/EditorHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,8 @@
v-model="isAcceptRetrieveTelemetryDialogOpenComputed"
/>
<accept-terms-dialog v-model="isAcceptTermsDialogOpenComputed" />
<update-notification-dialog
v-if="newUpdateResult.status == 'updateAvailable'"
v-model="isUpdateNotificationDialogOpenComputed"
:latest-version="newUpdateResult.latestVersion"
:new-update-infos="newUpdateResult.newUpdateInfos"
@skip-this-version-click="handleSkipThisVersionClick"
<update-notification-dialog-container
:can-open-dialog="canOpenNotificationDialog"
/>
</template>

Expand All @@ -193,7 +189,6 @@ import draggable from "vuedraggable";
import { QResizeObserver } from "quasar";
import cloneDeep from "clone-deep";
import Mousetrap from "mousetrap";
import semver from "semver";
import { useStore } from "@/store";
import HeaderBar from "@/components/HeaderBar.vue";
import AudioCell from "@/components/AudioCell.vue";
Expand All @@ -212,8 +207,7 @@ import AcceptTermsDialog from "@/components/AcceptTermsDialog.vue";
import DictionaryManageDialog from "@/components/DictionaryManageDialog.vue";
import EngineManageDialog from "@/components/EngineManageDialog.vue";
import ProgressDialog from "@/components/ProgressDialog.vue";
import UpdateNotificationDialog from "@/components/UpdateNotificationDialog.vue";
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";
import UpdateNotificationDialogContainer from "@/components/UpdateNotificationDialog/Container.vue";
import { AudioItem, EngineState } from "@/store/type";
import {
AudioKey,
Expand Down Expand Up @@ -553,23 +547,6 @@ watch(userOrderedCharacterInfos, (userOrderedCharacterInfos) => {
}
});
// エディタのアップデート確認
if (!import.meta.env.VITE_LATEST_UPDATE_INFOS_URL) {
throw new Error(
"環境変数VITE_LATEST_UPDATE_INFOS_URLが設定されていません。.envに記載してください。"
);
}
const newUpdateResult = useFetchNewUpdateInfos(
() => window.electron.getAppInfos().then((obj) => obj.version), // アプリのバージョン
import.meta.env.VITE_LATEST_UPDATE_INFOS_URL
);
const handleSkipThisVersionClick = (version: string) => {
store.dispatch("SET_ROOT_MISC_SETTING", {
key: "skipUpdateVersion",
value: version,
});
};
// ソフトウェアを初期化
const isCompletedInitialStartup = ref(false);
onMounted(async () => {
Expand Down Expand Up @@ -647,14 +624,7 @@ onMounted(async () => {
// 設定の読み込みを待機する
// FIXME: 設定が必要な処理はINIT_VUEXを実行しているApp.vueで行うべき
let vuexReadyTimeout = 0;
while (!store.state.isVuexReady) {
if (vuexReadyTimeout >= 15000) {
throw new Error("Vuexが準備できませんでした");
}
await new Promise((resolve) => setTimeout(resolve, 300));
vuexReadyTimeout += 300;
}
await store.dispatch("WAIT_VUEX_READY", { timeout: 15000 });
isAcceptRetrieveTelemetryDialogOpenComputed.value =
store.state.acceptRetrieveTelemetry === "Unconfirmed";
Expand All @@ -663,22 +633,6 @@ onMounted(async () => {
import.meta.env.MODE !== "development" &&
store.state.acceptTerms !== "Accepted";
// アップデート通知ダイアログ
if (newUpdateResult.value.status === "updateAvailable") {
const skipUpdateVersion = store.state.skipUpdateVersion ?? "0.0.0";
if (semver.valid(skipUpdateVersion) == undefined) {
// 処理を止めるほどではないので警告だけ
store.dispatch(
"LOG_WARN",
`skipUpdateVersionが不正です: ${skipUpdateVersion}`
);
} else if (
semver.gt(newUpdateResult.value.latestVersion, skipUpdateVersion)
) {
isUpdateNotificationDialogOpenComputed.value = true;
}
}
isCompletedInitialStartup.value = true;
});
Expand Down Expand Up @@ -854,18 +808,15 @@ const isAcceptRetrieveTelemetryDialogOpenComputed = computed({
}),
});
// アップデート通知
const isUpdateNotificationDialogOpenComputed = computed({
get: () =>
// エディタのアップデート確認ダイアログ
const canOpenNotificationDialog = computed(() => {
return (
!store.state.isAcceptTermsDialogOpen &&
!store.state.isCharacterOrderDialogOpen &&
!store.state.isDefaultStyleSelectDialogOpen &&
!store.state.isAcceptRetrieveTelemetryDialogOpen &&
store.state.isUpdateNotificationDialogOpen,
set: (val) =>
store.dispatch("SET_DIALOG_OPEN", {
isUpdateNotificationDialogOpen: val,
}),
isCompletedInitialStartup.value
);
});
// ドラッグ&ドロップ
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/components/UpdateNotificationDialog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
mount,
flushPromises,
DOMWrapper,
enableAutoUnmount,
} from "@vue/test-utils";
import { describe, it } from "vitest";
import { Quasar } from "quasar";

import UpdateNotificationDialogPresentation from "@/components/UpdateNotificationDialog/Presentation.vue";
import { assertNonNullable } from "@/type/utility";

const mountUpdateNotificationDialogPresentation = async (context?: {
latestVersion?: string;
onSkipThisVersionClick?: (version: string) => void;
}) => {
const latestVersion = context?.latestVersion ?? "1.0.0";
const onSkipThisVersionClick =
context?.onSkipThisVersionClick ?? (() => undefined);

const wrapper = mount(UpdateNotificationDialogPresentation, {
props: {
modelValue: true,
latestVersion,
newUpdateInfos: [],
onSkipThisVersionClick,
},
global: {
plugins: [Quasar],
},
});
await flushPromises();
const domWrapper = new DOMWrapper(document.body); // QDialogを取得するワークアラウンド

const buttons = domWrapper.findAll("button");

const skipButton = buttons.find((button) => button.text().match(/スキップ/));
assertNonNullable(skipButton);

const exitButton = buttons.find((button) => button.text().match(/閉じる/));
assertNonNullable(exitButton);

return { wrapper, skipButton, exitButton };
};

describe("Presentation", () => {
enableAutoUnmount(afterEach);

it("マウントできる", async () => {
mountUpdateNotificationDialogPresentation();
});

it("閉じるボタンを押すと閉じられる", async () => {
const { wrapper, exitButton } =
await mountUpdateNotificationDialogPresentation();
await exitButton.trigger("click");
expect(wrapper.emitted("update:modelValue")).toEqual([[false]]);
});

it("スキップボタンを押すとコールバックが実行される", async () => {
const onSkipThisVersionClick = vi.fn();
const { skipButton } = await mountUpdateNotificationDialogPresentation({
onSkipThisVersionClick,
});
await skipButton.trigger("click");
expect(onSkipThisVersionClick).toHaveBeenCalled();
});
});
6 changes: 3 additions & 3 deletions tests/unit/composable/useFetchNewUpdateInfos.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Ref } from "vue";
import { UpdateInfo } from "@/type/preload";
import { UpdateInfo, UrlString } from "@/type/preload";
import { useFetchNewUpdateInfos } from "@/composables/useFetchNewUpdateInfos";

// 最新バージョンの情報をfetchするモックを作成する
Expand Down Expand Up @@ -31,7 +31,7 @@ it("新バージョンがある場合、latestVersionに最新バージョンが

const result = useFetchNewUpdateInfos(
async () => currentVersion,
"Dummy Url"
UrlString("http://example.com")
);

await waitFinished(result);
Expand All @@ -48,7 +48,7 @@ it("新バージョンがない場合は状態が変わるだけ", async () => {

const result = useFetchNewUpdateInfos(
async () => currentVersion,
"Dummy Url"
UrlString("http://example.com")
);

await waitFinished(result);
Expand Down

0 comments on commit ab7bd48

Please sign in to comment.