From 51a50f7bf89e3a2836e41ed3ce75fbcbdc47f66a Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 17 Jan 2024 07:43:44 +0900 Subject: [PATCH 1/5] =?UTF-8?q?e2e=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- src/type/utility.ts | 9 +++ ...3\202\242\343\203\255\343\202\260.spec.ts" | 73 +++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 "tests/e2e/browser/\343\202\242\343\203\203\343\203\227\343\203\207\343\203\274\343\203\210\351\200\232\347\237\245\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" diff --git a/README.md b/README.md index 5e0839031e..cd01142973 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ npm run test-watch:browser-e2e -- --headed # テスト中の UI を表示 ``` Playwright を使用しているためテストパターンを生成することもできます。 -ブラウザ版を起動している状態で以下のコマンドを実行してください。 +**ブラウザ版を起動している状態で**以下のコマンドを実行してください。 ```bash npx playwright codegen http://localhost:5173/#/home --viewport-size=800,600 diff --git a/src/type/utility.ts b/src/type/utility.ts index 365c30db99..b01f91120a 100644 --- a/src/type/utility.ts +++ b/src/type/utility.ts @@ -4,3 +4,12 @@ export type IsEqual = (() => T extends X ? 1 : 2) extends < >() => T extends Y ? 1 : 2 ? true : false; + +// undefinedかnullでないことを保証する +export function assertNonNullable( + value: T +): asserts value is NonNullable { + if (value == undefined) { + throw new Error("Value is null or undefined"); + } +} diff --git "a/tests/e2e/browser/\343\202\242\343\203\203\343\203\227\343\203\207\343\203\274\343\203\210\351\200\232\347\237\245\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" "b/tests/e2e/browser/\343\202\242\343\203\203\343\203\227\343\203\207\343\203\274\343\203\210\351\200\232\347\237\245\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" new file mode 100644 index 0000000000..30f16bc503 --- /dev/null +++ "b/tests/e2e/browser/\343\202\242\343\203\203\343\203\227\343\203\207\343\203\274\343\203\210\351\200\232\347\237\245\343\203\200\343\202\244\343\202\242\343\203\255\343\202\260.spec.ts" @@ -0,0 +1,73 @@ +import { test, expect } from "@playwright/test"; +import dotenv from "dotenv"; +import semver from "semver"; +import { navigateToMain, gotoHome } from "../navigators"; +import { getNewestQuasarDialog } from "../locators"; +import { UpdateInfo } from "@/type/preload"; +import { assertNonNullable } from "@/type/utility"; + +// アップデート通知が出る環境にする +test.beforeEach(async ({ page }) => { + dotenv.config(); + + // 動作環境より新しいバージョン + const latestVersion = semver.inc( + process.env.VITE_APP_VERSION ?? process.env.npm_package_version ?? "0.0.0", + "major" + ); + assertNonNullable(latestVersion); + + // アップデート情報を返すAPIのモック + if (process.env.VITE_LATEST_UPDATE_INFOS_URL == undefined) { + throw new Error("VITE_LATEST_UPDATE_INFOS_URL is not defined"); + } + page.route(process.env.VITE_LATEST_UPDATE_INFOS_URL, (route) => { + const updateInfos: UpdateInfo[] = [ + { + version: latestVersion, + descriptions: [], + contributors: [], + }, + ]; + route.fulfill({ + status: 200, + body: JSON.stringify(updateInfos), + }); + }); +}); + +test.beforeEach(async ({ page }) => { + await gotoHome({ page }); + + await navigateToMain(page); + await page.waitForTimeout(100); +}); + +test("アップデートが通知されたりスキップしたりできる", async ({ page }) => { + await page.waitForTimeout(500); + + // 通知されている + const dialog = getNewestQuasarDialog(page); + await expect(dialog.getByText("アップデートのお知らせ")).toBeVisible(); + + // 普通に閉じると消える + await dialog.getByRole("button", { name: "閉じる" }).click(); + await page.waitForTimeout(500); + await expect(dialog).not.toBeVisible(); + + // 再度開くとまた表示される + await page.reload(); + await expect(dialog.getByText("アップデートのお知らせ")).toBeVisible(); + + // スキップすると消える + await dialog + .getByRole("button", { name: "このバージョンをスキップ" }) + .click(); + await page.waitForTimeout(500); + await expect(dialog).not.toBeVisible(); + + // 再度開いても表示されない(スキップされた) + await page.reload(); + await page.waitForTimeout(5000); // エンジン読み込み待機 + await expect(dialog).not.toBeVisible(); +}); From 392aa287922653bad84c1ce47b983f1a996d8c52 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 17 Jan 2024 07:50:32 +0900 Subject: [PATCH 2/5] =?UTF-8?q?=E8=89=B2=E3=80=85=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=81=97=E3=81=A1=E3=82=83=E3=81=A3=E3=81=9F=E3=82=84=E3=81=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.js | 6 ++ .../UpdateNotificationDialog/Container.vue | 89 +++++++++++++++++++ .../Presentation.vue} | 0 src/components/help/HelpDialog.vue | 4 +- src/composables/useFetchNewUpdateInfos.ts | 4 +- src/store/type.ts | 4 + src/store/ui.ts | 16 ++++ src/type/preload.ts | 4 + src/views/EditorHome.vue | 70 +++------------ .../UpdateNotificationDialog.spec.ts | 67 ++++++++++++++ .../composable/useFetchNewUpdateInfos.spec.ts | 6 +- 11 files changed, 206 insertions(+), 64 deletions(-) create mode 100644 src/components/UpdateNotificationDialog/Container.vue rename src/components/{UpdateNotificationDialog.vue => UpdateNotificationDialog/Presentation.vue} (100%) create mode 100644 tests/unit/components/UpdateNotificationDialog.spec.ts diff --git a/.eslintrc.js b/.eslintrc.js index 3abaecc545..3340e91301 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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", diff --git a/src/components/UpdateNotificationDialog/Container.vue b/src/components/UpdateNotificationDialog/Container.vue new file mode 100644 index 0000000000..23ed78b64e --- /dev/null +++ b/src/components/UpdateNotificationDialog/Container.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/src/components/UpdateNotificationDialog.vue b/src/components/UpdateNotificationDialog/Presentation.vue similarity index 100% rename from src/components/UpdateNotificationDialog.vue rename to src/components/UpdateNotificationDialog/Presentation.vue diff --git a/src/components/help/HelpDialog.vue b/src/components/help/HelpDialog.vue index 9d1ab2baa5..6933bf9314 100644 --- a/src/components/help/HelpDialog.vue +++ b/src/components/help/HelpDialog.vue @@ -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"; @@ -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ライセンス取得 diff --git a/src/composables/useFetchNewUpdateInfos.ts b/src/composables/useFetchNewUpdateInfos.ts index 83ba5a823a..2ef7f4ab91 100644 --- a/src/composables/useFetchNewUpdateInfos.ts +++ b/src/composables/useFetchNewUpdateInfos.ts @@ -1,7 +1,7 @@ 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"; /** * 現在のバージョンより新しいバージョンがリリースされているか調べる。 @@ -9,7 +9,7 @@ import { UpdateInfo, updateInfoSchema } from "@/type/preload"; */ export const useFetchNewUpdateInfos = ( currentVersionGetter: () => Promise, - newUpdateInfosUrl: string + newUpdateInfosUrl: UrlString ) => { const result = ref< | { diff --git a/src/store/type.ts b/src/store/type.ts index a52c62fe4a..100e906d0b 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -1266,6 +1266,10 @@ export type UiStoreTypes = { action(): void; }; + WAIT_VUEX_READY: { + action(palyoad: { timeout: number }): Promise; + }; + HYDRATE_UI_STORE: { action(): void; }; diff --git a/src/store/ui.ts b/src/store/ui.ts index dbf4a8f3be..382bddfeff 100644 --- a/src/store/ui.ts +++ b/src/store/ui.ts @@ -269,6 +269,22 @@ export const uiStore = createPartialStore({ }, }, + // 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; diff --git a/src/type/preload.ts b/src/type/preload.ts index 3c620e8207..afbaf90885 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -27,6 +27,10 @@ function checkIsMac(): boolean { } export const isMac = checkIsMac(); +const urlStringSchema = z.string().url().brand("URL"); +export type UrlString = z.infer; +export const UrlString = (url: string): UrlString => urlStringSchema.parse(url); + export const engineIdSchema = z.string().brand<"EngineId">(); export type EngineId = z.infer; export const EngineId = (id: string): EngineId => engineIdSchema.parse(id); diff --git a/src/views/EditorHome.vue b/src/views/EditorHome.vue index 1395dfae6c..6af7392e98 100644 --- a/src/views/EditorHome.vue +++ b/src/views/EditorHome.vue @@ -177,12 +177,8 @@ v-model="isAcceptRetrieveTelemetryDialogOpenComputed" /> - @@ -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"; @@ -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, @@ -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 () => { @@ -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"; @@ -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; }); @@ -854,13 +808,15 @@ const isAcceptRetrieveTelemetryDialogOpenComputed = computed({ }), }); -// アップデート通知 -const isUpdateNotificationDialogOpenComputed = computed({ - get: () => store.state.isUpdateNotificationDialogOpen, - set: (val) => - store.dispatch("SET_DIALOG_OPEN", { - isUpdateNotificationDialogOpen: val, - }), +// エディタのアップデート確認ダイアログ +const canOpenNotificationDialog = computed(() => { + return ( + !store.state.isAcceptTermsDialogOpen && + !store.state.isCharacterOrderDialogOpen && + !store.state.isDefaultStyleSelectDialogOpen && + !store.state.isAcceptRetrieveTelemetryDialogOpen && + isCompletedInitialStartup.value + ); }); // ドラッグ&ドロップ diff --git a/tests/unit/components/UpdateNotificationDialog.spec.ts b/tests/unit/components/UpdateNotificationDialog.spec.ts new file mode 100644 index 0000000000..29b643d954 --- /dev/null +++ b/tests/unit/components/UpdateNotificationDialog.spec.ts @@ -0,0 +1,67 @@ +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"; + +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(/スキップ/)); + if (skipButton == undefined) throw new Error("skipButton is undefined"); + + const exitButton = buttons.find((button) => button.text().match(/閉じる/)); + if (exitButton == undefined) throw new Error("exitButton is undefined"); + + 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(); + }); +}); diff --git a/tests/unit/composable/useFetchNewUpdateInfos.spec.ts b/tests/unit/composable/useFetchNewUpdateInfos.spec.ts index 9c798dc487..238966f10b 100644 --- a/tests/unit/composable/useFetchNewUpdateInfos.spec.ts +++ b/tests/unit/composable/useFetchNewUpdateInfos.spec.ts @@ -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するモックを作成する @@ -31,7 +31,7 @@ it("新バージョンがある場合、latestVersionに最新バージョンが const result = useFetchNewUpdateInfos( async () => currentVersion, - "Dummy Url" + UrlString("Dummy Url") ); await waitFinished(result); @@ -48,7 +48,7 @@ it("新バージョンがない場合は状態が変わるだけ", async () => { const result = useFetchNewUpdateInfos( async () => currentVersion, - "Dummy Url" + UrlString("Dummy Url") ); await waitFinished(result); From 63caae91b20d2520e72183d41bfd8b3d1d8c8354 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 17 Jan 2024 07:54:04 +0900 Subject: [PATCH 3/5] =?UTF-8?q?=E3=83=90=E3=82=B0=E3=83=95=E3=82=A3?= =?UTF-8?q?=E3=83=83=E3=82=AF=E3=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/EditorHome.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/views/EditorHome.vue b/src/views/EditorHome.vue index 1395dfae6c..bf5873808f 100644 --- a/src/views/EditorHome.vue +++ b/src/views/EditorHome.vue @@ -856,7 +856,12 @@ const isAcceptRetrieveTelemetryDialogOpenComputed = computed({ // アップデート通知 const isUpdateNotificationDialogOpenComputed = computed({ - get: () => store.state.isUpdateNotificationDialogOpen, + get: () => + !store.state.isAcceptTermsDialogOpen && + !store.state.isCharacterOrderDialogOpen && + !store.state.isDefaultStyleSelectDialogOpen && + !store.state.isAcceptRetrieveTelemetryDialogOpen && + store.state.isUpdateNotificationDialogOpen, set: (val) => store.dispatch("SET_DIALOG_OPEN", { isUpdateNotificationDialogOpen: val, From 5013db130ce7fe0a683d0ea82fea169cfba25758 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 17 Jan 2024 09:07:22 +0900 Subject: [PATCH 4/5] assertNonNullable --- tests/unit/components/UpdateNotificationDialog.spec.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/components/UpdateNotificationDialog.spec.ts b/tests/unit/components/UpdateNotificationDialog.spec.ts index 29b643d954..eb04e80c7f 100644 --- a/tests/unit/components/UpdateNotificationDialog.spec.ts +++ b/tests/unit/components/UpdateNotificationDialog.spec.ts @@ -8,6 +8,7 @@ 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; @@ -34,10 +35,10 @@ const mountUpdateNotificationDialogPresentation = async (context?: { const buttons = domWrapper.findAll("button"); const skipButton = buttons.find((button) => button.text().match(/スキップ/)); - if (skipButton == undefined) throw new Error("skipButton is undefined"); + assertNonNullable(skipButton); const exitButton = buttons.find((button) => button.text().match(/閉じる/)); - if (exitButton == undefined) throw new Error("exitButton is undefined"); + assertNonNullable(exitButton); return { wrapper, skipButton, exitButton }; }; From 6454dc549b32b6a0a9390a4fc2c07ebd8a5fae76 Mon Sep 17 00:00:00 2001 From: Hiroshiba Date: Wed, 17 Jan 2024 09:10:27 +0900 Subject: [PATCH 5/5] UrlString --- tests/unit/composable/useFetchNewUpdateInfos.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/composable/useFetchNewUpdateInfos.spec.ts b/tests/unit/composable/useFetchNewUpdateInfos.spec.ts index 238966f10b..1d1bd24496 100644 --- a/tests/unit/composable/useFetchNewUpdateInfos.spec.ts +++ b/tests/unit/composable/useFetchNewUpdateInfos.spec.ts @@ -31,7 +31,7 @@ it("新バージョンがある場合、latestVersionに最新バージョンが const result = useFetchNewUpdateInfos( async () => currentVersion, - UrlString("Dummy Url") + UrlString("http://example.com") ); await waitFinished(result); @@ -48,7 +48,7 @@ it("新バージョンがない場合は状態が変わるだけ", async () => { const result = useFetchNewUpdateInfos( async () => currentVersion, - UrlString("Dummy Url") + UrlString("http://example.com") ); await waitFinished(result);