openxiangda-cli 2.0.0-alpha.183 → 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.
Files changed (39) hide show
  1. package/dist/commands/admin.d.ts.map +1 -1
  2. package/dist/commands/admin.js +5 -3
  3. package/dist/commands/admin.js.map +1 -1
  4. package/dist/commands/spec.js +1 -1
  5. package/dist/commands/spec.js.map +1 -1
  6. package/package.json +5 -5
  7. package/template/AGENTS.md +2 -2
  8. package/template/apps/web/e2e/README.md +7 -0
  9. package/template/apps/web/package.json +2 -2
  10. package/template/apps/web/playwright.config.ts +3 -5
  11. package/template/apps/web/vite.config.ts +1 -9
  12. package/template/appspec/app.md +5 -1
  13. package/template/package.json +1 -1
  14. package/template/apps/web/data-api-live.e2e.html +0 -15
  15. package/template/apps/web/e2e/client-fixture.ts +0 -5
  16. package/template/apps/web/e2e/component-defaults-fixture.css +0 -15
  17. package/template/apps/web/e2e/data-api-live-fixture.tsx +0 -162
  18. package/template/apps/web/e2e/data-api-live.spec.ts +0 -144
  19. package/template/apps/web/e2e/field-protocol-fixture.tsx +0 -387
  20. package/template/apps/web/e2e/field-protocol.spec.ts +0 -285
  21. package/template/apps/web/e2e/mobile-files.spec.ts +0 -96
  22. package/template/apps/web/e2e/mobile-reference-fixture.tsx +0 -314
  23. package/template/apps/web/e2e/mobile-reference.spec.ts +0 -360
  24. package/template/apps/web/e2e/mobile-selection.spec.ts +0 -199
  25. package/template/apps/web/e2e/resource-experience-fixture.tsx +0 -491
  26. package/template/apps/web/e2e/resource-platform-mock.ts +0 -202
  27. package/template/apps/web/e2e/resources.spec.ts +0 -488
  28. package/template/apps/web/e2e/standard-admin.spec.ts +0 -511
  29. package/template/apps/web/e2e/workflow-entry-fixture.tsx +0 -107
  30. package/template/apps/web/e2e/workflow-entry.spec.ts +0 -556
  31. package/template/apps/web/e2e/workflow-experience-fixture.tsx +0 -338
  32. package/template/apps/web/e2e/workflow.spec.ts +0 -1265
  33. package/template/apps/web/field-protocol.e2e.html +0 -22
  34. package/template/apps/web/mobile-reference.e2e.html +0 -27
  35. package/template/apps/web/resource-experience.e2e.html +0 -16
  36. package/template/apps/web/scripts/check.mjs +0 -43
  37. package/template/apps/web/test/contracts.test.ts +0 -163
  38. package/template/apps/web/workflow-entry.e2e.html +0 -15
  39. package/template/apps/web/workflow-experience.e2e.html +0 -15
