openxiangda-cli 2.0.0-alpha.18 → 2.0.0-alpha.183
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.md +6 -14
- package/bin/run.js +79 -1
- package/dist/base.d.ts +60 -5
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +197 -10
- package/dist/base.js.map +1 -1
- package/dist/commands/accept.d.ts +28 -0
- package/dist/commands/accept.d.ts.map +1 -0
- package/dist/commands/accept.js +21 -0
- package/dist/commands/accept.js.map +1 -0
- package/dist/commands/admin.d.ts +32 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/admin.js +25 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/cancel.d.ts +19 -1
- package/dist/commands/cancel.d.ts.map +1 -1
- package/dist/commands/cancel.js +1 -1
- package/dist/commands/cancel.js.map +1 -1
- package/dist/commands/check.d.ts +23 -1
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +21 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.d.ts +27 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +10 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/create.d.ts +40 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +475 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/deploy.d.ts +28 -8
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +30 -18
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +23 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +28 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/docs.d.ts +30 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +20 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/login.d.ts +29 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/{auth/login.js → login.js} +27 -9
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +22 -2
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +9 -5
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/retry.d.ts +19 -1
- package/dist/commands/retry.d.ts.map +1 -1
- package/dist/commands/retry.js +5 -2
- package/dist/commands/retry.js.map +1 -1
- package/dist/commands/rollback.d.ts +23 -4
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +18 -5
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/skill.d.ts +32 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +50 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/spec.d.ts +42 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +123 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/start.d.ts +28 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +18 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +20 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +11 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts +28 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +18 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/create-workspace.d.ts +67 -0
- package/dist/create-workspace.d.ts.map +1 -0
- package/dist/create-workspace.js +631 -0
- package/dist/create-workspace.js.map +1 -0
- package/dist/studio-events.d.ts +9 -0
- package/dist/studio-events.d.ts.map +1 -0
- package/dist/studio-events.js +30 -0
- package/dist/studio-events.js.map +1 -0
- package/dist/toolchain-capsule.d.ts +4 -0
- package/dist/toolchain-capsule.d.ts.map +1 -0
- package/dist/toolchain-capsule.js +116 -0
- package/dist/toolchain-capsule.js.map +1 -0
- package/package.json +17 -10
- package/template/.dockerignore +23 -0
- package/template/.env.example +3 -0
- package/template/AGENTS.md +26 -0
- package/template/README.md +45 -0
- package/template/_gitignore +14 -0
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/client-fixture.ts +5 -0
- package/template/apps/web/e2e/component-defaults-fixture.css +15 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +162 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +144 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +387 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +285 -0
- package/template/apps/web/e2e/mobile-files.spec.ts +96 -0
- package/template/apps/web/e2e/mobile-reference-fixture.tsx +314 -0
- package/template/apps/web/e2e/mobile-reference.spec.ts +360 -0
- package/template/apps/web/e2e/mobile-selection.spec.ts +199 -0
- package/template/apps/web/e2e/resource-experience-fixture.tsx +491 -0
- package/template/apps/web/e2e/resource-platform-mock.ts +202 -0
- package/template/apps/web/e2e/resources.spec.ts +488 -0
- package/template/apps/web/e2e/standard-admin.spec.ts +511 -0
- package/template/apps/web/e2e/workflow-entry-fixture.tsx +107 -0
- package/template/apps/web/e2e/workflow-entry.spec.ts +556 -0
- package/template/apps/web/e2e/workflow-experience-fixture.tsx +338 -0
- package/template/apps/web/e2e/workflow.spec.ts +1265 -0
- package/template/apps/web/field-protocol.e2e.html +22 -0
- package/template/apps/web/index.html +12 -0
- package/template/apps/web/mobile-reference.e2e.html +27 -0
- package/template/apps/web/package.json +34 -0
- package/template/apps/web/playwright.config.ts +47 -0
- package/template/apps/web/resource-experience.e2e.html +16 -0
- package/template/apps/web/scripts/check.mjs +43 -0
- package/template/apps/web/scripts/verify-build.mjs +6 -0
- package/template/apps/web/src/document.css +5 -0
- package/template/apps/web/src/main.tsx +66 -0
- package/template/apps/web/src/user-end/tailwind.css +5 -0
- package/template/apps/web/test/contracts.test.ts +163 -0
- package/template/apps/web/test/loopback.test.ts +68 -0
- package/template/apps/web/test/standard-user-surfaces.typecheck.tsx +27 -0
- package/template/apps/web/tsconfig.json +12 -0
- package/template/apps/web/vite.config.ts +43 -0
- package/template/apps/web/workflow-entry.e2e.html +15 -0
- package/template/apps/web/workflow-experience.e2e.html +15 -0
- package/template/appspec/app.md +52 -0
- package/template/openxiangda.config.ts +55 -0
- package/template/package.json +28 -0
- package/template/packages/contracts/package.json +23 -0
- package/template/packages/contracts/src/generated.ts +265 -0
- package/template/packages/contracts/src/index.ts +1 -0
- package/template/packages/contracts/tsconfig.json +9 -0
- package/template/pnpm-workspace.yaml +7 -0
- package/template/tsconfig.base.json +13 -0
- package/dist/commands/app/create.d.ts +0 -15
- package/dist/commands/app/create.d.ts.map +0 -1
- package/dist/commands/app/create.js +0 -42
- package/dist/commands/app/create.js.map +0 -1
- package/dist/commands/app/info.d.ts +0 -9
- package/dist/commands/app/info.d.ts.map +0 -1
- package/dist/commands/app/info.js +0 -10
- package/dist/commands/app/info.js.map +0 -1
- package/dist/commands/app/link.d.ts +0 -10
- package/dist/commands/app/link.d.ts.map +0 -1
- package/dist/commands/app/link.js +0 -16
- package/dist/commands/app/link.js.map +0 -1
- package/dist/commands/app/provision.d.ts +0 -9
- package/dist/commands/app/provision.d.ts.map +0 -1
- package/dist/commands/app/provision.js +0 -10
- package/dist/commands/app/provision.js.map +0 -1
- package/dist/commands/auth/login.d.ts +0 -11
- package/dist/commands/auth/login.d.ts.map +0 -1
- package/dist/commands/auth/login.js.map +0 -1
- package/dist/commands/auth/logout.d.ts +0 -6
- package/dist/commands/auth/logout.d.ts.map +0 -1
- package/dist/commands/auth/logout.js +0 -30
- package/dist/commands/auth/logout.js.map +0 -1
- package/dist/commands/auth/status.d.ts +0 -6
- package/dist/commands/auth/status.d.ts.map +0 -1
- package/dist/commands/auth/status.js +0 -60
- package/dist/commands/auth/status.js.map +0 -1
- package/dist/commands/build.d.ts +0 -11
- package/dist/commands/build.d.ts.map +0 -1
- package/dist/commands/build.js +0 -23
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -9
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -7
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/event/delivery/list.d.ts +0 -10
- package/dist/commands/event/delivery/list.d.ts.map +0 -1
- package/dist/commands/event/delivery/list.js +0 -14
- package/dist/commands/event/delivery/list.js.map +0 -1
- package/dist/commands/event/delivery/replay.d.ts +0 -13
- package/dist/commands/event/delivery/replay.d.ts.map +0 -1
- package/dist/commands/event/delivery/replay.js +0 -16
- package/dist/commands/event/delivery/replay.js.map +0 -1
- package/dist/commands/event/subscription/list.d.ts +0 -9
- package/dist/commands/event/subscription/list.d.ts.map +0 -1
- package/dist/commands/event/subscription/list.js +0 -10
- package/dist/commands/event/subscription/list.js.map +0 -1
- package/dist/commands/event/subscription/rotate-secret.d.ts +0 -15
- package/dist/commands/event/subscription/rotate-secret.d.ts.map +0 -1
- package/dist/commands/event/subscription/rotate-secret.js +0 -33
- package/dist/commands/event/subscription/rotate-secret.js.map +0 -1
- package/dist/commands/event/subscription/status.d.ts +0 -14
- package/dist/commands/event/subscription/status.d.ts.map +0 -1
- package/dist/commands/event/subscription/status.js +0 -19
- package/dist/commands/event/subscription/status.js.map +0 -1
- package/dist/commands/event/timer/list.d.ts +0 -9
- package/dist/commands/event/timer/list.d.ts.map +0 -1
- package/dist/commands/event/timer/list.js +0 -10
- package/dist/commands/event/timer/list.js.map +0 -1
- package/dist/commands/event/timer/status.d.ts +0 -14
- package/dist/commands/event/timer/status.d.ts.map +0 -1
- package/dist/commands/event/timer/status.js +0 -19
- package/dist/commands/event/timer/status.js.map +0 -1
- package/dist/commands/generate.d.ts +0 -10
- package/dist/commands/generate.d.ts.map +0 -1
- package/dist/commands/generate.js +0 -8
- package/dist/commands/generate.js.map +0 -1
- package/dist/commands/oauth/audit/list.d.ts +0 -12
- package/dist/commands/oauth/audit/list.d.ts.map +0 -1
- package/dist/commands/oauth/audit/list.js +0 -20
- package/dist/commands/oauth/audit/list.js.map +0 -1
- package/dist/commands/oauth/client/create.d.ts +0 -13
- package/dist/commands/oauth/client/create.d.ts.map +0 -1
- package/dist/commands/oauth/client/create.js +0 -34
- package/dist/commands/oauth/client/create.js.map +0 -1
- package/dist/commands/oauth/client/list.d.ts +0 -9
- package/dist/commands/oauth/client/list.d.ts.map +0 -1
- package/dist/commands/oauth/client/list.js +0 -10
- package/dist/commands/oauth/client/list.js.map +0 -1
- package/dist/commands/oauth/client/revoke.d.ts +0 -12
- package/dist/commands/oauth/client/revoke.d.ts.map +0 -1
- package/dist/commands/oauth/client/revoke.js +0 -12
- package/dist/commands/oauth/client/revoke.js.map +0 -1
- package/dist/commands/oauth/client/rotate.d.ts +0 -13
- package/dist/commands/oauth/client/rotate.d.ts.map +0 -1
- package/dist/commands/oauth/client/rotate.js +0 -20
- package/dist/commands/oauth/client/rotate.js.map +0 -1
- package/dist/commands/oauth/client/update.d.ts +0 -15
- package/dist/commands/oauth/client/update.d.ts.map +0 -1
- package/dist/commands/oauth/client/update.js +0 -23
- package/dist/commands/oauth/client/update.js.map +0 -1
- package/dist/commands/oauth/runtime/rotate.d.ts +0 -12
- package/dist/commands/oauth/runtime/rotate.d.ts.map +0 -1
- package/dist/commands/oauth/runtime/rotate.js +0 -31
- package/dist/commands/oauth/runtime/rotate.js.map +0 -1
- package/dist/commands/oauth/runtime/status.d.ts +0 -10
- package/dist/commands/oauth/runtime/status.d.ts.map +0 -1
- package/dist/commands/oauth/runtime/status.js +0 -17
- package/dist/commands/oauth/runtime/status.js.map +0 -1
- package/dist/commands/promote.d.ts +0 -13
- package/dist/commands/promote.d.ts.map +0 -1
- package/dist/commands/promote.js +0 -9
- package/dist/commands/promote.js.map +0 -1
- package/dist/commands/secret/audit/list.d.ts +0 -14
- package/dist/commands/secret/audit/list.d.ts.map +0 -1
- package/dist/commands/secret/audit/list.js +0 -16
- package/dist/commands/secret/audit/list.js.map +0 -1
- package/dist/commands/secret/create.d.ts +0 -18
- package/dist/commands/secret/create.d.ts.map +0 -1
- package/dist/commands/secret/create.js +0 -33
- package/dist/commands/secret/create.js.map +0 -1
- package/dist/commands/secret/delete.d.ts +0 -15
- package/dist/commands/secret/delete.d.ts.map +0 -1
- package/dist/commands/secret/delete.js +0 -20
- package/dist/commands/secret/delete.js.map +0 -1
- package/dist/commands/secret/list.d.ts +0 -10
- package/dist/commands/secret/list.d.ts.map +0 -1
- package/dist/commands/secret/list.js +0 -14
- package/dist/commands/secret/list.js.map +0 -1
- package/dist/commands/secret/rotate.d.ts +0 -18
- package/dist/commands/secret/rotate.d.ts.map +0 -1
- package/dist/commands/secret/rotate.js +0 -30
- package/dist/commands/secret/rotate.js.map +0 -1
- package/dist/commands/secret/update.d.ts +0 -17
- package/dist/commands/secret/update.d.ts.map +0 -1
- package/dist/commands/secret/update.js +0 -37
- package/dist/commands/secret/update.js.map +0 -1
- package/dist/commands/skill/install.d.ts +0 -11
- package/dist/commands/skill/install.d.ts.map +0 -1
- package/dist/commands/skill/install.js +0 -52
- package/dist/commands/skill/install.js.map +0 -1
- package/dist/commands/skill/validate.d.ts +0 -9
- package/dist/commands/skill/validate.d.ts.map +0 -1
- package/dist/commands/skill/validate.js +0 -32
- package/dist/commands/skill/validate.js.map +0 -1
- package/dist/commands/test.d.ts +0 -9
- package/dist/commands/test.d.ts.map +0 -1
- package/dist/commands/test.js +0 -7
- package/dist/commands/test.js.map +0 -1
- package/dist/commands/workflow/provider/list.d.ts +0 -10
- package/dist/commands/workflow/provider/list.d.ts.map +0 -1
- package/dist/commands/workflow/provider/list.js +0 -14
- package/dist/commands/workflow/provider/list.js.map +0 -1
- package/dist/commands/workflow/provider/rotate.d.ts +0 -15
- package/dist/commands/workflow/provider/rotate.d.ts.map +0 -1
- package/dist/commands/workflow/provider/rotate.js +0 -24
- package/dist/commands/workflow/provider/rotate.js.map +0 -1
- package/dist/secret-value.d.ts +0 -5
- package/dist/secret-value.d.ts.map +0 -1
- package/dist/secret-value.js +0 -15
- package/dist/secret-value.js.map +0 -1
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
import { expect, test, type Locator, type Page } from '@playwright/test';
|
|
2
|
+
import { mockPlatform } from './resource-platform-mock';
|
|
3
|
+
|
|
4
|
+
async function chooseOption(page: Page, control: Locator, name: string) {
|
|
5
|
+
await control.click();
|
|
6
|
+
const listId = await control.getAttribute('aria-controls');
|
|
7
|
+
expect(listId).toBeTruthy();
|
|
8
|
+
// Closing overlays can coexist during animation; select only this input's list.
|
|
9
|
+
await page.locator(`[id="${listId}"]`).getByRole('option', { name, exact: true }).click();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function editablePlatform(page: Page, fixtureMode = 'business') {
|
|
13
|
+
const context = page.context();
|
|
14
|
+
await mockPlatform(context, true, undefined, true);
|
|
15
|
+
await context.route(/\/(?:admin|m\/admin)\/resources\//, async route => {
|
|
16
|
+
if (route.request().resourceType() !== 'document') return route.fallback();
|
|
17
|
+
const url = new URL(route.request().url()); url.pathname = '/resource-experience.e2e.html'; url.search = `?primary=${fixtureMode}`;
|
|
18
|
+
const response = await route.fetch({ url: url.href }); await route.fulfill({ response });
|
|
19
|
+
});
|
|
20
|
+
const records = Array.from({ length: 21 }, (_, index) => ({
|
|
21
|
+
id: `record-${index + 1}`, revision: 1, name: `业务记录 ${index + 1}`,
|
|
22
|
+
code: `R${index + 1}`, description: '', enabled: true, displayOrder: index,
|
|
23
|
+
created_at: '2026-09-05T00:00:00.000Z', updated_at: '2026-09-05T00:00:00.000Z',
|
|
24
|
+
}));
|
|
25
|
+
const state = {
|
|
26
|
+
records, writes: [] as any[], queries: [] as any[], reads: 0,
|
|
27
|
+
preferences: new Map<string, any>(),
|
|
28
|
+
failNextWrite: false, failReads: false, preference: null as any, failPreference: false, failDraft: false, drafts: [] as any[], draftWrites: [] as any[],
|
|
29
|
+
};
|
|
30
|
+
await context.route('**/native/admin-list/preferences/**', async route => {
|
|
31
|
+
const key = new URL(route.request().url()).pathname;
|
|
32
|
+
if (route.request().method() === 'PUT' && state.failPreference) return route.fulfill({ status: 500, json: { code: 500, message: '设置保存失败' } });
|
|
33
|
+
if (route.request().method() === 'PUT') { state.preference = route.request().postDataJSON(); state.preferences.set(key, state.preference); }
|
|
34
|
+
if (route.request().method() === 'DELETE') { state.preference = null; state.preferences.delete(key); }
|
|
35
|
+
await route.fulfill({ json: { code: 200, data: state.preferences.get(key) || null } });
|
|
36
|
+
});
|
|
37
|
+
await context.route('**/native/form-drafts/**', async route => {
|
|
38
|
+
const path = new URL(route.request().url()).pathname;
|
|
39
|
+
const body = route.request().method() === 'POST' ? route.request().postDataJSON() : {};
|
|
40
|
+
const ok = (data: any) => route.fulfill({ json: { code: 200, data } });
|
|
41
|
+
const conflict = () => route.fulfill({ status: 409, json: { code: 409, message: '草稿版本冲突' } });
|
|
42
|
+
if (path.endsWith('/save')) {
|
|
43
|
+
state.draftWrites.push(body);
|
|
44
|
+
if (state.failDraft) return route.fulfill({ status: 500, json: { code: 500, message: '暂存服务暂时不可用' } });
|
|
45
|
+
const current = state.drafts.find(item => item.id === body.id);
|
|
46
|
+
if ((current?.revision || 0) !== body.expectedRevision || (current && (current.viewCode || '') !== (body.viewCode || ''))) return conflict();
|
|
47
|
+
const draft = { ...body, revision: (current?.revision || 0) + 1, updatedAt: '2026-09-05T07:06:00.000Z', expiresAt: '2026-12-04T07:06:00.000Z' };
|
|
48
|
+
state.drafts = [draft, ...state.drafts.filter(item => item.id !== body.id)]; return ok(draft);
|
|
49
|
+
}
|
|
50
|
+
if (path.endsWith('/delete')) { state.drafts = state.drafts.filter(item => item.id !== body.id); return ok({ deleted: true }); }
|
|
51
|
+
if (path.endsWith('/submit')) {
|
|
52
|
+
const draft = state.drafts.find(item => item.id === body.id);
|
|
53
|
+
if (!draft || draft.revision !== body.expectedRevision || (draft.viewCode || '') !== (body.viewCode || '')) return conflict();
|
|
54
|
+
const operation = body.operations[0]; state.writes.push(operation);
|
|
55
|
+
const existing = records.find(item => item.id === operation.id);
|
|
56
|
+
if (existing && existing.revision !== operation.expectedRevision) return conflict();
|
|
57
|
+
const record = existing || { ...records[0], id: `record-${records.length + 1}` };
|
|
58
|
+
Object.assign(record, operation.data, { revision: (existing?.revision || 0) + 1 }); if (!existing) records.push(record);
|
|
59
|
+
state.drafts = state.drafts.filter(item => item.id !== body.id);
|
|
60
|
+
return ok({ items: [{ operation: operation.operation, id: record.id, revision: record.revision }] });
|
|
61
|
+
}
|
|
62
|
+
const query = new URL(route.request().url()).searchParams;
|
|
63
|
+
return ok({ items: state.drafts.filter(item => item.mode === query.get('mode') && (item.recordId || '') === (query.get('recordId') || '') && (item.viewCode || '') === (query.get('viewCode') || '')), limit: 20, retentionDays: 90 });
|
|
64
|
+
});
|
|
65
|
+
await context.route('**/native/data/resource-01/**', async route => {
|
|
66
|
+
const request = route.request();
|
|
67
|
+
const path = new URL(request.url()).pathname;
|
|
68
|
+
if (path.endsWith('/query')) {
|
|
69
|
+
const body = request.postDataJSON();
|
|
70
|
+
state.queries.push(body);
|
|
71
|
+
return route.fulfill({ json: { code: 200, data: {
|
|
72
|
+
schemaVersion: 'openxiangda.data-page/v2', resourceCode: 'resource-01',
|
|
73
|
+
items: records.slice(body.offset, body.offset + body.limit), total: records.length,
|
|
74
|
+
offset: body.offset, limit: body.limit,
|
|
75
|
+
} } });
|
|
76
|
+
}
|
|
77
|
+
const recordId = path.match(/\/records\/([^/]+)/)?.[1];
|
|
78
|
+
const current = records.find(record => record.id === recordId);
|
|
79
|
+
if (request.method() === 'GET' && current) {
|
|
80
|
+
state.reads += 1;
|
|
81
|
+
if (state.failReads) {
|
|
82
|
+
return route.fulfill({ status: 500, json: { code: 500, message: '暂时无法读取,请重试' } });
|
|
83
|
+
}
|
|
84
|
+
return route.fulfill({ json: { code: 200, data: { schemaVersion: 'openxiangda.data-record/v2', data: current } } });
|
|
85
|
+
}
|
|
86
|
+
if (request.method() === 'POST' && (path.endsWith('/records') || path.endsWith('/update'))) {
|
|
87
|
+
const body = request.postDataJSON();
|
|
88
|
+
state.writes.push(body);
|
|
89
|
+
if (state.failNextWrite || (current && body.expectedRevision !== current.revision)) {
|
|
90
|
+
state.failNextWrite = false;
|
|
91
|
+
return route.fulfill({ status: 409, json: { code: 409, message: '记录有冲突,请检查后重试' } });
|
|
92
|
+
}
|
|
93
|
+
// Keep the request in flight long enough to exercise double submission.
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 350));
|
|
95
|
+
const saved = current || { ...records[0], id: `record-${records.length + 1}` };
|
|
96
|
+
Object.assign(saved, body.data, { revision: current ? current.revision + 1 : 1 });
|
|
97
|
+
if (!current) records.push(saved);
|
|
98
|
+
return route.fulfill({ json: { code: 200, data: { schemaVersion: 'openxiangda.data-record/v2', data: saved } } });
|
|
99
|
+
}
|
|
100
|
+
return route.fallback();
|
|
101
|
+
});
|
|
102
|
+
return state;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
test('named views share records while keeping columns, drawer routes and drafts separate', async ({ page }, testInfo) => {
|
|
106
|
+
const state = await editablePlatform(page, 'named');
|
|
107
|
+
const base = '/admin/resources/resource-01/views';
|
|
108
|
+
await page.goto(`${base}/quick`);
|
|
109
|
+
await expect(page.getByRole('columnheader', { name: '栏目名称' })).toBeVisible();
|
|
110
|
+
await expect(page.getByRole('columnheader', { name: '启用状态' })).toHaveCount(0);
|
|
111
|
+
await page.getByRole('button', { name: '显示列', exact: true }).click();
|
|
112
|
+
const settings = page.getByRole('region', { name: '显示列设置' });
|
|
113
|
+
await settings.getByRole('checkbox', { name: '创建时间', exact: true }).check();
|
|
114
|
+
await page.getByRole('button', { name: '显示列', exact: true }).click();
|
|
115
|
+
await page.getByRole('button', { name: '保存显示列配置' }).click();
|
|
116
|
+
await expect(page.getByText('你调整了显示列配置')).toHaveCount(0);
|
|
117
|
+
expect([...state.preferences.keys()]).toHaveLength(1);
|
|
118
|
+
expect(decodeURIComponent([...state.preferences.keys()][0])).toContain(':view:quick');
|
|
119
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
120
|
+
const drawer = page.getByRole('dialog', { name: '新增数据' });
|
|
121
|
+
await expect(drawer.getByRole('heading', { name: '登记信息' })).toBeVisible();
|
|
122
|
+
await expect(drawer.getByLabel('栏目说明', { exact: true })).toHaveCount(0);
|
|
123
|
+
await drawer.getByLabel('栏目名称', { exact: true }).fill('简要登记的草稿');
|
|
124
|
+
await drawer.getByRole('button', { name: '暂存', exact: true }).click();
|
|
125
|
+
await expect.poll(() => state.drafts.length).toBe(1);
|
|
126
|
+
expect(state.drafts[0].viewCode).toBe('quick');
|
|
127
|
+
await drawer.getByRole('button', { name: '关闭', exact: true }).click();
|
|
128
|
+
await page.locator('.oxa-sider').getByText('完整管理', { exact: true }).click();
|
|
129
|
+
await expect(page).toHaveURL(`${base}/complete`);
|
|
130
|
+
await expect(page.getByRole('columnheader', { name: '启用状态' })).toBeVisible();
|
|
131
|
+
await expect(page.getByRole('columnheader', { name: '创建时间' })).toHaveCount(0);
|
|
132
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
133
|
+
await expect(drawer.getByLabel('栏目名称', { exact: true })).toBeEmpty();
|
|
134
|
+
await expect(drawer.getByLabel('栏目说明', { exact: true })).toBeVisible();
|
|
135
|
+
await expect(page.getByRole('dialog', { name: '暂存数据导入' })).toHaveCount(0);
|
|
136
|
+
await page.screenshot({ path: testInfo.outputPath('named-complete-drawer.png'), animations: 'disabled' });
|
|
137
|
+
await drawer.getByRole('button', { name: '关闭', exact: true }).click();
|
|
138
|
+
await page.locator('.oxa-sider').getByText('简要登记', { exact: true }).click();
|
|
139
|
+
await expect(page.getByRole('columnheader', { name: '创建时间' })).toBeVisible();
|
|
140
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
141
|
+
await page.getByRole('button', { name: '载入草稿' }).click();
|
|
142
|
+
await expect(drawer.getByLabel('栏目名称', { exact: true })).toHaveValue('简要登记的草稿');
|
|
143
|
+
const opened = page.context().waitForEvent('page');
|
|
144
|
+
await drawer.getByRole('button', { name: '新开页面' }).click();
|
|
145
|
+
const nextPage = await opened;
|
|
146
|
+
await expect(nextPage).toHaveURL(/\/views\/quick\/new\?draft=/);
|
|
147
|
+
await expect(nextPage.getByLabel('栏目名称', { exact: true })).toHaveValue('简要登记的草稿');
|
|
148
|
+
await expect(nextPage.getByLabel('栏目说明', { exact: true })).toHaveCount(0);
|
|
149
|
+
await nextPage.screenshot({ path: testInfo.outputPath('named-quick-new-page.png'), fullPage: true, animations: 'disabled' });
|
|
150
|
+
await nextPage.getByRole('button', { name: '提交', exact: true }).click();
|
|
151
|
+
await expect(nextPage).toHaveURL(`${base}/quick/record-22`);
|
|
152
|
+
expect(state.writes).toHaveLength(1);
|
|
153
|
+
expect(state.writes[0].resourceCode).toBe('resource-01');
|
|
154
|
+
expect(state.records.at(-1)?.name).toBe('简要登记的草稿');
|
|
155
|
+
expect(state.drafts).toHaveLength(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('mobile named forms retain their grouping and return to the same view after submit', async ({ page }, testInfo) => {
|
|
159
|
+
const state = await editablePlatform(page, 'named');
|
|
160
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
161
|
+
await page.goto('/m/admin/resources/resource-01/views/complete/new');
|
|
162
|
+
const form = page.locator('.oxa-mobile-form');
|
|
163
|
+
await expect(form.getByRole('heading', { name: '栏目信息' })).toBeVisible();
|
|
164
|
+
await expect(form.getByRole('heading', { name: '管理设置' })).toBeVisible();
|
|
165
|
+
await expect(page.getByRole('button', { name: '返回列表' })).toHaveCount(0);
|
|
166
|
+
await form.getByRole('textbox', { name: '栏目名称', exact: true }).fill('移动端完整登记');
|
|
167
|
+
await form.getByRole('textbox', { name: '栏目说明', exact: true }).fill('和简要登记共用一份数据');
|
|
168
|
+
await form.getByRole('button', { name: '暂存', exact: true }).click();
|
|
169
|
+
await expect.poll(() => state.drafts.length).toBe(1);
|
|
170
|
+
expect(state.drafts[0].viewCode).toBe('complete');
|
|
171
|
+
await page.reload();
|
|
172
|
+
await page.getByRole('button', { name: '载入草稿' }).click();
|
|
173
|
+
await expect(form.getByRole('textbox', { name: '栏目名称', exact: true })).toHaveValue('移动端完整登记');
|
|
174
|
+
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
175
|
+
await page.screenshot({ path: testInfo.outputPath('named-mobile-complete-form.png'), fullPage: true, animations: 'disabled' });
|
|
176
|
+
await form.getByRole('button', { name: '提交', exact: true }).click();
|
|
177
|
+
await expect(page).toHaveURL('/m/admin/resources/resource-01/views/complete/record-22');
|
|
178
|
+
expect(state.writes).toHaveLength(1);
|
|
179
|
+
expect(state.drafts).toHaveLength(0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('uses a collapsible menu and a single list toolbar with optional audit columns', async ({ page }, testInfo) => {
|
|
183
|
+
const state = await editablePlatform(page);
|
|
184
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
185
|
+
await expect(page.locator('.oxa-list-toolbar')).toHaveCount(1);
|
|
186
|
+
await expect(page.getByRole('navigation', { name: '页面访问历史' })).toHaveCount(0);
|
|
187
|
+
await expect(page.locator('.oxa-main h1, .oxa-main h2')).toHaveCount(0);
|
|
188
|
+
const group = page.locator('.ant-menu-submenu-title').filter({ hasText: '数据管理' });
|
|
189
|
+
await group.click();
|
|
190
|
+
await expect(page.locator('.oxa-sider').getByText('内容栏目', { exact: true })).toBeHidden();
|
|
191
|
+
await group.click();
|
|
192
|
+
await expect(page.locator('.oxa-sider').getByText('内容栏目', { exact: true })).toBeVisible();
|
|
193
|
+
await expect(page.getByRole('columnheader', { name: '创建时间' })).toHaveCount(0);
|
|
194
|
+
await expect(page.getByRole('columnheader', { name: '栏目名称' })).toBeVisible();
|
|
195
|
+
await page.screenshot({ path: testInfo.outputPath('standard-list.png'), fullPage: true, animations: 'disabled' });
|
|
196
|
+
await page.getByRole('button', { name: '显示列', exact: true }).click();
|
|
197
|
+
const settings = page.getByRole('region', { name: '显示列设置' });
|
|
198
|
+
await settings.getByRole('checkbox', { name: '创建时间', exact: true }).check();
|
|
199
|
+
await settings.getByRole('button', { name: '冻结栏目名称', exact: true }).click();
|
|
200
|
+
await expect(page.getByRole('columnheader', { name: '栏目名称' })).toHaveClass(/ant-table-cell-fix-start/);
|
|
201
|
+
await settings.getByRole('button', { name: '上移创建时间' }).click();
|
|
202
|
+
state.failPreference = true;
|
|
203
|
+
await page.getByRole('button', { name: '显示列', exact: true }).click();
|
|
204
|
+
await page.getByRole('button', { name: '保存显示列配置' }).click();
|
|
205
|
+
await expect(page.getByText('你调整了显示列配置')).toBeVisible();
|
|
206
|
+
state.failPreference = false;
|
|
207
|
+
await page.getByRole('button', { name: '保存显示列配置' }).click();
|
|
208
|
+
await expect(page.getByText('你调整了显示列配置')).toHaveCount(0);
|
|
209
|
+
expect(state.preference.columns.find((column: any) => column.key === 'created_at').visible).toBe(true);
|
|
210
|
+
expect(state.preference.columns.find((column: any) => column.key === 'name').fixed).toBe('left');
|
|
211
|
+
await page.reload();
|
|
212
|
+
await expect(page.getByRole('columnheader', { name: '创建时间' })).toBeVisible();
|
|
213
|
+
await expect(page.getByRole('columnheader', { name: '栏目名称' })).toHaveClass(/ant-table-cell-fix-start/);
|
|
214
|
+
await page.getByRole('button', { name: '显示列', exact: true }).click();
|
|
215
|
+
await settings.getByRole('button', { name: '恢复默认' }).click();
|
|
216
|
+
await expect(page.getByRole('columnheader', { name: '创建时间' })).toHaveCount(0);
|
|
217
|
+
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('grouped drawer validates, preserves input after failure and refreshes the same list after one retry', async ({ page }, testInfo) => {
|
|
221
|
+
const state = await editablePlatform(page);
|
|
222
|
+
const errors: string[] = [];
|
|
223
|
+
page.on('pageerror', error => errors.push(error.message));
|
|
224
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
225
|
+
const search = page.getByRole('searchbox', { name: '搜索数据' });
|
|
226
|
+
await search.fill('业务');
|
|
227
|
+
await search.press('Enter');
|
|
228
|
+
await page.locator('.ant-pagination-item-2').click();
|
|
229
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
230
|
+
const drawer = page.getByRole('dialog', { name: '新增数据' });
|
|
231
|
+
await expect(drawer.getByRole('heading', { name: '栏目设置' })).toBeVisible();
|
|
232
|
+
await expect(drawer.getByRole('heading', { name: '管理信息' })).toBeVisible();
|
|
233
|
+
await expect(drawer.locator('.oxa-section-card')).toHaveCount(0);
|
|
234
|
+
await drawer.getByRole('button', { name: '提交', exact: true }).click();
|
|
235
|
+
await expect(drawer.locator('.ant-form-item-explain-error')).toBeVisible();
|
|
236
|
+
expect(state.writes).toHaveLength(0);
|
|
237
|
+
await drawer.getByLabel('栏目名称', { exact: true }).fill('业务新增记录');
|
|
238
|
+
await drawer.getByLabel('栏目编码', { exact: true }).fill('NEW-1');
|
|
239
|
+
await drawer.getByLabel('栏目说明', { exact: true }).fill('失败后仍保留的内容');
|
|
240
|
+
await expect(drawer.locator('.ant-form-item-explain-error')).toHaveCount(0);
|
|
241
|
+
await page.screenshot({ path: testInfo.outputPath('standard-grouped-drawer.png'), animations: 'disabled' });
|
|
242
|
+
state.failNextWrite = true;
|
|
243
|
+
await drawer.getByRole('button', { name: '提交', exact: true }).click();
|
|
244
|
+
await expect(drawer.getByText('保存失败', { exact: true })).toBeVisible();
|
|
245
|
+
await expect(drawer.getByLabel('栏目名称', { exact: true })).toHaveValue('业务新增记录');
|
|
246
|
+
await expect(drawer.getByLabel('栏目说明', { exact: true })).toHaveValue('失败后仍保留的内容');
|
|
247
|
+
await drawer.getByRole('button', { name: '提交', exact: true }).dblclick({ delay: 30 });
|
|
248
|
+
await expect(drawer).toBeHidden();
|
|
249
|
+
expect(state.writes).toHaveLength(2);
|
|
250
|
+
expect(state.records.at(-1)?.name).toBe('业务新增记录');
|
|
251
|
+
await expect(search).toHaveValue('业务');
|
|
252
|
+
await expect(page.locator('.ant-pagination-item-2')).toHaveClass(/ant-pagination-item-active/);
|
|
253
|
+
await expect.poll(() => state.queries.at(-1)?.offset).toBe(10);
|
|
254
|
+
expect(JSON.stringify(state.queries.at(-1)?.where)).toContain('业务');
|
|
255
|
+
expect(errors).toEqual([]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test('drawer full screen preserves input, close discards edits and new page restores a saved draft', async ({ page }, testInfo) => {
|
|
259
|
+
const state = await editablePlatform(page);
|
|
260
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
261
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
262
|
+
let drawer = page.getByRole('dialog', { name: '新增数据' });
|
|
263
|
+
await drawer.getByRole('button', { name: '关闭', exact: true }).click();
|
|
264
|
+
await expect(drawer).toBeHidden();
|
|
265
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
266
|
+
drawer = page.getByRole('dialog', { name: '新增数据' });
|
|
267
|
+
await drawer.getByLabel('栏目名称', { exact: true }).fill('新页面继续填写');
|
|
268
|
+
await drawer.getByRole('button', { name: '全屏', exact: true }).click();
|
|
269
|
+
await expect.poll(async () => (await drawer.boundingBox())!.width).toBe(page.viewportSize()!.width);
|
|
270
|
+
await drawer.getByRole('button', { name: '退出全屏', exact: true }).click();
|
|
271
|
+
await drawer.getByRole('button', { name: '关闭', exact: true }).click();
|
|
272
|
+
await expect(drawer).toBeHidden();
|
|
273
|
+
await expect(page.getByRole('button', { name: '继续填写' })).toHaveCount(0);
|
|
274
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
275
|
+
await drawer.getByLabel('栏目名称', { exact: true }).fill('新页面继续填写');
|
|
276
|
+
const opened = page.context().waitForEvent('page');
|
|
277
|
+
await drawer.getByRole('button', { name: '新开页面' }).click();
|
|
278
|
+
const nextPage = await opened;
|
|
279
|
+
await expect(nextPage).toHaveURL(/\/admin\/resources\/resource-01\/new\?draft=/);
|
|
280
|
+
await expect(nextPage.locator('.oxa-topbar, .oxa-sider')).toHaveCount(0);
|
|
281
|
+
await expect(nextPage.locator('.oxa-independent-form-header')).toContainText('内容栏目');
|
|
282
|
+
await expect(nextPage.getByLabel('栏目名称', { exact: true })).toHaveValue('新页面继续填写');
|
|
283
|
+
await expect(drawer).toBeHidden();
|
|
284
|
+
expect(state.writes).toHaveLength(0);
|
|
285
|
+
expect(state.drafts).toHaveLength(1);
|
|
286
|
+
await nextPage.screenshot({ path: testInfo.outputPath('standard-new-page-draft.png'), fullPage: true, animations: 'disabled' });
|
|
287
|
+
await nextPage.getByRole('button', { name: '提交', exact: true }).click();
|
|
288
|
+
await expect(nextPage).toHaveURL('/admin/resources/resource-01/record-22');
|
|
289
|
+
expect(state.writes).toHaveLength(1); expect(state.drafts).toHaveLength(0);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('record reads can retry and reconnect refresh does not overwrite an edit or its revision', async ({ page }) => {
|
|
293
|
+
const state = await editablePlatform(page);
|
|
294
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
295
|
+
state.failReads = true;
|
|
296
|
+
await page.getByRole('button', { name: '编辑', exact: true }).first().click();
|
|
297
|
+
const drawer = page.getByRole('dialog', { name: '编辑数据' });
|
|
298
|
+
await expect(drawer.getByText('数据读取失败', { exact: true })).toBeVisible();
|
|
299
|
+
state.failReads = false;
|
|
300
|
+
await drawer.getByRole('button', { name: '重试' }).click();
|
|
301
|
+
await expect(drawer.getByLabel('栏目名称', { exact: true })).toHaveValue('业务记录 1');
|
|
302
|
+
await drawer.getByLabel('栏目名称', { exact: true }).fill('我的修改');
|
|
303
|
+
state.records[0].name = '另一个用户的修改';
|
|
304
|
+
state.records[0].revision = 2;
|
|
305
|
+
const reads = state.reads;
|
|
306
|
+
await page.context().setOffline(true);
|
|
307
|
+
await page.context().setOffline(false);
|
|
308
|
+
await expect.poll(() => state.reads).toBeGreaterThan(reads);
|
|
309
|
+
await expect(drawer.getByLabel('栏目名称', { exact: true })).toHaveValue('我的修改');
|
|
310
|
+
await drawer.getByRole('button', { name: '保存' }).click();
|
|
311
|
+
await expect(drawer.getByText('保存失败', { exact: true })).toBeVisible();
|
|
312
|
+
expect(state.writes.at(-1).expectedRevision).toBe(1);
|
|
313
|
+
await expect(drawer.getByLabel('栏目名称', { exact: true })).toHaveValue('我的修改');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('mobile grouped entry keeps one column and saves through the same Data API', async ({ page }, testInfo) => {
|
|
317
|
+
const state = await editablePlatform(page);
|
|
318
|
+
const errors: string[] = [];
|
|
319
|
+
page.on('pageerror', error => errors.push(error.message));
|
|
320
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
321
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
322
|
+
await page.evaluate(() => {
|
|
323
|
+
history.pushState({}, '', '/m/admin/resources/resource-01/new');
|
|
324
|
+
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
325
|
+
});
|
|
326
|
+
const form = page.locator('.oxa-mobile-form');
|
|
327
|
+
await expect(form.getByRole('heading', { name: '栏目设置' })).toBeVisible();
|
|
328
|
+
await expect(page.getByRole('button', { name: '返回列表' })).toHaveCount(0);
|
|
329
|
+
await expect(form.getByRole('button', { name: '取消', exact: true })).toHaveCount(0);
|
|
330
|
+
await form.getByRole('textbox', { name: '栏目名称', exact: true }).fill('移动端新增记录');
|
|
331
|
+
await form.getByRole('textbox', { name: '栏目说明', exact: true }).fill('使用平台移动输入组件');
|
|
332
|
+
expect(await form.locator('.oxa-grid').first().evaluate(element => getComputedStyle(element).gridTemplateColumns.split(' ').length)).toBe(1);
|
|
333
|
+
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
|
|
334
|
+
await page.screenshot({ path: testInfo.outputPath('standard-mobile-form.png'), animations: 'disabled' });
|
|
335
|
+
await form.getByRole('button', { name: '提交', exact: true }).click();
|
|
336
|
+
await expect(page).toHaveURL('/m/admin/resources/resource-01/record-22');
|
|
337
|
+
expect(state.writes).toHaveLength(1);
|
|
338
|
+
expect(state.records.at(-1)?.name).toBe('移动端新增记录');
|
|
339
|
+
expect(errors).toEqual([]);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('advanced grouped conditions, multiple sort priorities and selection-only actions work together', async ({ page }, testInfo) => {
|
|
343
|
+
const state = await editablePlatform(page);
|
|
344
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
345
|
+
await expect(page.getByRole('button', { name: /发布选中/ })).toHaveCount(0);
|
|
346
|
+
await page.locator('tbody').getByRole('checkbox').first().check();
|
|
347
|
+
await expect(page.getByRole('button', { name: '发布选中栏目(1)' })).toBeVisible();
|
|
348
|
+
await page.locator('tbody').getByRole('checkbox').first().uncheck();
|
|
349
|
+
await expect(page.getByRole('button', { name: /发布选中/ })).toHaveCount(0);
|
|
350
|
+
await page.getByRole('button', { name: '筛选', exact: true }).click();
|
|
351
|
+
const filter = page.getByRole('dialog', { name: '筛选' });
|
|
352
|
+
await filter.getByText('满足任一', { exact: true }).click();
|
|
353
|
+
await filter.getByRole('button', { name: '添加条件', exact: true }).click();
|
|
354
|
+
await filter.getByPlaceholder('筛选栏目名称').fill('记录 1');
|
|
355
|
+
await filter.getByRole('button', { name: '添加分组', exact: true }).click();
|
|
356
|
+
const nested = filter.locator('.oxa-condition-nested');
|
|
357
|
+
await chooseOption(page, nested.getByRole('combobox', { name: '筛选字段' }), '启用状态');
|
|
358
|
+
await nested.locator('.oxa-condition-value').getByRole('combobox').click();
|
|
359
|
+
await page.locator('.ant-select-dropdown:visible').getByText('否', { exact: true }).click();
|
|
360
|
+
await nested.getByRole('button', { name: '添加条件', exact: true }).click();
|
|
361
|
+
await chooseOption(page, nested.getByRole('combobox', { name: '筛选字段' }).nth(1), '显示顺序');
|
|
362
|
+
await nested.getByRole('spinbutton').fill('0');
|
|
363
|
+
await page.screenshot({ path: testInfo.outputPath('advanced-condition-groups.png'), animations: 'disabled' });
|
|
364
|
+
await filter.getByRole('button', { name: '应用筛选' }).click();
|
|
365
|
+
await expect.poll(() => state.queries.at(-1)?.where).toEqual({ and: [{ or: [
|
|
366
|
+
{ field: 'name', operator: 'contains', value: '记录 1' },
|
|
367
|
+
{ and: [{ field: 'enabled', operator: 'eq', value: false }, { field: 'displayOrder', operator: 'eq', value: 0 }] },
|
|
368
|
+
] }] });
|
|
369
|
+
await page.getByRole('button', { name: '排序', exact: true }).click();
|
|
370
|
+
const sort = page.getByRole('region', { name: '排序设置' });
|
|
371
|
+
await sort.getByRole('button', { name: '清空', exact: true }).click();
|
|
372
|
+
await sort.getByRole('button', { name: '添加排序规则' }).click();
|
|
373
|
+
await chooseOption(page, sort.getByRole('combobox', { name: '第1排序字段' }), '栏目名称');
|
|
374
|
+
await sort.getByText('降序', { exact: true }).click();
|
|
375
|
+
await sort.getByRole('button', { name: '添加排序规则' }).click();
|
|
376
|
+
await chooseOption(page, sort.getByRole('combobox', { name: '第2排序字段' }), '显示顺序');
|
|
377
|
+
await page.screenshot({ path: testInfo.outputPath('multiple-sort-rules.png'), animations: 'disabled' });
|
|
378
|
+
await sort.getByRole('button', { name: '确定', exact: true }).click();
|
|
379
|
+
await expect.poll(() => state.queries.at(-1)?.order).toEqual([{ field: 'name', direction: 'desc' }, { field: 'displayOrder', direction: 'asc' }]);
|
|
380
|
+
const exported = page.waitForRequest(request => request.url().includes('/export'));
|
|
381
|
+
await page.getByRole('button', { name: '导出', exact: true }).click();
|
|
382
|
+
const body = (await exported).postDataJSON(); expect(body.where).toEqual(state.queries.at(-1).where); expect(body.order).toEqual(state.queries.at(-1).order);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('partial draft save failure retains input; PC draft box resumes and deletes', async ({ page }, testInfo) => {
|
|
386
|
+
const state = await editablePlatform(page);
|
|
387
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
388
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
389
|
+
let drawer = page.getByRole('dialog', { name: '新增数据' });
|
|
390
|
+
await drawer.getByLabel('栏目说明', { exact: true }).fill('必填项尚未完成也可暂存');
|
|
391
|
+
state.failDraft = true;
|
|
392
|
+
await drawer.getByRole('button', { name: '暂存', exact: true }).click();
|
|
393
|
+
await expect(drawer.getByText(/暂存服务暂时不可用/)).toBeVisible();
|
|
394
|
+
await expect(drawer.getByLabel('栏目说明', { exact: true })).toHaveValue('必填项尚未完成也可暂存');
|
|
395
|
+
state.failDraft = false;
|
|
396
|
+
await drawer.getByRole('button', { name: '暂存', exact: true }).click();
|
|
397
|
+
await expect(drawer.getByText('已暂存,可从草稿箱继续编辑')).toBeVisible();
|
|
398
|
+
expect(state.writes).toHaveLength(0); expect(state.drafts).toHaveLength(1);
|
|
399
|
+
await drawer.getByRole('button', { name: '关闭', exact: true }).click();
|
|
400
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
401
|
+
drawer = page.getByRole('dialog', { name: '新增数据' });
|
|
402
|
+
await page.getByRole('button', { name: '载入草稿' }).click();
|
|
403
|
+
await expect(drawer.getByLabel('栏目说明', { exact: true })).toHaveValue('必填项尚未完成也可暂存');
|
|
404
|
+
await drawer.getByRole('button', { name: '草稿箱', exact: true }).click();
|
|
405
|
+
const box = page.getByRole('dialog', { name: /草稿箱/ });
|
|
406
|
+
await expect(box.getByRole('button', { name: '继续编辑' })).toBeVisible();
|
|
407
|
+
await page.screenshot({ path: testInfo.outputPath('desktop-draft-box.png'), animations: 'disabled' });
|
|
408
|
+
await box.getByRole('button', { name: '删除', exact: true }).click();
|
|
409
|
+
await page.getByRole('button', { name: '删除草稿', exact: true }).click();
|
|
410
|
+
await expect(box.getByRole('button', { name: '继续编辑' })).toHaveCount(0);
|
|
411
|
+
expect(state.drafts).toHaveLength(0);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('mobile partial drafts restore from a bottom sheet and submit once', async ({ page }, testInfo) => {
|
|
415
|
+
const state = await editablePlatform(page);
|
|
416
|
+
await page.addInitScript(() => { Object.defineProperty(crypto, 'randomUUID', { value: undefined, configurable: true }); });
|
|
417
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
418
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
419
|
+
await page.evaluate(() => { history.pushState({}, '', '/m/admin/resources/resource-01/new'); window.dispatchEvent(new PopStateEvent('popstate')); });
|
|
420
|
+
const form = page.locator('.oxa-mobile-form');
|
|
421
|
+
await form.getByRole('textbox', { name: '栏目说明', exact: true }).fill('手机暂存内容');
|
|
422
|
+
await form.getByRole('button', { name: '暂存', exact: true }).click();
|
|
423
|
+
await expect(page.getByText('已暂存,可从草稿箱继续编辑')).toBeVisible();
|
|
424
|
+
expect(state.writes).toHaveLength(0);
|
|
425
|
+
await page.reload();
|
|
426
|
+
const prompt = page.getByRole('dialog', { name: '载入暂存数据' });
|
|
427
|
+
await expect(prompt).toBeVisible();
|
|
428
|
+
await expect.poll(async () => { const box = await prompt.boundingBox(); return box ? Math.abs(box.y + box.height - 844) : Infinity; }).toBeLessThan(2);
|
|
429
|
+
expect(await prompt.locator('xpath=..').evaluate(el => el.className)).not.toContain('ant-modal');
|
|
430
|
+
await page.screenshot({ path: testInfo.outputPath('mobile-draft-restore.png'), animations: 'disabled' });
|
|
431
|
+
await prompt.getByRole('button', { name: '载入草稿' }).click();
|
|
432
|
+
await expect(form.getByRole('textbox', { name: '栏目说明', exact: true })).toHaveValue('手机暂存内容');
|
|
433
|
+
await page.getByRole('button', { name: /^草稿箱/ }).click();
|
|
434
|
+
const draftBox = page.getByRole('dialog', { name: /草稿箱/ });
|
|
435
|
+
await expect(draftBox.locator('.oxa-draft-card')).toBeVisible();
|
|
436
|
+
await expect.poll(async () => { const box = await draftBox.boundingBox(); return box ? Math.abs(box.y + box.height - 844) : Infinity; }).toBeLessThan(2);
|
|
437
|
+
await expect(draftBox.getByRole('button', { name: '继续编辑' })).toBeInViewport();
|
|
438
|
+
await page.screenshot({ path: testInfo.outputPath('mobile-draft-box.png'), animations: 'disabled' });
|
|
439
|
+
await page.getByRole('button', { name: '关闭弹层' }).click();
|
|
440
|
+
await form.getByRole('textbox', { name: '栏目名称', exact: true }).fill('手机恢复后提交');
|
|
441
|
+
await form.getByRole('button', { name: '提交', exact: true }).click();
|
|
442
|
+
await expect(page).toHaveURL('/m/admin/resources/resource-01/record-22');
|
|
443
|
+
expect(state.writes).toHaveLength(1); expect(state.drafts).toHaveLength(0);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('direct entry is independent and adapts desktop URLs to touch controls without losing values', async ({ page }) => {
|
|
447
|
+
await editablePlatform(page);
|
|
448
|
+
await page.goto('/admin/resources/resource-01/new');
|
|
449
|
+
await expect(page.locator('.oxa-sider, .oxa-topbar')).toHaveCount(0);
|
|
450
|
+
await expect(page.getByRole('button', { name: '返回列表' })).toHaveCount(0);
|
|
451
|
+
await page.getByRole('textbox', { name: /栏目名称/ }).fill('保留未暂存输入');
|
|
452
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
453
|
+
await expect(page.locator('.oxa-mobile-form')).toBeVisible();
|
|
454
|
+
await expect(page.locator('.oxa-mobile-entry')).toBeVisible();
|
|
455
|
+
await expect(page.locator('.oxa-mobile-entry')).toHaveCSS('padding-left', '0px');
|
|
456
|
+
const header = await page.locator('.oxa-mobile-header').boundingBox();
|
|
457
|
+
const entry = await page.locator('.oxa-mobile-entry').boundingBox();
|
|
458
|
+
expect(header?.x).toBe(entry?.x); expect(header?.width).toBe(entry?.width);
|
|
459
|
+
await expect(page.getByRole('button', { name: /^草稿箱/ })).toHaveCount(0);
|
|
460
|
+
await expect(page.getByRole('textbox', { name: /栏目名称/ })).toHaveValue('保留未暂存输入');
|
|
461
|
+
await expect(page.locator('.oxa-sider, .oxa-topbar')).toHaveCount(0);
|
|
462
|
+
await page.setViewportSize({ width: 1280, height: 900 });
|
|
463
|
+
await expect(page.locator('.oxa-form-full')).toBeVisible();
|
|
464
|
+
await expect(page.getByRole('textbox', { name: /栏目名称/ })).toHaveValue('保留未暂存输入');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('details open readonly in a drawer and closing unsaved entry has no confirmation or write', async ({ page }, testInfo) => {
|
|
468
|
+
const state = await editablePlatform(page);
|
|
469
|
+
await page.goto('/resource-experience.e2e.html?primary=business');
|
|
470
|
+
const initialUrl = page.url();
|
|
471
|
+
await page.getByRole('button', { name: '查看', exact: true }).first().click();
|
|
472
|
+
const detail = page.getByRole('dialog', { name: '内容栏目详情' });
|
|
473
|
+
await expect(detail).toBeVisible();
|
|
474
|
+
await expect(detail.locator('.oxa-record-detail-sections')).toBeVisible();
|
|
475
|
+
await expect(detail.getByRole('heading', { name: '业务记录 1', exact: true })).toBeVisible();
|
|
476
|
+
await expect(detail.getByRole('textbox')).toHaveCount(0);
|
|
477
|
+
await expect(detail.getByRole('button', { name: '提交', exact: true })).toHaveCount(0);
|
|
478
|
+
expect(page.url()).toBe(initialUrl);
|
|
479
|
+
await expect(detail).toHaveCSS('width', '850px');
|
|
480
|
+
await page.screenshot({ path: testInfo.outputPath('ordinary-detail-drawer-850.png'), animations: 'disabled' });
|
|
481
|
+
await detail.getByRole('button', { name: '编辑', exact: true }).first().click();
|
|
482
|
+
const edit = page.getByRole('dialog', { name: '编辑数据' });
|
|
483
|
+
await expect(page.getByRole('dialog')).toHaveCount(1);
|
|
484
|
+
await expect(edit.locator('.oxa-record-detail.is-editing')).toBeVisible();
|
|
485
|
+
await edit.getByLabel('栏目名称', { exact: true }).fill('普通原地编辑');
|
|
486
|
+
await page.screenshot({ path: testInfo.outputPath('ordinary-detail-inline-edit.png'), animations: 'disabled' });
|
|
487
|
+
await edit.getByRole('button', { name: '关闭', exact: true }).click();
|
|
488
|
+
await expect(detail).toBeVisible();
|
|
489
|
+
await expect(detail.getByRole('heading', { name: '业务记录 1', exact: true })).toBeVisible();
|
|
490
|
+
expect(page.url()).toBe(initialUrl);
|
|
491
|
+
await detail.getByRole('button', { name: '关闭', exact: true }).click();
|
|
492
|
+
await page.getByRole('button', { name: '新增', exact: true }).click();
|
|
493
|
+
const entry = page.getByRole('dialog', { name: '新增数据' });
|
|
494
|
+
await entry.getByRole('textbox', { name: /栏目名称/ }).fill('不保存的输入');
|
|
495
|
+
await entry.getByRole('button', { name: '关闭', exact: true }).click();
|
|
496
|
+
await expect(entry).toHaveCount(0);
|
|
497
|
+
await expect(page.getByText('放弃未暂存的修改?', { exact: true })).toHaveCount(0);
|
|
498
|
+
expect(state.writes).toHaveLength(0);
|
|
499
|
+
expect(state.draftWrites).toHaveLength(0);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
test('edit uses current record without prompting for an existing saved draft', async ({ page }) => {
|
|
504
|
+
const state = await editablePlatform(page);
|
|
505
|
+
state.drafts.push({id:'edit-draft', mode:'update', recordId:'record-1', revision:1, recordRevision:1, values:{name:'旧草稿'}, updatedAt:'2026-09-05T07:06:00.000Z'});
|
|
506
|
+
await page.goto('/admin/resources/resource-01/record-1/edit');
|
|
507
|
+
await expect(page.getByRole('textbox', {name:/栏目名称/})).toHaveValue('业务记录 1');
|
|
508
|
+
await expect(page.getByRole('dialog', {name:'载入暂存数据'})).toHaveCount(0);
|
|
509
|
+
await page.getByRole('button', {name:'草稿箱', exact:true}).click();
|
|
510
|
+
await expect(page.getByRole('dialog', {name:/草稿箱/})).toContainText('旧草稿');
|
|
511
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Refine } from '@refinedev/core';
|
|
2
|
+
import { App as AntdApp } from 'antd';
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import ReactDOM from 'react-dom/client';
|
|
5
|
+
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
6
|
+
import {
|
|
7
|
+
createApplicationProvider, GeneratedResourcePage, OpenXiangdaResourceDefinitionsProvider,
|
|
8
|
+
OpenXiangdaUiProvider, OpenXiangdaWorkflowDefinitionsProvider, RuntimeBoundary, WorkflowSubmissionPage,
|
|
9
|
+
type GeneratedResourceDefinition,
|
|
10
|
+
} from 'openxiangda/react';
|
|
11
|
+
import 'openxiangda/react/styles.css';
|
|
12
|
+
import 'openxiangda/mobile/styles.css';
|
|
13
|
+
|
|
14
|
+
const code = 'purchase-orders';
|
|
15
|
+
const capabilityBase = `app:openxiangda-application:data:${code}`;
|
|
16
|
+
const capabilities = {
|
|
17
|
+
read: `${capabilityBase}:read`, create: `${capabilityBase}:create`,
|
|
18
|
+
update: `${capabilityBase}:update`, delete: `${capabilityBase}:delete`,
|
|
19
|
+
};
|
|
20
|
+
const fieldCapabilities = { readCapabilities: [capabilities.read], createCapabilities: [], updateCapabilities: [] };
|
|
21
|
+
const definition: GeneratedResourceDefinition = {
|
|
22
|
+
code, name: '采购申请', capabilities,
|
|
23
|
+
surface: {
|
|
24
|
+
mutationOwner: 'workflow',
|
|
25
|
+
generated: { list: true, detail: true, create: false, update: false, delete: false },
|
|
26
|
+
fields: {
|
|
27
|
+
title: { label: '申请名称', type: 'text.short', widget: 'text', requiredHint: true, list: true, ...fieldCapabilities },
|
|
28
|
+
amount: { label: '申请金额', type: 'number.decimal', widget: 'money', requiredHint: true, list: true, ...fieldCapabilities },
|
|
29
|
+
internalNote: { label: '隐藏字段', type: 'text.short', widget: 'text', ...fieldCapabilities },
|
|
30
|
+
systemValue: { label: '系统字段', type: 'text.short', widget: 'text', system: true, ...fieldCapabilities },
|
|
31
|
+
decision: { label: '审批结论', type: 'text.short', widget: 'readonly', ...fieldCapabilities },
|
|
32
|
+
attachments: { label: '审批附件', type: 'file', widget: 'attachment', ...fieldCapabilities },
|
|
33
|
+
},
|
|
34
|
+
list: { defaultPageSize: 20, fieldOrder: ['title', 'amount'] },
|
|
35
|
+
form: { layout: 'flat', fieldOrder: ['title', 'amount', 'systemValue', 'decision'] },
|
|
36
|
+
detail: { layout: 'flat', fieldOrder: ['title', 'amount', 'decision'] },
|
|
37
|
+
mobile: { enabled: true },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const childCode = 'purchase-lines';
|
|
41
|
+
const childCapability = `app:openxiangda-application:data:${childCode}`;
|
|
42
|
+
const childDefinition: GeneratedResourceDefinition = {
|
|
43
|
+
code: childCode, name: '采购明细',
|
|
44
|
+
capabilities: { read: `${childCapability}:read`, create: `${childCapability}:create`, update: `${childCapability}:update`, delete: `${childCapability}:delete` },
|
|
45
|
+
surface: {
|
|
46
|
+
mutationOwner: 'native', generated: { list: false, detail: false, create: false, update: false, delete: false },
|
|
47
|
+
fields: {
|
|
48
|
+
purchaseId: { label: '主记录', type: 'text.short', widget: 'text', ...fieldCapabilities },
|
|
49
|
+
sortOrder: { label: '顺序', type: 'number.integer', widget: 'number', ...fieldCapabilities },
|
|
50
|
+
name: { label: '品名', type: 'text.short', widget: 'text', requiredHint: true, ...fieldCapabilities },
|
|
51
|
+
quantity: { label: '数量', type: 'number.integer', widget: 'number', requiredHint: true, ...fieldCapabilities },
|
|
52
|
+
hiddenNote: { label: '隐藏明细字段', type: 'text.short', widget: 'text', hidden: true, ...fieldCapabilities },
|
|
53
|
+
detailNote: { label: '仅详情说明', type: 'text.short', widget: 'text', ...fieldCapabilities },
|
|
54
|
+
},
|
|
55
|
+
form: { layout: 'flat', fieldOrder: ['name', 'quantity', 'hiddenNote'] },
|
|
56
|
+
detail: { layout: 'flat', fieldOrder: ['name', 'quantity', 'detailNote', 'hiddenNote'] },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
const definitions = { [code]: definition, [childCode]: childDefinition };
|
|
60
|
+
const workflowDefinitions = [{
|
|
61
|
+
code: 'purchase-approval', title: '采购审批', launch: { mode: 'standalone' },
|
|
62
|
+
subject: { resourceCode: code, factProjection: { amount: 'amount' }, summaryFields: ['amount'] },
|
|
63
|
+
processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
64
|
+
}] as const;
|
|
65
|
+
const query = new URLSearchParams(window.location.search);
|
|
66
|
+
if (query.get('fields') === 'complex') {
|
|
67
|
+
definition.surface.fields.items = {
|
|
68
|
+
label: '采购明细', type: 'subtable', widget: 'subtable',
|
|
69
|
+
subtable: { resourceCode: childCode, foreignKey: 'purchaseId', orderField: 'sortOrder', maxRows: 20 },
|
|
70
|
+
...fieldCapabilities,
|
|
71
|
+
};
|
|
72
|
+
definition.surface.detail!.fieldOrder!.push('items');
|
|
73
|
+
}
|
|
74
|
+
const mode = query.get('mode') || 'list';
|
|
75
|
+
const base = mode === 'mobile-detail' ? '/m/admin/purchases' : '/admin/purchases';
|
|
76
|
+
const paths = { fallback: '/admin', list: base, detail: `${base}/:id` };
|
|
77
|
+
|
|
78
|
+
function Entry() {
|
|
79
|
+
const [completions, setCompletions] = useState<string[]>([]);
|
|
80
|
+
const [render, setRender] = useState(0);
|
|
81
|
+
const [dismissed, setDismissed] = useState(false);
|
|
82
|
+
const location = useLocation();
|
|
83
|
+
if (mode === 'list') return <GeneratedResourcePage resourceCode={code} paths={paths} />;
|
|
84
|
+
if (mode === 'mobile-detail' || mode === 'desktop-detail') return <Routes><Route path={paths.detail} element={<GeneratedResourcePage resourceCode={code} mode="detail" variant={mode === 'mobile-detail' ? 'mobile' : 'desktop'} paths={paths} />} /></Routes>;
|
|
85
|
+
return <>
|
|
86
|
+
<button onClick={() => setRender(value => value + 1)}>重新渲染父页面</button>
|
|
87
|
+
<output data-testid="parent-render">{render}</output>
|
|
88
|
+
<output data-testid="completions">{completions.join(',')}</output>
|
|
89
|
+
<output data-testid="route">{location.pathname}{location.search}</output>
|
|
90
|
+
{!dismissed && <WorkflowSubmissionPage workflowCode="purchase-approval" variant={mode === 'mobile' ? 'mobile' : 'desktop'}
|
|
91
|
+
onDismiss={() => setDismissed(true)} onCompleted={id => setCompletions(values => [...values, id])} />}
|
|
92
|
+
</>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
96
|
+
<React.StrictMode>
|
|
97
|
+
<OpenXiangdaUiProvider><AntdApp><MemoryRouter initialEntries={[(mode === 'mobile-detail' || mode === 'desktop-detail') ? `${paths.list}/record-1` : paths.list]}><RuntimeBoundary>
|
|
98
|
+
<OpenXiangdaResourceDefinitionsProvider definitions={definitions}>
|
|
99
|
+
<OpenXiangdaWorkflowDefinitionsProvider definitions={workflowDefinitions}>
|
|
100
|
+
<Refine dataProvider={createApplicationProvider(definitions)} resources={[{ name: code, list: paths.list, show: paths.detail }]}>
|
|
101
|
+
<Entry />
|
|
102
|
+
</Refine>
|
|
103
|
+
</OpenXiangdaWorkflowDefinitionsProvider>
|
|
104
|
+
</OpenXiangdaResourceDefinitionsProvider>
|
|
105
|
+
</RuntimeBoundary></MemoryRouter></AntdApp></OpenXiangdaUiProvider>
|
|
106
|
+
</React.StrictMode>,
|
|
107
|
+
);
|