openxiangda-cli 2.0.0-alpha.56 → 2.0.0-alpha.58
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/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +13 -5
- package/dist/commands/create.js.map +1 -1
- package/dist/create-workspace.d.ts +6 -1
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +12 -9
- package/dist/create-workspace.js.map +1 -1
- package/package.json +4 -4
- package/template/.dockerignore +11 -0
- package/template/README.md +3 -1
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/e2e/instruments.spec.ts +912 -9
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/scripts/check.mjs +2 -2
- package/template/apps/web/src/AuthoritativeSelector.tsx +371 -0
- package/template/apps/web/src/CollegePage.tsx +181 -0
- package/template/apps/web/src/InstrumentDetailPage.tsx +94 -20
- package/template/apps/web/src/InstrumentForm.tsx +450 -88
- package/template/apps/web/src/InstrumentFormPage.tsx +49 -9
- package/template/apps/web/src/InstrumentListPage.tsx +30 -35
- package/template/apps/web/src/Shell.tsx +229 -41
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +698 -0
- package/template/apps/web/src/data-provider.ts +1 -1
- package/template/apps/web/src/fields.ts +17 -6
- package/template/apps/web/src/instrument.ts +1 -1
- package/template/apps/web/src/main.tsx +16 -1
- package/template/apps/web/src/platform-client.ts +213 -2
- package/template/apps/web/src/runtime.tsx +4 -2
- package/template/apps/web/src/styles.css +869 -29
- package/template/apps/web/test/contracts.test.ts +369 -10
- package/template/openxiangda.config.ts +24 -5
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +22 -0
- package/template/platform/data/colleges.ts +21 -0
- package/template/platform/data/instruments.ts +1 -1
- package/template/scripts/verify-template-budget.mjs +1 -1
|
@@ -1,4 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
expect,
|
|
3
|
+
test,
|
|
4
|
+
type Page,
|
|
5
|
+
type Request,
|
|
6
|
+
type Route,
|
|
7
|
+
} from '@playwright/test';
|
|
8
|
+
import {
|
|
9
|
+
instrumentFields,
|
|
10
|
+
} from '../src/fields';
|
|
11
|
+
|
|
12
|
+
const declaredInstrumentFields = new Set(
|
|
13
|
+
instrumentFields.filter(field => !field.system).map(field => field.key)
|
|
14
|
+
);
|
|
15
|
+
const collegeId = '11111111-1111-4111-8111-111111111111';
|
|
16
|
+
const userId = '22222222-2222-4222-8222-222222222222';
|
|
17
|
+
const departmentId = '33333333-3333-4333-8333-333333333333';
|
|
18
|
+
|
|
19
|
+
function expectDeclaredInstrumentQuery(request: Request) {
|
|
20
|
+
const body = request.postDataJSON() as {
|
|
21
|
+
filters?: Array<{ field: string }>;
|
|
22
|
+
order?: Array<{ field: string }>;
|
|
23
|
+
};
|
|
24
|
+
const fields = [
|
|
25
|
+
...(body.filters || []).map(filter => filter.field),
|
|
26
|
+
...(body.order || []).map(order => order.field),
|
|
27
|
+
];
|
|
28
|
+
expect(fields.every(field => declaredInstrumentFields.has(field))).toBe(true);
|
|
29
|
+
expect(fields).not.toContain('updatedAt');
|
|
30
|
+
expect(fields).not.toContain('updated_at');
|
|
31
|
+
return body;
|
|
32
|
+
}
|
|
2
33
|
|
|
3
34
|
test('renders the production warning and never requests a role session', async ({
|
|
4
35
|
page,
|
|
@@ -44,7 +75,7 @@ test('renders the production warning and never requests a role session', async (
|
|
|
44
75
|
},
|
|
45
76
|
principal: {
|
|
46
77
|
type: 'user',
|
|
47
|
-
userId
|
|
78
|
+
userId,
|
|
48
79
|
roleCodes: ['college_admin', 'instrument_admin'],
|
|
49
80
|
capabilityCodes: [
|
|
50
81
|
'app:instrument-center:data:instruments:read',
|
|
@@ -59,9 +90,11 @@ test('renders the production warning and never requests a role session', async (
|
|
|
59
90
|
return;
|
|
60
91
|
}
|
|
61
92
|
if (url.pathname.endsWith('/native/data/instruments/query')) {
|
|
93
|
+
expectDeclaredInstrumentQuery(route.request());
|
|
62
94
|
expect(route.request().postDataJSON()).toMatchObject({
|
|
63
95
|
schemaVersion: 'openxiangda.data-query/v2',
|
|
64
96
|
environmentKey: 'production',
|
|
97
|
+
order: [{ field: 'instrumentCode', direction: 'asc' }],
|
|
65
98
|
});
|
|
66
99
|
await route.fulfill({
|
|
67
100
|
contentType: 'application/json',
|
|
@@ -83,13 +116,13 @@ test('renders the production warning and never requests a role session', async (
|
|
|
83
116
|
usageStatus: 'active',
|
|
84
117
|
instrumentStatus: 'normal',
|
|
85
118
|
openToOutside: true,
|
|
86
|
-
collegeId
|
|
87
|
-
manageDepartmentId:
|
|
88
|
-
useDepartmentId:
|
|
89
|
-
instrumentAdminIds: [
|
|
90
|
-
contactUserIds: [
|
|
119
|
+
collegeId,
|
|
120
|
+
manageDepartmentId: departmentId,
|
|
121
|
+
useDepartmentId: departmentId,
|
|
122
|
+
instrumentAdminIds: [userId],
|
|
123
|
+
contactUserIds: [userId],
|
|
91
124
|
contactPhone: '13800138000',
|
|
92
|
-
|
|
125
|
+
updated_at: '2026-08-21T00:00:00.000Z',
|
|
93
126
|
},
|
|
94
127
|
],
|
|
95
128
|
total: 1,
|
|
@@ -100,6 +133,7 @@ test('renders the production warning and never requests a role session', async (
|
|
|
100
133
|
});
|
|
101
134
|
return;
|
|
102
135
|
}
|
|
136
|
+
if (await fulfillSelector(route)) return;
|
|
103
137
|
await route.fulfill({
|
|
104
138
|
status: 404,
|
|
105
139
|
contentType: 'application/json',
|
|
@@ -110,7 +144,10 @@ test('renders the production warning and never requests a role session', async (
|
|
|
110
144
|
await expect(page.getByTestId('production-data-warning')).toBeVisible();
|
|
111
145
|
await expect(page.getByTestId('instrument-title')).toHaveText('仪器资源');
|
|
112
146
|
await expect(page.getByText('场发射扫描电子显微镜')).toBeVisible();
|
|
113
|
-
await expect(
|
|
147
|
+
await expect(
|
|
148
|
+
page.getByRole('complementary').getByText('仪器管理', { exact: true })
|
|
149
|
+
).toBeVisible();
|
|
150
|
+
await expect(page.getByText('王老师', { exact: true }).first()).toBeVisible();
|
|
114
151
|
expect(requests.filter(url => url.includes('role-session'))).toHaveLength(0);
|
|
115
152
|
expect(
|
|
116
153
|
requests.some(url =>
|
|
@@ -119,6 +156,548 @@ test('renders the production warning and never requests a role session', async (
|
|
|
119
156
|
).toBe(true);
|
|
120
157
|
});
|
|
121
158
|
|
|
159
|
+
test('continues create, list, detail, update, and delete with declared query fields', async ({
|
|
160
|
+
page,
|
|
161
|
+
}) => {
|
|
162
|
+
test.setTimeout(60_000);
|
|
163
|
+
let record: Record<string, unknown> | undefined;
|
|
164
|
+
const queryBodies: unknown[] = [];
|
|
165
|
+
const operations: string[] = [];
|
|
166
|
+
const protectedCapabilities = instrumentFields.flatMap(field => [
|
|
167
|
+
...(field.createCapability ? [field.createCapability] : []),
|
|
168
|
+
...(field.updateCapability ? [field.updateCapability] : []),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
await installProductionRuntimeMeta(page);
|
|
172
|
+
await page.route('**/mock-upload/instrument-file-1', route =>
|
|
173
|
+
route.fulfill({ status: 200, body: '' })
|
|
174
|
+
);
|
|
175
|
+
await page.route('**/service/**', async route => {
|
|
176
|
+
const request = route.request();
|
|
177
|
+
const url = new URL(request.url());
|
|
178
|
+
const path = url.pathname;
|
|
179
|
+
if (
|
|
180
|
+
path.endsWith('/native/current-user') &&
|
|
181
|
+
url.searchParams.get('environmentKey') === 'production'
|
|
182
|
+
) {
|
|
183
|
+
await route.fulfill({
|
|
184
|
+
contentType: 'application/json',
|
|
185
|
+
body: JSON.stringify({
|
|
186
|
+
code: 200,
|
|
187
|
+
data: {
|
|
188
|
+
schemaVersion: 'openxiangda.current-user/v2',
|
|
189
|
+
appCode: 'instrument-center',
|
|
190
|
+
environment: {
|
|
191
|
+
id: 'production-id',
|
|
192
|
+
key: 'production',
|
|
193
|
+
activeAppVersionId: 'app-version-1',
|
|
194
|
+
headRevision: 7,
|
|
195
|
+
authzRevisionId: 'authz-revision-1',
|
|
196
|
+
authzVersion: 3,
|
|
197
|
+
scopeDataVersion: 'scope-data-version-1',
|
|
198
|
+
},
|
|
199
|
+
principal: {
|
|
200
|
+
type: 'user',
|
|
201
|
+
userId,
|
|
202
|
+
roleCodes: ['school_admin'],
|
|
203
|
+
capabilityCodes: [
|
|
204
|
+
'app:instrument-center:data:instruments:read',
|
|
205
|
+
'app:instrument-center:data:instruments:create',
|
|
206
|
+
'app:instrument-center:data:instruments:update',
|
|
207
|
+
'app:instrument-center:data:instruments:delete',
|
|
208
|
+
...protectedCapabilities,
|
|
209
|
+
],
|
|
210
|
+
isAppSuperAdmin: false,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
}),
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (await fulfillSelector(route)) return;
|
|
218
|
+
if (path.endsWith('/native/data/instruments/query')) {
|
|
219
|
+
queryBodies.push(expectDeclaredInstrumentQuery(request));
|
|
220
|
+
operations.push('query');
|
|
221
|
+
await route.fulfill({
|
|
222
|
+
contentType: 'application/json',
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
code: 200,
|
|
225
|
+
data: {
|
|
226
|
+
schemaVersion: 'openxiangda.data-page/v2',
|
|
227
|
+
resourceCode: 'instruments',
|
|
228
|
+
items: record ? [record] : [],
|
|
229
|
+
total: record ? 1 : 0,
|
|
230
|
+
limit: 20,
|
|
231
|
+
offset: 0,
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (path.endsWith('/files/uploads/initiate')) {
|
|
238
|
+
const body = request.postDataJSON() as {
|
|
239
|
+
fieldCode: string;
|
|
240
|
+
fileName: string;
|
|
241
|
+
fileSize: number;
|
|
242
|
+
};
|
|
243
|
+
expect(body).toMatchObject({
|
|
244
|
+
fieldCode: 'attachments',
|
|
245
|
+
fileName: '设备说明书.pdf',
|
|
246
|
+
});
|
|
247
|
+
operations.push('upload-initiate');
|
|
248
|
+
await route.fulfill({
|
|
249
|
+
contentType: 'application/json',
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
code: 200,
|
|
252
|
+
data: {
|
|
253
|
+
schemaVersion: 'openxiangda.data-file-upload-plan/v2',
|
|
254
|
+
resourceCode: 'instruments',
|
|
255
|
+
fieldCode: 'attachments',
|
|
256
|
+
file: {
|
|
257
|
+
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
258
|
+
id: 'instrument-file-1',
|
|
259
|
+
name: body.fileName,
|
|
260
|
+
size: body.fileSize,
|
|
261
|
+
contentType: 'application/pdf',
|
|
262
|
+
},
|
|
263
|
+
uploadMethod: 'PUT',
|
|
264
|
+
uploadUrl: `${url.origin}/mock-upload/instrument-file-1`,
|
|
265
|
+
headers: {},
|
|
266
|
+
expiresAt: '2026-08-21T18:00:00.000Z',
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (path.endsWith('/files/instrument-file-1/complete')) {
|
|
273
|
+
operations.push('upload-complete');
|
|
274
|
+
await route.fulfill({
|
|
275
|
+
contentType: 'application/json',
|
|
276
|
+
body: JSON.stringify({
|
|
277
|
+
code: 200,
|
|
278
|
+
data: {
|
|
279
|
+
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
280
|
+
id: 'instrument-file-1',
|
|
281
|
+
name: '设备说明书.pdf',
|
|
282
|
+
size: 2048,
|
|
283
|
+
contentType: 'application/pdf',
|
|
284
|
+
},
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (path.endsWith('/native/data/instruments/records')) {
|
|
290
|
+
const body = request.postDataJSON() as {
|
|
291
|
+
environmentKey: string;
|
|
292
|
+
data: Record<string, unknown>;
|
|
293
|
+
};
|
|
294
|
+
expect(body.environmentKey).toBe('production');
|
|
295
|
+
expect(body.data.attachments).toEqual([
|
|
296
|
+
{
|
|
297
|
+
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
298
|
+
id: 'instrument-file-1',
|
|
299
|
+
name: '设备说明书.pdf',
|
|
300
|
+
size: 2048,
|
|
301
|
+
contentType: 'application/pdf',
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
record = {
|
|
305
|
+
...body.data,
|
|
306
|
+
id: '092b0000-0000-4000-8000-000000000001',
|
|
307
|
+
revision: 1,
|
|
308
|
+
updated_at: '2026-08-21T01:00:00.000Z',
|
|
309
|
+
};
|
|
310
|
+
operations.push('create');
|
|
311
|
+
await fulfillRecord(route, record);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (path.endsWith('/update')) {
|
|
315
|
+
const body = request.postDataJSON() as {
|
|
316
|
+
environmentKey: string;
|
|
317
|
+
expectedRevision: number;
|
|
318
|
+
data: Record<string, unknown>;
|
|
319
|
+
};
|
|
320
|
+
expect(body.environmentKey).toBe('production');
|
|
321
|
+
expect(body.expectedRevision).toBe(record?.revision);
|
|
322
|
+
record = {
|
|
323
|
+
...record,
|
|
324
|
+
...body.data,
|
|
325
|
+
revision: Number(record?.revision) + 1,
|
|
326
|
+
updated_at: '2026-08-21T02:00:00.000Z',
|
|
327
|
+
};
|
|
328
|
+
operations.push('update');
|
|
329
|
+
await fulfillRecord(route, record);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (path.endsWith('/delete')) {
|
|
333
|
+
const deleted = record!;
|
|
334
|
+
const body = request.postDataJSON() as {
|
|
335
|
+
environmentKey: string;
|
|
336
|
+
expectedRevision: number;
|
|
337
|
+
};
|
|
338
|
+
expect(body.environmentKey).toBe('production');
|
|
339
|
+
expect(body.expectedRevision).toBe(record?.revision);
|
|
340
|
+
record = undefined;
|
|
341
|
+
operations.push('delete');
|
|
342
|
+
await fulfillRecord(route, deleted);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (path.endsWith('/audit')) {
|
|
346
|
+
await route.fulfill({
|
|
347
|
+
contentType: 'application/json',
|
|
348
|
+
body: JSON.stringify({
|
|
349
|
+
code: 200,
|
|
350
|
+
data: {
|
|
351
|
+
schemaVersion: 'openxiangda.data-audit-page/v2',
|
|
352
|
+
resourceCode: 'instruments',
|
|
353
|
+
recordId: record?.id,
|
|
354
|
+
items: [],
|
|
355
|
+
total: 0,
|
|
356
|
+
limit: 10,
|
|
357
|
+
offset: 0,
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
});
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (path.includes('/native/data/instruments/records/')) {
|
|
364
|
+
operations.push('get');
|
|
365
|
+
if (!record) {
|
|
366
|
+
await route.fulfill({
|
|
367
|
+
status: 404,
|
|
368
|
+
contentType: 'application/json',
|
|
369
|
+
body: JSON.stringify({ code: 404, message: 'not found' }),
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await fulfillRecord(route, record);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
await route.fulfill({
|
|
377
|
+
status: 404,
|
|
378
|
+
contentType: 'application/json',
|
|
379
|
+
body: JSON.stringify({ code: 404, message: 'not mocked' }),
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await page.goto('/view/instrument-center/instruments/new');
|
|
384
|
+
await expect(
|
|
385
|
+
page.getByRole('heading', { name: '新增仪器', level: 2 })
|
|
386
|
+
).toBeVisible();
|
|
387
|
+
await expect(page.getByText('管理与联系')).toBeVisible();
|
|
388
|
+
await expect(page.getByText('地址与资料')).toBeVisible();
|
|
389
|
+
await page.getByLabel('仪器编码').fill('INS-LIVE-001');
|
|
390
|
+
await page.getByLabel('仪器中文名').fill('原位电子显微镜');
|
|
391
|
+
await page.getByLabel('规格型号').fill('EM-LIVE');
|
|
392
|
+
await chooseOption(page, '仪器分类', '显微成像');
|
|
393
|
+
await page.getByLabel('资产编码').fill('ASSET-LIVE-001');
|
|
394
|
+
await page.getByLabel('启用日期').fill('2026-08-21');
|
|
395
|
+
await chooseOption(page, '所属学院', '先进材料学院');
|
|
396
|
+
await pickDirectoryDepartment(page, '管理部门', '材料测试中心');
|
|
397
|
+
await pickDirectoryDepartment(page, '使用部门', '材料测试中心');
|
|
398
|
+
await pickDirectoryMember(page, '仪器管理员', '王老师');
|
|
399
|
+
await pickDirectoryMember(page, '联系人', '王老师');
|
|
400
|
+
await page.getByLabel('联系电话').fill('13800138000');
|
|
401
|
+
await page
|
|
402
|
+
.locator('.oxa-file-field input[type="file"]')
|
|
403
|
+
.last()
|
|
404
|
+
.setInputFiles({
|
|
405
|
+
name: '设备说明书.pdf',
|
|
406
|
+
mimeType: 'application/pdf',
|
|
407
|
+
buffer: Buffer.from('openxiangda instrument manual'),
|
|
408
|
+
});
|
|
409
|
+
await expect(page.getByText('设备说明书.pdf')).toBeVisible();
|
|
410
|
+
await page.getByTestId('save-instrument').click();
|
|
411
|
+
|
|
412
|
+
await expect(page).toHaveURL(/\/view\/instrument-center\/instruments$/);
|
|
413
|
+
await expect(page.getByText('原位电子显微镜')).toBeVisible();
|
|
414
|
+
await expect(page.locator('.ant-table-tbody')).toContainText('2026');
|
|
415
|
+
await expect(
|
|
416
|
+
page
|
|
417
|
+
.getByRole('columnheader', { name: '更新时间' })
|
|
418
|
+
.locator('.ant-table-column-sorter')
|
|
419
|
+
).toHaveCount(0);
|
|
420
|
+
const queriesBeforeSort = queryBodies.length;
|
|
421
|
+
await page.getByRole('columnheader', { name: /仪器名称/ }).click();
|
|
422
|
+
await expect.poll(() => queryBodies.length).toBeGreaterThan(queriesBeforeSort);
|
|
423
|
+
expect(
|
|
424
|
+
(queryBodies.at(-1) as { order: Array<{ field: string; direction: string }> })
|
|
425
|
+
.order
|
|
426
|
+
).toEqual([{ field: 'chineseName', direction: 'asc' }]);
|
|
427
|
+
await page.getByText('原位电子显微镜').click();
|
|
428
|
+
await expect(
|
|
429
|
+
page.getByRole('heading', { name: '仪器详情', level: 2 })
|
|
430
|
+
).toBeVisible();
|
|
431
|
+
await expect(page.getByText('记录信息')).toBeVisible();
|
|
432
|
+
await expect(page.getByText('设备说明书.pdf')).toBeVisible();
|
|
433
|
+
await page.getByRole('button', { name: /编.*辑/ }).click();
|
|
434
|
+
await expect(page.getByRole('dialog')).toHaveCount(0);
|
|
435
|
+
await expect(page.getByText('管理与联系')).toBeVisible();
|
|
436
|
+
await page.getByLabel('仪器中文名').fill('原位电子显微镜(更新)');
|
|
437
|
+
await page.getByTestId('save-instrument').click();
|
|
438
|
+
await expect(
|
|
439
|
+
page.getByRole('heading', { name: '原位电子显微镜(更新)' })
|
|
440
|
+
).toBeVisible();
|
|
441
|
+
await page.getByRole('button', { name: '返回列表' }).click();
|
|
442
|
+
await expect(page).toHaveURL(/\/view\/instrument-center\/instruments$/);
|
|
443
|
+
await expect(
|
|
444
|
+
page.locator('.ant-table-tbody').getByText('原位电子显微镜(更新)')
|
|
445
|
+
).toBeVisible();
|
|
446
|
+
await page.getByLabel('删除').click();
|
|
447
|
+
await page.getByRole('button', { name: /确.*定/ }).click();
|
|
448
|
+
await expect(
|
|
449
|
+
page.locator('.ant-table-tbody').getByText('原位电子显微镜(更新)')
|
|
450
|
+
).toHaveCount(0);
|
|
451
|
+
|
|
452
|
+
await page.setViewportSize({ width: 760, height: 900 });
|
|
453
|
+
await page.goto('/view/instrument-center/instruments/new');
|
|
454
|
+
await expect(
|
|
455
|
+
page.getByRole('heading', { name: '新增仪器', level: 2 })
|
|
456
|
+
).toBeVisible();
|
|
457
|
+
await expect(page.getByRole('dialog', { name: '新增仪器' })).toHaveCount(0);
|
|
458
|
+
await expect(page.getByRole('button', { name: '展开侧边栏' })).toBeVisible();
|
|
459
|
+
|
|
460
|
+
expect(queryBodies.length).toBeGreaterThanOrEqual(2);
|
|
461
|
+
expect(operations).toEqual(
|
|
462
|
+
expect.arrayContaining([
|
|
463
|
+
'upload-initiate',
|
|
464
|
+
'upload-complete',
|
|
465
|
+
'create',
|
|
466
|
+
'query',
|
|
467
|
+
'get',
|
|
468
|
+
'update',
|
|
469
|
+
'delete',
|
|
470
|
+
])
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('college admin can choose only granted college on create and cannot reassign on update', async ({
|
|
475
|
+
page,
|
|
476
|
+
}) => {
|
|
477
|
+
const requests: string[] = [];
|
|
478
|
+
page.on('request', request => requests.push(request.url()));
|
|
479
|
+
const protectedCreate = instrumentFields.flatMap(field =>
|
|
480
|
+
field.createCapability ? [field.createCapability] : []
|
|
481
|
+
);
|
|
482
|
+
const protectedUpdate = instrumentFields.flatMap(field =>
|
|
483
|
+
field.updateCapability && field.key !== 'collegeId'
|
|
484
|
+
? [field.updateCapability]
|
|
485
|
+
: []
|
|
486
|
+
);
|
|
487
|
+
await installProductionRuntimeMeta(page);
|
|
488
|
+
await page.route('**/service/**', async route => {
|
|
489
|
+
const url = new URL(route.request().url());
|
|
490
|
+
if (url.pathname.endsWith('/native/current-user')) {
|
|
491
|
+
await fulfillIdentity(route, 'college_admin', [
|
|
492
|
+
'app:instrument-center:data:instruments:read',
|
|
493
|
+
'app:instrument-center:data:instruments:create',
|
|
494
|
+
'app:instrument-center:data:instruments:update',
|
|
495
|
+
...protectedCreate,
|
|
496
|
+
...protectedUpdate,
|
|
497
|
+
]);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
if (await fulfillSelector(route)) return;
|
|
501
|
+
if (url.pathname.includes('/native/data/instruments/records/')) {
|
|
502
|
+
await fulfillRecord(route, instrumentFixture());
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
await route.fulfill({ status: 404, body: '{}' });
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
await page.goto('/view/instrument-center/instruments/new');
|
|
509
|
+
await expect(page.getByLabel('所属学院')).toBeEnabled();
|
|
510
|
+
await chooseOption(page, '所属学院', '先进材料学院');
|
|
511
|
+
await expect(page.getByLabel('所属学院')).toHaveValue('');
|
|
512
|
+
await expect(page.getByLabel('仪器管理员')).toBeEnabled();
|
|
513
|
+
|
|
514
|
+
await page.goto(
|
|
515
|
+
'/view/instrument-center/instruments/092b0000-0000-4000-8000-000000000001/edit'
|
|
516
|
+
);
|
|
517
|
+
await expect(page.getByLabel('所属学院')).toBeDisabled();
|
|
518
|
+
await expect(page.getByLabel('仪器管理员')).toBeEnabled();
|
|
519
|
+
expect(requests.filter(url => url.includes('role-session'))).toHaveLength(0);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test('instrument admin edits ordinary fields but every protected field stays disabled', async ({
|
|
523
|
+
page,
|
|
524
|
+
}) => {
|
|
525
|
+
const requests: string[] = [];
|
|
526
|
+
page.on('request', request => requests.push(request.url()));
|
|
527
|
+
await installProductionRuntimeMeta(page);
|
|
528
|
+
await page.route('**/service/**', async route => {
|
|
529
|
+
const url = new URL(route.request().url());
|
|
530
|
+
if (url.pathname.endsWith('/native/current-user')) {
|
|
531
|
+
await fulfillIdentity(route, 'instrument_admin', [
|
|
532
|
+
'app:instrument-center:data:instruments:read',
|
|
533
|
+
'app:instrument-center:data:instruments:update',
|
|
534
|
+
]);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (await fulfillSelector(route)) return;
|
|
538
|
+
if (url.pathname.includes('/native/data/instruments/records/')) {
|
|
539
|
+
await fulfillRecord(route, instrumentFixture());
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
await route.fulfill({ status: 404, body: '{}' });
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
await page.goto(
|
|
546
|
+
'/view/instrument-center/instruments/092b0000-0000-4000-8000-000000000001/edit'
|
|
547
|
+
);
|
|
548
|
+
for (const label of [
|
|
549
|
+
'仪器编码',
|
|
550
|
+
'资产编码',
|
|
551
|
+
'资产价值(元)',
|
|
552
|
+
'所属学院',
|
|
553
|
+
'仪器管理员',
|
|
554
|
+
]) {
|
|
555
|
+
await expect(page.getByLabel(label)).toBeDisabled();
|
|
556
|
+
}
|
|
557
|
+
await expect(page.getByLabel('仪器中文名')).toBeEnabled();
|
|
558
|
+
expect(requests.filter(url => url.includes('role-session'))).toHaveLength(0);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test('directory selector debounces search and keeps empty or failed results honest', async ({
|
|
562
|
+
page,
|
|
563
|
+
}) => {
|
|
564
|
+
const keywords: string[] = [];
|
|
565
|
+
await installProductionRuntimeMeta(page);
|
|
566
|
+
await page.route('**/service/**', async route => {
|
|
567
|
+
const url = new URL(route.request().url());
|
|
568
|
+
if (url.pathname.endsWith('/native/current-user')) {
|
|
569
|
+
await fulfillIdentity(route, 'school_admin', [
|
|
570
|
+
'app:instrument-center:data:instruments:read',
|
|
571
|
+
'app:instrument-center:data:instruments:create',
|
|
572
|
+
...instrumentFields.flatMap(field =>
|
|
573
|
+
field.createCapability ? [field.createCapability] : []
|
|
574
|
+
),
|
|
575
|
+
]);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (url.pathname.endsWith('/directory/users')) {
|
|
579
|
+
const keyword = url.searchParams.get('keyword') || '';
|
|
580
|
+
keywords.push(keyword);
|
|
581
|
+
if (keyword === '错误词') {
|
|
582
|
+
await route.fulfill({
|
|
583
|
+
status: 503,
|
|
584
|
+
contentType: 'application/json',
|
|
585
|
+
body: JSON.stringify({
|
|
586
|
+
code: 503,
|
|
587
|
+
errorCode: 'DIRECTORY_UNAVAILABLE',
|
|
588
|
+
message: '目录暂不可用',
|
|
589
|
+
}),
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
await route.fulfill({
|
|
594
|
+
contentType: 'application/json',
|
|
595
|
+
body: JSON.stringify({
|
|
596
|
+
code: 200,
|
|
597
|
+
data: directoryPage('user', []),
|
|
598
|
+
}),
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
if (await fulfillSelector(route)) return;
|
|
603
|
+
await route.fulfill({ status: 404, body: '{}' });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
await page.goto('/view/instrument-center/instruments/new');
|
|
607
|
+
await page.getByRole('button', { name: /仪器管理员/ }).click();
|
|
608
|
+
const dialog = page.getByRole('dialog', { name: '选择成员' });
|
|
609
|
+
const input = dialog.getByPlaceholder('搜索成员姓名或工号');
|
|
610
|
+
await input.fill('王老师');
|
|
611
|
+
await input.fill('李老师');
|
|
612
|
+
await expect.poll(() => keywords).toEqual(['李老师']);
|
|
613
|
+
await expect(dialog.getByText('暂无成员')).toBeVisible();
|
|
614
|
+
await input.fill('错误词');
|
|
615
|
+
await expect.poll(() => keywords.at(-1)).toBe('错误词');
|
|
616
|
+
await expect(dialog.getByText(/DIRECTORY_UNAVAILABLE/)).toBeVisible();
|
|
617
|
+
await expect(dialog.getByText('王老师')).toHaveCount(0);
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test('school admin maintains the app-owned college UUID catalog without delete', async ({
|
|
621
|
+
page,
|
|
622
|
+
}) => {
|
|
623
|
+
let colleges = [
|
|
624
|
+
{ id: collegeId, revision: 1, name: '先进材料学院', enabled: true },
|
|
625
|
+
];
|
|
626
|
+
const operations: string[] = [];
|
|
627
|
+
await installProductionRuntimeMeta(page);
|
|
628
|
+
await page.route('**/service/**', async route => {
|
|
629
|
+
const request = route.request();
|
|
630
|
+
const url = new URL(request.url());
|
|
631
|
+
if (url.pathname.endsWith('/native/current-user')) {
|
|
632
|
+
await fulfillIdentity(route, 'school_admin', [
|
|
633
|
+
'app:instrument-center:data:colleges:read',
|
|
634
|
+
'app:instrument-center:data:colleges:create',
|
|
635
|
+
'app:instrument-center:data:colleges:update',
|
|
636
|
+
]);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (url.pathname.endsWith('/native/data/colleges/query')) {
|
|
640
|
+
operations.push('query');
|
|
641
|
+
await route.fulfill({
|
|
642
|
+
contentType: 'application/json',
|
|
643
|
+
body: JSON.stringify({
|
|
644
|
+
code: 200,
|
|
645
|
+
data: {
|
|
646
|
+
schemaVersion: 'openxiangda.data-page/v2',
|
|
647
|
+
resourceCode: 'colleges',
|
|
648
|
+
items: colleges,
|
|
649
|
+
total: colleges.length,
|
|
650
|
+
limit: 20,
|
|
651
|
+
offset: 0,
|
|
652
|
+
},
|
|
653
|
+
}),
|
|
654
|
+
});
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (url.pathname.endsWith(`/native/data/colleges/records/${collegeId}/update`)) {
|
|
658
|
+
const body = request.postDataJSON() as {
|
|
659
|
+
expectedRevision: number;
|
|
660
|
+
data: { name: string; enabled: boolean };
|
|
661
|
+
};
|
|
662
|
+
expect(body.expectedRevision).toBe(1);
|
|
663
|
+
colleges = [{ ...colleges[0]!, ...body.data, revision: 2 }];
|
|
664
|
+
operations.push('update');
|
|
665
|
+
await fulfillCollegeRecord(route, colleges[0]!);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (url.pathname.endsWith('/native/data/colleges/records')) {
|
|
669
|
+
const body = request.postDataJSON() as {
|
|
670
|
+
data: { name: string; enabled: boolean };
|
|
671
|
+
};
|
|
672
|
+
const created = {
|
|
673
|
+
id: '44444444-4444-4444-8444-444444444444',
|
|
674
|
+
revision: 1,
|
|
675
|
+
...body.data,
|
|
676
|
+
};
|
|
677
|
+
colleges = [...colleges, created];
|
|
678
|
+
operations.push('create');
|
|
679
|
+
await fulfillCollegeRecord(route, created);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
await route.fulfill({ status: 404, body: '{}' });
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
await page.goto('/view/instrument-center/colleges');
|
|
686
|
+
await expect(page.getByText('先进材料学院')).toBeVisible();
|
|
687
|
+
await page.getByRole('button', { name: /编.*辑/ }).click();
|
|
688
|
+
await page.getByLabel('学院名称').fill('材料科学学院');
|
|
689
|
+
await page.getByRole('button', { name: /保.*存/ }).click();
|
|
690
|
+
await expect(page.getByText('材料科学学院')).toBeVisible();
|
|
691
|
+
await page.getByRole('button', { name: /新.*增学院/ }).click();
|
|
692
|
+
await page.getByLabel('学院名称').fill('生命科学学院');
|
|
693
|
+
await page.getByRole('button', { name: /保.*存/ }).click();
|
|
694
|
+
await expect(page.getByText('生命科学学院')).toBeVisible();
|
|
695
|
+
await expect(page.getByRole('button', { name: /删.*除/ })).toHaveCount(0);
|
|
696
|
+
expect(operations).toEqual(
|
|
697
|
+
expect.arrayContaining(['query', 'update', 'create'])
|
|
698
|
+
);
|
|
699
|
+
});
|
|
700
|
+
|
|
122
701
|
test('keeps the production warning visible when current-user fails closed', async ({
|
|
123
702
|
page,
|
|
124
703
|
}) => {
|
|
@@ -148,3 +727,327 @@ test('keeps the production warning visible when current-user fails closed', asyn
|
|
|
148
727
|
await expect(page.getByTestId('production-data-warning')).toBeVisible();
|
|
149
728
|
await expect(page.getByText('无法读取平台当前用户')).toBeVisible();
|
|
150
729
|
});
|
|
730
|
+
|
|
731
|
+
async function installProductionRuntimeMeta(page: Page) {
|
|
732
|
+
await page.route('**/view/instrument-center/**', async route => {
|
|
733
|
+
if (route.request().resourceType() !== 'document') {
|
|
734
|
+
await route.continue();
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const response = await route.fetch();
|
|
738
|
+
const body = (await response.text()).replace(
|
|
739
|
+
'<head>',
|
|
740
|
+
`<head>
|
|
741
|
+
<meta name="openxiangda-runtime-base" content="/view/instrument-center/" />
|
|
742
|
+
<meta name="openxiangda-app-code" content="instrument-center" />
|
|
743
|
+
<meta name="openxiangda-environment" content="production" />`
|
|
744
|
+
);
|
|
745
|
+
await route.fulfill({ response, body });
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async function chooseOption(page: Page, label: string, option: string) {
|
|
750
|
+
const input = page.getByLabel(label);
|
|
751
|
+
await input.focus();
|
|
752
|
+
await input.press('ArrowDown');
|
|
753
|
+
await expect(
|
|
754
|
+
page.getByRole('option', { name: option, exact: true })
|
|
755
|
+
).toHaveCount(1);
|
|
756
|
+
await input.press('Enter');
|
|
757
|
+
await input.press('Escape');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function pickDirectoryMember(
|
|
761
|
+
page: Page,
|
|
762
|
+
label: string,
|
|
763
|
+
option: string
|
|
764
|
+
) {
|
|
765
|
+
const trigger = page.getByRole('button', { name: new RegExp(label) });
|
|
766
|
+
await trigger.click();
|
|
767
|
+
const dialog = page.getByRole('dialog', { name: '选择成员' });
|
|
768
|
+
await expect(dialog.getByText(option, { exact: true })).toBeVisible();
|
|
769
|
+
await dialog.getByText(option, { exact: true }).click();
|
|
770
|
+
await dialog.getByRole('button', { name: /确.*定/ }).click();
|
|
771
|
+
await expect(trigger).toContainText(option);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
async function pickDirectoryDepartment(
|
|
775
|
+
page: Page,
|
|
776
|
+
label: string,
|
|
777
|
+
option: string
|
|
778
|
+
) {
|
|
779
|
+
const trigger = page.getByRole('button', { name: new RegExp(label) });
|
|
780
|
+
await trigger.click();
|
|
781
|
+
const dialog = page.getByRole('dialog', { name: '选择部门' });
|
|
782
|
+
await dialog.getByText(option, { exact: true }).click();
|
|
783
|
+
await dialog.getByRole('button', { name: /确.*定/ }).click();
|
|
784
|
+
await expect(trigger).toContainText(option);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
async function fulfillSelector(route: Route) {
|
|
788
|
+
const request = route.request();
|
|
789
|
+
const url = new URL(request.url());
|
|
790
|
+
if (url.pathname.endsWith('/directory/departments/tree')) {
|
|
791
|
+
await route.fulfill({
|
|
792
|
+
contentType: 'application/json',
|
|
793
|
+
body: JSON.stringify({
|
|
794
|
+
code: 200,
|
|
795
|
+
data: directoryPage('department', [
|
|
796
|
+
{
|
|
797
|
+
kind: 'department',
|
|
798
|
+
id: departmentId,
|
|
799
|
+
label: '材料测试中心',
|
|
800
|
+
description: '先进材料学院 / 材料测试中心',
|
|
801
|
+
path: [
|
|
802
|
+
{ id: collegeId, label: '先进材料学院' },
|
|
803
|
+
{ id: departmentId, label: '材料测试中心' },
|
|
804
|
+
],
|
|
805
|
+
hasChildren: false,
|
|
806
|
+
selectable: true,
|
|
807
|
+
},
|
|
808
|
+
]),
|
|
809
|
+
}),
|
|
810
|
+
});
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
if (
|
|
814
|
+
url.pathname.endsWith(
|
|
815
|
+
`/directory/departments/${encodeURIComponent(departmentId)}/users`
|
|
816
|
+
)
|
|
817
|
+
) {
|
|
818
|
+
await route.fulfill({
|
|
819
|
+
contentType: 'application/json',
|
|
820
|
+
body: JSON.stringify({
|
|
821
|
+
code: 200,
|
|
822
|
+
data: directoryPage('user', [
|
|
823
|
+
{
|
|
824
|
+
kind: 'user',
|
|
825
|
+
id: userId,
|
|
826
|
+
label: '王老师',
|
|
827
|
+
description: '工号 10001 · 材料测试中心',
|
|
828
|
+
selectable: true,
|
|
829
|
+
},
|
|
830
|
+
]),
|
|
831
|
+
}),
|
|
832
|
+
});
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
if (url.pathname.endsWith('/directory/users')) {
|
|
836
|
+
await route.fulfill({
|
|
837
|
+
contentType: 'application/json',
|
|
838
|
+
body: JSON.stringify({
|
|
839
|
+
code: 200,
|
|
840
|
+
data: directoryPage('user', [
|
|
841
|
+
{ kind: 'user', id: userId, label: '王老师', selectable: true },
|
|
842
|
+
]),
|
|
843
|
+
}),
|
|
844
|
+
});
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
if (url.pathname.endsWith('/directory/departments')) {
|
|
848
|
+
await route.fulfill({
|
|
849
|
+
contentType: 'application/json',
|
|
850
|
+
body: JSON.stringify({
|
|
851
|
+
code: 200,
|
|
852
|
+
data: directoryPage('department', [
|
|
853
|
+
{
|
|
854
|
+
kind: 'department',
|
|
855
|
+
id: departmentId,
|
|
856
|
+
label: '材料测试中心',
|
|
857
|
+
selectable: true,
|
|
858
|
+
},
|
|
859
|
+
]),
|
|
860
|
+
}),
|
|
861
|
+
});
|
|
862
|
+
return true;
|
|
863
|
+
}
|
|
864
|
+
if (url.pathname.endsWith('/directory/resolve')) {
|
|
865
|
+
const body = request.postDataJSON() as {
|
|
866
|
+
kind: 'user' | 'department';
|
|
867
|
+
ids: string[];
|
|
868
|
+
};
|
|
869
|
+
await route.fulfill({
|
|
870
|
+
contentType: 'application/json',
|
|
871
|
+
body: JSON.stringify({
|
|
872
|
+
code: 200,
|
|
873
|
+
data: directoryPage(
|
|
874
|
+
body.kind,
|
|
875
|
+
body.ids.map(id => ({
|
|
876
|
+
kind: body.kind,
|
|
877
|
+
id,
|
|
878
|
+
label: body.kind === 'user' ? '王老师' : '材料测试中心',
|
|
879
|
+
selectable: true,
|
|
880
|
+
}))
|
|
881
|
+
),
|
|
882
|
+
}),
|
|
883
|
+
});
|
|
884
|
+
return true;
|
|
885
|
+
}
|
|
886
|
+
if (url.pathname.endsWith('/scope-values')) {
|
|
887
|
+
await route.fulfill({
|
|
888
|
+
contentType: 'application/json',
|
|
889
|
+
body: JSON.stringify({
|
|
890
|
+
code: 200,
|
|
891
|
+
data: scopePage(url.searchParams.get('operation') || 'update', [
|
|
892
|
+
{ value: collegeId, label: '先进材料学院', selectable: true },
|
|
893
|
+
]),
|
|
894
|
+
}),
|
|
895
|
+
});
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
if (url.pathname.endsWith('/scope-values/resolve')) {
|
|
899
|
+
const body = request.postDataJSON() as {
|
|
900
|
+
operation: string;
|
|
901
|
+
values: string[];
|
|
902
|
+
};
|
|
903
|
+
await route.fulfill({
|
|
904
|
+
contentType: 'application/json',
|
|
905
|
+
body: JSON.stringify({
|
|
906
|
+
code: 200,
|
|
907
|
+
data: scopePage(
|
|
908
|
+
body.operation,
|
|
909
|
+
body.values.map(value => ({
|
|
910
|
+
value,
|
|
911
|
+
label: '先进材料学院',
|
|
912
|
+
selectable: true,
|
|
913
|
+
}))
|
|
914
|
+
),
|
|
915
|
+
}),
|
|
916
|
+
});
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function directoryPage(
|
|
923
|
+
kind: 'user' | 'department',
|
|
924
|
+
items: Array<{
|
|
925
|
+
kind: 'user' | 'department';
|
|
926
|
+
id: string;
|
|
927
|
+
label: string;
|
|
928
|
+
description?: string;
|
|
929
|
+
path?: Array<{ id: string; label: string }>;
|
|
930
|
+
hasChildren?: boolean;
|
|
931
|
+
selectable: boolean;
|
|
932
|
+
}>
|
|
933
|
+
) {
|
|
934
|
+
return {
|
|
935
|
+
schemaVersion: 'openxiangda.directory-entry-page/v2',
|
|
936
|
+
kind,
|
|
937
|
+
items,
|
|
938
|
+
nextCursor: null,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function scopePage(
|
|
943
|
+
operation: string,
|
|
944
|
+
items: Array<{ value: string; label: string; selectable: boolean }>
|
|
945
|
+
) {
|
|
946
|
+
return {
|
|
947
|
+
schemaVersion: 'openxiangda.native-scope-value-page/v2',
|
|
948
|
+
environment: {
|
|
949
|
+
id: 'production-id',
|
|
950
|
+
key: 'production',
|
|
951
|
+
activeAppVersionId: 'app-version-1',
|
|
952
|
+
headRevision: 7,
|
|
953
|
+
authzRevisionId: 'authz-revision-1',
|
|
954
|
+
authzVersion: 3,
|
|
955
|
+
scopeDataVersion: 'scope-data-version-1',
|
|
956
|
+
},
|
|
957
|
+
resourceCode: 'instruments',
|
|
958
|
+
fieldCode: 'collegeId',
|
|
959
|
+
dimensionCode: 'college',
|
|
960
|
+
operation,
|
|
961
|
+
items,
|
|
962
|
+
nextCursor: null,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
async function fulfillRecord(
|
|
967
|
+
route: Route,
|
|
968
|
+
record: Record<string, unknown>
|
|
969
|
+
) {
|
|
970
|
+
await route.fulfill({
|
|
971
|
+
contentType: 'application/json',
|
|
972
|
+
body: JSON.stringify({
|
|
973
|
+
code: 200,
|
|
974
|
+
data: {
|
|
975
|
+
schemaVersion: 'openxiangda.data-record/v2',
|
|
976
|
+
resourceCode: 'instruments',
|
|
977
|
+
data: record,
|
|
978
|
+
},
|
|
979
|
+
}),
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function fulfillCollegeRecord(
|
|
984
|
+
route: Route,
|
|
985
|
+
record: Record<string, unknown>
|
|
986
|
+
) {
|
|
987
|
+
await route.fulfill({
|
|
988
|
+
contentType: 'application/json',
|
|
989
|
+
body: JSON.stringify({
|
|
990
|
+
code: 200,
|
|
991
|
+
data: {
|
|
992
|
+
schemaVersion: 'openxiangda.data-record/v2',
|
|
993
|
+
resourceCode: 'colleges',
|
|
994
|
+
data: record,
|
|
995
|
+
},
|
|
996
|
+
}),
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function fulfillIdentity(
|
|
1001
|
+
route: Route,
|
|
1002
|
+
roleCode: string,
|
|
1003
|
+
capabilityCodes: string[]
|
|
1004
|
+
) {
|
|
1005
|
+
await route.fulfill({
|
|
1006
|
+
contentType: 'application/json',
|
|
1007
|
+
body: JSON.stringify({
|
|
1008
|
+
code: 200,
|
|
1009
|
+
data: {
|
|
1010
|
+
schemaVersion: 'openxiangda.current-user/v2',
|
|
1011
|
+
appCode: 'instrument-center',
|
|
1012
|
+
environment: {
|
|
1013
|
+
id: 'production-id',
|
|
1014
|
+
key: 'production',
|
|
1015
|
+
activeAppVersionId: 'app-version-1',
|
|
1016
|
+
headRevision: 7,
|
|
1017
|
+
authzRevisionId: 'authz-revision-1',
|
|
1018
|
+
authzVersion: 3,
|
|
1019
|
+
scopeDataVersion: 'scope-data-version-1',
|
|
1020
|
+
},
|
|
1021
|
+
principal: {
|
|
1022
|
+
type: 'user',
|
|
1023
|
+
userId,
|
|
1024
|
+
roleCodes: [roleCode],
|
|
1025
|
+
capabilityCodes,
|
|
1026
|
+
isAppSuperAdmin: false,
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
}),
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function instrumentFixture() {
|
|
1034
|
+
return {
|
|
1035
|
+
id: '092b0000-0000-4000-8000-000000000001',
|
|
1036
|
+
revision: 2,
|
|
1037
|
+
instrumentCode: 'INS-001',
|
|
1038
|
+
chineseName: '场发射扫描电子显微镜',
|
|
1039
|
+
specModel: 'SU8600',
|
|
1040
|
+
categoryId: 'analysis_microscope',
|
|
1041
|
+
assetCode: 'ASSET-001',
|
|
1042
|
+
enabledDate: '2025-01-01',
|
|
1043
|
+
usageStatus: 'active',
|
|
1044
|
+
instrumentStatus: 'normal',
|
|
1045
|
+
openToOutside: true,
|
|
1046
|
+
collegeId,
|
|
1047
|
+
manageDepartmentId: departmentId,
|
|
1048
|
+
useDepartmentId: departmentId,
|
|
1049
|
+
instrumentAdminIds: [userId],
|
|
1050
|
+
contactUserIds: [userId],
|
|
1051
|
+
contactPhone: '13800138000',
|
|
1052
|
+
};
|
|
1053
|
+
}
|