@h-ai/cli 1.0.0-beta.2 → 1.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +74 -154
- package/README.ja.md +74 -154
- package/README.md +74 -154
- package/dist/{chunk-AKV2CQZV.js → chunk-2ZL5ORQ2.js} +316 -97
- package/dist/chunk-2ZL5ORQ2.js.map +1 -0
- package/dist/{chunk-EF7WOFWZ.js → chunk-L53P43B2.js} +1312 -163
- package/dist/chunk-L53P43B2.js.map +1 -0
- package/dist/cli-config-templates-LWMD2I6V.js +5 -0
- package/dist/{cli-config-templates-HHFHNXBN.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
- package/dist/{cli-skill-templates-PN4FLGBC.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
- package/dist/{cli-skill-templates-PN4FLGBC.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30118 -3253
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -1
- package/package.json +22 -18
- package/templates/README.md +45 -30
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -10
- package/templates/apps/admin/src/routes/+page.svelte.hbs +4 -4
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +17 -7
- package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +26 -6
- package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +2 -2
- package/templates/apps/api/.dockerignore.hbs +10 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +8 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +6 -1
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +13 -5
- package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +13 -5
- package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
- package/templates/apps/api/apps/{{projectName}}-serv/README.md.hbs +6 -2
- package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +24 -2
- package/templates/apps/api/apps/{{projectName}}-serv/package.json.hbs +4 -4
- package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +74 -24
- package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +153 -37
- package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +39 -13
- package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +106 -33
- package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +3 -1
- package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
- package/templates/apps/api/e2e/api-serv.spec.ts.hbs +20 -3
- package/templates/apps/api/e2e/capture-evidence.ts.hbs +4 -1
- package/templates/apps/api/eslint.config.js.hbs +3 -1
- package/templates/apps/api/images/serv.Dockerfile.hbs +15 -10
- package/templates/apps/api/playwright.config.ts.hbs +23 -19
- package/templates/apps/api/pnpm-workspace.yaml.hbs +27 -16
- package/templates/apps/fullstack/.dockerignore.hbs +10 -0
- package/templates/apps/fullstack/.gitignore.hbs +7 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +14 -12
- package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +5 -3
- package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +10 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +10 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +4 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +16 -4
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +10 -8
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +3 -3
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +8 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +0 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +4 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +16 -4
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +2 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +9 -9
- package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +10 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +0 -2
- package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +4 -1
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +19 -5
- package/templates/apps/fullstack/docs/security-configuration.md.hbs +4 -1
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +140 -28
- package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +19 -5
- package/templates/apps/fullstack/eslint.config.js.hbs +3 -1
- package/templates/apps/fullstack/images/serv.Dockerfile.hbs +19 -11
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +17 -8
- package/templates/apps/fullstack/package.json.hbs +2 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +7 -5
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +114 -23
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +57 -11
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +17 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +13 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +17 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -4
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +65 -30
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +1 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +69 -21
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +21 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +63 -11
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +147 -33
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +43 -12
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +146 -30
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +33 -10
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +142 -26
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +6 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +48 -66
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +0 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +15 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +33 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +16 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +104 -13
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +37 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +9 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +25 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +133 -19
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +14 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +66 -34
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +132 -30
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +8 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +23 -5
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +143 -21
- package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -2
- package/templates/apps/fullstack/playwright.config.ts.hbs +25 -3
- package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +30 -14
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +74 -30
- package/templates/apps/h5/src/routes/+page.svelte.hbs +2 -2
- package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +2 -2
- package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +2 -2
- package/templates/apps/mobile-app/capacitor.config.ts.hbs +5 -2
- package/templates/apps/mobile-app/messages/en-US.json +2 -2
- package/templates/apps/mobile-app/messages/ja-JP.json +2 -2
- package/templates/apps/mobile-app/messages/zh-CN.json +2 -2
- package/templates/apps/mobile-app/src/App.svelte.hbs +17 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +20 -6
- package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +8 -1
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +6 -2
- package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -1
- package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +5 -1
- package/templates/apps/website/scripts/static-website.mjs.hbs +19 -6
- package/templates/apps/website/src/routes/+layout.svelte.hbs +56 -29
- package/templates/apps/website/src/routes/+page.svelte.hbs +4 -4
- package/templates/apps/website/src/routes/about/+page.svelte.hbs +2 -2
- package/templates/apps/website/src/routes/contact/+page.svelte.hbs +2 -2
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +20 -5
- package/templates/base/.dockerignore.hbs +10 -0
- package/templates/base/.gitignore.hbs +7 -2
- package/templates/base/e2e/app.spec.ts.hbs +130 -36
- package/templates/base/eslint-rules.js +44 -25
- package/templates/base/eslint.config.js.hbs +3 -1
- package/templates/base/images/web.Dockerfile.hbs +25 -14
- package/templates/base/package.json.hbs +13 -7
- package/templates/base/patches/daisyui-5.7.50.patch +8 -0
- package/templates/base/patches/deepmerge-4.3.1.patch +51 -0
- package/templates/base/playwright.config.ts +3 -1
- package/templates/base/pnpm-workspace.yaml.hbs +33 -16
- package/templates/base/src/app.d.ts.hbs +1 -1
- package/templates/base/src/app.html.hbs +1 -0
- package/templates/base/src/hooks.server.ts.hbs +44 -33
- package/templates/base/src/lib/build-metadata.ts.hbs +0 -1
- package/templates/base/src/lib/server/init.ts.hbs +146 -35
- package/templates/base/src/lib/server/kit-config.ts.hbs +7 -4
- package/templates/base/src/routes/+layout.server.ts.hbs +2 -2
- package/templates/base/svelte.config.js.hbs +10 -100
- package/templates/base/tests/scaffold.test.ts.hbs +12 -3
- package/templates/base/tsconfig.json.hbs +7 -4
- package/templates/base/vite.config.ts.hbs +108 -14
- package/templates/base/vitest.config.ts.hbs +6 -2
- package/templates/features/ai/routes/api/chat/+server.ts +3 -1
- package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +3 -3
- package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +3 -3
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +14 -3
- package/templates/shared/browser-check/startup.js +115 -0
- package/templates/shared/browser-check/update.css +37 -0
- package/templates/shared/browser-check/update.html +19 -0
- package/templates/shared/browser-startup.spec.ts +23 -0
- package/templates/shared/build-metadata.ts +18 -4
- package/templates/shared/e2e-env.ts +22 -6
- package/templates/shared/native-name.d.mts +5 -0
- package/templates/shared/native-name.mjs +94 -0
- package/templates/shared/public-base-path.mjs +17 -4
- package/templates/shared/run-e2e.mjs +46 -12
- package/templates/shared/run-node.mjs +56 -0
- package/templates/shared/run-tauri.mjs +74 -0
- package/templates/shared/vite-runtime-config.ts +9 -3
- package/templates/skills/AGENTS.md +10 -73
- package/templates/skills/bridges/admin/AGENTS.md +9 -65
- package/templates/skills/bridges/api/AGENTS.md +8 -59
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +9 -54
- package/templates/skills/bridges/h5/AGENTS.md +8 -63
- package/templates/skills/bridges/mobile-app/AGENTS.md +8 -62
- package/templates/skills/bridges/website/AGENTS.md +8 -63
- package/templates/skills/hai-ai/SKILL.md +6 -2
- package/templates/skills/hai-ai/acp.md +153 -0
- package/templates/skills/hai-ai/guide.md +27 -12
- package/templates/skills/hai-ai/reference.md +24 -15
- package/templates/skills/hai-ai/resources.md +69 -0
- package/templates/skills/hai-api-client/SKILL.md +1 -1
- package/templates/skills/hai-api-client/reference.md +3 -1
- package/templates/skills/hai-app-create/collaboration.md +7 -0
- package/templates/skills/hai-app-create/development.md +19 -10
- package/templates/skills/hai-app-create/project.md +33 -0
- package/templates/skills/hai-app-create/reference.md +10 -10
- package/templates/skills/hai-app-tests/reference.md +1 -1
- package/templates/skills/hai-app-tests/validation.md +1 -0
- package/templates/skills/hai-build/SKILL.md +19 -9
- package/templates/skills/hai-build/paths.md +4 -2
- package/templates/skills/hai-build/reference.md +26 -8
- package/templates/skills/hai-cache/reference.md +6 -2
- package/templates/skills/hai-capacitor/SKILL.md +1 -0
- package/templates/skills/hai-capacitor/reference.md +24 -0
- package/templates/skills/hai-ci/SKILL.md +3 -0
- package/templates/skills/hai-core/SKILL.md +9 -4
- package/templates/skills/hai-core/reference.md +40 -10
- package/templates/skills/hai-core/request-access.md +2 -2
- package/templates/skills/hai-crypto/SKILL.md +2 -2
- package/templates/skills/hai-crypto/reference.md +7 -3
- package/templates/skills/hai-deploy/SKILL.md +41 -5
- package/templates/skills/hai-deploy/middleware.md +26 -14
- package/templates/skills/hai-framework-sync/SKILL.md +13 -9
- package/templates/skills/hai-framework-sync/sync.md +2 -0
- package/templates/skills/hai-iam/reference.md +2 -2
- package/templates/skills/hai-kit/SKILL.md +1 -1
- package/templates/skills/hai-kit/reference.md +19 -5
- package/templates/skills/hai-payment/reference.md +41 -17
- package/templates/skills/hai-reach/SKILL.md +3 -1
- package/templates/skills/hai-reach/reference.md +29 -9
- package/templates/skills/hai-reldb/SKILL.md +1 -1
- package/templates/skills/hai-reldb/reference.md +9 -2
- package/templates/skills/hai-scheduler/reference.md +1 -1
- package/templates/skills/hai-serv/SKILL.md +2 -1
- package/templates/skills/hai-serv/reference.md +26 -15
- package/templates/skills/hai-storage/reference.md +23 -6
- package/templates/skills/hai-ui/SKILL.md +8 -5
- package/templates/skills/hai-ui/charts.md +51 -0
- package/templates/skills/hai-ui/guide.md +1 -1
- package/templates/skills/hai-ui/reference.md +29 -38
- package/templates/skills/hai-vecdb/reference.md +8 -3
- package/dist/chunk-AKV2CQZV.js.map +0 -1
- package/dist/chunk-EF7WOFWZ.js.map +0 -1
- package/dist/chunk-MUHRBJSW.js +0 -29272
- package/dist/chunk-MUHRBJSW.js.map +0 -1
- package/dist/cli-config-templates-HHFHNXBN.js +0 -5
- package/dist/src-TEBHR5I5.js +0 -5
- package/dist/src-TEBHR5I5.js.map +0 -1
- package/templates/apps/fullstack/playwright.session-close.config.ts.hbs +0 -18
|
@@ -35,7 +35,13 @@ async function capture(page: Page, name: string, fallback: string) {
|
|
|
35
35
|
await mkdir(evidence, { recursive: true })
|
|
36
36
|
await expect(page.getByTestId('guide-loading')).toHaveCount(0)
|
|
37
37
|
await page.evaluate(() => document.fonts.ready)
|
|
38
|
-
await page.screenshot({
|
|
38
|
+
await page.screenshot({
|
|
39
|
+
path: evidence
|
|
40
|
+
? path.join(evidence, name)
|
|
41
|
+
: fallback,
|
|
42
|
+
fullPage: true,
|
|
43
|
+
animations: 'disabled',
|
|
44
|
+
})
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
async function dismissToasts(page: Page) {
|
|
@@ -62,13 +68,18 @@ async function login(page: Page, username: string, password: string, keepGuide =
|
|
|
62
68
|
async function userEditor(page: Page, username: string) {
|
|
63
69
|
await page.getByTestId('users-search').locator('input').fill(username)
|
|
64
70
|
await page.getByTestId('users-page').getByRole('button', { name: /查询|Search/ }).click()
|
|
65
|
-
const row = page.getByRole('row').filter({
|
|
71
|
+
const row = page.getByRole('row').filter({
|
|
72
|
+
has: page.getByText(`@${username}`, { exact: true }),
|
|
73
|
+
})
|
|
66
74
|
await expect(row).toBeVisible()
|
|
67
75
|
await row.getByRole('button', { name: /编辑|Edit/, exact: true }).click()
|
|
68
76
|
return row
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
for (const scenario of [
|
|
79
|
+
for (const scenario of [
|
|
80
|
+
{ offset: -600_000, width: 1440 },
|
|
81
|
+
{ offset: 600_000, width: 390 },
|
|
82
|
+
]) {
|
|
72
83
|
test(`clock offset ${scenario.offset} displays synchronization advice and recovers after correction`, async ({ page }, testInfo) => {
|
|
73
84
|
await page.setViewportSize({ width: scenario.width, height: 900 })
|
|
74
85
|
await page.addInitScript((offset) => {
|
|
@@ -78,6 +89,8 @@ for (const scenario of [{ offset: -600_000, width: 1440 }, { offset: 600_000, wi
|
|
|
78
89
|
await page.goto('./')
|
|
79
90
|
const advice = page.getByRole('alert').filter({ hasText: /同步|synchron|時刻/i })
|
|
80
91
|
await expect(advice).toBeVisible()
|
|
92
|
+
await expect(advice).toContainText(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/)
|
|
93
|
+
await expect(advice).toContainText(/请开启设备的自动校时或手工修改时间|set your device time manually|手動で時刻を設定/i)
|
|
81
94
|
await expect(advice).toContainText(/刷新|refresh|再読み込み/i)
|
|
82
95
|
await capture(page, `clock-skew-${scenario.width}.png`, testInfo.outputPath(`clock-skew-${scenario.width}.png`))
|
|
83
96
|
// 校时后由用户明确刷新;不能自动重放之前失败的业务请求。
|
|
@@ -97,10 +110,11 @@ test('service health endpoint is ready', async ({ request }) => {
|
|
|
97
110
|
})
|
|
98
111
|
|
|
99
112
|
test('service rejects plaintext business requests', async ({ request }) => {
|
|
100
|
-
const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
|
|
113
|
+
const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
|
|
114
|
+
data: { message: 'hello e2e' },
|
|
115
|
+
})
|
|
101
116
|
expect(response.status()).toBe(400)
|
|
102
117
|
})
|
|
103
|
-
|
|
104
118
|
{{#if fullstack.e2eFrontend}}
|
|
105
119
|
test('hash routing and responsive workspace preserve usable content width', async ({ page }, testInfo) => {
|
|
106
120
|
await page.goto('./#/roles?source=direct')
|
|
@@ -287,7 +301,11 @@ const additionalFrontends = [
|
|
|
287
301
|
|
|
288
302
|
for (const frontend of additionalFrontends) {
|
|
289
303
|
test(`${frontend.target} preview supports login, tour, help and profile navigation`, async ({ browser }, testInfo) => {
|
|
290
|
-
const page = await browser.newPage({
|
|
304
|
+
const page = await browser.newPage({
|
|
305
|
+
viewport: frontend.target === 'app'
|
|
306
|
+
? { width: 390, height: 844 }
|
|
307
|
+
: { width: 1440, height: 960 },
|
|
308
|
+
})
|
|
291
309
|
const pageErrors: string[] = []
|
|
292
310
|
page.on('pageerror', error => pageErrors.push(error.message))
|
|
293
311
|
try {
|
|
@@ -327,11 +345,17 @@ test('shared authentication supports public registration with configuration', as
|
|
|
327
345
|
await mkdir(evidence, { recursive: true })
|
|
328
346
|
await page.goto('./')
|
|
329
347
|
await expect(page.getByTestId('login-form')).toBeVisible()
|
|
330
|
-
await page.screenshot({
|
|
348
|
+
await page.screenshot({
|
|
349
|
+
path: path.join(evidence, '{{projectName}}-login.png'),
|
|
350
|
+
fullPage: true,
|
|
351
|
+
})
|
|
331
352
|
await page.getByRole('button', { name: /立即注册|Register now/i }).click()
|
|
332
353
|
const registerForm = page.locator('.register-form')
|
|
333
354
|
await expect(registerForm.locator('#register-username')).toBeVisible()
|
|
334
|
-
await page.screenshot({
|
|
355
|
+
await page.screenshot({
|
|
356
|
+
path: path.join(evidence, '{{projectName}}-register.png'),
|
|
357
|
+
fullPage: true,
|
|
358
|
+
})
|
|
335
359
|
const username = `signup${randomUUID().replaceAll('-', '').slice(0, 12)}`
|
|
336
360
|
await registerForm.locator('#register-username').fill(username)
|
|
337
361
|
await registerForm.locator('#register-email').fill(`${username}@example.com`)
|
|
@@ -345,7 +369,10 @@ test('shared authentication supports public registration with configuration', as
|
|
|
345
369
|
const email = `${username}@example.com`
|
|
346
370
|
await page.getByTestId('logout').click()
|
|
347
371
|
await page.getByRole('button', { name: /邮件验证码|Email code/i, exact: true }).click()
|
|
348
|
-
await page.screenshot({
|
|
372
|
+
await page.screenshot({
|
|
373
|
+
path: path.join(evidence, '{{projectName}}-email.png'),
|
|
374
|
+
fullPage: true,
|
|
375
|
+
})
|
|
349
376
|
await page.locator('#login-username').fill(email)
|
|
350
377
|
await page.getByRole('button', { name: /发送验证码|Send code/i, exact: true }).click()
|
|
351
378
|
await expect(page.getByRole('button', { name: /秒后重发|Resend in/ })).toBeDisabled()
|
|
@@ -361,14 +388,20 @@ test('shared authentication supports public registration with configuration', as
|
|
|
361
388
|
await expect(page.getByTestId('settings-page')).toBeVisible()
|
|
362
389
|
await page.getByTestId('logout').click()
|
|
363
390
|
await page.getByRole('button', { name: /忘记密码|Forgot password/i }).click()
|
|
364
|
-
await page.screenshot({
|
|
391
|
+
await page.screenshot({
|
|
392
|
+
path: path.join(evidence, '{{projectName}}-forgot.png'),
|
|
393
|
+
fullPage: true,
|
|
394
|
+
})
|
|
365
395
|
await page.locator('input[type="email"]').fill(email)
|
|
366
396
|
await page.locator('button[type="submit"]').click()
|
|
367
397
|
await expect(page.locator('.hai-toast-item[data-variant="success"]')).toBeVisible()
|
|
368
398
|
const reset = await received('password_reset')
|
|
369
399
|
expect(Boolean(reset?.token)).toBe(true)
|
|
370
400
|
await page.getByRole('button', { name: /已有重置令牌|I have a reset token/i }).click()
|
|
371
|
-
await page.screenshot({
|
|
401
|
+
await page.screenshot({
|
|
402
|
+
path: path.join(evidence, '{{projectName}}-reset.png'),
|
|
403
|
+
fullPage: true,
|
|
404
|
+
})
|
|
372
405
|
await page.goto(`./?token=${encodeURIComponent(reset?.token ?? '')}`)
|
|
373
406
|
await expect(page.locator('input[type="password"]').nth(0)).toBeVisible()
|
|
374
407
|
await page.locator('input[type="password"]').nth(0).fill('FullstackReset456')
|
|
@@ -399,7 +432,12 @@ test('frontend displays configured core metadata and restores the administrator
|
|
|
399
432
|
const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
|
|
400
433
|
if (evidence)
|
|
401
434
|
await mkdir(evidence, { recursive: true })
|
|
402
|
-
await page.screenshot({
|
|
435
|
+
await page.screenshot({
|
|
436
|
+
path: evidence
|
|
437
|
+
? path.join(evidence, '{{projectName}}-users.png')
|
|
438
|
+
: testInfo.outputPath('users.png'),
|
|
439
|
+
fullPage: true,
|
|
440
|
+
})
|
|
403
441
|
await page.getByTestId('logout').click()
|
|
404
442
|
await expect(page.getByTestId('login-form')).toBeVisible()
|
|
405
443
|
})
|
|
@@ -522,9 +560,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
|
|
|
522
560
|
await page.setViewportSize({ width, height: 900 })
|
|
523
561
|
if (width < 1024)
|
|
524
562
|
await expect.poll(() => page.locator('aside').evaluate(sidebar => sidebar.getBoundingClientRect().right)).toBeLessThanOrEqual(1)
|
|
525
|
-
const controls = await page.getByTestId('audit-advanced').evaluate(panel => [
|
|
563
|
+
const controls = await page.getByTestId('audit-advanced').evaluate(panel => [
|
|
564
|
+
...panel.querySelectorAll('input'),
|
|
565
|
+
].map((element) => {
|
|
526
566
|
// 复合控件的点击框包含边框和留白,内部文字输入区不等于完整控件高度。
|
|
527
|
-
const control = element.closest('.fieldset')
|
|
567
|
+
const control = element.closest('.fieldset')
|
|
568
|
+
? element.parentElement!
|
|
569
|
+
: element
|
|
528
570
|
const rect = control.getBoundingClientRect()
|
|
529
571
|
return { left: rect.left, right: rect.right, height: rect.height }
|
|
530
572
|
}))
|
|
@@ -540,7 +582,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
|
|
|
540
582
|
if (evidence)
|
|
541
583
|
await mkdir(evidence, { recursive: true })
|
|
542
584
|
const file = `{{projectName}}-audit-${width}.png`
|
|
543
|
-
await page.screenshot({
|
|
585
|
+
await page.screenshot({
|
|
586
|
+
path: evidence
|
|
587
|
+
? path.join(evidence, file)
|
|
588
|
+
: testInfo.outputPath(file),
|
|
589
|
+
fullPage: true,
|
|
590
|
+
animations: 'disabled',
|
|
591
|
+
})
|
|
544
592
|
}
|
|
545
593
|
}
|
|
546
594
|
const reset = page.waitForResponse(response => new URL(response.url()).pathname.endsWith('/admin/audit/list'))
|
|
@@ -561,7 +609,11 @@ test('audit filters collapse without losing values and fit compact workspaces',
|
|
|
561
609
|
await toggle.click()
|
|
562
610
|
await page.getByTestId('audit-page').getByRole('button', { name: /详情|Details/ }).first().click()
|
|
563
611
|
await expect(page.getByRole('dialog')).toBeVisible()
|
|
564
|
-
await page.screenshot({
|
|
612
|
+
await page.screenshot({
|
|
613
|
+
path: testInfo.outputPath('audit-short-table-details.png'),
|
|
614
|
+
fullPage: true,
|
|
615
|
+
animations: 'disabled',
|
|
616
|
+
})
|
|
565
617
|
})
|
|
566
618
|
|
|
567
619
|
test('administrator manages every duty through one lifecycle and the shared password reset form', async ({ page, browser }, testInfo) => {
|
|
@@ -647,7 +699,7 @@ test('administrator manages every duty through one lifecycle and the shared pass
|
|
|
647
699
|
await expect(row).toHaveCount(0)
|
|
648
700
|
})
|
|
649
701
|
|
|
650
|
-
test('administrator and user complete account, avatar, preference, password and audit workflows', async ({ page, browser }) => {
|
|
702
|
+
test('administrator and user complete account, avatar, preference, password and audit workflows', async ({ page, browser }, testInfo) => {
|
|
651
703
|
test.setTimeout(120_000)
|
|
652
704
|
const username = `member-${randomUUID().slice(0, 8)}`
|
|
653
705
|
const password = 'User-test-Pass123!'
|
|
@@ -716,8 +768,16 @@ test('administrator and user complete account, avatar, preference, password and
|
|
|
716
768
|
await userPage.getByRole('option', { name: 'English', exact: true }).click()
|
|
717
769
|
await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
|
|
718
770
|
await expect(userPage.locator('html')).toHaveAttribute('lang', 'en-US')
|
|
719
|
-
const themes = [
|
|
720
|
-
|
|
771
|
+
const themes = [
|
|
772
|
+
{{#each fullstack.theme.themes}}
|
|
773
|
+
'{{this}}',
|
|
774
|
+
{{/each}}
|
|
775
|
+
]
|
|
776
|
+
const selectedTheme = themes.includes('formal')
|
|
777
|
+
? 'formal'
|
|
778
|
+
: themes.includes('high-contrast')
|
|
779
|
+
? 'high-contrast'
|
|
780
|
+
: themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
|
|
721
781
|
await userPage.getByTestId('profile-theme').click()
|
|
722
782
|
await userPage.getByRole('option').nth(themes.indexOf(selectedTheme)).click()
|
|
723
783
|
await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
|
|
@@ -727,6 +787,20 @@ test('administrator and user complete account, avatar, preference, password and
|
|
|
727
787
|
await expect(userPage.getByTestId('settings-page')).toBeVisible()
|
|
728
788
|
await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
|
|
729
789
|
await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
|
|
790
|
+
if (themes.includes('eye-care')) {
|
|
791
|
+
await userPage.getByTestId('profile-theme').click()
|
|
792
|
+
await userPage.getByRole('option', { name: 'Eye care', exact: true }).click()
|
|
793
|
+
await expect(userPage.locator('html')).toHaveAttribute('data-theme', 'eye-care')
|
|
794
|
+
await userPage.reload()
|
|
795
|
+
await expect(userPage.getByTestId('settings-page')).toBeVisible()
|
|
796
|
+
await expect(userPage.locator('html')).toHaveAttribute('data-theme', 'eye-care')
|
|
797
|
+
await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe('light')
|
|
798
|
+
await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-base-100').trim())).toBe('#e8f0df')
|
|
799
|
+
await userPage.screenshot({
|
|
800
|
+
path: testInfo.outputPath('fullstack-eye-care.png'),
|
|
801
|
+
animations: 'disabled',
|
|
802
|
+
})
|
|
803
|
+
}
|
|
730
804
|
await expect(userPage.getByTestId('profile-form').locator('input[name="displayName"]')).toHaveValue('Updated member')
|
|
731
805
|
await expect(userPage.getByTestId('profile-form').locator('input[name="email"]')).toHaveValue('')
|
|
732
806
|
await expect.poll(() => userPage.getByTestId('profile-form').locator('img').evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
|
|
@@ -805,6 +879,8 @@ test('administrator and user complete account, avatar, preference, password and
|
|
|
805
879
|
{{#if fullstack.e2eFrontend}}
|
|
806
880
|
|
|
807
881
|
test('feedback submits an empty description and business operator can process and reopen it', async ({ page }, testInfo) => {
|
|
882
|
+
// 附件脱敏、桌面/手机截图和导出校验共用完整流程预算,目录代理也执行相同断言。
|
|
883
|
+
test.setTimeout(120_000)
|
|
808
884
|
await page.goto('./')
|
|
809
885
|
await login(page, 'business_operator', businessOperatorPassword)
|
|
810
886
|
const feedbackImage = Buffer.from(await page.evaluate(() => {
|
|
@@ -853,7 +929,11 @@ test('feedback submits an empty description and business operator can process an
|
|
|
853
929
|
await dialog.locator('input[type=file]').setInputFiles([
|
|
854
930
|
{ name: 'attached-preview.png', mimeType: 'image/png', buffer: feedbackImage },
|
|
855
931
|
{ name: 'attached-large-log.txt', mimeType: 'text/plain', buffer: feedbackLog },
|
|
856
|
-
{
|
|
932
|
+
{
|
|
933
|
+
name: 'discarded.txt',
|
|
934
|
+
mimeType: 'text/plain',
|
|
935
|
+
buffer: Buffer.from('discarded local material'),
|
|
936
|
+
},
|
|
857
937
|
])
|
|
858
938
|
const uploaded = dialog.locator('img[alt="attached-preview.png"]')
|
|
859
939
|
await expect(uploaded).toBeVisible()
|
|
@@ -861,7 +941,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
861
941
|
await expectDialogViewport()
|
|
862
942
|
await expectUncovered(uploaded)
|
|
863
943
|
await capture(page, 'feedback-image-before-mask.png', testInfo.outputPath('feedback-image-before-mask.png'))
|
|
864
|
-
await uploaded.screenshot({
|
|
944
|
+
await uploaded.screenshot({
|
|
945
|
+
path: testInfo.outputPath('feedback-image-before-mask-detail.png'),
|
|
946
|
+
animations: 'disabled',
|
|
947
|
+
})
|
|
865
948
|
const originalPreview = await uploaded.getAttribute('src')
|
|
866
949
|
await dialog.getByRole('button', { name: /遮挡敏感区域|Mask sensitive area|機密情報を隠す/ }).last().click()
|
|
867
950
|
await dialog.getByRole('button', { name: /应用标注|Apply annotation|注釈を適用/ }).last().click()
|
|
@@ -886,7 +969,15 @@ test('feedback submits an empty description and business operator can process an
|
|
|
886
969
|
await expectDialogViewport()
|
|
887
970
|
await expectUncovered(uploaded)
|
|
888
971
|
await capture(page, 'feedback-preview.png', testInfo.outputPath('feedback-preview.png'))
|
|
889
|
-
await uploaded.screenshot({
|
|
972
|
+
await uploaded.screenshot({
|
|
973
|
+
path: testInfo.outputPath('feedback-image-after-mask-detail.png'),
|
|
974
|
+
animations: 'disabled',
|
|
975
|
+
})
|
|
976
|
+
// 回执只表示记录已受理;等这份大附件真正上传成功,再开始后台处理操作。
|
|
977
|
+
const largeUpload = page.waitForResponse(response => response.request().method() === 'POST'
|
|
978
|
+
&& /\/feedback\/reports\/[^/]+\/attachments\/[^/]+$/.test(new URL(response.url()).pathname)
|
|
979
|
+
&& (response.request().postDataBuffer()?.byteLength ?? 0) > 2 * 1024 * 1024, { timeout: 60_000 })
|
|
980
|
+
.then(response => ({ response }), (error: unknown) => ({ error }))
|
|
890
981
|
await dialog.getByRole('button', { name: /提交反馈|Submit Feedback|フィードバックを送信/ }).click()
|
|
891
982
|
await expect(dialog.getByTestId('feedback-receipt')).toContainText(/FB-/)
|
|
892
983
|
const number = (await dialog.getByTestId('feedback-receipt').textContent())?.match(/FB-[A-Za-z0-9-]+/)?.[0]
|
|
@@ -895,13 +986,20 @@ test('feedback submits an empty description and business operator can process an
|
|
|
895
986
|
throw new Error('Feedback receipt number is missing')
|
|
896
987
|
await expectDialogViewport()
|
|
897
988
|
await capture(page, 'feedback-accepted.png', testInfo.outputPath('feedback-accepted.png'))
|
|
989
|
+
const completedUpload = await largeUpload
|
|
990
|
+
if ('error' in completedUpload)
|
|
991
|
+
throw completedUpload.error
|
|
992
|
+
expect(completedUpload.response.status()).toBe(200)
|
|
898
993
|
await dialog.getByRole('button', { name: /关闭|Close|閉じる/ }).filter({ hasText: /关闭|Close|閉じる/ }).click()
|
|
899
994
|
await page.getByTestId('nav-feedback').click()
|
|
900
995
|
await expect(page.getByTestId('feedback-page')).toBeVisible()
|
|
901
996
|
const row = page.getByRole('row').filter({ hasText: number })
|
|
902
997
|
await expect(row).toBeVisible()
|
|
903
|
-
|
|
904
|
-
await
|
|
998
|
+
const handled = page.waitForResponse(response => response.request().method() === 'PATCH' && new URL(response.url()).pathname.endsWith('/status'))
|
|
999
|
+
await row.getByRole('button', { name: /标记已处理|Mark(?: as)? handled|処理済み/ }).click()
|
|
1000
|
+
expect((await handled).status()).toBe(200)
|
|
1001
|
+
// 仅核对状态徽标;“标记已处理”按钮本身也包含“已处理”,不能据此放行。
|
|
1002
|
+
await expect(row.getByText(/^(已处理|Handled|処理済み)$/)).toBeVisible()
|
|
905
1003
|
await row.getByRole('button', { name: /撤销处理|Reopen|処理を取り消す/ }).scrollIntoViewIfNeeded()
|
|
906
1004
|
await capture(page, 'feedback-handled.png', testInfo.outputPath('feedback-handled.png'))
|
|
907
1005
|
await row.getByRole('button', { name: /撤销处理|Reopen|処理を取り消す/ }).click()
|
|
@@ -945,7 +1043,12 @@ test('feedback submits an empty description and business operator can process an
|
|
|
945
1043
|
const serverManifest: { version: string } = JSON.parse(await readFile(path.resolve('packages/{{projectName}}-serv/package.json'), 'utf8'))
|
|
946
1044
|
let expectedCommit = 'unknown'
|
|
947
1045
|
try {
|
|
948
|
-
execFileSync('git', [
|
|
1046
|
+
execFileSync('git', [
|
|
1047
|
+
'ls-files',
|
|
1048
|
+
'--error-unmatch',
|
|
1049
|
+
'--',
|
|
1050
|
+
'package.json',
|
|
1051
|
+
], { stdio: 'pipe', windowsHide: true })
|
|
949
1052
|
expectedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe', windowsHide: true }).trim()
|
|
950
1053
|
}
|
|
951
1054
|
catch { /* 无 Git 的源码归档必须明确保留 unknown,不冒用父工程提交。 */ }
|
|
@@ -954,7 +1057,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
954
1057
|
expect(expectedCommit).toMatch(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i)
|
|
955
1058
|
expect(browserManifest.version).not.toBe(serverManifest.version)
|
|
956
1059
|
}
|
|
957
|
-
for (const [metadata, version] of [
|
|
1060
|
+
for (const [metadata, version] of [
|
|
1061
|
+
[browserMetadata, browserManifest.version],
|
|
1062
|
+
[serverMetadata, serverManifest.version],
|
|
1063
|
+
] as const) {
|
|
958
1064
|
expect(Object.keys(metadata).sort()).toEqual(['appVersion', 'buildId', 'commit'])
|
|
959
1065
|
expect(metadata.appVersion).toBe(version)
|
|
960
1066
|
expect(metadata.buildId).toMatch(/^[\w.+-]{1,160}$/)
|
|
@@ -966,7 +1072,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
966
1072
|
expect(browserEnvironment?.data).toMatchObject(browserMetadata)
|
|
967
1073
|
expect(serverEnvironment?.data.service).toMatchObject(serverMetadata)
|
|
968
1074
|
expect(bundle.report).toMatchObject(browserMetadata)
|
|
969
|
-
await testInfo.attach('feedback-build-metadata', {
|
|
1075
|
+
await testInfo.attach('feedback-build-metadata', {
|
|
1076
|
+
body: JSON.stringify({ browser: browserMetadata, server: serverMetadata }),
|
|
1077
|
+
contentType: 'application/json',
|
|
1078
|
+
})
|
|
970
1079
|
expect(bundle.report.number).toBe(number)
|
|
971
1080
|
expect(bundle.manifest.reportId).toBe(bundle.report.id)
|
|
972
1081
|
expect(bundle.manifest.evidenceRevision).toBe(bundle.report.evidenceRevision)
|
|
@@ -982,7 +1091,10 @@ test('feedback submits an empty description and business operator can process an
|
|
|
982
1091
|
throw new Error('The exported large log attachment is missing')
|
|
983
1092
|
expect(bundle.files[largeLog.path]).toEqual(new Uint8Array(feedbackLog))
|
|
984
1093
|
expect(bundle.manifest.files.some(file => file.name === 'discarded.txt')).toBe(false)
|
|
985
|
-
await testInfo.attach('feedback-export-masked', {
|
|
1094
|
+
await testInfo.attach('feedback-export-masked', {
|
|
1095
|
+
body: Buffer.from(bundle.files[masked.path]!),
|
|
1096
|
+
contentType: 'image/png',
|
|
1097
|
+
})
|
|
986
1098
|
})
|
|
987
1099
|
{{/if}}
|
|
988
1100
|
{{/if}}
|
|
@@ -20,8 +20,13 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
|
|
|
20
20
|
// 每例使用独立初始账号,真实完成服务端限制下的首次改密后才能建立普通会话。
|
|
21
21
|
await expect(page.locator('#old-password')).toBeVisible()
|
|
22
22
|
await expect(page.getByTestId('logout')).toHaveCount(0)
|
|
23
|
-
await page.screenshot({
|
|
24
|
-
|
|
23
|
+
await page.screenshot({
|
|
24
|
+
path: test.info().outputPath('initial-password-renewal.png'),
|
|
25
|
+
fullPage: true,
|
|
26
|
+
})
|
|
27
|
+
const form = page.locator('form').filter({
|
|
28
|
+
has: page.locator('#old-password'),
|
|
29
|
+
})
|
|
25
30
|
const error = page.locator('.hai-toast-item[data-variant=error]')
|
|
26
31
|
await dismissToasts(page)
|
|
27
32
|
// 同密码仍由服务端拒绝,显示安全原因与关联编号;本地确认不一致则直接提示字段。
|
|
@@ -34,7 +39,10 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
|
|
|
34
39
|
await error.getByRole('button', { name: /^(查看排查信息|View diagnostic details|調査情報を表示)$/ }).click()
|
|
35
40
|
await expect(error.locator('code')).toHaveText(/^[\w.:-]{1,128}$/)
|
|
36
41
|
await expect(page.getByTestId('logout')).toHaveCount(0)
|
|
37
|
-
await page.screenshot({
|
|
42
|
+
await page.screenshot({
|
|
43
|
+
path: test.info().outputPath('initial-password-same.png'),
|
|
44
|
+
fullPage: true,
|
|
45
|
+
})
|
|
38
46
|
await expect(form.locator('.alert-error')).toHaveCount(0)
|
|
39
47
|
await dismissToasts(page)
|
|
40
48
|
const password = `${initialPassword}-Renewed!`
|
|
@@ -100,13 +108,19 @@ test('server idle expiry opens normal login in place and resumes one rejected sa
|
|
|
100
108
|
const dialog = page.getByTestId('session-recovery-dialog')
|
|
101
109
|
await expect(dialog).toBeVisible()
|
|
102
110
|
await expect(page.getByTestId('business-notice-input').locator('textarea')).toHaveValue(draft)
|
|
103
|
-
await page.screenshot({
|
|
111
|
+
await page.screenshot({
|
|
112
|
+
path: test.info().outputPath('idle-session-recovery.png'),
|
|
113
|
+
fullPage: true,
|
|
114
|
+
})
|
|
104
115
|
const form = dialog.locator('form.login-form')
|
|
105
116
|
await form.locator('input[type=password]').fill('Incorrect-test-password!')
|
|
106
117
|
await form.locator('button[type=submit]').click()
|
|
107
118
|
await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeVisible()
|
|
108
119
|
await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeInViewport({ ratio: 1 })
|
|
109
|
-
await page.screenshot({
|
|
120
|
+
await page.screenshot({
|
|
121
|
+
path: test.info().outputPath('idle-session-recovery-error-toast.png'),
|
|
122
|
+
fullPage: true,
|
|
123
|
+
})
|
|
110
124
|
expect(statuses).toHaveLength(1)
|
|
111
125
|
await form.locator('input[type=password]').fill(password)
|
|
112
126
|
await form.locator('button[type=submit]').click()
|
|
@@ -3,7 +3,9 @@ import { haiQualityConfig } from './eslint-rules.js'
|
|
|
3
3
|
|
|
4
4
|
export default antfu({
|
|
5
5
|
// unused-imports 使用 TS 规则,JS 同样使用 TS parser 以保持 scope 契约一致。
|
|
6
|
-
typescript: {
|
|
6
|
+
typescript: {
|
|
7
|
+
files: [GLOB_SRC],
|
|
8
|
+
},
|
|
7
9
|
formatters: true,
|
|
8
10
|
ignores: [
|
|
9
11
|
'**/build/**',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
1
|
+
ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
|
|
2
|
+
FROM ${HAI_NODE_IMAGE} AS build
|
|
4
3
|
|
|
5
4
|
ENV PNPM_HOME=/pnpm
|
|
6
5
|
ENV PATH=$PNPM_HOME:$PATH
|
|
@@ -8,20 +7,29 @@ ENV CI=true
|
|
|
8
7
|
ENV ONNXRUNTIME_NODE_INSTALL_CUDA=skip
|
|
9
8
|
WORKDIR /workspace
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
ARG HAI_NPM_REGISTRY=https://registry.npmjs.org
|
|
11
|
+
ARG COREPACK_NPM_REGISTRY=$HAI_NPM_REGISTRY
|
|
12
|
+
ARG npm_config_registry=$HAI_NPM_REGISTRY
|
|
13
|
+
ARG pnpm_config_registry=$HAI_NPM_REGISTRY
|
|
14
|
+
RUN node -e "const p=process.env.HTTPS_PROXY||process.env.HTTP_PROXY;if(p){const u=new URL(p);const s=require('node:net').connect({host:u.hostname,port:Number(u.port||(u.protocol==='https:'?443:80))});s.setTimeout(3000);s.on('connect',()=>s.end());const fail=()=>{process.stderr.write('Build proxy is unreachable from the container. Check the host address and proxy listener.\\n');process.exit(1)};s.on('error',fail);s.on('timeout',fail)}" && corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
|
|
12
15
|
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
13
16
|
COPY images/dependencies/ ./
|
|
14
|
-
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --reporter=append-only --ignore-scripts
|
|
18
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --reporter=append-only --offline --frozen-lockfile --ignore-scripts
|
|
19
|
+
COPY tsconfig.base.json ./
|
|
20
|
+
COPY scripts/ ./scripts/
|
|
21
|
+
COPY packages/{{projectName}}-contract/ ./packages/{{projectName}}-contract/
|
|
22
|
+
COPY packages/{{projectName}}-serv/ ./packages/{{projectName}}-serv/
|
|
17
23
|
ARG HAI_BUILD_ID=
|
|
18
24
|
ARG HAI_BUILD_COMMIT=unknown
|
|
19
25
|
RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" pnpm --filter {{fullstack.servPackageName}}... --if-present build
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
RUN
|
|
27
|
+
# SQLite 13 的原生二进制随 npm 包交付,在构建时验证实际加载。
|
|
28
|
+
RUN if [ -d node_modules/better-sqlite3 ]; then node -e "const Database=require('better-sqlite3');const db=new Database(':memory:');db.prepare('SELECT 1').get();db.close()"; fi
|
|
29
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
|
|
30
|
+
RUN rm -f /prod/service/README* && find /prod/service/dist -type f \( -name '*.map' -o -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) -delete
|
|
23
31
|
|
|
24
|
-
FROM
|
|
32
|
+
FROM ${HAI_NODE_IMAGE} AS service
|
|
25
33
|
|
|
26
34
|
ENV NODE_ENV=production
|
|
27
35
|
ENV HOST=0.0.0.0
|
|
@@ -34,5 +42,5 @@ RUN mkdir -p /app/data && chown node:node /app/data
|
|
|
34
42
|
USER node
|
|
35
43
|
EXPOSE 3000
|
|
36
44
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
37
|
-
CMD node -e "fetch('http://127.0.0.1:'+
|
|
45
|
+
CMD node -e "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config', configSchemas: { serv: ServConfigSchema } }); const { server, http } = ServConfigSchema.parse(core.config.getOrThrow('serv')); if (http.health === false) process.exit(0); const response = await fetch('http://127.0.0.1:' + server.port + http.health.path, { signal: AbortSignal.timeout(4000) }); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); } })()"
|
|
38
46
|
CMD ["node", "dist/index.js"]
|
|
@@ -1,25 +1,33 @@
|
|
|
1
|
+
ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
|
|
2
|
+
ARG HAI_NGINX_IMAGE=nginxinc/nginx-unprivileged:1.29.8-alpine
|
|
1
3
|
{{#if fullstack.frontends.web}}
|
|
2
|
-
FROM
|
|
3
|
-
ARG SQLITE_PREBUILD_MIRROR=
|
|
4
|
-
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
4
|
+
FROM ${HAI_NODE_IMAGE} AS build
|
|
5
5
|
|
|
6
6
|
ENV PNPM_HOME=/pnpm
|
|
7
7
|
ENV PATH=$PNPM_HOME:$PATH
|
|
8
8
|
ENV CI=true
|
|
9
9
|
WORKDIR /workspace
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
ARG HAI_NPM_REGISTRY=https://registry.npmjs.org
|
|
12
|
+
ARG COREPACK_NPM_REGISTRY=$HAI_NPM_REGISTRY
|
|
13
|
+
ARG npm_config_registry=$HAI_NPM_REGISTRY
|
|
14
|
+
ARG pnpm_config_registry=$HAI_NPM_REGISTRY
|
|
15
|
+
RUN node -e "const p=process.env.HTTPS_PROXY||process.env.HTTP_PROXY;if(p){const u=new URL(p);const s=require('node:net').connect({host:u.hostname,port:Number(u.port||(u.protocol==='https:'?443:80))});s.setTimeout(3000);s.on('connect',()=>s.end());const fail=()=>{process.stderr.write('Build proxy is unreachable from the container. Check the host address and proxy listener.\\n');process.exit(1)};s.on('error',fail);s.on('timeout',fail)}" && corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
|
|
12
16
|
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
13
17
|
COPY images/dependencies/ ./
|
|
14
|
-
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --reporter=append-only --ignore-scripts
|
|
19
|
+
RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --reporter=append-only --offline --frozen-lockfile --ignore-scripts
|
|
20
|
+
COPY tsconfig.base.json ./
|
|
21
|
+
COPY scripts/ ./scripts/
|
|
22
|
+
COPY packages/{{projectName}}-contract/ ./packages/{{projectName}}-contract/
|
|
23
|
+
COPY packages/{{projectName}}-shared/ ./packages/{{projectName}}-shared/
|
|
24
|
+
COPY apps/{{projectName}}-web/ ./apps/{{projectName}}-web/
|
|
17
25
|
ARG HAI_BUILD_ID=
|
|
18
26
|
ARG HAI_BUILD_COMMIT=unknown
|
|
19
27
|
ARG PUBLIC_BASE_PATH=
|
|
20
28
|
RUN pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile && HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" PUBLIC_BASE_PATH="$PUBLIC_BASE_PATH" pnpm --filter {{projectName}}-web... --if-present build
|
|
21
29
|
|
|
22
|
-
FROM
|
|
30
|
+
FROM ${HAI_NGINX_IMAGE} AS web
|
|
23
31
|
|
|
24
32
|
ENV PUBLIC_API_BASE=
|
|
25
33
|
ENV NGINX_ENVSUBST_FILTER=PUBLIC_API_BASE_BASE64
|
|
@@ -30,6 +38,7 @@ COPY images/web/nginx.conf /etc/nginx/conf.d/default.conf
|
|
|
30
38
|
COPY --chmod=755 images/web/19-runtime-config.envsh /docker-entrypoint.d/19-runtime-config.envsh
|
|
31
39
|
COPY images/web/runtime-config.js.template /etc/nginx/templates/runtime-config.js.template
|
|
32
40
|
COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
|
|
41
|
+
RUN find /usr/share/nginx/html -type f -name '*.map' -delete
|
|
33
42
|
|
|
34
43
|
EXPOSE 8080
|
|
35
44
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"lint:fix": "eslint *.js *.ts e2e --fix && pnpm -r --if-present lint:fix",
|
|
27
27
|
"test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present test",
|
|
28
28
|
"test:watch": "pnpm -r --parallel --if-present test:watch",
|
|
29
|
-
"test:e2e": "pnpm compile && node scripts/run-e2e.mjs --config playwright.config.ts{{#if fullstack.e2eFrontend}} --next --config playwright.
|
|
29
|
+
"test:e2e": "pnpm compile && node scripts/run-e2e.mjs --config playwright.config.ts{{#if fullstack.e2eFrontend}} --next --e2e-group=session-close --config playwright.config.ts{{/if}}",
|
|
30
30
|
"clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"{{fullstack.sharedPackageName}}": "workspace:*"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
+
"@h-ai/core": "{{haiPackageSpecifier}}",
|
|
38
39
|
"@h-ai/cli": "{{haiPackageSpecifier}}",
|
|
39
40
|
{{#if features.feedback}}
|
|
40
41
|
"@h-ai/feedback": "{{haiPackageSpecifier}}",
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
|
|
15
15
|
### 骨架说明
|
|
16
16
|
|
|
17
|
+
| 功能 | 输入示例 | 成功输出与边界 |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| app.info | 无业务参数 | notice、名称/版本/环境、语言、启动时间及frontends;只返回公开元信息 |
|
|
20
|
+
| app.echo | `{ message: 'hello' }`,1–200字符 | 相同message、echoedAt、requestId;演示回显,不持久化 |
|
|
21
|
+
| admin契约 | 用户、角色、审计、公告或个人设置操作参数 | 受当前身份/权限与资源范围约束的HaiResult;先检查success再读data |
|
|
22
|
+
| 工程聚合 | app/admin及框架模块contract | Serv和所有前端共用类型,契约声明不会自动授予权限 |
|
|
23
|
+
|
|
17
24
|
- `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
|
|
18
25
|
- `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
|
|
19
26
|
- `src/admin-schemas.ts` / `src/admin-contract.ts`:示例管理员用户、系统/审计/配置/业务操作员及基础成员角色、账号生命周期、邀请、用户列表服务端排序、审计分类/列头排序/统计/备份、业务公告和个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,通过服务端配置启用注册、密码/LDAP/验证码登录及密码重置。
|
|
@@ -23,11 +30,6 @@
|
|
|
23
30
|
API 前缀、健康检查与跨端 Header 等传输配置位于 serv 包的 `config/_serv.yml`。
|
|
24
31
|
`SUPPORTED_LOCALES` 为生成时实际编译的语言列表;`app.info` 中名称、版本、环境、默认语言来自后端 core 配置。更新公开契约时同时更新 serv、shared typed API client、各端 UI 与测试。
|
|
25
32
|
|
|
26
|
-
### 开发
|
|
27
|
-
|
|
28
|
-
- `pnpm --filter {{fullstack.contractPackageName}} typecheck`
|
|
29
|
-
- `pnpm --filter {{fullstack.contractPackageName}} test`
|
|
30
|
-
|
|
31
33
|
## 开发测试
|
|
32
34
|
|
|
33
35
|
```sh
|