diff --git a/.gitignore b/.gitignore index 37f5e64..a3d18a3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ node_modules/ # build output dist/ coverage/ +test-results/ +playwright-report/ +blob-report/ # local QA captures and generated media qa/ diff --git a/e2e/piper-languages.spec.js b/e2e/piper-languages.spec.js new file mode 100644 index 0000000..c927311 --- /dev/null +++ b/e2e/piper-languages.spec.js @@ -0,0 +1,40 @@ +import { expect, test } from "@playwright/test"; +import { readFile } from "node:fs/promises"; + +const VOICE_CASES = [ + { language: "中文", voice: "小雅", text: "你好,这是一次完整的中文语音合成测试。" }, + { language: "English", voice: "Heart", text: "Hello. This is a complete English voice synthesis test." }, + { language: "Deutsch", voice: "Thorsten", text: "Hallo! Dies ist ein vollständiger Sprachtest." }, + { language: "Español", voice: "DaveFX", text: "Hola. Esta es una prueba completa de síntesis de voz." }, + { language: "Français", voice: "Siwis", text: "Bonjour. Ceci est un test complet de synthèse vocale." }, + { language: "Italiano", voice: "Riccardo", text: "Ciao. Questo è un test completo di sintesi vocale." }, + { language: "Português", voice: "Faber", text: "Olá. Este é um teste completo de síntese de voz." }, +]; + +test.beforeEach(async ({ page }) => { + await page.addInitScript(() => localStorage.setItem("ai-voiceover-ui-language", "zh")); +}); + +for (const voiceCase of VOICE_CASES) { + test(`${voiceCase.language} ${voiceCase.voice} generates a valid WAV through the editor`, async ({ page }) => { + await page.goto("/"); + await page.locator("#script-input").fill(voiceCase.text); + await page.locator(".voice-filter").click(); + await page.locator(".menu-list button").getByText(voiceCase.language, { exact: true }).click(); + + await expect(page.locator(".voice-card.is-selected strong")).toHaveText(voiceCase.voice); + await page.locator(".generate-button").click(); + const clipDownload = page.getByRole("button", { name: "下载片段", exact: true }); + await expect(clipDownload).toBeEnabled({ timeout: 4 * 60 * 1000 }); + + const downloadPromise = page.waitForEvent("download"); + await clipDownload.click(); + const download = await downloadPromise; + const path = await download.path(); + const wav = await readFile(path); + + expect(wav.byteLength).toBeGreaterThan(44); + expect(wav.subarray(0, 4).toString("ascii")).toBe("RIFF"); + expect(wav.subarray(8, 12).toString("ascii")).toBe("WAVE"); + }); +} diff --git a/package-lock.json b/package-lock.json index 5fea85c..9957a68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.56.1", "@vitejs/plugin-react": "^5.0.4", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.7.0", @@ -1629,6 +1630,22 @@ "react-dom": ">= 16.8" } }, + "node_modules/@playwright/test": { + "version": "1.56.1", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.56.1.tgz", + "integrity": "sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4038,6 +4055,53 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "license": "MIT" }, + "node_modules/playwright": { + "version": "1.56.1", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.56.1.tgz", + "integrity": "sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.56.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.56.1", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.56.1.tgz", + "integrity": "sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.16", "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.16.tgz", diff --git a/package.json b/package.json index 6b2caf4..3f1c0b0 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "test": "vitest run", + "test:e2e:voices": "playwright test e2e/piper-languages.spec.js", "test:watch": "vitest", "test:coverage": "vitest run --coverage", "typecheck": "tsc --noEmit", @@ -42,6 +43,7 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", + "@playwright/test": "^1.56.1", "@vitejs/plugin-react": "^5.0.4", "@vitest/coverage-v8": "^4.1.10", "eslint": "^10.7.0", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..9eaec2c --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + fullyParallel: false, + workers: 1, + timeout: 5 * 60 * 1000, + use: { + baseURL: "http://127.0.0.1:5173", + headless: true, + }, + webServer: { + command: "npm run dev", + url: "http://127.0.0.1:5173", + reuseExistingServer: true, + timeout: 30 * 1000, + }, +}); diff --git a/src/components/panels.jsx b/src/components/panels.jsx index 989b162..9d38fc8 100644 --- a/src/components/panels.jsx +++ b/src/components/panels.jsx @@ -1023,6 +1023,8 @@ export function VoiceSynthesisPanel({ setFavoriteVoiceIds, t, }) { + const voiceLanguages = useMemo(() => [...new Set(VOICES.map((voice) => voice.language))], []); + return ( <>