Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: browser:e2eテストをモック用エンジンを使うように変更 #2442

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
8caa1cb
refactor: モック用エンジンを使ったe2eテストに差し替え
Hiroshiba Dec 27, 2024
571c7d9
[update snapshots]
Hiroshiba Dec 27, 2024
718e0ee
辞書mock追加
Hiroshiba Dec 27, 2024
aa88392
fix: 不要なインポートを削除し、コードを整理
Hiroshiba Dec 27, 2024
3f24ec2
いろいろ更新
Hiroshiba Dec 27, 2024
4b0b0d1
refactor: proxyStoreCreatorをプライベート関数に変更し、e2eテストにhoverアクションを追加
Hiroshiba Dec 27, 2024
d0927f7
refactor: hoverアクションのコードを整理し、可読性を向上
Hiroshiba Dec 28, 2024
dfe5bfb
Merge remote-tracking branch 'upstream/main' into モック用エンジンを使ったe2eテストに…
Hiroshiba Dec 28, 2024
dcb9b8b
モックのURLをmock://mockにする
Hiroshiba Dec 28, 2024
f955856
.env周り整理
Hiroshiba Dec 28, 2024
1e9f080
.env周り調整
Hiroshiba Dec 28, 2024
147297e
.がなかった
Hiroshiba Dec 28, 2024
39e7681
.envファイルの内容をログに出力するように変更
Hiroshiba Dec 28, 2024
73a4f5a
Merge branch 'main' into モック用エンジンを使ったe2eテストに差し替え
Hiroshiba Jan 3, 2025
00dd45a
Merge branch 'main' into モック用エンジンを使ったe2eテストに差し替え
Hiroshiba Jan 3, 2025
e4e7720
合わせる
Hiroshiba Jan 3, 2025
08ff0ac
不要だった変更を戻す
Hiroshiba Jan 3, 2025
75d7578
コメント整理
Hiroshiba Jan 3, 2025
5ee1005
Merge remote-tracking branch 'upstream/main' into モック用エンジンを使ったe2eテストに…
Hiroshiba Jan 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .env.test
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# テスト用の.envファイル。モックを使う。

VITE_APP_NAME=voicevox
VITE_DEFAULT_ENGINE_INFOS=`[
{
"uuid": "074fc39e-678b-4c13-8916-ffca8d505d1d",
"name": "VOICEVOX Engine",
"executionEnabled": true,
"executionFilePath": "../voicevox_engine/run.exe",
"name": "Mock Engine",
"uuid": "00000000-0000-0000-0000-000000000000",
"executionEnabled": false,
"executionFilePath": "dummy/path",
"executionArgs": [],
"host": "http://127.0.0.1:50021"
"host": "mock://mock"
}
]`
VITE_OFFICIAL_WEBSITE_URL=https://voicevox.hiroshiba.jp/
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,6 @@ jobs:
dest: ${{ github.workspace }}/voicevox_engine
target: ${{ matrix.voicevox_engine_asset_name }}

- name: Setup
run: |
# .env
cp tests/env/.env.test-e2e .env
sed -i -e 's|"path/to/engine"|"${{ steps.download-engine.outputs.run_path }}"|' .env
# GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする
PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})")
sed -i -e 's|random_port|'$PORT'|' .env
cat .env # ログ用

- name: Run npm run test:browser-e2e
run: |
if [ -n "${{ runner.debug }}" ]; then
Expand All @@ -123,6 +113,14 @@ jobs:

- name: Run npm run test:electron-e2e
run: |
# .env
cp tests/env/.env.test-electron .env
sed -i -e 's|"path/to/engine"|"${{ steps.download-engine.outputs.run_path }}"|' .env
# GitHub Actions 環境だとたまに50021が封じられていることがあるので、ランダムなポートを使うようにする
PORT=$(node -r net -e "server=net.createServer();server.listen(0,()=>{console.log(server.address().port);server.close()})")
sed -i -e 's|random_port|'$PORT'|' .env
cat .env # ログ用

if [ -n "${{ runner.debug }}" ]; then
export DEBUG="pw:browser*"
fi
Expand All @@ -132,6 +130,8 @@ jobs:
npm run test:electron-e2e
fi

rm .env

- name: Run npm run test:storybook-vrt
run: |
if [ -n "${{ runner.debug }}" ]; then
Expand Down
35 changes: 11 additions & 24 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
import type { PlaywrightTestConfig, Project } from "@playwright/test";
import { z } from "zod";
/**
* e2eテストと .env の設計:
* - デフォルトで .env.test を読み込む。
* モックエンジンが使われる。
* - Electronテストはテストファイル内で様々な .env を読み込む。
* テスト条件によって用意したい環境が異なるため。
*/

