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.
- 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/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,387 +0,0 @@
|
|
|
1
|
-
import { App, Button, ConfigProvider, Form, Typography } from 'antd';
|
|
2
|
-
import type {
|
|
3
|
-
DataAuditEntry,
|
|
4
|
-
DataFieldSurface,
|
|
5
|
-
DataFileRef,
|
|
6
|
-
} from 'openxiangda/core';
|
|
7
|
-
import React, { useState } from 'react';
|
|
8
|
-
import ReactDOM from 'react-dom/client';
|
|
9
|
-
import {
|
|
10
|
-
auditFieldChange,
|
|
11
|
-
fieldValueForData,
|
|
12
|
-
fieldValueForForm,
|
|
13
|
-
MobileSurfaceFieldControl,
|
|
14
|
-
SurfaceAuditFieldValue,
|
|
15
|
-
SurfaceFieldControl,
|
|
16
|
-
SurfaceFieldValue,
|
|
17
|
-
type SurfaceField,
|
|
18
|
-
type SurfaceFieldRenderers,
|
|
19
|
-
} from 'openxiangda/field-kit';
|
|
20
|
-
import { Button as MobileButton, Input as MobileInput, MobileSurface, Popup } from 'openxiangda/mobile';
|
|
21
|
-
|
|
22
|
-
if (new URLSearchParams(location.search).get('mode') === 'mobile-surface') {
|
|
23
|
-
await import('openxiangda/mobile/styles.css');
|
|
24
|
-
} else {
|
|
25
|
-
await import('openxiangda/react/styles.css');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const options = [
|
|
29
|
-
{ label: '启用', value: 'enabled' },
|
|
30
|
-
{ label: '停用', value: 'disabled' },
|
|
31
|
-
];
|
|
32
|
-
const source = {
|
|
33
|
-
kind: 'resource' as const,
|
|
34
|
-
resourceCode: 'field-lookups',
|
|
35
|
-
labelField: 'name',
|
|
36
|
-
searchFields: ['name'],
|
|
37
|
-
snapshotFields: ['code'],
|
|
38
|
-
};
|
|
39
|
-
const field = (
|
|
40
|
-
key: string,
|
|
41
|
-
type: string,
|
|
42
|
-
widget: string,
|
|
43
|
-
extra: Record<string, unknown> = {}
|
|
44
|
-
) =>
|
|
45
|
-
({
|
|
46
|
-
key,
|
|
47
|
-
label: key,
|
|
48
|
-
type,
|
|
49
|
-
widget,
|
|
50
|
-
readCapabilities: [],
|
|
51
|
-
createCapabilities: [],
|
|
52
|
-
updateCapabilities: [],
|
|
53
|
-
...(type === 'date-range' || type === 'datetime-range'
|
|
54
|
-
? { rangeBoundary: 'closed' as const }
|
|
55
|
-
: {}),
|
|
56
|
-
...extra,
|
|
57
|
-
} as unknown as SurfaceField);
|
|
58
|
-
|
|
59
|
-
const fields = [
|
|
60
|
-
field('单行文本', 'text.short', 'text', { maxLength: 120 }),
|
|
61
|
-
field('邮箱', 'text.short', 'email'),
|
|
62
|
-
field('电话', 'text.short', 'phone'),
|
|
63
|
-
field('多行文本', 'text.long', 'textarea'),
|
|
64
|
-
field('富文本', 'text.rich', 'rich-text'),
|
|
65
|
-
field('整数', 'number.integer', 'number', { scale: 0 }),
|
|
66
|
-
field('小数', 'number.decimal', 'number', { scale: 2 }),
|
|
67
|
-
field('金额', 'number.decimal', 'money', { scale: 2 }),
|
|
68
|
-
field('百分比', 'number.decimal', 'percent', { scale: 2 }),
|
|
69
|
-
field('布尔', 'boolean', 'switch'),
|
|
70
|
-
field('日期', 'date', 'date'),
|
|
71
|
-
field('时间', 'time', 'time', { timePrecision: 'second' }),
|
|
72
|
-
field('日期时间', 'datetime', 'datetime'),
|
|
73
|
-
field('日期范围', 'date-range', 'date-range'),
|
|
74
|
-
field('日期时间范围', 'datetime-range', 'datetime-range'),
|
|
75
|
-
field('静态单选下拉', 'option.single', 'select', { options }),
|
|
76
|
-
field('静态多选下拉', 'option.multiple', 'multi-select', { options }),
|
|
77
|
-
field('单选按钮', 'option.single', 'radio', { options }),
|
|
78
|
-
field('复选框', 'option.multiple', 'checkbox', { options }),
|
|
79
|
-
field('动态单选下拉', 'resource-ref.single', 'select', { source }),
|
|
80
|
-
field('动态多选下拉', 'resource-ref.multiple', 'multi-select', { source }),
|
|
81
|
-
field('级联单选', 'cascade.single', 'cascade', {
|
|
82
|
-
options: [
|
|
83
|
-
{
|
|
84
|
-
label: '设备',
|
|
85
|
-
value: 'equipment',
|
|
86
|
-
children: [{ label: '显微镜', value: 'microscope' }, { label: '光谱仪', value: 'spectrometer' }],
|
|
87
|
-
},
|
|
88
|
-
{ label: '服务', value: 'service', children: [{ label: '培训', value: 'training' }] },
|
|
89
|
-
],
|
|
90
|
-
}),
|
|
91
|
-
field('级联多选', 'cascade.multiple', 'cascade', {
|
|
92
|
-
options: [
|
|
93
|
-
{
|
|
94
|
-
label: '设备',
|
|
95
|
-
value: 'equipment',
|
|
96
|
-
children: [{ label: '显微镜', value: 'microscope' }, { label: '光谱仪', value: 'spectrometer' }],
|
|
97
|
-
},
|
|
98
|
-
{ label: '服务', value: 'service', children: [{ label: '培训', value: 'training' }] },
|
|
99
|
-
],
|
|
100
|
-
}),
|
|
101
|
-
field('成员单选', 'user.single', 'directory-user'),
|
|
102
|
-
field('成员多选', 'user.multiple', 'directory-user'),
|
|
103
|
-
field('部门单选', 'department.single', 'directory-department'),
|
|
104
|
-
field('部门多选', 'department.multiple', 'directory-department'),
|
|
105
|
-
field('资源单选', 'resource-ref.single', 'resource', { source }),
|
|
106
|
-
field('资源多选', 'resource-ref.multiple', 'resource', { source }),
|
|
107
|
-
field('附件', 'file', 'attachment', {
|
|
108
|
-
accept: ['.pdf', 'text/plain'],
|
|
109
|
-
maxCount: 3,
|
|
110
|
-
maxSizeMb: 20,
|
|
111
|
-
}),
|
|
112
|
-
field('图片', 'image', 'image', {
|
|
113
|
-
accept: ['image/*'],
|
|
114
|
-
maxCount: 3,
|
|
115
|
-
maxSizeMb: 20,
|
|
116
|
-
}),
|
|
117
|
-
field('业务签名', 'signature', 'signature'),
|
|
118
|
-
field('地址', 'address', 'address'),
|
|
119
|
-
field('精确定位', 'location', 'location'),
|
|
120
|
-
field('UUID', 'uuid', 'readonly', { requiredHint: true }),
|
|
121
|
-
field('JSON', 'json', 'json'),
|
|
122
|
-
field('流水号', 'serial-number', 'readonly'),
|
|
123
|
-
field('子表', 'subtable', 'subtable'),
|
|
124
|
-
] satisfies Array<DataFieldSurface & { key: string }>;
|
|
125
|
-
|
|
126
|
-
const user = { label: '张三', value: 'user-1', employeeNo: 'E1001' };
|
|
127
|
-
const department = {
|
|
128
|
-
label: '理学院',
|
|
129
|
-
value: 'college-a',
|
|
130
|
-
fullPath: '学校 / 理学院',
|
|
131
|
-
};
|
|
132
|
-
const reference = {
|
|
133
|
-
label: '显微镜',
|
|
134
|
-
value: 'resource-1',
|
|
135
|
-
resourceCode: 'field-lookups',
|
|
136
|
-
snapshot: { code: 'M-001' },
|
|
137
|
-
};
|
|
138
|
-
const fileRef: DataFileRef = {
|
|
139
|
-
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
140
|
-
id: '11111111-1111-4111-8111-111111111111',
|
|
141
|
-
name: '验收附件.pdf',
|
|
142
|
-
size: 1280,
|
|
143
|
-
contentType: 'application/pdf',
|
|
144
|
-
};
|
|
145
|
-
const imageRef: DataFileRef = {
|
|
146
|
-
...fileRef,
|
|
147
|
-
id: '22222222-2222-4222-8222-222222222222',
|
|
148
|
-
name: '验收图片.png',
|
|
149
|
-
contentType: 'image/png',
|
|
150
|
-
width: 640,
|
|
151
|
-
height: 480,
|
|
152
|
-
previewUrl: '/service/openxiangda-api/v2/applications/field-protocol-fixture/native/data/field-records/files/22222222-2222-4222-8222-222222222222/content?disposition=inline',
|
|
153
|
-
thumbnailUrl: '/service/openxiangda-api/v2/applications/field-protocol-fixture/native/data/field-records/files/22222222-2222-4222-8222-222222222222/content?variant=thumbnail&disposition=inline',
|
|
154
|
-
};
|
|
155
|
-
const values: Record<string, unknown> = {
|
|
156
|
-
单行文本: '仪器验收记录',
|
|
157
|
-
邮箱: 'user@example.com',
|
|
158
|
-
电话: '13800000000',
|
|
159
|
-
多行文本: '第一行\n第二行',
|
|
160
|
-
富文本: `<p>已保存的<strong>富文本</strong></p><img src="${imageRef.previewUrl}" alt="富文本图片">`,
|
|
161
|
-
整数: -7,
|
|
162
|
-
小数: 12.34,
|
|
163
|
-
金额: 5600.5,
|
|
164
|
-
百分比: 98.5,
|
|
165
|
-
布尔: true,
|
|
166
|
-
日期: '2026-08-24',
|
|
167
|
-
时间: '10:20:30',
|
|
168
|
-
日期时间: '2026-08-24T02:20:30.000Z',
|
|
169
|
-
日期范围: { start: '2026-08-01', end: '2026-08-31' },
|
|
170
|
-
日期时间范围: {
|
|
171
|
-
start: '2026-08-01T00:00:00.000Z',
|
|
172
|
-
end: '2026-08-31T23:59:59.000Z',
|
|
173
|
-
},
|
|
174
|
-
静态单选下拉: options[0],
|
|
175
|
-
静态多选下拉: options,
|
|
176
|
-
单选按钮: options[0],
|
|
177
|
-
复选框: options,
|
|
178
|
-
动态单选下拉: reference,
|
|
179
|
-
动态多选下拉: [reference],
|
|
180
|
-
级联单选: [
|
|
181
|
-
{ label: '设备', value: 'equipment' },
|
|
182
|
-
{ label: '显微镜', value: 'microscope' },
|
|
183
|
-
],
|
|
184
|
-
级联多选: [
|
|
185
|
-
[
|
|
186
|
-
{ label: '设备', value: 'equipment' },
|
|
187
|
-
{ label: '显微镜', value: 'microscope' },
|
|
188
|
-
],
|
|
189
|
-
],
|
|
190
|
-
成员单选: user,
|
|
191
|
-
成员多选: [user],
|
|
192
|
-
部门单选: department,
|
|
193
|
-
部门多选: [department],
|
|
194
|
-
资源单选: reference,
|
|
195
|
-
资源多选: [reference],
|
|
196
|
-
附件: [fileRef],
|
|
197
|
-
图片: [imageRef],
|
|
198
|
-
业务签名: {
|
|
199
|
-
file: {
|
|
200
|
-
...fileRef,
|
|
201
|
-
id: '33333333-3333-4333-8333-333333333333',
|
|
202
|
-
name: 'signature.png',
|
|
203
|
-
contentType: 'image/png',
|
|
204
|
-
},
|
|
205
|
-
signer: user,
|
|
206
|
-
signedAt: '2026-08-24T02:20:30.000Z',
|
|
207
|
-
hash: 'a'.repeat(64),
|
|
208
|
-
points: [{ x: 1, y: 1, t: 1 }],
|
|
209
|
-
},
|
|
210
|
-
地址: {
|
|
211
|
-
country: { label: '中国', value: 'CN' },
|
|
212
|
-
province: { label: '浙江省', value: '330000' },
|
|
213
|
-
city: { label: '杭州市', value: '330100' },
|
|
214
|
-
district: { label: '西湖区', value: '330106' },
|
|
215
|
-
detail: '文一路 1 号',
|
|
216
|
-
fullAddress: '浙江省杭州市西湖区文一路 1 号',
|
|
217
|
-
},
|
|
218
|
-
精确定位: {
|
|
219
|
-
source: 'browser',
|
|
220
|
-
longitude: 120.123456,
|
|
221
|
-
latitude: 30.234567,
|
|
222
|
-
accuracy: 5,
|
|
223
|
-
capturedAt: '2026-08-24T02:20:30.000Z',
|
|
224
|
-
},
|
|
225
|
-
JSON: { status: 'ok', nested: { count: 1 } },
|
|
226
|
-
流水号: 'FP-001000',
|
|
227
|
-
子表: [{ item_name: '第一行' }, { item_name: '第二行' }],
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
const auditEntry: DataAuditEntry = {
|
|
231
|
-
id: 'field-protocol-event',
|
|
232
|
-
operation: 'created',
|
|
233
|
-
recordId: 'field-protocol-record',
|
|
234
|
-
revision: 1,
|
|
235
|
-
actor: { principalType: 'user', subjectId: 'user-1' },
|
|
236
|
-
correlation: {
|
|
237
|
-
eventId: 'field-protocol-event',
|
|
238
|
-
requestId: null,
|
|
239
|
-
traceId: null,
|
|
240
|
-
environmentKey: 'preproduction',
|
|
241
|
-
appVersionId: 'field-protocol-version',
|
|
242
|
-
environmentHeadRevision: 1,
|
|
243
|
-
capturePlanRevision: 1,
|
|
244
|
-
cause: { eventId: null, subscriptionCode: null, depth: 0 },
|
|
245
|
-
},
|
|
246
|
-
changes: Object.fromEntries(
|
|
247
|
-
fields.map(item => [item.key, { after: values[item.key] ?? null }])
|
|
248
|
-
),
|
|
249
|
-
projection: {},
|
|
250
|
-
occurredAt: '2026-08-25T00:00:00.000Z',
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const renderers: SurfaceFieldRenderers = {
|
|
254
|
-
signer: user,
|
|
255
|
-
upload: async (_field, file) => {
|
|
256
|
-
if (new URLSearchParams(location.search).has('controlledUploads')) {
|
|
257
|
-
const response = await fetch('/__field-upload-fixture', { method: 'POST', body: file, headers: { 'x-file-name': encodeURIComponent(file.name) } });
|
|
258
|
-
if (!response.ok) throw new Error(await response.text());
|
|
259
|
-
}
|
|
260
|
-
return ({
|
|
261
|
-
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
262
|
-
id: crypto.randomUUID(),
|
|
263
|
-
name: file.name,
|
|
264
|
-
size: file.size,
|
|
265
|
-
contentType: file.type,
|
|
266
|
-
});
|
|
267
|
-
},
|
|
268
|
-
renderSubtable: () => (
|
|
269
|
-
<div className="field-protocol-subtable">2 行子表 · 新增 · 删除 · 排序</div>
|
|
270
|
-
),
|
|
271
|
-
renderValue: ({ field: item, value }) =>
|
|
272
|
-
item.type === 'subtable'
|
|
273
|
-
? `${Array.isArray(value) ? value.length : 0} 行子表`
|
|
274
|
-
: undefined,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
function MobileFacadeAcceptance() {
|
|
278
|
-
const [open, setOpen] = useState(false);
|
|
279
|
-
const [value, setValue] = useState('');
|
|
280
|
-
return <MobileSurface>
|
|
281
|
-
<MobileInput aria-label="平台移动输入" value={value} onChange={setValue} />
|
|
282
|
-
<MobileButton onClick={() => setOpen(true)}>打开移动弹层</MobileButton>
|
|
283
|
-
<Popup visible={open} onMaskClick={() => setOpen(false)}>
|
|
284
|
-
<div data-mobile-popup>
|
|
285
|
-
<p>已输入:{value}</p>
|
|
286
|
-
<MobileButton onClick={() => setOpen(false)}>关闭弹层</MobileButton>
|
|
287
|
-
</div>
|
|
288
|
-
</Popup>
|
|
289
|
-
</MobileSurface>;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function AcceptancePage() {
|
|
293
|
-
const mobile = new URLSearchParams(location.search).get('mode') === 'mobile';
|
|
294
|
-
const Control = mobile ? MobileSurfaceFieldControl : SurfaceFieldControl;
|
|
295
|
-
const [form] = Form.useForm();
|
|
296
|
-
const [saved, setSaved] = useState<Record<string, unknown>>();
|
|
297
|
-
return (
|
|
298
|
-
<ConfigProvider><App>
|
|
299
|
-
<main className={`field-protocol-page ${mobile ? 'is-mobile' : 'is-desktop'}`}>
|
|
300
|
-
<header>
|
|
301
|
-
<Typography.Title level={mobile ? 4 : 2}>
|
|
302
|
-
Field Kit 协议验收
|
|
303
|
-
</Typography.Title>
|
|
304
|
-
<Typography.Text type="secondary">
|
|
305
|
-
{mobile ? '移动编辑态' : '桌面编辑态与已存快照只读态'}
|
|
306
|
-
</Typography.Text>
|
|
307
|
-
</header>
|
|
308
|
-
<section aria-label={mobile ? '移动编辑字段' : '桌面编辑字段'} className="field-protocol-edit">
|
|
309
|
-
<Form form={form} initialValues={values} layout="vertical" onFinish={current => {
|
|
310
|
-
setSaved(Object.fromEntries(fields.map(item => [item.key, fieldValueForData(item, current[item.key])])));
|
|
311
|
-
}}>
|
|
312
|
-
<div className="field-protocol-grid">
|
|
313
|
-
{fields.map(item => (
|
|
314
|
-
<div data-field-code={item.key} key={item.key}>
|
|
315
|
-
<Control
|
|
316
|
-
disabled={item.widget === 'readonly'}
|
|
317
|
-
field={item}
|
|
318
|
-
operation="create"
|
|
319
|
-
renderers={renderers}
|
|
320
|
-
resourceCode="field-records"
|
|
321
|
-
/>
|
|
322
|
-
</div>
|
|
323
|
-
))}
|
|
324
|
-
</div>
|
|
325
|
-
<Button htmlType="submit">验证表单</Button>
|
|
326
|
-
{saved && <Button onClick={() => form.setFieldsValue(Object.fromEntries(fields.map(item => [item.key, fieldValueForForm(item, saved[item.key])])))}>重新载入已保存值</Button>}
|
|
327
|
-
<output data-saved-values>{saved ? JSON.stringify(saved) : ''}</output>
|
|
328
|
-
</Form>
|
|
329
|
-
</section>
|
|
330
|
-
{!mobile && (
|
|
331
|
-
<section aria-label="已存快照只读字段" className="field-protocol-readonly">
|
|
332
|
-
<Typography.Title level={3}>已存快照</Typography.Title>
|
|
333
|
-
<div className="field-protocol-read-grid">
|
|
334
|
-
{fields.map(item => (
|
|
335
|
-
<div data-read-field={item.key} key={item.key}>
|
|
336
|
-
<strong>{item.label}</strong>
|
|
337
|
-
<SurfaceFieldValue
|
|
338
|
-
field={item}
|
|
339
|
-
renderers={renderers}
|
|
340
|
-
resourceCode="field-records"
|
|
341
|
-
value={values[item.key]}
|
|
342
|
-
/>
|
|
343
|
-
</div>
|
|
344
|
-
))}
|
|
345
|
-
</div>
|
|
346
|
-
</section>
|
|
347
|
-
)}
|
|
348
|
-
{!mobile && (
|
|
349
|
-
<section aria-label="当前协议字段变更记录" className="field-protocol-readonly">
|
|
350
|
-
<Typography.Title level={3}>当前协议字段变更记录</Typography.Title>
|
|
351
|
-
<div className="field-protocol-read-grid">
|
|
352
|
-
{fields.map(item => (
|
|
353
|
-
<div data-audit-field={item.key} key={item.key}>
|
|
354
|
-
<strong>{item.label}</strong>
|
|
355
|
-
<SurfaceAuditFieldValue
|
|
356
|
-
field={item}
|
|
357
|
-
resourceCode="field-records"
|
|
358
|
-
value={auditFieldChange(auditEntry, item.key)?.after}
|
|
359
|
-
/>
|
|
360
|
-
</div>
|
|
361
|
-
))}
|
|
362
|
-
<div data-audit-digest="rich-text">
|
|
363
|
-
<strong>富文本摘要</strong>
|
|
364
|
-
<SurfaceAuditFieldValue
|
|
365
|
-
field={fields[4]}
|
|
366
|
-
value={{
|
|
367
|
-
kind: 'digest',
|
|
368
|
-
truncated: true,
|
|
369
|
-
valueType: 'text.rich',
|
|
370
|
-
bytes: 65_537,
|
|
371
|
-
sha256: 'a'.repeat(64),
|
|
372
|
-
}}
|
|
373
|
-
/>
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
</section>
|
|
377
|
-
)}
|
|
378
|
-
</main>
|
|
379
|
-
</App></ConfigProvider>
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
384
|
-
<React.StrictMode>
|
|
385
|
-
{new URLSearchParams(location.search).get('mode') === 'mobile-surface' ? <MobileFacadeAcceptance /> : <AcceptancePage />}
|
|
386
|
-
</React.StrictMode>
|
|
387
|
-
);
|
|
@@ -1,285 +0,0 @@
|
|
|
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
|
-
await page.route('**/files/*/content?*', async route => {
|
|
27
|
-
await route.fulfill({
|
|
28
|
-
body: Buffer.from(
|
|
29
|
-
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=',
|
|
30
|
-
'base64'
|
|
31
|
-
),
|
|
32
|
-
contentType: 'image/png',
|
|
33
|
-
status: 200,
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('renders and operates every desktop edit/read component', async ({ page }) => {
|
|
39
|
-
const pageErrors: string[] = [];
|
|
40
|
-
const addressRequests: string[] = [];
|
|
41
|
-
page.on('pageerror', error => pageErrors.push(error.message));
|
|
42
|
-
page.on('request', request => {
|
|
43
|
-
if (request.url().includes('/china-divisions/')) {
|
|
44
|
-
addressRequests.push(request.url());
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
await page.setViewportSize({ width: 1440, height: 1100 });
|
|
48
|
-
await page.goto('/field-protocol.e2e.html');
|
|
49
|
-
await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
|
|
50
|
-
await expect(page.locator('[data-read-field]')).toHaveCount(fieldCount);
|
|
51
|
-
await expect(page.locator('[data-audit-field]')).toHaveCount(fieldCount);
|
|
52
|
-
await expect(page.locator('[data-field-code="静态单选下拉"] .ant-select')).toBeVisible();
|
|
53
|
-
await expect(page.locator('[data-field-code="单选按钮"] .ant-radio-group')).toBeVisible();
|
|
54
|
-
await expect(page.locator('[data-field-code="复选框"] .ant-checkbox-group')).toBeVisible();
|
|
55
|
-
await expect(page.locator('[data-field-code="时间"] input')).toBeVisible();
|
|
56
|
-
await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
|
|
57
|
-
await expect(page.locator('[data-field-code="精确定位"]')).not.toContainText('手工');
|
|
58
|
-
await expect(page.locator('.ant-alert-error')).toHaveCount(0);
|
|
59
|
-
await expect.poll(() => addressRequests.length).toBeGreaterThan(0);
|
|
60
|
-
expect(addressRequests.every(url => new URL(url).pathname.startsWith('/service/china-divisions/'))).toBe(true);
|
|
61
|
-
|
|
62
|
-
await page.getByRole('button', { name: '验证表单' }).click();
|
|
63
|
-
await expect(page.locator('[data-field-code="UUID"] .ant-form-item-has-error')).toHaveCount(0);
|
|
64
|
-
|
|
65
|
-
await expect(page.locator('[data-read-field="图片"] img.oxa-file-thumbnail')).toHaveAttribute('src', /blob:/);
|
|
66
|
-
await expect(page.locator('[data-read-field="富文本"] img')).toHaveAttribute('src', /blob:/);
|
|
67
|
-
expect(await page.locator('[data-read-field="图片"] img.oxa-file-thumbnail').evaluate(
|
|
68
|
-
image => (image as HTMLImageElement).naturalWidth
|
|
69
|
-
)).toBeGreaterThan(0);
|
|
70
|
-
expect(await page.locator('[data-read-field="富文本"] img').evaluate(
|
|
71
|
-
image => (image as HTMLImageElement).naturalWidth
|
|
72
|
-
)).toBeGreaterThan(0);
|
|
73
|
-
await expect(page.locator('[aria-label="当前协议字段变更记录"]')).not.toContainText('[object Object]');
|
|
74
|
-
await expect(page.locator('[data-audit-digest="rich-text"]')).toContainText('大值摘要');
|
|
75
|
-
|
|
76
|
-
await page.locator('[data-field-code="精确定位"]').getByRole('button', { name: '浏览器定位' }).click();
|
|
77
|
-
await expect(page.locator('[data-field-code="精确定位"]')).toContainText('120.123456');
|
|
78
|
-
await expect(page.locator('[data-field-code="精确定位"]')).toContainText('30.234567');
|
|
79
|
-
|
|
80
|
-
await page
|
|
81
|
-
.locator('[data-field-code="业务签名"]')
|
|
82
|
-
.getByRole('button', { name: /(?:开始|重新)签名/ })
|
|
83
|
-
.click();
|
|
84
|
-
const canvas = page.locator('canvas[aria-label="手写签名画布"]');
|
|
85
|
-
await expect(canvas).toBeVisible();
|
|
86
|
-
const box = await canvas.boundingBox();
|
|
87
|
-
expect(box).not.toBeNull();
|
|
88
|
-
await canvas.dispatchEvent('pointerdown', {
|
|
89
|
-
pointerId: 1,
|
|
90
|
-
pointerType: 'pen',
|
|
91
|
-
buttons: 1,
|
|
92
|
-
clientX: box!.x + 30,
|
|
93
|
-
clientY: box!.y + 40,
|
|
94
|
-
});
|
|
95
|
-
for (let step = 1; step <= 8; step += 1) {
|
|
96
|
-
await canvas.dispatchEvent('pointermove', {
|
|
97
|
-
pointerId: 1,
|
|
98
|
-
pointerType: 'pen',
|
|
99
|
-
buttons: 1,
|
|
100
|
-
clientX: box!.x + 30 + (130 * step) / 8,
|
|
101
|
-
clientY: box!.y + 40 + (80 * step) / 8,
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
await canvas.dispatchEvent('pointerup', {
|
|
105
|
-
pointerId: 1,
|
|
106
|
-
pointerType: 'pen',
|
|
107
|
-
buttons: 0,
|
|
108
|
-
clientX: box!.x + 160,
|
|
109
|
-
clientY: box!.y + 120,
|
|
110
|
-
});
|
|
111
|
-
const saveSignature = page.getByRole('button', { name: '保存签名' });
|
|
112
|
-
await expect(saveSignature).toBeEnabled();
|
|
113
|
-
await saveSignature.click();
|
|
114
|
-
await expect(page.locator('[data-field-code="业务签名"]')).not.toContainText('尚未签名');
|
|
115
|
-
await expect(page.getByRole('dialog', { name: '手写签名' })).toBeHidden();
|
|
116
|
-
|
|
117
|
-
expect(pageErrors).toEqual([]);
|
|
118
|
-
|
|
119
|
-
expect(
|
|
120
|
-
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
|
121
|
-
).toBe(true);
|
|
122
|
-
await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-desktop.png' });
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('resource selectors query once when selection changes', async ({ page }) => {
|
|
126
|
-
const requestCounts = new Map<string, number>();
|
|
127
|
-
await page.route('**/source/query', async route => {
|
|
128
|
-
const match = new URL(route.request().url()).pathname.match(/\/fields\/([^/]+)\/source\/query$/);
|
|
129
|
-
const fieldCode = decodeURIComponent(match?.[1] || 'unknown');
|
|
130
|
-
requestCounts.set(fieldCode, (requestCounts.get(fieldCode) || 0) + 1);
|
|
131
|
-
const body = route.request().postDataJSON() as { operation?: 'create' | 'update' };
|
|
132
|
-
await route.fulfill({
|
|
133
|
-
contentType: 'application/json',
|
|
134
|
-
json: {
|
|
135
|
-
schemaVersion: 'openxiangda.data-field-source-page/v2',
|
|
136
|
-
environment: {
|
|
137
|
-
id: 'field-protocol-environment',
|
|
138
|
-
key: 'preproduction',
|
|
139
|
-
activeAppVersionId: 'field-protocol-version',
|
|
140
|
-
headRevision: 1,
|
|
141
|
-
authzRevisionId: 'field-protocol-authz',
|
|
142
|
-
authzVersion: 1,
|
|
143
|
-
scopeDataVersion: 'field-protocol-scope',
|
|
144
|
-
},
|
|
145
|
-
resourceCode: 'field-records',
|
|
146
|
-
fieldCode,
|
|
147
|
-
sourceResourceCode: 'field-lookups',
|
|
148
|
-
operation: body.operation || 'create',
|
|
149
|
-
items: [{
|
|
150
|
-
label: `候选-${fieldCode}`,
|
|
151
|
-
value: `candidate-${fieldCode}`,
|
|
152
|
-
resourceCode: 'field-lookups',
|
|
153
|
-
snapshot: { code: `code-${fieldCode}` },
|
|
154
|
-
}],
|
|
155
|
-
nextCursor: null,
|
|
156
|
-
},
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
await page.goto('/field-protocol.e2e.html');
|
|
161
|
-
await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
|
|
162
|
-
await page.waitForLoadState('networkidle');
|
|
163
|
-
for (const fieldCode of ['资源单选', '资源多选']) {
|
|
164
|
-
const combobox = page
|
|
165
|
-
.locator(`[data-field-code="${fieldCode}"]`)
|
|
166
|
-
.getByRole('combobox');
|
|
167
|
-
await expect(combobox).toBeVisible();
|
|
168
|
-
await combobox.click();
|
|
169
|
-
const dropdown = page.locator('.ant-select-dropdown:visible');
|
|
170
|
-
await expect(dropdown).toBeVisible();
|
|
171
|
-
await expect.poll(() => requestCounts.get(fieldCode) || 0).toBe(1);
|
|
172
|
-
await dropdown
|
|
173
|
-
.locator('.ant-select-item-option')
|
|
174
|
-
.filter({ hasText: `候选-${fieldCode}` })
|
|
175
|
-
.click();
|
|
176
|
-
await page.waitForTimeout(500);
|
|
177
|
-
expect(requestCounts.get(fieldCode)).toBe(1);
|
|
178
|
-
await page.keyboard.press('Escape');
|
|
179
|
-
await expect(dropdown).toBeHidden();
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('mobile selection, numeric validation and save/reload keep canonical values', async ({ page }) => {
|
|
184
|
-
const pageErrors: string[] = [];
|
|
185
|
-
page.on('pageerror', error => pageErrors.push(error.message));
|
|
186
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
187
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
188
|
-
const field = (label: string) => page.locator(`[data-field-code="${label}"]`);
|
|
189
|
-
await field('单行文本').getByRole('textbox').fill('移动录入验证');
|
|
190
|
-
await field('整数').getByRole('textbox').fill('1.5');
|
|
191
|
-
await page.getByRole('button', { name: '验证表单', exact: true }).click();
|
|
192
|
-
await expect(field('整数').getByRole('alert')).toContainText('整数');
|
|
193
|
-
await expect(page.locator('[data-saved-values]')).toBeEmpty();
|
|
194
|
-
await field('整数').getByRole('textbox').fill('12');
|
|
195
|
-
await field('小数').getByRole('textbox').fill('12.75');
|
|
196
|
-
await field('金额').getByRole('textbox').fill('');
|
|
197
|
-
const moneyInput = await field('金额').getByRole('textbox').boundingBox();
|
|
198
|
-
const currency = await field('金额').locator('[aria-hidden="true"]').first().boundingBox();
|
|
199
|
-
expect(moneyInput!.height).toBeGreaterThanOrEqual(24);
|
|
200
|
-
// Currency must stay beside the editable value, even with the full React stylesheet.
|
|
201
|
-
expect(currency!.y + currency!.height / 2).toBeGreaterThan(moneyInput!.y);
|
|
202
|
-
expect(currency!.y + currency!.height / 2).toBeLessThan(moneyInput!.y + moneyInput!.height);
|
|
203
|
-
|
|
204
|
-
const choice = field('静态单选下拉');
|
|
205
|
-
const oldChoice = await choice.getByRole('button', { name: '选择静态单选下拉' }).innerText();
|
|
206
|
-
await choice.getByRole('button', { name: '选择静态单选下拉' }).click();
|
|
207
|
-
await choice.getByText('停用', { exact: true }).last().click();
|
|
208
|
-
await choice.getByRole('button', { name: '取消', exact: true }).click();
|
|
209
|
-
await expect(choice.getByRole('button', { name: '选择静态单选下拉' })).toHaveText(oldChoice);
|
|
210
|
-
await choice.getByRole('button', { name: '选择静态单选下拉' }).click();
|
|
211
|
-
await choice.getByText('停用', { exact: true }).last().click();
|
|
212
|
-
await choice.getByRole('button', { name: '确定', exact: true }).click();
|
|
213
|
-
await expect(choice.getByRole('button', { name: '选择静态单选下拉' })).toHaveText('停用');
|
|
214
|
-
|
|
215
|
-
await field('日期').getByRole('button', { name: '选择日期', exact: true }).click();
|
|
216
|
-
await field('日期').getByText('确定', { exact: true }).click();
|
|
217
|
-
await field('时间').getByRole('button', { name: '选择时间', exact: true }).click();
|
|
218
|
-
await field('时间').getByText('确定', { exact: true }).click();
|
|
219
|
-
await page.getByRole('button', { name: '验证表单', exact: true }).click();
|
|
220
|
-
const result = page.locator('[data-saved-values]');
|
|
221
|
-
await expect(result).toContainText('移动录入验证');
|
|
222
|
-
const stored = JSON.parse(await result.innerText());
|
|
223
|
-
expect(stored['整数']).toBe(12);
|
|
224
|
-
expect(stored['小数']).toBe(12.75);
|
|
225
|
-
expect(stored['金额']).toBeNull();
|
|
226
|
-
expect(stored['静态单选下拉']).toEqual({ label: '停用', value: 'disabled' });
|
|
227
|
-
expect(stored['日期']).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
228
|
-
expect(stored['时间']).toMatch(/^\d{2}:\d{2}:\d{2}$/);
|
|
229
|
-
await field('单行文本').getByRole('textbox').fill('未保存修改');
|
|
230
|
-
await page.getByRole('button', { name: '重新载入已保存值' }).click();
|
|
231
|
-
await expect(field('单行文本').getByRole('textbox')).toHaveValue('移动录入验证');
|
|
232
|
-
await expect(field('小数').getByRole('textbox')).toHaveValue('12.75');
|
|
233
|
-
await field('日期').getByRole('button', { name: '清空日期', exact: true }).click();
|
|
234
|
-
await page.getByRole('button', { name: '验证表单', exact: true }).click();
|
|
235
|
-
await expect.poll(async () => JSON.parse(await result.innerText())['日期']).toBeNull();
|
|
236
|
-
expect(pageErrors).toEqual([]);
|
|
237
|
-
await page.evaluate(() => window.scrollTo(0, 0));
|
|
238
|
-
await page.screenshot({ path: 'test-results/foundation-mobile-controls.png' });
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test('renders every mobile control without horizontal overflow', async ({ page }) => {
|
|
242
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
243
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
244
|
-
await expect(page.locator('[data-field-code]')).toHaveCount(fieldCount);
|
|
245
|
-
await expect(page.locator('.oxa-mobile-field')).toHaveCount(fieldCount);
|
|
246
|
-
await expect(page.locator('[data-field-code="精确定位"] input')).toHaveCount(0);
|
|
247
|
-
await expect(page.locator('[data-field-code="附件"] input[type="file"]')).toBeHidden();
|
|
248
|
-
await expect(page.locator('.ant-alert-error')).toHaveCount(0);
|
|
249
|
-
expect(
|
|
250
|
-
await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)
|
|
251
|
-
).toBe(true);
|
|
252
|
-
await page.screenshot({ fullPage: true, path: 'test-results/field-protocol-mobile.png' });
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
test('mobile default components and popups do not reset the surrounding page', async ({ page }) => {
|
|
256
|
-
const pageErrors: string[] = [];
|
|
257
|
-
page.on('pageerror', error => pageErrors.push(error.message));
|
|
258
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
259
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile-surface');
|
|
260
|
-
const outsideStyles = () => page.evaluate(() => ({
|
|
261
|
-
background: getComputedStyle(document.documentElement).backgroundColor,
|
|
262
|
-
color: getComputedStyle(document.body).color,
|
|
263
|
-
fontSize: getComputedStyle(document.body).fontSize,
|
|
264
|
-
fontFamily: getComputedStyle(document.body).fontFamily,
|
|
265
|
-
link: getComputedStyle(document.getElementById('outside-mobile')!).color,
|
|
266
|
-
mobilePrimary: getComputedStyle(document.documentElement).getPropertyValue('--adm-color-primary'),
|
|
267
|
-
}));
|
|
268
|
-
const baseline = {
|
|
269
|
-
background: 'rgb(248, 249, 250)', color: 'rgb(31, 32, 33)',
|
|
270
|
-
fontSize: '17px', fontFamily: 'monospace', link: 'rgb(101, 42, 140)', mobilePrimary: '',
|
|
271
|
-
};
|
|
272
|
-
expect(await outsideStyles()).toEqual(baseline);
|
|
273
|
-
await page.getByRole('textbox', { name: '平台移动输入' }).fill('弹层继承当前页面');
|
|
274
|
-
await page.getByRole('button', { name: '打开移动弹层' }).click();
|
|
275
|
-
const popup = page.locator('.oxa-mobile-scope .adm-popup-body');
|
|
276
|
-
await expect(popup).toBeVisible();
|
|
277
|
-
await expect(popup).toContainText('弹层继承当前页面');
|
|
278
|
-
await expect(popup).toHaveCSS('background-color', 'rgb(255, 255, 255)');
|
|
279
|
-
expect(await outsideStyles()).toEqual(baseline);
|
|
280
|
-
await popup.getByRole('button', { name: '关闭弹层' }).click();
|
|
281
|
-
await expect(popup).toBeHidden();
|
|
282
|
-
expect(await outsideStyles()).toEqual(baseline);
|
|
283
|
-
expect(pageErrors).toEqual([]);
|
|
284
|
-
});
|
|
285
|
-
});
|