@cosmicdrift/kumiko-renderer-web 1.0.0 → 2.0.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/package.json +4 -4
- package/src/__tests__/action-menu.test.tsx +75 -0
- package/src/__tests__/breadcrumb.test.tsx +50 -0
- package/src/__tests__/create-app.test.tsx +171 -1
- package/src/__tests__/dashboard-screen.test.tsx +393 -0
- package/src/__tests__/default-app-shell.test.tsx +47 -1
- package/src/__tests__/form-action-bar.test.tsx +9 -0
- package/src/__tests__/kumiko-screen.test.tsx +155 -1
- package/src/__tests__/lightbox.test.tsx +46 -0
- package/src/__tests__/nav-tree.test.tsx +122 -0
- package/src/__tests__/primitives.test.tsx +32 -1
- package/src/__tests__/profile-menu.test.tsx +29 -0
- package/src/__tests__/projection-detail.test.tsx +101 -0
- package/src/__tests__/projection-list-actions.test.tsx +131 -0
- package/src/__tests__/render-list.test.tsx +11 -3
- package/src/__tests__/test-utils.tsx +1 -0
- package/src/__tests__/toast.test.tsx +9 -9
- package/src/__tests__/use-mutation.test.tsx +86 -0
- package/src/__tests__/workspace-shell.test.tsx +102 -47
- package/src/__tests__/xss-safety.test.tsx +62 -0
- package/src/app/__tests__/first-open-screen-qn.test.ts +77 -0
- package/src/app/browser-locale.ts +1 -1
- package/src/app/create-app.tsx +96 -29
- package/src/app/dashboard-body.tsx +454 -0
- package/src/index.ts +75 -1
- package/src/layout/__tests__/nav-registry.test.ts +105 -0
- package/src/layout/__tests__/shell-breadcrumb.test.ts +82 -0
- package/src/layout/default-app-shell.tsx +9 -71
- package/src/layout/nav-tree.tsx +167 -37
- package/src/layout/shell-breadcrumb.ts +46 -0
- package/src/layout/shell-header.tsx +114 -0
- package/src/layout/workspace-shell.tsx +45 -18
- package/src/primitives/__tests__/data-table-logic.test.ts +27 -0
- package/src/primitives/__tests__/input-testid.test.tsx +45 -0
- package/src/primitives/__tests__/money-input.test.tsx +13 -1
- package/src/primitives/dialog.tsx +47 -76
- package/src/primitives/index.tsx +215 -37
- package/src/primitives/layout.tsx +39 -0
- package/src/primitives/lightbox.tsx +39 -0
- package/src/primitives/modal-shell.tsx +62 -0
- package/src/primitives/money-input.tsx +20 -10
- package/src/primitives/toast.tsx +14 -9
- package/src/sse/live-events.ts +6 -1
- package/src/styles.css +25 -0
- package/src/tokens.ts +4 -0
- package/src/ui/__tests__/avatar.test.tsx +66 -0
- package/src/ui/__tests__/sheet.test.tsx +102 -0
- package/src/version/__tests__/update-checker.test.tsx +115 -0
- package/src/widgets/__tests__/ai-text-field.test.tsx +267 -0
- package/src/widgets/__tests__/drawer.test.tsx +35 -0
- package/src/widgets/__tests__/form-kit.test.tsx +368 -0
- package/src/widgets/__tests__/infinity-list.test.tsx +132 -0
- package/src/widgets/__tests__/query-table.test.tsx +118 -0
- package/src/widgets/__tests__/result-panel.test.tsx +117 -0
- package/src/widgets/__tests__/widgets.test.tsx +243 -0
- package/src/widgets/ai-text-field.tsx +370 -0
- package/src/widgets/charts.tsx +228 -0
- package/src/widgets/collapsible-section.tsx +42 -0
- package/src/widgets/detail-list.tsx +43 -0
- package/src/widgets/drawer.tsx +49 -0
- package/src/widgets/feed-list.tsx +35 -0
- package/src/widgets/form-fields.tsx +326 -0
- package/src/widgets/index.ts +59 -0
- package/src/widgets/infinity-list.tsx +112 -0
- package/src/widgets/mode-switch.tsx +41 -0
- package/src/widgets/progress-bar.tsx +27 -0
- package/src/widgets/progress-list.tsx +38 -0
- package/src/widgets/query-table.tsx +103 -0
- package/src/widgets/result-panel.tsx +251 -0
- package/src/widgets/section-card.tsx +30 -0
- package/src/widgets/stat.tsx +181 -0
- package/src/widgets/states.tsx +89 -0
- package/src/widgets/status-badge.tsx +51 -0
- package/src/widgets/use-draft.ts +40 -0
- package/src/ui/card.tsx +0 -93
- package/src/ui/collapsible.tsx +0 -34
- package/src/ui/dropdown-menu.tsx +0 -258
- package/src/ui/select.tsx +0 -191
- /package/src/__tests__/{download.test.ts → download.test.tsx} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "2.0.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "2.0.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "2.0.0",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { ActionMenu, type MenuItemDef } from "../primitives/action-menu";
|
|
5
|
+
|
|
6
|
+
describe("ActionMenu", () => {
|
|
7
|
+
test("renders trigger with aria-label and default testId", () => {
|
|
8
|
+
render(
|
|
9
|
+
<ActionMenu
|
|
10
|
+
trigger={<span>Open</span>}
|
|
11
|
+
triggerLabel="More actions"
|
|
12
|
+
items={[{ kind: "item", id: "a", label: "Alpha", onSelect: () => {} }]}
|
|
13
|
+
/>,
|
|
14
|
+
);
|
|
15
|
+
const trigger = screen.getByTestId("action-menu-trigger");
|
|
16
|
+
expect(trigger.getAttribute("aria-label")).toBe("More actions");
|
|
17
|
+
expect(screen.getByText("Open")).toBeTruthy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("opens menu: item, separator, label, shortcut, danger, icon", async () => {
|
|
21
|
+
const user = userEvent.setup();
|
|
22
|
+
const onSelect = mock(() => {});
|
|
23
|
+
const items: MenuItemDef[] = [
|
|
24
|
+
{ kind: "label", label: "Section" },
|
|
25
|
+
{
|
|
26
|
+
kind: "item",
|
|
27
|
+
id: "edit",
|
|
28
|
+
label: "Edit",
|
|
29
|
+
icon: <span data-testid="edit-icon">✎</span>,
|
|
30
|
+
shortcut: "⌘E",
|
|
31
|
+
onSelect,
|
|
32
|
+
},
|
|
33
|
+
{ kind: "separator" },
|
|
34
|
+
{
|
|
35
|
+
kind: "item",
|
|
36
|
+
id: "delete",
|
|
37
|
+
label: "Delete",
|
|
38
|
+
variant: "danger",
|
|
39
|
+
onSelect: () => {},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
kind: "item",
|
|
43
|
+
id: "disabled",
|
|
44
|
+
label: "Disabled",
|
|
45
|
+
disabled: true,
|
|
46
|
+
onSelect: () => {},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
render(
|
|
51
|
+
<ActionMenu
|
|
52
|
+
trigger={<span>Menu</span>}
|
|
53
|
+
items={items}
|
|
54
|
+
align="start"
|
|
55
|
+
minWidth="12rem"
|
|
56
|
+
testId="am"
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
await user.click(screen.getByTestId("am"));
|
|
60
|
+
|
|
61
|
+
await waitFor(() => {
|
|
62
|
+
expect(screen.getByText("Section")).toBeTruthy();
|
|
63
|
+
expect(screen.getByTestId("action-menu-item-edit")).toBeTruthy();
|
|
64
|
+
expect(screen.getByTestId("edit-icon")).toBeTruthy();
|
|
65
|
+
expect(screen.getByText("⌘E")).toBeTruthy();
|
|
66
|
+
expect(screen.getByTestId("action-menu-item-delete").className).toContain("text-destructive");
|
|
67
|
+
expect(screen.getByTestId("action-menu-item-disabled").getAttribute("data-disabled")).toBe(
|
|
68
|
+
"",
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await user.click(screen.getByTestId("action-menu-item-edit"));
|
|
73
|
+
expect(onSelect).toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import {
|
|
4
|
+
Breadcrumb,
|
|
5
|
+
BreadcrumbEllipsis,
|
|
6
|
+
BreadcrumbItem,
|
|
7
|
+
BreadcrumbLink,
|
|
8
|
+
BreadcrumbList,
|
|
9
|
+
BreadcrumbPage,
|
|
10
|
+
BreadcrumbSeparator,
|
|
11
|
+
} from "../ui/breadcrumb";
|
|
12
|
+
|
|
13
|
+
describe("Breadcrumb", () => {
|
|
14
|
+
test("renders nav list with links, separators, page, ellipsis", () => {
|
|
15
|
+
render(
|
|
16
|
+
<Breadcrumb>
|
|
17
|
+
<BreadcrumbList>
|
|
18
|
+
<BreadcrumbItem>
|
|
19
|
+
<BreadcrumbLink href="/">Home</BreadcrumbLink>
|
|
20
|
+
</BreadcrumbItem>
|
|
21
|
+
<BreadcrumbSeparator />
|
|
22
|
+
<BreadcrumbItem>
|
|
23
|
+
<BreadcrumbEllipsis />
|
|
24
|
+
</BreadcrumbItem>
|
|
25
|
+
<BreadcrumbSeparator>
|
|
26
|
+
<span data-testid="custom-sep">/</span>
|
|
27
|
+
</BreadcrumbSeparator>
|
|
28
|
+
<BreadcrumbItem>
|
|
29
|
+
<BreadcrumbLink asChild>
|
|
30
|
+
<a href="/docs">Docs</a>
|
|
31
|
+
</BreadcrumbLink>
|
|
32
|
+
</BreadcrumbItem>
|
|
33
|
+
<BreadcrumbSeparator />
|
|
34
|
+
<BreadcrumbItem>
|
|
35
|
+
<BreadcrumbPage>Current</BreadcrumbPage>
|
|
36
|
+
</BreadcrumbItem>
|
|
37
|
+
</BreadcrumbList>
|
|
38
|
+
</Breadcrumb>,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(screen.getByLabelText("breadcrumb")).toBeTruthy();
|
|
42
|
+
expect(screen.getByText("Home").getAttribute("href")).toBe("/");
|
|
43
|
+
expect(screen.getByText("More")).toBeTruthy();
|
|
44
|
+
expect(screen.getByTestId("custom-sep")).toBeTruthy();
|
|
45
|
+
expect(screen.getByText("Docs").getAttribute("href")).toBe("/docs");
|
|
46
|
+
const page = screen.getByText("Current");
|
|
47
|
+
expect(page.getAttribute("aria-current")).toBe("page");
|
|
48
|
+
expect(page.getAttribute("aria-disabled")).toBe("true");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import type {
|
|
3
|
+
DashboardScreenDefinition,
|
|
3
4
|
EntityDefinition,
|
|
4
5
|
EntityEditScreenDefinition,
|
|
5
6
|
EntityListScreenDefinition,
|
|
6
7
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
7
8
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AppSchema,
|
|
11
|
+
ColumnRendererProps,
|
|
12
|
+
FeatureSchema,
|
|
13
|
+
NavApi,
|
|
14
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
15
|
+
import { createStaticLocaleResolver } from "@cosmicdrift/kumiko-renderer";
|
|
9
16
|
import { act, screen, waitFor } from "@testing-library/react";
|
|
10
17
|
import type { ReactNode } from "react";
|
|
11
18
|
import type { ClientFeatureDefinition } from "../app/client-plugin";
|
|
@@ -54,6 +61,7 @@ const baseSchema: FeatureSchema = {
|
|
|
54
61
|
featureName: "tasks",
|
|
55
62
|
entities: { task: taskEntity },
|
|
56
63
|
screens: [editScreen, listScreen],
|
|
64
|
+
navs: [{ id: "task-edit", label: "tasks:nav.task-edit", screen: "tasks:screen:task-edit" }],
|
|
57
65
|
};
|
|
58
66
|
|
|
59
67
|
// createKumikoApp ruft createRoot(...).render(...) direkt auf — React 18+
|
|
@@ -148,6 +156,55 @@ describe("createKumikoApp", () => {
|
|
|
148
156
|
);
|
|
149
157
|
});
|
|
150
158
|
|
|
159
|
+
test("fallback screen skips access-restricted screens across features (#1176)", async () => {
|
|
160
|
+
mountRoot();
|
|
161
|
+
const restrictedFeature: FeatureSchema = {
|
|
162
|
+
featureName: "user",
|
|
163
|
+
entities: { task: taskEntity },
|
|
164
|
+
screens: [{ ...listScreen, id: "user-list", access: { roles: ["SystemAdmin"] } }],
|
|
165
|
+
};
|
|
166
|
+
const openFeature: FeatureSchema = { ...baseSchema, featureName: "tasks" };
|
|
167
|
+
const schema: AppSchema = { features: [restrictedFeature, openFeature] };
|
|
168
|
+
await mountApp({ schema, dispatcher: makeDispatcher() });
|
|
169
|
+
// Landing on user-list (SystemAdmin-only) would show an access-denied
|
|
170
|
+
// banner instead — the tasks:task-edit form must win instead.
|
|
171
|
+
await waitFor(() => expect(screen.getByTestId("render-edit-form")).toBeTruthy());
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("fallback screen: an openToAll screen counts as accessible", async () => {
|
|
175
|
+
mountRoot();
|
|
176
|
+
const restrictedFeature: FeatureSchema = {
|
|
177
|
+
featureName: "user",
|
|
178
|
+
entities: { task: taskEntity },
|
|
179
|
+
screens: [{ ...editScreen, id: "user-edit", access: { roles: ["SystemAdmin"] } }],
|
|
180
|
+
};
|
|
181
|
+
const openFeature: FeatureSchema = {
|
|
182
|
+
featureName: "tasks",
|
|
183
|
+
entities: { task: taskEntity },
|
|
184
|
+
screens: [{ ...listScreen, access: { openToAll: true } }],
|
|
185
|
+
navs: [{ id: "task-list", label: "tasks:nav.task-list", screen: "tasks:screen:task-list" }],
|
|
186
|
+
};
|
|
187
|
+
const schema: AppSchema = { features: [restrictedFeature, openFeature] };
|
|
188
|
+
await mountApp({ schema, dispatcher: makeDispatcher() });
|
|
189
|
+
expect(await screen.findByTestId("render-list-table-empty")).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("all screens access-restricted → throws (no accessible fallback)", () => {
|
|
193
|
+
mountRoot();
|
|
194
|
+
const restrictedOnly: AppSchema = {
|
|
195
|
+
features: [
|
|
196
|
+
{
|
|
197
|
+
featureName: "user",
|
|
198
|
+
entities: { task: taskEntity },
|
|
199
|
+
screens: [{ ...editScreen, access: { roles: ["SystemAdmin"] } }],
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
};
|
|
203
|
+
expect(() => createKumikoApp({ schema: restrictedOnly, dispatcher: makeDispatcher() })).toThrow(
|
|
204
|
+
/no screens/,
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
151
208
|
test("clientFeatures.columnRenderers → bei Key-Kollision warnt + last-wins gewinnt", async () => {
|
|
152
209
|
// Zwei Features liefern denselben Renderer-Key — der Merge in
|
|
153
210
|
// create-app warnt und behält den späteren Eintrag (Last-Wins).
|
|
@@ -175,6 +232,9 @@ describe("createKumikoApp", () => {
|
|
|
175
232
|
columns: [{ field: "color", renderer: { react: { __component: "Swatch" } } }],
|
|
176
233
|
},
|
|
177
234
|
],
|
|
235
|
+
navs: [
|
|
236
|
+
{ id: "color-list", label: "tasks:nav.color-list", screen: "tasks:screen:color-list" },
|
|
237
|
+
],
|
|
178
238
|
};
|
|
179
239
|
|
|
180
240
|
mountRoot();
|
|
@@ -228,6 +288,9 @@ describe("createKumikoApp", () => {
|
|
|
228
288
|
featureName: "tasks",
|
|
229
289
|
entities: { task: colorEntity },
|
|
230
290
|
screens: [colorListScreen],
|
|
291
|
+
navs: [
|
|
292
|
+
{ id: "color-list", label: "tasks:nav.color-list", screen: "tasks:screen:color-list" },
|
|
293
|
+
],
|
|
231
294
|
};
|
|
232
295
|
const dispatcher = createMockDispatcher({
|
|
233
296
|
query: (async () => ({
|
|
@@ -252,6 +315,113 @@ describe("createKumikoApp", () => {
|
|
|
252
315
|
expect(screen.getByTestId("ca-swatch-field").textContent).toBe("color");
|
|
253
316
|
});
|
|
254
317
|
|
|
318
|
+
test("schema.translations (r.translations, #1059) resolves nav/dashboard labels without any clientFeatures duplication", async () => {
|
|
319
|
+
// Mirrors cap-counter's real shape: a feature that declares r.translations
|
|
320
|
+
// for its own nav label but ships NO web/i18n.ts / clientFeatures entry at
|
|
321
|
+
// all. Before #1059 this label rendered as the raw i18n key forever.
|
|
322
|
+
const dashboardScreen: DashboardScreenDefinition = {
|
|
323
|
+
id: "overview",
|
|
324
|
+
type: "dashboard",
|
|
325
|
+
panels: [
|
|
326
|
+
{
|
|
327
|
+
kind: "stat",
|
|
328
|
+
id: "cap-list",
|
|
329
|
+
label: "cap-counter:nav.cap-list",
|
|
330
|
+
query: "cap-counter:query:stat",
|
|
331
|
+
valueField: "value",
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
const schema: FeatureSchema = {
|
|
336
|
+
featureName: "cap-counter",
|
|
337
|
+
entities: {},
|
|
338
|
+
screens: [dashboardScreen],
|
|
339
|
+
navs: [
|
|
340
|
+
{
|
|
341
|
+
id: "overview",
|
|
342
|
+
label: "cap-counter:nav.cap-list",
|
|
343
|
+
screen: "cap-counter:screen:overview",
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
translations: {
|
|
347
|
+
"cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
|
|
348
|
+
},
|
|
349
|
+
};
|
|
350
|
+
const dispatcher = createMockDispatcher({
|
|
351
|
+
query: (async () => ({
|
|
352
|
+
isSuccess: true,
|
|
353
|
+
data: { value: "3" },
|
|
354
|
+
})) as unknown as Dispatcher["query"],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
mountRoot();
|
|
358
|
+
await mountApp({
|
|
359
|
+
schema,
|
|
360
|
+
dispatcher,
|
|
361
|
+
locale: createStaticLocaleResolver({ locale: "en" }),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
await waitFor(() => expect(screen.getByText("Caps")).toBeTruthy());
|
|
365
|
+
expect(screen.queryByText("cap-counter:nav.cap-list")).toBeNull();
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("clientFeatures.translations still overrides schema.translations for the same key (precedence)", async () => {
|
|
369
|
+
// App-Override muss auch gegen ein framework-eigenes Label gewinnen —
|
|
370
|
+
// sonst kann eine App ein bundled-feature-Label nicht mehr anpassen,
|
|
371
|
+
// sobald #1059 dessen r.translations automatisch mitliefert.
|
|
372
|
+
const dashboardScreen: DashboardScreenDefinition = {
|
|
373
|
+
id: "overview",
|
|
374
|
+
type: "dashboard",
|
|
375
|
+
panels: [
|
|
376
|
+
{
|
|
377
|
+
kind: "stat",
|
|
378
|
+
id: "cap-list",
|
|
379
|
+
label: "cap-counter:nav.cap-list",
|
|
380
|
+
query: "cap-counter:query:stat",
|
|
381
|
+
valueField: "value",
|
|
382
|
+
},
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
const schema: FeatureSchema = {
|
|
386
|
+
featureName: "cap-counter",
|
|
387
|
+
entities: {},
|
|
388
|
+
screens: [dashboardScreen],
|
|
389
|
+
navs: [
|
|
390
|
+
{
|
|
391
|
+
id: "overview",
|
|
392
|
+
label: "cap-counter:nav.cap-list",
|
|
393
|
+
screen: "cap-counter:screen:overview",
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
translations: {
|
|
397
|
+
"cap-counter:nav.cap-list": { de: "Limits", en: "Caps" },
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
const dispatcher = createMockDispatcher({
|
|
401
|
+
query: (async () => ({
|
|
402
|
+
isSuccess: true,
|
|
403
|
+
data: { value: "3" },
|
|
404
|
+
})) as unknown as Dispatcher["query"],
|
|
405
|
+
});
|
|
406
|
+
const override: ClientFeatureDefinition = {
|
|
407
|
+
name: "cap-counter-app-override",
|
|
408
|
+
translations: {
|
|
409
|
+
en: { "cap-counter:nav.cap-list": "Quota" },
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
mountRoot();
|
|
414
|
+
await mountApp({
|
|
415
|
+
schema,
|
|
416
|
+
dispatcher,
|
|
417
|
+
clientFeatures: [override],
|
|
418
|
+
locale: createStaticLocaleResolver({ locale: "en" }),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await waitFor(() => expect(screen.getByText("Quota")).toBeTruthy());
|
|
422
|
+
expect(screen.queryByText("Caps")).toBeNull();
|
|
423
|
+
});
|
|
424
|
+
|
|
255
425
|
test("navAdapter override: eigener Router steuert den aktiven Screen", async () => {
|
|
256
426
|
// Beweist den Nav-Seam: der Default-Adapter liest location.pathname,
|
|
257
427
|
// dieser Memory-Adapter hardcoded die Route. Wenn swap funktioniert,
|