openxiangda-cli 2.0.0-alpha.99 → 2.0.1
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/LICENSE +21 -0
- package/README.md +4 -8
- package/bin/run.js +2 -12
- package/dist/base.d.ts +30 -6
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +119 -22
- package/dist/base.js.map +1 -1
- package/dist/commands/accept.d.ts +8 -3
- package/dist/commands/accept.d.ts.map +1 -1
- package/dist/commands/admin.d.ts +32 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/admin.js +27 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/auth.d.ts +30 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +13 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/cancel.d.ts +30 -0
- package/dist/commands/cancel.d.ts.map +1 -0
- package/dist/commands/cancel.js +12 -0
- package/dist/commands/cancel.js.map +1 -0
- package/dist/commands/check.d.ts +12 -3
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +21 -4
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/context.d.ts +27 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +10 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/create.d.ts +20 -3
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +396 -22
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/deploy.d.ts +13 -3
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +17 -16
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.d.ts +10 -3
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +7 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/docs.d.ts +30 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +20 -0
- package/dist/commands/docs.js.map +1 -0
- package/dist/commands/login.d.ts +8 -3
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logs.d.ts +10 -3
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +7 -3
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/retry.d.ts +30 -0
- package/dist/commands/retry.d.ts.map +1 -0
- package/dist/commands/retry.js +12 -0
- package/dist/commands/retry.js.map +1 -0
- package/dist/commands/rollback.d.ts +11 -3
- package/dist/commands/rollback.d.ts.map +1 -1
- package/dist/commands/rollback.js +9 -3
- package/dist/commands/rollback.js.map +1 -1
- package/dist/commands/skill.d.ts +9 -3
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +9 -2
- package/dist/commands/skill.js.map +1 -1
- package/dist/commands/spec.d.ts +42 -0
- package/dist/commands/spec.d.ts.map +1 -0
- package/dist/commands/spec.js +123 -0
- package/dist/commands/spec.js.map +1 -0
- package/dist/commands/start.d.ts +28 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +18 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/status.d.ts +9 -3
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +10 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/stop.d.ts +28 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +18 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/create-workspace.d.ts +36 -0
- package/dist/create-workspace.d.ts.map +1 -1
- package/dist/create-workspace.js +366 -8
- package/dist/create-workspace.js.map +1 -1
- package/dist/studio-events.d.ts +9 -0
- package/dist/studio-events.d.ts.map +1 -0
- package/dist/studio-events.js +30 -0
- package/dist/studio-events.js.map +1 -0
- package/package.json +15 -6
- package/template/AGENTS.md +25 -21
- package/template/README.md +31 -12
- package/template/apps/web/e2e/README.md +7 -0
- package/template/apps/web/package.json +4 -2
- package/template/apps/web/playwright.config.ts +4 -3
- package/template/apps/web/src/document.css +5 -0
- package/template/apps/web/src/main.tsx +48 -3
- package/template/apps/web/src/user-end/tailwind.css +5 -0
- package/template/apps/web/test/loopback.test.ts +8 -2
- package/template/apps/web/test/standard-user-surfaces.typecheck.tsx +27 -0
- package/template/apps/web/vite.config.ts +5 -1
- package/template/appspec/app.md +56 -0
- package/template/openxiangda.config.ts +50 -25
- package/template/package.json +4 -4
- package/template/packages/contracts/package.json +1 -1
- package/template/packages/contracts/src/generated.ts +143 -2
- package/template/apps/server/Dockerfile +0 -20
- package/template/apps/server/package.json +0 -27
- package/template/apps/server/src/app.module.ts +0 -43
- package/template/apps/server/src/main.ts +0 -5
- package/template/apps/server/test/smoke.test.ts +0 -16
- package/template/apps/server/tsconfig.json +0 -9
- 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/data-api-live-fixture.tsx +0 -146
- package/template/apps/web/e2e/data-api-live.spec.ts +0 -138
- package/template/apps/web/e2e/field-protocol-fixture.tsx +0 -347
- package/template/apps/web/e2e/field-protocol.spec.ts +0 -186
- package/template/apps/web/e2e/resources.spec.ts +0 -285
- package/template/apps/web/e2e/workflow.spec.ts +0 -329
- package/template/apps/web/field-protocol.e2e.html +0 -15
- package/template/apps/web/scripts/check.mjs +0 -43
- package/template/apps/web/test/contracts.test.ts +0 -99
- package/template/scripts/verify-template-budget.mjs +0 -49
|
@@ -1,285 +0,0 @@
|
|
|
1
|
-
import { expect, test, type Page } from '@playwright/test';
|
|
2
|
-
import {
|
|
3
|
-
appCode,
|
|
4
|
-
capabilities,
|
|
5
|
-
resourceCodes,
|
|
6
|
-
resourceDefinitions,
|
|
7
|
-
} from '../../../packages/contracts/src/generated.js';
|
|
8
|
-
|
|
9
|
-
type DeclaredField = { label: string; widget?: string };
|
|
10
|
-
type DeclaredResource = {
|
|
11
|
-
name: string;
|
|
12
|
-
surface: {
|
|
13
|
-
fields: Record<string, DeclaredField>;
|
|
14
|
-
list?: { filterFields?: readonly string[] };
|
|
15
|
-
mobile?: { enabled?: boolean };
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const codes = [...resourceCodes] as string[];
|
|
20
|
-
const definitions = resourceDefinitions as Record<string, DeclaredResource>;
|
|
21
|
-
const allCapabilities = [...capabilities] as string[];
|
|
22
|
-
const runtimeBase = `/view/${appCode}`;
|
|
23
|
-
|
|
24
|
-
function runtimeAuthorization(authorized: boolean) {
|
|
25
|
-
return {
|
|
26
|
-
schemaVersion: 'openxiangda.runtime-authorization/v2',
|
|
27
|
-
state: 'active',
|
|
28
|
-
environment: {
|
|
29
|
-
id: 'preproduction-id',
|
|
30
|
-
key: 'preproduction',
|
|
31
|
-
activeAppVersionId: 'app-version-1',
|
|
32
|
-
headRevision: 1,
|
|
33
|
-
authzRevisionId: 'authz-1',
|
|
34
|
-
authzVersion: 1,
|
|
35
|
-
scopeDataVersion: 'scope-1',
|
|
36
|
-
},
|
|
37
|
-
principal: {
|
|
38
|
-
type: 'user_union',
|
|
39
|
-
userId: 'acceptance-user',
|
|
40
|
-
roleCodes: ['college_admin', 'instrument_admin'],
|
|
41
|
-
capabilityCodes: authorized ? allCapabilities : [],
|
|
42
|
-
isAppSuperAdmin: false,
|
|
43
|
-
identityScope: 'user-union:preproduction-id:acceptance-user:authz-1',
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function mockPlatform(
|
|
49
|
-
page: Page,
|
|
50
|
-
authorized = true,
|
|
51
|
-
targetRuntimeBase = runtimeBase,
|
|
52
|
-
) {
|
|
53
|
-
let contextReads = 0;
|
|
54
|
-
const observed: Array<{ path: string; roleSessionId: string }> = [];
|
|
55
|
-
await page.route(`**${targetRuntimeBase}/**`, async route => {
|
|
56
|
-
if (route.request().resourceType() !== 'document') return route.continue();
|
|
57
|
-
const response = await route.fetch();
|
|
58
|
-
const body = (await response.text()).replace(
|
|
59
|
-
'<head>',
|
|
60
|
-
`<head><meta name="openxiangda-runtime-base" content="${targetRuntimeBase}/" />` +
|
|
61
|
-
`<meta name="openxiangda-app-code" content="${appCode}" />` +
|
|
62
|
-
'<meta name="openxiangda-environment" content="preproduction" />'
|
|
63
|
-
);
|
|
64
|
-
await route.fulfill({ response, body });
|
|
65
|
-
});
|
|
66
|
-
await page.route('**/service/**', async route => {
|
|
67
|
-
const url = new URL(route.request().url());
|
|
68
|
-
const request = route.request();
|
|
69
|
-
const roleSessionId = request.headers()['x-openxiangda-role-session-id'] || '';
|
|
70
|
-
if (url.pathname.endsWith('/native/authz/current')) {
|
|
71
|
-
contextReads += 1;
|
|
72
|
-
return route.fulfill({
|
|
73
|
-
contentType: 'application/json',
|
|
74
|
-
body: JSON.stringify({
|
|
75
|
-
code: 200,
|
|
76
|
-
data: runtimeAuthorization(authorized),
|
|
77
|
-
}),
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
const sourceQuery = url.pathname.match(
|
|
81
|
-
/\/native\/data-resources\/([^/]+)\/fields\/([^/]+)\/source\/query$/
|
|
82
|
-
);
|
|
83
|
-
if (sourceQuery) {
|
|
84
|
-
observed.push({ path: url.pathname, roleSessionId });
|
|
85
|
-
return route.fulfill({
|
|
86
|
-
contentType: 'application/json',
|
|
87
|
-
body: JSON.stringify({
|
|
88
|
-
code: 200,
|
|
89
|
-
data: {
|
|
90
|
-
schemaVersion: 'openxiangda.data-field-source-page/v2',
|
|
91
|
-
resourceCode: decodeURIComponent(sourceQuery[1]),
|
|
92
|
-
fieldCode: decodeURIComponent(sourceQuery[2]),
|
|
93
|
-
items: [],
|
|
94
|
-
nextCursor: null,
|
|
95
|
-
},
|
|
96
|
-
}),
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
const exportRequest = url.pathname.match(
|
|
100
|
-
/\/native\/data\/([^/]+)\/export$/
|
|
101
|
-
);
|
|
102
|
-
if (exportRequest) {
|
|
103
|
-
observed.push({ path: url.pathname, roleSessionId });
|
|
104
|
-
return route.fulfill({
|
|
105
|
-
status: 200,
|
|
106
|
-
contentType: 'text/csv; charset=utf-8',
|
|
107
|
-
headers: {
|
|
108
|
-
'content-disposition': `attachment; filename="${decodeURIComponent(
|
|
109
|
-
exportRequest[1]
|
|
110
|
-
)}.csv"`,
|
|
111
|
-
},
|
|
112
|
-
body: 'id,name\nacceptance-1,OpenXiangda\n',
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
|
|
116
|
-
if (query) {
|
|
117
|
-
observed.push({ path: url.pathname, roleSessionId });
|
|
118
|
-
return route.fulfill({
|
|
119
|
-
contentType: 'application/json',
|
|
120
|
-
body: JSON.stringify({
|
|
121
|
-
code: 200,
|
|
122
|
-
data: {
|
|
123
|
-
schemaVersion: 'openxiangda.native-data-page/v2',
|
|
124
|
-
resourceCode: decodeURIComponent(query[1]),
|
|
125
|
-
appVersionId: 'app-version-1',
|
|
126
|
-
environmentHeadRevision: 1,
|
|
127
|
-
items: [],
|
|
128
|
-
total: 0,
|
|
129
|
-
limit: 500,
|
|
130
|
-
offset: 0,
|
|
131
|
-
},
|
|
132
|
-
}),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
if (url.pathname.includes('/directory/')) {
|
|
136
|
-
observed.push({ path: url.pathname, roleSessionId });
|
|
137
|
-
return route.fulfill({
|
|
138
|
-
contentType: 'application/json',
|
|
139
|
-
body: JSON.stringify({ code: 200, data: { items: [], nextCursor: null } }),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
if (url.pathname.includes('/openxiangda-app-api/v2/')) {
|
|
143
|
-
observed.push({ path: url.pathname, roleSessionId });
|
|
144
|
-
return route.fulfill({
|
|
145
|
-
contentType: 'application/json',
|
|
146
|
-
body: JSON.stringify({ code: 200, data: { status: 'ok' } }),
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
return route.fulfill({
|
|
150
|
-
status: 404,
|
|
151
|
-
contentType: 'application/json',
|
|
152
|
-
body: JSON.stringify({ code: 404, message: 'not mocked' }),
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
return {
|
|
156
|
-
observed,
|
|
157
|
-
get contextReads() {
|
|
158
|
-
return contextReads;
|
|
159
|
-
},
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
test('renders the compiled desktop resource contract and shared workbench', async ({ page }) => {
|
|
164
|
-
await mockPlatform(page);
|
|
165
|
-
if (!codes.length) {
|
|
166
|
-
await page.goto(`${runtimeBase}/`);
|
|
167
|
-
await expect(page.getByText('还没有声明数据资源')).toBeVisible();
|
|
168
|
-
} else {
|
|
169
|
-
const code = codes[0];
|
|
170
|
-
const definition = definitions[code];
|
|
171
|
-
await page.goto(`${runtimeBase}/admin`);
|
|
172
|
-
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
173
|
-
await expect(page).toHaveURL(`${runtimeBase}/${code}`);
|
|
174
|
-
await expect(page.locator('.oxa-sider').getByText(definition.name, { exact: true })).toBeVisible();
|
|
175
|
-
await expect(page.getByRole('button', { name: /导出$/ })).toBeVisible();
|
|
176
|
-
await expect(page.getByRole('button', { name: /列设置$/ })).toBeVisible();
|
|
177
|
-
await expect(page.getByRole('button', { name: /密度$/ })).toBeVisible();
|
|
178
|
-
if ((definition.surface.list?.filterFields?.length || 0) > 2) {
|
|
179
|
-
const more = page.getByRole('button', { name: /更多筛选/ });
|
|
180
|
-
await expect(more).toBeVisible();
|
|
181
|
-
await more.click();
|
|
182
|
-
await expect(page.getByRole('button', { name: '收起筛选' })).toBeVisible();
|
|
183
|
-
}
|
|
184
|
-
const downloadPromise = page.waitForEvent('download');
|
|
185
|
-
await page.getByRole('button', { name: /导出$/ }).click();
|
|
186
|
-
const download = await downloadPromise;
|
|
187
|
-
expect(download.suggestedFilename()).toMatch(/\.csv$/);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
await expect(page.locator('.oxa-topbar')).toHaveCSS('height', '52px');
|
|
191
|
-
await expect(page.getByText('仪器' + '资源管理')).toHaveCount(0);
|
|
192
|
-
await page.locator('.oxa-current-user').click();
|
|
193
|
-
await expect(page.getByText('跟随系统', { exact: true })).toBeVisible();
|
|
194
|
-
await page.getByText('深色', { exact: true }).click();
|
|
195
|
-
await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'dark');
|
|
196
|
-
await expect.poll(() => page.evaluate(() => localStorage.getItem('openxiangda.admin.appearance'))).toBe('dark');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('opens the stable admin entry under the preproduction gateway mount', async ({ page }) => {
|
|
200
|
-
test.skip(!codes.length, 'No resource route exists in an empty application.');
|
|
201
|
-
const devRuntimeBase = `/dev/${appCode}`;
|
|
202
|
-
await mockPlatform(page, true, devRuntimeBase);
|
|
203
|
-
await page.goto(`${devRuntimeBase}/admin`);
|
|
204
|
-
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
205
|
-
await expect(page).toHaveURL(`${devRuntimeBase}/${codes[0]}`);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
test('uses the current user role union without exposing a RoleSession header', async ({ page }) => {
|
|
209
|
-
const platform = await mockPlatform(page);
|
|
210
|
-
await page.goto(`${runtimeBase}/admin`);
|
|
211
|
-
if (codes.length) {
|
|
212
|
-
await expect(page.locator('.oxa-list-surface')).toBeVisible();
|
|
213
|
-
} else {
|
|
214
|
-
await expect(page.getByText('还没有声明数据资源')).toBeVisible();
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
await page.evaluate(async () => {
|
|
218
|
-
const client = await import('/e2e/client-fixture.ts');
|
|
219
|
-
await client.searchResource('acceptance-resource', 'name', {
|
|
220
|
-
keyword: '显微镜',
|
|
221
|
-
});
|
|
222
|
-
await client.searchDirectory('user', { keyword: '老师' });
|
|
223
|
-
await client.requestApplicationApi('/health/current-user');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
const protectedRequests = platform.observed.filter(item =>
|
|
227
|
-
/\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
|
|
228
|
-
item.path
|
|
229
|
-
)
|
|
230
|
-
);
|
|
231
|
-
expect(protectedRequests.length).toBeGreaterThanOrEqual(3);
|
|
232
|
-
expect(protectedRequests.every(item => item.roleSessionId === '')).toBe(true);
|
|
233
|
-
expect(platform.contextReads).toBe(1);
|
|
234
|
-
await page.locator('.oxa-current-user').click();
|
|
235
|
-
await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
|
|
236
|
-
'college_admin、instrument_admin'
|
|
237
|
-
);
|
|
238
|
-
await expect(page.getByText('切换应用角色', { exact: true })).toHaveCount(0);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test('renders platform mobile controls from declared field widgets', async ({ page }) => {
|
|
242
|
-
const candidate = codes
|
|
243
|
-
.map(code => ({ code, definition: definitions[code] }))
|
|
244
|
-
.find(item =>
|
|
245
|
-
item.definition.surface.mobile?.enabled &&
|
|
246
|
-
Object.values(item.definition.surface.fields).some(field =>
|
|
247
|
-
['directory-user', 'directory-department', 'resource', 'file'].includes(field.widget || '')
|
|
248
|
-
)
|
|
249
|
-
);
|
|
250
|
-
test.skip(!candidate, 'No mobile platform field is declared.');
|
|
251
|
-
await mockPlatform(page);
|
|
252
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
253
|
-
await page.goto(`${runtimeBase}/m/${candidate!.code}/new`);
|
|
254
|
-
await expect(page.locator('.oxa-mobile-form')).toBeVisible();
|
|
255
|
-
|
|
256
|
-
const widgets = new Set(Object.values(candidate!.definition.surface.fields).map(field => field.widget));
|
|
257
|
-
if (widgets.has('directory-user') || widgets.has('directory-department')) {
|
|
258
|
-
const trigger = page.locator('.oxa-directory-trigger').first();
|
|
259
|
-
await expect(trigger).toBeVisible();
|
|
260
|
-
await trigger.click();
|
|
261
|
-
await expect(page.locator('.oxa-directory-mobile-drawer')).toBeVisible();
|
|
262
|
-
await page.keyboard.press('Escape');
|
|
263
|
-
}
|
|
264
|
-
if (widgets.has('resource')) {
|
|
265
|
-
const trigger = page.locator('.oxa-mobile-reference-trigger').first();
|
|
266
|
-
await expect(trigger).toBeVisible();
|
|
267
|
-
await trigger.click();
|
|
268
|
-
await expect(page.locator('.oxa-mobile-reference-sheet')).toBeVisible();
|
|
269
|
-
await page.keyboard.press('Escape');
|
|
270
|
-
}
|
|
271
|
-
if (widgets.has('file')) {
|
|
272
|
-
await expect(page.locator('.oxa-mobile-file-field')).toBeVisible();
|
|
273
|
-
await expect(page.locator('.oxa-mobile-file-field input[type="file"]')).toBeHidden();
|
|
274
|
-
await expect(page.getByRole('button', { name: '上传文件' })).toBeVisible();
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
test('fails closed when the current user lacks the declared read capability', async ({ page }) => {
|
|
279
|
-
test.skip(!codes.length, 'No resource permission exists in an empty application.');
|
|
280
|
-
await mockPlatform(page, false);
|
|
281
|
-
const code = codes[0];
|
|
282
|
-
await page.goto(`${runtimeBase}/${code}`);
|
|
283
|
-
await expect(page.getByText(new RegExp(`当前平台用户无${definitions[code].name}页面权限`))).toBeVisible();
|
|
284
|
-
await expect(page.locator('.oxa-list-surface')).toHaveCount(0);
|
|
285
|
-
});
|
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
import { expect, test, type Page } from '@playwright/test';
|
|
2
|
-
import { appCode } from '../../../packages/contracts/src/generated.js';
|
|
3
|
-
|
|
4
|
-
const runtimeBase = `/view/${appCode}`;
|
|
5
|
-
const taskId = '11111111-1111-4111-8111-111111111111';
|
|
6
|
-
const instanceId = '22222222-2222-4222-8222-222222222222';
|
|
7
|
-
function envelope(data: unknown, code = 200) {
|
|
8
|
-
return { code, message: code === 200 ? 'success' : 'forbidden', data };
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function task(completed = false) {
|
|
12
|
-
return {
|
|
13
|
-
schemaVersion: 'openxiangda.workflow-task/v2',
|
|
14
|
-
engineVersion: '2.0',
|
|
15
|
-
id: taskId,
|
|
16
|
-
instanceId,
|
|
17
|
-
workflowCode: 'purchase-approval',
|
|
18
|
-
businessKey: 'PO-2026-0825',
|
|
19
|
-
nodeId: 'manager-review',
|
|
20
|
-
title: '采购申请审批',
|
|
21
|
-
status: completed ? 'completed' : 'assigned',
|
|
22
|
-
approvalMode: 'single',
|
|
23
|
-
taskKind: 'normal',
|
|
24
|
-
assignedRoleSubjectKey: 'membership:manager',
|
|
25
|
-
activeParticipantId: 'participant-1',
|
|
26
|
-
participants: [],
|
|
27
|
-
originTaskId: null,
|
|
28
|
-
returnSessionId: null,
|
|
29
|
-
version: completed ? 2 : 1,
|
|
30
|
-
dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
|
|
31
|
-
createdAt: '2026-08-25T01:00:00.000Z',
|
|
32
|
-
updatedAt: '2026-08-25T01:10:00.000Z',
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function surface(completed = false) {
|
|
37
|
-
return {
|
|
38
|
-
schemaVersion: 'openxiangda.workflow-surface/v2',
|
|
39
|
-
protocolVersion: 'workflow_surface_v2',
|
|
40
|
-
surfaceRevision: completed ? 'revision-2' : 'revision-1',
|
|
41
|
-
engineVersion: '2.0',
|
|
42
|
-
instanceSequence: completed ? 4 : 3,
|
|
43
|
-
navigationTarget: {
|
|
44
|
-
kind: 'resource_record',
|
|
45
|
-
appCode,
|
|
46
|
-
environmentKey: 'preproduction',
|
|
47
|
-
resourceCode: 'purchase-orders',
|
|
48
|
-
recordId: 'purchase-1',
|
|
49
|
-
desktopPath: '/purchase-orders/purchase-1',
|
|
50
|
-
mobilePath: '/m/purchase-orders/purchase-1',
|
|
51
|
-
},
|
|
52
|
-
instance: {
|
|
53
|
-
schemaVersion: 'openxiangda.workflow-instance/v2',
|
|
54
|
-
engineVersion: '2.0',
|
|
55
|
-
id: instanceId,
|
|
56
|
-
appCode,
|
|
57
|
-
environmentId: 'preproduction-id',
|
|
58
|
-
environmentKey: 'preproduction',
|
|
59
|
-
workflowCode: 'purchase-approval',
|
|
60
|
-
definitionVersion: 1,
|
|
61
|
-
bindingVersion: 1,
|
|
62
|
-
businessKey: 'PO-2026-0825',
|
|
63
|
-
generation: 1,
|
|
64
|
-
status: completed ? 'approved' : 'running',
|
|
65
|
-
initiatorUserId: 'applicant-user',
|
|
66
|
-
initiatorAuthorizationDigest: null,
|
|
67
|
-
dataRef: { resourceCode: 'purchase-orders', recordId: 'purchase-1' },
|
|
68
|
-
dataRevision: '8',
|
|
69
|
-
currentNodeId: completed ? null : 'manager-review',
|
|
70
|
-
outcome: completed ? 'approved' : null,
|
|
71
|
-
version: completed ? 3 : 2,
|
|
72
|
-
eventSequence: completed ? 4 : 3,
|
|
73
|
-
startedAt: '2026-08-25T01:00:00.000Z',
|
|
74
|
-
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
75
|
-
},
|
|
76
|
-
task: completed ? null : task(false),
|
|
77
|
-
presentation: {
|
|
78
|
-
status: { label: completed ? '已同意' : '待审批', tone: 'warning' },
|
|
79
|
-
layout: 'approval_detail',
|
|
80
|
-
businessData: {
|
|
81
|
-
申请金额: '¥28,600',
|
|
82
|
-
采购类型: '办公设备',
|
|
83
|
-
期望到货: '2026-09-05',
|
|
84
|
-
申请人: '张三',
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
fieldPolicy: { default: 'readonly', fields: {} },
|
|
88
|
-
operations: completed
|
|
89
|
-
? []
|
|
90
|
-
: [
|
|
91
|
-
{
|
|
92
|
-
key: 'approve',
|
|
93
|
-
kind: 'workflow_command',
|
|
94
|
-
label: '同意',
|
|
95
|
-
placement: 'primary',
|
|
96
|
-
tone: 'primary',
|
|
97
|
-
visible: true,
|
|
98
|
-
enabled: true,
|
|
99
|
-
inputSchema: {
|
|
100
|
-
type: 'object',
|
|
101
|
-
properties: {
|
|
102
|
-
comment: {
|
|
103
|
-
type: 'string',
|
|
104
|
-
title: '审批意见',
|
|
105
|
-
format: 'textarea',
|
|
106
|
-
maxLength: 500,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
uiSchema: {
|
|
111
|
-
confirmText: '确认同意',
|
|
112
|
-
confirmation: { description: '提交后流程继续流转。' },
|
|
113
|
-
},
|
|
114
|
-
execute: {
|
|
115
|
-
method: 'POST',
|
|
116
|
-
href: '/workflow/approve',
|
|
117
|
-
idempotencyRequired: true,
|
|
118
|
-
},
|
|
119
|
-
refresh: ['surface', 'timeline', 'work_center'],
|
|
120
|
-
},
|
|
121
|
-
...['reject', 'return', 'transfer', 'add_assignee'].map((key) => ({
|
|
122
|
-
key,
|
|
123
|
-
kind: 'workflow_command',
|
|
124
|
-
label: {
|
|
125
|
-
reject: '拒绝',
|
|
126
|
-
return: '退回',
|
|
127
|
-
transfer: '转交',
|
|
128
|
-
add_assignee: '加签',
|
|
129
|
-
}[key],
|
|
130
|
-
placement: 'secondary',
|
|
131
|
-
tone: key === 'reject' ? 'danger' : 'neutral',
|
|
132
|
-
visible: true,
|
|
133
|
-
enabled: true,
|
|
134
|
-
inputSchema: { type: 'object', properties: {} },
|
|
135
|
-
uiSchema: { confirmText: `确认${key}` },
|
|
136
|
-
execute: {
|
|
137
|
-
method: 'POST',
|
|
138
|
-
href: `/workflow/${key}`,
|
|
139
|
-
idempotencyRequired: true,
|
|
140
|
-
},
|
|
141
|
-
refresh: ['surface', 'timeline', 'work_center'],
|
|
142
|
-
})),
|
|
143
|
-
],
|
|
144
|
-
extensions: {},
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function timeline(completed = false) {
|
|
149
|
-
return {
|
|
150
|
-
engineVersion: '2.0',
|
|
151
|
-
instanceId,
|
|
152
|
-
flow: [
|
|
153
|
-
{
|
|
154
|
-
key: 'start',
|
|
155
|
-
nodeId: null,
|
|
156
|
-
kind: 'start',
|
|
157
|
-
title: '张三',
|
|
158
|
-
status: 'completed',
|
|
159
|
-
startedAt: '2026-08-25T01:00:00.000Z',
|
|
160
|
-
completedAt: '2026-08-25T01:00:00.000Z',
|
|
161
|
-
assignees: [],
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
key: 'manager',
|
|
165
|
-
nodeId: 'manager-review',
|
|
166
|
-
kind: 'approval',
|
|
167
|
-
title: '部门负责人',
|
|
168
|
-
status: completed ? 'completed' : 'active',
|
|
169
|
-
startedAt: '2026-08-25T01:01:00.000Z',
|
|
170
|
-
completedAt: completed ? '2026-08-25T01:12:00.000Z' : null,
|
|
171
|
-
assignees: [{ userId: 'acceptance-user', displayName: '李明' }],
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
key: 'finance',
|
|
175
|
-
nodeId: 'finance-review',
|
|
176
|
-
kind: 'approval',
|
|
177
|
-
title: '财务负责人',
|
|
178
|
-
status: completed ? 'completed' : 'waiting',
|
|
179
|
-
startedAt: null,
|
|
180
|
-
completedAt: null,
|
|
181
|
-
assignees: [{ userId: 'finance-user', displayName: '财务负责人' }],
|
|
182
|
-
},
|
|
183
|
-
],
|
|
184
|
-
items: [],
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async function mockWorkflow(page: Page, options: { forbidden?: boolean } = {}) {
|
|
189
|
-
let completed = false;
|
|
190
|
-
await page.route(`**${runtimeBase}/**`, async (route) => {
|
|
191
|
-
if (route.request().resourceType() !== 'document') return route.continue();
|
|
192
|
-
const response = await route.fetch();
|
|
193
|
-
const body = (await response.text()).replace(
|
|
194
|
-
'<head>',
|
|
195
|
-
`<head><meta name="openxiangda-runtime-base" content="${runtimeBase}/" />` +
|
|
196
|
-
`<meta name="openxiangda-app-code" content="${appCode}" />` +
|
|
197
|
-
'<meta name="openxiangda-environment" content="preproduction" />',
|
|
198
|
-
);
|
|
199
|
-
await route.fulfill({ response, body });
|
|
200
|
-
});
|
|
201
|
-
await page.route('**/service/**', async (route) => {
|
|
202
|
-
const url = new URL(route.request().url());
|
|
203
|
-
const path = url.pathname;
|
|
204
|
-
if (path.endsWith('/native/authz/current')) {
|
|
205
|
-
return route.fulfill({
|
|
206
|
-
contentType: 'application/json',
|
|
207
|
-
body: JSON.stringify(
|
|
208
|
-
envelope({
|
|
209
|
-
schemaVersion: 'openxiangda.runtime-authorization/v2',
|
|
210
|
-
state: 'active',
|
|
211
|
-
environment: {
|
|
212
|
-
id: 'preproduction-id',
|
|
213
|
-
key: 'preproduction',
|
|
214
|
-
activeAppVersionId: 'version-1',
|
|
215
|
-
headRevision: 1,
|
|
216
|
-
authzRevisionId: 'authz-1',
|
|
217
|
-
authzVersion: 1,
|
|
218
|
-
scopeDataVersion: 'scope-1',
|
|
219
|
-
},
|
|
220
|
-
principal: {
|
|
221
|
-
type: 'user_union',
|
|
222
|
-
userId: 'acceptance-user',
|
|
223
|
-
roleCodes: ['department_manager'],
|
|
224
|
-
capabilityCodes: [],
|
|
225
|
-
isAppSuperAdmin: false,
|
|
226
|
-
identityScope:
|
|
227
|
-
'user-union:preproduction-id:acceptance-user:authz-1',
|
|
228
|
-
},
|
|
229
|
-
}),
|
|
230
|
-
),
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
if (path.includes('/directory/')) {
|
|
234
|
-
return route.fulfill({
|
|
235
|
-
contentType: 'application/json',
|
|
236
|
-
body: JSON.stringify(envelope({ items: [], nextCursor: null })),
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
if (options.forbidden && path.includes('/workflow/')) {
|
|
240
|
-
return route.fulfill({
|
|
241
|
-
status: 403,
|
|
242
|
-
contentType: 'application/json',
|
|
243
|
-
body: JSON.stringify(envelope(null, 403)),
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
if (path.endsWith('/workflow/work-center/items')) {
|
|
247
|
-
return route.fulfill({
|
|
248
|
-
contentType: 'application/json',
|
|
249
|
-
body: JSON.stringify(
|
|
250
|
-
envelope({
|
|
251
|
-
engineVersion: '2.0',
|
|
252
|
-
authorizationMode: 'current_user_role_union',
|
|
253
|
-
authorizationDigest: 'digest-1',
|
|
254
|
-
status: 'pending',
|
|
255
|
-
total: 1,
|
|
256
|
-
limit: 20,
|
|
257
|
-
offset: 0,
|
|
258
|
-
items: [task(completed)],
|
|
259
|
-
}),
|
|
260
|
-
),
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
if (path.endsWith(`/workflow/tasks/${taskId}/surface`)) {
|
|
264
|
-
return route.fulfill({
|
|
265
|
-
contentType: 'application/json',
|
|
266
|
-
body: JSON.stringify(envelope(surface(completed))),
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
if (path.endsWith(`/workflow/instances/${instanceId}/timeline`)) {
|
|
270
|
-
return route.fulfill({
|
|
271
|
-
contentType: 'application/json',
|
|
272
|
-
body: JSON.stringify(envelope(timeline(completed))),
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
if (path.endsWith(`/workflow/tasks/${taskId}/commands/approve`)) {
|
|
276
|
-
completed = true;
|
|
277
|
-
return route.fulfill({
|
|
278
|
-
contentType: 'application/json',
|
|
279
|
-
body: JSON.stringify(envelope({ status: 'completed' })),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
return route.fulfill({
|
|
283
|
-
status: 404,
|
|
284
|
-
contentType: 'application/json',
|
|
285
|
-
body: JSON.stringify(envelope(null, 404)),
|
|
286
|
-
});
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
test('renders a paged desktop work center from the current-user role union', async ({
|
|
291
|
-
page,
|
|
292
|
-
}) => {
|
|
293
|
-
await mockWorkflow(page);
|
|
294
|
-
await page.goto(`${runtimeBase}/admin/work-center`);
|
|
295
|
-
await expect(page.getByRole('heading', { name: '审批工作台' })).toBeVisible();
|
|
296
|
-
await expect(page.getByRole('link', { name: '采购申请审批' })).toBeVisible();
|
|
297
|
-
await expect(page.getByText('PO-2026-0825')).toBeVisible();
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test('renders the independent mobile task and executes only a Surface operation', async ({
|
|
301
|
-
page,
|
|
302
|
-
}) => {
|
|
303
|
-
await mockWorkflow(page);
|
|
304
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
305
|
-
await page.goto(`${runtimeBase}/m/tasks/${taskId}`);
|
|
306
|
-
await expect(page.locator('.oxa-workflow-mobile-page')).toBeVisible();
|
|
307
|
-
await expect(page.getByText('¥28,600')).toBeVisible();
|
|
308
|
-
await expect(page.getByText('财务负责人').first()).toBeVisible();
|
|
309
|
-
await expect(page.getByRole('button', { name: /同\s*意/ })).toBeVisible();
|
|
310
|
-
await page.getByRole('button', { name: /同\s*意/ }).click();
|
|
311
|
-
await page.getByLabel('审批意见').fill('同意采购');
|
|
312
|
-
await page.getByRole('button', { name: '确认同意' }).click();
|
|
313
|
-
await expect(page.getByText('同意已提交')).toBeVisible();
|
|
314
|
-
await expect(page.getByRole('button', { name: /同\s*意/ })).toHaveCount(0);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test('shows an explicit authorization error instead of an empty work center', async ({
|
|
318
|
-
page,
|
|
319
|
-
}) => {
|
|
320
|
-
test.setTimeout(45_000);
|
|
321
|
-
await mockWorkflow(page, { forbidden: true });
|
|
322
|
-
await page.goto(`${runtimeBase}/admin/work-center`, {
|
|
323
|
-
waitUntil: 'domcontentloaded',
|
|
324
|
-
});
|
|
325
|
-
await expect(page.getByRole('alert')).toContainText('HTTP_403: forbidden', {
|
|
326
|
-
timeout: 30_000,
|
|
327
|
-
});
|
|
328
|
-
await expect(page.getByRole('link', { name: '采购申请审批' })).toHaveCount(0);
|
|
329
|
-
});
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<meta name="openxiangda-runtime-base" content="/" />
|
|
7
|
-
<meta name="openxiangda-app-code" content="field-protocol-fixture" />
|
|
8
|
-
<meta name="openxiangda-environment" content="preproduction" />
|
|
9
|
-
<title>Field protocol acceptance</title>
|
|
10
|
-
</head>
|
|
11
|
-
<body>
|
|
12
|
-
<div id="root"></div>
|
|
13
|
-
<script type="module" src="/e2e/field-protocol-fixture.tsx"></script>
|
|
14
|
-
</body>
|
|
15
|
-
</html>
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { join, resolve } from 'node:path';
|
|
3
|
-
|
|
4
|
-
const root = resolve(import.meta.dirname, '..');
|
|
5
|
-
const files = [];
|
|
6
|
-
function visit(directory) {
|
|
7
|
-
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
8
|
-
const path = join(directory, entry.name);
|
|
9
|
-
if (entry.isDirectory() && !['.umi', '.umi-production', '.turbopack'].includes(entry.name)) visit(path);
|
|
10
|
-
else if (/\.(?:ts|tsx)$/.test(entry.name)) files.push(path);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
visit(join(root, 'src'));
|
|
14
|
-
const lines = files.reduce((total, file) => total + readFileSync(file, 'utf8').split(/\r?\n/).length, 0);
|
|
15
|
-
// The shared Surface renderer is deliberately counted as application source;
|
|
16
|
-
// keep its small budget explicit instead of hiding it in generated output.
|
|
17
|
-
// Field Kit families are separate modules so a semantic field does not add a
|
|
18
|
-
// branch to one universal renderer. Keep explicit ceilings while allowing the
|
|
19
|
-
// complete 30-field standard kit to remain decomposed.
|
|
20
|
-
if (files.length > 36) throw new Error(`WEB_SOURCE_FILE_BUDGET_EXCEEDED:${files.length}>36`);
|
|
21
|
-
if (lines > 10_000) throw new Error(`WEB_BUSINESS_LOC_BUDGET_EXCEEDED:${lines}>10000`);
|
|
22
|
-
const source = files.map((file) => readFileSync(file, 'utf8')).join('\n');
|
|
23
|
-
for (const marker of [
|
|
24
|
-
'@umijs/',
|
|
25
|
-
'openxiangda-' + 'admin',
|
|
26
|
-
'openxiangda-' + 'user',
|
|
27
|
-
'instrument_query',
|
|
28
|
-
'instrument_save',
|
|
29
|
-
'function.invoke',
|
|
30
|
-
'authorization' + '-mode',
|
|
31
|
-
'focused' + '_role',
|
|
32
|
-
'/native/current' + '-user',
|
|
33
|
-
'college-' + 'am',
|
|
34
|
-
'user-' + 'wang',
|
|
35
|
-
'dept-' + 'am-test',
|
|
36
|
-
]) {
|
|
37
|
-
if (source.includes(marker)) throw new Error(`WEB_FORBIDDEN_MARKER:${marker}`);
|
|
38
|
-
}
|
|
39
|
-
const playwright = readFileSync(join(root, 'playwright.config.ts'), 'utf8');
|
|
40
|
-
if (!playwright.includes('reuseExistingServer: false')) {
|
|
41
|
-
throw new Error('WEB_E2E_MUST_NOT_REUSE_UNKNOWN_SERVER');
|
|
42
|
-
}
|
|
43
|
-
process.stdout.write(`Verified Vite/Refine web source: ${files.length} files, ${lines} LOC.\n`);
|