@greensecurity/javascript-sdk 0.46.0-beta.9 → 0.47.0-beta.0

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 (223) hide show
  1. package/dist/commonjs/__tests__/apps/contract.test.js +288 -4
  2. package/dist/commonjs/__tests__/apps/contract.test.js.map +1 -1
  3. package/dist/commonjs/__tests__/apps/devpreview-routes.test.d.ts +2 -0
  4. package/dist/commonjs/__tests__/apps/devpreview-routes.test.d.ts.map +1 -0
  5. package/dist/commonjs/__tests__/apps/devpreview-routes.test.js +86 -0
  6. package/dist/commonjs/__tests__/apps/devpreview-routes.test.js.map +1 -0
  7. package/dist/commonjs/__tests__/apps/kit/example.test.js +84 -4
  8. package/dist/commonjs/__tests__/apps/kit/example.test.js.map +1 -1
  9. package/dist/commonjs/__tests__/apps/kit/pages.test.d.ts +2 -0
  10. package/dist/commonjs/__tests__/apps/kit/pages.test.d.ts.map +1 -0
  11. package/dist/commonjs/__tests__/apps/kit/pages.test.js +119 -0
  12. package/dist/commonjs/__tests__/apps/kit/pages.test.js.map +1 -0
  13. package/dist/commonjs/__tests__/apps/kit/render.test.js +24 -0
  14. package/dist/commonjs/__tests__/apps/kit/render.test.js.map +1 -1
  15. package/dist/commonjs/__tests__/apps/kit/store.test.d.ts +2 -0
  16. package/dist/commonjs/__tests__/apps/kit/store.test.d.ts.map +1 -0
  17. package/dist/commonjs/__tests__/apps/kit/store.test.js +123 -0
  18. package/dist/commonjs/__tests__/apps/kit/store.test.js.map +1 -0
  19. package/dist/commonjs/__tests__/apps/post-install-page.test.d.ts +2 -0
  20. package/dist/commonjs/__tests__/apps/post-install-page.test.d.ts.map +1 -0
  21. package/dist/commonjs/__tests__/apps/post-install-page.test.js +110 -0
  22. package/dist/commonjs/__tests__/apps/post-install-page.test.js.map +1 -0
  23. package/dist/commonjs/__tests__/apps/routes.test.d.ts +2 -0
  24. package/dist/commonjs/__tests__/apps/routes.test.d.ts.map +1 -0
  25. package/dist/commonjs/__tests__/apps/routes.test.js +238 -0
  26. package/dist/commonjs/__tests__/apps/routes.test.js.map +1 -0
  27. package/dist/commonjs/__tests__/vendors.test.js +1 -1
  28. package/dist/commonjs/__tests__/zoneDevicesCreate.test.d.ts +2 -0
  29. package/dist/commonjs/__tests__/zoneDevicesCreate.test.d.ts.map +1 -0
  30. package/dist/commonjs/__tests__/zoneDevicesCreate.test.js +47 -0
  31. package/dist/commonjs/__tests__/zoneDevicesCreate.test.js.map +1 -0
  32. package/dist/commonjs/__tests__/zones.test.js +4 -28
  33. package/dist/commonjs/__tests__/zones.test.js.map +1 -1
  34. package/dist/commonjs/admin-funcs/zoneDevicesCreate.d.ts +11 -0
  35. package/dist/commonjs/admin-funcs/zoneDevicesCreate.d.ts.map +1 -0
  36. package/dist/commonjs/admin-funcs/zoneDevicesCreate.js +14 -0
  37. package/dist/commonjs/admin-funcs/zoneDevicesCreate.js.map +1 -0
  38. package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.d.ts +3 -0
  39. package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.d.ts.map +1 -0
  40. package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.js +11 -0
  41. package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.js.map +1 -0
  42. package/dist/commonjs/apps/components.d.ts +321 -0
  43. package/dist/commonjs/apps/components.d.ts.map +1 -1
  44. package/dist/commonjs/apps/components.js +168 -2
  45. package/dist/commonjs/apps/components.js.map +1 -1
  46. package/dist/commonjs/apps/context.d.ts +24 -0
  47. package/dist/commonjs/apps/context.d.ts.map +1 -1
  48. package/dist/commonjs/apps/context.js.map +1 -1
  49. package/dist/commonjs/apps/devpreview.d.ts +20 -3
  50. package/dist/commonjs/apps/devpreview.d.ts.map +1 -1
  51. package/dist/commonjs/apps/devpreview.js +18 -7
  52. package/dist/commonjs/apps/devpreview.js.map +1 -1
  53. package/dist/commonjs/apps/hooks/index.d.ts +33 -2
  54. package/dist/commonjs/apps/hooks/index.d.ts.map +1 -1
  55. package/dist/commonjs/apps/hooks/index.js +41 -1
  56. package/dist/commonjs/apps/hooks/index.js.map +1 -1
  57. package/dist/commonjs/apps/hooks/store.d.ts +48 -0
  58. package/dist/commonjs/apps/hooks/store.d.ts.map +1 -0
  59. package/dist/commonjs/apps/hooks/store.js +87 -0
  60. package/dist/commonjs/apps/hooks/store.js.map +1 -0
  61. package/dist/commonjs/apps/manifest.d.ts +64 -3
  62. package/dist/commonjs/apps/manifest.d.ts.map +1 -1
  63. package/dist/commonjs/apps/manifest.js +128 -27
  64. package/dist/commonjs/apps/manifest.js.map +1 -1
  65. package/dist/commonjs/apps/protocol.d.ts +1 -1
  66. package/dist/commonjs/apps/protocol.d.ts.map +1 -1
  67. package/dist/commonjs/apps/route.d.ts +11 -1
  68. package/dist/commonjs/apps/route.d.ts.map +1 -1
  69. package/dist/commonjs/apps/route.js +13 -7
  70. package/dist/commonjs/apps/route.js.map +1 -1
  71. package/dist/commonjs/apps/routes.d.ts +65 -0
  72. package/dist/commonjs/apps/routes.d.ts.map +1 -0
  73. package/dist/commonjs/apps/routes.js +202 -0
  74. package/dist/commonjs/apps/routes.js.map +1 -0
  75. package/dist/commonjs/apps/surfaces.d.ts +20 -2
  76. package/dist/commonjs/apps/surfaces.d.ts.map +1 -1
  77. package/dist/commonjs/apps/surfaces.js +45 -7
  78. package/dist/commonjs/apps/surfaces.js.map +1 -1
  79. package/dist/commonjs/apps/testing/index.d.ts +17 -1
  80. package/dist/commonjs/apps/testing/index.d.ts.map +1 -1
  81. package/dist/commonjs/apps/testing/index.js +50 -1
  82. package/dist/commonjs/apps/testing/index.js.map +1 -1
  83. package/dist/commonjs/apps/ui/index.d.ts +13 -0
  84. package/dist/commonjs/apps/ui/index.d.ts.map +1 -1
  85. package/dist/commonjs/apps/ui/index.js +19 -2
  86. package/dist/commonjs/apps/ui/index.js.map +1 -1
  87. package/dist/commonjs/index.extras.d.ts +4 -0
  88. package/dist/commonjs/index.extras.d.ts.map +1 -1
  89. package/dist/commonjs/index.extras.js +5 -1
  90. package/dist/commonjs/index.extras.js.map +1 -1
  91. package/dist/commonjs/lib/config.d.ts +3 -3
  92. package/dist/commonjs/lib/config.js +3 -3
  93. package/dist/commonjs/models/components/vendorcredentialsummary.d.ts +7 -0
  94. package/dist/commonjs/models/components/vendorcredentialsummary.d.ts.map +1 -1
  95. package/dist/commonjs/models/components/vendorcredentialsummary.js +2 -0
  96. package/dist/commonjs/models/components/vendorcredentialsummary.js.map +1 -1
  97. package/dist/esm/__tests__/apps/contract.test.js +289 -5
  98. package/dist/esm/__tests__/apps/contract.test.js.map +1 -1
  99. package/dist/esm/__tests__/apps/devpreview-routes.test.d.ts +2 -0
  100. package/dist/esm/__tests__/apps/devpreview-routes.test.d.ts.map +1 -0
  101. package/dist/esm/__tests__/apps/devpreview-routes.test.js +84 -0
  102. package/dist/esm/__tests__/apps/devpreview-routes.test.js.map +1 -0
  103. package/dist/esm/__tests__/apps/kit/example.test.js +85 -5
  104. package/dist/esm/__tests__/apps/kit/example.test.js.map +1 -1
  105. package/dist/esm/__tests__/apps/kit/pages.test.d.ts +2 -0
  106. package/dist/esm/__tests__/apps/kit/pages.test.d.ts.map +1 -0
  107. package/dist/esm/__tests__/apps/kit/pages.test.js +117 -0
  108. package/dist/esm/__tests__/apps/kit/pages.test.js.map +1 -0
  109. package/dist/esm/__tests__/apps/kit/render.test.js +25 -1
  110. package/dist/esm/__tests__/apps/kit/render.test.js.map +1 -1
  111. package/dist/esm/__tests__/apps/kit/store.test.d.ts +2 -0
  112. package/dist/esm/__tests__/apps/kit/store.test.d.ts.map +1 -0
  113. package/dist/esm/__tests__/apps/kit/store.test.js +121 -0
  114. package/dist/esm/__tests__/apps/kit/store.test.js.map +1 -0
  115. package/dist/esm/__tests__/apps/post-install-page.test.d.ts +2 -0
  116. package/dist/esm/__tests__/apps/post-install-page.test.d.ts.map +1 -0
  117. package/dist/esm/__tests__/apps/post-install-page.test.js +108 -0
  118. package/dist/esm/__tests__/apps/post-install-page.test.js.map +1 -0
  119. package/dist/esm/__tests__/apps/routes.test.d.ts +2 -0
  120. package/dist/esm/__tests__/apps/routes.test.d.ts.map +1 -0
  121. package/dist/esm/__tests__/apps/routes.test.js +236 -0
  122. package/dist/esm/__tests__/apps/routes.test.js.map +1 -0
  123. package/dist/esm/__tests__/vendors.test.js +1 -1
  124. package/dist/esm/__tests__/zoneDevicesCreate.test.d.ts +2 -0
  125. package/dist/esm/__tests__/zoneDevicesCreate.test.d.ts.map +1 -0
  126. package/dist/esm/__tests__/zoneDevicesCreate.test.js +45 -0
  127. package/dist/esm/__tests__/zoneDevicesCreate.test.js.map +1 -0
  128. package/dist/esm/__tests__/zones.test.js +4 -28
  129. package/dist/esm/__tests__/zones.test.js.map +1 -1
  130. package/dist/esm/admin-funcs/zoneDevicesCreate.d.ts +11 -0
  131. package/dist/esm/admin-funcs/zoneDevicesCreate.d.ts.map +1 -0
  132. package/dist/esm/admin-funcs/zoneDevicesCreate.js +11 -0
  133. package/dist/esm/admin-funcs/zoneDevicesCreate.js.map +1 -0
  134. package/dist/esm/admin-funcs/zoneDevicesMacAvailability.d.ts +3 -0
  135. package/dist/esm/admin-funcs/zoneDevicesMacAvailability.d.ts.map +1 -0
  136. package/dist/esm/admin-funcs/zoneDevicesMacAvailability.js +8 -0
  137. package/dist/esm/admin-funcs/zoneDevicesMacAvailability.js.map +1 -0
  138. package/dist/esm/apps/components.d.ts +321 -0
  139. package/dist/esm/apps/components.d.ts.map +1 -1
  140. package/dist/esm/apps/components.js +163 -1
  141. package/dist/esm/apps/components.js.map +1 -1
  142. package/dist/esm/apps/context.d.ts +24 -0
  143. package/dist/esm/apps/context.d.ts.map +1 -1
  144. package/dist/esm/apps/context.js.map +1 -1
  145. package/dist/esm/apps/devpreview.d.ts +20 -3
  146. package/dist/esm/apps/devpreview.d.ts.map +1 -1
  147. package/dist/esm/apps/devpreview.js +19 -8
  148. package/dist/esm/apps/devpreview.js.map +1 -1
  149. package/dist/esm/apps/hooks/index.d.ts +33 -2
  150. package/dist/esm/apps/hooks/index.d.ts.map +1 -1
  151. package/dist/esm/apps/hooks/index.js +37 -1
  152. package/dist/esm/apps/hooks/index.js.map +1 -1
  153. package/dist/esm/apps/hooks/store.d.ts +48 -0
  154. package/dist/esm/apps/hooks/store.d.ts.map +1 -0
  155. package/dist/esm/apps/hooks/store.js +84 -0
  156. package/dist/esm/apps/hooks/store.js.map +1 -0
  157. package/dist/esm/apps/manifest.d.ts +64 -3
  158. package/dist/esm/apps/manifest.d.ts.map +1 -1
  159. package/dist/esm/apps/manifest.js +116 -26
  160. package/dist/esm/apps/manifest.js.map +1 -1
  161. package/dist/esm/apps/protocol.d.ts +1 -1
  162. package/dist/esm/apps/protocol.d.ts.map +1 -1
  163. package/dist/esm/apps/route.d.ts +11 -1
  164. package/dist/esm/apps/route.d.ts.map +1 -1
  165. package/dist/esm/apps/route.js +14 -8
  166. package/dist/esm/apps/route.js.map +1 -1
  167. package/dist/esm/apps/routes.d.ts +65 -0
  168. package/dist/esm/apps/routes.d.ts.map +1 -0
  169. package/dist/esm/apps/routes.js +194 -0
  170. package/dist/esm/apps/routes.js.map +1 -0
  171. package/dist/esm/apps/surfaces.d.ts +20 -2
  172. package/dist/esm/apps/surfaces.d.ts.map +1 -1
  173. package/dist/esm/apps/surfaces.js +39 -2
  174. package/dist/esm/apps/surfaces.js.map +1 -1
  175. package/dist/esm/apps/testing/index.d.ts +17 -1
  176. package/dist/esm/apps/testing/index.d.ts.map +1 -1
  177. package/dist/esm/apps/testing/index.js +49 -1
  178. package/dist/esm/apps/testing/index.js.map +1 -1
  179. package/dist/esm/apps/ui/index.d.ts +13 -0
  180. package/dist/esm/apps/ui/index.d.ts.map +1 -1
  181. package/dist/esm/apps/ui/index.js +17 -1
  182. package/dist/esm/apps/ui/index.js.map +1 -1
  183. package/dist/esm/index.extras.d.ts +4 -0
  184. package/dist/esm/index.extras.d.ts.map +1 -1
  185. package/dist/esm/index.extras.js +5 -1
  186. package/dist/esm/index.extras.js.map +1 -1
  187. package/dist/esm/lib/config.d.ts +3 -3
  188. package/dist/esm/lib/config.js +3 -3
  189. package/dist/esm/models/components/vendorcredentialsummary.d.ts +7 -0
  190. package/dist/esm/models/components/vendorcredentialsummary.d.ts.map +1 -1
  191. package/dist/esm/models/components/vendorcredentialsummary.js +2 -0
  192. package/dist/esm/models/components/vendorcredentialsummary.js.map +1 -1
  193. package/examples/apps/hello-green/src/store.ts +15 -0
  194. package/jsr.json +1 -1
  195. package/package.json +1 -1
  196. package/src/__tests__/apps/contract.test.ts +305 -4
  197. package/src/__tests__/apps/devpreview-routes.test.ts +103 -0
  198. package/src/__tests__/apps/kit/example.test.ts +94 -5
  199. package/src/__tests__/apps/kit/pages.test.ts +137 -0
  200. package/src/__tests__/apps/kit/render.test.ts +35 -1
  201. package/src/__tests__/apps/kit/store.test.ts +149 -0
  202. package/src/__tests__/apps/post-install-page.test.ts +126 -0
  203. package/src/__tests__/apps/routes.test.ts +318 -0
  204. package/src/__tests__/vendors.test.ts +1 -1
  205. package/src/__tests__/zoneDevicesCreate.test.ts +56 -0
  206. package/src/__tests__/zones.test.ts +4 -28
  207. package/src/admin-funcs/zoneDevicesCreate.ts +22 -0
  208. package/src/admin-funcs/zoneDevicesMacAvailability.ts +8 -0
  209. package/src/apps/components.ts +183 -1
  210. package/src/apps/context.ts +25 -0
  211. package/src/apps/devpreview.ts +20 -7
  212. package/src/apps/hooks/index.ts +42 -2
  213. package/src/apps/hooks/store.ts +115 -0
  214. package/src/apps/manifest.ts +151 -27
  215. package/src/apps/protocol.ts +1 -1
  216. package/src/apps/route.ts +22 -10
  217. package/src/apps/routes.ts +230 -0
  218. package/src/apps/surfaces.ts +45 -12
  219. package/src/apps/testing/index.ts +48 -2
  220. package/src/apps/ui/index.ts +18 -1
  221. package/src/index.extras.ts +5 -1
  222. package/src/lib/config.ts +3 -3
  223. package/src/models/components/vendorcredentialsummary.ts +9 -0