import type { PlaywrightTestConfig, Project } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config({ override: true });

dotenv.config({ path: ".env.test", override: true });

let project: Project;
let webServers: PlaywrightTestConfig["webServer"];
const isElectron = process.env.VITE_TARGET === "electron";
const isBrowser = process.env.VITE_TARGET === "browser";
const isStorybook = process.env.TARGET === "storybook";

// エンジンの起動が必要
const defaultEngineInfosEnv = process.env.VITE_DEFAULT_ENGINE_INFOS ?? "[]";
const envSchema = z // FIXME: electron起動時のものと共通化したい
.object({
host: z.string(),
executionFilePath: z.string(),
executionArgs: z.array(z.string()),
executionEnabled: z.boolean(),
})
.passthrough()
.array();
const engineInfos = envSchema.parse(JSON.parse(defaultEngineInfosEnv));

const engineServers = engineInfos
.filter((info) => info.executionEnabled)
.map((info) => ({
command: `${info.executionFilePath} ${info.executionArgs.join(" ")}`,
url: `${info.host}/version`,
reuseExistingServer: !process.env.CI,
}));
const viteServer = {
command: "vite --mode test --port 7357",
port: 7357,
Expand All @@ -46,7 +33,7 @@ if (isElectron) {
webServers = [viteServer];
} else if (isBrowser) {
project = { name: "browser", testDir: "./tests/e2e/browser" };
webServers = [viteServer, ...engineServers];
webServers = [viteServer];
} else if (isStorybook) {
project = { name: "storybook", testDir: "./tests/e2e/storybook" };
webServers = [storybookServer];
Expand Down
44 changes: 43 additions & 1 deletion src/infrastructures/EngineConnector.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { createEngineUrl, EngineUrlParams } from "@/domain/url";
import { createOpenAPIEngineMock } from "@/mock/engineMock";
import { Configuration, DefaultApi, DefaultApiInterface } from "@/openapi";

export interface IEngineConnectorFactory {
Expand All @@ -6,6 +8,7 @@ export interface IEngineConnectorFactory {
instance: (host: string) => DefaultApiInterface;
}

// 通常エンジン
const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => {
const instanceMapper: Record<string, DefaultApiInterface> = {};
return {
Expand All @@ -21,6 +24,45 @@ const OpenAPIEngineConnectorFactoryImpl = (): IEngineConnectorFactory => {
},
};
};

export const OpenAPIEngineConnectorFactory =
OpenAPIEngineConnectorFactoryImpl();

// モック用エンジン
const OpenAPIMockEngineConnectorFactoryImpl = (): IEngineConnectorFactory => {
let mockInstance: DefaultApiInterface | undefined;
return {
instance: () => {
if (!mockInstance) {
mockInstance = createOpenAPIEngineMock();
}
return mockInstance;
},
};
};
export const OpenAPIMockEngineConnectorFactory =
OpenAPIMockEngineConnectorFactoryImpl();

// 通常エンジンとモック用エンジンの両対応
// モック用エンジンのURLのときはモックを、そうじゃないときは通常エンジンを返す。
const OpenAPIEngineAndMockConnectorFactoryImpl =
(): IEngineConnectorFactory => {
// モック用エンジンのURLは `mock://mock` とする
const mockUrlParams: EngineUrlParams = {
protocol: "mock:",
hostname: "mock",
port: "",
pathname: "",
};

return {
instance: (host: string) => {
if (host == createEngineUrl(mockUrlParams)) {
return OpenAPIMockEngineConnectorFactory.instance(host);
} else {
return OpenAPIEngineConnectorFactory.instance(host);
}
},
};
};
export const OpenAPIEngineAndMockConnectorFactory =
OpenAPIEngineAndMockConnectorFactoryImpl();
Hiroshiba marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 10 additions & 1 deletion src/store/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ProxyStoreState, ProxyStoreTypes, EditorAudioQuery } from "./type";
import { createPartialStore } from "./vuex";
import { createEngineUrl } from "@/domain/url";
import { isElectron, isProduction } from "@/helpers/platform";
import {
IEngineConnectorFactory,
OpenAPIEngineAndMockConnectorFactory,
OpenAPIEngineConnectorFactory,
} from "@/infrastructures/EngineConnector";
import { AudioQuery } from "@/openapi";
Expand Down Expand Up @@ -69,4 +71,11 @@ export const convertAudioQueryFromEngineToEditor = (
};
};

export const proxyStore = proxyStoreCreator(OpenAPIEngineConnectorFactory);
// 製品PC版は通常エンジンのみを、それ以外はモックエンジンも使えるようする
const getConnectorFactory = () => {
if (isElectron && isProduction) {
return OpenAPIEngineConnectorFactory;
}
return OpenAPIEngineAndMockConnectorFactory;
};
export const proxyStore = proxyStoreCreator(getConnectorFactory());
1 change: 0 additions & 1 deletion tests/e2e/browser/アクセント.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ test("アクセントの読み部分をクリックすると読みを変更で
await expect(page.locator(".text-cell").first()).toBeVisible();
await page.locator(".text-cell").first().click();
const input = page.getByLabel("1番目のアクセント区間の読み");
await input.evaluate((node) => console.log(node.outerHTML));
expect(await input.inputValue()).toBe("テストデス");
await input.fill("テストテスト");
await input.press("Enter");
Expand Down
124 changes: 0 additions & 124 deletions tests/e2e/browser/スクリーンショット.spec.ts
Original file line number Diff line number Diff line change
@@ -1,130 +1,6 @@
import path from "path";
import fs from "fs/promises";
import { test, expect } from "@playwright/test";
import { gotoHome, navigateToMain } from "../navigators";
import {
Speaker,
SpeakerFromJSON,
SpeakerInfo,
SpeakerInfoFromJSON,
SpeakerInfoToJSON,
SpeakerToJSON,
} from "@/openapi";

let speakerImages:
| {
portrait: string;
icon: string;
}[]
| undefined = undefined;

/**
* 差し替え用の立ち絵・アイコンを取得する。
* TODO: エンジンモックを使ってこのコードを削除する。
*/
async function getSpeakerImages(): Promise<
{
portrait: string;
icon: string;
}[]
> {
if (!speakerImages) {
const assetsPath = path.resolve(
__dirname,
"../../../src/mock/engineMock/assets",
);
const images = await fs.readdir(assetsPath);
const icons = images.filter((image) => image.startsWith("icon"));
icons.sort(
(a, b) =>
parseInt(a.split(".")[0].split("_")[1]) -
parseInt(b.split(".")[0].split("_")[1]),
);
speakerImages = await Promise.all(
icons.map(async (iconPath) => {
const portraitPath = iconPath.replace("icon_", "portrait_");
const portrait = await fs.readFile(
path.join(assetsPath, portraitPath),
"base64",
);
const icon = await fs.readFile(
path.join(assetsPath, iconPath),
"base64",
);

return { portrait, icon };
}),
);
}
return speakerImages;
}

test.beforeEach(async ({ page }) => {
let speakers: Speaker[];
const speakerImages = await getSpeakerImages();
// Voicevox Nemo EngineでもVoicevox Engineでも同じ結果が選られるように、
// GET /speakers、GET /speaker_infoの話者名、スタイル名、画像を差し替える。
await page.route(/\/speakers$/, async (route) => {
const response = await route.fetch();
const json: Speaker[] = await response
.json()
.then((json: unknown[]) => json.map(SpeakerFromJSON));
let i = 0;
for (const speaker of json) {
i++;
speaker.name = `Speaker ${i}`;
let j = 0;
for (const style of speaker.styles) {
j++;
style.name = `Style ${i}-${j}`;
}
}
speakers = json;
await route.fulfill({
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
body: JSON.stringify(json.map(SpeakerToJSON)),
});
});
await page.route(/\/speaker_info\?/, async (route) => {
if (!speakers) {
// Unreachableのはず
throw new Error("speakers is not initialized");
}
const url = new URL(route.request().url());
const speakerUuid = url.searchParams.get("speaker_uuid");
if (!speakerUuid) {
throw new Error("speaker_uuid is not set");
}
const response = await route.fetch();
const json: SpeakerInfo = await response.json().then(SpeakerInfoFromJSON);
const speakerIndex = speakers.findIndex(
(speaker) => speaker.speakerUuid === speakerUuid,
);
if (speakerIndex === -1) {
throw new Error(`speaker_uuid=${speakerUuid} is not found`);
}
const image = speakerImages[speakerIndex % speakerImages.length];
json.portrait = image.portrait;
for (const style of json.styleInfos) {
style.icon = image.icon;
if ("portrait" in style) {
delete style.portrait;
}
}
await route.fulfill({
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json",
},
body: JSON.stringify(SpeakerInfoToJSON(json)),
});
});
});
test.beforeEach(gotoHome);

test("メイン画面の表示", async ({ page }) => {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading