openxiangda-cli 2.0.0-alpha.84 → 2.0.0-alpha.86
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/package.json +4 -4
- package/template/AGENTS.md +4 -4
- package/template/README.md +2 -2
- package/template/apps/server/package.json +1 -1
- package/template/apps/web/data-api-live.e2e.html +15 -0
- package/template/apps/web/e2e/data-api-live-fixture.tsx +141 -0
- package/template/apps/web/e2e/data-api-live.spec.ts +138 -0
- package/template/apps/web/e2e/field-protocol-fixture.tsx +280 -0
- package/template/apps/web/e2e/field-protocol.spec.ts +100 -0
- package/template/apps/web/e2e/resources.spec.ts +258 -20
- package/template/apps/web/field-protocol.e2e.html +12 -0
- package/template/apps/web/package.json +1 -1
- package/template/apps/web/playwright.config.ts +3 -1
- package/template/apps/web/scripts/check.mjs +8 -5
- package/template/apps/web/scripts/verify-build.mjs +1 -1
- package/template/apps/web/src/AuthoritativeSelector.tsx +165 -132
- package/template/apps/web/src/FilePreviewPage.tsx +30 -9
- package/template/apps/web/src/RoleSubjectSelect.tsx +128 -0
- package/template/apps/web/src/Shell.tsx +93 -6
- package/template/apps/web/src/components/platform-fields/AddressField.tsx +338 -0
- package/template/apps/web/src/components/platform-fields/AttachmentFileList.tsx +25 -6
- package/template/apps/web/src/components/platform-fields/CascadeField.tsx +49 -0
- package/template/apps/web/src/components/platform-fields/DateTimeField.tsx +148 -0
- package/template/apps/web/src/components/platform-fields/JsonField.tsx +77 -0
- package/template/apps/web/src/components/platform-fields/LocationField.tsx +164 -0
- package/template/apps/web/src/components/platform-fields/PlatformDirectoryPicker.tsx +19 -44
- package/template/apps/web/src/components/platform-fields/ResourceReferenceField.tsx +67 -0
- package/template/apps/web/src/components/platform-fields/RichTextField.tsx +196 -0
- package/template/apps/web/src/components/platform-fields/SignatureField.tsx +315 -0
- package/template/apps/web/src/components/platform-fields/SubtableField.tsx +553 -0
- package/template/apps/web/src/components/platform-fields/address-value.ts +80 -0
- package/template/apps/web/src/components/platform-fields/cascade-value.ts +66 -0
- package/template/apps/web/src/components/platform-fields/directory-value.ts +53 -0
- package/template/apps/web/src/components/platform-fields/field-form-codec.ts +98 -0
- package/template/apps/web/src/components/platform-fields/location-value.ts +87 -0
- package/template/apps/web/src/components/platform-fields/resource-query.ts +131 -0
- package/template/apps/web/src/components/platform-fields/rich-text-value.ts +59 -0
- package/template/apps/web/src/components/platform-fields/subtable-value.ts +187 -0
- package/template/apps/web/src/components/resource/GeneratedResourceCrud.tsx +194 -108
- package/template/apps/web/src/components/resource/ResourceBatchActions.tsx +8 -1
- package/template/apps/web/src/components/resource/SurfaceFields.tsx +385 -79
- package/template/apps/web/src/components/resource/generated-resource-definition.ts +18 -0
- package/template/apps/web/src/components/resource/resource-import.ts +4 -4
- package/template/apps/web/src/data-provider.ts +59 -34
- package/template/apps/web/src/platform-client.ts +385 -111
- package/template/apps/web/src/runtime.tsx +166 -14
- package/template/apps/web/src/styles.css +264 -0
- package/template/apps/web/test/address-field.test.ts +66 -0
- package/template/apps/web/test/cascade-value.test.ts +68 -0
- package/template/apps/web/test/contracts.test.ts +29 -5
- package/template/apps/web/test/directory-value.test.ts +55 -0
- package/template/apps/web/test/field-bounds.test.ts +17 -0
- package/template/apps/web/test/field-form-codec.test.ts +85 -0
- package/template/apps/web/test/location-field.test.ts +83 -0
- package/template/apps/web/test/resource-import.test.ts +8 -7
- package/template/apps/web/test/resource-query.test.ts +144 -0
- package/template/apps/web/test/rich-json-field.test.ts +53 -0
- package/template/apps/web/test/signature-serial-field.test.ts +40 -0
- package/template/apps/web/test/subtable-value.test.ts +144 -0
- package/template/package.json +3 -3
- package/template/packages/contracts/src/generated.ts +69 -0
- package/template/scripts/verify-template-budget.mjs +11 -4
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { expect, test } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
const fieldCount = 38;
|
|
4
|
+
|
|
5
|
+
test.describe('Field Kit protocol acceptance', () => {
|
|
6
|
+
test.use({
|
|
7
|
+
geolocation: { longitude: 120.123456, latitude: 30.234567 },
|
|
8
|
+
permissions: ['geolocation'],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test.beforeEach(async ({ page }) => {
|
|
12
|
+
await page.route('**/china-divisions/**', async route => {
|
|
13
|
+
await route.fulfill({
|
|
14
|
+
contentType: 'application/json',
|
|
15
|
+
json: {
|
|
16
|
+
code: 200,
|
|
17
|
+
data: [{
|
|
18
|
+
adcode: '330000',
|
|
19
|
+
name: '浙江省',
|
|
20
|
+
level: 'province',
|
|
21
|
+
hasChildren: true,
|
|
22
|
+
}],
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('renders and operates every desktop edit/read component', async ({ page }) => {
|
|
29
|
+
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
30
|
+
await page.goto('/field-protocol.e2e.html');
|
|
31
|
+
await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
|
|
32
|
+
await expect(page.locator('[data-read-field]')).toHaveCount(fieldCount);
|
|
33
|
+
await expect(page.locator('[data-field-code="静态单选下拉"] .ant-select')).toBeVisible();
|
|
34
|
+
await expect(page.locator('[data-field-code="单选按钮"] .ant-radio-group')).toBeVisible();
|
|
35
|
+
await expect(page.locator('[data-field-code="复选框"] .ant-checkbox-group')).toBeVisible();
|
|
36
|
+
await expect(page.locator('[data-field-code="时间"] input')).toBeVisible();
|
|
37
|
+
await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
|
|
38
|
+
await expect(page.locator('[data-field-code="精确定位"]')).not.toContainText('手工');
|
|
39
|
+
await expect(page.locator('.ant-alert-error')).toHaveCount(0);
|
|
40
|
+
|
|
41
|
+
await page.locator('[data-field-code="精确定位"]').getByRole('button', { name: '浏览器定位' }).click();
|
|
42
|
+
await expect(page.locator('[data-field-code="精确定位"]')).toContainText('120.123456');
|
|
43
|
+
await expect(page.locator('[data-field-code="精确定位"]')).toContainText('30.234567');
|
|
44
|
+
|
|
45
|
+
await page
|
|
46
|
+
.locator('[data-field-code="业务签名"]')
|
|
47
|
+
.getByRole('button', { name: /(?:开始|重新)签名/ })
|
|
48
|
+
.click();
|
|
49
|
+
const canvas = page.locator('canvas[aria-label="手写签名画布"]');
|
|
50
|
+
await expect(canvas).toBeVisible();
|
|
51
|
+
const box = await canvas.boundingBox();
|
|
52
|
+
expect(box).not.toBeNull();
|
|
53
|
+
await canvas.dispatchEvent('pointerdown', {
|
|
54
|
+
pointerId: 1,
|
|
55
|
+
pointerType: 'pen',
|
|
56
|
+
buttons: 1,
|
|
57
|
+
clientX: box!.x + 30,
|
|
58
|
+
clientY: box!.y + 40,
|
|
59
|
+
});
|
|
60
|
+
for (let step = 1; step <= 8; step += 1) {
|
|
61
|
+
await canvas.dispatchEvent('pointermove', {
|
|
62
|
+
pointerId: 1,
|
|
63
|
+
pointerType: 'pen',
|
|
64
|
+
buttons: 1,
|
|
65
|
+
clientX: box!.x + 30 + (130 * step) / 8,
|
|
66
|
+
clientY: box!.y + 40 + (80 * step) / 8,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
await canvas.dispatchEvent('pointerup', {
|
|
70
|
+
pointerId: 1,
|
|
71
|
+
pointerType: 'pen',
|
|
72
|
+
buttons: 0,
|
|
73
|
+
clientX: box!.x + 160,
|
|
74
|
+
clientY: box!.y + 120,
|
|
75
|
+
});
|
|
76
|
+
const saveSignature = page.getByRole('button', { name: '保存签名' });
|
|
77
|
+
await expect(saveSignature).toBeEnabled();
|
|
78
|
+
await saveSignature.click();
|
|
79
|
+
await expect(page.locator('[data-field-code="业务签名"]')).not.toContainText('尚未签名');
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
|
83
|
+
).toBe(true);
|
|
84
|
+
await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-desktop.png' });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('renders every mobile control without horizontal overflow', async ({ page }) => {
|
|
88
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
89
|
+
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
90
|
+
await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
|
|
91
|
+
await expect(page.locator('.oxa-mobile-field')).toHaveCount(fieldCount);
|
|
92
|
+
await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
|
|
93
|
+
await expect(page.locator('[data-field-code="附件"] input[type="file"]')).toBeHidden();
|
|
94
|
+
await expect(page.locator('.ant-alert-error')).toHaveCount(0);
|
|
95
|
+
expect(
|
|
96
|
+
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
|
97
|
+
).toBe(true);
|
|
98
|
+
await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-mobile.png' });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -21,11 +21,132 @@ const definitions = resourceDefinitions as Record<string, DeclaredResource>;
|
|
|
21
21
|
const allCapabilities = [...capabilities] as string[];
|
|
22
22
|
const runtimeBase = `/view/${appCode}`;
|
|
23
23
|
|
|
24
|
+
const roleSubjects = [
|
|
25
|
+
{
|
|
26
|
+
subjectKey: 'membership:college-admin',
|
|
27
|
+
subjectKind: 'membership',
|
|
28
|
+
role: {
|
|
29
|
+
code: 'college_admin',
|
|
30
|
+
name: '学院管理员',
|
|
31
|
+
source: 'package',
|
|
32
|
+
description: '管理本学院业务数据',
|
|
33
|
+
},
|
|
34
|
+
revision: 1,
|
|
35
|
+
scopeDimensionCount: 1,
|
|
36
|
+
scopeSummary: [],
|
|
37
|
+
scopeSummaryTruncated: false,
|
|
38
|
+
validFrom: null,
|
|
39
|
+
validTo: null,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
subjectKey: 'membership:instrument-admin',
|
|
43
|
+
subjectKind: 'membership',
|
|
44
|
+
role: {
|
|
45
|
+
code: 'instrument_admin',
|
|
46
|
+
name: '仪器管理员',
|
|
47
|
+
source: 'package',
|
|
48
|
+
description: '管理分配的仪器',
|
|
49
|
+
},
|
|
50
|
+
revision: 1,
|
|
51
|
+
scopeDimensionCount: 0,
|
|
52
|
+
scopeSummary: [],
|
|
53
|
+
scopeSummaryTruncated: false,
|
|
54
|
+
validFrom: null,
|
|
55
|
+
validTo: null,
|
|
56
|
+
},
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
function roleContext(subjectKey: string, authorized: boolean) {
|
|
60
|
+
const subject = roleSubjects.find(item => item.subjectKey === subjectKey)!;
|
|
61
|
+
const roleSessionId = `role-session-${subject.role.code}`;
|
|
62
|
+
return {
|
|
63
|
+
schemaVersion: 'openxiangda.role-session-context/v2',
|
|
64
|
+
state: 'active',
|
|
65
|
+
environment: {
|
|
66
|
+
id: 'preproduction-id',
|
|
67
|
+
key: 'preproduction',
|
|
68
|
+
kind: 'preproduction',
|
|
69
|
+
activeAppVersionId: 'app-version-1',
|
|
70
|
+
headRevision: 1,
|
|
71
|
+
authzVersion: 1,
|
|
72
|
+
roleSubjectSetVersion: 'subjects-1',
|
|
73
|
+
},
|
|
74
|
+
identityScope: `preproduction-id:${subjectKey}:1`,
|
|
75
|
+
subjectProfile: {
|
|
76
|
+
schemaVersion: 'openxiangda.subject-profile/v2',
|
|
77
|
+
userId: 'acceptance-user',
|
|
78
|
+
displayName: '验收用户',
|
|
79
|
+
avatarUrl: null,
|
|
80
|
+
jobNumber: null,
|
|
81
|
+
affiliatedDepartment: { id: 'department-1', name: '实验中心' },
|
|
82
|
+
},
|
|
83
|
+
currentRoleSubject: subject,
|
|
84
|
+
roleSubjectPage: {
|
|
85
|
+
schemaVersion: 'openxiangda.role-subject-page/v2',
|
|
86
|
+
items: roleSubjects,
|
|
87
|
+
total: roleSubjects.length,
|
|
88
|
+
nextCursor: null,
|
|
89
|
+
roleSubjectSetVersion: 'subjects-1',
|
|
90
|
+
},
|
|
91
|
+
roleSession: {
|
|
92
|
+
schemaVersion: 'openxiangda.native-role-session/v2',
|
|
93
|
+
id: roleSessionId,
|
|
94
|
+
environmentId: 'preproduction-id',
|
|
95
|
+
environmentKey: 'preproduction',
|
|
96
|
+
loginSessionId: 'login-session-1',
|
|
97
|
+
userId: 'acceptance-user',
|
|
98
|
+
selectionKey: subjectKey,
|
|
99
|
+
subjectKind: 'membership',
|
|
100
|
+
activeRoleCode: subject.role.code,
|
|
101
|
+
activeRoleSource: 'package',
|
|
102
|
+
activeRoleMembershipId: subjectKey.slice('membership:'.length),
|
|
103
|
+
activeAuthzRevisionId: 'authz-1',
|
|
104
|
+
activeAuthzVersion: 1,
|
|
105
|
+
activeAppVersionId: 'app-version-1',
|
|
106
|
+
activeEnvironmentHeadRevision: 1,
|
|
107
|
+
activeMembershipRevision: 1,
|
|
108
|
+
status: 'active',
|
|
109
|
+
issuedAt: '2026-08-23T00:00:00.000Z',
|
|
110
|
+
expiresAt: '2099-08-23T00:00:00.000Z',
|
|
111
|
+
lastUsedAt: '2026-08-23T00:00:00.000Z',
|
|
112
|
+
},
|
|
113
|
+
principal: {
|
|
114
|
+
schemaVersion: 'openxiangda.native-principal/v2',
|
|
115
|
+
tenantId: 'tenant-1',
|
|
116
|
+
appCode,
|
|
117
|
+
environmentId: 'preproduction-id',
|
|
118
|
+
environmentKey: 'preproduction',
|
|
119
|
+
activeAppVersionId: 'app-version-1',
|
|
120
|
+
environmentHeadRevision: 1,
|
|
121
|
+
principalType: 'user',
|
|
122
|
+
subjectId: 'acceptance-user',
|
|
123
|
+
userId: 'acceptance-user',
|
|
124
|
+
loginSessionId: 'login-session-1',
|
|
125
|
+
roleSessionId,
|
|
126
|
+
subjectKind: 'membership',
|
|
127
|
+
activeRoleCode: subject.role.code,
|
|
128
|
+
activeRoleSource: 'package',
|
|
129
|
+
activeRoleMembershipId: subjectKey.slice('membership:'.length),
|
|
130
|
+
activeMembershipRevision: 1,
|
|
131
|
+
authzRevisionId: 'authz-1',
|
|
132
|
+
authzVersion: 1,
|
|
133
|
+
scopeDataVersion: 'scope-1',
|
|
134
|
+
isAppSuperAdmin: false,
|
|
135
|
+
capabilities: authorized ? allCapabilities : [],
|
|
136
|
+
expiresAt: null,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
24
141
|
async function mockPlatform(
|
|
25
142
|
page: Page,
|
|
26
143
|
authorized = true,
|
|
27
144
|
targetRuntimeBase = runtimeBase,
|
|
28
145
|
) {
|
|
146
|
+
let activeSubjectKey = roleSubjects[0].subjectKey;
|
|
147
|
+
let contextReads = 0;
|
|
148
|
+
const observed: Array<{ path: string; roleSessionId: string }> = [];
|
|
149
|
+
const switches: Array<Record<string, unknown>> = [];
|
|
29
150
|
await page.route(`**${targetRuntimeBase}/**`, async route => {
|
|
30
151
|
if (route.request().resourceType() !== 'document') return route.continue();
|
|
31
152
|
const response = await route.fetch();
|
|
@@ -39,36 +160,69 @@ async function mockPlatform(
|
|
|
39
160
|
});
|
|
40
161
|
await page.route('**/service/**', async route => {
|
|
41
162
|
const url = new URL(route.request().url());
|
|
42
|
-
|
|
163
|
+
const request = route.request();
|
|
164
|
+
const roleSessionId = request.headers()['x-openxiangda-role-session-id'] || '';
|
|
165
|
+
if (url.pathname.endsWith('/native/authz/role-session/context/role-subjects')) {
|
|
166
|
+
return route.fulfill({
|
|
167
|
+
contentType: 'application/json',
|
|
168
|
+
body: JSON.stringify({
|
|
169
|
+
code: 200,
|
|
170
|
+
data: {
|
|
171
|
+
schemaVersion: 'openxiangda.role-subject-page/v2',
|
|
172
|
+
items: roleSubjects,
|
|
173
|
+
total: roleSubjects.length,
|
|
174
|
+
nextCursor: null,
|
|
175
|
+
roleSubjectSetVersion: 'subjects-1',
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (url.pathname.endsWith('/native/authz/role-session/context/switch')) {
|
|
181
|
+
const body = request.postDataJSON() as Record<string, unknown>;
|
|
182
|
+
switches.push(body);
|
|
183
|
+
if (roleSubjects.some(item => item.subjectKey === body.targetRoleSubjectKey)) {
|
|
184
|
+
activeSubjectKey = String(body.targetRoleSubjectKey) as typeof activeSubjectKey;
|
|
185
|
+
}
|
|
186
|
+
return route.fulfill({
|
|
187
|
+
contentType: 'application/json',
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
code: 200,
|
|
190
|
+
data: roleContext(activeSubjectKey, authorized),
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (url.pathname.endsWith('/native/authz/role-session/context')) {
|
|
195
|
+
contextReads += 1;
|
|
196
|
+
return route.fulfill({
|
|
197
|
+
contentType: 'application/json',
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
code: 200,
|
|
200
|
+
data: roleContext(activeSubjectKey, authorized),
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const sourceQuery = url.pathname.match(
|
|
205
|
+
/\/native\/data-resources\/([^/]+)\/fields\/([^/]+)\/source\/query$/
|
|
206
|
+
);
|
|
207
|
+
if (sourceQuery) {
|
|
208
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
43
209
|
return route.fulfill({
|
|
44
210
|
contentType: 'application/json',
|
|
45
211
|
body: JSON.stringify({
|
|
46
212
|
code: 200,
|
|
47
213
|
data: {
|
|
48
|
-
schemaVersion: 'openxiangda.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
activeAppVersionId: 'app-version-1',
|
|
54
|
-
headRevision: 1,
|
|
55
|
-
authzRevisionId: 'authz-1',
|
|
56
|
-
authzVersion: 1,
|
|
57
|
-
scopeDataVersion: 'scope-1',
|
|
58
|
-
},
|
|
59
|
-
principal: {
|
|
60
|
-
type: 'user',
|
|
61
|
-
userId: 'acceptance-user',
|
|
62
|
-
roleCodes: [],
|
|
63
|
-
capabilityCodes: authorized ? allCapabilities : [],
|
|
64
|
-
isAppSuperAdmin: authorized,
|
|
65
|
-
},
|
|
214
|
+
schemaVersion: 'openxiangda.data-field-source-page/v2',
|
|
215
|
+
resourceCode: decodeURIComponent(sourceQuery[1]),
|
|
216
|
+
fieldCode: decodeURIComponent(sourceQuery[2]),
|
|
217
|
+
items: [],
|
|
218
|
+
nextCursor: null,
|
|
66
219
|
},
|
|
67
220
|
}),
|
|
68
221
|
});
|
|
69
222
|
}
|
|
70
223
|
const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
|
|
71
224
|
if (query) {
|
|
225
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
72
226
|
return route.fulfill({
|
|
73
227
|
contentType: 'application/json',
|
|
74
228
|
body: JSON.stringify({
|
|
@@ -86,18 +240,33 @@ async function mockPlatform(
|
|
|
86
240
|
}),
|
|
87
241
|
});
|
|
88
242
|
}
|
|
89
|
-
if (url.pathname.includes('/
|
|
243
|
+
if (url.pathname.includes('/directory/')) {
|
|
244
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
90
245
|
return route.fulfill({
|
|
91
246
|
contentType: 'application/json',
|
|
92
247
|
body: JSON.stringify({ code: 200, data: { items: [], nextCursor: null } }),
|
|
93
248
|
});
|
|
94
249
|
}
|
|
250
|
+
if (url.pathname.includes('/openxiangda-app-api/v2/')) {
|
|
251
|
+
observed.push({ path: url.pathname, roleSessionId });
|
|
252
|
+
return route.fulfill({
|
|
253
|
+
contentType: 'application/json',
|
|
254
|
+
body: JSON.stringify({ code: 200, data: { status: 'ok' } }),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
95
257
|
return route.fulfill({
|
|
96
258
|
status: 404,
|
|
97
259
|
contentType: 'application/json',
|
|
98
260
|
body: JSON.stringify({ code: 404, message: 'not mocked' }),
|
|
99
261
|
});
|
|
100
262
|
});
|
|
263
|
+
return {
|
|
264
|
+
observed,
|
|
265
|
+
switches,
|
|
266
|
+
get contextReads() {
|
|
267
|
+
return contextReads;
|
|
268
|
+
},
|
|
269
|
+
};
|
|
101
270
|
}
|
|
102
271
|
|
|
103
272
|
test('renders the compiled desktop resource contract and shared workbench', async ({ page }) => {
|
|
@@ -145,6 +314,75 @@ test('opens the stable admin entry under the preproduction gateway mount', async
|
|
|
145
314
|
await expect(page).toHaveURL(`${devRuntimeBase}/${codes[0]}`);
|
|
146
315
|
});
|
|
147
316
|
|
|
317
|
+
test('binds Data, Directory, App API, and role switching to one active RoleSession', async ({ page }) => {
|
|
318
|
+
const platform = await mockPlatform(page);
|
|
319
|
+
await page.goto(`${runtimeBase}/admin`);
|
|
320
|
+
if (codes.length) {
|
|
321
|
+
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
322
|
+
} else {
|
|
323
|
+
await expect(page.getByText('还没有声明数据资源')).toBeVisible();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
await page.evaluate(async () => {
|
|
327
|
+
const client = await import('/src/platform-client.ts');
|
|
328
|
+
await client.searchResource('acceptance-resource', 'name', {
|
|
329
|
+
keyword: '显微镜',
|
|
330
|
+
});
|
|
331
|
+
await client.searchDirectory('user', { keyword: '老师' });
|
|
332
|
+
await client.requestApplicationApi('/health/current-user');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const initialSession = 'role-session-college_admin';
|
|
336
|
+
expect(
|
|
337
|
+
platform.observed.filter(item =>
|
|
338
|
+
/\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
|
|
339
|
+
item.path
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
).toEqual(
|
|
343
|
+
expect.arrayContaining([
|
|
344
|
+
expect.objectContaining({ roleSessionId: initialSession }),
|
|
345
|
+
expect.objectContaining({ roleSessionId: initialSession }),
|
|
346
|
+
expect.objectContaining({ roleSessionId: initialSession }),
|
|
347
|
+
])
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
await page.locator('.oxa-current-user').click();
|
|
351
|
+
await page.getByText('切换应用角色', { exact: true }).click();
|
|
352
|
+
await page.getByRole('combobox', { name: '选择应用角色' }).click();
|
|
353
|
+
await page.getByText('仪器管理员', { exact: true }).click();
|
|
354
|
+
await page.getByRole('button', { name: '切换角色' }).click();
|
|
355
|
+
|
|
356
|
+
await expect.poll(() => platform.switches.length).toBe(1);
|
|
357
|
+
expect(platform.switches[0]).toMatchObject({
|
|
358
|
+
targetRoleSubjectKey: 'membership:instrument-admin',
|
|
359
|
+
expectedRoleSessionId: initialSession,
|
|
360
|
+
environmentKey: 'preproduction',
|
|
361
|
+
});
|
|
362
|
+
await expect.poll(() => platform.contextReads).toBeGreaterThan(1);
|
|
363
|
+
await page.locator('.oxa-current-user').click();
|
|
364
|
+
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
|
|
365
|
+
'仪器管理员'
|
|
366
|
+
);
|
|
367
|
+
await page.keyboard.press('Escape');
|
|
368
|
+
await page.evaluate(async () => {
|
|
369
|
+
const client = await import('/src/platform-client.ts');
|
|
370
|
+
await client.searchResource('acceptance-resource', 'name', {
|
|
371
|
+
keyword: '光谱仪',
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
await expect
|
|
375
|
+
.poll(
|
|
376
|
+
() =>
|
|
377
|
+
platform.observed.filter(
|
|
378
|
+
item =>
|
|
379
|
+
item.path.includes('/native/data-resources/') &&
|
|
380
|
+
item.roleSessionId === 'role-session-instrument_admin'
|
|
381
|
+
).length
|
|
382
|
+
)
|
|
383
|
+
.toBeGreaterThan(0);
|
|
384
|
+
});
|
|
385
|
+
|
|
148
386
|
test('renders platform mobile controls from declared field widgets', async ({ page }) => {
|
|
149
387
|
const candidate = codes
|
|
150
388
|
.map(code => ({ code, definition: definitions[code] }))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Field protocol acceptance</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/e2e/field-protocol-fixture.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -16,7 +16,9 @@ export default defineConfig({
|
|
|
16
16
|
env: {
|
|
17
17
|
OPENXIANGDA_WEB_PORT: String(e2ePort),
|
|
18
18
|
OPENXIANGDA_ENVIRONMENT_KEY: 'production',
|
|
19
|
-
OPENXIANGDA_DEV_PROXY:
|
|
19
|
+
OPENXIANGDA_DEV_PROXY:
|
|
20
|
+
process.env.OPENXIANGDA_E2E_PLATFORM_URL ||
|
|
21
|
+
'http://127.0.0.1:7001',
|
|
20
22
|
},
|
|
21
23
|
},
|
|
22
24
|
});
|
|
@@ -14,10 +14,11 @@ visit(join(root, 'src'));
|
|
|
14
14
|
const lines = files.reduce((total, file) => total + readFileSync(file, 'utf8').split(/\r?\n/).length, 0);
|
|
15
15
|
// The shared Surface renderer is deliberately counted as application source;
|
|
16
16
|
// keep its small budget explicit instead of hiding it in generated output.
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
if (
|
|
17
|
+
// Field Kit families are separate modules so a semantic field does not add a
|
|
18
|
+
// branch to one universal renderer. Keep explicit ceilings while allowing the
|
|
19
|
+
// complete 30-field standard kit to remain decomposed.
|
|
20
|
+
if (files.length > 36) throw new Error(`WEB_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>36`);
|
|
21
|
+
if (lines > 10_000) throw new Error(`WEB_BUSINESS_LOC_BUDGET_EXCEEDED:${lines}>10000`);
|
|
21
22
|
const source = files.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
22
23
|
for (const marker of [
|
|
23
24
|
'@umijs/',
|
|
@@ -26,7 +27,9 @@ for (const marker of [
|
|
|
26
27
|
'instrument_query',
|
|
27
28
|
'instrument_save',
|
|
28
29
|
'function.invoke',
|
|
29
|
-
'
|
|
30
|
+
'authorization' + '-mode',
|
|
31
|
+
'focused' + '_role',
|
|
32
|
+
'/native/current' + '-user',
|
|
30
33
|
'college-' + 'am',
|
|
31
34
|
'user-' + 'wang',
|
|
32
35
|
'dept-' + 'am-test',
|
|
@@ -13,7 +13,7 @@ const gzip = javascript.reduce(
|
|
|
13
13
|
(bytes, file) => bytes + gzipSync(readFileSync(file), { level: 9 }).byteLength,
|
|
14
14
|
0
|
|
15
15
|
);
|
|
16
|
-
if (gzip >
|
|
16
|
+
if (gzip > 800_000) throw new Error(`WEB_JS_GZIP_BUDGET_EXCEEDED:${gzip}>800000`);
|
|
17
17
|
if (names.some(name => name.endsWith('.map'))) throw new Error('WEB_SOURCE_MAP_FORBIDDEN');
|
|
18
18
|
const html = readFileSync(join(dist, 'index.html'), 'utf8');
|
|
19
19
|
if (!html.includes('<div id="root"></div>')) throw new Error('WEB_ROOT_MISSING');
|