Skip to content

Commit

Permalink
refactor: browser:e2eテストをモック用エンジンを使うように変更 (#2442)
Browse files Browse the repository at this point in the history
  • Loading branch information
Hiroshiba authored Jan 4, 2025
1 parent 4c32048 commit 11c2e16
Show file tree
Hide file tree
Showing 13 changed files with 113 additions and 208 deletions.
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();
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

0 comments on commit 11c2e16

Please sign in to comment.