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,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
|
-
});
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { expect, test, type Page } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
const field = (page: Page, label: string) => page.locator(`[data-field-code="${label}"]`);
|
|
4
|
-
const picker = (page: Page, label: string) => field(page, label).getByRole('dialog', { name: `选择${label}` });
|
|
5
|
-
const open = async (page: Page, label: string) => {
|
|
6
|
-
await field(page, label).getByRole('button', { name: `选择${label}`, exact: true }).click();
|
|
7
|
-
await expect(picker(page, label)).toBeVisible();
|
|
8
|
-
};
|
|
9
|
-
const submit = async (page: Page) => {
|
|
10
|
-
await page.getByRole('button', { name: '验证表单', exact: true }).click();
|
|
11
|
-
await expect(page.locator('[data-saved-values]')).not.toBeEmpty();
|
|
12
|
-
return JSON.parse(await page.locator('[data-saved-values]').innerText());
|
|
13
|
-
};
|
|
14
|
-
const department = (id: string, label: string, hasChildren = false, selectable = true) => ({
|
|
15
|
-
kind: 'department', id, label, hasChildren, selectable,
|
|
16
|
-
snapshot: { value: id, label, fullPath: `学校 / ${label}` },
|
|
17
|
-
});
|
|
18
|
-
const member = (id: string, label: string, selectable = true) => ({
|
|
19
|
-
kind: 'user', id, label, selectable,
|
|
20
|
-
snapshot: { value: id, label, employeeNo: `E-${id}`, departments: [{ value: 'college-a', label: '理学院' }] },
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test.describe('mobile platform selection fields', () => {
|
|
24
|
-
test.beforeEach(async ({ page }) => {
|
|
25
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
26
|
-
await page.route('**/china-divisions/**', route => route.fulfill({ json: [] }));
|
|
27
|
-
await page.route('**/files/*/content?**', route => route.fulfill({ status: 204, body: '' }));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('member browsing, paging and search retain staged snapshots until confirmation', async ({ page }) => {
|
|
31
|
-
const errors: string[] = [];
|
|
32
|
-
page.on('pageerror', error => errors.push(error.message));
|
|
33
|
-
const queries: string[] = [];
|
|
34
|
-
await page.route('**/directory/**', async route => {
|
|
35
|
-
const url = new URL(route.request().url());
|
|
36
|
-
queries.push(url.pathname + url.search);
|
|
37
|
-
const tree = url.pathname.endsWith('/tree');
|
|
38
|
-
const browse = url.pathname.endsWith('/users') && url.pathname.includes('/departments/');
|
|
39
|
-
const items = tree ? [department('college-a', '理学院')]
|
|
40
|
-
: browse ? (url.searchParams.get('page') === '2' ? [member('user-3', '李老师')] : [member('user-2', '王老师'), member('blocked', '不可选成员', false)])
|
|
41
|
-
: [member('user-4', '陈老师')];
|
|
42
|
-
await route.fulfill({ json: { schemaVersion: 'openxiangda.directory-entry-page/v2', kind: tree ? 'department' : 'user', items,
|
|
43
|
-
nextCursor: browse && url.searchParams.get('page') === '1' ? '2' : null } });
|
|
44
|
-
});
|
|
45
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
46
|
-
await open(page, '成员多选');
|
|
47
|
-
const sheet = picker(page, '成员多选');
|
|
48
|
-
await sheet.getByRole('button', { name: '进入理学院' }).click();
|
|
49
|
-
await expect(sheet.getByText('不可选成员', { exact: true })).toBeVisible();
|
|
50
|
-
await expect(sheet.locator('.adm-check-list-item').filter({ hasText: '不可选成员' })).toHaveAttribute('aria-disabled', 'true');
|
|
51
|
-
await sheet.getByText('不可选成员', { exact: true }).click({ force: true });
|
|
52
|
-
await expect(sheet.getByLabel('已选项目')).not.toContainText('不可选成员');
|
|
53
|
-
await sheet.getByText('王老师', { exact: true }).click();
|
|
54
|
-
await sheet.getByRole('button', { name: '更多成员' }).click();
|
|
55
|
-
await sheet.getByText('李老师', { exact: true }).click();
|
|
56
|
-
await sheet.getByPlaceholder('搜索成员多选').fill('陈老师');
|
|
57
|
-
await sheet.getByText('陈老师', { exact: true }).click();
|
|
58
|
-
await expect(sheet.getByLabel('已选项目')).toContainText('王老师');
|
|
59
|
-
await expect(sheet.getByLabel('已选项目')).toContainText('李老师');
|
|
60
|
-
await sheet.getByRole('button', { name: '关闭', exact: true }).click();
|
|
61
|
-
await expect(field(page, '成员多选').getByRole('button', { name: '选择成员多选', exact: true })).toHaveText('张三');
|
|
62
|
-
|
|
63
|
-
await open(page, '成员多选');
|
|
64
|
-
await sheet.getByRole('button', { name: '清空', exact: true }).click();
|
|
65
|
-
await sheet.getByRole('button', { name: '进入理学院' }).click();
|
|
66
|
-
await sheet.getByText('王老师', { exact: true }).click();
|
|
67
|
-
await sheet.getByPlaceholder('搜索成员多选').fill('陈老师');
|
|
68
|
-
await sheet.getByText('陈老师', { exact: true }).click();
|
|
69
|
-
await sheet.getByRole('button', { name: '确定(2)', exact: true }).click();
|
|
70
|
-
const saved = await submit(page);
|
|
71
|
-
expect(saved['成员多选']).toEqual([member('user-2', '王老师').snapshot, member('user-4', '陈老师').snapshot]);
|
|
72
|
-
await page.getByRole('button', { name: '重新载入已保存值' }).click();
|
|
73
|
-
await expect(field(page, '成员多选').getByRole('button', { name: '选择成员多选', exact: true })).toHaveText('王老师、陈老师');
|
|
74
|
-
expect(queries.some(query => query.includes('page=2'))).toBe(true);
|
|
75
|
-
expect(errors).toEqual([]);
|
|
76
|
-
await open(page, '成员多选');
|
|
77
|
-
await sheet.getByRole('button', { name: '进入理学院' }).click();
|
|
78
|
-
await expect(sheet.getByText('王老师', { exact: true }).last()).toBeVisible();
|
|
79
|
-
await page.screenshot({ path: 'test-results/mobile-member-picker.png' });
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('explicit mobile department control browses disabled ancestors and retries failed pages', async ({ page }) => {
|
|
83
|
-
await page.setViewportSize({ width: 1200, height: 900 });
|
|
84
|
-
let failures = 1;
|
|
85
|
-
await page.route('**/directory/**', async route => {
|
|
86
|
-
const url = new URL(route.request().url());
|
|
87
|
-
const parent = url.searchParams.get('parentId');
|
|
88
|
-
if (parent === 'root' && failures-- > 0) { await route.fulfill({ status: 503, json: { code: 'OPENXIANGDA_DIRECTORY_REQUEST_FAILED', message: '目录暂时不可用' } }); return; }
|
|
89
|
-
await route.fulfill({ json: { schemaVersion: 'openxiangda.directory-entry-page/v2', kind: 'department',
|
|
90
|
-
items: parent === 'root' ? [department('college-a', '理学院'), department('college-b', '文学院')]
|
|
91
|
-
: url.searchParams.get('offset') ? [department('office', '办公室')]
|
|
92
|
-
: [department('root', '学校', true, false)], nextCursor: !parent && !url.searchParams.get('offset') ? '100' : null } });
|
|
93
|
-
});
|
|
94
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
95
|
-
await open(page, '部门多选');
|
|
96
|
-
const sheet = picker(page, '部门多选');
|
|
97
|
-
await expect(sheet.locator('.ant-tree, .ant-select, .ant-drawer')).toHaveCount(0);
|
|
98
|
-
await sheet.getByRole('button', { name: '清空', exact: true }).click();
|
|
99
|
-
await sheet.getByRole('button', { name: '更多部门' }).click();
|
|
100
|
-
await sheet.getByText('办公室', { exact: true }).click();
|
|
101
|
-
await sheet.getByRole('button', { name: '进入学校' }).click();
|
|
102
|
-
await expect(sheet.getByRole('alert')).toContainText('目录暂时不可用');
|
|
103
|
-
await sheet.getByRole('button', { name: '重试', exact: true }).click();
|
|
104
|
-
await sheet.getByText('文学院', { exact: true }).click();
|
|
105
|
-
await sheet.getByRole('button', { name: '确定(2)', exact: true }).click();
|
|
106
|
-
expect((await submit(page))['部门多选']).toEqual([department('office', '办公室').snapshot, department('college-b', '文学院').snapshot]);
|
|
107
|
-
await open(page, '部门单选');
|
|
108
|
-
await picker(page, '部门单选').getByRole('button', { name: '进入学校' }).click();
|
|
109
|
-
await picker(page, '部门单选').getByText('文学院', { exact: true }).click();
|
|
110
|
-
await picker(page, '部门单选').getByRole('button', { name: '确定(1)', exact: true }).click();
|
|
111
|
-
expect((await submit(page))['部门单选']).toEqual(department('college-b', '文学院').snapshot);
|
|
112
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
113
|
-
await open(page, '部门多选');
|
|
114
|
-
await sheet.getByRole('button', { name: '进入学校' }).click();
|
|
115
|
-
await expect(sheet.getByText('文学院', { exact: true }).last()).toBeVisible();
|
|
116
|
-
await page.screenshot({ path: 'test-results/mobile-department-picker.png' });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('reference search ignores stale pages, preserves off-page choices and retries without losing them', async ({ page }) => {
|
|
120
|
-
let releaseOld: (() => void) | undefined;
|
|
121
|
-
let oldFinished = false;
|
|
122
|
-
let failed = false;
|
|
123
|
-
const reference = (value: string, label: string) => ({ value, label, resourceCode: 'field-lookups', snapshot: { code: `CODE-${value}` } });
|
|
124
|
-
await page.route('**/source/query', async route => {
|
|
125
|
-
const request = route.request().postDataJSON();
|
|
126
|
-
if (request.cursor === 'old-next') { await new Promise<void>(resolve => { releaseOld = resolve; }); }
|
|
127
|
-
if (request.keyword === '故障' && !failed) { failed = true; await route.fulfill({ status: 503, json: { code: 'OPENXIANGDA_DATA_SOURCE_FAILED', message: '候选加载失败' } }); return; }
|
|
128
|
-
const items = request.cursor ? [reference('stale', '过期候选')] : request.keyword ? [reference('b', '测量服务')] : [reference('a', '检测设备')];
|
|
129
|
-
await route.fulfill({ json: { schemaVersion: 'openxiangda.data-field-source-page/v2',
|
|
130
|
-
environment: { id: 'env', key: 'production', activeAppVersionId: 'version', headRevision: 1, authzRevisionId: 'authz' },
|
|
131
|
-
resourceCode: 'field-records', fieldCode: request.fieldCode, operation: request.operation, sourceResourceCode: 'field-lookups',
|
|
132
|
-
items, nextCursor: request.keyword || request.cursor ? null : 'old-next' } });
|
|
133
|
-
if (request.cursor) oldFinished = true;
|
|
134
|
-
});
|
|
135
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
136
|
-
await open(page, '动态多选下拉');
|
|
137
|
-
const sheet = picker(page, '动态多选下拉');
|
|
138
|
-
await sheet.getByRole('button', { name: '清空', exact: true }).click();
|
|
139
|
-
await sheet.getByText('检测设备', { exact: true }).click();
|
|
140
|
-
await sheet.getByRole('button', { name: '加载更多', exact: true }).click();
|
|
141
|
-
await expect.poll(() => Boolean(releaseOld)).toBe(true);
|
|
142
|
-
await sheet.getByPlaceholder('搜索动态多选下拉').fill('测量');
|
|
143
|
-
await sheet.getByText('测量服务', { exact: true }).click();
|
|
144
|
-
releaseOld!();
|
|
145
|
-
await expect.poll(() => oldFinished).toBe(true);
|
|
146
|
-
await expect(sheet.getByText('过期候选')).toHaveCount(0);
|
|
147
|
-
await expect(sheet.getByRole('button', { name: '加载更多', exact: true })).toHaveCount(0);
|
|
148
|
-
await expect(sheet.getByLabel('已选项目')).toContainText('检测设备');
|
|
149
|
-
await page.screenshot({ path: 'test-results/mobile-reference-picker.png' });
|
|
150
|
-
await sheet.getByPlaceholder('搜索动态多选下拉').fill('故障');
|
|
151
|
-
await expect(sheet.getByRole('alert')).toContainText('候选加载失败');
|
|
152
|
-
await sheet.getByRole('button', { name: '重试', exact: true }).click();
|
|
153
|
-
await expect(sheet.getByText('测量服务', { exact: true }).last()).toBeVisible();
|
|
154
|
-
await sheet.getByPlaceholder('搜索动态多选下拉').press('Enter');
|
|
155
|
-
await expect(page.locator('[data-saved-values]')).toBeEmpty();
|
|
156
|
-
await sheet.getByRole('button', { name: '确定(2)', exact: true }).click();
|
|
157
|
-
expect((await submit(page))['动态多选下拉']).toEqual([reference('a', '检测设备'), reference('b', '测量服务')]);
|
|
158
|
-
await page.getByRole('button', { name: '重新载入已保存值' }).click();
|
|
159
|
-
await open(page, '动态多选下拉');
|
|
160
|
-
await sheet.getByRole('button', { name: '移除检测设备', exact: true }).click();
|
|
161
|
-
await sheet.getByRole('button', { name: '确定(1)', exact: true }).click();
|
|
162
|
-
expect((await submit(page))['动态多选下拉']).toEqual([reference('b', '测量服务')]);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('cascade drilldown and path search commit complete single/multiple snapshots', async ({ page }) => {
|
|
166
|
-
await page.goto('/field-protocol.e2e.html?mode=mobile');
|
|
167
|
-
await open(page, '级联多选');
|
|
168
|
-
const sheet = picker(page, '级联多选');
|
|
169
|
-
await sheet.getByRole('button', { name: '清空', exact: true }).click();
|
|
170
|
-
await sheet.getByRole('button', { name: '进入设备' }).click();
|
|
171
|
-
await sheet.getByText('光谱仪', { exact: true }).click();
|
|
172
|
-
await sheet.getByRole('button', { name: '首页', exact: true }).click();
|
|
173
|
-
await sheet.getByPlaceholder('搜索选项或路径').fill('服务');
|
|
174
|
-
await sheet.getByText('培训', { exact: true }).click();
|
|
175
|
-
await expect(sheet.getByLabel('已选项目')).toContainText('设备 / 光谱仪');
|
|
176
|
-
await expect(sheet.getByLabel('已选项目')).toContainText('服务 / 培训');
|
|
177
|
-
await page.screenshot({ path: 'test-results/mobile-cascade-picker.png' });
|
|
178
|
-
await sheet.getByRole('button', { name: '关闭', exact: true }).click();
|
|
179
|
-
await expect(field(page, '级联多选').getByRole('button', { name: '选择级联多选', exact: true })).toHaveText('设备 / 显微镜');
|
|
180
|
-
await open(page, '级联多选');
|
|
181
|
-
await sheet.getByRole('button', { name: '进入设备' }).click();
|
|
182
|
-
await sheet.getByText('光谱仪', { exact: true }).click();
|
|
183
|
-
await sheet.getByRole('button', { name: '确定(2)', exact: true }).click();
|
|
184
|
-
const expected = [
|
|
185
|
-
[{ label: '设备', value: 'equipment' }, { label: '显微镜', value: 'microscope' }],
|
|
186
|
-
[{ label: '设备', value: 'equipment' }, { label: '光谱仪', value: 'spectrometer' }],
|
|
187
|
-
];
|
|
188
|
-
expect((await submit(page))['级联多选']).toEqual(expected);
|
|
189
|
-
await open(page, '级联单选');
|
|
190
|
-
await picker(page, '级联单选').getByRole('button', { name: '首页', exact: true }).click();
|
|
191
|
-
await picker(page, '级联单选').getByRole('button', { name: '进入服务' }).click();
|
|
192
|
-
await picker(page, '级联单选').getByText('培训', { exact: true }).click();
|
|
193
|
-
await picker(page, '级联单选').getByRole('button', { name: '确定', exact: true }).click();
|
|
194
|
-
expect((await submit(page))['级联单选']).toEqual([{ label: '服务', value: 'service' }, { label: '培训', value: 'training' }]);
|
|
195
|
-
await page.getByRole('button', { name: '重新载入已保存值' }).click();
|
|
196
|
-
await expect(field(page, '级联单选').getByRole('button', { name: '选择级联单选', exact: true })).toHaveText('服务 / 培训');
|
|
197
|
-
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(0);
|
|
198
|
-
});
|
|
199
|
-
});
|