@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hand-written. App pages in the developer kit (docs/third-party-apps/fullpageview-plan.md, P3):
|
|
3
|
+
* the PageView root, `usePageRoute()`, `navigate({ type: "appPage" })` through the hooks, and the
|
|
4
|
+
* testing kit's route support (`getMockContext`, `mockPageRoute`).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createElement as h } from "react";
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import type { PageRoute } from "../../../apps/context.js";
|
|
10
|
+
import { navigate, useHostApi, usePageRoute } from "../../../apps/hooks/index.js";
|
|
11
|
+
import type { RouteDescriptor } from "../../../apps/route.js";
|
|
12
|
+
import { getMockContext, mockPageRoute, renderForTest } from "../../../apps/testing/index.js";
|
|
13
|
+
import { Button, ContextView, PageView, Text, type UiProps } from "../../../apps/ui/index.js";
|
|
14
|
+
|
|
15
|
+
function ReportDetail() {
|
|
16
|
+
const route = usePageRoute();
|
|
17
|
+
const host = useHostApi();
|
|
18
|
+
return h(
|
|
19
|
+
PageView,
|
|
20
|
+
{ title: `Report ${route?.params["id"] ?? "?"}`, actions: h(Button, { onPress: () => navigate({ type: "appPage", path: "/reports" }, { host }) }, "All reports") },
|
|
21
|
+
h(Text, null, route ? `${route.path} at ${route.pathname}` : "no route"),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("PageView", () => {
|
|
26
|
+
test("is a view root with the catalog's props and slots (checked by tsc)", () => {
|
|
27
|
+
const props: UiProps<"PageView"> = { title: "Reports", description: "All reports", actions: h(Button, null, "New"), banner: h(Text, null, "Beta") };
|
|
28
|
+
// @ts-expect-error title is required
|
|
29
|
+
const untitled: UiProps<"PageView"> = { description: "x" };
|
|
30
|
+
// @ts-expect-error PageView has no primaryAction slot (ContextView's)
|
|
31
|
+
const wrongSlot: UiProps<"PageView"> = { title: "t", primaryAction: h(Button, null, "Go") };
|
|
32
|
+
expect([props, untitled, wrongSlot]).toHaveLength(3);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("renders as the root with its slots", async () => {
|
|
36
|
+
const view = await renderForTest(h(ReportDetail), {
|
|
37
|
+
context: { environment: { route: mockPageRoute("/reports/:id", "/reports/7") } },
|
|
38
|
+
});
|
|
39
|
+
expect(view.root.component).toBe("PageView");
|
|
40
|
+
expect(view.root.props["title"]).toBe("Report 7");
|
|
41
|
+
expect(view.root.slotted("actions").map((e) => e.text)).toEqual(["All reports"]);
|
|
42
|
+
expect(view.find("Text")!.text).toBe("/reports/:id at /reports/7");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("usePageRoute", () => {
|
|
47
|
+
test("is undefined on a view that is not a page", async () => {
|
|
48
|
+
const Panel = () => h(ContextView, { title: "Panel" }, h(Text, null, usePageRoute() === undefined ? "none" : "route"));
|
|
49
|
+
const view = await renderForTest(h(Panel));
|
|
50
|
+
expect(view.find("Text")!.text).toBe("none");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("re-renders with the new route when the host pushes one through updateContext", async () => {
|
|
54
|
+
const view = await renderForTest(h(ReportDetail), {
|
|
55
|
+
context: { environment: { route: mockPageRoute("/reports/:id", "/reports/7") } },
|
|
56
|
+
});
|
|
57
|
+
expect(view.root.props["title"]).toBe("Report 7");
|
|
58
|
+
|
|
59
|
+
await view.updateContext({ environment: { ...view.context.environment, route: mockPageRoute("/reports/:id", "/reports/8") } });
|
|
60
|
+
expect(view.root.props["title"]).toBe("Report 8");
|
|
61
|
+
expect(view.find("Text")!.text).toBe("/reports/:id at /reports/8");
|
|
62
|
+
expect(view.errors).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("throws outside a mounted view", () => {
|
|
66
|
+
expect(() => usePageRoute()).toThrow();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("navigate to an app page", () => {
|
|
71
|
+
test("sends a concrete path to the host", async () => {
|
|
72
|
+
const view = await renderForTest(h(ReportDetail), {
|
|
73
|
+
context: { environment: { route: mockPageRoute("/reports/:id", "/reports/7") } },
|
|
74
|
+
});
|
|
75
|
+
await view.root.slotted("actions")[0]!.trigger("onPress");
|
|
76
|
+
expect(view.host.callsTo("navigate")).toEqual([[{ type: "appPage", path: "/reports" }]]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("rejects a declared (param) path, a path that climbs out and a missing path, before the host sees them", async () => {
|
|
80
|
+
const view = await renderForTest(h(ReportDetail), {
|
|
81
|
+
context: { environment: { route: mockPageRoute("/reports/:id", "/reports/7") } },
|
|
82
|
+
});
|
|
83
|
+
const bad: unknown[] = [
|
|
84
|
+
{ type: "appPage", path: "/reports/:id" },
|
|
85
|
+
{ type: "appPage", path: "/../settings" },
|
|
86
|
+
{ type: "appPage", path: "reports" },
|
|
87
|
+
{ type: "appPage" },
|
|
88
|
+
];
|
|
89
|
+
for (const to of bad) {
|
|
90
|
+
await expect(navigate(to as RouteDescriptor, { host: view.host })).rejects.toThrow(TypeError);
|
|
91
|
+
}
|
|
92
|
+
expect(view.host.callsTo("navigate")).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("type-checks through the hook API", () => {
|
|
96
|
+
const ok: Parameters<typeof navigate>[0] = { type: "appPage", path: "/reports/7", envParams: { tab: "summary" } };
|
|
97
|
+
// @ts-expect-error appPage needs a path
|
|
98
|
+
const noPath: Parameters<typeof navigate>[0] = { type: "appPage" };
|
|
99
|
+
// @ts-expect-error an app can only open its own pages: there is no appId
|
|
100
|
+
const otherApp: Parameters<typeof navigate>[0] = { type: "appPage", path: "/x", appId: 2 };
|
|
101
|
+
expect([ok, noPath, otherApp]).toHaveLength(3);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("testing kit routes", () => {
|
|
106
|
+
test("mockPageRoute decodes params the way the host does", () => {
|
|
107
|
+
expect(mockPageRoute("/reports/:id", "/reports/7")).toEqual({ path: "/reports/:id", pathname: "/reports/7", params: { id: "7" } });
|
|
108
|
+
expect(mockPageRoute("/vendors/:vendor_id/notes/:note", "/vendors/a%20b/notes/3")).toEqual({
|
|
109
|
+
path: "/vendors/:vendor_id/notes/:note",
|
|
110
|
+
pathname: "/vendors/a%20b/notes/3",
|
|
111
|
+
params: { vendor_id: "a b", note: "3" },
|
|
112
|
+
});
|
|
113
|
+
expect(mockPageRoute("/reports")).toEqual({ path: "/reports", pathname: "/reports", params: {} });
|
|
114
|
+
expect(() => mockPageRoute("/reports/:id", "/vendors/7")).toThrow(/does not match/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("getMockContext has no route by default", () => {
|
|
118
|
+
const ctx = getMockContext();
|
|
119
|
+
expect(ctx.environment.route).toBeUndefined();
|
|
120
|
+
expect(ctx.environment.surfaceId).toBe("gs.company.drawer.default");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("getMockContext takes a route, puts the view on the page surface and completes a partial one", () => {
|
|
124
|
+
const route: PageRoute = mockPageRoute("/reports/:id", "/reports/7");
|
|
125
|
+
const ctx = getMockContext({ environment: { route } });
|
|
126
|
+
expect(ctx.environment.route).toEqual(route);
|
|
127
|
+
expect(ctx.environment.surfaceId).toBe("page");
|
|
128
|
+
expect(ctx.environment.portal).toBe("company");
|
|
129
|
+
|
|
130
|
+
expect(getMockContext({ environment: { route: { path: "/reports" } } }).environment.route).toEqual({
|
|
131
|
+
path: "/reports",
|
|
132
|
+
pathname: "/reports",
|
|
133
|
+
params: {},
|
|
134
|
+
});
|
|
135
|
+
expect(getMockContext({ environment: { surfaceId: "custom", route: { path: "/" } } }).environment.surfaceId).toBe("custom");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { createElement as h, Fragment, startTransition, useLayoutEffect, useState, type ReactNode } from "react";
|
|
8
8
|
import { flushSync } from "react-dom";
|
|
9
9
|
import { describe, expect, test } from "vitest";
|
|
10
|
-
import { COMPONENTS, type ComponentName } from "../../../apps/components.js";
|
|
10
|
+
import { COMPONENTS, type ChartClickEvent, type ComponentName } from "../../../apps/components.js";
|
|
11
11
|
import { navigate, showToast, useExtensionContext, useHostApi } from "../../../apps/hooks/index.js";
|
|
12
12
|
import { defineRemoteElements, ELEMENTS, ExtensionContextProvider } from "../../../apps/runtime/index.js";
|
|
13
13
|
import { sessionForContext } from "../../../apps/runtime/session.js";
|
|
@@ -15,7 +15,9 @@ import { createMockHost, getMockContext, mockTokens, renderForTest, type MockHos
|
|
|
15
15
|
import * as ui from "../../../apps/ui/index.js";
|
|
16
16
|
import {
|
|
17
17
|
Badge,
|
|
18
|
+
BarChart,
|
|
18
19
|
Button,
|
|
20
|
+
ComboChart,
|
|
19
21
|
ContextView,
|
|
20
22
|
FocusView,
|
|
21
23
|
Link,
|
|
@@ -199,6 +201,38 @@ describe("ui wrappers", () => {
|
|
|
199
201
|
});
|
|
200
202
|
});
|
|
201
203
|
|
|
204
|
+
describe("charts", () => {
|
|
205
|
+
test("data reaches the host and onClick receives the ChartClickEvent detail", async () => {
|
|
206
|
+
const clicks: ChartClickEvent[] = [];
|
|
207
|
+
const data = [{ label: "Mon", value: 3 }, { label: "Tue", value: 5 }];
|
|
208
|
+
const combo = { labels: ["Q1", "Q2"], bars: { name: "Visits", values: [4, 6] }, line: { name: "Average", values: [2, 3] } };
|
|
209
|
+
const view = await renderForTest(
|
|
210
|
+
h(ContextView, { title: "Charts" },
|
|
211
|
+
h(BarChart, { data, theme: "vibrant", interactive: true, onClick: (e: ChartClickEvent) => { clicks.push(e); return e.index; } }),
|
|
212
|
+
h(ComboChart, { data: combo, onClick: (e: ChartClickEvent) => clicks.push(e) }),
|
|
213
|
+
),
|
|
214
|
+
);
|
|
215
|
+
const bar = view.find("BarChart")!;
|
|
216
|
+
expect(bar.props).toEqual({ data, theme: "vibrant", interactive: true });
|
|
217
|
+
expect(bar.events).toEqual(["click"]);
|
|
218
|
+
const detail: ChartClickEvent = { index: 1, label: "Tue", value: 5 };
|
|
219
|
+
await expect(bar.trigger("onClick", detail)).resolves.toBe(1);
|
|
220
|
+
|
|
221
|
+
const comboNode = view.find("ComboChart")!;
|
|
222
|
+
expect(comboNode.props["data"]).toEqual(combo);
|
|
223
|
+
const lineDetail: ChartClickEvent = { index: 0, label: "Q1", value: 2, series: "Average" };
|
|
224
|
+
await comboNode.trigger("onClick", lineDetail);
|
|
225
|
+
expect(clicks).toEqual([detail, lineDetail]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("chart props are typed (checked by tsc)", () => {
|
|
229
|
+
const ok: UiProps<"BarChart"> = { data: [{ label: "a", value: 1 }], onClick: (e: ChartClickEvent) => e.label };
|
|
230
|
+
// @ts-expect-error styling props are not accepted
|
|
231
|
+
const styled: UiProps<"BarChart"> = { data: [{ label: "a", value: 1 }], color: "red" };
|
|
232
|
+
expect([ok, styled]).toHaveLength(2);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
202
236
|
describe("FocusView", () => {
|
|
203
237
|
function Panel(props: { onShownChange?: (shown: boolean) => void }) {
|
|
204
238
|
const [shown, setShown] = useState(false);
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hand-written. `createAppStore` (state shared by the views of one app) and `useNavParams`, through
|
|
3
|
+
* the real runtime. The host mounts every view of an app into one sandbox; here each
|
|
4
|
+
* `renderForTest` is its own runtime in the same module graph, which shares module state the
|
|
5
|
+
* same way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createElement as h } from "react";
|
|
9
|
+
import { describe, expect, test, vi } from "vitest";
|
|
10
|
+
import { createAppStore, useNavParams } from "../../../apps/hooks/index.js";
|
|
11
|
+
import { getMockContext, mockPageRoute, renderForTest } from "../../../apps/testing/index.js";
|
|
12
|
+
import { Button, ContextView, PageView, Text } from "../../../apps/ui/index.js";
|
|
13
|
+
|
|
14
|
+
describe("createAppStore", () => {
|
|
15
|
+
test("setState merges fields, keeps the object when nothing changes, and tells listeners", () => {
|
|
16
|
+
const store = createAppStore({ count: 0, label: "a" });
|
|
17
|
+
const listener = vi.fn();
|
|
18
|
+
const unsubscribe = store.subscribe(listener);
|
|
19
|
+
const first = store.getState();
|
|
20
|
+
|
|
21
|
+
store.setState({ count: 1 });
|
|
22
|
+
expect(store.getState()).toEqual({ count: 1, label: "a" });
|
|
23
|
+
expect(listener).toHaveBeenCalledWith({ count: 1, label: "a" }, first);
|
|
24
|
+
|
|
25
|
+
const second = store.getState();
|
|
26
|
+
store.setState({ count: 1 });
|
|
27
|
+
store.setState((s) => ({ label: s.label }));
|
|
28
|
+
expect(store.getState()).toBe(second);
|
|
29
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
30
|
+
|
|
31
|
+
store.setState((s) => ({ count: s.count + 1 }));
|
|
32
|
+
expect(store.getState().count).toBe(2);
|
|
33
|
+
unsubscribe();
|
|
34
|
+
store.setState({ count: 3 });
|
|
35
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("two mounted views see each other's updates", async () => {
|
|
39
|
+
const store = createAppStore({ saved: 0, last: "" });
|
|
40
|
+
|
|
41
|
+
function Panel() {
|
|
42
|
+
const saved = store.useStore((s) => s.saved);
|
|
43
|
+
return h(
|
|
44
|
+
ContextView,
|
|
45
|
+
{ title: "Panel" },
|
|
46
|
+
h(Text, null, `saved ${saved}`),
|
|
47
|
+
h(Button, { onPress: () => store.setState((s) => ({ saved: s.saved + 1, last: "from panel" })) }, "Save"),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
function Page() {
|
|
51
|
+
const state = store.useStore();
|
|
52
|
+
return h(
|
|
53
|
+
PageView,
|
|
54
|
+
{ title: "Page" },
|
|
55
|
+
h(Text, null, `saved ${state.saved}, last ${state.last || "none"}`),
|
|
56
|
+
h(Button, { onPress: () => store.setState({ last: "from page" }) }, "Touch"),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const panel = await renderForTest(h(Panel));
|
|
61
|
+
const page = await renderForTest(h(Page), { context: { environment: { route: mockPageRoute("/") } } });
|
|
62
|
+
expect(page.find("Text")!.text).toBe("saved 0, last none");
|
|
63
|
+
|
|
64
|
+
await panel.find("Button")!.trigger("onPress");
|
|
65
|
+
await page.settle();
|
|
66
|
+
expect(panel.find("Text")!.text).toBe("saved 1");
|
|
67
|
+
expect(page.find("Text")!.text).toBe("saved 1, last from panel");
|
|
68
|
+
|
|
69
|
+
await page.find("Button")!.trigger("onPress");
|
|
70
|
+
await panel.settle();
|
|
71
|
+
expect(page.find("Text")!.text).toBe("saved 1, last from page");
|
|
72
|
+
// The panel selects `saved` only, which did not change.
|
|
73
|
+
expect(panel.find("Text")!.text).toBe("saved 1");
|
|
74
|
+
expect([...panel.errors, ...page.errors]).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a view's subscription ends when it unmounts", async () => {
|
|
78
|
+
const store = createAppStore({ n: 0 });
|
|
79
|
+
let active = 0;
|
|
80
|
+
const subscribe = store.subscribe;
|
|
81
|
+
store.subscribe = (listener) => {
|
|
82
|
+
active += 1;
|
|
83
|
+
const unsubscribe = subscribe(listener);
|
|
84
|
+
return () => {
|
|
85
|
+
active -= 1;
|
|
86
|
+
unsubscribe();
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
let renders = 0;
|
|
90
|
+
function Counter() {
|
|
91
|
+
renders += 1;
|
|
92
|
+
return h(ContextView, { title: "Counter" }, h(Text, null, String(store.useStore((s) => s.n))));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const a = await renderForTest(h(Counter));
|
|
96
|
+
const b = await renderForTest(h(Counter));
|
|
97
|
+
expect(active).toBe(2);
|
|
98
|
+
|
|
99
|
+
await a.unmount();
|
|
100
|
+
expect(active).toBe(1);
|
|
101
|
+
|
|
102
|
+
const before = renders;
|
|
103
|
+
store.setState({ n: 1 });
|
|
104
|
+
await b.settle();
|
|
105
|
+
expect(b.find("Text")!.text).toBe("1");
|
|
106
|
+
expect(renders).toBe(before + 1); // only the mounted view rendered
|
|
107
|
+
|
|
108
|
+
await b.unmount();
|
|
109
|
+
expect(active).toBe(0);
|
|
110
|
+
store.setState({ n: 2 });
|
|
111
|
+
expect(renders).toBe(before + 1);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("a selector that builds a new object from an unchanged state does not loop", async () => {
|
|
115
|
+
const store = createAppStore({ a: 1, b: 2 });
|
|
116
|
+
function Sum() {
|
|
117
|
+
const pair = store.useStore((s) => ({ sum: s.a + s.b }));
|
|
118
|
+
return h(ContextView, { title: "Sum" }, h(Text, null, String(pair.sum)));
|
|
119
|
+
}
|
|
120
|
+
const view = await renderForTest(h(Sum));
|
|
121
|
+
expect(view.find("Text")!.text).toBe("3");
|
|
122
|
+
store.setState({ a: 5 });
|
|
123
|
+
await view.settle();
|
|
124
|
+
expect(view.find("Text")!.text).toBe("7");
|
|
125
|
+
expect(view.errors).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("useNavParams", () => {
|
|
130
|
+
function Target() {
|
|
131
|
+
const params = useNavParams();
|
|
132
|
+
return h(ContextView, { title: "Target" }, h(Text, null, JSON.stringify(params)));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
test("is empty unless the view opened from a navigation", async () => {
|
|
136
|
+
const view = await renderForTest(h(Target));
|
|
137
|
+
expect(view.find("Text")!.text).toBe("{}");
|
|
138
|
+
expect(getMockContext().environment.navParams).toEqual({});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("returns the navigation's envParams, separate from the URL query, and follows updateContext", async () => {
|
|
142
|
+
const view = await renderForTest(h(Target), {
|
|
143
|
+
context: { environment: { navParams: { vendorId: "42" }, queryParams: { vendorId: "666" } } },
|
|
144
|
+
});
|
|
145
|
+
expect(view.find("Text")!.text).toBe('{"vendorId":"42"}');
|
|
146
|
+
await view.updateContext({ environment: { ...view.context.environment, navParams: {} } });
|
|
147
|
+
expect(view.find("Text")!.text).toBe("{}");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hand-written. `post_install_action: { type: "app_page", path }`: installing opens one of the
|
|
3
|
+
* app's own pages. The cases come from `fixtures/post-install-page.json`, which the server's catalog
|
|
4
|
+
* spec reads too; see docs/third-party-apps/fullpageview-plan.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { describe, expect, test } from "vitest";
|
|
10
|
+
import {
|
|
11
|
+
CONCRETE_ROUTE_PATH_PATTERN,
|
|
12
|
+
effectivePostInstallAction,
|
|
13
|
+
manifestJsonSchema,
|
|
14
|
+
validateManifest,
|
|
15
|
+
type AppManifestV2,
|
|
16
|
+
} from "../../apps/index.js";
|
|
17
|
+
|
|
18
|
+
const FIXTURES = resolve(process.cwd(), "src", "__tests__", "apps", "fixtures", "post-install-page.json");
|
|
19
|
+
|
|
20
|
+
interface Case { name: string; action: Record<string, unknown>; routes?: unknown[]; issue?: string }
|
|
21
|
+
|
|
22
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
23
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function loadFixtures(): { routes: unknown[]; accepted: Case[]; rejected: Case[] } {
|
|
27
|
+
const parsed: unknown = JSON.parse(readFileSync(FIXTURES, "utf8"));
|
|
28
|
+
if (!isRecord(parsed) || !Array.isArray(parsed["routes"]) || !Array.isArray(parsed["accepted"]) || !Array.isArray(parsed["rejected"])) {
|
|
29
|
+
throw new Error("fixtures/post-install-page.json needs routes, accepted and rejected arrays");
|
|
30
|
+
}
|
|
31
|
+
return { routes: parsed["routes"], accepted: parsed["accepted"] as Case[], rejected: parsed["rejected"] as Case[] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const fixtures = loadFixtures();
|
|
35
|
+
|
|
36
|
+
function manifestFor(testCase: Case): Record<string, unknown> {
|
|
37
|
+
return {
|
|
38
|
+
id: 42,
|
|
39
|
+
name: "Acme Reports",
|
|
40
|
+
icon: "https://cdn.acme.com/icon.png",
|
|
41
|
+
short_description: "Reports about your vendors.",
|
|
42
|
+
version: "1.0.0",
|
|
43
|
+
configuration_instructions: "Install and sign in.",
|
|
44
|
+
support_instructions: "support@acme.com",
|
|
45
|
+
ui_extensions: {
|
|
46
|
+
bundle: {
|
|
47
|
+
url: "https://cdn.acme.com/gs-app/1.0.0/app.js",
|
|
48
|
+
integrity: "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC",
|
|
49
|
+
},
|
|
50
|
+
routes: testCase.routes ?? fixtures.routes,
|
|
51
|
+
},
|
|
52
|
+
post_install_action: testCase.action,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function issuePaths(testCase: Case): string[] {
|
|
57
|
+
const result = validateManifest(manifestFor(testCase));
|
|
58
|
+
return result.ok ? [] : result.issues.map((issue) => issue.path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("post_install_action app_page", () => {
|
|
62
|
+
test.each(fixtures.accepted.map((c) => [c.name, c] as const))("accepts %s", (_name, testCase) => {
|
|
63
|
+
expect(issuePaths(testCase)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test.each(fixtures.rejected.map((c) => [c.name, c] as const))("rejects %s", (_name, testCase) => {
|
|
67
|
+
const paths = issuePaths(testCase);
|
|
68
|
+
expect(paths.length).toBeGreaterThan(0);
|
|
69
|
+
for (const path of paths) expect(path).toBe(testCase.issue);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("an explicit app_page action is returned unchanged", () => {
|
|
73
|
+
const result = validateManifest(manifestFor({ name: "welcome", action: { type: "app_page", path: "/welcome" } }));
|
|
74
|
+
if (!result.ok) throw new Error("fixture invalid");
|
|
75
|
+
const manifest: AppManifestV2 = result.manifest;
|
|
76
|
+
|
|
77
|
+
expect(effectivePostInstallAction(manifest)).toEqual({ type: "app_page", path: "/welcome" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("the JSON schema carries the app_page type and the concrete path pattern", () => {
|
|
81
|
+
const schema: unknown = manifestJsonSchema();
|
|
82
|
+
if (!isRecord(schema) || !isRecord(schema["properties"])) throw new Error("schema has no properties");
|
|
83
|
+
const action = schema["properties"]["post_install_action"];
|
|
84
|
+
if (!isRecord(action) || !isRecord(action["properties"])) throw new Error("schema has no post_install_action");
|
|
85
|
+
const { type, path } = action["properties"];
|
|
86
|
+
|
|
87
|
+
expect(isRecord(type) && type["enum"]).toEqual(["url", "app_settings_view", "app_main_view", "app_page"]);
|
|
88
|
+
expect(isRecord(path) && path["maxLength"]).toBe(200);
|
|
89
|
+
// The generator escapes `/` as `\/`; the pattern is the same.
|
|
90
|
+
expect(isRecord(path) && typeof path["pattern"] === "string" && path["pattern"].replaceAll("\\/", "/"))
|
|
91
|
+
.toBe(CONCRETE_ROUTE_PATH_PATTERN);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("a failing ui_extensions field", () => {
|
|
96
|
+
test("is reported alone, without phantom post-install or view issues", () => {
|
|
97
|
+
const manifest = {
|
|
98
|
+
id: 1,
|
|
99
|
+
name: "A",
|
|
100
|
+
icon: "https://a.example.com/i.png",
|
|
101
|
+
short_description: "x",
|
|
102
|
+
configuration_instructions: "x",
|
|
103
|
+
support_instructions: "x",
|
|
104
|
+
version: "1.0.0",
|
|
105
|
+
ui_extensions: {
|
|
106
|
+
bundle: { url: "https://a.example.com/a.js", integrity: `sha384-${"A".repeat(64)}` },
|
|
107
|
+
views: [{ viewport: "settings", component: "S" }],
|
|
108
|
+
routes: [{ path: "/../settings", component: "P", title: "P" }],
|
|
109
|
+
},
|
|
110
|
+
post_install_action: { type: "app_settings_view" },
|
|
111
|
+
};
|
|
112
|
+
const result = validateManifest(manifest);
|
|
113
|
+
expect(result.ok).toBe(false);
|
|
114
|
+
if (result.ok) return;
|
|
115
|
+
expect(result.issues.map((issue) => issue.path)).toEqual(["ui_extensions.routes.0.path"]);
|
|
116
|
+
|
|
117
|
+
const pageAction = validateManifest({ ...manifest, post_install_action: { type: "app_page", path: "/reports" } });
|
|
118
|
+
expect(pageAction.ok ? [] : pageAction.issues.map((issue) => issue.path)).toEqual(["ui_extensions.routes.0.path"]);
|
|
119
|
+
|
|
120
|
+
const strayPath = validateManifest({ ...manifest, post_install_action: { type: "url", url: "https://a.example.com/", path: "/x" } });
|
|
121
|
+
expect(strayPath.ok ? [] : strayPath.issues.map((issue) => issue.path)).toEqual([
|
|
122
|
+
"ui_extensions.routes.0.path",
|
|
123
|
+
"post_install_action.path",
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
});
|