@@ -12,6 +12,8 @@ import { describe, expect, test } from "vitest";
12
12
  import {
13
13
  canonicalBuildPayload,
14
14
  APP_WEBHOOK_EVENTS,
15
+ CHART_DATA_LIMITS,
16
+ CHART_THEMES,
15
17
  COMPONENTS,
16
18
  componentsCatalogJson,
17
19
  CSP_DENYLIST,
@@ -25,11 +27,18 @@ import {
25
27
  getSurface,
26
28
  isBootstrapToHost,
27
29
  isCalendarDate,
30
+ isChartData,
31
+ isPieChartData,
32
+ CHART_OTHER_LABEL,
33
+ MAX_CHART_VALUE,
34
+ isComboChartData,
28
35
  isPlatformPermission,
29
36
  isRouteDescriptor,
30
37
  isSensitivePermission,
38
+ isSeriesChartData,
31
39
  isSurfaceId,
32
40
  manifestJsonSchema,
41
+ MAX_CHART_SERIES,
33
42
  mergeDevManifest,
34
43
  permissionReviewIssues,
35
44
  PLATFORM_PERMISSIONS,
@@ -41,6 +50,9 @@ import {
41
50
  tokenNeedsRefresh,
42
51
  validateManifest,
43
52
  type AppManifestV2,
53
+ type ChartClickEvent,
54
+ type ComboChartData,
55
+ type SeriesChartData,
44
56
  type ComponentProps,
45
57
  type HostToBootstrap,
46
58
  } from "../../apps/index.js";
@@ -86,7 +98,7 @@ describe("surfaces", () => {
86
98
  expect(new Set(ids).size).toBe(ids.length);
87
99
 
88
100
  for (const s of SURFACES) {
89
- if (s.id === "settings") continue;
101
+ if (s.portal === null) continue;
90
102
  expect(s.id).toMatch(/^gs\.(vendor|company|facility|system)\.[a-z_]+(\.[a-z_]+)*$/);
91
103
  expect(s.id.startsWith(`gs.${s.portal}.`)).toBe(true);
92
104
  expect(s.id.endsWith(s.kind)).toBe(true);
@@ -98,6 +110,11 @@ describe("surfaces", () => {
98
110
  expect(s.route.startsWith(s.portal === null ? "/[portal]/" : `/${s.portal}`)).toBe(true);
99
111
  }
100
112
  expect(getSurface("settings")?.route).toBe("/[portal]/apps/[appId]/settings");
113
+ expect(getSurface("page")?.route).toBe("/[portal]/apps/[appId]/pages/[...path]");
114
+ });
115
+
116
+ test("the only portal-less surfaces are the app's own settings page and pages", () => {
117
+ expect(SURFACES.filter((s) => s.portal === null).map((s) => s.id).sort()).toEqual(["page", "settings"]);
101
118
  });
102
119
 
103
120
  test("wave 1 is the vendor and company portals only", () => {
@@ -482,9 +499,18 @@ describe("components", () => {
482
499
  expect(COMPONENTS.Text.children).toEqual(["ui-inline", "ui-link"]);
483
500
  });
484
501
 
485
- test("only ContextView and SettingsView are roots", () => {
502
+ test("only ContextView, SettingsView and PageView are roots", () => {
486
503
  const roots = Object.entries(COMPONENTS).filter(([, c]) => "root" in c).map(([name]) => name);
487
- expect(roots.sort()).toEqual(["ContextView", "SettingsView"]);
504
+ expect(roots.sort()).toEqual(["ContextView", "PageView", "SettingsView"]);
505
+ });
506
+
507
+ test("PageView takes a required title, a description and the actions and banner slots", () => {
508
+ expect(COMPONENTS.PageView.tagName).toBe("ui-page-view");
509
+ expect(COMPONENTS.PageView.slots).toEqual(["actions", "banner"]);
510
+ expect(sanitizeProps("ui-page-view", { title: "Reports", description: "All of them" }).props).toEqual({ title: "Reports", description: "All of them" });
511
+ expect(sanitizeProps("ui-page-view", { title: "t".repeat(101) }).dropped).toEqual(["title"]);
512
+ expect(sanitizeProps("ui-page-view", { title: "Reports", description: "d".repeat(301) }).dropped).toEqual(["description"]);
513
+ expect(componentsCatalogJson().find((c) => c.name === "PageView")).toMatchObject({ tagName: "ui-page-view", root: true });
488
514
  });
489
515
 
490
516
  test("sanitizeProps keeps declared props of the right type and drops everything else", () => {
@@ -688,6 +714,281 @@ describe("components", () => {
688
714
  });
689
715
  });
690
716
 
717
+ describe("charts", () => {
718
+ const CHART_TAGS: Record<string, string> = {
719
+ BarChart: "ui-bar-chart",
720
+ HorizontalBarChart: "ui-horizontal-bar-chart",
721
+ StackedBarChart: "ui-stacked-bar-chart",
722
+ LineChart: "ui-line-chart",
723
+ AreaChart: "ui-area-chart",
724
+ ComboChart: "ui-combo-chart",
725
+ PieChart: "ui-pie-chart",
726
+ };
727
+ const labels = (n: number) => Array.from({ length: n }, (_, i) => `L${i}`);
728
+ const datums = (n: number) => labels(n).map((label, i) => ({ label, value: i }));
729
+ const seriesData = (nLabels: number, nSeries = 2): SeriesChartData => ({
730
+ labels: labels(nLabels),
731
+ series: Array.from({ length: nSeries }, (_, s) => ({ name: `S${s}`, values: labels(nLabels).map((_, i) => i + s) })),
732
+ });
733
+ const combo = (n: number): ComboChartData => ({
734
+ labels: labels(n),
735
+ bars: { name: "Visits", values: labels(n).map((_, i) => i) },
736
+ line: { name: "Average", values: labels(n).map((_, i) => i / 2) },
737
+ });
738
+ const BAD_VALUES: unknown[] = [-1, Number.NaN, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, "3", null];
739
+ const BAD_LABELS: unknown[] = ["", " ", "x".repeat(61), 5, null];
740
+ class Datum { constructor(public label: string, public value: number) {} }
741
+ class DatumArray extends Array<unknown> {}
742
+
743
+ test("seven chart entries with the same data-only props, a click event and no children", () => {
744
+ for (const [name, tag] of Object.entries(CHART_TAGS)) {
745
+ const c = COMPONENTS[name as keyof typeof COMPONENTS];
746
+ expect(c.tagName, name).toBe(tag);
747
+ expect(Object.keys(c.props).sort(), name).toEqual(["data", "interactive", "theme"]);
748
+ expect(c.events, name).toEqual({ onClick: { event: "click" } });
749
+ expect(c.children, name).toEqual([]);
750
+ expect(c.text, name).toBe(false);
751
+ expect(c.slots, name).toEqual([]);
752
+ const json = componentsCatalogJson().find((j) => j.name === name)!;
753
+ expect(json.props["data"]).toEqual({ type: "json", required: true });
754
+ expect(json.props["theme"]).toEqual({ type: "enum", required: false, values: ["green", "grayscale", "vibrant"] });
755
+ expect(json.events).toEqual({ onClick: "click" });
756
+ }
757
+ expect(CHART_THEMES).toEqual(["green", "grayscale", "vibrant"]);
758
+ expect(MAX_CHART_SERIES).toBe(6);
759
+ });
760
+
761
+ test("sanitizeProps requires data, checks theme and drops styling props", () => {
762
+ expect(sanitizeProps("ui-bar-chart", { theme: "green" }).missing).toEqual(["data"]);
763
+ const result = sanitizeProps("ui-bar-chart", { data: datums(3), theme: "neon", interactive: true, color: "#f00", style: {} });
764
+ expect(result.props).toEqual({ data: datums(3), interactive: true });
765
+ expect(result.dropped.sort()).toEqual(["color", "style", "theme"]);
766
+ expect(result.missing).toEqual([]);
767
+ const bad = sanitizeProps("ui-line-chart", { data: seriesData(1) });
768
+ expect(bad.dropped).toEqual(["data"]);
769
+ expect(bad.missing).toEqual(["data"]);
770
+ });
771
+
772
+ describe("chart value bounds", () => {
773
+ test("accepts values up to MAX_CHART_VALUE and rejects larger ones", () => {
774
+ const bar = CHART_DATA_LIMITS.BarChart;
775
+ expect(isChartData([{ label: "a", value: MAX_CHART_VALUE }], bar)).toBe(true);
776
+ for (const value of [MAX_CHART_VALUE + 2, 1e308, Number.MAX_VALUE]) {
777
+ expect(isChartData([{ label: "a", value }], bar)).toBe(false);
778
+ }
779
+ });
780
+
781
+ test("keeps every accepted total finite", () => {
782
+ const pie = Array.from({ length: 50 }, (_, i) => ({ label: `s${i}`, value: MAX_CHART_VALUE }));
783
+ expect(isPieChartData(pie)).toBe(true);
784
+ expect(Number.isFinite(pie.reduce((sum, d) => sum + d.value, 0))).toBe(true);
785
+ const limits = CHART_DATA_LIMITS.StackedBarChart;
786
+ const series = Array.from({ length: 6 }, (_, i) => ({ name: `s${i}`, values: [MAX_CHART_VALUE] }));
787
+ expect(isSeriesChartData({ labels: ["a"], series }, limits)).toBe(true);
788
+ const huge = [{ name: "x", values: [1e308] }, { name: "y", values: [1e308] }];
789
+ expect(isSeriesChartData({ labels: ["a"], series: huge }, limits)).toBe(false);
790
+ });
791
+ });
792
+
793
+ describe("isPieChartData", () => {
794
+ const datums = (n: number, first?: string) =>
795
+ Array.from({ length: n }, (_, i) => ({ label: i === 0 && first !== undefined ? first : `s${i}`, value: i + 1 }));
796
+
797
+ test("allows an Other label when the pie will not combine slices", () => {
798
+ expect(isPieChartData(datums(6, CHART_OTHER_LABEL))).toBe(true);
799
+ });
800
+
801
+ test("rejects a caller Other label when extra items will be combined into Other", () => {
802
+ for (const label of [CHART_OTHER_LABEL, "other", " OTHER "]) expect(isPieChartData(datums(7, label))).toBe(false);
803
+ expect(isPieChartData(datums(7, "Other documents"))).toBe(true);
804
+ });
805
+
806
+ test("keeps the generic pie limits", () => {
807
+ expect(isPieChartData(datums(50))).toBe(true);
808
+ expect(isPieChartData(datums(51))).toBe(false);
809
+ expect(isPieChartData([])).toBe(false);
810
+ });
811
+
812
+ test("is the guard the PieChart catalog entry uses", () => {
813
+ expect(sanitizeProps("ui-pie-chart", { data: datums(7, "Other") }).missing).toContain("data");
814
+ expect(sanitizeProps("ui-pie-chart", { data: datums(7) }).missing).not.toContain("data");
815
+ });
816
+ });
817
+
818
+ describe("isChartData", () => {
819
+ const bar = CHART_DATA_LIMITS.BarChart;
820
+ test("accepts a valid sample, including zero values and 60-character labels", () => {
821
+ expect(isChartData([{ label: "Mon", value: 0 }, { label: "x".repeat(60), value: 12.5 }], bar)).toBe(true);
822
+ });
823
+ test.each(BAD_VALUES)("rejects value %s", (value) => {
824
+ expect(isChartData([{ label: "a", value }], bar)).toBe(false);
825
+ });
826
+ test.each(BAD_LABELS)("rejects label %j", (label) => {
827
+ expect(isChartData([{ label, value: 1 }], bar)).toBe(false);
828
+ });
829
+ test("rejects duplicate labels", () => {
830
+ expect(isChartData([{ label: "a", value: 1 }, { label: "a", value: 2 }], bar)).toBe(false);
831
+ });
832
+ test("enforces each component's item counts", () => {
833
+ expect(isChartData([], bar)).toBe(false);
834
+ expect(isChartData(datums(31), bar)).toBe(true);
835
+ expect(isChartData(datums(32), bar)).toBe(false);
836
+ expect(isChartData(datums(25), CHART_DATA_LIMITS.HorizontalBarChart)).toBe(true);
837
+ expect(isChartData(datums(26), CHART_DATA_LIMITS.HorizontalBarChart)).toBe(false);
838
+ expect(isChartData(datums(50), CHART_DATA_LIMITS.PieChart)).toBe(true);
839
+ expect(isChartData(datums(51), CHART_DATA_LIMITS.PieChart)).toBe(false);
840
+ expect(sanitizeProps("ui-bar-chart", { data: datums(32) }).dropped).toEqual(["data"]);
841
+ expect(sanitizeProps("ui-pie-chart", { data: datums(32) }).dropped).toEqual([]);
842
+ });
843
+ test("rejects non-arrays, class instances, sparse arrays and extra keys", () => {
844
+ for (const v of [undefined, null, "a", 1, { label: "a", value: 1 }, new Map()]) expect(isChartData(v, bar)).toBe(false);
845
+ expect(isChartData([new Datum("a", 1)], bar)).toBe(false);
846
+ expect(isChartData(DatumArray.from([{ label: "a", value: 1 }]), bar)).toBe(false);
847
+ // eslint-disable-next-line no-sparse-arrays
848
+ expect(isChartData([{ label: "a", value: 1 }, , { label: "b", value: 2 }], bar)).toBe(false);
849
+ expect(isChartData([{ label: "a", value: 1, color: "red" }], bar)).toBe(false);
850
+ expect(isChartData([Object.assign(Object.create(null) as object, { label: "a", value: 1 })], bar)).toBe(true);
851
+ });
852
+ });
853
+
854
+ describe("isSeriesChartData", () => {
855
+ const stacked = CHART_DATA_LIMITS.StackedBarChart;
856
+ const line = CHART_DATA_LIMITS.LineChart;
857
+ test("accepts a valid sample", () => {
858
+ expect(isSeriesChartData(seriesData(5, 3), stacked)).toBe(true);
859
+ expect(isSeriesChartData(seriesData(5, MAX_CHART_SERIES), line)).toBe(true);
860
+ });
861
+ test("rejects more than six series, and no series", () => {
862
+ expect(isSeriesChartData(seriesData(5, MAX_CHART_SERIES + 1), stacked)).toBe(false);
863
+ expect(isSeriesChartData(seriesData(5, MAX_CHART_SERIES + 1), line)).toBe(false);
864
+ expect(isSeriesChartData(seriesData(5, 0), stacked)).toBe(false);
865
+ });
866
+ test("rejects a series whose values length differs from the labels", () => {
867
+ const d = seriesData(4);
868
+ d.series[1]!.values.pop();
869
+ expect(isSeriesChartData(d, stacked)).toBe(false);
870
+ d.series[1]!.values.push(1, 2);
871
+ expect(isSeriesChartData(d, stacked)).toBe(false);
872
+ });
873
+ test.each(BAD_VALUES)("rejects value %s", (value) => {
874
+ const d = seriesData(3) as { series: Array<{ values: unknown[] }> };
875
+ d.series[0]!.values[1] = value;
876
+ expect(isSeriesChartData(d, stacked)).toBe(false);
877
+ });
878
+ test.each(BAD_LABELS)("rejects label or series name %j", (bad) => {
879
+ const withLabel = seriesData(3) as { labels: unknown[] };
880
+ withLabel.labels[0] = bad;
881
+ expect(isSeriesChartData(withLabel, stacked)).toBe(false);
882
+ const withName = seriesData(3) as { series: Array<{ name: unknown }> };
883
+ withName.series[0]!.name = bad;
884
+ expect(isSeriesChartData(withName, stacked)).toBe(false);
885
+ });
886
+ test("rejects duplicate labels and duplicate series names", () => {
887
+ const dupLabel = seriesData(3);
888
+ dupLabel.labels[2] = dupLabel.labels[0]!;
889
+ expect(isSeriesChartData(dupLabel, stacked)).toBe(false);
890
+ const dupName = seriesData(3);
891
+ dupName.series[1]!.name = dupName.series[0]!.name;
892
+ expect(isSeriesChartData(dupName, stacked)).toBe(false);
893
+ });
894
+ test("enforces each component's label counts", () => {
895
+ expect(isSeriesChartData(seriesData(1), stacked)).toBe(true);
896
+ expect(isSeriesChartData(seriesData(31), stacked)).toBe(true);
897
+ expect(isSeriesChartData(seriesData(32), stacked)).toBe(false);
898
+ expect(isSeriesChartData(seriesData(1), line)).toBe(false);
899
+ expect(isSeriesChartData(seriesData(60), line)).toBe(true);
900
+ expect(isSeriesChartData(seriesData(61), line)).toBe(false);
901
+ expect(isSeriesChartData(seriesData(61), CHART_DATA_LIMITS.AreaChart)).toBe(false);
902
+ expect(isSeriesChartData(seriesData(1), CHART_DATA_LIMITS.AreaChart)).toBe(false);
903
+ });
904
+ test("rejects non-objects, class instances and extra keys", () => {
905
+ for (const v of [undefined, null, [], "x", datums(3)]) expect(isSeriesChartData(v, stacked)).toBe(false);
906
+ const d = seriesData(2);
907
+ expect(isSeriesChartData({ labels: d.labels, series: DatumArray.from(d.series) }, stacked)).toBe(false);
908
+ expect(isSeriesChartData({ ...d, labels: DatumArray.from(d.labels) }, stacked)).toBe(false);
909
+ expect(isSeriesChartData(Object.assign(new (class Data {})(), d), stacked)).toBe(false);
910
+ expect(isSeriesChartData({ ...d, title: "x" }, stacked)).toBe(false);
911
+ expect(isSeriesChartData({ ...d, series: [{ ...d.series[0]!, color: "red" }] }, stacked)).toBe(false);
912
+ });
913
+ });
914
+
915
+ describe("isComboChartData", () => {
916
+ const limits = CHART_DATA_LIMITS.ComboChart;
917
+ test("accepts a valid sample", () => {
918
+ expect(isComboChartData(combo(1), limits)).toBe(true);
919
+ expect(isComboChartData(combo(31), limits)).toBe(true);
920
+ });
921
+ test("rejects over-cap and empty labels", () => {
922
+ expect(isComboChartData(combo(32), limits)).toBe(false);
923
+ expect(isComboChartData(combo(0), limits)).toBe(false);
924
+ });
925
+ test("rejects bars or line values whose length differs from the labels", () => {
926
+ const bars = combo(3);
927
+ bars.bars.values.pop();
928
+ expect(isComboChartData(bars, limits)).toBe(false);
929
+ const line = combo(3);
930
+ line.line.values.push(1);
931
+ expect(isComboChartData(line, limits)).toBe(false);
932
+ });
933
+ test.each(BAD_VALUES)("rejects value %s", (value) => {
934
+ for (const key of ["bars", "line"] as const) {
935
+ const d = combo(3) as unknown as Record<typeof key, { values: unknown[] }>;
936
+ d[key].values[0] = value;
937
+ expect(isComboChartData(d, limits)).toBe(false);
938
+ }
939
+ });
940
+ test.each(BAD_LABELS)("rejects label or series name %j", (bad) => {
941
+ const withLabel = combo(3) as unknown as { labels: unknown[] };
942
+ withLabel.labels[1] = bad;
943
+ expect(isComboChartData(withLabel, limits)).toBe(false);
944
+ const withName = combo(3) as unknown as { line: { name: unknown } };
945
+ withName.line.name = bad;
946
+ expect(isComboChartData(withName, limits)).toBe(false);
947
+ });
948
+ test("rejects duplicate labels and a line named like the bars", () => {
949
+ const dupLabel = combo(3);
950
+ dupLabel.labels[1] = dupLabel.labels[0]!;
951
+ expect(isComboChartData(dupLabel, limits)).toBe(false);
952
+ const dupName = combo(3);
953
+ dupName.line.name = dupName.bars.name;
954
+ expect(isComboChartData(dupName, limits)).toBe(false);
955
+ });
956
+ test("rejects non-objects, class instances, series arrays and extra keys", () => {
957
+ for (const v of [undefined, null, [], "x", seriesData(3)]) expect(isComboChartData(v, limits)).toBe(false);
958
+ const d = combo(2);
959
+ expect(isComboChartData({ ...d, bars: [d.bars] }, limits)).toBe(false);
960
+ expect(isComboChartData({ ...d, labels: DatumArray.from(d.labels) }, limits)).toBe(false);
961
+ expect(isComboChartData(Object.assign(new (class Data {})(), d), limits)).toBe(false);
962
+ expect(isComboChartData({ ...d, extra: 1 }, limits)).toBe(false);
963
+ });
964
+ });
965
+
966
+ test("ComponentProps types chart data and the click detail (compile-time)", () => {
967
+ const bar: ComponentProps<"BarChart"> = {
968
+ data: [{ label: "Mon", value: 3 }],
969
+ theme: "vibrant",
970
+ interactive: true,
971
+ onClick: (e: ChartClickEvent) => e.index + e.value + e.label.length,
972
+ };
973
+ const comboProps: ComponentProps<"ComboChart"> = {
974
+ data: combo(2),
975
+ onClick: (e: ChartClickEvent) => e.series ?? e.label,
976
+ };
977
+ const stacked: ComponentProps<"StackedBarChart"> = { data: seriesData(2) };
978
+ // @ts-expect-error data is required
979
+ const noData: ComponentProps<"BarChart"> = { theme: "green" };
980
+ // @ts-expect-error a bar chart takes ChartDatum[], not series data
981
+ const wrongShape: ComponentProps<"BarChart"> = { data: seriesData(2) };
982
+ // @ts-expect-error themes are a closed set
983
+ const neon: ComponentProps<"PieChart"> = { data: datums(2), theme: "neon" };
984
+ // @ts-expect-error there are no colour props
985
+ const colored: ComponentProps<"LineChart"> = { data: seriesData(2), colors: ["#f00"] };
986
+ // @ts-expect-error onClick receives a ChartClickEvent, not a string
987
+ const wrongDetail: ComponentProps<"ComboChart"> = { data: combo(2), onClick: (s: string) => s.length };
988
+ expect([bar, comboProps, stacked, noData, wrongShape, neon, colored, wrongDetail]).toHaveLength(8);
989
+ });
990
+ });
991
+
691
992
  describe("protocol and helpers", () => {
692
993
  test("isBootstrapToHost accepts known envelopes only", () => {
693
994
  expect(isBootstrapToHost({ kind: "ready" })).toBe(true);
@@ -749,7 +1050,7 @@ describe("protocol and helpers", () => {
749
1050
  describe("bundle hygiene", () => {
750
1051
  // The host renderer imports these; a value import of zod (directly or via manifest.ts or
751
1052
  // devpreview.ts) would pull zod into the host app bundle. Type-only imports are erased.
752
- test.each(["surfaces.ts", "route.ts", "context.ts", "protocol.ts", "components.ts", "url.ts", "devsession.ts", "review.ts"])(
1053
+ test.each(["surfaces.ts", "route.ts", "routes.ts", "context.ts", "protocol.ts", "components.ts", "url.ts", "devsession.ts", "review.ts"])(
753
1054
  "%s has no value import of zod, manifest.ts or devpreview.ts",
754
1055
  (file) => {
755
1056
  const source = readFileSync(resolve(process.cwd(), "src", "apps", file), "utf8");
@@ -0,0 +1,103 @@
1
+ /*
2
+ * Hand-written. Dev manifests (`green-app.dev.json`) and app pages: a dev file may override
3
+ * `ui_extensions.routes` the way it overrides views, so `apps-cli dev` previews a page before it is
4
+ * published. The server mirrors these rules in `MarketplaceAppDevManifest`; see
5
+ * docs/third-party-apps/fullpageview-plan.md.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import {
10
+ APP_ROUTES_MAX,
11
+ devManifestSchema,
12
+ DevManifestError,
13
+ mergeDevManifest,
14
+ validateManifest,
15
+ type AppManifestV2,
16
+ } from "../../apps/index.js";
17
+
18
+ function page(path: string, fields: Record<string, unknown> = {}): Record<string, unknown> {
19
+ return { path, component: "Page", title: "Page", ...fields };
20
+ }
21
+
22
+ function published(overrides: Record<string, unknown> = {}): AppManifestV2 {
23
+ const result = validateManifest({
24
+ id: 42,
25
+ name: "Acme Reports",
26
+ icon: "https://cdn.acme.com/icon.png",
27
+ short_description: "Reports about your vendors.",
28
+ version: "1.0.0",
29
+ configuration_instructions: "Install and sign in.",
30
+ support_instructions: "support@acme.com",
31
+ ui_extensions: {
32
+ bundle: {
33
+ url: "https://cdn.acme.com/gs-app/1.0.0/app.js",
34
+ integrity: "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC",
35
+ },
36
+ views: [{ viewport: "settings", component: "AppSettings" }],
37
+ routes: [page("/reports")],
38
+ },
39
+ ...overrides,
40
+ });
41
+ if (!result.ok) throw new Error("fixture invalid");
42
+ return result.manifest;
43
+ }
44
+
45
+ function issuePaths(value: unknown): string[] {
46
+ const result = devManifestSchema.safeParse(value);
47
+ return result.success ? [] : result.error.issues.map((issue) => issue.path.join("."));
48
+ }
49
+
50
+ describe("dev manifest routes", () => {
51
+ test("a dev file may declare routes, with the published route rules", () => {
52
+ const dev = {
53
+ extends: "green-app.json",
54
+ ui_extensions: { routes: [page("/", { navigation: { label: "Home" } }), page("/reports/:id", { portals: ["vendor"] })] },
55
+ };
56
+
57
+ expect(devManifestSchema.safeParse(dev).success).toBe(true);
58
+ });
59
+
60
+ test("dev routes are held to the grammar, title and portals rules", () => {
61
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [page("/../settings")] } })).toEqual(["ui_extensions.routes.0.path"]);
62
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [{ path: "/reports", component: "Page" }] } })).toEqual(["ui_extensions.routes.0.title"]);
63
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [page("/reports", { portals: ["admin"] })] } })).toEqual(["ui_extensions.routes.0.portals.0"]);
64
+ const tooMany = Array.from({ length: APP_ROUTES_MAX + 1 }, (_, i) => page(`/p${i}`));
65
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: tooMany } })).toEqual(["ui_extensions.routes"]);
66
+ });
67
+
68
+ test("dev routes reject duplicate paths, repeated params and repeated portals, like published ones", () => {
69
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [page("/vendors/:id"), page("/vendors/:vendor_id")] } }))
70
+ .toEqual(["ui_extensions.routes.1.path"]);
71
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [page("/a/:id/b/:id")] } })).toEqual(["ui_extensions.routes.0.path"]);
72
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [page("/r", { portals: ["vendor", "vendor"] })] } }))
73
+ .toEqual(["ui_extensions.routes.0.portals"]);
74
+ expect(issuePaths({ extends: "x", ui_extensions: { routes: [page("/r/:id", { navigation: {} })] } }))
75
+ .toEqual(["ui_extensions.routes.0.navigation"]);
76
+ });
77
+
78
+ test("mergeDevManifest replaces the published routes when the dev file declares them", () => {
79
+ const routes = [page("/"), page("/reports/:id")];
80
+ const merged = mergeDevManifest(published(), devManifestSchema.parse({ extends: "green-app.json", ui_extensions: { routes } }));
81
+
82
+ expect(merged.ui_extensions?.routes).toEqual(routes);
83
+ expect(merged.ui_extensions?.views).toEqual([{ viewport: "settings", component: "AppSettings" }]);
84
+ });
85
+
86
+ test("mergeDevManifest keeps the published routes when the dev file leaves them out, and drops them for []", () => {
87
+ const base = published();
88
+
89
+ expect(mergeDevManifest(base, { extends: "green-app.json", ui_extensions: { views: [] } }).ui_extensions?.routes)
90
+ .toEqual([page("/reports")]);
91
+ expect(mergeDevManifest(base, { extends: "green-app.json" }).ui_extensions?.routes).toEqual([page("/reports")]);
92
+ expect(mergeDevManifest(base, { extends: "green-app.json", ui_extensions: { routes: [] } }).ui_extensions?.routes).toEqual([]);
93
+ });
94
+
95
+ test("a versionless (v1) base cannot be previewed with routes", () => {
96
+ const v1 = published({ version: undefined, ui_extensions: undefined });
97
+ const dev = devManifestSchema.parse({ extends: "green-app.json", ui_extensions: { routes: [page("/")] } });
98
+
99
+ expect(() => mergeDevManifest(v1, dev)).toThrow(DevManifestError);
100
+ expect(() => mergeDevManifest(v1, dev)).toThrow("the base manifest needs a version before its routes can be previewed");
101
+ expect(() => mergeDevManifest(v1, { extends: "green-app.json", ui_extensions: { routes: [] } })).not.toThrow();
102
+ });
103
+ });
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  * Hand-written. Keeps examples/apps/hello-green (the C15 template) valid against the C0
3
- * manifest contract. Building it and printing its SRI is `npm run build` in that folder.
3
+ * manifest contract, and renders its settings view and its pages. Building it and printing its SRI is `npm run build` in that folder.
4
4
  */
5
5
 
6
6
  import { spawnSync } from "node:child_process";
@@ -9,7 +9,7 @@ import { join } from "node:path";
9
9
  import { describe, expect, test, vi } from "vitest";
10
10
  import { createElement as h, type ComponentType } from "react";
11
11
  import { validateManifest } from "../../../apps/manifest.js";
12
- import { renderForTest } from "../../../apps/testing/index.js";
12
+ import { mockPageRoute, renderForTest } from "../../../apps/testing/index.js";
13
13
 
14
14
  // The example imports the SDK by package name, which resolves to the built `dist/` copy. Point
15
15
  // those imports at the sources under test so the example and this harness share one runtime.
@@ -105,14 +105,103 @@ describe("examples/apps/hello-green", () => {
105
105
  60_000,
106
106
  );
107
107
 
108
- test("every manifest view is registered with defineApp", () => {
108
+ test("every manifest view and page is registered with defineApp", () => {
109
109
  const result = validateManifest(manifest);
110
110
  if (!result.ok) throw new Error("invalid manifest");
111
111
  const entry = readFileSync(join(EXAMPLE_DIR, "src", "index.tsx"), "utf8");
112
112
  const views = /defineApp\(\{\s*views:\s*\{([^}]*)\}/.exec(entry)?.[1] ?? "";
113
113
  const registered = views.split(",").map((v) => v.trim()).filter(Boolean);
114
- for (const view of result.manifest.ui_extensions?.views ?? []) {
115
- expect(registered).toContain(view.component);
114
+ const components = [
115
+ ...(result.manifest.ui_extensions?.views ?? []).map((v) => v.component),
116
+ ...(result.manifest.ui_extensions?.routes ?? []).map((r) => r.component),
117
+ ];
118
+ expect(components).toEqual(expect.arrayContaining(["Reports", "ReportDetail"]));
119
+ for (const component of components) {
120
+ expect(registered).toContain(component);
116
121
  }
117
122
  });
123
+
124
+ test("it declares a listed reports page and a detail page", () => {
125
+ const result = validateManifest(manifest);
126
+ if (!result.ok) throw new Error("invalid manifest");
127
+ expect(result.manifest.ui_extensions?.routes).toEqual([
128
+ { path: "/reports", component: "Reports", title: "Reports", navigation: { label: "Hello reports" } },
129
+ { path: "/reports/:id", component: "ReportDetail", title: "Report" },
130
+ ]);
131
+ });
132
+ });
133
+
134
+ describe("hello-green pages", () => {
135
+ // Through a variable, for the same reason as AppSettings above.
136
+ const pagesModule = (name: string) => `../../../../examples/apps/hello-green/src/${name}.js`;
137
+
138
+ test("Reports links each row to its /reports/:id page by concrete path", async () => {
139
+ const { Reports } = (await import(/* @vite-ignore */ pagesModule("Reports"))) as { Reports: ComponentType };
140
+ const view = await renderForTest(h(Reports), {
141
+ context: { environment: { route: mockPageRoute("/reports") } },
142
+ });
143
+ expect(view.root.component).toBe("PageView");
144
+ expect(view.root.props["title"]).toBe("Reports");
145
+ await view.find("List")!.trigger("onAction", "8");
146
+ await view.root.slotted("actions")[0]!.trigger("onPress");
147
+ expect(view.host.callsTo("navigate")).toEqual([
148
+ [{ type: "appPage", path: "/reports/8" }],
149
+ [{ type: "appPage", path: "/reports/7" }],
150
+ ]);
151
+ });
152
+
153
+ test("the vendor panel and the Reports page share notes through the store, and the panel passes the vendor as navParams", async () => {
154
+ const { VendorPanel } = (await import(/* @vite-ignore */ pagesModule("VendorPanel"))) as { VendorPanel: ComponentType };
155
+ const { Reports } = (await import(/* @vite-ignore */ pagesModule("Reports"))) as { Reports: ComponentType };
156
+ const originalFetch = globalThis.fetch;
157
+ globalThis.fetch = (async () =>
158
+ new Response(JSON.stringify({ error: "Not found", success: false, code: 404 }), {
159
+ status: 404,
160
+ headers: { "content-type": "application/json" },
161
+ })) as typeof fetch;
162
+ try {
163
+ const panel = await renderForTest(h(VendorPanel), {
164
+ context: { environment: { surfaceId: "gs.company.vendor.detail", objectContext: { object: "vendor", id: 42 } } },
165
+ });
166
+ const reports = await renderForTest(h(Reports), {
167
+ context: { environment: { route: mockPageRoute("/reports"), navParams: { vendorId: "42" } } },
168
+ });
169
+ expect(reports.root.slotted("banner")[0]?.props["title"]).toBe("Opened from vendor #42");
170
+ expect(reports.text()).toContain("No notes saved this session.");
171
+
172
+ await panel.find("TextField")!.trigger("onChange", "Badge expired");
173
+ await panel.find("FocusView")!.slotted("primaryAction")[0]!.trigger("onPress");
174
+ await reports.settle();
175
+ expect(panel.text()).toContain("Notes saved this session: 1");
176
+ expect(reports.text()).toContain("Notes saved this session: 1. Latest, on vendor #42: Badge expired");
177
+
178
+ await panel.find("Button", (b) => b.text === "Open reports")!.trigger("onPress");
179
+ // `navigate()` without `{ host }` goes through the most recently mounted view's host, here
180
+ // the page's; both belong to the same app.
181
+ expect([...panel.host.callsTo("navigate"), ...reports.host.callsTo("navigate")]).toEqual([
182
+ [{ type: "appPage", path: "/reports", envParams: { vendorId: "42" } }],
183
+ ]);
184
+ expect([...panel.errors, ...reports.errors]).toEqual([]);
185
+ } finally {
186
+ globalThis.fetch = originalFetch;
187
+ }
188
+ });
189
+
190
+ test("ReportDetail reads its id from usePageRoute and follows a route change", async () => {
191
+ const { ReportDetail } = (await import(/* @vite-ignore */ pagesModule("ReportDetail"))) as { ReportDetail: ComponentType };
192
+ const view = await renderForTest(h(ReportDetail), {
193
+ context: { environment: { route: mockPageRoute("/reports/:id", "/reports/7") } },
194
+ });
195
+ expect(view.root.props["title"]).toBe("Weekly check-ins");
196
+
197
+ await view.updateContext({ environment: { ...view.context.environment, route: mockPageRoute("/reports/:id", "/reports/8") } });
198
+ expect(view.root.props["title"]).toBe("Expiring credentials");
199
+
200
+ await view.updateContext({ environment: { ...view.context.environment, route: mockPageRoute("/reports/:id", "/reports/99") } });
201
+ expect(view.root.props["title"]).toBe("Report not found");
202
+
203
+ await view.root.slotted("actions")[0]!.trigger("onPress");
204
+ expect(view.host.callsTo("navigate")).toEqual([[{ type: "appPage", path: "/reports" }]]);
205
+ expect(view.errors).toEqual([]);
206
+ });
118
207
  });