openxiangda-cli 2.0.0-alpha.18 → 2.0.0-alpha.183

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.
Files changed (293) hide show
  1. package/README.md +6 -14
  2. package/bin/run.js +79 -1
  3. package/dist/base.d.ts +60 -5
  4. package/dist/base.d.ts.map +1 -1
  5. package/dist/base.js +197 -10
  6. package/dist/base.js.map +1 -1
  7. package/dist/commands/accept.d.ts +28 -0
  8. package/dist/commands/accept.d.ts.map +1 -0
  9. package/dist/commands/accept.js +21 -0
  10. package/dist/commands/accept.js.map +1 -0
  11. package/dist/commands/admin.d.ts +32 -0
  12. package/dist/commands/admin.d.ts.map +1 -0
  13. package/dist/commands/admin.js +25 -0
  14. package/dist/commands/admin.js.map +1 -0
  15. package/dist/commands/cancel.d.ts +19 -1
  16. package/dist/commands/cancel.d.ts.map +1 -1
  17. package/dist/commands/cancel.js +1 -1
  18. package/dist/commands/cancel.js.map +1 -1
  19. package/dist/commands/check.d.ts +23 -1
  20. package/dist/commands/check.d.ts.map +1 -1
  21. package/dist/commands/check.js +21 -4
  22. package/dist/commands/check.js.map +1 -1
  23. package/dist/commands/context.d.ts +27 -0
  24. package/dist/commands/context.d.ts.map +1 -0
  25. package/dist/commands/context.js +10 -0
  26. package/dist/commands/context.js.map +1 -0
  27. package/dist/commands/create.d.ts +40 -0
  28. package/dist/commands/create.d.ts.map +1 -0
  29. package/dist/commands/create.js +475 -0
  30. package/dist/commands/create.js.map +1 -0
  31. package/dist/commands/deploy.d.ts +28 -8
  32. package/dist/commands/deploy.d.ts.map +1 -1
  33. package/dist/commands/deploy.js +30 -18
  34. package/dist/commands/deploy.js.map +1 -1
  35. package/dist/commands/dev.d.ts +23 -1
  36. package/dist/commands/dev.d.ts.map +1 -1
  37. package/dist/commands/dev.js +28 -4
  38. package/dist/commands/dev.js.map +1 -1
  39. package/dist/commands/docs.d.ts +30 -0
  40. package/dist/commands/docs.d.ts.map +1 -0
  41. package/dist/commands/docs.js +20 -0
  42. package/dist/commands/docs.js.map +1 -0
  43. package/dist/commands/login.d.ts +29 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/{auth/login.js → login.js} +27 -9
  46. package/dist/commands/login.js.map +1 -0
  47. package/dist/commands/logs.d.ts +22 -2
  48. package/dist/commands/logs.d.ts.map +1 -1
  49. package/dist/commands/logs.js +9 -5
  50. package/dist/commands/logs.js.map +1 -1
  51. package/dist/commands/retry.d.ts +19 -1
  52. package/dist/commands/retry.d.ts.map +1 -1
  53. package/dist/commands/retry.js +5 -2
  54. package/dist/commands/retry.js.map +1 -1
  55. package/dist/commands/rollback.d.ts +23 -4
  56. package/dist/commands/rollback.d.ts.map +1 -1
  57. package/dist/commands/rollback.js +18 -5
  58. package/dist/commands/rollback.js.map +1 -1
  59. package/dist/commands/skill.d.ts +32 -0
  60. package/dist/commands/skill.d.ts.map +1 -0
  61. package/dist/commands/skill.js +50 -0
  62. package/dist/commands/skill.js.map +1 -0
  63. package/dist/commands/spec.d.ts +42 -0
  64. package/dist/commands/spec.d.ts.map +1 -0
  65. package/dist/commands/spec.js +123 -0
  66. package/dist/commands/spec.js.map +1 -0
  67. package/dist/commands/start.d.ts +28 -0
  68. package/dist/commands/start.d.ts.map +1 -0
  69. package/dist/commands/start.js +18 -0
  70. package/dist/commands/start.js.map +1 -0
  71. package/dist/commands/status.d.ts +20 -1
  72. package/dist/commands/status.d.ts.map +1 -1
  73. package/dist/commands/status.js +11 -4
  74. package/dist/commands/status.js.map +1 -1
  75. package/dist/commands/stop.d.ts +28 -0
  76. package/dist/commands/stop.d.ts.map +1 -0
  77. package/dist/commands/stop.js +18 -0
  78. package/dist/commands/stop.js.map +1 -0
  79. package/dist/create-workspace.d.ts +67 -0
  80. package/dist/create-workspace.d.ts.map +1 -0
  81. package/dist/create-workspace.js +631 -0
  82. package/dist/create-workspace.js.map +1 -0
  83. package/dist/studio-events.d.ts +9 -0
  84. package/dist/studio-events.d.ts.map +1 -0
  85. package/dist/studio-events.js +30 -0
  86. package/dist/studio-events.js.map +1 -0
  87. package/dist/toolchain-capsule.d.ts +4 -0
  88. package/dist/toolchain-capsule.d.ts.map +1 -0
  89. package/dist/toolchain-capsule.js +116 -0
  90. package/dist/toolchain-capsule.js.map +1 -0
  91. package/package.json +17 -10
  92. package/template/.dockerignore +23 -0
  93. package/template/.env.example +3 -0
  94. package/template/AGENTS.md +26 -0
  95. package/template/README.md +45 -0
  96. package/template/_gitignore +14 -0
  97. package/template/apps/web/data-api-live.e2e.html +15 -0
  98. package/template/apps/web/e2e/client-fixture.ts +5 -0
  99. package/template/apps/web/e2e/component-defaults-fixture.css +15 -0
  100. package/template/apps/web/e2e/data-api-live-fixture.tsx +162 -0
  101. package/template/apps/web/e2e/data-api-live.spec.ts +144 -0
  102. package/template/apps/web/e2e/field-protocol-fixture.tsx +387 -0
  103. package/template/apps/web/e2e/field-protocol.spec.ts +285 -0
  104. package/template/apps/web/e2e/mobile-files.spec.ts +96 -0
  105. package/template/apps/web/e2e/mobile-reference-fixture.tsx +314 -0
  106. package/template/apps/web/e2e/mobile-reference.spec.ts +360 -0
  107. package/template/apps/web/e2e/mobile-selection.spec.ts +199 -0
  108. package/template/apps/web/e2e/resource-experience-fixture.tsx +491 -0
  109. package/template/apps/web/e2e/resource-platform-mock.ts +202 -0
  110. package/template/apps/web/e2e/resources.spec.ts +488 -0
  111. package/template/apps/web/e2e/standard-admin.spec.ts +511 -0
  112. package/template/apps/web/e2e/workflow-entry-fixture.tsx +107 -0
  113. package/template/apps/web/e2e/workflow-entry.spec.ts +556 -0
  114. package/template/apps/web/e2e/workflow-experience-fixture.tsx +338 -0
  115. package/template/apps/web/e2e/workflow.spec.ts +1265 -0
  116. package/template/apps/web/field-protocol.e2e.html +22 -0
  117. package/template/apps/web/index.html +12 -0
  118. package/template/apps/web/mobile-reference.e2e.html +27 -0
  119. package/template/apps/web/package.json +34 -0
  120. package/template/apps/web/playwright.config.ts +47 -0
  121. package/template/apps/web/resource-experience.e2e.html +16 -0
  122. package/template/apps/web/scripts/check.mjs +43 -0
  123. package/template/apps/web/scripts/verify-build.mjs +6 -0
  124. package/template/apps/web/src/document.css +5 -0
  125. package/template/apps/web/src/main.tsx +66 -0
  126. package/template/apps/web/src/user-end/tailwind.css +5 -0
  127. package/template/apps/web/test/contracts.test.ts +163 -0
  128. package/template/apps/web/test/loopback.test.ts +68 -0
  129. package/template/apps/web/test/standard-user-surfaces.typecheck.tsx +27 -0
  130. package/template/apps/web/tsconfig.json +12 -0
  131. package/template/apps/web/vite.config.ts +43 -0
  132. package/template/apps/web/workflow-entry.e2e.html +15 -0
  133. package/template/apps/web/workflow-experience.e2e.html +15 -0
  134. package/template/appspec/app.md +52 -0
  135. package/template/openxiangda.config.ts +55 -0
  136. package/template/package.json +28 -0
  137. package/template/packages/contracts/package.json +23 -0
  138. package/template/packages/contracts/src/generated.ts +265 -0
  139. package/template/packages/contracts/src/index.ts +1 -0
  140. package/template/packages/contracts/tsconfig.json +9 -0
  141. package/template/pnpm-workspace.yaml +7 -0
  142. package/template/tsconfig.base.json +13 -0
  143. package/dist/commands/app/create.d.ts +0 -15
  144. package/dist/commands/app/create.d.ts.map +0 -1
  145. package/dist/commands/app/create.js +0 -42
  146. package/dist/commands/app/create.js.map +0 -1
  147. package/dist/commands/app/info.d.ts +0 -9
  148. package/dist/commands/app/info.d.ts.map +0 -1
  149. package/dist/commands/app/info.js +0 -10
  150. package/dist/commands/app/info.js.map +0 -1
  151. package/dist/commands/app/link.d.ts +0 -10
  152. package/dist/commands/app/link.d.ts.map +0 -1
  153. package/dist/commands/app/link.js +0 -16
  154. package/dist/commands/app/link.js.map +0 -1
  155. package/dist/commands/app/provision.d.ts +0 -9
  156. package/dist/commands/app/provision.d.ts.map +0 -1
  157. package/dist/commands/app/provision.js +0 -10
  158. package/dist/commands/app/provision.js.map +0 -1
  159. package/dist/commands/auth/login.d.ts +0 -11
  160. package/dist/commands/auth/login.d.ts.map +0 -1
  161. package/dist/commands/auth/login.js.map +0 -1
  162. package/dist/commands/auth/logout.d.ts +0 -6
  163. package/dist/commands/auth/logout.d.ts.map +0 -1
  164. package/dist/commands/auth/logout.js +0 -30
  165. package/dist/commands/auth/logout.js.map +0 -1
  166. package/dist/commands/auth/status.d.ts +0 -6
  167. package/dist/commands/auth/status.d.ts.map +0 -1
  168. package/dist/commands/auth/status.js +0 -60
  169. package/dist/commands/auth/status.js.map +0 -1
  170. package/dist/commands/build.d.ts +0 -11
  171. package/dist/commands/build.d.ts.map +0 -1
  172. package/dist/commands/build.js +0 -23
  173. package/dist/commands/build.js.map +0 -1
  174. package/dist/commands/doctor.d.ts +0 -9
  175. package/dist/commands/doctor.d.ts.map +0 -1
  176. package/dist/commands/doctor.js +0 -7
  177. package/dist/commands/doctor.js.map +0 -1
  178. package/dist/commands/event/delivery/list.d.ts +0 -10
  179. package/dist/commands/event/delivery/list.d.ts.map +0 -1
  180. package/dist/commands/event/delivery/list.js +0 -14
  181. package/dist/commands/event/delivery/list.js.map +0 -1
  182. package/dist/commands/event/delivery/replay.d.ts +0 -13
  183. package/dist/commands/event/delivery/replay.d.ts.map +0 -1
  184. package/dist/commands/event/delivery/replay.js +0 -16
  185. package/dist/commands/event/delivery/replay.js.map +0 -1
  186. package/dist/commands/event/subscription/list.d.ts +0 -9
  187. package/dist/commands/event/subscription/list.d.ts.map +0 -1
  188. package/dist/commands/event/subscription/list.js +0 -10
  189. package/dist/commands/event/subscription/list.js.map +0 -1
  190. package/dist/commands/event/subscription/rotate-secret.d.ts +0 -15
  191. package/dist/commands/event/subscription/rotate-secret.d.ts.map +0 -1
  192. package/dist/commands/event/subscription/rotate-secret.js +0 -33
  193. package/dist/commands/event/subscription/rotate-secret.js.map +0 -1
  194. package/dist/commands/event/subscription/status.d.ts +0 -14
  195. package/dist/commands/event/subscription/status.d.ts.map +0 -1
  196. package/dist/commands/event/subscription/status.js +0 -19
  197. package/dist/commands/event/subscription/status.js.map +0 -1
  198. package/dist/commands/event/timer/list.d.ts +0 -9
  199. package/dist/commands/event/timer/list.d.ts.map +0 -1
  200. package/dist/commands/event/timer/list.js +0 -10
  201. package/dist/commands/event/timer/list.js.map +0 -1
  202. package/dist/commands/event/timer/status.d.ts +0 -14
  203. package/dist/commands/event/timer/status.d.ts.map +0 -1
  204. package/dist/commands/event/timer/status.js +0 -19
  205. package/dist/commands/event/timer/status.js.map +0 -1
  206. package/dist/commands/generate.d.ts +0 -10
  207. package/dist/commands/generate.d.ts.map +0 -1
  208. package/dist/commands/generate.js +0 -8
  209. package/dist/commands/generate.js.map +0 -1
  210. package/dist/commands/oauth/audit/list.d.ts +0 -12
  211. package/dist/commands/oauth/audit/list.d.ts.map +0 -1
  212. package/dist/commands/oauth/audit/list.js +0 -20
  213. package/dist/commands/oauth/audit/list.js.map +0 -1
  214. package/dist/commands/oauth/client/create.d.ts +0 -13
  215. package/dist/commands/oauth/client/create.d.ts.map +0 -1
  216. package/dist/commands/oauth/client/create.js +0 -34
  217. package/dist/commands/oauth/client/create.js.map +0 -1
  218. package/dist/commands/oauth/client/list.d.ts +0 -9
  219. package/dist/commands/oauth/client/list.d.ts.map +0 -1
  220. package/dist/commands/oauth/client/list.js +0 -10
  221. package/dist/commands/oauth/client/list.js.map +0 -1
  222. package/dist/commands/oauth/client/revoke.d.ts +0 -12
  223. package/dist/commands/oauth/client/revoke.d.ts.map +0 -1
  224. package/dist/commands/oauth/client/revoke.js +0 -12
  225. package/dist/commands/oauth/client/revoke.js.map +0 -1
  226. package/dist/commands/oauth/client/rotate.d.ts +0 -13
  227. package/dist/commands/oauth/client/rotate.d.ts.map +0 -1
  228. package/dist/commands/oauth/client/rotate.js +0 -20
  229. package/dist/commands/oauth/client/rotate.js.map +0 -1
  230. package/dist/commands/oauth/client/update.d.ts +0 -15
  231. package/dist/commands/oauth/client/update.d.ts.map +0 -1
  232. package/dist/commands/oauth/client/update.js +0 -23
  233. package/dist/commands/oauth/client/update.js.map +0 -1
  234. package/dist/commands/oauth/runtime/rotate.d.ts +0 -12
  235. package/dist/commands/oauth/runtime/rotate.d.ts.map +0 -1
  236. package/dist/commands/oauth/runtime/rotate.js +0 -31
  237. package/dist/commands/oauth/runtime/rotate.js.map +0 -1
  238. package/dist/commands/oauth/runtime/status.d.ts +0 -10
  239. package/dist/commands/oauth/runtime/status.d.ts.map +0 -1
  240. package/dist/commands/oauth/runtime/status.js +0 -17
  241. package/dist/commands/oauth/runtime/status.js.map +0 -1
  242. package/dist/commands/promote.d.ts +0 -13
  243. package/dist/commands/promote.d.ts.map +0 -1
  244. package/dist/commands/promote.js +0 -9
  245. package/dist/commands/promote.js.map +0 -1
  246. package/dist/commands/secret/audit/list.d.ts +0 -14
  247. package/dist/commands/secret/audit/list.d.ts.map +0 -1
  248. package/dist/commands/secret/audit/list.js +0 -16
  249. package/dist/commands/secret/audit/list.js.map +0 -1
  250. package/dist/commands/secret/create.d.ts +0 -18
  251. package/dist/commands/secret/create.d.ts.map +0 -1
  252. package/dist/commands/secret/create.js +0 -33
  253. package/dist/commands/secret/create.js.map +0 -1
  254. package/dist/commands/secret/delete.d.ts +0 -15
  255. package/dist/commands/secret/delete.d.ts.map +0 -1
  256. package/dist/commands/secret/delete.js +0 -20
  257. package/dist/commands/secret/delete.js.map +0 -1
  258. package/dist/commands/secret/list.d.ts +0 -10
  259. package/dist/commands/secret/list.d.ts.map +0 -1
  260. package/dist/commands/secret/list.js +0 -14
  261. package/dist/commands/secret/list.js.map +0 -1
  262. package/dist/commands/secret/rotate.d.ts +0 -18
  263. package/dist/commands/secret/rotate.d.ts.map +0 -1
  264. package/dist/commands/secret/rotate.js +0 -30
  265. package/dist/commands/secret/rotate.js.map +0 -1
  266. package/dist/commands/secret/update.d.ts +0 -17
  267. package/dist/commands/secret/update.d.ts.map +0 -1
  268. package/dist/commands/secret/update.js +0 -37
  269. package/dist/commands/secret/update.js.map +0 -1
  270. package/dist/commands/skill/install.d.ts +0 -11
  271. package/dist/commands/skill/install.d.ts.map +0 -1
  272. package/dist/commands/skill/install.js +0 -52
  273. package/dist/commands/skill/install.js.map +0 -1
  274. package/dist/commands/skill/validate.d.ts +0 -9
  275. package/dist/commands/skill/validate.d.ts.map +0 -1
  276. package/dist/commands/skill/validate.js +0 -32
  277. package/dist/commands/skill/validate.js.map +0 -1
  278. package/dist/commands/test.d.ts +0 -9
  279. package/dist/commands/test.d.ts.map +0 -1
  280. package/dist/commands/test.js +0 -7
  281. package/dist/commands/test.js.map +0 -1
  282. package/dist/commands/workflow/provider/list.d.ts +0 -10
  283. package/dist/commands/workflow/provider/list.d.ts.map +0 -1
  284. package/dist/commands/workflow/provider/list.js +0 -14
  285. package/dist/commands/workflow/provider/list.js.map +0 -1
  286. package/dist/commands/workflow/provider/rotate.d.ts +0 -15
  287. package/dist/commands/workflow/provider/rotate.d.ts.map +0 -1
  288. package/dist/commands/workflow/provider/rotate.js +0 -24
  289. package/dist/commands/workflow/provider/rotate.js.map +0 -1
  290. package/dist/secret-value.d.ts +0 -5
  291. package/dist/secret-value.d.ts.map +0 -1
  292. package/dist/secret-value.js +0 -15
  293. package/dist/secret-value.js.map +0 -1
