openxiangda-cli 2.0.0-alpha.182 → 2.0.0-alpha.185
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base.js +2 -2
- package/dist/base.js.map +1 -1
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +5 -3
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +34 -16
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +1 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/spec.js +1 -1
- package/dist/commands/spec.js.map +1 -1
- package/package.json +5 -5
- package/template/AGENTS.md +2 -2
- package/template/apps/web/e2e/README.md +7 -0
- package/template/apps/web/package.json +2 -2
- package/template/apps/web/playwright.config.ts +3 -5
- package/template/apps/web/vite.config.ts +1 -9
- package/template/appspec/app.md +5 -1
- package/template/package.json +1 -1
- package/template/apps/web/data-api-live.e2e.html +0 -15
- package/template/apps/web/e2e/client-fixture.ts +0 -5
- package/template/apps/web/e2e/component-defaults-fixture.css +0 -15
- package/template/apps/web/e2e/data-api-live-fixture.tsx +0 -162
- package/template/apps/web/e2e/data-api-live.spec.ts +0 -144
- package/template/apps/web/e2e/field-protocol-fixture.tsx +0 -387
- package/template/apps/web/e2e/field-protocol.spec.ts +0 -285
- package/template/apps/web/e2e/mobile-files.spec.ts +0 -96
- package/template/apps/web/e2e/mobile-reference-fixture.tsx +0 -314
- package/template/apps/web/e2e/mobile-reference.spec.ts +0 -360
- package/template/apps/web/e2e/mobile-selection.spec.ts +0 -199
- package/template/apps/web/e2e/resource-experience-fixture.tsx +0 -491
- package/template/apps/web/e2e/resource-platform-mock.ts +0 -202
- package/template/apps/web/e2e/resources.spec.ts +0 -488
- package/template/apps/web/e2e/standard-admin.spec.ts +0 -511
- package/template/apps/web/e2e/workflow-entry-fixture.tsx +0 -107
- package/template/apps/web/e2e/workflow-entry.spec.ts +0 -556
- package/template/apps/web/e2e/workflow-experience-fixture.tsx +0 -338
- package/template/apps/web/e2e/workflow.spec.ts +0 -1265
- package/template/apps/web/field-protocol.e2e.html +0 -22
- package/template/apps/web/mobile-reference.e2e.html +0 -27
- package/template/apps/web/resource-experience.e2e.html +0 -16
- package/template/apps/web/scripts/check.mjs +0 -43
- package/template/apps/web/test/contracts.test.ts +0 -163
- package/template/apps/web/workflow-entry.e2e.html +0 -15
- package/template/apps/web/workflow-experience.e2e.html +0 -15
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import ReactDOM from 'react-dom/client';
|
|
3
|
-
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
-
import { App, ConfigProvider, Form } from 'antd';
|
|
5
|
-
import {
|
|
6
|
-
RuntimeBoundary,
|
|
7
|
-
OpenXiangdaResourceDefinitionsProvider,
|
|
8
|
-
} from 'openxiangda/react';
|
|
9
|
-
import {
|
|
10
|
-
MobileSurfaceFieldControl,
|
|
11
|
-
SubtableField,
|
|
12
|
-
buildSubtableOperations,
|
|
13
|
-
fieldValueForData,
|
|
14
|
-
type SurfaceField,
|
|
15
|
-
type SurfaceFieldRenderers,
|
|
16
|
-
type SubtableDraftRow,
|
|
17
|
-
} from 'openxiangda/field-kit';
|
|
18
|
-
import { Button } from 'openxiangda/mobile';
|
|
19
|
-
import type { DataResourceSurface, DataFileRef } from 'openxiangda/core';
|
|
20
|
-
import 'openxiangda/react/styles.css';
|
|
21
|
-
|
|
22
|
-
const options = ['选项一', '选项二', '选项三'].map((label, index) => ({
|
|
23
|
-
label,
|
|
24
|
-
value: String(index + 1),
|
|
25
|
-
}));
|
|
26
|
-
const field = (
|
|
27
|
-
key: string,
|
|
28
|
-
type: string,
|
|
29
|
-
widget: string,
|
|
30
|
-
extra = {}
|
|
31
|
-
): SurfaceField =>
|
|
32
|
-
({
|
|
33
|
-
key,
|
|
34
|
-
label: key,
|
|
35
|
-
type,
|
|
36
|
-
widget,
|
|
37
|
-
readCapabilities: [],
|
|
38
|
-
createCapabilities: [],
|
|
39
|
-
updateCapabilities: [],
|
|
40
|
-
...extra,
|
|
41
|
-
} as SurfaceField);
|
|
42
|
-
const childFields = {
|
|
43
|
-
choice: field('choice', 'option.single', 'select', {
|
|
44
|
-
label: '下拉单选',
|
|
45
|
-
options,
|
|
46
|
-
}),
|
|
47
|
-
quantity: field('quantity', 'number.integer', 'number', {
|
|
48
|
-
label: '数值',
|
|
49
|
-
requiredHint: true,
|
|
50
|
-
scale: 0,
|
|
51
|
-
}),
|
|
52
|
-
name: field('name', 'text.short', 'text', {
|
|
53
|
-
label: '单行文本',
|
|
54
|
-
requiredHint: true,
|
|
55
|
-
}),
|
|
56
|
-
parent_id: field('parent_id', 'uuid', 'text'),
|
|
57
|
-
row_order: field('row_order', 'number.integer', 'number'),
|
|
58
|
-
};
|
|
59
|
-
const childSurface: DataResourceSurface = {
|
|
60
|
-
mutationOwner: 'native',
|
|
61
|
-
generated: {
|
|
62
|
-
list: true,
|
|
63
|
-
detail: true,
|
|
64
|
-
create: true,
|
|
65
|
-
update: true,
|
|
66
|
-
delete: true,
|
|
67
|
-
},
|
|
68
|
-
fields: childFields,
|
|
69
|
-
};
|
|
70
|
-
const definitions = {
|
|
71
|
-
'form-items': {
|
|
72
|
-
code: 'form-items',
|
|
73
|
-
name: '表单明细',
|
|
74
|
-
capabilities: {
|
|
75
|
-
read: 'read',
|
|
76
|
-
create: 'create',
|
|
77
|
-
update: 'update',
|
|
78
|
-
delete: 'delete',
|
|
79
|
-
},
|
|
80
|
-
surface: childSurface,
|
|
81
|
-
},
|
|
82
|
-
};
|
|
83
|
-
const groups = [
|
|
84
|
-
{
|
|
85
|
-
title: '基础信息',
|
|
86
|
-
fields: [
|
|
87
|
-
field('单行文本', 'text.short', 'text'),
|
|
88
|
-
field('多行文本', 'text.long', 'textarea'),
|
|
89
|
-
field('数值', 'number.integer', 'number', { scale: 0 }),
|
|
90
|
-
field('评分', 'number.integer', 'rating'),
|
|
91
|
-
field('单选', 'option.single', 'radio', { options }),
|
|
92
|
-
field('复选', 'option.multiple', 'checkbox', { options }),
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
title: '选择与时间',
|
|
97
|
-
fields: [
|
|
98
|
-
field('下拉单选', 'option.single', 'select', { options }),
|
|
99
|
-
field('下拉复选', 'option.multiple', 'multi-select', { options }),
|
|
100
|
-
field('日期', 'date', 'date'),
|
|
101
|
-
field('日期时间', 'datetime', 'datetime'),
|
|
102
|
-
field('日期区间', 'date-range', 'date-range', {
|
|
103
|
-
rangeBoundary: 'closed',
|
|
104
|
-
}),
|
|
105
|
-
field('日期时间区间', 'datetime-range', 'datetime-range', {
|
|
106
|
-
rangeBoundary: 'closed',
|
|
107
|
-
}),
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
title: '文件与明细',
|
|
112
|
-
fields: [
|
|
113
|
-
field('图片上传', 'image', 'image', {
|
|
114
|
-
maxCount: 3,
|
|
115
|
-
maxSizeMb: 20,
|
|
116
|
-
accept: ['image/*'],
|
|
117
|
-
}),
|
|
118
|
-
field('附件', 'file', 'attachment', { maxCount: 3, maxSizeMb: 20 }),
|
|
119
|
-
field('流水号', 'serial-number', 'readonly'),
|
|
120
|
-
field('子表单', 'subtable', 'subtable', {
|
|
121
|
-
subtable: {
|
|
122
|
-
resourceCode: 'form-items',
|
|
123
|
-
foreignKey: 'parent_id',
|
|
124
|
-
orderField: 'row_order',
|
|
125
|
-
maxRows: 3,
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
128
|
-
],
|
|
129
|
-
},
|
|
130
|
-
{
|
|
131
|
-
title: '位置与确认',
|
|
132
|
-
fields: [
|
|
133
|
-
field('地址', 'address', 'address'),
|
|
134
|
-
field('定位', 'location', 'location'),
|
|
135
|
-
field('级联选择', 'cascade.single', 'cascade', {
|
|
136
|
-
options: [
|
|
137
|
-
{
|
|
138
|
-
label: '部门',
|
|
139
|
-
value: 'department',
|
|
140
|
-
children: [
|
|
141
|
-
{ label: 'A部门', value: 'a' },
|
|
142
|
-
{ label: 'B部门', value: 'b' },
|
|
143
|
-
],
|
|
144
|
-
},
|
|
145
|
-
],
|
|
146
|
-
}),
|
|
147
|
-
field('手写签名', 'signature', 'signature'),
|
|
148
|
-
field('富文本', 'text.rich', 'rich-text'),
|
|
149
|
-
],
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
const file: DataFileRef = {
|
|
153
|
-
schemaVersion: 'openxiangda.data-file-ref/v2',
|
|
154
|
-
id: '11111111-1111-4111-8111-111111111111',
|
|
155
|
-
name: '使用说明.pdf',
|
|
156
|
-
size: 12680,
|
|
157
|
-
contentType: 'application/pdf',
|
|
158
|
-
};
|
|
159
|
-
const image: DataFileRef = {
|
|
160
|
-
...file,
|
|
161
|
-
id: '22222222-2222-4222-8222-222222222222',
|
|
162
|
-
name: '活动图片.png',
|
|
163
|
-
contentType: 'image/png',
|
|
164
|
-
width: 640,
|
|
165
|
-
height: 480,
|
|
166
|
-
};
|
|
167
|
-
const initialRows: SubtableDraftRow[] = [
|
|
168
|
-
{
|
|
169
|
-
key: 'item-1',
|
|
170
|
-
id: 'item-1',
|
|
171
|
-
revision: 4,
|
|
172
|
-
state: 'persisted',
|
|
173
|
-
data: { choice: options[2], quantity: 11, name: '测试记录' },
|
|
174
|
-
originalData: { choice: options[2], quantity: 11, name: '测试记录' },
|
|
175
|
-
originalOrder: 0,
|
|
176
|
-
},
|
|
177
|
-
];
|
|
178
|
-
const initial = {
|
|
179
|
-
评分: 3,
|
|
180
|
-
单选: options[1],
|
|
181
|
-
复选: options.slice(1),
|
|
182
|
-
图片上传: [image],
|
|
183
|
-
附件: [file],
|
|
184
|
-
子表单: initialRows,
|
|
185
|
-
日期: '2026-09-05',
|
|
186
|
-
日期时间: '2026-09-05T09:45:33.000Z',
|
|
187
|
-
日期区间: { start: '2026-09-05', end: '2026-09-08' },
|
|
188
|
-
日期时间区间: {
|
|
189
|
-
start: '2026-09-05T09:00:00.000Z',
|
|
190
|
-
end: '2026-09-08T10:00:00.000Z',
|
|
191
|
-
},
|
|
192
|
-
富文本: '<p>已有<strong>格式内容</strong></p>',
|
|
193
|
-
};
|
|
194
|
-
const renderers: SurfaceFieldRenderers = {
|
|
195
|
-
signer: { value: 'acceptance-user', label: '王老师' },
|
|
196
|
-
upload: async (_field, file) => {
|
|
197
|
-
const response = await fetch('/__reference-upload-fixture', { method: 'POST', body: file, headers: { 'x-file-name': encodeURIComponent(file.name) } });
|
|
198
|
-
if (!response.ok) throw new Error('上传失败');
|
|
199
|
-
return response.json();
|
|
200
|
-
},
|
|
201
|
-
renderSubtable: ({ field, disabled, operation, recordId }) => (
|
|
202
|
-
<SubtableField
|
|
203
|
-
mobile
|
|
204
|
-
field={field}
|
|
205
|
-
disabled={disabled}
|
|
206
|
-
operation={operation}
|
|
207
|
-
parentRecordId={recordId}
|
|
208
|
-
/>
|
|
209
|
-
),
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
function MobileReferenceForm() {
|
|
213
|
-
const [form] = Form.useForm();
|
|
214
|
-
const [saved, setSaved] = useState<unknown>();
|
|
215
|
-
const [operations, setOperations] = useState<unknown>();
|
|
216
|
-
const [notice, setNotice] = useState('');
|
|
217
|
-
return (
|
|
218
|
-
<main className="oxa-mobile-entry oxa-mobile-scope reference-form">
|
|
219
|
-
<header className="oxa-mobile-header">
|
|
220
|
-
<h3>未命名表单</h3>
|
|
221
|
-
</header>
|
|
222
|
-
<Form
|
|
223
|
-
className="oxa-mobile-form"
|
|
224
|
-
form={form}
|
|
225
|
-
initialValues={initial}
|
|
226
|
-
onFinish={values => {
|
|
227
|
-
setSaved(
|
|
228
|
-
Object.fromEntries(
|
|
229
|
-
groups
|
|
230
|
-
.flatMap(group => group.fields)
|
|
231
|
-
.map(item => [
|
|
232
|
-
item.key,
|
|
233
|
-
fieldValueForData(item, values[item.key]),
|
|
234
|
-
])
|
|
235
|
-
)
|
|
236
|
-
);
|
|
237
|
-
setOperations(
|
|
238
|
-
buildSubtableOperations({
|
|
239
|
-
rows: values.子表单,
|
|
240
|
-
childResourceCode: 'form-items',
|
|
241
|
-
childSurface,
|
|
242
|
-
foreignKey: 'parent_id',
|
|
243
|
-
orderField: 'row_order',
|
|
244
|
-
maxRows: 3,
|
|
245
|
-
parent: { operation: 'update', id: 'parent-1' },
|
|
246
|
-
canWrite: () => true,
|
|
247
|
-
canDelete: true,
|
|
248
|
-
})
|
|
249
|
-
);
|
|
250
|
-
setNotice('提交成功');
|
|
251
|
-
}}
|
|
252
|
-
>
|
|
253
|
-
<div className="reference-content">
|
|
254
|
-
{groups.map(group => (
|
|
255
|
-
<section key={group.title} aria-label={group.title}>
|
|
256
|
-
<h2 className="reference-group-title">{group.title}</h2>
|
|
257
|
-
<div className="reference-group">
|
|
258
|
-
{group.fields.map(field => (
|
|
259
|
-
<div data-field-code={field.key} key={field.key}>
|
|
260
|
-
<MobileSurfaceFieldControl
|
|
261
|
-
field={field}
|
|
262
|
-
operation="create"
|
|
263
|
-
resourceCode="field-records"
|
|
264
|
-
renderers={renderers}
|
|
265
|
-
/>
|
|
266
|
-
</div>
|
|
267
|
-
))}
|
|
268
|
-
</div>
|
|
269
|
-
</section>
|
|
270
|
-
))}
|
|
271
|
-
{notice && (
|
|
272
|
-
<div className="reference-notice" role="status">
|
|
273
|
-
{notice}
|
|
274
|
-
</div>
|
|
275
|
-
)}
|
|
276
|
-
</div>
|
|
277
|
-
<footer className="oxa-actions">
|
|
278
|
-
<Button
|
|
279
|
-
onClick={() => {
|
|
280
|
-
sessionStorage.setItem(
|
|
281
|
-
'mobile-reference-draft',
|
|
282
|
-
JSON.stringify(form.getFieldsValue())
|
|
283
|
-
);
|
|
284
|
-
setNotice('已暂存');
|
|
285
|
-
}}
|
|
286
|
-
>
|
|
287
|
-
暂存
|
|
288
|
-
</Button>
|
|
289
|
-
<Button color="primary" type="submit">
|
|
290
|
-
提交
|
|
291
|
-
</Button>
|
|
292
|
-
</footer>
|
|
293
|
-
<output data-saved-values>{JSON.stringify(saved)}</output>
|
|
294
|
-
<output data-operations>{JSON.stringify(operations)}</output>
|
|
295
|
-
</Form>
|
|
296
|
-
</main>
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
301
|
-
<React.StrictMode>
|
|
302
|
-
<ConfigProvider>
|
|
303
|
-
<App>
|
|
304
|
-
<BrowserRouter>
|
|
305
|
-
<RuntimeBoundary>
|
|
306
|
-
<OpenXiangdaResourceDefinitionsProvider definitions={definitions}>
|
|
307
|
-
<MobileReferenceForm />
|
|
308
|
-
</OpenXiangdaResourceDefinitionsProvider>
|
|
309
|
-
</RuntimeBoundary>
|
|
310
|
-
</BrowserRouter>
|
|
311
|
-
</App>
|
|
312
|
-
</ConfigProvider>
|
|
313
|
-
</React.StrictMode>
|
|
314
|
-
);
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import { expect, test, type Page } from '@playwright/test';
|
|
2
|
-
import { mockPlatform } from './resource-platform-mock';
|
|
3
|
-
|
|
4
|
-
const field = (page: Page, label: string) =>
|
|
5
|
-
page.locator(`[data-field-code="${label}"]`);
|
|
6
|
-
const sheet = (page: Page, label: string) =>
|
|
7
|
-
page.getByRole('dialog', { name: `选择${label}`, exact: true });
|
|
8
|
-
const open = async (page: Page, label: string) => {
|
|
9
|
-
await field(page, label)
|
|
10
|
-
.getByRole('button', { name: `选择${label}`, exact: true })
|
|
11
|
-
.click();
|
|
12
|
-
await expect(sheet(page, label)).toBeVisible();
|
|
13
|
-
};
|
|
14
|
-
const submit = async (page: Page) => {
|
|
15
|
-
await page.getByRole('button', { name: '提交', exact: true }).click();
|
|
16
|
-
await expect(page.locator('[data-saved-values]')).not.toBeEmpty();
|
|
17
|
-
return JSON.parse(
|
|
18
|
-
(await page.locator('[data-saved-values]').textContent()) || '{}'
|
|
19
|
-
);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
test.describe('confirmed mobile form reference', () => {
|
|
23
|
-
test.beforeEach(async ({ page }) => {
|
|
24
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
25
|
-
await mockPlatform(page.context(), true, undefined, true);
|
|
26
|
-
const uploads = new Map<string, Buffer>();
|
|
27
|
-
await page.route('**/__reference-upload-fixture', route => {
|
|
28
|
-
const id = crypto.randomUUID();
|
|
29
|
-
const body = route.request().postDataBuffer()!;
|
|
30
|
-
uploads.set(id, body);
|
|
31
|
-
return route.fulfill({ json: { schemaVersion: 'openxiangda.data-file-ref/v2', id, name: decodeURIComponent(route.request().headers()['x-file-name']), size: body.length, contentType: 'image/png' } });
|
|
32
|
-
});
|
|
33
|
-
await page.route('**/files/*/content?**', route => {
|
|
34
|
-
const id = new URL(route.request().url()).pathname.split('/files/')[1].split('/')[0];
|
|
35
|
-
const uploaded = uploads.get(id);
|
|
36
|
-
return uploaded ? route.fulfill({ contentType: 'image/png', body: uploaded }) : route.fulfill({
|
|
37
|
-
contentType: 'image/svg+xml',
|
|
38
|
-
body: '<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480"><rect width="640" height="480" fill="#e7eef1"/><path d="M0 480L220 180L410 480M230 480L450 250L640 480" fill="#9badbb"/></svg>',
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
await page.route('**/files/*/preview?**', route =>
|
|
42
|
-
route.fulfill({
|
|
43
|
-
json: {
|
|
44
|
-
canPreview: true,
|
|
45
|
-
previewType: 'image',
|
|
46
|
-
file: { name: '活动图片.png' },
|
|
47
|
-
},
|
|
48
|
-
})
|
|
49
|
-
);
|
|
50
|
-
await page.goto('/mobile-reference.e2e.html');
|
|
51
|
-
await expect(
|
|
52
|
-
page.getByRole('heading', { name: '未命名表单' })
|
|
53
|
-
).toBeVisible();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('grouped borderless inputs, inline choices, rating and staged dropdowns', async ({
|
|
57
|
-
page,
|
|
58
|
-
}, info) => {
|
|
59
|
-
const errors: string[] = [];
|
|
60
|
-
page.on('pageerror', error => errors.push(error.message));
|
|
61
|
-
await page.screenshot({
|
|
62
|
-
animations: 'disabled',
|
|
63
|
-
path: info.outputPath('mobile-form-empty.png'),
|
|
64
|
-
});
|
|
65
|
-
await expect(field(page, '单行文本').getByRole('textbox')).toHaveCSS(
|
|
66
|
-
'border-top-width',
|
|
67
|
-
'0px'
|
|
68
|
-
);
|
|
69
|
-
await field(page, '单行文本').getByRole('textbox').fill('会议记录');
|
|
70
|
-
await field(page, '多行文本').getByRole('textbox').fill('第一行\n第二行');
|
|
71
|
-
await field(page, '数值').getByRole('textbox').fill('18');
|
|
72
|
-
await field(page, '评分')
|
|
73
|
-
.getByRole('radio', { name: '4', exact: true })
|
|
74
|
-
.click();
|
|
75
|
-
await field(page, '单选').getByText('选项三', { exact: true }).click();
|
|
76
|
-
await field(page, '复选').getByText('选项一', { exact: true }).click();
|
|
77
|
-
await page.screenshot({
|
|
78
|
-
animations: 'disabled',
|
|
79
|
-
path: info.outputPath('mobile-basic-fields.png'),
|
|
80
|
-
});
|
|
81
|
-
await open(page, '下拉单选');
|
|
82
|
-
await sheet(page, '下拉单选').getByText('选项二', { exact: true }).click();
|
|
83
|
-
await sheet(page, '下拉单选')
|
|
84
|
-
.getByRole('button', { name: '取消', exact: true })
|
|
85
|
-
.click();
|
|
86
|
-
await expect(
|
|
87
|
-
field(page, '下拉单选').getByRole('button', {
|
|
88
|
-
name: '选择下拉单选',
|
|
89
|
-
exact: true,
|
|
90
|
-
})
|
|
91
|
-
).toHaveText('请选择');
|
|
92
|
-
await open(page, '下拉单选');
|
|
93
|
-
await sheet(page, '下拉单选')
|
|
94
|
-
.getByPlaceholder('搜索', { exact: true })
|
|
95
|
-
.fill('三');
|
|
96
|
-
await sheet(page, '下拉单选').getByText('选项三', { exact: true }).click();
|
|
97
|
-
await page.screenshot({
|
|
98
|
-
animations: 'disabled',
|
|
99
|
-
path: info.outputPath('mobile-single-select.png'),
|
|
100
|
-
});
|
|
101
|
-
await sheet(page, '下拉单选')
|
|
102
|
-
.getByRole('button', { name: '确定', exact: true })
|
|
103
|
-
.click();
|
|
104
|
-
await open(page, '下拉复选');
|
|
105
|
-
await sheet(page, '下拉复选').getByText('选项一', { exact: true }).click();
|
|
106
|
-
await sheet(page, '下拉复选').getByText('选项二', { exact: true }).click();
|
|
107
|
-
await expect(sheet(page, '下拉复选')).toContainText('当前已选中 2 项');
|
|
108
|
-
await page.screenshot({
|
|
109
|
-
animations: 'disabled',
|
|
110
|
-
path: info.outputPath('mobile-multi-select.png'),
|
|
111
|
-
});
|
|
112
|
-
await sheet(page, '下拉复选')
|
|
113
|
-
.getByRole('button', { name: '确定', exact: true })
|
|
114
|
-
.click();
|
|
115
|
-
const saved = await submit(page);
|
|
116
|
-
expect(saved.评分).toBe(4);
|
|
117
|
-
expect(saved.数值).toBe(18);
|
|
118
|
-
expect(saved.多行文本).toBe('第一行\n第二行');
|
|
119
|
-
expect(saved.单选).toEqual({ label: '选项三', value: '3' });
|
|
120
|
-
expect(saved.复选).toHaveLength(3);
|
|
121
|
-
expect(saved.下拉复选).toHaveLength(2);
|
|
122
|
-
expect(saved.下拉单选).toEqual({ label: '选项三', value: '3' });
|
|
123
|
-
expect(saved.富文本).toBe('<p>已有<strong>格式内容</strong></p>');
|
|
124
|
-
expect(errors).toEqual([]);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test('calendar, time wheels and range steps preserve cancellation and boundaries', async ({
|
|
128
|
-
page,
|
|
129
|
-
}, info) => {
|
|
130
|
-
await open(page, '日期时间');
|
|
131
|
-
const date = sheet(page, '日期时间');
|
|
132
|
-
await expect(date.locator('.adm-calendar')).toBeVisible();
|
|
133
|
-
await date
|
|
134
|
-
.locator('.adm-calendar-cell:not(.adm-calendar-cell-disabled)')
|
|
135
|
-
.filter({ hasText: /^7$/ })
|
|
136
|
-
.click();
|
|
137
|
-
await page.screenshot({
|
|
138
|
-
animations: 'disabled',
|
|
139
|
-
path: info.outputPath('mobile-calendar.png'),
|
|
140
|
-
});
|
|
141
|
-
await date
|
|
142
|
-
.locator('.oxa-mobile-date-time-footer')
|
|
143
|
-
.getByRole('button')
|
|
144
|
-
.click();
|
|
145
|
-
await expect(date.locator('.adm-picker-view')).toBeVisible();
|
|
146
|
-
await page.screenshot({
|
|
147
|
-
animations: 'disabled',
|
|
148
|
-
path: info.outputPath('mobile-time-wheels.png'),
|
|
149
|
-
});
|
|
150
|
-
await date.getByRole('button', { name: '取消', exact: true }).click();
|
|
151
|
-
expect((await submit(page)).日期时间).toBe('2026-09-05T09:45:33.000Z');
|
|
152
|
-
await open(page, '日期时间区间');
|
|
153
|
-
const range = sheet(page, '日期时间区间');
|
|
154
|
-
await expect(range).toContainText('选择开始时间');
|
|
155
|
-
await page.screenshot({
|
|
156
|
-
animations: 'disabled',
|
|
157
|
-
path: info.outputPath('mobile-range-start.png'),
|
|
158
|
-
});
|
|
159
|
-
await range.getByRole('button', { name: '下一步', exact: true }).click();
|
|
160
|
-
await expect(range).toContainText('选择结束时间');
|
|
161
|
-
await range.getByRole('button', { name: '上一步', exact: true }).click();
|
|
162
|
-
await expect(range).toContainText('选择开始时间');
|
|
163
|
-
await range.getByRole('button', { name: '下一步', exact: true }).click();
|
|
164
|
-
await range.getByRole('button', { name: '确定', exact: true }).click();
|
|
165
|
-
const saved = await submit(page);
|
|
166
|
-
expect(saved.日期时间区间).toEqual({
|
|
167
|
-
start: '2026-09-05T09:00:00.000Z',
|
|
168
|
-
end: '2026-09-08T10:00:00.000Z',
|
|
169
|
-
});
|
|
170
|
-
await open(page, '日期区间');
|
|
171
|
-
await sheet(page, '日期区间')
|
|
172
|
-
.getByRole('button', { name: '下一步', exact: true })
|
|
173
|
-
.click();
|
|
174
|
-
await sheet(page, '日期区间')
|
|
175
|
-
.getByRole('button', { name: '确定', exact: true })
|
|
176
|
-
.click();
|
|
177
|
-
expect((await submit(page)).日期区间).toEqual({
|
|
178
|
-
start: '2026-09-05',
|
|
179
|
-
end: '2026-09-08',
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('address drilldown, location, cascade, touch signature and plain rich text use canonical values', async ({
|
|
184
|
-
page,
|
|
185
|
-
}, info) => {
|
|
186
|
-
const levels = [
|
|
187
|
-
{ adcode: '330000', name: '浙江省', hasChildren: true },
|
|
188
|
-
{ adcode: '330100', name: '杭州市', hasChildren: true },
|
|
189
|
-
{ adcode: '330110', name: '余杭区', hasChildren: true },
|
|
190
|
-
{ adcode: '330110001', name: '五常街道', hasChildren: false },
|
|
191
|
-
];
|
|
192
|
-
await page.route('**/china-divisions/**', route => {
|
|
193
|
-
const parent = new URL(route.request().url()).searchParams.get(
|
|
194
|
-
'parentAdcode'
|
|
195
|
-
);
|
|
196
|
-
return route.fulfill({
|
|
197
|
-
json: [
|
|
198
|
-
levels[
|
|
199
|
-
parent ? levels.findIndex(item => item.adcode === parent) + 1 : 0
|
|
200
|
-
],
|
|
201
|
-
],
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
await field(page, '地址')
|
|
205
|
-
.getByRole('button', { name: '选择行政区地址' })
|
|
206
|
-
.click();
|
|
207
|
-
const address = page.getByRole('dialog', { name: '选择行政区地址' });
|
|
208
|
-
for (const level of levels)
|
|
209
|
-
await address.getByText(level.name, { exact: true }).click();
|
|
210
|
-
await page.screenshot({
|
|
211
|
-
animations: 'disabled',
|
|
212
|
-
path: info.outputPath('mobile-address-picker.png'),
|
|
213
|
-
});
|
|
214
|
-
await address.getByRole('button', { name: '确定', exact: true }).click();
|
|
215
|
-
await field(page, '地址')
|
|
216
|
-
.getByRole('textbox', { name: '详细地址' })
|
|
217
|
-
.pressSequentially('文一西路 1 号');
|
|
218
|
-
await page.context().grantPermissions(['geolocation']);
|
|
219
|
-
await page.context().setGeolocation({
|
|
220
|
-
latitude: 30.234567,
|
|
221
|
-
longitude: 120.123456,
|
|
222
|
-
accuracy: 10,
|
|
223
|
-
});
|
|
224
|
-
await field(page, '定位').getByRole('button', { name: '获取定位' }).click();
|
|
225
|
-
await expect(field(page, '定位')).toContainText('120.123456');
|
|
226
|
-
await open(page, '级联选择');
|
|
227
|
-
await sheet(page, '级联选择')
|
|
228
|
-
.getByRole('button', { name: '进入部门' })
|
|
229
|
-
.click();
|
|
230
|
-
await sheet(page, '级联选择').getByText('B部门', { exact: true }).click();
|
|
231
|
-
await page.screenshot({
|
|
232
|
-
animations: 'disabled',
|
|
233
|
-
path: info.outputPath('mobile-cascade-picker.png'),
|
|
234
|
-
});
|
|
235
|
-
await sheet(page, '级联选择')
|
|
236
|
-
.getByRole('button', { name: '确定', exact: true })
|
|
237
|
-
.click();
|
|
238
|
-
await field(page, '手写签名')
|
|
239
|
-
.getByRole('button', { name: '开始签名' })
|
|
240
|
-
.click();
|
|
241
|
-
const signature = page.getByRole('dialog', {
|
|
242
|
-
name: '手写签名',
|
|
243
|
-
exact: true,
|
|
244
|
-
});
|
|
245
|
-
const canvas = signature.locator('canvas');
|
|
246
|
-
await canvas.click({ trial: true });
|
|
247
|
-
const box = (await canvas.boundingBox())!;
|
|
248
|
-
await page.mouse.move(box.x + 40, box.y + 80);
|
|
249
|
-
await page.mouse.down();
|
|
250
|
-
await page.mouse.move(box.x + 180, box.y + 120, { steps: 8 });
|
|
251
|
-
await page.mouse.move(box.x + 110, box.y + 160, { steps: 6 });
|
|
252
|
-
await page.mouse.up();
|
|
253
|
-
await page.screenshot({
|
|
254
|
-
animations: 'disabled',
|
|
255
|
-
path: info.outputPath('mobile-signature-pad.png'),
|
|
256
|
-
});
|
|
257
|
-
await signature.getByRole('button', { name: '保存签名' }).click();
|
|
258
|
-
await expect(signature).toHaveCount(0);
|
|
259
|
-
await expect(field(page, '手写签名').getByRole('img', { name: '业务签名' })).toHaveJSProperty('naturalHeight', 240);
|
|
260
|
-
await field(page, '富文本')
|
|
261
|
-
.getByRole('textbox')
|
|
262
|
-
.fill('第一行 <说明>\n第二行 & 补充');
|
|
263
|
-
const saved = await submit(page);
|
|
264
|
-
expect(saved.地址.detail).toBe('文一西路 1 号');
|
|
265
|
-
expect(saved.地址.street.label).toBe('五常街道');
|
|
266
|
-
expect(saved.定位.longitude).toBe(120.123456);
|
|
267
|
-
expect(saved.定位.source).toBe('browser');
|
|
268
|
-
expect(saved.级联选择).toEqual([
|
|
269
|
-
{ value: 'department', label: '部门' },
|
|
270
|
-
{ value: 'b', label: 'B部门' },
|
|
271
|
-
]);
|
|
272
|
-
expect(saved.手写签名.hash).toMatch(/^[0-9a-f]{64}$/);
|
|
273
|
-
expect(saved.手写签名.points.length).toBeGreaterThan(1);
|
|
274
|
-
expect(saved.富文本).toBe(
|
|
275
|
-
'<p>第一行 <说明></p><p>第二行 & 补充</p>'
|
|
276
|
-
);
|
|
277
|
-
expect(
|
|
278
|
-
await page.evaluate(
|
|
279
|
-
() => document.documentElement.scrollWidth <= innerWidth
|
|
280
|
-
)
|
|
281
|
-
).toBe(true);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('inline subtable validates collapsed rows and preserves update/delete revisions', async ({
|
|
285
|
-
page,
|
|
286
|
-
}, info) => {
|
|
287
|
-
const subtable = field(page, '子表单');
|
|
288
|
-
await expect(subtable.locator('.oxa-mobile-subtable-card')).toHaveCount(1);
|
|
289
|
-
const row = subtable.locator('.oxa-mobile-subtable-card').first();
|
|
290
|
-
await row
|
|
291
|
-
.getByRole('textbox', { name: '单行文本', exact: true })
|
|
292
|
-
.fill('修改后的记录');
|
|
293
|
-
await row.getByRole('button', { name: '折叠第1项', exact: true }).click();
|
|
294
|
-
await expect(
|
|
295
|
-
row.getByRole('textbox', { name: '单行文本', exact: true })
|
|
296
|
-
).toBeHidden();
|
|
297
|
-
await subtable.getByRole('button', { name: '新增一项' }).click();
|
|
298
|
-
const second = subtable.locator('.oxa-mobile-subtable-card').nth(1);
|
|
299
|
-
await second
|
|
300
|
-
.getByRole('textbox', { name: '数值', exact: true })
|
|
301
|
-
.fill('1.5');
|
|
302
|
-
await second
|
|
303
|
-
.getByRole('button', { name: '折叠第2项', exact: true })
|
|
304
|
-
.click();
|
|
305
|
-
await page.getByRole('button', { name: '提交', exact: true }).click();
|
|
306
|
-
await expect(page.locator('[data-saved-values]')).toBeEmpty();
|
|
307
|
-
await expect(
|
|
308
|
-
second.getByRole('textbox', { name: '数值', exact: true })
|
|
309
|
-
).toBeVisible();
|
|
310
|
-
await expect(second.getByRole('alert').first()).toBeVisible();
|
|
311
|
-
await second.getByRole('textbox', { name: '数值', exact: true }).fill('12');
|
|
312
|
-
await second
|
|
313
|
-
.getByRole('textbox', { name: '单行文本', exact: true })
|
|
314
|
-
.fill('新增记录');
|
|
315
|
-
await row.getByRole('button', { name: '展开第1项', exact: true }).click();
|
|
316
|
-
await field(page, '图片上传').scrollIntoViewIfNeeded();
|
|
317
|
-
await page.screenshot({
|
|
318
|
-
animations: 'disabled',
|
|
319
|
-
path: info.outputPath('mobile-files-and-subtable.png'),
|
|
320
|
-
});
|
|
321
|
-
await subtable.evaluate(element =>
|
|
322
|
-
window.scrollBy(0, element.getBoundingClientRect().top - 82)
|
|
323
|
-
);
|
|
324
|
-
await page.screenshot({
|
|
325
|
-
animations: 'disabled',
|
|
326
|
-
path: info.outputPath('mobile-inline-subtable.png'),
|
|
327
|
-
});
|
|
328
|
-
await submit(page);
|
|
329
|
-
const operations = JSON.parse(
|
|
330
|
-
(await page.locator('[data-operations]').textContent()) || '[]'
|
|
331
|
-
);
|
|
332
|
-
expect(operations[0]).toMatchObject({
|
|
333
|
-
operation: 'update',
|
|
334
|
-
id: 'item-1',
|
|
335
|
-
expectedRevision: 4,
|
|
336
|
-
data: { name: '修改后的记录' },
|
|
337
|
-
});
|
|
338
|
-
expect(operations[1]).toMatchObject({
|
|
339
|
-
operation: 'create',
|
|
340
|
-
data: {
|
|
341
|
-
name: '新增记录',
|
|
342
|
-
quantity: 12,
|
|
343
|
-
parent_id: 'parent-1',
|
|
344
|
-
row_order: 1,
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
await row.getByRole('button', { name: '删除', exact: true }).click();
|
|
348
|
-
await submit(page);
|
|
349
|
-
expect(
|
|
350
|
-
JSON.parse(
|
|
351
|
-
(await page.locator('[data-operations]').textContent()) || '[]'
|
|
352
|
-
)[0]
|
|
353
|
-
).toMatchObject({ operation: 'delete', id: 'item-1', expectedRevision: 4 });
|
|
354
|
-
await page.screenshot({
|
|
355
|
-
animations: 'disabled',
|
|
356
|
-
path: info.outputPath('mobile-form-complete.png'),
|
|
357
|
-
fullPage: true,
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
});
|