@k2b/cloud 0.6.0-rc.0 → 0.6.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.
@@ -1,74 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { isServer, render } from "solid-js/web";
3
- import { createDomTestHarness } from "../../../ui/test/dom";
4
- import type { SearchItem } from "../api/search/schemas";
5
-
6
- const waitFor = async (condition: () => boolean, label: string) => {
7
- for (let attempt = 0; attempt < 100; attempt += 1) {
8
- if (condition()) return;
9
- await Bun.sleep(10);
10
- }
11
- throw new Error(`Timed out waiting for ${label}`);
12
- };
13
-
14
- const item = (id: string): SearchItem => ({
15
- appId: "notebooks",
16
- appName: "Notebooks",
17
- appIcon: "ti ti-notebook",
18
- readable: true,
19
- ref: { type: "notebooks.note", id },
20
- title: id,
21
- href: `/app/notebooks/${id}`,
22
- preview: `${id} preview`,
23
- });
24
-
25
- describe("CloudResourceSearch selection", () => {
26
- if (isServer) {
27
- test.skip("runs with browser export conditions", () => {});
28
- return;
29
- }
30
-
31
- test("applies picker filters and returns the selected resource", async () => {
32
- const dom = createDomTestHarness();
33
- const originalFetch = globalThis.fetch;
34
- const requests: string[] = [];
35
- globalThis.fetch = (async (input) => {
36
- requests.push(String(input));
37
- return Response.json({
38
- query: "",
39
- count: 2,
40
- apps: [{ id: "notebooks", name: "Notebooks", icon: "ti ti-notebook" }],
41
- items: [item("existing"), item("selectable")],
42
- });
43
- }) as typeof fetch;
44
-
45
- const { default: CloudResourceSearch } = await import("./CloudResourceSearch");
46
- let selected: SearchItem | undefined;
47
- const dispose = render(
48
- () => (
49
- <CloudResourceSearch
50
- initialAppId="notebooks"
51
- requireReader
52
- excludeRefs={[{ type: "notebooks.note", id: "existing" }]}
53
- onSelect={(item) => {
54
- selected = item;
55
- }}
56
- />
57
- ),
58
- dom.root,
59
- );
60
-
61
- try {
62
- await waitFor(() => dom.root.textContent?.includes("selectable preview") ?? false, "the filtered result");
63
- expect(requests[0]).toBe("/api/search?provider_limit=10&app=notebooks&require_reader=true");
64
- expect(dom.root.textContent).not.toContain("existing preview");
65
-
66
- dom.root.querySelector<HTMLButtonElement>("section button")!.click();
67
- expect(selected?.ref).toEqual({ type: "notebooks.note", id: "selectable" });
68
- } finally {
69
- dispose();
70
- globalThis.fetch = originalFetch;
71
- dom.cleanup();
72
- }
73
- });
74
- });
@@ -1,96 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { isServer, render } from "solid-js/web";
3
- import { createDomTestHarness } from "../../../ui/test/dom";
4
-
5
- const deferred = <T,>() => {
6
- let resolve!: (value: T) => void;
7
- const promise = new Promise<T>((nextResolve) => {
8
- resolve = nextResolve;
9
- });
10
- return { promise, resolve };
11
- };
12
-
13
- const waitFor = async (condition: () => boolean, label: string) => {
14
- for (let attempt = 0; attempt < 100; attempt += 1) {
15
- if (condition()) return;
16
- await Bun.sleep(10);
17
- }
18
- throw new Error(`Timed out waiting for ${label}`);
19
- };
20
-
21
- const searchResponse = (title: string) =>
22
- Response.json({
23
- query: title.toLowerCase(),
24
- count: 1,
25
- apps: [{ id: "notebooks", name: "Notebooks", icon: "ti ti-notebook" }],
26
- items: [
27
- {
28
- appId: "notebooks",
29
- appName: "Notebooks",
30
- appIcon: "ti ti-notebook",
31
- readable: true,
32
- ref: { type: "notebooks.note", id: title.toLowerCase() },
33
- title,
34
- href: `/app/notebooks/${title.toLowerCase()}`,
35
- preview: `${title} preview`,
36
- },
37
- ],
38
- });
39
-
40
- const catalogResponse = () =>
41
- Response.json({
42
- query: "",
43
- count: 0,
44
- apps: [{ id: "notebooks", name: "Notebooks", icon: "ti ti-notebook" }],
45
- items: [],
46
- });
47
-
48
- describe("GlobalSearchDialog query lifecycle", () => {
49
- if (isServer) {
50
- test.skip("runs with browser export conditions", () => {});
51
- return;
52
- }
53
-
54
- test("keeps last-good results during refresh and renders one loading indicator", async () => {
55
- const dom = createDomTestHarness();
56
- const originalFetch = globalThis.fetch;
57
- const requests: Array<{ signal: AbortSignal; response: ReturnType<typeof deferred<Response>> }> = [];
58
- globalThis.fetch = ((_input: string | URL | Request, init?: RequestInit) => {
59
- const response = deferred<Response>();
60
- requests.push({ signal: init?.signal as AbortSignal, response });
61
- return response.promise;
62
- }) as typeof fetch;
63
-
64
- const { default: GlobalSearchDialog } = await import("./GlobalSearchDialog");
65
- const dispose = render(() => <GlobalSearchDialog close={() => {}} helpApps={[]} />, dom.root);
66
-
67
- try {
68
- const input = dom.root.querySelector<HTMLInputElement>('[aria-label="Search Cloud resources"]')!;
69
- await waitFor(() => requests.length === 1, "the app catalog request");
70
- requests[0]!.response.resolve(catalogResponse());
71
-
72
- input.value = "alpha";
73
- input.dispatchEvent(new dom.window.Event("input", { bubbles: true }) as unknown as Event);
74
- await waitFor(() => requests.length === 2, "the first debounced search");
75
- requests[1]!.response.resolve(searchResponse("Alpha"));
76
- await waitFor(() => dom.root.textContent?.includes("Alpha preview") ?? false, "the first result");
77
-
78
- input.value = "beta";
79
- input.dispatchEvent(new dom.window.Event("input", { bubbles: true }) as unknown as Event);
80
- await waitFor(() => requests.length === 3, "the refreshed search");
81
-
82
- expect(dom.root.textContent).toContain("Alpha preview");
83
- const spinners = dom.root.querySelectorAll(".ti-loader-2.animate-spin");
84
- expect(spinners).toHaveLength(1);
85
- expect(spinners[0]?.closest("label")).not.toBeNull();
86
-
87
- requests[2]!.response.resolve(searchResponse("Beta"));
88
- await waitFor(() => dom.root.textContent?.includes("Beta preview") ?? false, "the refreshed result");
89
- expect(dom.root.textContent).not.toContain("Alpha preview");
90
- } finally {
91
- dispose();
92
- globalThis.fetch = originalFetch;
93
- dom.cleanup();
94
- }
95
- });
96
- });
@@ -1,148 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { mkdtempSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { resolve } from "node:path";
5
- import { createConfig } from "@k2b/ssr";
6
- import { Hono } from "hono";
7
- import { createComponent } from "solid-js";
8
-
9
- const root = mkdtempSync(resolve(tmpdir(), "cloud-layout-render-tests-"));
10
- const { plugin } = createConfig({ dev: true, rootDir: root });
11
- Bun.plugin(plugin());
12
- process.once("exit", () => rmSync(root, { recursive: true, force: true }));
13
-
14
- const { defineApp } = await import("../_internal/define-app");
15
- const { default: Layout } = await import("./Layout");
16
- const { default: MinimalLayout } = await import("./MinimalLayout");
17
- const { default: AdminLayout } = await import("./AdminLayout");
18
-
19
- const app = defineApp({
20
- id: "layout-render-probe",
21
- name: "Layout render probe",
22
- icon: "ti ti-layout",
23
- description: "Anonymous and minimal layout render probe",
24
- baseUrl: "http://layout-render-probe:3000",
25
- routes: ["/layout", "/minimal"],
26
- });
27
-
28
- type LayoutContextArg = Parameters<typeof Layout>[0]["c"];
29
- type MinimalLayoutContextArg = Parameters<typeof MinimalLayout>[0]["c"];
30
- type PreferencePosition = Exclude<Parameters<typeof MinimalLayout>[0]["preferences"], false | undefined>;
31
- const preferencePositions: PreferencePosition[] = ["top-left", "top-right", "bottom-left", "bottom-right"];
32
-
33
- const server = new Hono()
34
- .use("*", async (c, next) => {
35
- c.set("runtime" as never, { apps: [] } as never);
36
- await next();
37
- })
38
- .get(
39
- "/admin",
40
- ...app.ssr(
41
- (c) => () =>
42
- createComponent(AdminLayout, {
43
- c,
44
- title: "Settings",
45
- scroll: c.req.query("scroll") !== "false",
46
- children: "Admin content",
47
- }),
48
- ),
49
- )
50
- .get(
51
- "/layout",
52
- ...app.ssr(
53
- (c) => () =>
54
- createComponent(Layout, {
55
- c: c as unknown as LayoutContextArg,
56
- fullPage: true,
57
- title: "Public tools",
58
- children: "Anonymous content",
59
- }),
60
- ),
61
- )
62
- .get(
63
- "/minimal/:position",
64
- ...app.ssr((c) => {
65
- const requested = c.req.param("position");
66
- const preferences = preferencePositions.find((position) => position === requested) ?? false;
67
- return () =>
68
- createComponent(MinimalLayout, {
69
- c: c as unknown as MinimalLayoutContextArg,
70
- preferences,
71
- children: "Minimal content",
72
- });
73
- }),
74
- );
75
-
76
- describe("Cloud layouts SSR", () => {
77
- test("admin shell is viewport-bound and delegates scrolling only when requested", async () => {
78
- const ordinary = await (await server.request("/admin")).text();
79
- const bounded = await (await server.request("/admin?scroll=false")).text();
80
- for (const html of [ordinary, bounded]) {
81
- expect(html).toContain("h-dvh overflow-hidden");
82
- expect(html).toContain("Admin content");
83
- expect(html).toContain('data-scroll-preserve="admin-sidebar"');
84
- }
85
- expect(ordinary).not.toMatch(/k2b-app-workspace__main[^>]+data-scroll="false"/);
86
- expect(bounded).toMatch(/k2b-app-workspace__main[^>]+data-scroll="false"/);
87
- expect(bounded).not.toContain("p-[var(--ui-space-shell)]");
88
- });
89
- test("keeps anonymous login and preferences visible without rendering the application rail", async () => {
90
- const response = await server.request("/layout", {
91
- headers: { Cookie: "theme=dark", "Accept-Language": "de-CH" },
92
- });
93
- const html = await response.text();
94
-
95
- expect(response.status).toBe(200);
96
- expect(html).toContain('<html lang="de-CH" class="dark"');
97
- expect(html).toContain("layout-header");
98
- expect(html).toContain('href="/auth/login"');
99
- expect(html).toContain("Anmelden");
100
- expect(html).toContain("Darstellung und Sprache");
101
- expect(html).toContain("Heller Modus");
102
- expect(html).toContain("English");
103
- expect(html).not.toContain("layout-rail-navigation");
104
- expect(html).not.toContain('data-layout-authenticated="true"');
105
- expect(html).not.toContain('href="/me"');
106
- });
107
-
108
- test("positions minimal preferences in every supported corner without adding Cloud chrome", async () => {
109
- const expectedMenuPositions = {
110
- "top-left": "bottom-right",
111
- "top-right": "bottom-left",
112
- "bottom-left": "top-right",
113
- "bottom-right": "top-left",
114
- } as const;
115
-
116
- for (const position of preferencePositions) {
117
- const response = await server.request(`/minimal/${position}`, { headers: { "Accept-Language": "en" } });
118
- const html = await response.text();
119
- expect(response.status).toBe(200);
120
- expect(html).toContain(`minimal-layout-preferences--${position}`);
121
- expect(html).toContain(`data-position="${expectedMenuPositions[position]}"`);
122
- expect(html).toContain("Appearance and language");
123
- expect(html).toContain("Minimal content");
124
- expect(html).not.toContain("cloud-app-canvas");
125
- expect(html).not.toContain("layout-header");
126
- expect(html).not.toContain("layout-rail");
127
- }
128
- });
129
-
130
- test("can disable the minimal preference control completely", async () => {
131
- const response = await server.request("/minimal/off");
132
- const html = await response.text();
133
-
134
- expect(response.status).toBe(200);
135
- expect(html).toContain("Minimal content");
136
- expect(html).not.toContain("minimal-layout-preferences");
137
- expect(html).not.toContain("Appearance and language");
138
- });
139
-
140
- test("keeps every corner safe-area aware", async () => {
141
- const css = await Bun.file(new URL("../styles/utilities-navigation.css", import.meta.url)).text();
142
- for (const position of preferencePositions) expect(css).toContain(`.minimal-layout-preferences--${position}`);
143
- expect(css).toContain("env(safe-area-inset-top)");
144
- expect(css).toContain("env(safe-area-inset-right)");
145
- expect(css).toContain("env(safe-area-inset-bottom)");
146
- expect(css).toContain("env(safe-area-inset-left)");
147
- });
148
- });