openxiangda-cli 2.0.0-alpha.15 → 2.0.0-alpha.151

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.
Files changed (277) hide show
  1. package/README.md +51 -15
  2. package/bin/run.js +79 -1
  3. package/dist/base.d.ts +59 -5
  4. package/dist/base.d.ts.map +1 -1
  5. package/dist/base.js +213 -10
  6. package/dist/base.js.map +1 -1
  7. package/dist/commands/accept.d.ts +28 -0
  8. package/dist/commands/accept.d.ts.map +1 -0
  9. package/dist/commands/accept.js +21 -0
  10. package/dist/commands/accept.js.map +1 -0
  11. package/dist/commands/cancel.d.ts +19 -1
  12. package/dist/commands/cancel.d.ts.map +1 -1
  13. package/dist/commands/cancel.js +1 -1
  14. package/dist/commands/cancel.js.map +1 -1
  15. package/dist/commands/check.d.ts +22 -1
  16. package/dist/commands/check.d.ts.map +1 -1
  17. package/dist/commands/check.js +17 -4
  18. package/dist/commands/check.js.map +1 -1
  19. package/dist/commands/create.d.ts +39 -0
  20. package/dist/commands/create.d.ts.map +1 -0
  21. package/dist/commands/create.js +457 -0
  22. package/dist/commands/create.js.map +1 -0
  23. package/dist/commands/deploy.d.ts +26 -8
  24. package/dist/commands/deploy.d.ts.map +1 -1
  25. package/dist/commands/deploy.js +31 -17
  26. package/dist/commands/deploy.js.map +1 -1
  27. package/dist/commands/dev.d.ts +23 -1
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +28 -4
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/login.d.ts +29 -0
  32. package/dist/commands/login.d.ts.map +1 -0
  33. package/dist/commands/login.js +73 -0
  34. package/dist/commands/login.js.map +1 -0
  35. package/dist/commands/logs.d.ts +22 -2
  36. package/dist/commands/logs.d.ts.map +1 -1
  37. package/dist/commands/logs.js +9 -5
  38. package/dist/commands/logs.js.map +1 -1
  39. package/dist/commands/retry.d.ts +19 -1
  40. package/dist/commands/retry.d.ts.map +1 -1
  41. package/dist/commands/retry.js +5 -2
  42. package/dist/commands/retry.js.map +1 -1
  43. package/dist/commands/rollback.d.ts +23 -4
  44. package/dist/commands/rollback.d.ts.map +1 -1
  45. package/dist/commands/rollback.js +18 -5
  46. package/dist/commands/rollback.js.map +1 -1
  47. package/dist/commands/skill.d.ts +31 -0
  48. package/dist/commands/skill.d.ts.map +1 -0
  49. package/dist/commands/skill.js +43 -0
  50. package/dist/commands/skill.js.map +1 -0
  51. package/dist/commands/spec.d.ts +39 -0
  52. package/dist/commands/spec.d.ts.map +1 -0
  53. package/dist/commands/spec.js +117 -0
  54. package/dist/commands/spec.js.map +1 -0
  55. package/dist/commands/start.d.ts +28 -0
  56. package/dist/commands/start.d.ts.map +1 -0
  57. package/dist/commands/start.js +18 -0
  58. package/dist/commands/start.js.map +1 -0
  59. package/dist/commands/status.d.ts +19 -1
  60. package/dist/commands/status.d.ts.map +1 -1
  61. package/dist/commands/status.js +1 -1
  62. package/dist/commands/status.js.map +1 -1
  63. package/dist/commands/stop.d.ts +28 -0
  64. package/dist/commands/stop.d.ts.map +1 -0
  65. package/dist/commands/stop.js +18 -0
  66. package/dist/commands/stop.js.map +1 -0
  67. package/dist/create-workspace.d.ts +67 -0
  68. package/dist/create-workspace.d.ts.map +1 -0
  69. package/dist/create-workspace.js +631 -0
  70. package/dist/create-workspace.js.map +1 -0
  71. package/dist/studio-events.d.ts +9 -0
  72. package/dist/studio-events.d.ts.map +1 -0
  73. package/dist/studio-events.js +30 -0
  74. package/dist/studio-events.js.map +1 -0
  75. package/dist/toolchain-capsule.d.ts +4 -0
  76. package/dist/toolchain-capsule.d.ts.map +1 -0
  77. package/dist/toolchain-capsule.js +116 -0
  78. package/dist/toolchain-capsule.js.map +1 -0
  79. package/package.json +16 -9
  80. package/template/.dockerignore +23 -0
  81. package/template/.env.example +3 -0
  82. package/template/AGENTS.md +47 -0
  83. package/template/README.md +65 -0
  84. package/template/_gitignore +14 -0
  85. package/template/apps/server/Dockerfile +20 -0
  86. package/template/apps/server/package.json +27 -0
  87. package/template/apps/server/src/app.module.ts +23 -0
  88. package/template/apps/server/src/main.ts +5 -0
  89. package/template/apps/server/test/smoke.test.ts +17 -0
  90. package/template/apps/server/tsconfig.json +9 -0
  91. package/template/apps/web/data-api-live.e2e.html +15 -0
  92. package/template/apps/web/e2e/client-fixture.ts +5 -0
  93. package/template/apps/web/e2e/data-api-live-fixture.tsx +162 -0
  94. package/template/apps/web/e2e/data-api-live.spec.ts +144 -0
  95. package/template/apps/web/e2e/field-protocol-fixture.tsx +350 -0
  96. package/template/apps/web/e2e/field-protocol.spec.ts +195 -0
  97. package/template/apps/web/e2e/resource-experience-fixture.tsx +446 -0
  98. package/template/apps/web/e2e/resources.spec.ts +638 -0
  99. package/template/apps/web/e2e/theme-contract-fixture.css +15 -0
  100. package/template/apps/web/e2e/workflow-experience-fixture.tsx +300 -0
  101. package/template/apps/web/e2e/workflow.spec.ts +1057 -0
  102. package/template/apps/web/field-protocol.e2e.html +15 -0
  103. package/template/apps/web/index.html +12 -0
  104. package/template/apps/web/package.json +34 -0
  105. package/template/apps/web/playwright.config.ts +47 -0
  106. package/template/apps/web/resource-experience.e2e.html +15 -0
  107. package/template/apps/web/scripts/check.mjs +43 -0
  108. package/template/apps/web/scripts/verify-build.mjs +6 -0
  109. package/template/apps/web/src/main.tsx +63 -0
  110. package/template/apps/web/src/user-end/tailwind.css +17 -0
  111. package/template/apps/web/test/contracts.test.ts +155 -0
  112. package/template/apps/web/test/loopback.test.ts +68 -0
  113. package/template/apps/web/tsconfig.json +12 -0
  114. package/template/apps/web/vite.config.ts +41 -0
  115. package/template/apps/web/workflow-experience.e2e.html +15 -0
  116. package/template/appspec/app.md +36 -0
  117. package/template/openxiangda.config.ts +77 -0
  118. package/template/package.json +29 -0
  119. package/template/packages/contracts/package.json +23 -0
  120. package/template/packages/contracts/src/generated.ts +263 -0
  121. package/template/packages/contracts/src/index.ts +1 -0
  122. package/template/packages/contracts/tsconfig.json +9 -0
  123. package/template/pnpm-workspace.yaml +7 -0
  124. package/template/scripts/verify-template-budget.mjs +49 -0
  125. package/template/tsconfig.base.json +13 -0
  126. package/dist/commands/app/create.d.ts +0 -15
  127. package/dist/commands/app/create.d.ts.map +0 -1
  128. package/dist/commands/app/create.js +0 -42
  129. package/dist/commands/app/create.js.map +0 -1
  130. package/dist/commands/app/info.d.ts +0 -9
  131. package/dist/commands/app/info.d.ts.map +0 -1
  132. package/dist/commands/app/info.js +0 -10
  133. package/dist/commands/app/info.js.map +0 -1
  134. package/dist/commands/app/link.d.ts +0 -10
  135. package/dist/commands/app/link.d.ts.map +0 -1
  136. package/dist/commands/app/link.js +0 -16
  137. package/dist/commands/app/link.js.map +0 -1
  138. package/dist/commands/app/provision.d.ts +0 -9
  139. package/dist/commands/app/provision.d.ts.map +0 -1
  140. package/dist/commands/app/provision.js +0 -10
  141. package/dist/commands/app/provision.js.map +0 -1
  142. package/dist/commands/auth/login.d.ts +0 -10
  143. package/dist/commands/auth/login.d.ts.map +0 -1
  144. package/dist/commands/auth/login.js +0 -34
  145. package/dist/commands/auth/login.js.map +0 -1
  146. package/dist/commands/auth/logout.d.ts +0 -6
  147. package/dist/commands/auth/logout.d.ts.map +0 -1
  148. package/dist/commands/auth/logout.js +0 -10
  149. package/dist/commands/auth/logout.js.map +0 -1
  150. package/dist/commands/auth/status.d.ts +0 -6
  151. package/dist/commands/auth/status.d.ts.map +0 -1
  152. package/dist/commands/auth/status.js +0 -19
  153. package/dist/commands/auth/status.js.map +0 -1
  154. package/dist/commands/build.d.ts +0 -11
  155. package/dist/commands/build.d.ts.map +0 -1
  156. package/dist/commands/build.js +0 -23
  157. package/dist/commands/build.js.map +0 -1
  158. package/dist/commands/doctor.d.ts +0 -9
  159. package/dist/commands/doctor.d.ts.map +0 -1
  160. package/dist/commands/doctor.js +0 -7
  161. package/dist/commands/doctor.js.map +0 -1
  162. package/dist/commands/event/delivery/list.d.ts +0 -10
  163. package/dist/commands/event/delivery/list.d.ts.map +0 -1
  164. package/dist/commands/event/delivery/list.js +0 -14
  165. package/dist/commands/event/delivery/list.js.map +0 -1
  166. package/dist/commands/event/delivery/replay.d.ts +0 -13
  167. package/dist/commands/event/delivery/replay.d.ts.map +0 -1
  168. package/dist/commands/event/delivery/replay.js +0 -16
  169. package/dist/commands/event/delivery/replay.js.map +0 -1
  170. package/dist/commands/event/subscription/list.d.ts +0 -9
  171. package/dist/commands/event/subscription/list.d.ts.map +0 -1
  172. package/dist/commands/event/subscription/list.js +0 -10
  173. package/dist/commands/event/subscription/list.js.map +0 -1
  174. package/dist/commands/event/subscription/rotate-secret.d.ts +0 -15
  175. package/dist/commands/event/subscription/rotate-secret.d.ts.map +0 -1
  176. package/dist/commands/event/subscription/rotate-secret.js +0 -33
  177. package/dist/commands/event/subscription/rotate-secret.js.map +0 -1
  178. package/dist/commands/event/subscription/status.d.ts +0 -14
  179. package/dist/commands/event/subscription/status.d.ts.map +0 -1
  180. package/dist/commands/event/subscription/status.js +0 -19
  181. package/dist/commands/event/subscription/status.js.map +0 -1
  182. package/dist/commands/event/timer/list.d.ts +0 -9
  183. package/dist/commands/event/timer/list.d.ts.map +0 -1
  184. package/dist/commands/event/timer/list.js +0 -10
  185. package/dist/commands/event/timer/list.js.map +0 -1
  186. package/dist/commands/event/timer/status.d.ts +0 -14
  187. package/dist/commands/event/timer/status.d.ts.map +0 -1
  188. package/dist/commands/event/timer/status.js +0 -19
  189. package/dist/commands/event/timer/status.js.map +0 -1
  190. package/dist/commands/generate.d.ts +0 -10
  191. package/dist/commands/generate.d.ts.map +0 -1
  192. package/dist/commands/generate.js +0 -8
  193. package/dist/commands/generate.js.map +0 -1
  194. package/dist/commands/oauth/audit/list.d.ts +0 -12
  195. package/dist/commands/oauth/audit/list.d.ts.map +0 -1
  196. package/dist/commands/oauth/audit/list.js +0 -20
  197. package/dist/commands/oauth/audit/list.js.map +0 -1
  198. package/dist/commands/oauth/client/create.d.ts +0 -13
  199. package/dist/commands/oauth/client/create.d.ts.map +0 -1
  200. package/dist/commands/oauth/client/create.js +0 -34
  201. package/dist/commands/oauth/client/create.js.map +0 -1
  202. package/dist/commands/oauth/client/list.d.ts +0 -9
  203. package/dist/commands/oauth/client/list.d.ts.map +0 -1
  204. package/dist/commands/oauth/client/list.js +0 -10
  205. package/dist/commands/oauth/client/list.js.map +0 -1
  206. package/dist/commands/oauth/client/revoke.d.ts +0 -12
  207. package/dist/commands/oauth/client/revoke.d.ts.map +0 -1
  208. package/dist/commands/oauth/client/revoke.js +0 -12
  209. package/dist/commands/oauth/client/revoke.js.map +0 -1
  210. package/dist/commands/oauth/client/rotate.d.ts +0 -13
  211. package/dist/commands/oauth/client/rotate.d.ts.map +0 -1
  212. package/dist/commands/oauth/client/rotate.js +0 -20
  213. package/dist/commands/oauth/client/rotate.js.map +0 -1
  214. package/dist/commands/oauth/client/update.d.ts +0 -15
  215. package/dist/commands/oauth/client/update.d.ts.map +0 -1
  216. package/dist/commands/oauth/client/update.js +0 -23
  217. package/dist/commands/oauth/client/update.js.map +0 -1
  218. package/dist/commands/oauth/runtime/rotate.d.ts +0 -12
  219. package/dist/commands/oauth/runtime/rotate.d.ts.map +0 -1
  220. package/dist/commands/oauth/runtime/rotate.js +0 -31
  221. package/dist/commands/oauth/runtime/rotate.js.map +0 -1
  222. package/dist/commands/oauth/runtime/status.d.ts +0 -10
  223. package/dist/commands/oauth/runtime/status.d.ts.map +0 -1
  224. package/dist/commands/oauth/runtime/status.js +0 -17
  225. package/dist/commands/oauth/runtime/status.js.map +0 -1
  226. package/dist/commands/promote.d.ts +0 -13
  227. package/dist/commands/promote.d.ts.map +0 -1
  228. package/dist/commands/promote.js +0 -9
  229. package/dist/commands/promote.js.map +0 -1
  230. package/dist/commands/secret/audit/list.d.ts +0 -14
  231. package/dist/commands/secret/audit/list.d.ts.map +0 -1
  232. package/dist/commands/secret/audit/list.js +0 -16
  233. package/dist/commands/secret/audit/list.js.map +0 -1
  234. package/dist/commands/secret/create.d.ts +0 -18
  235. package/dist/commands/secret/create.d.ts.map +0 -1
  236. package/dist/commands/secret/create.js +0 -33
  237. package/dist/commands/secret/create.js.map +0 -1
  238. package/dist/commands/secret/delete.d.ts +0 -15
  239. package/dist/commands/secret/delete.d.ts.map +0 -1
  240. package/dist/commands/secret/delete.js +0 -20
  241. package/dist/commands/secret/delete.js.map +0 -1
  242. package/dist/commands/secret/list.d.ts +0 -10
  243. package/dist/commands/secret/list.d.ts.map +0 -1
  244. package/dist/commands/secret/list.js +0 -14
  245. package/dist/commands/secret/list.js.map +0 -1
  246. package/dist/commands/secret/rotate.d.ts +0 -18
  247. package/dist/commands/secret/rotate.d.ts.map +0 -1
  248. package/dist/commands/secret/rotate.js +0 -30
  249. package/dist/commands/secret/rotate.js.map +0 -1
  250. package/dist/commands/secret/update.d.ts +0 -17
  251. package/dist/commands/secret/update.d.ts.map +0 -1
  252. package/dist/commands/secret/update.js +0 -37
  253. package/dist/commands/secret/update.js.map +0 -1
  254. package/dist/commands/skill/install.d.ts +0 -11
  255. package/dist/commands/skill/install.d.ts.map +0 -1
  256. package/dist/commands/skill/install.js +0 -52
  257. package/dist/commands/skill/install.js.map +0 -1
  258. package/dist/commands/skill/validate.d.ts +0 -9
  259. package/dist/commands/skill/validate.d.ts.map +0 -1
  260. package/dist/commands/skill/validate.js +0 -32
  261. package/dist/commands/skill/validate.js.map +0 -1
  262. package/dist/commands/test.d.ts +0 -9
  263. package/dist/commands/test.d.ts.map +0 -1
  264. package/dist/commands/test.js +0 -7
  265. package/dist/commands/test.js.map +0 -1
  266. package/dist/commands/workflow/provider/list.d.ts +0 -10
  267. package/dist/commands/workflow/provider/list.d.ts.map +0 -1
  268. package/dist/commands/workflow/provider/list.js +0 -14
  269. package/dist/commands/workflow/provider/list.js.map +0 -1
  270. package/dist/commands/workflow/provider/rotate.d.ts +0 -15
  271. package/dist/commands/workflow/provider/rotate.d.ts.map +0 -1
  272. package/dist/commands/workflow/provider/rotate.js +0 -24
  273. package/dist/commands/workflow/provider/rotate.js.map +0 -1
  274. package/dist/secret-value.d.ts +0 -5
  275. package/dist/secret-value.d.ts.map +0 -1
  276. package/dist/secret-value.js +0 -15
  277. package/dist/secret-value.js.map +0 -1