@@ -1,491 +0,0 @@
1
- import React, { useState } from 'react';
2
- import ReactDOM from 'react-dom/client';
3
- import { Button, Input, Modal, Select, Typography } from 'antd';
4
- import { useNavigate } from 'react-router-dom';
5
- import {
6
- defineApplicationContributions,
7
- OpenXiangdaAdminPage,
8
- OpenXiangdaApplication,
9
- } from 'openxiangda/react';
10
- import type { DataFieldSurface, DataResourceSurface } from 'openxiangda/core';
11
- import 'openxiangda/react/styles.css';
12
- import './component-defaults-fixture.css';
13
-
14
- const field = (
15
- label: string,
16
- type: DataFieldSurface['type'] = 'text.short',
17
- widget: DataFieldSurface['widget'] = 'text',
18
- extra: Partial<DataFieldSurface> = {}
19
- ): DataFieldSurface => ({
20
- label,
21
- type,
22
- widget,
23
- readCapabilities: [],
24
- createCapabilities: [],
25
- updateCapabilities: [],
26
- ...extra,
27
- });
28
-
29
- const fields: DataResourceSurface['fields'] = {
30
- departmentMultiple: field('构成部门', 'department.multiple', 'directory-department', {
31
- list: true,
32
- section: '组织信息',
33
- }),
34
- departmentSingle: field('牵头部门', 'department.single', 'directory-department', {
35
- list: true,
36
- section: '组织信息',
37
- }),
38
- userMultiple: field('小组成员', 'user.multiple', 'directory-user', {
39
- list: true,
40
- section: '组织信息',
41
- }),
42
- resourceMultiple: field('关联资源', 'resource-ref.multiple', 'resource', {
43
- list: true,
44
- section: '组织信息',
45
- source: {
46
- kind: 'resource',
47
- resourceCode: 'resource-01',
48
- labelField: 'name',
49
- searchFields: ['name'],
50
- },
51
- }),
52
- enabled: field('启用状态', 'boolean', 'switch', {
53
- list: true,
54
- section: '组织信息',
55
- }),
56
- displayOrder: field('显示顺序', 'number.integer', 'number', {
57
- list: true,
58
- section: '组织信息',
59
- }),
60
- description: field('栏目说明', 'text.long', 'textarea', {
61
- section: '栏目设置',
62
- }),
63
- status: field('状态', 'option.single', 'select', {
64
- options: [
65
- { label: '启用', value: 'enabled' },
66
- { label: '停用', value: 'disabled' },
67
- ],
68
- section: '栏目设置',
69
- }),
70
- name: field('栏目名称', 'text.short', 'text', { section: '栏目设置' }),
71
- code: field('栏目编码', 'text.short', 'text', { section: '栏目设置' }),
72
- contentType: field('内容类型', 'option.single', 'select', {
73
- options: [
74
- { label: '文章', value: 'article' },
75
- { label: '图集', value: 'gallery' },
76
- ],
77
- section: '栏目设置',
78
- }),
79
- manager: field('栏目管理员', 'user.single', 'directory-user', {
80
- section: '管理信息',
81
- }),
82
- facility: field('配套设施', 'option.multiple', 'multi-select', {
83
- options: [
84
- { label: '投影仪', value: 'projector' },
85
- { label: '白板', value: 'whiteboard' },
86
- ],
87
- section: '管理信息',
88
- }),
89
- openingDay: field('开放星期', 'option.single', 'select', {
90
- options: [
91
- { label: '工作日', value: 'weekday' },
92
- { label: '周末', value: 'weekend' },
93
- ],
94
- section: '管理信息',
95
- }),
96
- };
97
-
98
- const routeManifest = {
99
- schemaVersion: 'openxiangda.application-route-manifest/v3',
100
- appCode: 'openxiangda-application',
101
- devicePolicy: {
102
- kind: 'viewport-family',
103
- mobileMaxWidthPx: 900,
104
- desktopMinWidthPx: 901,
105
- },
106
- rootEntry: { code: 'application-root', desktop: '/', mobile: '/m/' },
107
- authentication: {
108
- desktop: { routeCode: 'application-login', path: '/login' },
109
- mobile: { routeCode: 'application-login-mobile', path: '/m/login' },
110
- },
111
- routes: [],
112
- digest:
113
- '88831760f6e672a77babbf039b35a6febca7852092f1579c2721c99596a265c4',
114
- } as const;
115
-
116
- const surface: DataResourceSurface = {
117
- mutationOwner: 'native',
118
- generated: {
119
- list: true,
120
- detail: true,
121
- create: true,
122
- update: true,
123
- delete: true,
124
- },
125
- fields,
126
- list: {
127
- defaultPageSize: 20,
128
- searchableFields: ['name', 'code'],
129
- filterFields: ['contentType', 'status', 'manager', 'facility', 'openingDay'],
130
- defaultSort: { field: 'code', order: 'asc' },
131
- },
132
- form: {
133
- layout: 'sections',
134
- fieldOrder: [
135
- 'code',
136
- 'name',
137
- 'contentType',
138
- 'description',
139
- 'status',
140
- 'manager',
141
- 'facility',
142
- 'openingDay',
143
- ],
144
- },
145
- detail: {
146
- layout: 'sections',
147
- fieldOrder: [
148
- 'code',
149
- 'name',
150
- 'contentType',
151
- 'description',
152
- 'status',
153
- 'manager',
154
- 'facility',
155
- 'openingDay',
156
- ],
157
- },
158
- mobile: { enabled: true },
159
- };
160
-
161
- const { enabled, displayOrder, ...remainingFields } = fields;
162
- const fixtureMode = new URLSearchParams(location.search).get('primary') || (location.pathname.includes('/views/') ? 'named' : location.pathname.endsWith('.html') ? null : 'business');
163
- const acceptanceSurface: DataResourceSurface = fixtureMode === 'falsy'
164
- ? { ...surface, fields: { enabled, displayOrder, ...remainingFields } }
165
- : fixtureMode === 'business' || fixtureMode === 'named'
166
- ? {
167
- ...surface,
168
- fields: { ...fields, name: { ...fields.name, list: true, requiredHint: true, sortable: true }, displayOrder: { ...fields.displayOrder, sortable: true } },
169
- list: { ...surface.list, fieldOrder: ['name', 'enabled', 'displayOrder'], filterFields: ['name', 'enabled', 'displayOrder', ...surface.list!.filterFields!], defaultPageSize: 10 },
170
- }
171
- : surface;
172
-
173
- if (fixtureMode === 'named') acceptanceSurface.views = [
174
- { code: 'quick', name: '简要登记', generated: { list: true, detail: true, create: true, update: true, delete: false },
175
- list: { fieldOrder: ['name', 'code'], defaultPageSize: 10, searchableFields: ['name'] },
176
- form: { layout: 'sections', fieldOrder: ['name', 'code'] }, detail: { layout: 'sections', fieldOrder: ['name', 'code'] },
177
- sections: [{ title: '登记信息', fields: ['name', 'code'] }], mobile: { enabled: true } },
178
- { code: 'complete', name: '完整管理', generated: { list: true, detail: true, create: true, update: true, delete: true },
179
- list: { fieldOrder: ['name', 'enabled', 'displayOrder'], defaultPageSize: 10, searchableFields: ['name'] },
180
- form: { layout: 'sections', fieldOrder: ['name', 'code', 'description', 'enabled', 'displayOrder'] },
181
- detail: { layout: 'sections', fieldOrder: ['name', 'code', 'description', 'enabled', 'displayOrder'] },
182
- sections: [{ title: '栏目信息', fields: ['name', 'code', 'description'] }, { title: '管理设置', fields: ['enabled', 'displayOrder'] }],
183
- mobile: { enabled: true } },
184
- ];
185
-
186
- const names = [
187
- '内容栏目',
188
- '内容文章',
189
- '内容专题',
190
- '会员档案',
191
- '门户轮播',
192
- '工会小组',
193
- '场地预约',
194
- '场地不可用时段',
195
- '场地档案',
196
- '通知公告',
197
- '活动报名',
198
- '资料下载',
199
- ];
200
- const resourceDefinitions = Object.fromEntries(
201
- names.map((name, index) => {
202
- const code = `resource-${String(index + 1).padStart(2, '0')}`;
203
- return [
204
- code,
205
- {
206
- code,
207
- name,
208
- capabilities: {
209
- read: `app:openxiangda-application:data:${code}:read`,
210
- create: `app:openxiangda-application:data:${code}:create`,
211
- update: `app:openxiangda-application:data:${code}:update`,
212
- delete: `app:openxiangda-application:data:${code}:delete`,
213
- },
214
- surface: acceptanceSurface,
215
- },
216
- ];
217
- })
218
- );
219
-
220
- function ContentOperationsPage() {
221
- const navigate = useNavigate();
222
- return (
223
- <OpenXiangdaAdminPage
224
- className="component-defaults-page"
225
- data-testid="custom-admin-page"
226
- >
227
- <Typography.Title level={2}>内容业务操作</Typography.Title>
228
- <div className="component-defaults-panel">
229
- 自定义后台页默认组件面板
230
- <Button onClick={() => navigate('/resource-01')}>打开用户端</Button>
231
- </div>
232
- </OpenXiangdaAdminPage>
233
- );
234
- }
235
-
236
- function ResourcePortalPage() {
237
- const [open, setOpen] = useState(false);
238
- return <main>
239
- <Typography.Title level={2}>用户端内容门户</Typography.Title>
240
- <Input aria-label="用户端输入" />
241
- <Select options={[{ label: '可用选项', value: 'available' }]} style={{ width: 200 }} />
242
- <Button onClick={() => setOpen(true)}>打开用户弹窗</Button>
243
- <Modal open={open} title="用户端确认" onCancel={() => setOpen(false)} onOk={() => setOpen(false)}>
244
- 默认组件保持可用
245
- </Modal>
246
- </main>;
247
- }
248
-
249
- const applicationContributions = defineApplicationContributions(
250
- {
251
- contentOperations: {
252
- code: 'content-operations',
253
- path: '/admin/operations/content',
254
- label: '内容业务操作',
255
- surface: 'admin',
256
- capability: 'app:openxiangda-application:operations:read',
257
- tabPersistence: 'session',
258
- keepAlive: 'none',
259
- },
260
- resourcePortal: {
261
- code: 'resource-portal',
262
- path: '/resource-01',
263
- label: '用户端内容门户',
264
- surface: 'user',
265
- tabPersistence: 'session',
266
- keepAlive: 'none',
267
- },
268
- } as const,
269
- {
270
- pages: {
271
- contentOperations: ContentOperationsPage,
272
- resourcePortal: ResourcePortalPage,
273
- },
274
- resources: {
275
- 'resource-01': {
276
- toolbar: [
277
- {
278
- code: 'publish-selected',
279
- requiresSelection: true,
280
- label: '发布选中栏目',
281
- capability: 'app:openxiangda-application:operations:read',
282
- render: ({ selectedRecords }) => (
283
- <Button>发布选中栏目({selectedRecords.length})</Button>
284
- ),
285
- },
286
- ],
287
- },
288
- },
289
- },
290
- );
291
-
292
- if (location.pathname.endsWith('/resource-experience.e2e.html')) {
293
- history.replaceState({}, '', '/admin/resources/resource-01');
294
- }
295
-
296
- ReactDOM.createRoot(document.getElementById('root')!).render(
297
- <React.StrictMode>
298
- <OpenXiangdaApplication
299
- appCode="openxiangda-application"
300
- appName="资源体验验收"
301
- adminNavigation={[
302
- {
303
- code: 'resources',
304
- label: '数据管理',
305
- icon: 'database',
306
- order: 0,
307
- items: [
308
- ...(acceptanceSurface.views || []).map((view, index) => ({
309
- pageCode: `resource:resource-01:view:${view.code}:list`, label: view.name, order: index - 2,
310
- })),
311
- { pageCode: 'resource:resource-01:list', label: '内容栏目', order: 0 },
312
- { pageCode: 'resource:resource-02:list', label: '内容文章', order: 1 },
313
- { pageCode: 'resource:resource-03:list', label: '内容专题', order: 2 },
314
- { pageCode: 'resource:resource-04:list', label: '会员档案', order: 3 },
315
- { pageCode: 'resource:resource-05:list', label: '门户轮播', order: 4 },
316
- { pageCode: 'resource:resource-06:list', label: '工会小组', order: 5 },
317
- { pageCode: 'resource:resource-07:list', label: '场地预约', order: 6 },
318
- {
319
- pageCode: 'resource:resource-08:list',
320
- label: '场地不可用时段',
321
- order: 7,
322
- },
323
- { pageCode: 'resource:resource-09:list', label: '场地档案', order: 8 },
324
- { pageCode: 'resource:resource-10:list', label: '通知公告', order: 9 },
325
- { pageCode: 'resource:resource-11:list', label: '活动报名', order: 10 },
326
- { pageCode: 'resource:resource-12:list', label: '资料下载', order: 11 },
327
- { pageCode: 'operation:content-operations', order: 12 },
328
- ],
329
- },
330
- ]}
331
- adminPages={[
332
- ...(acceptanceSurface.views || []).flatMap(view => (['list', 'detail', 'create', 'update'] as const).map(operation => ({
333
- code: `resource:resource-01:view:${view.code}:${operation}`,
334
- kind: `resource-${operation}` as const,
335
- path: `/admin/resources/resource-01/views/${view.code}${({ list: '', detail: '/:id', create: '/new', update: '/:id/edit' })[operation]}`,
336
- label: `${({ list: '', detail: '', create: '新增', update: '编辑' })[operation]}${view.name}`,
337
- navigationEligible: operation === 'list',
338
- capability: resourceDefinitions['resource-01'].capabilities[operation === 'list' || operation === 'detail' ? 'read' : operation],
339
- resourceCode: 'resource-01', viewCode: view.code,
340
- }))),
341
- {
342
- code: 'resource:resource-01:list',
343
- kind: 'resource-list',
344
- path: '/admin/resources/resource-01',
345
- label: '资源 01',
346
- navigationEligible: true,
347
- capability: resourceDefinitions['resource-01'].capabilities.read,
348
- resourceCode: 'resource-01',
349
- },
350
- {
351
- code: 'resource:resource-01:detail',
352
- kind: 'resource-detail',
353
- path: '/admin/resources/resource-01/:id',
354
- label: '资源 01 详情',
355
- navigationEligible: false,
356
- capability: resourceDefinitions['resource-01'].capabilities.read,
357
- resourceCode: 'resource-01',
358
- },
359
- {
360
- code: 'resource:resource-01:create',
361
- kind: 'resource-create',
362
- path: '/admin/resources/resource-01/new',
363
- label: '新增资源 01',
364
- navigationEligible: false,
365
- capability: resourceDefinitions['resource-01'].capabilities.create,
366
- resourceCode: 'resource-01',
367
- },
368
- {
369
- code: 'resource:resource-01:update',
370
- kind: 'resource-update',
371
- path: '/admin/resources/resource-01/:id/edit',
372
- label: '编辑资源 01',
373
- navigationEligible: false,
374
- capability: resourceDefinitions['resource-01'].capabilities.update,
375
- resourceCode: 'resource-01',
376
- },
377
- {
378
- code: 'resource:resource-02:list',
379
- kind: 'resource-list',
380
- path: '/admin/resources/resource-02',
381
- label: '内容文章',
382
- navigationEligible: true,
383
- capability: resourceDefinitions['resource-02'].capabilities.read,
384
- resourceCode: 'resource-02',
385
- },
386
- {
387
- code: 'resource:resource-03:list',
388
- kind: 'resource-list',
389
- path: '/admin/resources/resource-03',
390
- label: '内容专题',
391
- navigationEligible: true,
392
- capability: resourceDefinitions['resource-03'].capabilities.read,
393
- resourceCode: 'resource-03',
394
- },
395
- {
396
- code: 'resource:resource-04:list',
397
- kind: 'resource-list',
398
- path: '/admin/resources/resource-04',
399
- label: '会员档案',
400
- navigationEligible: true,
401
- capability: resourceDefinitions['resource-04'].capabilities.read,
402
- resourceCode: 'resource-04',
403
- },
404
- {
405
- code: 'resource:resource-05:list',
406
- kind: 'resource-list',
407
- path: '/admin/resources/resource-05',
408
- label: '门户轮播',
409
- navigationEligible: true,
410
- capability: resourceDefinitions['resource-05'].capabilities.read,
411
- resourceCode: 'resource-05',
412
- },
413
- {
414
- code: 'resource:resource-06:list',
415
- kind: 'resource-list',
416
- path: '/admin/resources/resource-06',
417
- label: '工会小组',
418
- navigationEligible: true,
419
- capability: resourceDefinitions['resource-06'].capabilities.read,
420
- resourceCode: 'resource-06',
421
- },
422
- {
423
- code: 'resource:resource-07:list',
424
- kind: 'resource-list',
425
- path: '/admin/resources/resource-07',
426
- label: '场地预约',
427
- navigationEligible: true,
428
- capability: resourceDefinitions['resource-07'].capabilities.read,
429
- resourceCode: 'resource-07',
430
- },
431
- {
432
- code: 'resource:resource-08:list',
433
- kind: 'resource-list',
434
- path: '/admin/resources/resource-08',
435
- label: '场地不可用时段',
436
- navigationEligible: true,
437
- capability: resourceDefinitions['resource-08'].capabilities.read,
438
- resourceCode: 'resource-08',
439
- },
440
- {
441
- code: 'resource:resource-09:list',
442
- kind: 'resource-list',
443
- path: '/admin/resources/resource-09',
444
- label: '场地档案',
445
- navigationEligible: true,
446
- capability: resourceDefinitions['resource-09'].capabilities.read,
447
- resourceCode: 'resource-09',
448
- },
449
- {
450
- code: 'resource:resource-10:list',
451
- kind: 'resource-list',
452
- path: '/admin/resources/resource-10',
453
- label: '通知公告',
454
- navigationEligible: true,
455
- capability: resourceDefinitions['resource-10'].capabilities.read,
456
- resourceCode: 'resource-10',
457
- },
458
- {
459
- code: 'resource:resource-11:list',
460
- kind: 'resource-list',
461
- path: '/admin/resources/resource-11',
462
- label: '活动报名',
463
- navigationEligible: true,
464
- capability: resourceDefinitions['resource-11'].capabilities.read,
465
- resourceCode: 'resource-11',
466
- },
467
- {
468
- code: 'resource:resource-12:list',
469
- kind: 'resource-list',
470
- path: '/admin/resources/resource-12',
471
- label: '资料下载',
472
- navigationEligible: true,
473
- capability: resourceDefinitions['resource-12'].capabilities.read,
474
- resourceCode: 'resource-12',
475
- },
476
- {
477
- code: 'operation:content-operations',
478
- kind: 'operation',
479
- path: '/admin/operations/content',
480
- label: '内容业务操作',
481
- navigationEligible: true,
482
- capability: 'app:openxiangda-application:operations:read',
483
- routeCode: 'content-operations',
484
- },
485
- ]}
486
- contributions={applicationContributions}
487
- resourceDefinitions={resourceDefinitions}
488
- routeManifest={routeManifest}
489
- />
490
- </React.StrictMode>
491
- );
@@ -1,202 +0,0 @@
1
- import type { Page, BrowserContext } from '@playwright/test';
2
- import { appCode, capabilities } from '../../../packages/contracts/src/generated.js';
3
- const runtimeBase = `/view/${appCode}`;
4
- const allCapabilities = [...capabilities] as string[];
5
-
6
- export function runtimeAuthorization(authorized: boolean, superAdmin = false) {
7
- return {
8
- schemaVersion: 'openxiangda.runtime-authorization/v2',
9
- state: 'active',
10
- environment: {
11
- id: 'preproduction-id',
12
- key: 'preproduction',
13
- activeAppVersionId: 'app-version-1',
14
- headRevision: 1,
15
- authzRevisionId: 'authz-1',
16
- authzVersion: 1,
17
- scopeDataVersion: 'scope-1',
18
- },
19
- subjectProfile: {
20
- schemaVersion: 'openxiangda.subject-profile/v2',
21
- userId: 'acceptance-user',
22
- displayName: '王老师',
23
- avatarUrl: null,
24
- jobNumber: 'T001',
25
- affiliatedDepartment: { id: 'department-1', name: '理学院' },
26
- },
27
- roles: [
28
- { code: 'college_admin', name: '学院管理员', source: 'package' },
29
- { code: 'instrument_admin', name: '仪器管理员', source: 'package' },
30
- ],
31
- principal: {
32
- type: 'user_union',
33
- userId: 'acceptance-user',
34
- roleCodes: ['college_admin', 'instrument_admin'],
35
- capabilityCodes: authorized
36
- ? [
37
- ...allCapabilities,
38
- 'app:openxiangda-application:operations:read',
39
- ]
40
- : [],
41
- isAppSuperAdmin: superAdmin,
42
- identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
43
- },
44
- };
45
- }
46
-
47
- export async function mockPlatform(
48
- page: Page | BrowserContext,
49
- authorized = true,
50
- targetRuntimeBase = runtimeBase,
51
- superAdmin = false,
52
- queryItemsByResource: Record<string, Array<Record<string, unknown>>> = {},
53
- ) {
54
- let contextReads = 0;
55
- let avatarCompletes = 0;
56
- const observed: Array<{ path: string; perspectiveCode: string }> = [];
57
- await page.route('https://upload.example.test/avatar-upload', route =>
58
- route.fulfill({ status: 200, body: '' })
59
- );
60
- await page.route(`**${targetRuntimeBase}/**`, async route => {
61
- if (route.request().resourceType() !== 'document') return route.continue();
62
- const response = await route.fetch();
63
- const body = (await response.text()).replace(
64
- '<head>',
65
- `<head><meta name="openxiangda-runtime-base" content="${targetRuntimeBase}/" />` +
66
- `<meta name="openxiangda-app-code" content="${appCode}" />` +
67
- '<meta name="openxiangda-environment" content="preproduction" />'
68
- );
69
- await route.fulfill({ response, body });
70
- });
71
- await page.route('**/service/**', async route => {
72
- const url = new URL(route.request().url());
73
- const request = route.request();
74
- const perspectiveCode = request.headers()['x-openxiangda-perspective'] || '';
75
- if (url.pathname.endsWith('/native/authz/current')) {
76
- contextReads += 1;
77
- return route.fulfill({
78
- contentType: 'application/json',
79
- body: JSON.stringify({
80
- code: 200,
81
- data: runtimeAuthorization(authorized, superAdmin),
82
- }),
83
- });
84
- }
85
- if (url.pathname.endsWith('/native/authz/profile/avatar/initiate')) {
86
- return route.fulfill({
87
- contentType: 'application/json',
88
- body: JSON.stringify({
89
- code: 200,
90
- data: {
91
- uploadUrl: 'https://upload.example.test/avatar-upload',
92
- uploadMethod: 'put',
93
- headers: { 'content-type': 'image/png' },
94
- objectName: 'images/tenant/app/user/avatar.png',
95
- },
96
- }),
97
- });
98
- }
99
- if (url.pathname.endsWith('/native/authz/profile/avatar/complete')) {
100
- avatarCompletes += 1;
101
- return route.fulfill({
102
- contentType: 'application/json',
103
- body: JSON.stringify({
104
- code: 200,
105
- data: {
106
- schemaVersion: 'openxiangda.subject-profile/v2',
107
- userId: 'acceptance-user',
108
- displayName: '王老师',
109
- avatarUrl: 'https://cdn.example.test/avatar/new.png',
110
- jobNumber: 'T001',
111
- affiliatedDepartment: { id: 'department-1', name: '理学院' },
112
- },
113
- }),
114
- });
115
- }
116
- const sourceQuery = url.pathname.match(
117
- /\/native\/data-resources\/([^/]+)\/fields\/([^/]+)\/source\/query$/
118
- );
119
- if (sourceQuery) {
120
- observed.push({ path: url.pathname, perspectiveCode });
121
- return route.fulfill({
122
- contentType: 'application/json',
123
- body: JSON.stringify({
124
- code: 200,
125
- data: {
126
- schemaVersion: 'openxiangda.data-field-source-page/v2',
127
- resourceCode: decodeURIComponent(sourceQuery[1]),
128
- fieldCode: decodeURIComponent(sourceQuery[2]),
129
- items: [],
130
- nextCursor: null,
131
- },
132
- }),
133
- });
134
- }
135
- const exportRequest = url.pathname.match(
136
- /\/native\/data\/([^/]+)\/export$/
137
- );
138
- if (exportRequest) {
139
- observed.push({ path: url.pathname, perspectiveCode });
140
- return route.fulfill({
141
- status: 200,
142
- contentType: 'text/csv; charset=utf-8',
143
- headers: {
144
- 'content-disposition': `attachment; filename="${decodeURIComponent(
145
- exportRequest[1]
146
- )}.csv"`,
147
- },
148
- body: 'id,name\nacceptance-1,OpenXiangda\n',
149
- });
150
- }
151
- const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
152
- if (query) {
153
- const resourceCode = decodeURIComponent(query[1]);
154
- const items = queryItemsByResource[resourceCode] || [];
155
- observed.push({ path: url.pathname, perspectiveCode });
156
- return route.fulfill({
157
- contentType: 'application/json',
158
- body: JSON.stringify({
159
- code: 200,
160
- data: {
161
- schemaVersion: 'openxiangda.data-page/v2',
162
- resourceCode,
163
- appVersionId: 'app-version-1',
164
- environmentHeadRevision: 1,
165
- items,
166
- total: items.length,
167
- limit: 500,
168
- offset: 0,
169
- },
170
- }),
171
- });
172
- }
173
- if (url.pathname.includes('/directory/')) {
174
- observed.push({ path: url.pathname, perspectiveCode });
175
- return route.fulfill({
176
- contentType: 'application/json',
177
- body: JSON.stringify({ code: 200, data: { items: [], nextCursor: null } }),
178
- });
179
- }
180
- if (url.pathname.includes('/openxiangda-app-api/v2/')) {
181
- observed.push({ path: url.pathname, perspectiveCode });
182
- return route.fulfill({
183
- contentType: 'application/json',
184
- body: JSON.stringify({ code: 200, data: { status: 'ok' } }),
185
- });
186
- }
187
- return route.fulfill({
188
- status: 404,
189
- contentType: 'application/json',
190
- body: JSON.stringify({ code: 404, message: 'not mocked' }),
191
- });
192
- });
193
- return {
194
- observed,
195
- get contextReads() {
196
- return contextReads;
197
- },
198
- get avatarCompletes() {
199
- return avatarCompletes;
200
- },
201
- };
202
- }