🔨 chore: add e2e workflow (#9677)

* add e2e test

* Potential fix for code scanning alert no. 137: Workflow does not contain permissions

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* remove

* update

* fix tests

* add e2e

* update

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Arvin Xu
2025-10-14 07:32:52 +02:00
committed by GitHub
parent 543db87745
commit 8693d95e0d
6 changed files with 166 additions and 1 deletions
+52
View File
@@ -0,0 +1,52 @@
name: E2E CI
permissions:
contents: read
on:
pull_request:
push:
concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e:
name: Test Web App
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies (bun)
run: bun install
- name: Install Playwright browsers (with system deps)
run: bunx playwright install --with-deps chromium
- name: Run E2E tests
env:
PORT: 3010
run: bun run e2e
- name: Upload Playwright HTML report (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report
if-no-files-found: ignore
- name: Upload Playwright traces (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results
if-no-files-found: ignore
+73
View File
@@ -0,0 +1,73 @@
import { expect, test } from '@playwright/test';
// 覆盖核心可访问路径(含重定向来源)
const baseRoutes: string[] = [
'/',
'/chat',
'/discover',
'/image',
'/files',
'/repos', // next.config.ts -> /files
'/changelog',
];
// settings 路由改为通过 query 参数控制 active tab
// 参考 SettingsTabs: about, agent, common, hotkey, llm, provider, proxy, storage, system-agent, tts
const settingsTabs = [
'common',
'llm',
'provider',
'about',
'hotkey',
'proxy',
'storage',
'tts',
'system-agent',
'agent',
];
const routes: string[] = [...baseRoutes, ...settingsTabs.map((key) => `/settings?active=${key}`)];
// CI 环境下跳过容易不稳定或受特性开关影响的路由
const ciSkipPaths = new Set<string>([
'/image',
'/changelog',
'/settings?active=common',
'/settings?active=llm',
]);
// @ts-ignore
async function assertNoPageErrors(page: Parameters<typeof test>[0]['page']) {
const pageErrors: Error[] = [];
const consoleErrors: string[] = [];
page.on('pageerror', (err: Error) => pageErrors.push(err));
page.on('console', (msg: any) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
// 仅校验页面级错误,忽略控制台 error 以提升稳定性
expect
.soft(pageErrors, `page errors: ${pageErrors.map((e) => e.message).join('\n')}`)
.toHaveLength(0);
}
test.describe('Smoke: core routes', () => {
for (const path of routes) {
test(`should open ${path} without error`, async ({ page }) => {
if (process.env.CI && ciSkipPaths.has(path)) test.skip(true, 'skip flaky route on CI');
const response = await page.goto(path, { waitUntil: 'commit' });
// 2xx 或 3xx 视为可接受(允许中间件/重定向)
const status = response?.status() ?? 0;
expect(status, `unexpected status for ${path}: ${status}`).toBeLessThan(400);
// 一般错误标题防御
await expect(page).not.toHaveTitle(/not found|error/i);
// body 可见
await expect(page.locator('body')).toBeVisible();
await assertNoPageErrors(page);
});
}
});
+4
View File
@@ -53,6 +53,9 @@
"dev:desktop": "next dev --turbopack -p 3015",
"docs:i18n": "lobe-i18n md && npm run lint:md && npm run lint:mdx && prettier -c --write locales/**/*",
"docs:seo": "lobe-seo && npm run lint:mdx",
"e2e": "playwright test",
"e2e:install": "playwright install",
"e2e:ui": "playwright test --ui",
"i18n": "npm run workflow:i18n && lobe-i18n && prettier -c --write \"locales/**\"",
"lint": "npm run lint:ts && npm run lint:style && npm run type-check && npm run lint:circular",
"lint:circular": "npm run lint:circular:main && npm run lint:circular:packages",
@@ -299,6 +302,7 @@
"@next/bundle-analyzer": "^15.5.4",
"@next/eslint-plugin-next": "^15.5.4",
"@peculiar/webcrypto": "^1.5.0",
"@playwright/test": "^1.49.1",
"@prettier/sync": "^0.6.1",
"@semantic-release/exec": "^6.0.3",
"@testing-library/jest-dom": "^6.9.1",
+35
View File
@@ -0,0 +1,35 @@
import { defineConfig, devices } from '@playwright/test';
const PORT = process.env.PORT ? Number(process.env.PORT) : 3010;
export default defineConfig({
expect: { timeout: 10_000 },
fullyParallel: true,
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
reporter: 'list',
retries: 0,
testDir: './e2e',
timeout: 60_000,
use: {
baseURL: `http://localhost:${PORT}`,
trace: 'on-first-retry',
},
webServer: {
command: 'npm run dev',
env: {
ENABLE_AUTH_PROTECTION: '0',
ENABLE_OIDC: '0',
NEXT_PUBLIC_ENABLE_CLERK_AUTH: '0',
NEXT_PUBLIC_ENABLE_NEXT_AUTH: '0',
NODE_OPTIONS: '--max-old-space-size=6144',
},
reuseExistingServer: true,
timeout: 120_000,
url: `http://localhost:${PORT}/chat`,
},
});
+1 -1
View File
@@ -31,6 +31,6 @@
}
]
},
"exclude": ["node_modules", "public/sw.js", "apps/desktop", "tmp", "temp", ".temp"],
"exclude": ["node_modules", "public/sw.js", "apps/desktop", "tmp", "temp", ".temp", "e2e"],
"include": ["**/*.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next-env.d.ts"]
}
+1
View File
@@ -46,6 +46,7 @@ export default defineConfig({
'**/build/**',
'**/apps/desktop/**',
'**/packages/**',
'**/e2e/**',
],
globals: true,
server: {