@@ -0,0 +1,488 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import { mockPlatform } from './resource-platform-mock';
3
+ import {
4
+ appCode,
5
+ capabilities,
6
+ resourceCodes,
7
+ resourceDefinitions,
8
+ } from '../../../packages/contracts/src/generated.js';
9
+
10
+ type DeclaredField = { label: string; widget?: string };
11
+ type DeclaredResource = {
12
+ name: string;
13
+ surface: {
14
+ fields: Record<string, DeclaredField>;
15
+ list?: { filterFields?: readonly string[] };
16
+ mobile?: { enabled?: boolean };
17
+ };
18
+ };
19
+
20
+ const codes = [...resourceCodes] as string[];
21
+ const definitions = resourceDefinitions as Record<string, DeclaredResource>;
22
+ const allCapabilities = [...capabilities] as string[];
23
+ const runtimeBase = `/view/${appCode}`;
24
+
25
+ const semanticResourceItem = {
26
+ id: 'group-1',
27
+ name: '组织信息演示',
28
+ revision: 1,
29
+ updated_at: '2026-08-28T01:00:00.000Z',
30
+ departmentMultiple: [
31
+ { label: '信息技术部', value: 'department-it' },
32
+ { label: '人文艺术部', value: 'department-humanities' },
33
+ ],
34
+ departmentSingle: { label: '校工会', value: 'department-union' },
35
+ userMultiple: [
36
+ { label: '周明远', value: 'user-zhou', employeeNo: 'T1001' },
37
+ { label: '沈书雅', value: 'user-shen', employeeNo: 'T1002' },
38
+ ],
39
+ resourceMultiple: [
40
+ { label: '教职工之家', value: 'resource-home', resourceCode: 'resource-01' },
41
+ { label: '文体活动室', value: 'resource-activity', resourceCode: 'resource-01' },
42
+ ],
43
+ enabled: false,
44
+ displayOrder: 0,
45
+ };
46
+
47
+ test('renders the compiled desktop resource contract and shared workbench', async ({ page }) => {
48
+ await mockPlatform(page);
49
+ if (!codes.length) {
50
+ await page.goto(`${runtimeBase}/`);
51
+ await expect(page).toHaveURL(`${runtimeBase}/home`);
52
+ await expect(page.getByText('OpenXiangda 应用', { exact: true })).toBeVisible();
53
+ await page.goto(`${runtimeBase}/admin`);
54
+ await expect(page.getByText('还没有声明数据资源')).toBeVisible();
55
+ } else {
56
+ const code = codes[0];
57
+ const definition = definitions[code];
58
+ await page.goto(`${runtimeBase}/admin`);
59
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
60
+ await expect(page).toHaveURL(`${runtimeBase}/admin/resources/${code}`);
61
+ await expect(page.locator('.oxa-sider').getByText(definition.name, { exact: true })).toBeVisible();
62
+ await expect(page.getByRole('button', { name: /导出$/ })).toBeVisible();
63
+ await expect(page.getByRole('button', { name: /显示列$/ })).toBeVisible();
64
+ if ((definition.surface.list?.filterFields?.length || 0) > 2) {
65
+ const more = page.getByRole('button', { name: /^筛选/ });
66
+ await expect(more).toBeVisible();
67
+ await more.click();
68
+ const modal = page.getByRole('dialog', { name: '筛选' });
69
+ await expect(modal).toBeVisible();
70
+ await expect(modal.getByRole('button', { name: '应用筛选' })).toBeVisible();
71
+ await expect
72
+ .poll(async () => (await modal.boundingBox())?.width || 0)
73
+ .toBeGreaterThan(700);
74
+ await modal.getByRole('button', { name: 'Close' }).click();
75
+ }
76
+ const downloadPromise = page.waitForEvent('download');
77
+ await page.getByRole('button', { name: /导出$/ }).click();
78
+ const download = await downloadPromise;
79
+ expect(download.suggestedFilename()).toMatch(/\.csv$/);
80
+ }
81
+
82
+ await expect(page.locator('.oxa-topbar')).toHaveCSS('height', '52px');
83
+ await expect(page.getByText('仪器' + '资源管理')).toHaveCount(0);
84
+ await page.locator('.oxa-current-user').click();
85
+ await expect(page.locator('.oxa-user-dropdown-profile')).toContainText('王老师');
86
+ await expect(page.locator('.oxa-user-dropdown-profile')).toContainText('理学院');
87
+ await expect(page.locator('.oxa-user-dropdown-roles')).toContainText('学院管理员');
88
+ await expect(page.locator('.oxa-user-dropdown-roles')).not.toContainText('college_admin');
89
+ await expect(page.getByText('外观', { exact: true })).toHaveCount(0);
90
+ await expect(page.getByText('跟随系统', { exact: true })).toHaveCount(0);
91
+ });
92
+
93
+ test('default components ignore old preferences and leave the surrounding document unchanged', async ({ page }) => {
94
+ const errors: string[] = [];
95
+ page.on('pageerror', error => errors.push(error.message));
96
+ await page.emulateMedia({ colorScheme: 'dark' });
97
+ await page.addInitScript(() => localStorage.setItem('openxiangda.admin.appearance', 'dark'));
98
+ const platform = await mockPlatform(page);
99
+ await page.goto('/resource-experience.e2e.html');
100
+ await page
101
+ .locator('.oxa-sider')
102
+ .getByText('内容业务操作', { exact: true })
103
+ .click();
104
+
105
+ const customPage = page.getByTestId('custom-admin-page');
106
+ const customPanel = customPage.locator('.component-defaults-panel');
107
+ await expect(customPage).toBeVisible();
108
+ await expect(customPanel).toBeVisible();
109
+ const readDefaultColors = () =>
110
+ page.evaluate(() => {
111
+ const host = document.querySelector('[data-testid="custom-admin-page"]');
112
+ if (!host) throw new Error('CUSTOM_ADMIN_COMPONENT_HOST_NOT_READY');
113
+ const probe = document.createElement('div');
114
+ probe.style.cssText = [
115
+ 'position:fixed',
116
+ 'width:1px',
117
+ 'height:1px',
118
+ 'background:var(--ant-color-bg-layout)',
119
+ 'color:var(--ant-color-bg-container)',
120
+ 'border:1px solid var(--ant-color-border-secondary)',
121
+ 'outline:1px solid var(--ant-color-text)',
122
+ ].join(';');
123
+ host.appendChild(probe);
124
+ const styles = getComputedStyle(probe);
125
+ const result = {
126
+ canvas: styles.backgroundColor,
127
+ surface: styles.color,
128
+ border: styles.borderTopColor,
129
+ text: styles.outlineColor,
130
+ };
131
+ probe.remove();
132
+ return result;
133
+ });
134
+ const defaults = await readDefaultColors();
135
+ expect(defaults.surface).toBe('rgb(255, 255, 255)');
136
+ await expect(customPage).toHaveCSS('background-color', defaults.canvas);
137
+ await expect(customPanel).toHaveCSS('background-color', defaults.surface);
138
+ await expect(customPanel).toHaveCSS('color', defaults.text);
139
+ await expect(customPanel).toHaveCSS('border-color', defaults.border);
140
+
141
+ const hostState = () => page.evaluate(() => {
142
+ const host = getComputedStyle(document.getElementById('outside-ui')!);
143
+ return {
144
+ font: host.fontFamily, color: host.color, background: host.backgroundColor,
145
+ boxSizing: host.boxSizing,
146
+ documentStyle: document.documentElement.getAttribute('style'),
147
+ documentAttributes: [...document.documentElement.attributes].map(attribute => attribute.name),
148
+ };
149
+ });
150
+ const outside = await hostState();
151
+ expect(outside).toEqual({
152
+ font: 'monospace', color: 'rgb(31, 32, 33)', background: 'rgb(248, 249, 250)',
153
+ boxSizing: 'content-box', documentStyle: null, documentAttributes: ['lang'],
154
+ });
155
+
156
+ await page.emulateMedia({ colorScheme: 'light' });
157
+ expect(await readDefaultColors()).toEqual(defaults);
158
+ await expect(customPanel).toHaveCSS('background-color', defaults.surface);
159
+ expect(await hostState()).toEqual(outside);
160
+
161
+ const identityReads = platform.contextReads;
162
+ await page.getByRole('button', { name: '打开用户端' }).click();
163
+ await page.getByRole('textbox', { name: '用户端输入' }).fill('保留业务输入');
164
+ await page.getByRole('combobox').click();
165
+ const dropdown = page.locator('.ant-select-dropdown:visible');
166
+ await expect(dropdown).toHaveCSS('background-color', defaults.surface);
167
+ await dropdown.getByText('可用选项', { exact: true }).click();
168
+ await page.getByRole('button', { name: '打开用户弹窗' }).click();
169
+ const dialog = page.getByRole('dialog');
170
+ await expect(dialog).toBeVisible();
171
+ await expect(dialog.locator('.ant-modal-container')).toHaveCSS('background-color', defaults.surface);
172
+ await page.emulateMedia({ colorScheme: 'dark' });
173
+ await expect(dialog.locator('.ant-modal-container')).toHaveCSS('background-color', defaults.surface);
174
+ await dialog.getByRole('button', { name: '确 定' }).click();
175
+ await expect(page.getByRole('textbox', { name: '用户端输入' })).toHaveValue('保留业务输入');
176
+ expect(await hostState()).toEqual(outside);
177
+ expect(platform.contextReads).toBe(identityReads);
178
+ expect(errors).toEqual([]);
179
+ });
180
+
181
+ test('opens the stable admin entry under the preproduction gateway mount', async ({ page }) => {
182
+ test.skip(!codes.length, 'No resource route exists in an empty application.');
183
+ const devRuntimeBase = `/dev/${appCode}`;
184
+ await mockPlatform(page, true, devRuntimeBase);
185
+ await page.goto(`${devRuntimeBase}/admin`);
186
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
187
+ await expect(page).toHaveURL(
188
+ `${devRuntimeBase}/admin/resources/${codes[0]}`,
189
+ );
190
+ });
191
+
192
+ test('uses the complete current-user role union when no Perspective is selected', async ({ page }) => {
193
+ const platform = await mockPlatform(page);
194
+ await page.goto(`${runtimeBase}/admin`);
195
+ if (codes.length) {
196
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
197
+ } else {
198
+ await expect(page.getByText('还没有声明数据资源')).toBeVisible();
199
+ }
200
+
201
+ await page.evaluate(async () => {
202
+ const client = await import('/e2e/client-fixture.ts');
203
+ await client.searchResource('acceptance-resource', 'name', {
204
+ keyword: '显微镜',
205
+ });
206
+ await client.searchDirectory('user', { keyword: '老师' });
207
+ await client.requestApplicationApi('/health/current-user');
208
+ });
209
+
210
+ const protectedRequests = platform.observed.filter(item =>
211
+ /\/native\/data(?:-resources)?\/|\/directory\/|\/openxiangda-app-api\/v2\//.test(
212
+ item.path
213
+ )
214
+ );
215
+ expect(protectedRequests.length).toBeGreaterThanOrEqual(3);
216
+ expect(protectedRequests.every(item => item.perspectiveCode === '')).toBe(true);
217
+ expect(platform.contextReads).toBe(1);
218
+ await page.locator('.oxa-current-user').click();
219
+ await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
220
+ '学院管理员'
221
+ );
222
+ await expect(page.locator('.oxa-user-dropdown-roles')).toContainText(
223
+ '仪器管理员'
224
+ );
225
+ await expect(page.locator('.oxa-user-dropdown-roles')).not.toContainText(
226
+ 'college_admin'
227
+ );
228
+ await expect(page.getByText('切换应用角色', { exact: true })).toHaveCount(0);
229
+ });
230
+
231
+ test('updates the current user avatar through the platform-owned profile contract', async ({ page }) => {
232
+ const platform = await mockPlatform(page);
233
+ await page.goto(`${runtimeBase}/admin`);
234
+ await page.locator('.oxa-current-user').click();
235
+ await page.locator('.oxa-user-dropdown input[type="file"]').first().setInputFiles({
236
+ name: 'avatar.png',
237
+ mimeType: 'image/png',
238
+ buffer: Buffer.from(
239
+ '89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d49444154789c6360f8cff00000040101005fe245400000000049454e44ae426082',
240
+ 'hex'
241
+ ),
242
+ });
243
+ await expect(page.getByText('头像已更新', { exact: true })).toBeVisible();
244
+ await expect.poll(() => platform.avatarCompletes).toBe(1);
245
+ await expect(page.locator('.oxa-current-user img')).toHaveAttribute(
246
+ 'src',
247
+ 'https://cdn.example.test/avatar/new.png'
248
+ );
249
+ });
250
+
251
+ test('preserves the left menu scroll position when switching resource pages', async ({ page }) => {
252
+ await mockPlatform(page, true, runtimeBase, true);
253
+ await page.setViewportSize({ width: 1280, height: 400 });
254
+ await page.goto('/resource-experience.e2e.html');
255
+ const scroller = page.locator('.oxa-menu-scroll');
256
+ await scroller.evaluate(node => {
257
+ node.scrollTop = 240;
258
+ node.dispatchEvent(new Event('scroll'));
259
+ });
260
+ await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
261
+ await page
262
+ .locator('.oxa-sider')
263
+ .getByText('资料下载', { exact: true })
264
+ .evaluate(node => (node as HTMLElement).click());
265
+ await expect(page).toHaveURL('/admin/resources/resource-12');
266
+ await expect.poll(() => scroller.evaluate(node => node.scrollTop)).toBe(240);
267
+ });
268
+
269
+ test('mounts generated operation routes and typed actions in the unified Shell', async ({ page }) => {
270
+ await mockPlatform(page, true, runtimeBase, true);
271
+ await page.goto('/resource-experience.e2e.html');
272
+ await expect(
273
+ page.getByRole('button', { name: '发布选中栏目(0)' })
274
+ ).toHaveCount(0);
275
+ await page.locator('.oxa-sider').getByText('内容业务操作', { exact: true }).click();
276
+ await expect(page).toHaveURL('/admin/operations/content');
277
+ await expect(
278
+ page.getByRole('heading', { name: '内容业务操作' })
279
+ ).toBeVisible();
280
+ await expect(page.locator('.oxa-sider')).toBeVisible();
281
+ });
282
+
283
+ test('keeps the user portal and namespaced generated admin resource in one router', async ({ page }) => {
284
+ await mockPlatform(page, true, runtimeBase, true);
285
+ await page.goto('/resource-experience.e2e.html');
286
+ await page.evaluate(() => {
287
+ history.pushState({}, '', '/resource-01');
288
+ window.dispatchEvent(new PopStateEvent('popstate'));
289
+ });
290
+ await expect(
291
+ page.getByRole('heading', { name: '用户端内容门户' }),
292
+ ).toBeVisible();
293
+ await expect(page.locator('.oxa-sider')).toHaveCount(0);
294
+
295
+ await page.evaluate(() => {
296
+ history.pushState({}, '', '/admin/resources/resource-01');
297
+ window.dispatchEvent(new PopStateEvent('popstate'));
298
+ });
299
+ await expect(page.locator('.oxa-list-surface')).toBeVisible();
300
+ await expect(page.locator('.oxa-sider')).toHaveCount(1);
301
+ });
302
+
303
+ test('renders canonical semantic labels in the real generated resource list', async ({ page }) => {
304
+ const resourceCode = 'resource-01';
305
+ await mockPlatform(page, true, runtimeBase, true, {
306
+ [resourceCode]: [semanticResourceItem],
307
+ });
308
+ await page.route('**/native/data/resource-01/records/group-1?*', async route => {
309
+ await route.fulfill({ json: { code: 200, data: { schemaVersion: 'openxiangda.data-record/v2', data: semanticResourceItem } } });
310
+ });
311
+ await page.goto('/resource-experience.e2e.html');
312
+ await expect(page).toHaveURL(`/admin/resources/${resourceCode}`);
313
+ const row = page.getByRole('row').filter({ hasText: '信息技术部' });
314
+ await expect(row).toHaveCount(1);
315
+ for (const label of [
316
+ '信息技术部',
317
+ '人文艺术部',
318
+ '校工会',
319
+ '周明远',
320
+ '沈书雅',
321
+ '教职工之家',
322
+ '文体活动室',
323
+ ]) {
324
+ await expect(row).toContainText(label);
325
+ }
326
+ await expect(row).not.toContainText('[object Object]');
327
+ await expect(row.getByRole('cell').nth(5)).toHaveText('否');
328
+ await expect(row.getByRole('cell').nth(6)).toHaveText('0');
329
+ await page.screenshot({
330
+ fullPage: true,
331
+ path: 'test-results/generated-resource-semantic-values-desktop.png',
332
+ });
333
+ await row.getByText('信息技术部', { exact: true }).click();
334
+ await expect(page).toHaveURL(`/admin/resources/${resourceCode}`);
335
+ const detail = page.getByRole('dialog');
336
+ await expect(detail).toBeVisible();
337
+ await expect(detail).toContainText('组织信息演示');
338
+ const frame = detail.locator('[data-detail-frame="standard"]');
339
+ await expect(frame).toBeVisible();
340
+ await expect(frame.locator('.oxa-record-detail-card')).toHaveCount(2);
341
+ await expect(frame.locator('.oxa-record-detail-card').getByRole('heading', { level: 2 })).toHaveText(['栏目设置', '管理信息']);
342
+ });
343
+
344
+ test('renders the canonical first field on the mobile generated list', async ({ page }) => {
345
+ const resourceCode = 'resource-01';
346
+ await mockPlatform(page, true, runtimeBase, true, {
347
+ [resourceCode]: [semanticResourceItem],
348
+ });
349
+ await page.setViewportSize({ width: 390, height: 844 });
350
+ await page.goto('/resource-experience.e2e.html');
351
+ await page.evaluate(resource => {
352
+ history.pushState({}, '', `/m/admin/resources/${resource}`);
353
+ window.dispatchEvent(new PopStateEvent('popstate'));
354
+ }, resourceCode);
355
+ const card = page.locator('.oxa-mobile-record-card').filter({ hasText: '信息技术部' });
356
+ await expect(card).toHaveCount(1);
357
+ await expect(card).toContainText('人文艺术部');
358
+ await expect(card).toContainText('校工会');
359
+ await expect(card).toContainText('周明远');
360
+ await expect(card).toContainText('教职工之家');
361
+ await expect(card).not.toContainText('[object Object]');
362
+ await page.screenshot({
363
+ fullPage: true,
364
+ path: 'test-results/generated-resource-semantic-values-mobile.png',
365
+ });
366
+ await card.getByText('信息技术部', { exact: true }).click();
367
+ await expect(page).toHaveURL(`/m/admin/resources/${resourceCode}/group-1`);
368
+ });
369
+
370
+ test('preserves false and zero when they lead the mobile generated list', async ({ page }) => {
371
+ const resourceCode = 'resource-01';
372
+ await mockPlatform(page, true, runtimeBase, true, {
373
+ [resourceCode]: [semanticResourceItem],
374
+ });
375
+ await page.setViewportSize({ width: 390, height: 844 });
376
+ await page.goto('/resource-experience.e2e.html?primary=falsy');
377
+ await page.evaluate(resource => {
378
+ history.pushState({}, '', `/m/admin/resources/${resource}`);
379
+ window.dispatchEvent(new PopStateEvent('popstate'));
380
+ }, resourceCode);
381
+ const card = page.locator('.oxa-mobile-record-card');
382
+ await expect(card).toHaveCount(1);
383
+ await expect(card.locator('.oxa-mobile-record-heading')).toHaveText('否');
384
+ await expect(card.locator('.oxa-mobile-record-fields > div').filter({ hasText: '显示顺序' })).toContainText('0');
385
+ await card.locator('.oxa-mobile-record-heading').getByText('否', { exact: true }).click();
386
+ await expect(page).toHaveURL(`/m/admin/resources/${resourceCode}/group-1`);
387
+ });
388
+
389
+ test('fails closed on a direct operation route without its capability', async ({ page }) => {
390
+ await mockPlatform(page, false, runtimeBase, false);
391
+ await page.goto('/resource-experience.e2e.html');
392
+ await page.evaluate(() => {
393
+ history.pushState({}, '', '/admin/operations/content');
394
+ window.dispatchEvent(new PopStateEvent('popstate'));
395
+ });
396
+ await expect(page.getByText('当前平台用户无此页面权限')).toBeVisible();
397
+ await expect(
398
+ page.locator('.oxa-sider').getByText('内容业务操作', { exact: true })
399
+ ).toHaveCount(0);
400
+ });
401
+
402
+ test('keeps platform implementation guidance out of generated forms', async ({ page }) => {
403
+ await mockPlatform(page, true, runtimeBase, true);
404
+ await page.goto('/resource-experience.e2e.html');
405
+ await page.getByRole('button', { name: '新增', exact: true }).click();
406
+ await expect(page.getByRole('dialog', { name: '新增数据' })).toBeVisible();
407
+ await expect(page).toHaveURL('/admin/resources/resource-01');
408
+ const formLabels = page.locator('.ant-form-item-label label');
409
+ await expect(formLabels.first()).toBeVisible();
410
+ const labels = await formLabels.allTextContents();
411
+ expect(labels.slice(0, 2)).toEqual(['栏目编码', '栏目名称']);
412
+ await expect(
413
+ page.getByText('仅显示当前角色可编辑的字段;保存时将校验数据版本与权限范围。', {
414
+ exact: true,
415
+ })
416
+ ).toHaveCount(0);
417
+ await expect(page.getByText('当前角色不可修改此字段', { exact: true })).toHaveCount(0);
418
+ });
419
+
420
+ test('keeps one toolbar and opens all declared filters on demand', async ({ page }) => {
421
+ await mockPlatform(page, true, runtimeBase, true);
422
+ await page.goto('/resource-experience.e2e.html');
423
+ await expect(page.locator('.oxa-list-toolbar')).toHaveCount(1);
424
+ await expect(page.locator('.oxa-filter-primary-item')).toHaveCount(0);
425
+ await expect(
426
+ page.getByPlaceholder('搜索栏目名称或栏目编码')
427
+ ).toBeVisible();
428
+ const more = page.getByRole('button', { name: '筛选' });
429
+ await more.click();
430
+ const modal = page.getByRole('dialog', { name: '筛选' });
431
+ await expect(modal).toBeVisible();
432
+ await modal.getByRole('button', { name: '添加条件', exact: true }).click();
433
+ await modal.getByRole('combobox', { name: '筛选字段' }).click();
434
+ await expect(page.getByRole('option', { name: '栏目管理员' })).toBeVisible();
435
+ await expect(page.getByRole('option', { name: '配套设施' })).toBeVisible();
436
+ await expect(page.getByRole('option', { name: '开放星期' })).toBeVisible();
437
+ await expect
438
+ .poll(async () => (await modal.boundingBox())?.width || 0)
439
+ .toBeGreaterThan(700);
440
+ });
441
+
442
+ test('renders platform mobile controls from declared field widgets', async ({ page }) => {
443
+ const candidate = codes
444
+ .map(code => ({ code, definition: definitions[code] }))
445
+ .find(item =>
446
+ item.definition.surface.mobile?.enabled &&
447
+ Object.values(item.definition.surface.fields).some(field =>
448
+ ['directory-user', 'directory-department', 'resource', 'file'].includes(field.widget || '')
449
+ )
450
+ );
451
+ test.skip(!candidate, 'No mobile platform field is declared.');
452
+ await mockPlatform(page);
453
+ await page.setViewportSize({ width: 390, height: 844 });
454
+ await page.goto(
455
+ `${runtimeBase}/m/admin/resources/${candidate!.code}/new`,
456
+ );
457
+ await expect(page.locator('.oxa-mobile-form')).toBeVisible();
458
+
459
+ const widgets = new Set(Object.values(candidate!.definition.surface.fields).map(field => field.widget));
460
+ if (widgets.has('directory-user') || widgets.has('directory-department')) {
461
+ const trigger = page.locator('.oxa-directory-trigger').first();
462
+ await expect(trigger).toBeVisible();
463
+ await trigger.click();
464
+ await expect(page.locator('.oxa-directory-mobile-drawer')).toBeVisible();
465
+ await page.keyboard.press('Escape');
466
+ }
467
+ if (widgets.has('resource')) {
468
+ const trigger = page.locator('.oxa-mobile-reference-trigger').first();
469
+ await expect(trigger).toBeVisible();
470
+ await trigger.click();
471
+ await expect(page.locator('.oxa-mobile-reference-sheet')).toBeVisible();
472
+ await page.keyboard.press('Escape');
473
+ }
474
+ if (widgets.has('file')) {
475
+ await expect(page.locator('.oxa-mobile-file-field')).toBeVisible();
476
+ await expect(page.locator('.oxa-mobile-file-field input[type="file"]')).toBeHidden();
477
+ await expect(page.getByRole('button', { name: '上传文件' })).toBeVisible();
478
+ }
479
+ });
480
+
481
+ test('fails closed when the current user lacks the declared read capability', async ({ page }) => {
482
+ test.skip(!codes.length, 'No resource permission exists in an empty application.');
483
+ await mockPlatform(page, false);
484
+ const code = codes[0];
485
+ await page.goto(`${runtimeBase}/admin/resources/${code}`);
486
+ await expect(page.getByText(new RegExp(`当前平台用户无${definitions[code].name}页面权限`))).toBeVisible();
487
+ await expect(page.locator('.oxa-list-surface')).toHaveCount(0);
488
+ });