@@ -0,0 +1,1057 @@
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 } = {},
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
+ return `/workflow-experience.e2e.html?${params.toString()}`;
33
+ }
34
+
35
+ function task(completed = false, customDetail = false) {
36
+ return {
37
+ schemaVersion: 'openxiangda.workflow-task/v2',
38
+ engineVersion: '2.0',
39
+ id: taskId,
40
+ instanceId,
41
+ workflowCode: 'purchase-approval',
42
+ businessKey: 'PO-2026-0825',
43
+ nodeId: 'manager-review',
44
+ title: '采购申请审批',
45
+ status: completed ? 'completed' : 'assigned',
46
+ approvalMode: 'single',
47
+ taskKind: 'normal',
48
+ assignedRoleSubjectKey: 'membership:manager',
49
+ activeParticipantId: 'participant-1',
50
+ participants: [],
51
+ originTaskId: null,
52
+ returnSessionId: null,
53
+ version: completed ? 2 : 1,
54
+ dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
55
+ createdAt: '2026-08-25T01:00:00.000Z',
56
+ updatedAt: '2026-08-25T01:10:00.000Z',
57
+ detailNavigation: detailNavigation(customDetail),
58
+ };
59
+ }
60
+
61
+ function surface(completed = false, customDetail = false) {
62
+ return {
63
+ schemaVersion: 'openxiangda.workflow-surface/v2',
64
+ protocolVersion: 'workflow_surface_v2',
65
+ surfaceRevision: (completed ? 'b' : 'a').repeat(64),
66
+ engineVersion: '2.0',
67
+ commandToken: 'A'.repeat(43),
68
+ commandTokenExpiresAt: '2099-08-29T09:05:00.000Z',
69
+ instanceSequence: completed ? 4 : 3,
70
+ detailNavigation: detailNavigation(customDetail),
71
+ navigationTarget: {
72
+ kind: 'resource_record',
73
+ appCode,
74
+ environmentKey: 'preproduction',
75
+ resourceCode: 'purchase-orders',
76
+ recordId: 'purchase-1',
77
+ desktopPath: '/purchase-orders/purchase-1',
78
+ mobilePath: '/m/purchase-orders/purchase-1',
79
+ },
80
+ instance: {
81
+ schemaVersion: 'openxiangda.workflow-instance/v2',
82
+ engineVersion: '2.0',
83
+ id: instanceId,
84
+ appCode,
85
+ environmentId: 'preproduction-id',
86
+ environmentKey: 'preproduction',
87
+ workflowCode: 'purchase-approval',
88
+ definitionVersion: 1,
89
+ bindingVersion: 1,
90
+ businessKey: 'PO-2026-0825',
91
+ generation: 1,
92
+ status: completed ? 'approved' : 'running',
93
+ initiatorUserId: 'applicant-user',
94
+ initiatorAuthorizationDigest: null,
95
+ dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
96
+ dataRevision: '8',
97
+ currentNodeId: completed ? null : 'manager-review',
98
+ outcome: completed ? 'approved' : null,
99
+ version: completed ? 3 : 2,
100
+ eventSequence: completed ? 4 : 3,
101
+ startedAt: '2026-08-25T01:00:00.000Z',
102
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
103
+ },
104
+ task: completed ? null : task(false, customDetail),
105
+ presentation: {
106
+ status: { label: completed ? '已同意' : '待审批', tone: 'warning' },
107
+ layout: 'approval_detail',
108
+ businessData: {
109
+ status: 'fresh',
110
+ resourceCode: 'purchase-orders',
111
+ recordId: 'purchase-1',
112
+ requestedRevision: 8,
113
+ sourceRevision: 8,
114
+ fields: { amount: 28600 },
115
+ projectionDigest: 'c'.repeat(64),
116
+ },
117
+ businessDetail: {
118
+ status: 'stale',
119
+ resourceCode: 'purchase-orders',
120
+ resourceName: '采购申请',
121
+ recordId: 'purchase-1',
122
+ requestedRevision: 8,
123
+ sourceRevision: 9,
124
+ surface: {
125
+ mutationOwner: 'workflow',
126
+ fields: {
127
+ amount: {
128
+ label: '申请金额',
129
+ type: 'number.decimal',
130
+ widget: 'money',
131
+ section: '申请信息',
132
+ readCapabilities: [],
133
+ createCapabilities: [],
134
+ updateCapabilities: [],
135
+ },
136
+ internalVenue: {
137
+ label: '技术场地引用',
138
+ type: 'reference',
139
+ widget: 'reference',
140
+ section: '系统信息',
141
+ system: true,
142
+ readCapabilities: [],
143
+ createCapabilities: [],
144
+ updateCapabilities: [],
145
+ },
146
+ },
147
+ detail: {
148
+ layout: 'sections',
149
+ fieldOrder: ['amount', 'internalVenue'],
150
+ },
151
+ },
152
+ record: {
153
+ amount: 30000,
154
+ internalVenue: '77777777-7777-4777-8777-777777777777',
155
+ },
156
+ subtables: {},
157
+ projectionDigest: 'd'.repeat(64),
158
+ },
159
+ summary: {
160
+ title: '采购申请审批',
161
+ initiatorDisplayName: '张三',
162
+ departmentDisplayName: '理学院',
163
+ submittedAt: '2026-08-25T01:00:00.000Z',
164
+ businessNumber: 'PO-2026-0825',
165
+ },
166
+ },
167
+ fieldPolicy: { default: 'readonly', fields: {} },
168
+ operations: completed
169
+ ? []
170
+ : [
171
+ {
172
+ key: 'approve',
173
+ kind: 'workflow_command',
174
+ label: '同意',
175
+ group: 'decision',
176
+ audience: 'participant',
177
+ placement: 'primary',
178
+ emphasis: 'primary',
179
+ tone: 'primary',
180
+ visible: true,
181
+ enabled: true,
182
+ inputSchema: {
183
+ type: 'object',
184
+ properties: {
185
+ comment: {
186
+ type: 'string',
187
+ title: '审批意见',
188
+ format: 'textarea',
189
+ maxLength: 500,
190
+ },
191
+ },
192
+ },
193
+ uiSchema: {
194
+ confirmText: '确认同意',
195
+ confirmation: { description: '提交后流程继续流转。' },
196
+ },
197
+ execute: {
198
+ method: 'POST',
199
+ href: `/openxiangda-api/v2/applications/${appCode}/workflow/tasks/${taskId}/commands/approve`,
200
+ idempotencyRequired: true,
201
+ },
202
+ refresh: ['surface', 'timeline', 'work_center'],
203
+ },
204
+ ...['reject', 'return', 'transfer', 'add_assignee'].map(key => ({
205
+ key,
206
+ kind: 'workflow_command',
207
+ label: {
208
+ reject: '拒绝',
209
+ return: '退回',
210
+ transfer: '转交',
211
+ add_assignee: '加签',
212
+ }[key],
213
+ group: key === 'reject' ? 'decision' : 'task_collaboration',
214
+ audience: 'participant',
215
+ placement: key === 'reject' ? 'primary' : 'overflow',
216
+ emphasis:
217
+ key === 'reject'
218
+ ? 'danger'
219
+ : key === 'return'
220
+ ? 'warning'
221
+ : 'neutral',
222
+ tone: key === 'reject' ? 'danger' : 'neutral',
223
+ visible: true,
224
+ enabled: true,
225
+ inputSchema: { type: 'object', properties: {} },
226
+ uiSchema: { confirmText: `确认${key}` },
227
+ execute: {
228
+ method: 'POST',
229
+ href: `/openxiangda-api/v2/applications/${appCode}/workflow/tasks/${taskId}/commands/${key}`,
230
+ idempotencyRequired: true,
231
+ },
232
+ refresh: ['surface', 'timeline', 'work_center'],
233
+ })),
234
+ ],
235
+ extensions: {},
236
+ };
237
+ }
238
+
239
+ function timeline(completed = false) {
240
+ const flow = [
241
+ {
242
+ key: 'start',
243
+ nodeId: null,
244
+ kind: 'start',
245
+ title: '张三',
246
+ status: 'completed',
247
+ startedAt: '2026-08-25T01:00:00.000Z',
248
+ completedAt: '2026-08-25T01:00:00.000Z',
249
+ assignees: [],
250
+ operations: [],
251
+ },
252
+ {
253
+ key: 'manager',
254
+ nodeId: 'manager-review',
255
+ kind: 'approval',
256
+ title: '部门负责人',
257
+ status: completed ? 'completed' : 'active',
258
+ startedAt: '2026-08-25T01:01:00.000Z',
259
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
260
+ assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
261
+ operations: [{
262
+ id: 'operation-transfer',
263
+ operation: 'transfer',
264
+ operationLabel: '转交',
265
+ actorUserId: 'acceptance-user',
266
+ actorDisplayName: '李明',
267
+ actingForUserId: null,
268
+ reason: '由值班负责人继续处理',
269
+ detail: {},
270
+ severity: 'normal',
271
+ createdAt: '2026-08-25T01:08:00.000Z',
272
+ }],
273
+ },
274
+ {
275
+ key: 'finance',
276
+ nodeId: 'finance-review',
277
+ kind: 'approval',
278
+ title: '财务负责人',
279
+ status: completed ? 'completed' : 'waiting',
280
+ startedAt: null,
281
+ completedAt: null,
282
+ assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
283
+ operations: [],
284
+ },
285
+ ];
286
+ const items = [{
287
+ id: 'operation-transfer',
288
+ operation: 'transfer',
289
+ operationLabel: '转交',
290
+ actorUserId: 'acceptance-user',
291
+ actorDisplayName: '李明',
292
+ actingForUserId: null,
293
+ reason: '由值班负责人继续处理',
294
+ detail: {},
295
+ severity: 'normal',
296
+ createdAt: '2026-08-25T01:08:00.000Z',
297
+ }];
298
+ const display = {
299
+ entries: [
300
+ {
301
+ key: 'start',
302
+ kind: 'submission',
303
+ nodeId: null,
304
+ nodeKind: 'start',
305
+ title: '提交申请',
306
+ status: 'completed',
307
+ enteredAt: '2026-08-25T01:00:00.000Z',
308
+ leftAt: '2026-08-25T01:00:00.000Z',
309
+ primaryDisplayTime: '2026-08-25T01:00:00.000Z',
310
+ people: [{
311
+ userId: 'applicant-user',
312
+ displayName: '张三',
313
+ avatarUrl: null,
314
+ departmentDisplayName: '理学院',
315
+ }],
316
+ operations: [],
317
+ result: {},
318
+ terminalReason: null,
319
+ },
320
+ {
321
+ key: 'manager',
322
+ kind: 'node',
323
+ nodeId: 'manager-review',
324
+ nodeKind: 'approval',
325
+ title: '部门负责人',
326
+ status: completed ? 'completed' : 'active',
327
+ enteredAt: '2026-08-25T01:01:00.000Z',
328
+ leftAt: completed ? '2026-08-25T01:12:00.000Z' : null,
329
+ primaryDisplayTime: '2026-08-25T01:08:00.000Z',
330
+ people: [],
331
+ operations: [{
332
+ id: 'operation-transfer',
333
+ operation: 'transfer',
334
+ operationLabel: '转交',
335
+ actor: {
336
+ userId: 'acceptance-user',
337
+ displayName: '李明',
338
+ avatarUrl: null,
339
+ departmentDisplayName: null,
340
+ },
341
+ actingForUserId: null,
342
+ reason: '由值班负责人继续处理',
343
+ severity: 'normal',
344
+ occurredAt: '2026-08-25T01:08:00.000Z',
345
+ }],
346
+ result: {},
347
+ terminalReason: null,
348
+ },
349
+ {
350
+ key: 'finance',
351
+ kind: completed ? 'node' : 'planned_node',
352
+ nodeId: 'finance-review',
353
+ nodeKind: 'approval',
354
+ title: '财务负责人',
355
+ status: completed ? 'completed' : 'waiting',
356
+ enteredAt: null,
357
+ leftAt: null,
358
+ primaryDisplayTime: null,
359
+ people: [{
360
+ userId: 'finance-user',
361
+ displayName: '财务负责人',
362
+ avatarUrl: null,
363
+ departmentDisplayName: null,
364
+ }],
365
+ operations: [],
366
+ result: {},
367
+ terminalReason: null,
368
+ },
369
+ ],
370
+ };
371
+ return {
372
+ engineVersion: '2.0',
373
+ instanceId,
374
+ instanceSequence: completed ? 4 : 3,
375
+ timelineRevision: (completed ? 'f' : 'e').repeat(64),
376
+ flow,
377
+ items,
378
+ display,
379
+ };
380
+ }
381
+
382
+ function detail(completed = false, customDetail = false) {
383
+ const currentSurface = surface(completed, customDetail);
384
+ const currentTimeline = timeline(completed);
385
+ return {
386
+ schemaVersion: 'openxiangda.workflow-detail-surface/v2',
387
+ protocolVersion: 'workflow_detail_surface_v2',
388
+ detailRevision: (completed ? '2' : '1').repeat(64),
389
+ instanceSequence: currentSurface.instanceSequence,
390
+ timelineRevision: currentTimeline.timelineRevision,
391
+ surface: currentSurface,
392
+ timeline: currentTimeline,
393
+ currentStatus: {
394
+ code: completed ? 'approved' : 'assigned',
395
+ label: completed ? '已同意' : '待审批',
396
+ tone: completed ? 'success' : 'warning',
397
+ },
398
+ nodes: currentTimeline.display.entries,
399
+ handlingRecords: currentTimeline.display.entries.flatMap(
400
+ entry => entry.operations
401
+ ),
402
+ operations: currentSurface.operations,
403
+ navigationContext: {
404
+ desktopReturnPath: '/admin/work-center',
405
+ mobileReturnPath: '/m/work-center',
406
+ },
407
+ };
408
+ }
409
+
410
+ function launchSurface() {
411
+ return {
412
+ schemaVersion: 'openxiangda.workflow-launch-surface/v2',
413
+ protocolVersion: 'workflow_launch_surface_v2',
414
+ surfaceRevision: 'launch-revision-1',
415
+ engineVersion: '2.0',
416
+ appCode,
417
+ environmentKey: 'preproduction',
418
+ environmentId: 'preproduction-id',
419
+ workflowCode: 'purchase-approval',
420
+ title: '采购审批',
421
+ launchMode: 'standalone',
422
+ processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
423
+ subject: {
424
+ resourceCode: 'purchase-orders',
425
+ factProjection: { amount: 'amount' },
426
+ summaryFields: ['amount'],
427
+ },
428
+ inputSchema: { type: 'object', properties: {} },
429
+ head: {
430
+ workflowRevision: 4,
431
+ definitionVersion: 1,
432
+ bindingVersion: 1,
433
+ nativeRevision: 3,
434
+ contractRevisionId: 'contract-revision-1',
435
+ },
436
+ paths: {
437
+ desktop: '/admin/workflows/purchase-approval/new',
438
+ mobile: '/m/workflows/purchase-approval/start',
439
+ },
440
+ commit: {
441
+ method: 'POST',
442
+ href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
443
+ idempotencyRequired: true,
444
+ },
445
+ };
446
+ }
447
+
448
+ function processCommand(status: 'accepted' | 'started' = 'started') {
449
+ return {
450
+ schemaVersion: 'openxiangda.business-process.command/v2',
451
+ id: commandId,
452
+ appCode,
453
+ environmentKey: 'preproduction',
454
+ operationCode: 'openxiangda.workflow.purchase-approval.submit',
455
+ idempotencyKey: 'process:purchase-approval:test',
456
+ workflowCode: 'purchase-approval',
457
+ status,
458
+ revision: status === 'started' ? 4 : 1,
459
+ subject: {
460
+ resourceCode: 'purchase-orders',
461
+ id: '66666666-6666-4666-8666-666666666666',
462
+ dataRevision: 1,
463
+ factDigest: 'a'.repeat(64),
464
+ },
465
+ definitionVersion: 1,
466
+ bindingVersion: 1,
467
+ requirements: [],
468
+ answers: {},
469
+ preview: {},
470
+ workflowInstanceId: status === 'started' ? instanceId : null,
471
+ attemptCount: 1,
472
+ lastError: null,
473
+ replayed: false,
474
+ createdAt: '2026-08-29T01:00:00.000Z',
475
+ updatedAt: '2026-08-29T01:00:01.000Z',
476
+ };
477
+ }
478
+
479
+ function processCommandSurface() {
480
+ const command = processCommand('started');
481
+ const base = `/openxiangda-api/v2/applications/${appCode}/business-process/commands/${commandId}`;
482
+ return {
483
+ schemaVersion: 'openxiangda.process-command-surface/v2',
484
+ surfaceRevision: 'b'.repeat(64),
485
+ command,
486
+ subject: {
487
+ resourceCode: 'purchase-orders',
488
+ recordId: command.subject.id,
489
+ dataRevision: 1,
490
+ form: {
491
+ kind: 'native-resource-form',
492
+ resourceCode: 'purchase-orders',
493
+ recordId: command.subject.id,
494
+ },
495
+ },
496
+ requirements: [],
497
+ resume: {
498
+ status: { method: 'GET', href: base },
499
+ surface: { method: 'GET', href: `${base}/surface` },
500
+ answer: null,
501
+ retry: null,
502
+ navigationTarget: {
503
+ kind: 'PLATFORM_ROUTE',
504
+ routeCode: 'resource.record',
505
+ appCode,
506
+ pathParams: {
507
+ resourceCode: 'purchase-orders',
508
+ recordId: command.subject.id,
509
+ },
510
+ query: { processCommandId: commandId },
511
+ access: 'AUTHENTICATED',
512
+ },
513
+ desktop: `/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
514
+ mobile: `/m/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
515
+ },
516
+ };
517
+ }
518
+
519
+ function todoPage() {
520
+ return {
521
+ schemaVersion: 'openxiangda.application-todo-center/v2',
522
+ appCode,
523
+ environmentKey: 'preproduction',
524
+ view: 'pending',
525
+ counts: { pending: 1, informational: 2, completed: 3, unread: 1 },
526
+ items: [
527
+ {
528
+ messageId: '33333333-3333-4333-8333-333333333333',
529
+ recipientId: '44444444-4444-4444-8444-444444444444',
530
+ state: 'action_required',
531
+ interactionState: 'unread',
532
+ title: '设备采购申请待审批',
533
+ summary: '张三提交了一笔办公设备采购申请',
534
+ fields: [
535
+ { code: 'amount', label: '申请金额', value: '¥28,600' },
536
+ { code: 'department', label: '申请部门', value: '理学院' },
537
+ ],
538
+ actions: [{ code: 'open', label: '去处理' }],
539
+ sourceKind: 'PLATFORM_EVENT',
540
+ sourceLabel: '流程中心',
541
+ occurredAt: '2026-08-28T01:00:00.000Z',
542
+ updatedAt: '2026-08-28T01:01:00.000Z',
543
+ navigation: {
544
+ desktopPath: `/admin/operations/purchases/${instanceId}?taskId=${taskId}`,
545
+ mobilePath: `/m/purchases/${instanceId}?taskId=${taskId}`,
546
+ external: false,
547
+ navigationUnavailable: false,
548
+ },
549
+ },
550
+ ],
551
+ total: 1,
552
+ limit: 12,
553
+ offset: 0,
554
+ nextOffset: null,
555
+ };
556
+ }
557
+
558
+ async function mockWorkflow(
559
+ page: Page,
560
+ options: {
561
+ commandConflict?: boolean;
562
+ forbidden?: boolean;
563
+ customDetail?: boolean;
564
+ } = {}
565
+ ) {
566
+ let completed = false;
567
+ await page.route('**/service/**', async route => {
568
+ const url = new URL(route.request().url());
569
+ const path = url.pathname;
570
+ if (path.endsWith('/auth/surface')) {
571
+ return route.fulfill({
572
+ contentType: 'application/json',
573
+ body: JSON.stringify(envelope({ csrfToken: 'workflow-csrf-1' })),
574
+ });
575
+ }
576
+ if (path.endsWith('/native/authz/current')) {
577
+ return route.fulfill({
578
+ contentType: 'application/json',
579
+ body: JSON.stringify(
580
+ envelope({
581
+ schemaVersion: 'openxiangda.runtime-authorization/v2',
582
+ state: 'active',
583
+ environment: {
584
+ id: 'preproduction-id',
585
+ key: 'preproduction',
586
+ activeAppVersionId: 'version-1',
587
+ headRevision: 1,
588
+ authzRevisionId: 'authz-1',
589
+ authzVersion: 1,
590
+ scopeDataVersion: 'scope-1',
591
+ },
592
+ subjectProfile: {
593
+ schemaVersion: 'openxiangda.subject-profile/v2',
594
+ userId: 'acceptance-user',
595
+ displayName: '王老师',
596
+ avatarUrl: null,
597
+ jobNumber: 'T001',
598
+ affiliatedDepartment: { id: 'department-1', name: '理学院' },
599
+ },
600
+ roles: [
601
+ {
602
+ code: 'department_manager',
603
+ name: '部门管理员',
604
+ source: 'package',
605
+ },
606
+ ],
607
+ principal: {
608
+ type: 'user_union',
609
+ userId: 'acceptance-user',
610
+ roleCodes: ['department_manager'],
611
+ capabilityCodes: [],
612
+ isAppSuperAdmin: true,
613
+ identityScope:
614
+ 'user-union:preproduction-id:acceptance-user:authz-1',
615
+ },
616
+ })
617
+ ),
618
+ });
619
+ }
620
+ if (path.includes('/directory/')) {
621
+ return route.fulfill({
622
+ contentType: 'application/json',
623
+ body: JSON.stringify(envelope({ items: [], nextCursor: null })),
624
+ });
625
+ }
626
+ if (options.forbidden && path.includes('/workflow/')) {
627
+ return route.fulfill({
628
+ status: 403,
629
+ contentType: 'application/json',
630
+ body: JSON.stringify(envelope(null, 403)),
631
+ });
632
+ }
633
+ if (path.endsWith('/todos') && route.request().method() === 'GET') {
634
+ return route.fulfill({
635
+ contentType: 'application/json',
636
+ body: JSON.stringify(envelope(todoPage())),
637
+ });
638
+ }
639
+ if (path.includes('/todos/') && path.endsWith('/interactions')) {
640
+ return route.fulfill({
641
+ contentType: 'application/json',
642
+ body: JSON.stringify(
643
+ envelope({
644
+ accepted: true,
645
+ duplicate: false,
646
+ interactionState: 'clicked',
647
+ })
648
+ ),
649
+ });
650
+ }
651
+ if (
652
+ path.endsWith(
653
+ '/workflow/definitions/purchase-approval/launch-surface'
654
+ )
655
+ ) {
656
+ return route.fulfill({
657
+ contentType: 'application/json',
658
+ body: JSON.stringify(envelope(launchSurface())),
659
+ });
660
+ }
661
+ if (
662
+ path.endsWith('/business-process/standard-commands') &&
663
+ route.request().method() === 'POST'
664
+ ) {
665
+ const body = route.request().postDataJSON();
666
+ expect(body).toMatchObject({
667
+ schemaVersion: 'openxiangda.standard-process.commit/v2',
668
+ environmentKey: 'preproduction',
669
+ workflowCode: 'purchase-approval',
670
+ mutation: { kind: 'create', data: { amount: '28600' } },
671
+ });
672
+ for (const forbidden of [
673
+ 'facts',
674
+ 'dataRef',
675
+ 'preparationToken',
676
+ 'processOperationCode',
677
+ ]) {
678
+ expect(body).not.toHaveProperty(forbidden);
679
+ }
680
+ return route.fulfill({
681
+ contentType: 'application/json',
682
+ body: JSON.stringify(envelope(processCommand('accepted'))),
683
+ });
684
+ }
685
+ if (
686
+ path.endsWith(`/business-process/commands/${commandId}/surface`) &&
687
+ route.request().method() === 'GET'
688
+ ) {
689
+ return route.fulfill({
690
+ contentType: 'application/json',
691
+ body: JSON.stringify(envelope(processCommandSurface())),
692
+ });
693
+ }
694
+ if (path.endsWith('/workflow/work-center/items')) {
695
+ return route.fulfill({
696
+ contentType: 'application/json',
697
+ body: JSON.stringify(
698
+ envelope({
699
+ engineVersion: '2.0',
700
+ authorizationMode: 'current_user_role_union',
701
+ authorizationDigest: 'digest-1',
702
+ status: 'pending',
703
+ total: 1,
704
+ limit: 20,
705
+ offset: 0,
706
+ items: [task(completed, options.customDetail)],
707
+ })
708
+ ),
709
+ });
710
+ }
711
+ if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
712
+ return route.fulfill({
713
+ contentType: 'application/json',
714
+ body: JSON.stringify(
715
+ envelope(surface(completed, options.customDetail))
716
+ ),
717
+ });
718
+ }
719
+ if (
720
+ path.endsWith(`/workflow/tasks/${taskId}/detail`) ||
721
+ path.endsWith(`/workflow/instances/${instanceId}/detail`)
722
+ ) {
723
+ expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
724
+ 'workflow-csrf-1'
725
+ );
726
+ return route.fulfill({
727
+ contentType: 'application/json',
728
+ body: JSON.stringify(
729
+ envelope(detail(completed, options.customDetail))
730
+ ),
731
+ });
732
+ }
733
+ if (path.endsWith(`/workflow/instances/${instanceId}/surface`)) {
734
+ return route.fulfill({
735
+ contentType: 'application/json',
736
+ body: JSON.stringify(
737
+ envelope(surface(completed, options.customDetail))
738
+ ),
739
+ });
740
+ }
741
+ if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
742
+ return route.fulfill({
743
+ contentType: 'application/json',
744
+ body: JSON.stringify(envelope(timeline(completed))),
745
+ });
746
+ }
747
+ if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
748
+ expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
749
+ 'workflow-csrf-1'
750
+ );
751
+ expect(route.request().postDataJSON()).toMatchObject({
752
+ commandToken: 'A'.repeat(43),
753
+ input: { comment: '同意采购' },
754
+ });
755
+ expect(route.request().postDataJSON()).not.toHaveProperty(
756
+ 'expectedTaskVersion'
757
+ );
758
+ expect(route.request().postDataJSON()).not.toHaveProperty(
759
+ 'expectedInstanceVersion'
760
+ );
761
+ if (options.commandConflict) {
762
+ return route.fulfill({
763
+ status: 409,
764
+ contentType: 'application/json',
765
+ body: JSON.stringify(
766
+ envelope({ freshSurface: surface(false, options.customDetail) }, 409)
767
+ ),
768
+ });
769
+ }
770
+ completed = true;
771
+ return route.fulfill({
772
+ contentType: 'application/json',
773
+ body: JSON.stringify(envelope({ status: 'completed' })),
774
+ });
775
+ }
776
+ return route.fulfill({
777
+ status: 404,
778
+ contentType: 'application/json',
779
+ body: JSON.stringify(envelope(null, 404)),
780
+ });
781
+ });
782
+ }
783
+
784
+ test('renders a paged desktop work center from the current-user role union', async ({
785
+ page,
786
+ }) => {
787
+ await mockWorkflow(page);
788
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center');
789
+ await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
790
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
791
+ await expect(page.getByText('PO-2026-0825')).toBeVisible();
792
+ });
793
+
794
+ test('renders the generic desktop and mobile application todo center without a second router', async ({
795
+ page,
796
+ }) => {
797
+ await mockWorkflow(page);
798
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/todos');
799
+ await expect(page.getByRole('heading', { name: '待办中心' })).toBeVisible();
800
+ await expect(page.locator('.oxa-todo-table')).toBeVisible();
801
+ await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
802
+ await expect(page.getByText('设备采购申请待审批').first()).toBeVisible();
803
+ await expect(page.getByText('¥28,600').first()).toBeVisible();
804
+ await expect(page.locator('.ant-spin-spinning')).toHaveCount(0);
805
+ const unreadRequest = page.waitForRequest(request => {
806
+ const url = new URL(request.url());
807
+ return url.pathname.endsWith('/todos') && url.searchParams.get('unread') === 'true';
808
+ });
809
+ await page.getByRole('switch', { name: '仅看未读' }).click();
810
+ await unreadRequest;
811
+ await page.getByRole('button', { name: /查看详情/ }).first().click();
812
+ await expect(page).toHaveURL(
813
+ new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}`)
814
+ );
815
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
816
+ `${instanceId} / ${taskId}`
817
+ );
818
+
819
+ await page.setViewportSize({ width: 390, height: 844 });
820
+ await page.goto('/workflow-experience.e2e.html?initial=/m/todos');
821
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
822
+ await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
823
+ await page.getByRole('button', { name: /查看详情/ }).first().click();
824
+ await expect(page).toHaveURL(
825
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}`)
826
+ );
827
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
828
+ `${instanceId} / ${taskId}`
829
+ );
830
+ });
831
+
832
+ test('launches the same workflow from the canonical mobile Surface and follows custom detail routing', async ({
833
+ page,
834
+ }) => {
835
+ await mockWorkflow(page, { customDetail: true });
836
+ await page.setViewportSize({ width: 390, height: 844 });
837
+ await page.goto(
838
+ '/workflow-experience.e2e.html?initial=/m/workflows/purchase-approval/start'
839
+ );
840
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
841
+ await expect(page.getByRole('heading', { name: '采购审批' })).toBeVisible();
842
+ await page.getByRole('spinbutton', { name: '请输入申请金额' }).fill('28600');
843
+ await page.getByRole('button', { name: '提交审批' }).click();
844
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
845
+ `${instanceId} / ${taskId}`
846
+ );
847
+ await expect(page).toHaveURL(
848
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
849
+ );
850
+ });
851
+
852
+ test('routes desktop and mobile work-center entries to the declared custom detail pages', async ({
853
+ page,
854
+ }) => {
855
+ await mockWorkflow(page, { customDetail: true });
856
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center');
857
+ await page.getByRole('link', { name: '采购申请审批' }).click();
858
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
859
+ `${instanceId} / ${taskId}`
860
+ );
861
+ await expect(page).toHaveURL(
862
+ new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}$`)
863
+ );
864
+
865
+ await page.setViewportSize({ width: 390, height: 844 });
866
+ await page.goto('/workflow-experience.e2e.html?initial=/m/work-center');
867
+ await page.getByText('采购申请审批').click();
868
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
869
+ `${instanceId} / ${taskId}`
870
+ );
871
+ await expect(page).toHaveURL(
872
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
873
+ );
874
+ });
875
+
876
+ test('redirects a direct standard task URL to the declared custom detail page', async ({
877
+ page,
878
+ }) => {
879
+ await mockWorkflow(page, { customDetail: true });
880
+ await page.goto(
881
+ `/workflow-experience.e2e.html?initial=/tasks/${taskId}`
882
+ );
883
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
884
+ `${instanceId} / ${taskId}`
885
+ );
886
+ });
887
+
888
+ test('renders a standalone desktop detail and redirects the legacy admin URL', async ({
889
+ page,
890
+ }) => {
891
+ await mockWorkflow(page);
892
+ await page.goto(
893
+ workflowFixtureUrl(`/admin/tasks/${taskId}?from=legacy#approval`),
894
+ );
895
+ await expect(page.locator('.oxa-workflow-desktop-standalone-page')).toBeVisible();
896
+ await expect(page).toHaveURL(
897
+ new RegExp(`/tasks/${taskId}\\?from=legacy#approval$`),
898
+ );
899
+ await expect(page.locator('.oxa-app-layout')).toHaveCount(0);
900
+ await expect(page.locator('.oxa-topbar')).toHaveCount(0);
901
+ await expect(page.getByText('¥30,000')).toBeVisible();
902
+ await expect(page.getByText('技术场地引用')).toHaveCount(0);
903
+ await expect(page.getByText('业务数据已变化')).toHaveCount(0);
904
+ await expect(page.getByText('流程信息')).toHaveCount(0);
905
+ await expect(page.getByText('操作记录')).toHaveCount(0);
906
+ await expect(page.getByText('由值班负责人继续处理')).toHaveCount(1);
907
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
908
+ await expect(page.getByRole('button', { name: /拒\s*绝/ })).toBeVisible();
909
+ });
910
+
911
+ test('shows a friendly refresh prompt only after a real command conflict', async ({
912
+ page,
913
+ }) => {
914
+ await mockWorkflow(page, { commandConflict: true });
915
+ await page.goto(workflowFixtureUrl(`/tasks/${taskId}`));
916
+ await expect(page.getByText('业务数据已变化')).toHaveCount(0);
917
+ await page.getByRole('button', { name: /同\s*意/ }).click();
918
+ await page.getByLabel('审批意见').fill('同意采购');
919
+ await page.getByRole('button', { name: '确认同意' }).click();
920
+ await expect(page.getByText('内容已更新,请刷新后重试')).toBeVisible();
921
+ });
922
+
923
+ test('renders the independent mobile task and executes only a Surface operation', async ({
924
+ page,
925
+ }) => {
926
+ await mockWorkflow(page);
927
+ await page.setViewportSize({ width: 390, height: 844 });
928
+ await page.goto(`/workflow-experience.e2e.html?initial=/m/tasks/${taskId}`);
929
+ await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
930
+ await expect(page.getByText('¥30,000')).toBeVisible();
931
+ await expect(page.getByText('财务负责人').first()).toBeVisible();
932
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
933
+ await page.getByRole('button', { name: /同\s*意/ }).click();
934
+ await page.getByLabel('审批意见').fill('同意采购');
935
+ await page.getByRole('button', { name: '确认同意' }).click();
936
+ await expect(page.getByText('同意已提交')).toBeVisible();
937
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
938
+ });
939
+
940
+ test('shows an explicit authorization error instead of an empty work center', async ({
941
+ page,
942
+ }) => {
943
+ test.setTimeout(45_000);
944
+ await mockWorkflow(page, { forbidden: true });
945
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center', {
946
+ waitUntil: 'domcontentloaded',
947
+ });
948
+ await expect(page.getByRole('alert')).toContainText('403: forbidden', {
949
+ timeout: 30_000,
950
+ });
951
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
952
+ });
953
+
954
+ test('negotiates direct standard routes after BrowserRouter listener setup', async ({
955
+ page,
956
+ }) => {
957
+ await mockWorkflow(page);
958
+ await page.setViewportSize({ width: 360, height: 844 });
959
+ await page.goto(
960
+ workflowFixtureUrl('/admin/todos?view=pending#unread', {
961
+ state: 'deep-link',
962
+ }),
963
+ );
964
+
965
+ // The initial desktop URL is replaced by the mobile pair and the rendered
966
+ // surface follows the Router update. The fixture is StrictMode-mounted.
967
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
968
+ await expect(page.locator('.oxa-todo-center-desktop')).toHaveCount(0);
969
+ await expect(page).toHaveURL(/\/m\/todos\?view=pending#unread$/);
970
+ const historyLength = await page.evaluate(() => window.history.length);
971
+ const negotiatedUrl = page.url();
972
+ await expect
973
+ .poll(() => page.url(), { timeout: 1_000 })
974
+ .toBe(negotiatedUrl);
975
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
976
+ routeNegotiationState: 'deep-link',
977
+ });
978
+
979
+ // 900px remains mobile; crossing to 901px switches the same manifest pair
980
+ // back to desktop without adding a browser-history entry.
981
+ await page.setViewportSize({ width: 900, height: 844 });
982
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
983
+ await page.setViewportSize({ width: 901, height: 844 });
984
+ await expect(page.locator('.oxa-todo-center-desktop')).toBeVisible();
985
+ await expect(page).toHaveURL(/\/admin\/todos\?view=pending#unread$/);
986
+ expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
987
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
988
+ routeNegotiationState: 'deep-link',
989
+ });
990
+ await page.goBack();
991
+ await expect(page).not.toHaveURL(/\/admin\/todos\?view=pending#unread$/);
992
+
993
+ // Direct task and instance URLs carry their dynamic parameter and deep-link
994
+ // context to the independent mobile renderer at the narrow width.
995
+ await page.setViewportSize({ width: 360, height: 844 });
996
+ await page.goto(
997
+ workflowFixtureUrl(`/tasks/${taskId}?from=todo#approval`),
998
+ );
999
+ await expect(page.locator('.oxa-workflow-detail-mobile-native')).toBeVisible();
1000
+ await expect(page).toHaveURL(
1001
+ new RegExp(`/m/tasks/${taskId}\\?from=todo#approval$`),
1002
+ );
1003
+
1004
+ await page.goto(
1005
+ workflowFixtureUrl(`/workflows/${instanceId}?from=todo#timeline`),
1006
+ );
1007
+ await expect(page.locator('.oxa-workflow-detail-mobile-native')).toBeVisible();
1008
+ await expect(page).toHaveURL(
1009
+ new RegExp(`/m/workflows/${instanceId}\\?from=todo#timeline$`),
1010
+ );
1011
+ });
1012
+
1013
+ test('negotiates launch pairs in both directions and preserves the mount basename', async ({
1014
+ page,
1015
+ }) => {
1016
+ await mockWorkflow(page);
1017
+ const base = '/runtime/route-negotiation';
1018
+ await page.setViewportSize({ width: 360, height: 844 });
1019
+ await page.goto(
1020
+ workflowFixtureUrl(
1021
+ `${base}/admin/workflows/purchase-approval/new?source=todo#launch`,
1022
+ { base },
1023
+ ),
1024
+ );
1025
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
1026
+ await expect(page).toHaveURL(
1027
+ new RegExp(
1028
+ `${base}/m/workflows/purchase-approval/start\\?source=todo#launch$`,
1029
+ ),
1030
+ );
1031
+
1032
+ await page.setViewportSize({ width: 1_440, height: 900 });
1033
+ await expect(page.locator('.oxa-workflow-submission-desktop')).toBeVisible();
1034
+ await expect(page).toHaveURL(
1035
+ new RegExp(
1036
+ `${base}/admin/workflows/purchase-approval/new\\?source=todo#launch$`,
1037
+ ),
1038
+ );
1039
+ });
1040
+
1041
+ test('does not negotiate non-standard application contribution routes', async ({
1042
+ page,
1043
+ }) => {
1044
+ await mockWorkflow(page);
1045
+ const base = '/runtime/route-negotiation';
1046
+ await page.setViewportSize({ width: 360, height: 844 });
1047
+ const customPath = `${base}/admin/operations/purchases/${instanceId}?taskId=${taskId}`;
1048
+ await page.goto(workflowFixtureUrl(customPath, { base }));
1049
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
1050
+ `${instanceId} / ${taskId}`,
1051
+ );
1052
+ const customUrl = page.url();
1053
+ await expect(page).toHaveURL(new RegExp(`${base}/admin/operations/`));
1054
+ await expect
1055
+ .poll(() => page.url(), { timeout: 1_000 })
1056
+ .toBe(customUrl);
1057
+ });