添加 Playwright E2E 测试及构建修复
- 修复 vite.config.mjs base 路径为 "./" 解决 file:// 协议加载失败 - 添加测试专用 mockDialog IPC 绕过原生文件选择对话框 - 重建 better-sqlite3 以适配 Electron 内嵌 Node ABI - 新增 6 个 E2E 测试:模板增删改、批量生成、文件解析 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fcf3d5c7c2
commit
9738d2cd67
@ -55,12 +55,19 @@ ipcMain.handle("template:parse", async (_, filePath) => {
|
|||||||
return callPython({ action: "parse_template", file_path: filePath });
|
return callPython({ action: "parse_template", file_path: filePath });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let _mockDialogPath = null;
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
ipcMain.handle("test:mockDialog", (_, p) => { _mockDialogPath = p; });
|
||||||
|
}
|
||||||
|
|
||||||
ipcMain.handle("file:select", async (_, filters = []) => {
|
ipcMain.handle("file:select", async (_, filters = []) => {
|
||||||
|
if (_mockDialogPath) { const p = _mockDialogPath; _mockDialogPath = null; return p; }
|
||||||
const result = await dialog.showOpenDialog({ filters, properties: ["openFile"] });
|
const result = await dialog.showOpenDialog({ filters, properties: ["openFile"] });
|
||||||
return result.canceled ? null : result.filePaths[0];
|
return result.canceled ? null : result.filePaths[0];
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle("file:selectDir", async () => {
|
ipcMain.handle("file:selectDir", async () => {
|
||||||
|
if (_mockDialogPath) { const p = _mockDialogPath; _mockDialogPath = null; return p; }
|
||||||
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
const result = await dialog.showOpenDialog({ properties: ["openDirectory"] });
|
||||||
return result.canceled ? null : result.filePaths[0];
|
return result.canceled ? null : result.filePaths[0];
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,4 +14,7 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
ipcRenderer.invoke("file:copyToAppData", srcPath, templateId),
|
ipcRenderer.invoke("file:copyToAppData", srcPath, templateId),
|
||||||
|
|
||||||
generate: (req) => ipcRenderer.invoke("generate:run", req),
|
generate: (req) => ipcRenderer.invoke("generate:run", req),
|
||||||
|
|
||||||
|
// 仅测试环境使用:注入下一次对话框返回值
|
||||||
|
mockDialog: (p) => ipcRenderer.invoke("test:mockDialog", p),
|
||||||
});
|
});
|
||||||
|
|||||||
1184
package-lock.json
generated
1184
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -4,20 +4,22 @@
|
|||||||
"main": "electron/main.js",
|
"main": "electron/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"dev": "concurrently \"npm run dev:renderer\" \"wait-on http://localhost:5173 && electron .\"",
|
"dev": "concurrently \"npm run dev:renderer\" \"wait-on http://localhost:5173 && NODE_ENV=development electron .\"",
|
||||||
"dev:renderer": "vite --config renderer/vite.config.mjs",
|
"dev:renderer": "vite --config renderer/vite.config.mjs --port 5173 --strictPort",
|
||||||
"build": "vite build --config renderer/vite.config.mjs && electron-builder",
|
"build": "vite build --config renderer/vite.config.mjs && electron-builder",
|
||||||
"test": "cd /Users/mikivl/workspace/excel-batch-editor && python3 -m pytest tests/python/ -v"
|
"test": "cd /Users/mikivl/workspace/excel-batch-editor && python3 -m pytest tests/python/ -v"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
"electron": "^28.0.0",
|
"electron": "^28.0.0",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
"concurrently": "^8.2.2",
|
"electron-rebuild": "^3.2.9",
|
||||||
"wait-on": "^7.2.0",
|
|
||||||
"vite": "^5.0.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0"
|
"vite": "^5.0.0",
|
||||||
|
"wait-on": "^7.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-sqlite3": "^11.0.0",
|
"better-sqlite3": "^11.0.0",
|
||||||
|
|||||||
10
playwright.config.js
Normal file
10
playwright.config.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const { defineConfig } = require("@playwright/test");
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
timeout: 30000,
|
||||||
|
reporter: "list",
|
||||||
|
use: {
|
||||||
|
headless: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -4,6 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
|
base: "./",
|
||||||
build: {
|
build: {
|
||||||
outDir: "dist",
|
outDir: "dist",
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
|
|||||||
116
tests/e2e/app.spec.js
Normal file
116
tests/e2e/app.spec.js
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
const { test, expect, _electron: electron } = require("@playwright/test");
|
||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
const APP_ROOT = path.join(__dirname, "../..");
|
||||||
|
const FIXTURE_TEMPLATE = path.join(__dirname, "../fixtures/sample_template.xlsx");
|
||||||
|
const OUTPUT_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ebe-test-"));
|
||||||
|
|
||||||
|
let app;
|
||||||
|
let page;
|
||||||
|
|
||||||
|
test.beforeAll(async () => {
|
||||||
|
app = await electron.launch({
|
||||||
|
args: [APP_ROOT],
|
||||||
|
// 用 test 模式:不是 production,mock IPC 注册;不是 development,加载 dist
|
||||||
|
env: { ...process.env, NODE_ENV: "test" },
|
||||||
|
});
|
||||||
|
page = await app.firstWindow();
|
||||||
|
page.on("console", (msg) => {
|
||||||
|
if (msg.type() === "error") console.error("[renderer]", msg.text());
|
||||||
|
});
|
||||||
|
await page.waitForSelector("button:has-text('+ 新建模板')", { timeout: 10000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
fs.rmSync(OUTPUT_DIR, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T1: 显示模板列表页", async () => {
|
||||||
|
await expect(page.locator("button:has-text('+ 新建模板')")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T2: 新建模板", async () => {
|
||||||
|
await page.click("button:has-text('+ 新建模板')");
|
||||||
|
await page.waitForSelector("input[placeholder='如:月度报告模板']", { timeout: 5000 });
|
||||||
|
|
||||||
|
await page.fill("input[placeholder='如:月度报告模板']", "测试模板");
|
||||||
|
await page.fill("input[placeholder='如:财务部']", "测试分组");
|
||||||
|
|
||||||
|
// 注入下次对话框返回值,再点击选择文件
|
||||||
|
await page.evaluate((p) => window.api.mockDialog(p), FIXTURE_TEMPLATE);
|
||||||
|
await page.click("button:has-text('选择文件')");
|
||||||
|
|
||||||
|
// 等待文件路径填入
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => Array.from(document.querySelectorAll("input[readonly]")).some(i => i.value.includes(".xlsx")),
|
||||||
|
{ timeout: 8000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 等待 parse 完成
|
||||||
|
await page.waitForTimeout(2000);
|
||||||
|
|
||||||
|
await page.click("button:has-text('保存模板')");
|
||||||
|
await page.waitForSelector("span:has-text('测试模板')", { timeout: 8000 });
|
||||||
|
await expect(page.locator("span:has-text('测试模板')")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T3: 模板列表显示详情", async () => {
|
||||||
|
await page.click("span:has-text('测试模板')");
|
||||||
|
await expect(page.locator("h1")).toContainText("测试模板", { timeout: 3000 });
|
||||||
|
await expect(page.locator("button:has-text('批量生成')")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T4: 批量生成 - 手动输入", async () => {
|
||||||
|
await page.click("button:has-text('批量生成')");
|
||||||
|
await page.waitForSelector("h1:has-text('批量生成')", { timeout: 5000 });
|
||||||
|
|
||||||
|
await page.click("label:has-text('手动填写')");
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
const inputs = page.locator("table tbody tr:first-child input");
|
||||||
|
const count = await inputs.count();
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
await inputs.nth(i).fill(`值${i + 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注入输出目录
|
||||||
|
await page.evaluate((dir) => window.api.mockDialog(dir), OUTPUT_DIR);
|
||||||
|
await page.click("button:has-text('选择目录')");
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => Array.from(document.querySelectorAll("input[readonly]")).some(i => i.value.length > 0 && !i.value.includes(".xlsx")),
|
||||||
|
{ timeout: 5000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.click("button:has-text('开始生成')");
|
||||||
|
await expect(page.locator("text=生成结果")).toBeVisible({ timeout: 20000 });
|
||||||
|
|
||||||
|
const files = fs.readdirSync(OUTPUT_DIR);
|
||||||
|
console.log("生成的文件:", files);
|
||||||
|
expect(files.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T5: 编辑模板名称", async () => {
|
||||||
|
await page.click("button:has-text('← 返回模板列表')");
|
||||||
|
await page.waitForSelector("button:has-text('+ 新建模板')", { timeout: 5000 });
|
||||||
|
|
||||||
|
await page.click("span:has-text('测试模板')");
|
||||||
|
await page.waitForSelector("button:has-text('编辑字段映射')", { timeout: 3000 });
|
||||||
|
await page.click("button:has-text('编辑字段映射')");
|
||||||
|
|
||||||
|
await page.waitForSelector("input[placeholder='如:月度报告模板']", { timeout: 5000 });
|
||||||
|
await page.fill("input[placeholder='如:月度报告模板']", "测试模板(已修改)");
|
||||||
|
await page.click("button:has-text('保存模板')");
|
||||||
|
|
||||||
|
await page.waitForSelector("span:has-text('测试模板(已修改)')", { timeout: 5000 });
|
||||||
|
await expect(page.locator("span:has-text('测试模板(已修改)')")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("T6: 删除模板", async () => {
|
||||||
|
page.once("dialog", (d) => d.accept());
|
||||||
|
await page.locator("div.flex.items-center.justify-between button:has-text('删除')").first().click();
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
await expect(page.locator("span:has-text('测试模板(已修改)')")).not.toBeVisible({ timeout: 3000 });
|
||||||
|
});
|
||||||
BIN
tests/fixtures/sample_template.xlsx
vendored
BIN
tests/fixtures/sample_template.xlsx
vendored
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user