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,1265 @@
|
|
|
1
|
+
import { expect, test, type Page } from '@playwright/test';
|
|
2
|
+
import { appCode } from '../../../packages/contracts/src/generated.js';
|
|
3
|
+
|
|
4
|
+
const taskId = '11111111-1111-4111-8111-111111111111';
|
|
5
|
+
const instanceId = '22222222-2222-4222-8222-222222222222';
|
|
6
|
+
const commandId = '55555555-5555-4555-8555-555555555555';
|
|
7
|
+
function envelope(data: unknown, code = 200) {
|
|
8
|
+
return { code, message: code === 200 ? 'success' : 'forbidden', data };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function detailNavigation(custom = false) {
|
|
12
|
+
return custom
|
|
13
|
+
? {
|
|
14
|
+
custom: true,
|
|
15
|
+
desktopPath: `/admin/operations/purchases/${instanceId}?taskId=${taskId}`,
|
|
16
|
+
mobilePath: `/m/purchases/${instanceId}?taskId=${taskId}`,
|
|
17
|
+
}
|
|
18
|
+
: {
|
|
19
|
+
custom: false,
|
|
20
|
+
desktopPath: `/tasks/${taskId}`,
|
|
21
|
+
mobilePath: `/m/tasks/${taskId}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function workflowFixtureUrl(
|
|
26
|
+
initialPath: string,
|
|
27
|
+
options: { base?: string; state?: string; surfaces?: 'custom' | 'error' } = {},
|
|
28
|
+
) {
|
|
29
|
+
const params = new URLSearchParams({ initial: initialPath });
|
|
30
|
+
if (options.base) params.set('base', options.base);
|
|
31
|
+
if (options.state) params.set('state', options.state);
|
|
32
|
+
if (options.surfaces) params.set('surfaces', options.surfaces);
|
|
33
|
+
return `/workflow-experience.e2e.html?${params.toString()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function task(completed = false, customDetail = false) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 'openxiangda.workflow-task/v2',
|
|
39
|
+
engineVersion: '2.0',
|
|
40
|
+
id: taskId,
|
|
41
|
+
instanceId,
|
|
42
|
+
workflowCode: 'purchase-approval',
|
|
43
|
+
businessKey: 'PO-2026-0825',
|
|
44
|
+
nodeId: 'manager-review',
|
|
45
|
+
title: '采购申请审批',
|
|
46
|
+
status: completed ? 'completed' : 'assigned',
|
|
47
|
+
approvalMode: 'single',
|
|
48
|
+
taskKind: 'normal',
|
|
49
|
+
assignedRoleSubjectKey: 'membership:manager',
|
|
50
|
+
activeParticipantId: 'participant-1',
|
|
51
|
+
participants: [],
|
|
52
|
+
originTaskId: null,
|
|
53
|
+
returnSessionId: null,
|
|
54
|
+
version: completed ? 2 : 1,
|
|
55
|
+
dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
|
|
56
|
+
createdAt: '2026-08-25T01:00:00.000Z',
|
|
57
|
+
updatedAt: '2026-08-25T01:10:00.000Z',
|
|
58
|
+
detailNavigation: detailNavigation(customDetail),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function surface(completed = false, customDetail = false) {
|
|
63
|
+
return {
|
|
64
|
+
schemaVersion: 'openxiangda.workflow-surface/v2',
|
|
65
|
+
protocolVersion: 'workflow_surface_v2',
|
|
66
|
+
surfaceRevision: (completed ? 'b' : 'a').repeat(64),
|
|
67
|
+
engineVersion: '2.0',
|
|
68
|
+
commandToken: 'A'.repeat(43),
|
|
69
|
+
commandTokenExpiresAt: '2099-08-29T09:05:00.000Z',
|
|
70
|
+
instanceSequence: completed ? 4 : 3,
|
|
71
|
+
detailNavigation: detailNavigation(customDetail),
|
|
72
|
+
navigationTarget: {
|
|
73
|
+
kind: 'resource_record',
|
|
74
|
+
appCode,
|
|
75
|
+
environmentKey: 'preproduction',
|
|
76
|
+
resourceCode: 'purchase-orders',
|
|
77
|
+
recordId: 'purchase-1',
|
|
78
|
+
desktopPath: '/purchase-orders/purchase-1',
|
|
79
|
+
mobilePath: '/m/purchase-orders/purchase-1',
|
|
80
|
+
},
|
|
81
|
+
instance: {
|
|
82
|
+
schemaVersion: 'openxiangda.workflow-instance/v2',
|
|
83
|
+
engineVersion: '2.0',
|
|
84
|
+
id: instanceId,
|
|
85
|
+
appCode,
|
|
86
|
+
environmentId: 'preproduction-id',
|
|
87
|
+
environmentKey: 'preproduction',
|
|
88
|
+
workflowCode: 'purchase-approval',
|
|
89
|
+
definitionVersion: 1,
|
|
90
|
+
bindingVersion: 1,
|
|
91
|
+
businessKey: 'PO-2026-0825',
|
|
92
|
+
generation: 1,
|
|
93
|
+
status: completed ? 'approved' : 'running',
|
|
94
|
+
initiatorUserId: 'applicant-user',
|
|
95
|
+
initiatorAuthorizationDigest: null,
|
|
96
|
+
dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
|
|
97
|
+
dataRevision: '8',
|
|
98
|
+
currentNodeId: completed ? null : 'manager-review',
|
|
99
|
+
outcome: completed ? 'approved' : null,
|
|
100
|
+
version: completed ? 3 : 2,
|
|
101
|
+
eventSequence: completed ? 4 : 3,
|
|
102
|
+
startedAt: '2026-08-25T01:00:00.000Z',
|
|
103
|
+
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
104
|
+
},
|
|
105
|
+
task: completed ? null : task(false, customDetail),
|
|
106
|
+
presentation: {
|
|
107
|
+
status: { label: completed ? '已同意' : '待审批', tone: 'warning' },
|
|
108
|
+
layout: 'approval_detail',
|
|
109
|
+
businessData: {
|
|
110
|
+
status: 'fresh',
|
|
111
|
+
resourceCode: 'purchase-orders',
|
|
112
|
+
recordId: 'purchase-1',
|
|
113
|
+
requestedRevision: 8,
|
|
114
|
+
sourceRevision: 8,
|
|
115
|
+
fields: { amount: 28600 },
|
|
116
|
+
projectionDigest: 'c'.repeat(64),
|
|
117
|
+
},
|
|
118
|
+
businessDetail: {
|
|
119
|
+
status: 'stale',
|
|
120
|
+
resourceCode: 'purchase-orders',
|
|
121
|
+
resourceName: '采购申请',
|
|
122
|
+
recordId: 'purchase-1',
|
|
123
|
+
requestedRevision: 8,
|
|
124
|
+
sourceRevision: 9,
|
|
125
|
+
surface: {
|
|
126
|
+
mutationOwner: 'workflow',
|
|
127
|
+
fields: {
|
|
128
|
+
amount: {
|
|
129
|
+
label: '申请金额',
|
|
130
|
+
type: 'number.decimal',
|
|
131
|
+
widget: 'money',
|
|
132
|
+
section: '申请信息',
|
|
133
|
+
readCapabilities: [],
|
|
134
|
+
createCapabilities: [],
|
|
135
|
+
updateCapabilities: [],
|
|
136
|
+
},
|
|
137
|
+
internalVenue: {
|
|
138
|
+
label: '技术场地引用',
|
|
139
|
+
type: 'reference',
|
|
140
|
+
widget: 'reference',
|
|
141
|
+
section: '系统信息',
|
|
142
|
+
system: true,
|
|
143
|
+
readCapabilities: [],
|
|
144
|
+
createCapabilities: [],
|
|
145
|
+
updateCapabilities: [],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
detail: {
|
|
149
|
+
layout: 'sections',
|
|
150
|
+
fieldOrder: ['amount', 'internalVenue'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
record: {
|
|
154
|
+
amount: 30000,
|
|
155
|
+
internalVenue: '77777777-7777-4777-8777-777777777777',
|
|
156
|
+
},
|
|
157
|
+
subtables: {},
|
|
158
|
+
projectionDigest: 'd'.repeat(64),
|
|
159
|
+
},
|
|
160
|
+
summary: {
|
|
161
|
+
title: '采购申请审批',
|
|
162
|
+
initiatorDisplayName: '张三',
|
|
163
|
+
departmentDisplayName: '理学院',
|
|
164
|
+
submittedAt: '2026-08-25T01:00:00.000Z',
|
|
165
|
+
businessNumber: 'PO-2026-0825',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
fieldPolicy: { default: 'readonly', fields: {} },
|
|
169
|
+
operations: completed
|
|
170
|
+
? []
|
|
171
|
+
: [
|
|
172
|
+
{
|
|
173
|
+
key: 'approve',
|
|
174
|
+
kind: 'workflow_command',
|
|
175
|
+
label: '同意',
|
|
176
|
+
group: 'decision',
|
|
177
|
+
audience: 'participant',
|
|
178
|
+
placement: 'primary',
|
|
179
|
+
emphasis: 'primary',
|
|
180
|
+
tone: 'primary',
|
|
181
|
+
visible: true,
|
|
182
|
+
enabled: true,
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: 'object',
|
|
185
|
+
properties: {
|
|
186
|
+
comment: {
|
|
187
|
+
type: 'string',
|
|
188
|
+
title: '审批意见',
|
|
189
|
+
format: 'textarea',
|
|
190
|
+
maxLength: 500,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
uiSchema: {
|
|
195
|
+
confirmText: '确认同意',
|
|
196
|
+
confirmation: { description: '提交后流程继续流转。' },
|
|
197
|
+
},
|
|
198
|
+
execute: {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
href: `/openxiangda-api/v2/applications/${appCode}/workflow/tasks/${taskId}/commands/approve`,
|
|
201
|
+
idempotencyRequired: true,
|
|
202
|
+
},
|
|
203
|
+
refresh: ['surface', 'timeline', 'work_center'],
|
|
204
|
+
},
|
|
205
|
+
...['reject', 'return', 'transfer', 'add_assignee'].map(key => ({
|
|
206
|
+
key,
|
|
207
|
+
kind: 'workflow_command',
|
|
208
|
+
label: {
|
|
209
|
+
reject: '拒绝',
|
|
210
|
+
return: '退回',
|
|
211
|
+
transfer: '转交',
|
|
212
|
+
add_assignee: '加签',
|
|
213
|
+
}[key],
|
|
214
|
+
group: key === 'reject' ? 'decision' : 'task_collaboration',
|
|
215
|
+
audience: 'participant',
|
|
216
|
+
placement: key === 'reject' ? 'primary' : 'overflow',
|
|
217
|
+
emphasis:
|
|
218
|
+
key === 'reject'
|
|
219
|
+
? 'danger'
|
|
220
|
+
: key === 'return'
|
|
221
|
+
? 'warning'
|
|
222
|
+
: 'neutral',
|
|
223
|
+
tone: key === 'reject' ? 'danger' : 'neutral',
|
|
224
|
+
visible: true,
|
|
225
|
+
enabled: true,
|
|
226
|
+
inputSchema: { type: 'object', properties: {} },
|
|
227
|
+
uiSchema: { confirmText: `确认${key}` },
|
|
228
|
+
execute: {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
href: `/openxiangda-api/v2/applications/${appCode}/workflow/tasks/${taskId}/commands/${key}`,
|
|
231
|
+
idempotencyRequired: true,
|
|
232
|
+
},
|
|
233
|
+
refresh: ['surface', 'timeline', 'work_center'],
|
|
234
|
+
})),
|
|
235
|
+
],
|
|
236
|
+
extensions: {},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function timeline(completed = false) {
|
|
241
|
+
const flow = [
|
|
242
|
+
{
|
|
243
|
+
key: 'start',
|
|
244
|
+
nodeId: null,
|
|
245
|
+
kind: 'start',
|
|
246
|
+
title: '张三',
|
|
247
|
+
status: 'completed',
|
|
248
|
+
startedAt: '2026-08-25T01:00:00.000Z',
|
|
249
|
+
completedAt: '2026-08-25T01:00:00.000Z',
|
|
250
|
+
assignees: [],
|
|
251
|
+
operations: [],
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
key: 'manager',
|
|
255
|
+
nodeId: 'manager-review',
|
|
256
|
+
kind: 'approval',
|
|
257
|
+
title: '部门负责人',
|
|
258
|
+
status: completed ? 'completed' : 'active',
|
|
259
|
+
startedAt: '2026-08-25T01:01:00.000Z',
|
|
260
|
+
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
261
|
+
assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
|
|
262
|
+
operations: [{
|
|
263
|
+
id: 'operation-transfer',
|
|
264
|
+
operation: 'transfer',
|
|
265
|
+
operationLabel: '转交',
|
|
266
|
+
actorUserId: 'acceptance-user',
|
|
267
|
+
actorDisplayName: '李明',
|
|
268
|
+
actingForUserId: null,
|
|
269
|
+
reason: '由值班负责人继续处理',
|
|
270
|
+
detail: {},
|
|
271
|
+
severity: 'normal',
|
|
272
|
+
createdAt: '2026-08-25T01:08:00.000Z',
|
|
273
|
+
}],
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
key: 'finance',
|
|
277
|
+
nodeId: 'finance-review',
|
|
278
|
+
kind: 'approval',
|
|
279
|
+
title: '财务负责人',
|
|
280
|
+
status: completed ? 'completed' : 'waiting',
|
|
281
|
+
startedAt: null,
|
|
282
|
+
completedAt: null,
|
|
283
|
+
assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
|
|
284
|
+
operations: [],
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
const items = [{
|
|
288
|
+
id: 'operation-transfer',
|
|
289
|
+
operation: 'transfer',
|
|
290
|
+
operationLabel: '转交',
|
|
291
|
+
actorUserId: 'acceptance-user',
|
|
292
|
+
actorDisplayName: '李明',
|
|
293
|
+
actingForUserId: null,
|
|
294
|
+
reason: '由值班负责人继续处理',
|
|
295
|
+
detail: {},
|
|
296
|
+
severity: 'normal',
|
|
297
|
+
createdAt: '2026-08-25T01:08:00.000Z',
|
|
298
|
+
}];
|
|
299
|
+
const display = {
|
|
300
|
+
entries: [
|
|
301
|
+
{
|
|
302
|
+
key: 'start',
|
|
303
|
+
kind: 'submission',
|
|
304
|
+
nodeId: null,
|
|
305
|
+
nodeKind: 'start',
|
|
306
|
+
title: '提交申请',
|
|
307
|
+
status: 'completed',
|
|
308
|
+
enteredAt: '2026-08-25T01:00:00.000Z',
|
|
309
|
+
leftAt: '2026-08-25T01:00:00.000Z',
|
|
310
|
+
primaryDisplayTime: '2026-08-25T01:00:00.000Z',
|
|
311
|
+
people: [{
|
|
312
|
+
userId: 'applicant-user',
|
|
313
|
+
displayName: '张三',
|
|
314
|
+
avatarUrl: null,
|
|
315
|
+
departmentDisplayName: '理学院',
|
|
316
|
+
}],
|
|
317
|
+
operations: [],
|
|
318
|
+
result: {},
|
|
319
|
+
terminalReason: null,
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
key: 'manager',
|
|
323
|
+
kind: 'node',
|
|
324
|
+
nodeId: 'manager-review',
|
|
325
|
+
nodeKind: 'approval',
|
|
326
|
+
title: '部门负责人',
|
|
327
|
+
status: completed ? 'completed' : 'active',
|
|
328
|
+
enteredAt: '2026-08-25T01:01:00.000Z',
|
|
329
|
+
leftAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
330
|
+
primaryDisplayTime: '2026-08-25T01:08:00.000Z',
|
|
331
|
+
people: [],
|
|
332
|
+
operations: [{
|
|
333
|
+
id: 'operation-transfer',
|
|
334
|
+
operation: 'transfer',
|
|
335
|
+
operationLabel: '转交',
|
|
336
|
+
actor: {
|
|
337
|
+
userId: 'acceptance-user',
|
|
338
|
+
displayName: '李明',
|
|
339
|
+
avatarUrl: null,
|
|
340
|
+
departmentDisplayName: null,
|
|
341
|
+
},
|
|
342
|
+
actingForUserId: null,
|
|
343
|
+
reason: '由值班负责人继续处理',
|
|
344
|
+
severity: 'normal',
|
|
345
|
+
occurredAt: '2026-08-25T01:08:00.000Z',
|
|
346
|
+
}],
|
|
347
|
+
result: {},
|
|
348
|
+
terminalReason: null,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
key: 'finance',
|
|
352
|
+
kind: completed ? 'node' : 'planned_node',
|
|
353
|
+
nodeId: 'finance-review',
|
|
354
|
+
nodeKind: 'approval',
|
|
355
|
+
title: '财务负责人',
|
|
356
|
+
status: completed ? 'completed' : 'waiting',
|
|
357
|
+
enteredAt: null,
|
|
358
|
+
leftAt: null,
|
|
359
|
+
primaryDisplayTime: null,
|
|
360
|
+
people: [{
|
|
361
|
+
userId: 'finance-user',
|
|
362
|
+
displayName: '财务负责人',
|
|
363
|
+
avatarUrl: null,
|
|
364
|
+
departmentDisplayName: null,
|
|
365
|
+
}],
|
|
366
|
+
operations: [],
|
|
367
|
+
result: {},
|
|
368
|
+
terminalReason: null,
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
engineVersion: '2.0',
|
|
374
|
+
instanceId,
|
|
375
|
+
instanceSequence: completed ? 4 : 3,
|
|
376
|
+
timelineRevision: (completed ? 'f' : 'e').repeat(64),
|
|
377
|
+
flow,
|
|
378
|
+
items,
|
|
379
|
+
display,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function detail(completed = false, customDetail = false) {
|
|
384
|
+
const currentSurface = surface(completed, customDetail);
|
|
385
|
+
const currentTimeline = timeline(completed);
|
|
386
|
+
return {
|
|
387
|
+
schemaVersion: 'openxiangda.workflow-detail-surface/v2',
|
|
388
|
+
protocolVersion: 'workflow_detail_surface_v2',
|
|
389
|
+
detailRevision: (completed ? '2' : '1').repeat(64),
|
|
390
|
+
instanceSequence: currentSurface.instanceSequence,
|
|
391
|
+
timelineRevision: currentTimeline.timelineRevision,
|
|
392
|
+
surface: currentSurface,
|
|
393
|
+
timeline: currentTimeline,
|
|
394
|
+
currentStatus: {
|
|
395
|
+
code: completed ? 'approved' : 'assigned',
|
|
396
|
+
label: completed ? '已同意' : '待审批',
|
|
397
|
+
tone: completed ? 'success' : 'warning',
|
|
398
|
+
},
|
|
399
|
+
nodes: currentTimeline.display.entries,
|
|
400
|
+
handlingRecords: currentTimeline.display.entries.flatMap(
|
|
401
|
+
entry => entry.operations
|
|
402
|
+
),
|
|
403
|
+
operations: currentSurface.operations,
|
|
404
|
+
navigationContext: {
|
|
405
|
+
desktopReturnPath: '/admin/work-center',
|
|
406
|
+
mobileReturnPath: '/m/work-center',
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function launchSurface() {
|
|
412
|
+
return {
|
|
413
|
+
schemaVersion: 'openxiangda.workflow-launch-surface/v3',
|
|
414
|
+
protocolVersion: 'workflow_launch_surface_v3',
|
|
415
|
+
surfaceRevision: 'launch-revision-1',
|
|
416
|
+
engineVersion: '2.0',
|
|
417
|
+
appCode,
|
|
418
|
+
environmentKey: 'preproduction',
|
|
419
|
+
environmentId: 'preproduction-id',
|
|
420
|
+
workflowCode: 'purchase-approval',
|
|
421
|
+
title: '采购审批',
|
|
422
|
+
launchMode: 'standalone',
|
|
423
|
+
processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
424
|
+
subject: {
|
|
425
|
+
resourceCode: 'purchase-orders',
|
|
426
|
+
factProjection: { amount: 'amount' },
|
|
427
|
+
summaryFields: ['amount'],
|
|
428
|
+
},
|
|
429
|
+
inputSchema: { type: 'object', properties: {} },
|
|
430
|
+
head: {
|
|
431
|
+
workflowRevision: 4,
|
|
432
|
+
definitionVersion: 1,
|
|
433
|
+
bindingVersion: 1,
|
|
434
|
+
nativeRevision: 3,
|
|
435
|
+
contractRevisionId: 'contract-revision-1',
|
|
436
|
+
},
|
|
437
|
+
paths: {
|
|
438
|
+
desktop: '/workflows/purchase-approval/start',
|
|
439
|
+
mobile: '/m/workflows/purchase-approval/start',
|
|
440
|
+
},
|
|
441
|
+
commit: {
|
|
442
|
+
method: 'POST',
|
|
443
|
+
href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
|
|
444
|
+
idempotencyRequired: true,
|
|
445
|
+
},
|
|
446
|
+
submission: {
|
|
447
|
+
kind: 'standard-process',
|
|
448
|
+
processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
449
|
+
commit: {
|
|
450
|
+
method: 'POST',
|
|
451
|
+
href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
|
|
452
|
+
idempotencyRequired: true,
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function processCommand(status: 'accepted' | 'started' = 'started') {
|
|
459
|
+
return {
|
|
460
|
+
schemaVersion: 'openxiangda.business-process.command/v2',
|
|
461
|
+
id: commandId,
|
|
462
|
+
appCode,
|
|
463
|
+
environmentKey: 'preproduction',
|
|
464
|
+
operationCode: 'openxiangda.workflow.purchase-approval.submit',
|
|
465
|
+
idempotencyKey: 'process:purchase-approval:test',
|
|
466
|
+
workflowCode: 'purchase-approval',
|
|
467
|
+
status,
|
|
468
|
+
revision: status === 'started' ? 4 : 1,
|
|
469
|
+
subject: {
|
|
470
|
+
resourceCode: 'purchase-orders',
|
|
471
|
+
id: '66666666-6666-4666-8666-666666666666',
|
|
472
|
+
dataRevision: 1,
|
|
473
|
+
factDigest: 'a'.repeat(64),
|
|
474
|
+
},
|
|
475
|
+
definitionVersion: 1,
|
|
476
|
+
bindingVersion: 1,
|
|
477
|
+
requirements: [],
|
|
478
|
+
answers: {},
|
|
479
|
+
preview: {},
|
|
480
|
+
workflowInstanceId: status === 'started' ? instanceId : null,
|
|
481
|
+
attemptCount: 1,
|
|
482
|
+
lastError: null,
|
|
483
|
+
replayed: false,
|
|
484
|
+
createdAt: '2026-08-29T01:00:00.000Z',
|
|
485
|
+
updatedAt: '2026-08-29T01:00:01.000Z',
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function processCommandSurface() {
|
|
490
|
+
const command = processCommand('started');
|
|
491
|
+
const base = `/openxiangda-api/v2/applications/${appCode}/business-process/commands/${commandId}`;
|
|
492
|
+
return {
|
|
493
|
+
schemaVersion: 'openxiangda.process-command-surface/v2',
|
|
494
|
+
surfaceRevision: 'b'.repeat(64),
|
|
495
|
+
command,
|
|
496
|
+
subject: {
|
|
497
|
+
resourceCode: 'purchase-orders',
|
|
498
|
+
recordId: command.subject.id,
|
|
499
|
+
dataRevision: 1,
|
|
500
|
+
form: {
|
|
501
|
+
kind: 'native-resource-form',
|
|
502
|
+
resourceCode: 'purchase-orders',
|
|
503
|
+
recordId: command.subject.id,
|
|
504
|
+
},
|
|
505
|
+
},
|
|
506
|
+
requirements: [],
|
|
507
|
+
resume: {
|
|
508
|
+
status: { method: 'GET', href: base },
|
|
509
|
+
surface: { method: 'GET', href: `${base}/surface` },
|
|
510
|
+
answer: null,
|
|
511
|
+
retry: null,
|
|
512
|
+
navigationTarget: {
|
|
513
|
+
kind: 'PLATFORM_ROUTE',
|
|
514
|
+
routeCode: 'resource.record',
|
|
515
|
+
appCode,
|
|
516
|
+
pathParams: {
|
|
517
|
+
resourceCode: 'purchase-orders',
|
|
518
|
+
recordId: command.subject.id,
|
|
519
|
+
},
|
|
520
|
+
query: { processCommandId: commandId },
|
|
521
|
+
access: 'AUTHENTICATED',
|
|
522
|
+
},
|
|
523
|
+
desktop: `/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
|
|
524
|
+
mobile: `/m/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function todoPage(offset = 0, view = 'all') {
|
|
530
|
+
const first = {
|
|
531
|
+
messageId: '33333333-3333-4333-8333-333333333333',
|
|
532
|
+
recipientId: '44444444-4444-4444-8444-444444444444',
|
|
533
|
+
state: 'action_required',
|
|
534
|
+
interactionState: 'unread',
|
|
535
|
+
title: '设备采购申请待审批',
|
|
536
|
+
summary: '张三提交了一笔办公设备采购申请',
|
|
537
|
+
fields: [
|
|
538
|
+
{ code: 'amount', label: '申请金额', value: '¥28,600' },
|
|
539
|
+
{ code: 'department', label: '申请部门', value: '理学院' },
|
|
540
|
+
],
|
|
541
|
+
actions: [{ code: 'open', label: '去处理' }],
|
|
542
|
+
sourceKind: 'PLATFORM_EVENT',
|
|
543
|
+
sourceLabel: '流程中心',
|
|
544
|
+
occurredAt: '2026-08-28T01:00:00.000Z',
|
|
545
|
+
updatedAt: '2026-08-28T01:01:00.000Z',
|
|
546
|
+
navigation: {
|
|
547
|
+
desktopPath: `/admin/operations/purchases/${instanceId}?taskId=${taskId}`,
|
|
548
|
+
mobilePath: `/m/purchases/${instanceId}?taskId=${taskId}`,
|
|
549
|
+
external: false,
|
|
550
|
+
navigationUnavailable: false,
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
const second = {
|
|
554
|
+
...first,
|
|
555
|
+
messageId: '77777777-7777-4777-8777-777777777777',
|
|
556
|
+
title: '采购结果通知',
|
|
557
|
+
interactionState: 'read',
|
|
558
|
+
};
|
|
559
|
+
return {
|
|
560
|
+
schemaVersion: 'openxiangda.application-todo-center/v2',
|
|
561
|
+
appCode,
|
|
562
|
+
environmentKey: 'preproduction',
|
|
563
|
+
view,
|
|
564
|
+
counts: { pending: 1, informational: 2, completed: 3, unread: 1 },
|
|
565
|
+
items: [offset > 0 ? second : first],
|
|
566
|
+
total: 2,
|
|
567
|
+
limit: 12,
|
|
568
|
+
offset,
|
|
569
|
+
nextOffset: offset > 0 ? null : 1,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function mockWorkflow(
|
|
574
|
+
page: Page,
|
|
575
|
+
options: {
|
|
576
|
+
commandConflict?: boolean;
|
|
577
|
+
forbidden?: boolean;
|
|
578
|
+
customDetail?: boolean;
|
|
579
|
+
} = {}
|
|
580
|
+
) {
|
|
581
|
+
let completed = false;
|
|
582
|
+
await page.route('**/service/**', async route => {
|
|
583
|
+
const url = new URL(route.request().url());
|
|
584
|
+
const path = url.pathname;
|
|
585
|
+
if (path.endsWith('/auth/surface')) {
|
|
586
|
+
return route.fulfill({
|
|
587
|
+
contentType: 'application/json',
|
|
588
|
+
body: JSON.stringify(envelope({ csrfToken: 'workflow-csrf-1' })),
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
if (path.endsWith('/native/authz/current')) {
|
|
592
|
+
return route.fulfill({
|
|
593
|
+
contentType: 'application/json',
|
|
594
|
+
body: JSON.stringify(
|
|
595
|
+
envelope({
|
|
596
|
+
schemaVersion: 'openxiangda.runtime-authorization/v2',
|
|
597
|
+
state: 'active',
|
|
598
|
+
environment: {
|
|
599
|
+
id: 'preproduction-id',
|
|
600
|
+
key: 'preproduction',
|
|
601
|
+
activeAppVersionId: 'version-1',
|
|
602
|
+
headRevision: 1,
|
|
603
|
+
authzRevisionId: 'authz-1',
|
|
604
|
+
authzVersion: 1,
|
|
605
|
+
scopeDataVersion: 'scope-1',
|
|
606
|
+
},
|
|
607
|
+
subjectProfile: {
|
|
608
|
+
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
609
|
+
userId: 'acceptance-user',
|
|
610
|
+
displayName: '王老师',
|
|
611
|
+
avatarUrl: null,
|
|
612
|
+
jobNumber: 'T001',
|
|
613
|
+
affiliatedDepartment: { id: 'department-1', name: '理学院' },
|
|
614
|
+
},
|
|
615
|
+
roles: [
|
|
616
|
+
{
|
|
617
|
+
code: 'department_manager',
|
|
618
|
+
name: '部门管理员',
|
|
619
|
+
source: 'package',
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
principal: {
|
|
623
|
+
type: 'user_union',
|
|
624
|
+
userId: 'acceptance-user',
|
|
625
|
+
roleCodes: ['department_manager'],
|
|
626
|
+
capabilityCodes: [],
|
|
627
|
+
isAppSuperAdmin: true,
|
|
628
|
+
identityScope:
|
|
629
|
+
'user-union:preproduction-id:acceptance-user:authz-1',
|
|
630
|
+
},
|
|
631
|
+
})
|
|
632
|
+
),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
if (path.includes('/directory/')) {
|
|
636
|
+
return route.fulfill({
|
|
637
|
+
contentType: 'application/json',
|
|
638
|
+
body: JSON.stringify(envelope({ items: [], nextCursor: null })),
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
if (options.forbidden && path.includes('/workflow/')) {
|
|
642
|
+
return route.fulfill({
|
|
643
|
+
status: 403,
|
|
644
|
+
contentType: 'application/json',
|
|
645
|
+
body: JSON.stringify(envelope(null, 403)),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
if (path.endsWith('/todos') && route.request().method() === 'GET') {
|
|
649
|
+
return route.fulfill({
|
|
650
|
+
contentType: 'application/json',
|
|
651
|
+
body: JSON.stringify(
|
|
652
|
+
envelope(todoPage(Number(url.searchParams.get('offset') || 0), url.searchParams.get('view') || 'all')),
|
|
653
|
+
),
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (path.includes('/todos/') && path.endsWith('/interactions')) {
|
|
657
|
+
return route.fulfill({
|
|
658
|
+
contentType: 'application/json',
|
|
659
|
+
body: JSON.stringify(
|
|
660
|
+
envelope({
|
|
661
|
+
accepted: true,
|
|
662
|
+
duplicate: false,
|
|
663
|
+
interactionState: 'clicked',
|
|
664
|
+
})
|
|
665
|
+
),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
if (
|
|
669
|
+
path.endsWith(
|
|
670
|
+
'/workflow/definitions/purchase-approval/launch-surface'
|
|
671
|
+
)
|
|
672
|
+
) {
|
|
673
|
+
return route.fulfill({
|
|
674
|
+
contentType: 'application/json',
|
|
675
|
+
body: JSON.stringify(envelope(launchSurface())),
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
if (
|
|
679
|
+
path.endsWith('/business-process/standard-commands') &&
|
|
680
|
+
route.request().method() === 'POST'
|
|
681
|
+
) {
|
|
682
|
+
const body = route.request().postDataJSON();
|
|
683
|
+
expect(body).toMatchObject({
|
|
684
|
+
schemaVersion: 'openxiangda.standard-process.commit/v2',
|
|
685
|
+
environmentKey: 'preproduction',
|
|
686
|
+
workflowCode: 'purchase-approval',
|
|
687
|
+
mutation: { kind: 'create', data: { amount: 28600 } },
|
|
688
|
+
});
|
|
689
|
+
for (const forbidden of [
|
|
690
|
+
'facts',
|
|
691
|
+
'dataRef',
|
|
692
|
+
'preparationToken',
|
|
693
|
+
'processOperationCode',
|
|
694
|
+
]) {
|
|
695
|
+
expect(body).not.toHaveProperty(forbidden);
|
|
696
|
+
}
|
|
697
|
+
return route.fulfill({
|
|
698
|
+
contentType: 'application/json',
|
|
699
|
+
body: JSON.stringify(envelope(processCommand('accepted'))),
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
if (
|
|
703
|
+
path.endsWith(`/business-process/commands/${commandId}/surface`) &&
|
|
704
|
+
route.request().method() === 'GET'
|
|
705
|
+
) {
|
|
706
|
+
return route.fulfill({
|
|
707
|
+
contentType: 'application/json',
|
|
708
|
+
body: JSON.stringify(envelope(processCommandSurface())),
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
if (path.endsWith('/workflow/work-center/items')) {
|
|
712
|
+
const view = url.searchParams.get('view') || 'pending';
|
|
713
|
+
return route.fulfill({
|
|
714
|
+
contentType: 'application/json',
|
|
715
|
+
body: JSON.stringify(
|
|
716
|
+
envelope({
|
|
717
|
+
engineVersion: '2.0',
|
|
718
|
+
authorizationMode: 'current_user_role_union',
|
|
719
|
+
authorizationDigest: 'digest-1',
|
|
720
|
+
view,
|
|
721
|
+
counts: { created: 1, pending: completed ? 0 : 1, handled: completed ? 1 : 0, cc: 1 },
|
|
722
|
+
total: 1,
|
|
723
|
+
limit: 20,
|
|
724
|
+
offset: 0,
|
|
725
|
+
items: [{ ...task(completed, options.customDetail), id: view === 'pending' ? taskId : instanceId, itemKind: view === 'pending' ? 'task' : 'instance', instanceId, taskId: view === 'pending' ? taskId : null, workflowTitle: '采购审批', taskTitle: '负责人审批', instanceStatus: completed ? 'approved' : 'running', resourceCode: 'purchase-orders', recordId: 'purchase-1', occurredAt: '2026-08-25T01:00:00.000Z' }],
|
|
726
|
+
})
|
|
727
|
+
),
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
|
|
731
|
+
return route.fulfill({
|
|
732
|
+
contentType: 'application/json',
|
|
733
|
+
body: JSON.stringify(
|
|
734
|
+
envelope(surface(completed, options.customDetail))
|
|
735
|
+
),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (
|
|
739
|
+
path.endsWith(`/workflow/tasks/${taskId}/detail`) ||
|
|
740
|
+
path.endsWith(`/workflow/instances/${instanceId}/detail`)
|
|
741
|
+
) {
|
|
742
|
+
expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
|
|
743
|
+
'workflow-csrf-1'
|
|
744
|
+
);
|
|
745
|
+
return route.fulfill({
|
|
746
|
+
contentType: 'application/json',
|
|
747
|
+
body: JSON.stringify(
|
|
748
|
+
envelope(detail(completed, options.customDetail))
|
|
749
|
+
),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
if (path.endsWith(`/workflow/instances/${instanceId}/surface`)) {
|
|
753
|
+
return route.fulfill({
|
|
754
|
+
contentType: 'application/json',
|
|
755
|
+
body: JSON.stringify(
|
|
756
|
+
envelope(surface(completed, options.customDetail))
|
|
757
|
+
),
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
|
|
761
|
+
return route.fulfill({
|
|
762
|
+
contentType: 'application/json',
|
|
763
|
+
body: JSON.stringify(envelope(timeline(completed))),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
|
|
767
|
+
expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
|
|
768
|
+
'workflow-csrf-1'
|
|
769
|
+
);
|
|
770
|
+
expect(route.request().postDataJSON()).toMatchObject({
|
|
771
|
+
commandToken: 'A'.repeat(43),
|
|
772
|
+
input: { comment: '同意采购' },
|
|
773
|
+
});
|
|
774
|
+
expect(route.request().postDataJSON()).not.toHaveProperty(
|
|
775
|
+
'expectedTaskVersion'
|
|
776
|
+
);
|
|
777
|
+
expect(route.request().postDataJSON()).not.toHaveProperty(
|
|
778
|
+
'expectedInstanceVersion'
|
|
779
|
+
);
|
|
780
|
+
if (options.commandConflict) {
|
|
781
|
+
return route.fulfill({
|
|
782
|
+
status: 409,
|
|
783
|
+
contentType: 'application/json',
|
|
784
|
+
body: JSON.stringify(
|
|
785
|
+
envelope({ freshSurface: surface(false, options.customDetail) }, 409)
|
|
786
|
+
),
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
completed = true;
|
|
790
|
+
return route.fulfill({
|
|
791
|
+
contentType: 'application/json',
|
|
792
|
+
body: JSON.stringify(envelope({ status: 'completed' })),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return route.fulfill({
|
|
796
|
+
status: 404,
|
|
797
|
+
contentType: 'application/json',
|
|
798
|
+
body: JSON.stringify(envelope(null, 404)),
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
test('renders a paged desktop work center from the current-user role union', async ({
|
|
804
|
+
page,
|
|
805
|
+
}) => {
|
|
806
|
+
await mockWorkflow(page);
|
|
807
|
+
await page.goto('/workflow-experience.e2e.html?initial=/work-center');
|
|
808
|
+
await expect(page.getByRole('heading', { name: '待办中心' })).toBeVisible();
|
|
809
|
+
await expect(page.getByRole('button', { name: '采购申请审批' })).toBeVisible();
|
|
810
|
+
await expect(page.getByText('采购审批', { exact: true })).toBeVisible();
|
|
811
|
+
for (const [view, label] of [['created', '我创建的'], ['handled', '我已处理'], ['cc', '抄送我的'], ['pending', '待我审批']]) {
|
|
812
|
+
const request = page.waitForRequest(request => new URL(request.url()).pathname.endsWith('/workflow/work-center/items') && new URL(request.url()).searchParams.get('view') === view);
|
|
813
|
+
await page.getByRole('tab', { name: new RegExp(label) }).click();
|
|
814
|
+
await request;
|
|
815
|
+
await expect(page.getByRole('button', { name: '采购申请审批' })).toBeVisible();
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
for (const mobile of [false, true]) {
|
|
820
|
+
for (const multiple of [false, true]) {
|
|
821
|
+
test(`${mobile ? 'mobile' : 'desktop'} ${multiple ? 'CC' : 'transfer'} uses the standard member picker`, async ({ page }) => {
|
|
822
|
+
await page.setViewportSize(mobile ? { width: 320, height: 480 } : { width: 1440, height: 900 });
|
|
823
|
+
await mockWorkflow(page);
|
|
824
|
+
const current = detail();
|
|
825
|
+
const key = multiple ? 'cc' : 'transfer';
|
|
826
|
+
const label = multiple ? '抄送' : '转交';
|
|
827
|
+
const field = multiple ? 'userIds' : 'userId';
|
|
828
|
+
const cc = {
|
|
829
|
+
key, kind: 'workflow_command', label, group: 'instance_control', audience: 'participant',
|
|
830
|
+
placement: 'overflow', emphasis: 'neutral', tone: 'neutral', visible: true, enabled: true,
|
|
831
|
+
inputSchema: { type: 'object', additionalProperties: false, required: [field], properties: {
|
|
832
|
+
[field]: multiple ? { type: 'array', title: '抄送给', minItems: 1, maxItems: 20, uniqueItems: true, items: { type: 'string' } } : { type: 'string', title: '转交给' },
|
|
833
|
+
comment: { type: 'string', title: '留言', maxLength: 4000 },
|
|
834
|
+
} },
|
|
835
|
+
uiSchema: { presentation: 'dialog', confirmText: `确认${label}`, properties: { [field]: { component: 'user_select', mode: multiple ? 'multiple' : 'single' } } },
|
|
836
|
+
execute: { method: 'POST', href: `/openxiangda-api/v2/applications/${appCode}/workflow/${multiple ? `instances/${instanceId}` : `tasks/${taskId}`}/commands/${key}`, idempotencyRequired: true },
|
|
837
|
+
refresh: ['surface', 'timeline', 'work_center'],
|
|
838
|
+
};
|
|
839
|
+
const augmented = { ...current, operations: [...current.operations.filter(item => item.key !== key), cc], surface: { ...current.surface, operations: [...current.surface.operations.filter(item => item.key !== key), cc] } };
|
|
840
|
+
await page.route('**/workflow/tasks/*/detail', route => route.fulfill({ json: envelope(augmented) }));
|
|
841
|
+
await page.route('**/directory/**', route => {
|
|
842
|
+
const tree = new URL(route.request().url()).pathname.endsWith('/tree');
|
|
843
|
+
const items = tree ? [] : ['陈老师', '李老师'].map((label, index) => ({ kind: 'user', id: `cc-user-${index + 1}`, label, selectable: true, snapshot: { value: `cc-user-${index + 1}`, label } }));
|
|
844
|
+
return route.fulfill({ json: envelope({ schemaVersion: 'openxiangda.directory-entry-page/v2', kind: tree ? 'department' : 'user', items, nextCursor: null }) });
|
|
845
|
+
});
|
|
846
|
+
let writes = 0;
|
|
847
|
+
await page.route(`**/commands/${key}`, async route => {
|
|
848
|
+
writes += 1;
|
|
849
|
+
expect(route.request().postDataJSON()).toMatchObject({ commandToken: current.surface.commandToken, input: { [field]: multiple ? ['cc-user-1', 'cc-user-2'] : 'cc-user-1', comment: '请查阅' } });
|
|
850
|
+
expect(route.request().postDataJSON().idempotencyKey).toBeTruthy();
|
|
851
|
+
expect(route.request().headers()['x-openxiangda-csrf-token']).toBe('workflow-csrf-1');
|
|
852
|
+
await route.fulfill({ json: envelope({ status: 'completed', advanced: false, instanceId }) });
|
|
853
|
+
});
|
|
854
|
+
await page.goto(workflowFixtureUrl(`${mobile ? '/m' : ''}/tasks/${taskId}`));
|
|
855
|
+
const footer = page.locator('.oxa-record-detail-footer');
|
|
856
|
+
await expect(footer).toBeVisible();
|
|
857
|
+
await expect(footer.getByRole('button', { name: /关\s*闭|编\s*辑/ })).toHaveCount(0);
|
|
858
|
+
if (mobile) {
|
|
859
|
+
const boxes = await footer.locator('.oxa-workflow-actions > button').evaluateAll(elements => elements.map(element => element.getBoundingClientRect().y));
|
|
860
|
+
expect(boxes).toHaveLength(3);
|
|
861
|
+
expect(Math.max(...boxes) - Math.min(...boxes)).toBeLessThan(2);
|
|
862
|
+
}
|
|
863
|
+
await page.getByRole('button', { name: '更多操作' }).click();
|
|
864
|
+
await page.getByRole('button', { name: label, exact: true }).click();
|
|
865
|
+
await page.getByRole('button', { name: `确认${label}` }).click();
|
|
866
|
+
expect(writes).toBe(0);
|
|
867
|
+
await expect(page.getByRole('combobox')).toHaveCount(0);
|
|
868
|
+
if (mobile) {
|
|
869
|
+
const bounds = await page.locator('.oxa-workflow-operation-drawer .ant-drawer-content-wrapper').boundingBox();
|
|
870
|
+
expect(bounds!.y).toBeGreaterThanOrEqual(0);
|
|
871
|
+
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(481);
|
|
872
|
+
await expect(page.getByRole('button', { name: `确认${label}` })).toBeInViewport();
|
|
873
|
+
}
|
|
874
|
+
const trigger = page.locator(mobile ? '.oxa-mobile-choice-field button' : '.oxa-directory-trigger').first();
|
|
875
|
+
await trigger.click();
|
|
876
|
+
if (mobile) {
|
|
877
|
+
await expect(page.locator('.oxa-mobile-selection-sheet')).toBeInViewport();
|
|
878
|
+
await expect(page.getByRole('button', { name: '关闭', exact: true }).last()).toBeInViewport();
|
|
879
|
+
await expect(page.getByRole('button', { name: '确定', exact: true })).toBeInViewport();
|
|
880
|
+
}
|
|
881
|
+
await page.getByPlaceholder(mobile ? /搜索/ : '搜索成员姓名或工号').fill('老师');
|
|
882
|
+
await page.getByText('陈老师', { exact: true }).click();
|
|
883
|
+
await page.getByRole('button', { name: mobile ? '关闭' : /取\s*消/, exact: true }).last().click();
|
|
884
|
+
await page.getByRole('button', { name: `确认${label}` }).click();
|
|
885
|
+
expect(writes).toBe(0);
|
|
886
|
+
await trigger.click();
|
|
887
|
+
await page.getByPlaceholder(mobile ? /搜索/ : '搜索成员姓名或工号').fill('老师');
|
|
888
|
+
await page.getByText('陈老师', { exact: true }).click();
|
|
889
|
+
if (multiple) await page.getByText('李老师', { exact: true }).click();
|
|
890
|
+
await page.getByRole('button', { name: mobile ? `确定(${multiple ? 2 : 1})` : /确\s*定/, exact: true }).last().click();
|
|
891
|
+
await page.getByLabel('留言').fill('请查阅');
|
|
892
|
+
await page.getByRole('button', { name: `确认${label}` }).click();
|
|
893
|
+
await expect.poll(() => writes).toBe(1);
|
|
894
|
+
await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
test('renders the generic desktop and mobile application todo center without a second router', async ({
|
|
902
|
+
page,
|
|
903
|
+
}) => {
|
|
904
|
+
await mockWorkflow(page);
|
|
905
|
+
await page.goto('/workflow-experience.e2e.html?initial=/todos');
|
|
906
|
+
await expect(page.getByRole('heading', { name: '消息中心' })).toBeVisible();
|
|
907
|
+
await expect(page.getByTestId('standard-frame-desktop')).toHaveCount(0);
|
|
908
|
+
await expect(page.locator('.oxa-todo-table')).toBeVisible();
|
|
909
|
+
await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
|
|
910
|
+
await expect(page.getByText('设备采购申请待审批').first()).toBeVisible();
|
|
911
|
+
await expect(page.getByText('¥28,600').first()).toBeVisible();
|
|
912
|
+
await expect(page.locator('.ant-spin-spinning')).toHaveCount(0);
|
|
913
|
+
const unreadRequest = page.waitForRequest(request => {
|
|
914
|
+
const url = new URL(request.url());
|
|
915
|
+
return url.pathname.endsWith('/todos') && url.searchParams.get('unread') === 'true';
|
|
916
|
+
});
|
|
917
|
+
await page.getByRole('switch', { name: '仅看未读' }).click();
|
|
918
|
+
await unreadRequest;
|
|
919
|
+
await page.getByRole('button', { name: /查看详情/ }).first().click();
|
|
920
|
+
await expect(page).toHaveURL(
|
|
921
|
+
new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}`)
|
|
922
|
+
);
|
|
923
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
924
|
+
`${instanceId} / ${taskId}`
|
|
925
|
+
);
|
|
926
|
+
|
|
927
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
928
|
+
await page.goto('/workflow-experience.e2e.html?initial=/m/todos');
|
|
929
|
+
await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
|
|
930
|
+
await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
|
|
931
|
+
await page.getByRole('button', { name: /查看详情/ }).first().click();
|
|
932
|
+
await expect(page).toHaveURL(
|
|
933
|
+
new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}`)
|
|
934
|
+
);
|
|
935
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
936
|
+
`${instanceId} / ${taskId}`
|
|
937
|
+
);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test('selects paired application todo renderers while the platform owns data, paging and navigation', async ({
|
|
941
|
+
page,
|
|
942
|
+
}) => {
|
|
943
|
+
await mockWorkflow(page);
|
|
944
|
+
await page.goto(
|
|
945
|
+
workflowFixtureUrl('/todos', { surfaces: 'custom' }),
|
|
946
|
+
);
|
|
947
|
+
const desktopFrame = page.getByTestId('standard-frame-desktop');
|
|
948
|
+
await expect(desktopFrame).toHaveAttribute(
|
|
949
|
+
'data-page-kind',
|
|
950
|
+
'application-todo-center',
|
|
951
|
+
);
|
|
952
|
+
await expect(desktopFrame).toHaveAttribute(
|
|
953
|
+
'data-route-code',
|
|
954
|
+
'application.todo-center.desktop',
|
|
955
|
+
);
|
|
956
|
+
await expect(page.getByTestId('custom-todo-desktop')).toBeVisible();
|
|
957
|
+
await expect(page.getByText('设备采购申请待审批')).toBeVisible();
|
|
958
|
+
|
|
959
|
+
const nextPage = page.waitForRequest(request => {
|
|
960
|
+
const url = new URL(request.url());
|
|
961
|
+
return url.pathname.endsWith('/todos') && url.searchParams.get('offset') === '1';
|
|
962
|
+
});
|
|
963
|
+
await page.getByRole('button', { name: '加载更多' }).click();
|
|
964
|
+
await nextPage;
|
|
965
|
+
await expect(page.getByText('采购结果通知')).toBeVisible();
|
|
966
|
+
|
|
967
|
+
const unreadRequest = page.waitForRequest(request => {
|
|
968
|
+
const url = new URL(request.url());
|
|
969
|
+
return url.pathname.endsWith('/todos') && url.searchParams.get('unread') === 'true';
|
|
970
|
+
});
|
|
971
|
+
await page.getByRole('button', { name: '切换未读' }).click();
|
|
972
|
+
await unreadRequest;
|
|
973
|
+
await expect(page.getByTestId('custom-todo-query')).toContainText(
|
|
974
|
+
'all/true/all',
|
|
975
|
+
);
|
|
976
|
+
|
|
977
|
+
await page.getByRole('button', { name: '设备采购申请待审批' }).click();
|
|
978
|
+
await expect(page).toHaveURL(
|
|
979
|
+
new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}`),
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
983
|
+
await page.goto(
|
|
984
|
+
workflowFixtureUrl('/m/todos', { surfaces: 'custom' }),
|
|
985
|
+
);
|
|
986
|
+
await expect(page.getByTestId('custom-todo-mobile')).toBeVisible();
|
|
987
|
+
await expect(page.getByTestId('standard-frame-mobile')).toHaveAttribute(
|
|
988
|
+
'data-route-code',
|
|
989
|
+
'application.todo-center.mobile',
|
|
990
|
+
);
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
test('frames platform-owned workflow launch, task and instance routes with safe metadata', async ({
|
|
994
|
+
page,
|
|
995
|
+
}) => {
|
|
996
|
+
await mockWorkflow(page);
|
|
997
|
+
for (const [path, kind, routeCode, params] of [
|
|
998
|
+
[
|
|
999
|
+
'/workflows/purchase-approval/start',
|
|
1000
|
+
'workflow-launch',
|
|
1001
|
+
'workflow.purchase-approval.launch.desktop',
|
|
1002
|
+
'purchase-approval',
|
|
1003
|
+
],
|
|
1004
|
+
[
|
|
1005
|
+
`/tasks/${taskId}`,
|
|
1006
|
+
'workflow-task',
|
|
1007
|
+
'workflow.task.desktop',
|
|
1008
|
+
taskId,
|
|
1009
|
+
],
|
|
1010
|
+
[
|
|
1011
|
+
`/workflows/${instanceId}`,
|
|
1012
|
+
'workflow-instance',
|
|
1013
|
+
'workflow.instance.desktop',
|
|
1014
|
+
instanceId,
|
|
1015
|
+
],
|
|
1016
|
+
] as const) {
|
|
1017
|
+
await page.goto(workflowFixtureUrl(path, { surfaces: 'custom' }));
|
|
1018
|
+
const frame = page.getByTestId('standard-frame-desktop');
|
|
1019
|
+
await expect(frame).toHaveAttribute('data-page-kind', kind);
|
|
1020
|
+
await expect(frame).toHaveAttribute('data-route-code', routeCode);
|
|
1021
|
+
await expect(frame).toHaveAttribute(
|
|
1022
|
+
'data-route-params',
|
|
1023
|
+
new RegExp(params),
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test('contains an application standard renderer failure inside the platform boundary', async ({
|
|
1029
|
+
page,
|
|
1030
|
+
}) => {
|
|
1031
|
+
await mockWorkflow(page);
|
|
1032
|
+
await page.goto(workflowFixtureUrl('/todos', { surfaces: 'error' }));
|
|
1033
|
+
await expect(page.getByText('页面显示失败')).toBeVisible();
|
|
1034
|
+
await expect(page.getByText(/平台登录态和业务数据未受影响/)).toBeVisible();
|
|
1035
|
+
await expect(page.getByRole('button', { name: '重新加载页面' })).toBeVisible();
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test('launches the same workflow from the canonical mobile Surface and follows custom detail routing', async ({
|
|
1039
|
+
page,
|
|
1040
|
+
}) => {
|
|
1041
|
+
await mockWorkflow(page, { customDetail: true });
|
|
1042
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
1043
|
+
await page.goto(
|
|
1044
|
+
'/workflow-experience.e2e.html?initial=/m/workflows/purchase-approval/start'
|
|
1045
|
+
);
|
|
1046
|
+
await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
|
|
1047
|
+
await expect(page.getByRole('heading', { name: '采购审批' })).toBeVisible();
|
|
1048
|
+
await page.getByRole('textbox', { name: '申请金额', exact: true }).fill('28600');
|
|
1049
|
+
await page.getByRole('button', { name: '提交审批' }).click();
|
|
1050
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
1051
|
+
`${instanceId} / ${taskId}`
|
|
1052
|
+
);
|
|
1053
|
+
await expect(page).toHaveURL(
|
|
1054
|
+
new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
test('routes desktop and mobile work-center entries to the declared custom detail pages', async ({
|
|
1059
|
+
page,
|
|
1060
|
+
}) => {
|
|
1061
|
+
await mockWorkflow(page, { customDetail: true });
|
|
1062
|
+
await page.goto('/workflow-experience.e2e.html?initial=/work-center');
|
|
1063
|
+
await page.getByRole('button', { name: '采购申请审批' }).click();
|
|
1064
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
1065
|
+
`${instanceId} / ${taskId}`
|
|
1066
|
+
);
|
|
1067
|
+
await expect(page).toHaveURL(
|
|
1068
|
+
new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}$`)
|
|
1069
|
+
);
|
|
1070
|
+
|
|
1071
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
1072
|
+
await page.goto('/workflow-experience.e2e.html?initial=/m/work-center');
|
|
1073
|
+
await page.getByText('采购申请审批').click();
|
|
1074
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
1075
|
+
`${instanceId} / ${taskId}`
|
|
1076
|
+
);
|
|
1077
|
+
await expect(page).toHaveURL(
|
|
1078
|
+
new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
test('redirects a direct standard task URL to the declared custom detail page', async ({
|
|
1083
|
+
page,
|
|
1084
|
+
}) => {
|
|
1085
|
+
await mockWorkflow(page, { customDetail: true });
|
|
1086
|
+
await page.goto(
|
|
1087
|
+
`/workflow-experience.e2e.html?initial=/tasks/${taskId}`
|
|
1088
|
+
);
|
|
1089
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
1090
|
+
`${instanceId} / ${taskId}`
|
|
1091
|
+
);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test('renders a standalone desktop detail on the canonical user URL', async ({
|
|
1095
|
+
page,
|
|
1096
|
+
}) => {
|
|
1097
|
+
await mockWorkflow(page);
|
|
1098
|
+
await page.goto(
|
|
1099
|
+
workflowFixtureUrl(`/tasks/${taskId}?from=todo#approval`),
|
|
1100
|
+
);
|
|
1101
|
+
await expect(page.locator('.oxa-record-detail-desktop.is-page')).toBeVisible();
|
|
1102
|
+
await expect(page).toHaveURL(
|
|
1103
|
+
new RegExp(`/tasks/${taskId}\\?from=todo#approval$`),
|
|
1104
|
+
);
|
|
1105
|
+
await expect(page.locator('.oxa-app-layout')).toHaveCount(0);
|
|
1106
|
+
await expect(page.locator('.oxa-topbar')).toHaveCount(0);
|
|
1107
|
+
await expect(page.getByText('¥30,000')).toBeVisible();
|
|
1108
|
+
await expect(page.getByText('技术场地引用')).toHaveCount(0);
|
|
1109
|
+
await expect(page.getByText('业务数据已变化')).toHaveCount(0);
|
|
1110
|
+
await expect(page.getByText('流程信息')).toHaveCount(0);
|
|
1111
|
+
await expect(page.getByText('操作记录')).toHaveCount(0);
|
|
1112
|
+
await page.getByRole('tab', { name: '审批历史' }).click();
|
|
1113
|
+
await expect(page.getByText('由值班负责人继续处理')).toHaveCount(1);
|
|
1114
|
+
await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
|
|
1115
|
+
await expect(page.getByRole('button', { name: /拒\s*绝/ })).toBeVisible();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
test('shows a friendly refresh prompt only after a real command conflict', async ({
|
|
1119
|
+
page,
|
|
1120
|
+
}) => {
|
|
1121
|
+
await mockWorkflow(page, { commandConflict: true });
|
|
1122
|
+
await page.goto(workflowFixtureUrl(`/tasks/${taskId}`));
|
|
1123
|
+
await expect(page.getByText('业务数据已变化')).toHaveCount(0);
|
|
1124
|
+
await page.getByRole('button', { name: /同\s*意/ }).click();
|
|
1125
|
+
await page.getByLabel('审批意见').fill('同意采购');
|
|
1126
|
+
await page.getByRole('button', { name: '确认同意' }).click();
|
|
1127
|
+
await expect(page.getByText('内容已更新,请刷新后重试')).toBeVisible();
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test('renders the independent mobile task and executes only a Surface operation', async ({
|
|
1131
|
+
page,
|
|
1132
|
+
}) => {
|
|
1133
|
+
await mockWorkflow(page);
|
|
1134
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
1135
|
+
await page.goto(`/workflow-experience.e2e.html?initial=/m/tasks/${taskId}`);
|
|
1136
|
+
await expect(page.locator('.oxa-record-detail-mobile')).toBeVisible();
|
|
1137
|
+
await expect(page.getByText('¥30,000')).toBeVisible();
|
|
1138
|
+
await page.getByRole('tab', { name: '审批历史' }).click();
|
|
1139
|
+
await expect(page.getByText('财务负责人').first()).toBeVisible();
|
|
1140
|
+
await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
|
|
1141
|
+
await page.getByRole('button', { name: /同\s*意/ }).click();
|
|
1142
|
+
await page.getByLabel('审批意见').fill('同意采购');
|
|
1143
|
+
await page.getByRole('button', { name: '确认同意' }).click();
|
|
1144
|
+
await expect(page.getByText('同意已提交')).toBeVisible();
|
|
1145
|
+
await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
test('shows an explicit authorization error instead of an empty work center', async ({
|
|
1149
|
+
page,
|
|
1150
|
+
}) => {
|
|
1151
|
+
test.setTimeout(45_000);
|
|
1152
|
+
await mockWorkflow(page, { forbidden: true });
|
|
1153
|
+
await page.goto('/workflow-experience.e2e.html?initial=/work-center', {
|
|
1154
|
+
waitUntil: 'domcontentloaded',
|
|
1155
|
+
});
|
|
1156
|
+
await expect(page.getByRole('alert')).toContainText('403: forbidden', {
|
|
1157
|
+
timeout: 30_000,
|
|
1158
|
+
});
|
|
1159
|
+
await expect(page.getByRole('button', { name: '采购申请审批' })).toHaveCount(0);
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
test('negotiates direct standard routes after BrowserRouter listener setup', async ({
|
|
1163
|
+
page,
|
|
1164
|
+
}) => {
|
|
1165
|
+
await mockWorkflow(page);
|
|
1166
|
+
await page.setViewportSize({ width: 360, height: 844 });
|
|
1167
|
+
await page.goto(
|
|
1168
|
+
workflowFixtureUrl('/todos?view=pending#unread', {
|
|
1169
|
+
state: 'deep-link',
|
|
1170
|
+
}),
|
|
1171
|
+
);
|
|
1172
|
+
|
|
1173
|
+
// The initial desktop URL is replaced by the mobile pair and the rendered
|
|
1174
|
+
// surface follows the Router update. The fixture is StrictMode-mounted.
|
|
1175
|
+
await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
|
|
1176
|
+
await expect(page.locator('.oxa-todo-center-desktop')).toHaveCount(0);
|
|
1177
|
+
await expect(page).toHaveURL(/\/m\/todos\?view=pending#unread$/);
|
|
1178
|
+
const historyLength = await page.evaluate(() => window.history.length);
|
|
1179
|
+
const negotiatedUrl = page.url();
|
|
1180
|
+
await expect
|
|
1181
|
+
.poll(() => page.url(), { timeout: 1_000 })
|
|
1182
|
+
.toBe(negotiatedUrl);
|
|
1183
|
+
expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
|
|
1184
|
+
routeNegotiationState: 'deep-link',
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
// 900px remains mobile; crossing to 901px switches the same manifest pair
|
|
1188
|
+
// back to desktop without adding a browser-history entry.
|
|
1189
|
+
await page.setViewportSize({ width: 900, height: 844 });
|
|
1190
|
+
await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
|
|
1191
|
+
await page.setViewportSize({ width: 901, height: 844 });
|
|
1192
|
+
await expect(page.locator('.oxa-todo-center-desktop')).toBeVisible();
|
|
1193
|
+
await expect(page).toHaveURL(/\/todos\?view=pending#unread$/);
|
|
1194
|
+
expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
|
|
1195
|
+
expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
|
|
1196
|
+
routeNegotiationState: 'deep-link',
|
|
1197
|
+
});
|
|
1198
|
+
await page.goBack();
|
|
1199
|
+
await expect(page).not.toHaveURL(/\/todos\?view=pending#unread$/);
|
|
1200
|
+
|
|
1201
|
+
// Direct task and instance URLs carry their dynamic parameter and deep-link
|
|
1202
|
+
// context to the independent mobile renderer at the narrow width.
|
|
1203
|
+
await page.setViewportSize({ width: 360, height: 844 });
|
|
1204
|
+
await page.goto(
|
|
1205
|
+
workflowFixtureUrl(`/tasks/${taskId}?from=todo#approval`),
|
|
1206
|
+
);
|
|
1207
|
+
await expect(page.locator('.oxa-record-detail-mobile')).toBeVisible();
|
|
1208
|
+
await expect(page).toHaveURL(
|
|
1209
|
+
new RegExp(`/m/tasks/${taskId}\\?from=todo#approval$`),
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
await page.goto(
|
|
1213
|
+
workflowFixtureUrl(`/workflows/${instanceId}?from=todo#timeline`),
|
|
1214
|
+
);
|
|
1215
|
+
await expect(page.locator('.oxa-record-detail-mobile')).toBeVisible();
|
|
1216
|
+
await expect(page).toHaveURL(
|
|
1217
|
+
new RegExp(`/m/workflows/${instanceId}\\?from=todo#timeline$`),
|
|
1218
|
+
);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1221
|
+
test('negotiates launch pairs in both directions and preserves the mount basename', async ({
|
|
1222
|
+
page,
|
|
1223
|
+
}) => {
|
|
1224
|
+
await mockWorkflow(page);
|
|
1225
|
+
const base = '/runtime/route-negotiation';
|
|
1226
|
+
await page.setViewportSize({ width: 360, height: 844 });
|
|
1227
|
+
await page.goto(
|
|
1228
|
+
workflowFixtureUrl(
|
|
1229
|
+
`${base}/workflows/purchase-approval/start?source=todo#launch`,
|
|
1230
|
+
{ base },
|
|
1231
|
+
),
|
|
1232
|
+
);
|
|
1233
|
+
await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
|
|
1234
|
+
await expect(page).toHaveURL(
|
|
1235
|
+
new RegExp(
|
|
1236
|
+
`${base}/m/workflows/purchase-approval/start\\?source=todo#launch$`,
|
|
1237
|
+
),
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
await page.setViewportSize({ width: 1_440, height: 900 });
|
|
1241
|
+
await expect(page.locator('.oxa-workflow-entry')).toBeVisible();
|
|
1242
|
+
await expect(page).toHaveURL(
|
|
1243
|
+
new RegExp(
|
|
1244
|
+
`${base}/workflows/purchase-approval/start\\?source=todo#launch$`,
|
|
1245
|
+
),
|
|
1246
|
+
);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
test('does not negotiate non-standard application contribution routes', async ({
|
|
1250
|
+
page,
|
|
1251
|
+
}) => {
|
|
1252
|
+
await mockWorkflow(page);
|
|
1253
|
+
const base = '/runtime/route-negotiation';
|
|
1254
|
+
await page.setViewportSize({ width: 360, height: 844 });
|
|
1255
|
+
const customPath = `${base}/admin/operations/purchases/${instanceId}?taskId=${taskId}`;
|
|
1256
|
+
await page.goto(workflowFixtureUrl(customPath, { base }));
|
|
1257
|
+
await expect(page.getByTestId('custom-workflow-detail')).toContainText(
|
|
1258
|
+
`${instanceId} / ${taskId}`,
|
|
1259
|
+
);
|
|
1260
|
+
const customUrl = page.url();
|
|
1261
|
+
await expect(page).toHaveURL(new RegExp(`${base}/admin/operations/`));
|
|
1262
|
+
await expect
|
|
1263
|
+
.poll(() => page.url(), { timeout: 1_000 })
|
|
1264
|
+
.toBe(customUrl);
|
|
1265
|
+
});
|