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

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 +937 -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,937 @@
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 ? 'revision-2' : 'revision-1',
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: '/workflow/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: `/workflow/${key}`,
230
+ idempotencyRequired: true,
231
+ },
232
+ refresh: ['surface', 'timeline', 'work_center'],
233
+ })),
234
+ ],
235
+ extensions: {},
236
+ };
237
+ }
238
+
239
+ function timeline(completed = false) {
240
+ return {
241
+ engineVersion: '2.0',
242
+ instanceId,
243
+ flow: [
244
+ {
245
+ key: 'start',
246
+ nodeId: null,
247
+ kind: 'start',
248
+ title: '张三',
249
+ status: 'completed',
250
+ startedAt: '2026-08-25T01:00:00.000Z',
251
+ completedAt: '2026-08-25T01:00:00.000Z',
252
+ assignees: [],
253
+ operations: [],
254
+ },
255
+ {
256
+ key: 'manager',
257
+ nodeId: 'manager-review',
258
+ kind: 'approval',
259
+ title: '部门负责人',
260
+ status: completed ? 'completed' : 'active',
261
+ startedAt: '2026-08-25T01:01:00.000Z',
262
+ completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
263
+ assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
264
+ operations: [{
265
+ id: 'operation-transfer',
266
+ operation: 'transfer',
267
+ operationLabel: '转交',
268
+ actorUserId: 'acceptance-user',
269
+ actorDisplayName: '李明',
270
+ actingForUserId: null,
271
+ reason: '由值班负责人继续处理',
272
+ detail: {},
273
+ severity: 'normal',
274
+ createdAt: '2026-08-25T01:08:00.000Z',
275
+ }],
276
+ },
277
+ {
278
+ key: 'finance',
279
+ nodeId: 'finance-review',
280
+ kind: 'approval',
281
+ title: '财务负责人',
282
+ status: completed ? 'completed' : 'waiting',
283
+ startedAt: null,
284
+ completedAt: null,
285
+ assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
286
+ operations: [],
287
+ },
288
+ ],
289
+ items: [{
290
+ id: 'operation-transfer',
291
+ operation: 'transfer',
292
+ operationLabel: '转交',
293
+ actorUserId: 'acceptance-user',
294
+ actorDisplayName: '李明',
295
+ actingForUserId: null,
296
+ reason: '由值班负责人继续处理',
297
+ detail: {},
298
+ severity: 'normal',
299
+ createdAt: '2026-08-25T01:08:00.000Z',
300
+ }],
301
+ };
302
+ }
303
+
304
+ function launchSurface() {
305
+ return {
306
+ schemaVersion: 'openxiangda.workflow-launch-surface/v2',
307
+ protocolVersion: 'workflow_launch_surface_v2',
308
+ surfaceRevision: 'launch-revision-1',
309
+ engineVersion: '2.0',
310
+ appCode,
311
+ environmentKey: 'preproduction',
312
+ environmentId: 'preproduction-id',
313
+ workflowCode: 'purchase-approval',
314
+ title: '采购审批',
315
+ launchMode: 'standalone',
316
+ processOperationCode: 'openxiangda.workflow.purchase-approval.submit',
317
+ subject: {
318
+ resourceCode: 'purchase-orders',
319
+ factProjection: { amount: 'amount' },
320
+ summaryFields: ['amount'],
321
+ },
322
+ inputSchema: { type: 'object', properties: {} },
323
+ head: {
324
+ workflowRevision: 4,
325
+ definitionVersion: 1,
326
+ bindingVersion: 1,
327
+ nativeRevision: 3,
328
+ contractRevisionId: 'contract-revision-1',
329
+ },
330
+ paths: {
331
+ desktop: '/admin/workflows/purchase-approval/new',
332
+ mobile: '/m/workflows/purchase-approval/start',
333
+ },
334
+ commit: {
335
+ method: 'POST',
336
+ href: `/openxiangda-api/v2/applications/${appCode}/business-process/standard-commands`,
337
+ idempotencyRequired: true,
338
+ },
339
+ };
340
+ }
341
+
342
+ function processCommand(status: 'accepted' | 'started' = 'started') {
343
+ return {
344
+ schemaVersion: 'openxiangda.business-process.command/v2',
345
+ id: commandId,
346
+ appCode,
347
+ environmentKey: 'preproduction',
348
+ operationCode: 'openxiangda.workflow.purchase-approval.submit',
349
+ idempotencyKey: 'process:purchase-approval:test',
350
+ workflowCode: 'purchase-approval',
351
+ status,
352
+ revision: status === 'started' ? 4 : 1,
353
+ subject: {
354
+ resourceCode: 'purchase-orders',
355
+ id: '66666666-6666-4666-8666-666666666666',
356
+ dataRevision: 1,
357
+ factDigest: 'a'.repeat(64),
358
+ },
359
+ definitionVersion: 1,
360
+ bindingVersion: 1,
361
+ requirements: [],
362
+ answers: {},
363
+ preview: {},
364
+ workflowInstanceId: status === 'started' ? instanceId : null,
365
+ attemptCount: 1,
366
+ lastError: null,
367
+ replayed: false,
368
+ createdAt: '2026-08-29T01:00:00.000Z',
369
+ updatedAt: '2026-08-29T01:00:01.000Z',
370
+ };
371
+ }
372
+
373
+ function processCommandSurface() {
374
+ const command = processCommand('started');
375
+ const base = `/openxiangda-api/v2/applications/${appCode}/business-process/commands/${commandId}`;
376
+ return {
377
+ schemaVersion: 'openxiangda.process-command-surface/v2',
378
+ surfaceRevision: 'b'.repeat(64),
379
+ command,
380
+ subject: {
381
+ resourceCode: 'purchase-orders',
382
+ recordId: command.subject.id,
383
+ dataRevision: 1,
384
+ form: {
385
+ kind: 'native-resource-form',
386
+ resourceCode: 'purchase-orders',
387
+ recordId: command.subject.id,
388
+ },
389
+ },
390
+ requirements: [],
391
+ resume: {
392
+ status: { method: 'GET', href: base },
393
+ surface: { method: 'GET', href: `${base}/surface` },
394
+ answer: null,
395
+ retry: null,
396
+ navigationTarget: {
397
+ kind: 'PLATFORM_ROUTE',
398
+ routeCode: 'resource.record',
399
+ appCode,
400
+ pathParams: {
401
+ resourceCode: 'purchase-orders',
402
+ recordId: command.subject.id,
403
+ },
404
+ query: { processCommandId: commandId },
405
+ access: 'AUTHENTICATED',
406
+ },
407
+ desktop: `/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
408
+ mobile: `/m/purchase-orders/${command.subject.id}?processCommandId=${commandId}`,
409
+ },
410
+ };
411
+ }
412
+
413
+ function todoPage() {
414
+ return {
415
+ schemaVersion: 'openxiangda.application-todo-center/v2',
416
+ appCode,
417
+ environmentKey: 'preproduction',
418
+ view: 'pending',
419
+ counts: { pending: 1, informational: 2, completed: 3, unread: 1 },
420
+ items: [
421
+ {
422
+ messageId: '33333333-3333-4333-8333-333333333333',
423
+ recipientId: '44444444-4444-4444-8444-444444444444',
424
+ state: 'action_required',
425
+ interactionState: 'unread',
426
+ title: '设备采购申请待审批',
427
+ summary: '张三提交了一笔办公设备采购申请',
428
+ fields: [
429
+ { code: 'amount', label: '申请金额', value: '¥28,600' },
430
+ { code: 'department', label: '申请部门', value: '理学院' },
431
+ ],
432
+ actions: [{ code: 'open', label: '去处理' }],
433
+ sourceKind: 'PLATFORM_EVENT',
434
+ sourceLabel: '流程中心',
435
+ occurredAt: '2026-08-28T01:00:00.000Z',
436
+ updatedAt: '2026-08-28T01:01:00.000Z',
437
+ navigation: {
438
+ desktopPath: `/admin/operations/purchases/${instanceId}?taskId=${taskId}`,
439
+ mobilePath: `/m/purchases/${instanceId}?taskId=${taskId}`,
440
+ external: false,
441
+ navigationUnavailable: false,
442
+ },
443
+ },
444
+ ],
445
+ total: 1,
446
+ limit: 12,
447
+ offset: 0,
448
+ nextOffset: null,
449
+ };
450
+ }
451
+
452
+ async function mockWorkflow(
453
+ page: Page,
454
+ options: {
455
+ commandConflict?: boolean;
456
+ forbidden?: boolean;
457
+ customDetail?: boolean;
458
+ } = {}
459
+ ) {
460
+ let completed = false;
461
+ await page.route('**/service/**', async route => {
462
+ const url = new URL(route.request().url());
463
+ const path = url.pathname;
464
+ if (path.endsWith('/auth/surface')) {
465
+ return route.fulfill({
466
+ contentType: 'application/json',
467
+ body: JSON.stringify(envelope({ csrfToken: 'workflow-csrf-1' })),
468
+ });
469
+ }
470
+ if (path.endsWith('/native/authz/current')) {
471
+ return route.fulfill({
472
+ contentType: 'application/json',
473
+ body: JSON.stringify(
474
+ envelope({
475
+ schemaVersion: 'openxiangda.runtime-authorization/v2',
476
+ state: 'active',
477
+ environment: {
478
+ id: 'preproduction-id',
479
+ key: 'preproduction',
480
+ activeAppVersionId: 'version-1',
481
+ headRevision: 1,
482
+ authzRevisionId: 'authz-1',
483
+ authzVersion: 1,
484
+ scopeDataVersion: 'scope-1',
485
+ },
486
+ subjectProfile: {
487
+ schemaVersion: 'openxiangda.subject-profile/v2',
488
+ userId: 'acceptance-user',
489
+ displayName: '王老师',
490
+ avatarUrl: null,
491
+ jobNumber: 'T001',
492
+ affiliatedDepartment: { id: 'department-1', name: '理学院' },
493
+ },
494
+ roles: [
495
+ {
496
+ code: 'department_manager',
497
+ name: '部门管理员',
498
+ source: 'package',
499
+ },
500
+ ],
501
+ principal: {
502
+ type: 'user_union',
503
+ userId: 'acceptance-user',
504
+ roleCodes: ['department_manager'],
505
+ capabilityCodes: [],
506
+ isAppSuperAdmin: true,
507
+ identityScope:
508
+ 'user-union:preproduction-id:acceptance-user:authz-1',
509
+ },
510
+ })
511
+ ),
512
+ });
513
+ }
514
+ if (path.includes('/directory/')) {
515
+ return route.fulfill({
516
+ contentType: 'application/json',
517
+ body: JSON.stringify(envelope({ items: [], nextCursor: null })),
518
+ });
519
+ }
520
+ if (options.forbidden && path.includes('/workflow/')) {
521
+ return route.fulfill({
522
+ status: 403,
523
+ contentType: 'application/json',
524
+ body: JSON.stringify(envelope(null, 403)),
525
+ });
526
+ }
527
+ if (path.endsWith('/todos') && route.request().method() === 'GET') {
528
+ return route.fulfill({
529
+ contentType: 'application/json',
530
+ body: JSON.stringify(envelope(todoPage())),
531
+ });
532
+ }
533
+ if (path.includes('/todos/') && path.endsWith('/interactions')) {
534
+ return route.fulfill({
535
+ contentType: 'application/json',
536
+ body: JSON.stringify(
537
+ envelope({
538
+ accepted: true,
539
+ duplicate: false,
540
+ interactionState: 'clicked',
541
+ })
542
+ ),
543
+ });
544
+ }
545
+ if (
546
+ path.endsWith(
547
+ '/workflow/definitions/purchase-approval/launch-surface'
548
+ )
549
+ ) {
550
+ return route.fulfill({
551
+ contentType: 'application/json',
552
+ body: JSON.stringify(envelope(launchSurface())),
553
+ });
554
+ }
555
+ if (
556
+ path.endsWith('/business-process/standard-commands') &&
557
+ route.request().method() === 'POST'
558
+ ) {
559
+ const body = route.request().postDataJSON();
560
+ expect(body).toMatchObject({
561
+ schemaVersion: 'openxiangda.standard-process.commit/v2',
562
+ environmentKey: 'preproduction',
563
+ workflowCode: 'purchase-approval',
564
+ mutation: { kind: 'create', data: { amount: '28600' } },
565
+ });
566
+ for (const forbidden of [
567
+ 'facts',
568
+ 'dataRef',
569
+ 'preparationToken',
570
+ 'processOperationCode',
571
+ ]) {
572
+ expect(body).not.toHaveProperty(forbidden);
573
+ }
574
+ return route.fulfill({
575
+ contentType: 'application/json',
576
+ body: JSON.stringify(envelope(processCommand('accepted'))),
577
+ });
578
+ }
579
+ if (
580
+ path.endsWith(`/business-process/commands/${commandId}/surface`) &&
581
+ route.request().method() === 'GET'
582
+ ) {
583
+ return route.fulfill({
584
+ contentType: 'application/json',
585
+ body: JSON.stringify(envelope(processCommandSurface())),
586
+ });
587
+ }
588
+ if (path.endsWith('/workflow/work-center/items')) {
589
+ return route.fulfill({
590
+ contentType: 'application/json',
591
+ body: JSON.stringify(
592
+ envelope({
593
+ engineVersion: '2.0',
594
+ authorizationMode: 'current_user_role_union',
595
+ authorizationDigest: 'digest-1',
596
+ status: 'pending',
597
+ total: 1,
598
+ limit: 20,
599
+ offset: 0,
600
+ items: [task(completed, options.customDetail)],
601
+ })
602
+ ),
603
+ });
604
+ }
605
+ if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
606
+ return route.fulfill({
607
+ contentType: 'application/json',
608
+ body: JSON.stringify(
609
+ envelope(surface(completed, options.customDetail))
610
+ ),
611
+ });
612
+ }
613
+ if (path.endsWith(`/workflow/instances/${instanceId}/surface`)) {
614
+ return route.fulfill({
615
+ contentType: 'application/json',
616
+ body: JSON.stringify(
617
+ envelope(surface(completed, options.customDetail))
618
+ ),
619
+ });
620
+ }
621
+ if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
622
+ return route.fulfill({
623
+ contentType: 'application/json',
624
+ body: JSON.stringify(envelope(timeline(completed))),
625
+ });
626
+ }
627
+ if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
628
+ expect(route.request().headers()['x-openxiangda-csrf-token']).toBe(
629
+ 'workflow-csrf-1'
630
+ );
631
+ expect(route.request().postDataJSON()).toMatchObject({
632
+ commandToken: 'A'.repeat(43),
633
+ input: { comment: '同意采购' },
634
+ });
635
+ expect(route.request().postDataJSON()).not.toHaveProperty(
636
+ 'expectedTaskVersion'
637
+ );
638
+ expect(route.request().postDataJSON()).not.toHaveProperty(
639
+ 'expectedInstanceVersion'
640
+ );
641
+ if (options.commandConflict) {
642
+ return route.fulfill({
643
+ status: 409,
644
+ contentType: 'application/json',
645
+ body: JSON.stringify(
646
+ envelope({ freshSurface: surface(false, options.customDetail) }, 409)
647
+ ),
648
+ });
649
+ }
650
+ completed = true;
651
+ return route.fulfill({
652
+ contentType: 'application/json',
653
+ body: JSON.stringify(envelope({ status: 'completed' })),
654
+ });
655
+ }
656
+ return route.fulfill({
657
+ status: 404,
658
+ contentType: 'application/json',
659
+ body: JSON.stringify(envelope(null, 404)),
660
+ });
661
+ });
662
+ }
663
+
664
+ test('renders a paged desktop work center from the current-user role union', async ({
665
+ page,
666
+ }) => {
667
+ await mockWorkflow(page);
668
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center');
669
+ await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
670
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
671
+ await expect(page.getByText('PO-2026-0825')).toBeVisible();
672
+ });
673
+
674
+ test('renders the generic desktop and mobile application todo center without a second router', async ({
675
+ page,
676
+ }) => {
677
+ await mockWorkflow(page);
678
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/todos');
679
+ await expect(page.getByRole('heading', { name: '待办中心' })).toBeVisible();
680
+ await expect(page.locator('.oxa-todo-table')).toBeVisible();
681
+ await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
682
+ await expect(page.getByText('设备采购申请待审批').first()).toBeVisible();
683
+ await expect(page.getByText('¥28,600').first()).toBeVisible();
684
+ await expect(page.locator('.ant-spin-spinning')).toHaveCount(0);
685
+ const unreadRequest = page.waitForRequest(request => {
686
+ const url = new URL(request.url());
687
+ return url.pathname.endsWith('/todos') && url.searchParams.get('unread') === 'true';
688
+ });
689
+ await page.getByRole('switch', { name: '仅看未读' }).click();
690
+ await unreadRequest;
691
+ await page.getByRole('button', { name: /查看详情/ }).first().click();
692
+ await expect(page).toHaveURL(
693
+ new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}`)
694
+ );
695
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
696
+ `${instanceId} / ${taskId}`
697
+ );
698
+
699
+ await page.setViewportSize({ width: 390, height: 844 });
700
+ await page.goto('/workflow-experience.e2e.html?initial=/m/todos');
701
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
702
+ await expect(page.locator('.oxa-todo-preview-card')).toHaveCount(0);
703
+ await page.getByRole('button', { name: /查看详情/ }).first().click();
704
+ await expect(page).toHaveURL(
705
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}`)
706
+ );
707
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
708
+ `${instanceId} / ${taskId}`
709
+ );
710
+ });
711
+
712
+ test('launches the same workflow from the canonical mobile Surface and follows custom detail routing', async ({
713
+ page,
714
+ }) => {
715
+ await mockWorkflow(page, { customDetail: true });
716
+ await page.setViewportSize({ width: 390, height: 844 });
717
+ await page.goto(
718
+ '/workflow-experience.e2e.html?initial=/m/workflows/purchase-approval/start'
719
+ );
720
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
721
+ await expect(page.getByRole('heading', { name: '采购审批' })).toBeVisible();
722
+ await page.getByRole('spinbutton', { name: '请输入申请金额' }).fill('28600');
723
+ await page.getByRole('button', { name: '提交审批' }).click();
724
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
725
+ `${instanceId} / ${taskId}`
726
+ );
727
+ await expect(page).toHaveURL(
728
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
729
+ );
730
+ });
731
+
732
+ test('routes desktop and mobile work-center entries to the declared custom detail pages', async ({
733
+ page,
734
+ }) => {
735
+ await mockWorkflow(page, { customDetail: true });
736
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center');
737
+ await page.getByRole('link', { name: '采购申请审批' }).click();
738
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
739
+ `${instanceId} / ${taskId}`
740
+ );
741
+ await expect(page).toHaveURL(
742
+ new RegExp(`/admin/operations/purchases/${instanceId}\\?taskId=${taskId}$`)
743
+ );
744
+
745
+ await page.setViewportSize({ width: 390, height: 844 });
746
+ await page.goto('/workflow-experience.e2e.html?initial=/m/work-center');
747
+ await page.getByText('采购申请审批').click();
748
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
749
+ `${instanceId} / ${taskId}`
750
+ );
751
+ await expect(page).toHaveURL(
752
+ new RegExp(`/m/purchases/${instanceId}\\?taskId=${taskId}$`)
753
+ );
754
+ });
755
+
756
+ test('redirects a direct standard task URL to the declared custom detail page', async ({
757
+ page,
758
+ }) => {
759
+ await mockWorkflow(page, { customDetail: true });
760
+ await page.goto(
761
+ `/workflow-experience.e2e.html?initial=/tasks/${taskId}`
762
+ );
763
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
764
+ `${instanceId} / ${taskId}`
765
+ );
766
+ });
767
+
768
+ test('renders a standalone desktop detail and redirects the legacy admin URL', async ({
769
+ page,
770
+ }) => {
771
+ await mockWorkflow(page);
772
+ await page.goto(
773
+ workflowFixtureUrl(`/admin/tasks/${taskId}?from=legacy#approval`),
774
+ );
775
+ await expect(page.locator('.oxa-workflow-desktop-standalone-page')).toBeVisible();
776
+ await expect(page).toHaveURL(
777
+ new RegExp(`/tasks/${taskId}\\?from=legacy#approval$`),
778
+ );
779
+ await expect(page.locator('.oxa-app-layout')).toHaveCount(0);
780
+ await expect(page.locator('.oxa-topbar')).toHaveCount(0);
781
+ await expect(page.getByText('¥30,000')).toBeVisible();
782
+ await expect(page.getByText('技术场地引用')).toHaveCount(0);
783
+ await expect(page.getByText('业务数据已变化')).toHaveCount(0);
784
+ await expect(page.getByText('流程信息')).toHaveCount(0);
785
+ await expect(page.getByText('操作记录')).toHaveCount(0);
786
+ await expect(page.getByText('由值班负责人继续处理')).toHaveCount(1);
787
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
788
+ await expect(page.getByRole('button', { name: /拒\s*绝/ })).toBeVisible();
789
+ });
790
+
791
+ test('shows a friendly refresh prompt only after a real command conflict', async ({
792
+ page,
793
+ }) => {
794
+ await mockWorkflow(page, { commandConflict: true });
795
+ await page.goto(workflowFixtureUrl(`/tasks/${taskId}`));
796
+ await expect(page.getByText('业务数据已变化')).toHaveCount(0);
797
+ await page.getByRole('button', { name: /同\s*意/ }).click();
798
+ await page.getByLabel('审批意见').fill('同意采购');
799
+ await page.getByRole('button', { name: '确认同意' }).click();
800
+ await expect(page.getByText('内容已更新,请刷新后重试')).toBeVisible();
801
+ });
802
+
803
+ test('renders the independent mobile task and executes only a Surface operation', async ({
804
+ page,
805
+ }) => {
806
+ await mockWorkflow(page);
807
+ await page.setViewportSize({ width: 390, height: 844 });
808
+ await page.goto(`/workflow-experience.e2e.html?initial=/m/tasks/${taskId}`);
809
+ await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
810
+ await expect(page.getByText('¥30,000')).toBeVisible();
811
+ await expect(page.getByText('财务负责人').first()).toBeVisible();
812
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
813
+ await page.getByRole('button', { name: /同\s*意/ }).click();
814
+ await page.getByLabel('审批意见').fill('同意采购');
815
+ await page.getByRole('button', { name: '确认同意' }).click();
816
+ await expect(page.getByText('同意已提交')).toBeVisible();
817
+ await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
818
+ });
819
+
820
+ test('shows an explicit authorization error instead of an empty work center', async ({
821
+ page,
822
+ }) => {
823
+ test.setTimeout(45_000);
824
+ await mockWorkflow(page, { forbidden: true });
825
+ await page.goto('/workflow-experience.e2e.html?initial=/admin/work-center', {
826
+ waitUntil: 'domcontentloaded',
827
+ });
828
+ await expect(page.getByRole('alert')).toContainText('403: forbidden', {
829
+ timeout: 30_000,
830
+ });
831
+ await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
832
+ });
833
+
834
+ test('negotiates direct standard routes after BrowserRouter listener setup', async ({
835
+ page,
836
+ }) => {
837
+ await mockWorkflow(page);
838
+ await page.setViewportSize({ width: 360, height: 844 });
839
+ await page.goto(
840
+ workflowFixtureUrl('/admin/todos?view=pending#unread', {
841
+ state: 'deep-link',
842
+ }),
843
+ );
844
+
845
+ // The initial desktop URL is replaced by the mobile pair and the rendered
846
+ // surface follows the Router update. The fixture is StrictMode-mounted.
847
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
848
+ await expect(page.locator('.oxa-todo-center-desktop')).toHaveCount(0);
849
+ await expect(page).toHaveURL(/\/m\/todos\?view=pending#unread$/);
850
+ const historyLength = await page.evaluate(() => window.history.length);
851
+ const negotiatedUrl = page.url();
852
+ await expect
853
+ .poll(() => page.url(), { timeout: 1_000 })
854
+ .toBe(negotiatedUrl);
855
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
856
+ routeNegotiationState: 'deep-link',
857
+ });
858
+
859
+ // 900px remains mobile; crossing to 901px switches the same manifest pair
860
+ // back to desktop without adding a browser-history entry.
861
+ await page.setViewportSize({ width: 900, height: 844 });
862
+ await expect(page.locator('.oxa-todo-center-mobile')).toBeVisible();
863
+ await page.setViewportSize({ width: 901, height: 844 });
864
+ await expect(page.locator('.oxa-todo-center-desktop')).toBeVisible();
865
+ await expect(page).toHaveURL(/\/admin\/todos\?view=pending#unread$/);
866
+ expect(await page.evaluate(() => window.history.length)).toBe(historyLength);
867
+ expect(await page.evaluate(() => window.history.state?.usr)).toEqual({
868
+ routeNegotiationState: 'deep-link',
869
+ });
870
+ await page.goBack();
871
+ await expect(page).not.toHaveURL(/\/admin\/todos\?view=pending#unread$/);
872
+
873
+ // Direct task and instance URLs carry their dynamic parameter and deep-link
874
+ // context to the independent mobile renderer at the narrow width.
875
+ await page.setViewportSize({ width: 360, height: 844 });
876
+ await page.goto(
877
+ workflowFixtureUrl(`/tasks/${taskId}?from=todo#approval`),
878
+ );
879
+ await expect(page.locator('.oxa-workflow-detail-mobile-native')).toBeVisible();
880
+ await expect(page).toHaveURL(
881
+ new RegExp(`/m/tasks/${taskId}\\?from=todo#approval$`),
882
+ );
883
+
884
+ await page.goto(
885
+ workflowFixtureUrl(`/workflows/${instanceId}?from=todo#timeline`),
886
+ );
887
+ await expect(page.locator('.oxa-workflow-detail-mobile-native')).toBeVisible();
888
+ await expect(page).toHaveURL(
889
+ new RegExp(`/m/workflows/${instanceId}\\?from=todo#timeline$`),
890
+ );
891
+ });
892
+
893
+ test('negotiates launch pairs in both directions and preserves the mount basename', async ({
894
+ page,
895
+ }) => {
896
+ await mockWorkflow(page);
897
+ const base = '/runtime/route-negotiation';
898
+ await page.setViewportSize({ width: 360, height: 844 });
899
+ await page.goto(
900
+ workflowFixtureUrl(
901
+ `${base}/admin/workflows/purchase-approval/new?source=todo#launch`,
902
+ { base },
903
+ ),
904
+ );
905
+ await expect(page.locator('.oxa-workflow-submission-mobile')).toBeVisible();
906
+ await expect(page).toHaveURL(
907
+ new RegExp(
908
+ `${base}/m/workflows/purchase-approval/start\\?source=todo#launch$`,
909
+ ),
910
+ );
911
+
912
+ await page.setViewportSize({ width: 1_440, height: 900 });
913
+ await expect(page.locator('.oxa-workflow-submission-desktop')).toBeVisible();
914
+ await expect(page).toHaveURL(
915
+ new RegExp(
916
+ `${base}/admin/workflows/purchase-approval/new\\?source=todo#launch$`,
917
+ ),
918
+ );
919
+ });
920
+
921
+ test('does not negotiate non-standard application contribution routes', async ({
922
+ page,
923
+ }) => {
924
+ await mockWorkflow(page);
925
+ const base = '/runtime/route-negotiation';
926
+ await page.setViewportSize({ width: 360, height: 844 });
927
+ const customPath = `${base}/admin/operations/purchases/${instanceId}?taskId=${taskId}`;
928
+ await page.goto(workflowFixtureUrl(customPath, { base }));
929
+ await expect(page.getByTestId('custom-workflow-detail')).toContainText(
930
+ `${instanceId} / ${taskId}`,
931
+ );
932
+ const customUrl = page.url();
933
+ await expect(page).toHaveURL(new RegExp(`${base}/admin/operations/`));
934
+ await expect
935
+ .poll(() => page.url(), { timeout: 1_000 })
936
+ .toBe(customUrl);
937
+ });