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