@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.
- package/dist/commonjs/__tests__/apps/contract.test.js +288 -4
- package/dist/commonjs/__tests__/apps/contract.test.js.map +1 -1
- package/dist/commonjs/__tests__/apps/devpreview-routes.test.d.ts +2 -0
- package/dist/commonjs/__tests__/apps/devpreview-routes.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/apps/devpreview-routes.test.js +86 -0
- package/dist/commonjs/__tests__/apps/devpreview-routes.test.js.map +1 -0
- package/dist/commonjs/__tests__/apps/kit/example.test.js +84 -4
- package/dist/commonjs/__tests__/apps/kit/example.test.js.map +1 -1
- package/dist/commonjs/__tests__/apps/kit/pages.test.d.ts +2 -0
- package/dist/commonjs/__tests__/apps/kit/pages.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/apps/kit/pages.test.js +119 -0
- package/dist/commonjs/__tests__/apps/kit/pages.test.js.map +1 -0
- package/dist/commonjs/__tests__/apps/kit/render.test.js +24 -0
- package/dist/commonjs/__tests__/apps/kit/render.test.js.map +1 -1
- package/dist/commonjs/__tests__/apps/kit/store.test.d.ts +2 -0
- package/dist/commonjs/__tests__/apps/kit/store.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/apps/kit/store.test.js +123 -0
- package/dist/commonjs/__tests__/apps/kit/store.test.js.map +1 -0
- package/dist/commonjs/__tests__/apps/post-install-page.test.d.ts +2 -0
- package/dist/commonjs/__tests__/apps/post-install-page.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/apps/post-install-page.test.js +110 -0
- package/dist/commonjs/__tests__/apps/post-install-page.test.js.map +1 -0
- package/dist/commonjs/__tests__/apps/routes.test.d.ts +2 -0
- package/dist/commonjs/__tests__/apps/routes.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/apps/routes.test.js +238 -0
- package/dist/commonjs/__tests__/apps/routes.test.js.map +1 -0
- package/dist/commonjs/__tests__/vendors.test.js +1 -1
- package/dist/commonjs/__tests__/zoneDevicesCreate.test.d.ts +2 -0
- package/dist/commonjs/__tests__/zoneDevicesCreate.test.d.ts.map +1 -0
- package/dist/commonjs/__tests__/zoneDevicesCreate.test.js +47 -0
- package/dist/commonjs/__tests__/zoneDevicesCreate.test.js.map +1 -0
- package/dist/commonjs/__tests__/zones.test.js +4 -28
- package/dist/commonjs/__tests__/zones.test.js.map +1 -1
- package/dist/commonjs/admin-funcs/zoneDevicesCreate.d.ts +11 -0
- package/dist/commonjs/admin-funcs/zoneDevicesCreate.d.ts.map +1 -0
- package/dist/commonjs/admin-funcs/zoneDevicesCreate.js +14 -0
- package/dist/commonjs/admin-funcs/zoneDevicesCreate.js.map +1 -0
- package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.d.ts +3 -0
- package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.d.ts.map +1 -0
- package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.js +11 -0
- package/dist/commonjs/admin-funcs/zoneDevicesMacAvailability.js.map +1 -0
- package/dist/commonjs/apps/components.d.ts +321 -0
- package/dist/commonjs/apps/components.d.ts.map +1 -1
- package/dist/commonjs/apps/components.js +168 -2
- package/dist/commonjs/apps/components.js.map +1 -1
- package/dist/commonjs/apps/context.d.ts +24 -0
- package/dist/commonjs/apps/context.d.ts.map +1 -1
- package/dist/commonjs/apps/context.js.map +1 -1
- package/dist/commonjs/apps/devpreview.d.ts +20 -3
- package/dist/commonjs/apps/devpreview.d.ts.map +1 -1
- package/dist/commonjs/apps/devpreview.js +18 -7
- package/dist/commonjs/apps/devpreview.js.map +1 -1
- package/dist/commonjs/apps/hooks/index.d.ts +33 -2
- package/dist/commonjs/apps/hooks/index.d.ts.map +1 -1
- package/dist/commonjs/apps/hooks/index.js +41 -1
- package/dist/commonjs/apps/hooks/index.js.map +1 -1
- package/dist/commonjs/apps/hooks/store.d.ts +48 -0
- package/dist/commonjs/apps/hooks/store.d.ts.map +1 -0
- package/dist/commonjs/apps/hooks/store.js +87 -0
- package/dist/commonjs/apps/hooks/store.js.map +1 -0
- package/dist/commonjs/apps/manifest.d.ts +64 -3
- package/dist/commonjs/apps/manifest.d.ts.map +1 -1
- package/dist/commonjs/apps/manifest.js +128 -27
- package/dist/commonjs/apps/manifest.js.map +1 -1
- package/dist/commonjs/apps/protocol.d.ts +1 -1
- package/dist/commonjs/apps/protocol.d.ts.map +1 -1
- package/dist/commonjs/apps/route.d.ts +11 -1
- package/dist/commonjs/apps/route.d.ts.map +1 -1
- package/dist/commonjs/apps/route.js +13 -7
- package/dist/commonjs/apps/route.js.map +1 -1
- package/dist/commonjs/apps/routes.d.ts +65 -0
- package/dist/commonjs/apps/routes.d.ts.map +1 -0
- package/dist/commonjs/apps/routes.js +202 -0
- package/dist/commonjs/apps/routes.js.map +1 -0
- package/dist/commonjs/apps/surfaces.d.ts +20 -2
- package/dist/commonjs/apps/surfaces.d.ts.map +1 -1
- package/dist/commonjs/apps/surfaces.js +45 -7
- package/dist/commonjs/apps/surfaces.js.map +1 -1
- package/dist/commonjs/apps/testing/index.d.ts +17 -1
- package/dist/commonjs/apps/testing/index.d.ts.map +1 -1
- package/dist/commonjs/apps/testing/index.js +50 -1
- package/dist/commonjs/apps/testing/index.js.map +1 -1
- package/dist/commonjs/apps/ui/index.d.ts +13 -0
- package/dist/commonjs/apps/ui/index.d.ts.map +1 -1
- package/dist/commonjs/apps/ui/index.js +19 -2
- package/dist/commonjs/apps/ui/index.js.map +1 -1
- package/dist/commonjs/index.extras.d.ts +4 -0
- package/dist/commonjs/index.extras.d.ts.map +1 -1
- package/dist/commonjs/index.extras.js +5 -1
- package/dist/commonjs/index.extras.js.map +1 -1
- package/dist/commonjs/lib/config.d.ts +3 -3
- package/dist/commonjs/lib/config.js +3 -3
- package/dist/commonjs/models/components/vendorcredentialsummary.d.ts +7 -0
- package/dist/commonjs/models/components/vendorcredentialsummary.d.ts.map +1 -1
- package/dist/commonjs/models/components/vendorcredentialsummary.js +2 -0
- package/dist/commonjs/models/components/vendorcredentialsummary.js.map +1 -1
- package/dist/esm/__tests__/apps/contract.test.js +289 -5
- package/dist/esm/__tests__/apps/contract.test.js.map +1 -1
- package/dist/esm/__tests__/apps/devpreview-routes.test.d.ts +2 -0
- package/dist/esm/__tests__/apps/devpreview-routes.test.d.ts.map +1 -0
- package/dist/esm/__tests__/apps/devpreview-routes.test.js +84 -0
- package/dist/esm/__tests__/apps/devpreview-routes.test.js.map +1 -0
- package/dist/esm/__tests__/apps/kit/example.test.js +85 -5
- package/dist/esm/__tests__/apps/kit/example.test.js.map +1 -1
- package/dist/esm/__tests__/apps/kit/pages.test.d.ts +2 -0
- package/dist/esm/__tests__/apps/kit/pages.test.d.ts.map +1 -0
- package/dist/esm/__tests__/apps/kit/pages.test.js +117 -0
- package/dist/esm/__tests__/apps/kit/pages.test.js.map +1 -0
- package/dist/esm/__tests__/apps/kit/render.test.js +25 -1
- package/dist/esm/__tests__/apps/kit/render.test.js.map +1 -1
- package/dist/esm/__tests__/apps/kit/store.test.d.ts +2 -0
- package/dist/esm/__tests__/apps/kit/store.test.d.ts.map +1 -0
- package/dist/esm/__tests__/apps/kit/store.test.js +121 -0
- package/dist/esm/__tests__/apps/kit/store.test.js.map +1 -0
- package/dist/esm/__tests__/apps/post-install-page.test.d.ts +2 -0
- package/dist/esm/__tests__/apps/post-install-page.test.d.ts.map +1 -0
- package/dist/esm/__tests__/apps/post-install-page.test.js +108 -0
- package/dist/esm/__tests__/apps/post-install-page.test.js.map +1 -0
- package/dist/esm/__tests__/apps/routes.test.d.ts +2 -0
- package/dist/esm/__tests__/apps/routes.test.d.ts.map +1 -0
- package/dist/esm/__tests__/apps/routes.test.js +236 -0
- package/dist/esm/__tests__/apps/routes.test.js.map +1 -0
- package/dist/esm/__tests__/vendors.test.js +1 -1
- package/dist/esm/__tests__/zoneDevicesCreate.test.d.ts +2 -0
- package/dist/esm/__tests__/zoneDevicesCreate.test.d.ts.map +1 -0
- package/dist/esm/__tests__/zoneDevicesCreate.test.js +45 -0
- package/dist/esm/__tests__/zoneDevicesCreate.test.js.map +1 -0
- package/dist/esm/__tests__/zones.test.js +4 -28
- package/dist/esm/__tests__/zones.test.js.map +1 -1
- package/dist/esm/admin-funcs/zoneDevicesCreate.d.ts +11 -0
- package/dist/esm/admin-funcs/zoneDevicesCreate.d.ts.map +1 -0
- package/dist/esm/admin-funcs/zoneDevicesCreate.js +11 -0
- package/dist/esm/admin-funcs/zoneDevicesCreate.js.map +1 -0
- package/dist/esm/admin-funcs/zoneDevicesMacAvailability.d.ts +3 -0
- package/dist/esm/admin-funcs/zoneDevicesMacAvailability.d.ts.map +1 -0
- package/dist/esm/admin-funcs/zoneDevicesMacAvailability.js +8 -0
- package/dist/esm/admin-funcs/zoneDevicesMacAvailability.js.map +1 -0
- package/dist/esm/apps/components.d.ts +321 -0
- package/dist/esm/apps/components.d.ts.map +1 -1
- package/dist/esm/apps/components.js +163 -1
- package/dist/esm/apps/components.js.map +1 -1
- package/dist/esm/apps/context.d.ts +24 -0
- package/dist/esm/apps/context.d.ts.map +1 -1
- package/dist/esm/apps/context.js.map +1 -1
- package/dist/esm/apps/devpreview.d.ts +20 -3
- package/dist/esm/apps/devpreview.d.ts.map +1 -1
- package/dist/esm/apps/devpreview.js +19 -8
- package/dist/esm/apps/devpreview.js.map +1 -1
- package/dist/esm/apps/hooks/index.d.ts +33 -2
- package/dist/esm/apps/hooks/index.d.ts.map +1 -1
- package/dist/esm/apps/hooks/index.js +37 -1
- package/dist/esm/apps/hooks/index.js.map +1 -1
- package/dist/esm/apps/hooks/store.d.ts +48 -0
- package/dist/esm/apps/hooks/store.d.ts.map +1 -0
- package/dist/esm/apps/hooks/store.js +84 -0
- package/dist/esm/apps/hooks/store.js.map +1 -0
- package/dist/esm/apps/manifest.d.ts +64 -3
- package/dist/esm/apps/manifest.d.ts.map +1 -1
- package/dist/esm/apps/manifest.js +116 -26
- package/dist/esm/apps/manifest.js.map +1 -1
- package/dist/esm/apps/protocol.d.ts +1 -1
- package/dist/esm/apps/protocol.d.ts.map +1 -1
- package/dist/esm/apps/route.d.ts +11 -1
- package/dist/esm/apps/route.d.ts.map +1 -1
- package/dist/esm/apps/route.js +14 -8
- package/dist/esm/apps/route.js.map +1 -1
- package/dist/esm/apps/routes.d.ts +65 -0
- package/dist/esm/apps/routes.d.ts.map +1 -0
- package/dist/esm/apps/routes.js +194 -0
- package/dist/esm/apps/routes.js.map +1 -0
- package/dist/esm/apps/surfaces.d.ts +20 -2
- package/dist/esm/apps/surfaces.d.ts.map +1 -1
- package/dist/esm/apps/surfaces.js +39 -2
- package/dist/esm/apps/surfaces.js.map +1 -1
- package/dist/esm/apps/testing/index.d.ts +17 -1
- package/dist/esm/apps/testing/index.d.ts.map +1 -1
- package/dist/esm/apps/testing/index.js +49 -1
- package/dist/esm/apps/testing/index.js.map +1 -1
- package/dist/esm/apps/ui/index.d.ts +13 -0
- package/dist/esm/apps/ui/index.d.ts.map +1 -1
- package/dist/esm/apps/ui/index.js +17 -1
- package/dist/esm/apps/ui/index.js.map +1 -1
- package/dist/esm/index.extras.d.ts +4 -0
- package/dist/esm/index.extras.d.ts.map +1 -1
- package/dist/esm/index.extras.js +5 -1
- package/dist/esm/index.extras.js.map +1 -1
- package/dist/esm/lib/config.d.ts +3 -3
- package/dist/esm/lib/config.js +3 -3
- package/dist/esm/models/components/vendorcredentialsummary.d.ts +7 -0
- package/dist/esm/models/components/vendorcredentialsummary.d.ts.map +1 -1
- package/dist/esm/models/components/vendorcredentialsummary.js +2 -0
- package/dist/esm/models/components/vendorcredentialsummary.js.map +1 -1
- package/examples/apps/hello-green/src/store.ts +15 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/__tests__/apps/contract.test.ts +305 -4
- package/src/__tests__/apps/devpreview-routes.test.ts +103 -0
- package/src/__tests__/apps/kit/example.test.ts +94 -5
- package/src/__tests__/apps/kit/pages.test.ts +137 -0
- package/src/__tests__/apps/kit/render.test.ts +35 -1
- package/src/__tests__/apps/kit/store.test.ts +149 -0
- package/src/__tests__/apps/post-install-page.test.ts +126 -0
- package/src/__tests__/apps/routes.test.ts +318 -0
- package/src/__tests__/vendors.test.ts +1 -1
- package/src/__tests__/zoneDevicesCreate.test.ts +56 -0
- package/src/__tests__/zones.test.ts +4 -28
- package/src/admin-funcs/zoneDevicesCreate.ts +22 -0
- package/src/admin-funcs/zoneDevicesMacAvailability.ts +8 -0
- package/src/apps/components.ts +183 -1
- package/src/apps/context.ts +25 -0
- package/src/apps/devpreview.ts +20 -7
- package/src/apps/hooks/index.ts +42 -2
- package/src/apps/hooks/store.ts +115 -0
- package/src/apps/manifest.ts +151 -27
- package/src/apps/protocol.ts +1 -1
- package/src/apps/route.ts +22 -10
- package/src/apps/routes.ts +230 -0
- package/src/apps/surfaces.ts +45 -12
- package/src/apps/testing/index.ts +48 -2
- package/src/apps/ui/index.ts +18 -1
- package/src/index.extras.ts +5 -1
- package/src/lib/config.ts +3 -3
- 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.
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
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
|
});
|