openxiangda-cli 2.0.0-alpha.75 → 2.0.0-alpha.76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openxiangda-cli",
3
- "version": "2.0.0-alpha.75",
3
+ "version": "2.0.0-alpha.76",
4
4
  "description": "Thin application-level CLI for OpenXiangda 2.0.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,31 +1,178 @@
1
- import { expect, test } from '@playwright/test';
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';
2
8
 
3
- test('clean scaffold renders the shared shell without business fixtures', async ({ page }) => {
4
- await page.route('**/view/clean-app/**', async route => {
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
+ async function mockPlatform(page: Page, authorized = true) {
25
+ await page.route(`**${runtimeBase}/**`, async route => {
5
26
  if (route.request().resourceType() !== 'document') return route.continue();
6
27
  const response = await route.fetch();
7
- const body = (await response.text()).replace('<head>', '<head><meta name="openxiangda-runtime-base" content="/view/clean-app/" /><meta name="openxiangda-app-code" content="clean-app" /><meta name="openxiangda-environment" content="preproduction" />');
28
+ const body = (await response.text()).replace(
29
+ '<head>',
30
+ `<head><meta name="openxiangda-runtime-base" content="${runtimeBase}/" />` +
31
+ `<meta name="openxiangda-app-code" content="${appCode}" />` +
32
+ '<meta name="openxiangda-environment" content="preproduction" />'
33
+ );
8
34
  await route.fulfill({ response, body });
9
35
  });
10
36
  await page.route('**/service/**', async route => {
11
37
  const url = new URL(route.request().url());
12
38
  if (url.pathname.endsWith('/native/current-user')) {
13
- return route.fulfill({ contentType: 'application/json', body: JSON.stringify({ code: 200, data: { schemaVersion: 'openxiangda.current-user/v2', appCode: 'clean-app', environment: { id: 'preproduction-id', key: 'preproduction', activeAppVersionId: 'app-version-1', headRevision: 1, authzRevisionId: 'authz-1', authzVersion: 1, scopeDataVersion: 'scope-1' }, principal: { type: 'user', userId: 'user-1', roleCodes: [], capabilityCodes: [], isAppSuperAdmin: false } } }) });
39
+ return route.fulfill({
40
+ contentType: 'application/json',
41
+ body: JSON.stringify({
42
+ code: 200,
43
+ data: {
44
+ schemaVersion: 'openxiangda.current-user/v2',
45
+ appCode,
46
+ environment: {
47
+ id: 'preproduction-id',
48
+ key: 'preproduction',
49
+ activeAppVersionId: 'app-version-1',
50
+ headRevision: 1,
51
+ authzRevisionId: 'authz-1',
52
+ authzVersion: 1,
53
+ scopeDataVersion: 'scope-1',
54
+ },
55
+ principal: {
56
+ type: 'user',
57
+ userId: 'acceptance-user',
58
+ roleCodes: [],
59
+ capabilityCodes: authorized ? allCapabilities : [],
60
+ isAppSuperAdmin: authorized,
61
+ },
62
+ },
63
+ }),
64
+ });
65
+ }
66
+ const query = url.pathname.match(/\/native\/data\/([^/]+)\/query$/);
67
+ if (query) {
68
+ return route.fulfill({
69
+ contentType: 'application/json',
70
+ body: JSON.stringify({
71
+ code: 200,
72
+ data: {
73
+ schemaVersion: 'openxiangda.native-data-page/v2',
74
+ resourceCode: decodeURIComponent(query[1]),
75
+ appVersionId: 'app-version-1',
76
+ environmentHeadRevision: 1,
77
+ items: [],
78
+ total: 0,
79
+ limit: 500,
80
+ offset: 0,
81
+ },
82
+ }),
83
+ });
84
+ }
85
+ if (url.pathname.includes('/native/directory/')) {
86
+ return route.fulfill({
87
+ contentType: 'application/json',
88
+ body: JSON.stringify({ code: 200, data: { items: [], nextCursor: null } }),
89
+ });
14
90
  }
15
- return route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ code: 404, message: 'not mocked' }) });
91
+ return route.fulfill({
92
+ status: 404,
93
+ contentType: 'application/json',
94
+ body: JSON.stringify({ code: 404, message: 'not mocked' }),
95
+ });
16
96
  });
17
- await page.goto('/view/clean-app/');
18
- await expect(page.getByText('还没有声明数据资源')).toBeVisible();
19
- await expect(page.getByText('仪器' + '资源管理')).toHaveCount(0);
20
- await expect(page.locator('.oxa-topbar')).toHaveCSS('height', '52px');
97
+ }
98
+
99
+ test('renders the compiled desktop resource contract and shared workbench', async ({ page }) => {
100
+ await mockPlatform(page);
101
+ if (!codes.length) {
102
+ await page.goto(`${runtimeBase}/`);
103
+ await expect(page.getByText('还没有声明数据资源')).toBeVisible();
104
+ } else {
105
+ const code = codes[0];
106
+ const definition = definitions[code];
107
+ await page.goto(`${runtimeBase}/${code}`);
108
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
109
+ await expect(page.locator('.oxa-sider').getByText(definition.name, { exact: true })).toBeVisible();
110
+ await expect(page.getByRole('button', { name: /导出$/ })).toBeVisible();
111
+ await expect(page.getByRole('button', { name: /列设置$/ })).toBeVisible();
112
+ await expect(page.getByRole('button', { name: /密度$/ })).toBeVisible();
113
+ if ((definition.surface.list?.filterFields?.length || 0) > 2) {
114
+ const more = page.getByRole('button', { name: /更多筛选/ });
115
+ await expect(more).toBeVisible();
116
+ await more.click();
117
+ await expect(page.getByRole('button', { name: '收起筛选' })).toBeVisible();
118
+ }
119
+ const downloadPromise = page.waitForEvent('download');
120
+ await page.getByRole('button', { name: /导出$/ }).click();
121
+ const download = await downloadPromise;
122
+ expect(download.suggestedFilename()).toMatch(/\.csv$/);
123
+ }
21
124
 
125
+ await expect(page.locator('.oxa-topbar')).toHaveCSS('height', '52px');
126
+ await expect(page.getByText('仪器' + '资源管理')).toHaveCount(0);
22
127
  await page.locator('.oxa-current-user').click();
23
128
  await expect(page.getByText('跟随系统', { exact: true })).toBeVisible();
24
129
  await page.getByText('深色', { exact: true }).click();
25
130
  await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'dark');
26
- await expect
27
- .poll(() => page.evaluate(() => localStorage.getItem('openxiangda.admin.appearance')))
28
- .toBe('dark');
29
- await page.reload();
30
- await expect(page.locator('html')).toHaveAttribute('data-oxa-theme', 'dark');
131
+ await expect.poll(() => page.evaluate(() => localStorage.getItem('openxiangda.admin.appearance'))).toBe('dark');
132
+ });
133
+
134
+ test('renders platform mobile controls from declared field widgets', async ({ page }) => {
135
+ const candidate = codes
136
+ .map(code => ({ code, definition: definitions[code] }))
137
+ .find(item =>
138
+ item.definition.surface.mobile?.enabled &&
139
+ Object.values(item.definition.surface.fields).some(field =>
140
+ ['directory-user', 'directory-department', 'resource', 'file'].includes(field.widget || '')
141
+ )
142
+ );
143
+ test.skip(!candidate, 'No mobile platform field is declared.');
144
+ await mockPlatform(page);
145
+ await page.setViewportSize({ width: 390, height: 844 });
146
+ await page.goto(`${runtimeBase}/m/${candidate!.code}/new`);
147
+ await expect(page.locator('.oxa-mobile-form')).toBeVisible();
148
+
149
+ const widgets = new Set(Object.values(candidate!.definition.surface.fields).map(field => field.widget));
150
+ if (widgets.has('directory-user') || widgets.has('directory-department')) {
151
+ const trigger = page.locator('.oxa-directory-trigger').first();
152
+ await expect(trigger).toBeVisible();
153
+ await trigger.click();
154
+ await expect(page.locator('.oxa-directory-mobile-drawer')).toBeVisible();
155
+ await page.keyboard.press('Escape');
156
+ }
157
+ if (widgets.has('resource')) {
158
+ const trigger = page.locator('.oxa-mobile-reference-trigger').first();
159
+ await expect(trigger).toBeVisible();
160
+ await trigger.click();
161
+ await expect(page.locator('.oxa-mobile-reference-sheet')).toBeVisible();
162
+ await page.keyboard.press('Escape');
163
+ }
164
+ if (widgets.has('file')) {
165
+ await expect(page.locator('.oxa-mobile-file-field')).toBeVisible();
166
+ await expect(page.locator('.oxa-mobile-file-field input[type="file"]')).toBeHidden();
167
+ await expect(page.getByRole('button', { name: '上传文件' })).toBeVisible();
168
+ }
169
+ });
170
+
171
+ test('fails closed when the current user lacks the declared read capability', async ({ page }) => {
172
+ test.skip(!codes.length, 'No resource permission exists in an empty application.');
173
+ await mockPlatform(page, false);
174
+ const code = codes[0];
175
+ await page.goto(`${runtimeBase}/${code}`);
176
+ await expect(page.getByText(new RegExp(`当前平台用户无${definitions[code].name}页面权限`))).toBeVisible();
177
+ await expect(page.locator('.oxa-list-surface')).toHaveCount(0);
31
178
  });
@@ -22,7 +22,7 @@
22
22
  "build": "pnpm --recursive build"
23
23
  },
24
24
  "devDependencies": {
25
- "openxiangda-cli": "2.0.0-alpha.75",
25
+ "openxiangda-cli": "2.0.0-alpha.76",
26
26
  "openxiangda-contracts": "2.0.0-alpha.35",
27
27
  "openxiangda-devkit-core": "2.0.0-alpha.46",
28
28
  "typescript": "5.9.3"