docs: 交付文档、维护指引与端到端回归
补齐 README、开发计划、维护交接与 E2E 用例,执行最终验收。
This commit is contained in:
1 parent
18732801b5
commit
6cd904d460
216 files changed
+37038
No files matched your search
@@ -0,0 +1,115 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { defineConfig, devices, type PlaywrightTestConfig } from '@playwright/test'
|
||||
import { env } from './src/env'
|
||||
import { storageStatePath } from './src/org-state'
|
||||
|
||||
const isCI = !!process.env.CI
|
||||
// Reuse a stack already started with `./scripts/stack.sh up` — locally for fast
|
||||
// reruns, and on CI, which starts it first so every service logs to e2e/.stack/.
|
||||
const reuse = true
|
||||
const enterprise = env.edition === 'enterprise'
|
||||
if (enterprise && !existsSync(env.enterpriseSpecDir)) {
|
||||
throw new Error(`E2E_EDITION=enterprise but ${env.enterpriseSpecDir} is missing — check out the enterprise_frontend submodule`)
|
||||
}
|
||||
|
||||
const signedInAsAdmin = { ...devices['Desktop Chrome'], storageState: storageStatePath('admin') }
|
||||
const ORG_WIDE = /\.org\.spec\.ts$/
|
||||
// Tests that need a capability the stack they run on leaves off: Redis, or a
|
||||
// single backend worker (socket.io polling isn't sticky across several).
|
||||
const unsupported = [env.redis ? null : /@redis/, env.multiWorker ? /@one-worker/ : null].filter((tag): tag is RegExp => tag !== null)
|
||||
// Screenshot baselines are Linux-only (generated in CI); they run in their own project.
|
||||
const VISUAL = /visual\/.*\.spec\.ts$/
|
||||
const NOT_DEFAULT = [ORG_WIDE, VISUAL]
|
||||
|
||||
/**
|
||||
* Nightly only (E2E_MORE_BROWSERS=1): the customer-facing widget on the other
|
||||
* engines and on phones. The dashboard stays Chromium-only.
|
||||
*/
|
||||
const otherBrowsers: PlaywrightTestConfig['projects'] = process.env.E2E_MORE_BROWSERS
|
||||
? [
|
||||
['widget-firefox', devices['Desktop Firefox']],
|
||||
['widget-webkit', devices['Desktop Safari']],
|
||||
['widget-android', devices['Pixel 7']],
|
||||
['widget-iphone', devices['iPhone 14']],
|
||||
].map(([name, device]) => ({
|
||||
name: name as string,
|
||||
dependencies: ['setup'],
|
||||
testIgnore: NOT_DEFAULT,
|
||||
grep: /@widget/,
|
||||
grepInvert: unsupported,
|
||||
use: { ...(device as object), storageState: storageStatePath('admin') },
|
||||
}))
|
||||
: []
|
||||
|
||||
// Both editions share the project names `setup`, `chromium` and `org`, so the same
|
||||
// commands run either way. Each edition brings its own setup (OSS: first-run form;
|
||||
// enterprise: signup), and both write the same org contract, so the OSS specs in
|
||||
// `chromium`/`org` run unchanged against the enterprise stack.
|
||||
const projects: PlaywrightTestConfig['projects'] = [
|
||||
enterprise
|
||||
? { name: 'setup', testDir: env.enterpriseSpecDir, testMatch: /.*\.setup\.ts/ }
|
||||
: { name: 'setup', testMatch: /.*\.setup\.ts/ },
|
||||
{ name: 'chromium', dependencies: ['setup'], testIgnore: NOT_DEFAULT, grepInvert: unsupported, use: signedInAsAdmin },
|
||||
// Specs that change org-wide settings (AI config, business hours, …). Run them in a
|
||||
// separate invocation with --workers=1 (see package.json "test") so they never race
|
||||
// the parallel suite, and are not skipped when an unrelated parallel test fails.
|
||||
{ name: 'org', dependencies: ['setup'], testMatch: ORG_WIDE, use: signedInAsAdmin },
|
||||
...(enterprise
|
||||
? [
|
||||
{ name: 'enterprise', testDir: env.enterpriseSpecDir, dependencies: ['setup'], testIgnore: ORG_WIDE, use: signedInAsAdmin },
|
||||
{ name: 'enterprise-org', testDir: env.enterpriseSpecDir, dependencies: ['setup'], testMatch: ORG_WIDE, use: signedInAsAdmin },
|
||||
]
|
||||
: []),
|
||||
// Widget screenshots, light and dark. Baselines live next to the specs, Linux only.
|
||||
{ name: 'visual', dependencies: ['setup'], testMatch: VISUAL, use: signedInAsAdmin },
|
||||
...otherBrowsers,
|
||||
]
|
||||
|
||||
const service = (name: string, url: string, timeout: number) => ({
|
||||
name,
|
||||
command: `./scripts/stack.sh ${name}`,
|
||||
url,
|
||||
reuseExistingServer: reuse,
|
||||
timeout,
|
||||
})
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './specs',
|
||||
outputDir: './test-results',
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 2 : undefined,
|
||||
timeout: 60_000,
|
||||
expect: { timeout: 10_000 },
|
||||
reporter: isCI
|
||||
? [['github'], ['list'], ['html', { open: 'never' }]]
|
||||
: [['list'], ['html', { open: 'never' }]],
|
||||
|
||||
use: {
|
||||
baseURL: env.frontendUrl,
|
||||
// Every failure, including the first attempt, keeps its trace.
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
// The dashboard is a PWA; a service worker caching across tests only adds flake.
|
||||
serviceWorkers: 'block',
|
||||
// The widget launcher floats forever; with reduced motion (which the loader honours)
|
||||
// it holds still, so clicks don't wait on an element that is never "stable".
|
||||
contextOptions: { reducedMotion: 'reduce' },
|
||||
// CI browsers run in UTC; pin it so a local run behaves the same.
|
||||
timezoneId: 'UTC',
|
||||
},
|
||||
|
||||
projects,
|
||||
|
||||
webServer: [
|
||||
service('fake-llm', `${env.fakeLlmUrl}/health`, 30_000),
|
||||
service('fake-externals', `${env.fakeExternalsUrl}/health`, 30_000),
|
||||
service('fake-mcp', `${env.fakeMcpUrl}/health`, 30_000),
|
||||
service('customer-site', `${env.customerSiteUrl}/health`, 30_000),
|
||||
service('mail', `${env.mailUrl}/livez`, 30_000),
|
||||
service('backend', `${env.backendUrl}/health`, 180_000),
|
||||
service('frontend', `${env.frontendUrl}/config.js`, 300_000),
|
||||
],
|
||||
})
|
||||
Reference in new issue
Block a user