@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1
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/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3801
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1733
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -88
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-query.test.tsx +0 -149
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,719 +0,0 @@
|
|
|
1
|
-
// fw#2166: projectionDetail screens can declare header `actions`, and get a
|
|
2
|
-
// default "edit" action for free when `detailFor` names an entity that has a
|
|
3
|
-
// visible entityEdit screen somewhere in the app (not just this feature —
|
|
4
|
-
// the motivating case is a projectionDetail whose query belongs to one
|
|
5
|
-
// feature while the entity's entityEdit screen lives in another). This
|
|
6
|
-
// renders the real path (KumikoScreen → ProjectionDetailBody → RenderEdit)
|
|
7
|
-
// under a stub dispatcher + AppFeaturesProvider, and proves the default-edit
|
|
8
|
-
// resolution rules from the boot-validator's detailFor doc (cross-feature
|
|
9
|
-
// lookup, access-gating, id: "edit" declared-action suppression).
|
|
10
|
-
|
|
11
|
-
import { describe, expect, test } from "bun:test";
|
|
12
|
-
import type {
|
|
13
|
-
AccessRule,
|
|
14
|
-
EntityEditScreenDefinition,
|
|
15
|
-
ProjectionDetailScreenDefinition,
|
|
16
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
17
|
-
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
18
|
-
import { act, fireEvent, render, waitFor } from "@testing-library/react";
|
|
19
|
-
import type { ComponentType, ReactNode } from "react";
|
|
20
|
-
import { useState } from "react";
|
|
21
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
22
|
-
import { UserRolesProvider } from "../../context/user-roles-context";
|
|
23
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
24
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
25
|
-
import {
|
|
26
|
-
type ActionOverflowMenuProps,
|
|
27
|
-
type BannerProps,
|
|
28
|
-
type ButtonProps,
|
|
29
|
-
type CardProps,
|
|
30
|
-
type CorePrimitives,
|
|
31
|
-
type DialogProps,
|
|
32
|
-
type FormProps,
|
|
33
|
-
PrimitivesProvider,
|
|
34
|
-
} from "../../primitives";
|
|
35
|
-
import { AppFeaturesProvider } from "../app-features-context";
|
|
36
|
-
import type { FeatureSchema } from "../feature-schema";
|
|
37
|
-
import { KumikoScreen } from "../kumiko-screen";
|
|
38
|
-
import type { NavApi, ScreenTarget } from "../nav";
|
|
39
|
-
import { NavProvider } from "../nav";
|
|
40
|
-
|
|
41
|
-
const noop = (): ReactNode => null;
|
|
42
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
43
|
-
|
|
44
|
-
// Card's title/subtitle/headerActions arrive as `slots`; this stub renders
|
|
45
|
-
// them so tests asserting on header-action content still see it.
|
|
46
|
-
const CardWithSlots: ComponentType<CardProps> = ({ slots, children }) => (
|
|
47
|
-
<>
|
|
48
|
-
{slots?.title}
|
|
49
|
-
{slots?.subtitle}
|
|
50
|
-
{slots?.headerActions}
|
|
51
|
-
{children}
|
|
52
|
-
</>
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
const TestButton: ComponentType<ButtonProps> = ({ children, onClick, testId }) => (
|
|
56
|
-
<button type="button" data-testid={testId} onClick={() => void onClick?.()}>
|
|
57
|
-
{children}
|
|
58
|
-
</button>
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// Header actions (and their error banner) now render inside ProjectionDetail's
|
|
62
|
-
// own head Card, passed through as `headerRegion` (fw#2713) rather than
|
|
63
|
-
// through Form's `actions`/`secondaryActions` slots — wrapped in a distinct
|
|
64
|
-
// testid'd container so a test can assert both live there, not in the
|
|
65
|
-
// actions/body regions Form itself owns.
|
|
66
|
-
const FormWithActions: ComponentType<FormProps> = ({
|
|
67
|
-
children,
|
|
68
|
-
actions,
|
|
69
|
-
secondaryActions,
|
|
70
|
-
headerRegion,
|
|
71
|
-
}) => (
|
|
72
|
-
<>
|
|
73
|
-
{headerRegion !== undefined && <div data-testid="form-header">{headerRegion}</div>}
|
|
74
|
-
<div data-testid="form-body">{children}</div>
|
|
75
|
-
<div data-testid="form-actions">
|
|
76
|
-
{secondaryActions}
|
|
77
|
-
{actions}
|
|
78
|
-
</div>
|
|
79
|
-
</>
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// passChildren would drop testId — this test asserts on Banner structure/
|
|
83
|
-
// placement, so it needs a real (if minimal) wrapper element.
|
|
84
|
-
const TestBanner: ComponentType<BannerProps> = ({ children, testId }) => (
|
|
85
|
-
<div data-testid={testId}>{children}</div>
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
// Minimal overflow-menu double (A7): a toggle button reveals the items as
|
|
89
|
-
// plain buttons, own testid per item so a test can assert an item is
|
|
90
|
-
// reachable only after the menu opens — no dropdown/portal machinery, this
|
|
91
|
-
// package doesn't test the real (renderer-web) widget.
|
|
92
|
-
const TestActionOverflowMenu: ComponentType<ActionOverflowMenuProps> = ({
|
|
93
|
-
items,
|
|
94
|
-
label,
|
|
95
|
-
testId,
|
|
96
|
-
}) => {
|
|
97
|
-
const [open, setOpen] = useState(false);
|
|
98
|
-
return (
|
|
99
|
-
<div>
|
|
100
|
-
<button
|
|
101
|
-
type="button"
|
|
102
|
-
aria-label={label}
|
|
103
|
-
data-testid={testId ?? "action-overflow-trigger"}
|
|
104
|
-
onClick={() => setOpen((o) => !o)}
|
|
105
|
-
>
|
|
106
|
-
{"⋯"}
|
|
107
|
-
</button>
|
|
108
|
-
{open &&
|
|
109
|
-
items.map((item) => (
|
|
110
|
-
<button
|
|
111
|
-
key={item.id}
|
|
112
|
-
type="button"
|
|
113
|
-
data-testid={`${testId ?? "action-overflow"}-item-${item.id}`}
|
|
114
|
-
disabled={item.disabled}
|
|
115
|
-
onClick={item.onSelect}
|
|
116
|
-
>
|
|
117
|
-
{item.label}
|
|
118
|
-
</button>
|
|
119
|
-
))}
|
|
120
|
-
</div>
|
|
121
|
-
);
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// Records the props of the last Dialog rendered *open* (a closed Dialog from
|
|
125
|
-
// an always-mounted RenderEditActionButton never overwrites it) — lets a
|
|
126
|
-
// test assert a confirm was requested and drive its onConfirm, without a
|
|
127
|
-
// real dialog/portal implementation.
|
|
128
|
-
type DialogSpy = ComponentType<DialogProps> & { lastProps: DialogProps | null };
|
|
129
|
-
const TestDialog: DialogSpy = Object.assign(
|
|
130
|
-
(props: DialogProps): ReactNode => {
|
|
131
|
-
if (props.open) TestDialog.lastProps = props;
|
|
132
|
-
return null;
|
|
133
|
-
},
|
|
134
|
-
{ lastProps: null as DialogProps | null },
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
const testPrimitives: CorePrimitives = {
|
|
138
|
-
Button: TestButton,
|
|
139
|
-
Banner: TestBanner,
|
|
140
|
-
Field: passChildren,
|
|
141
|
-
Input: noop,
|
|
142
|
-
DataTable: noop,
|
|
143
|
-
Form: FormWithActions,
|
|
144
|
-
Section: passChildren,
|
|
145
|
-
Card: CardWithSlots,
|
|
146
|
-
Grid: passChildren,
|
|
147
|
-
GridCell: passChildren,
|
|
148
|
-
Text: passChildren,
|
|
149
|
-
Heading: noop,
|
|
150
|
-
Dialog: TestDialog,
|
|
151
|
-
Modal: noop,
|
|
152
|
-
Lightbox: noop,
|
|
153
|
-
ConfigSourceBadge: noop,
|
|
154
|
-
ConfigCascadeView: noop,
|
|
155
|
-
Link: noop,
|
|
156
|
-
ActionOverflowMenu: TestActionOverflowMenu,
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
function stubDispatcher(
|
|
160
|
-
record: Readonly<Record<string, unknown>>,
|
|
161
|
-
writeErrorMessage?: string,
|
|
162
|
-
): Dispatcher {
|
|
163
|
-
const writeResult =
|
|
164
|
-
writeErrorMessage !== undefined
|
|
165
|
-
? {
|
|
166
|
-
isSuccess: false,
|
|
167
|
-
error: {
|
|
168
|
-
code: "write-failed",
|
|
169
|
-
httpStatus: 500,
|
|
170
|
-
i18nKey: "kumiko.errors.does-not-exist-in-any-bundle",
|
|
171
|
-
message: writeErrorMessage,
|
|
172
|
-
},
|
|
173
|
-
}
|
|
174
|
-
: { isSuccess: true, data: {} };
|
|
175
|
-
return {
|
|
176
|
-
write: (async () => writeResult) as unknown as Dispatcher["write"],
|
|
177
|
-
query: (async () => ({ isSuccess: true, data: record })) as unknown as Dispatcher["query"],
|
|
178
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as unknown as Dispatcher["batch"],
|
|
179
|
-
statusStore: {
|
|
180
|
-
getState: () => "online",
|
|
181
|
-
subscribe: () => () => {},
|
|
182
|
-
} as unknown as Dispatcher["statusStore"],
|
|
183
|
-
async *stream() {},
|
|
184
|
-
pendingWrites: () => [],
|
|
185
|
-
pendingFiles: () => [],
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function detailScreen(
|
|
190
|
-
overrides?: Partial<ProjectionDetailScreenDefinition & { readonly detailFor?: string }>,
|
|
191
|
-
): FeatureSchema["screens"][number] {
|
|
192
|
-
return {
|
|
193
|
-
id: "rent-detail",
|
|
194
|
-
type: "projectionDetail",
|
|
195
|
-
query: "app:query:rent:detail",
|
|
196
|
-
layout: { sections: [{ title: "s", fields: ["description"] }] },
|
|
197
|
-
detailFor: "rent",
|
|
198
|
-
...overrides,
|
|
199
|
-
} as FeatureSchema["screens"][number];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Registry-qualified id ("<feature>:screen:<short>"), matching real schema
|
|
203
|
-
// output — a bare short id here would hide the QN-vs-short-form bug this
|
|
204
|
-
// test suite exists to catch (fw#2166 review finding 1/4).
|
|
205
|
-
function editScreen(
|
|
206
|
-
entity: string,
|
|
207
|
-
access?: AccessRule,
|
|
208
|
-
featureName = "app",
|
|
209
|
-
): EntityEditScreenDefinition {
|
|
210
|
-
return {
|
|
211
|
-
id: `${featureName}:screen:rent-edit`,
|
|
212
|
-
type: "entityEdit",
|
|
213
|
-
entity,
|
|
214
|
-
layout: { sections: [{ columns: 1, fields: ["name"] }] },
|
|
215
|
-
...(access !== undefined && { access }),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function renderDetail(opts: {
|
|
220
|
-
readonly primarySchema: FeatureSchema;
|
|
221
|
-
readonly features: readonly FeatureSchema[];
|
|
222
|
-
readonly userRoles?: readonly string[];
|
|
223
|
-
readonly record?: Readonly<Record<string, unknown>>;
|
|
224
|
-
readonly onNavigate?: (target: ScreenTarget) => void;
|
|
225
|
-
readonly writeErrorMessage?: string;
|
|
226
|
-
}): ReturnType<typeof render> {
|
|
227
|
-
const navApi: NavApi = {
|
|
228
|
-
route: { screenId: "app:screen:rent-detail" },
|
|
229
|
-
// Header actions only ever navigate with a ScreenTarget (screenId +
|
|
230
|
-
// entityId) — same narrowing idiom as nav.tsx's resolveTarget.
|
|
231
|
-
navigate: (target) => {
|
|
232
|
-
if ("screenId" in target) opts.onNavigate?.(target);
|
|
233
|
-
},
|
|
234
|
-
replace: () => {},
|
|
235
|
-
hrefFor: () => "",
|
|
236
|
-
searchParams: {},
|
|
237
|
-
setSearchParams: () => {},
|
|
238
|
-
};
|
|
239
|
-
return render(
|
|
240
|
-
<LocaleProvider
|
|
241
|
-
resolver={createStaticLocaleResolver({ locale: "de-DE" })}
|
|
242
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
243
|
-
>
|
|
244
|
-
<DispatcherProvider
|
|
245
|
-
dispatcher={stubDispatcher(
|
|
246
|
-
opts.record ?? { id: "rent-1", description: "Rent for April" },
|
|
247
|
-
opts.writeErrorMessage,
|
|
248
|
-
)}
|
|
249
|
-
>
|
|
250
|
-
<AppFeaturesProvider features={opts.features}>
|
|
251
|
-
<UserRolesProvider roles={opts.userRoles}>
|
|
252
|
-
<NavProvider value={navApi}>
|
|
253
|
-
<PrimitivesProvider value={testPrimitives}>
|
|
254
|
-
<KumikoScreen
|
|
255
|
-
schema={opts.primarySchema}
|
|
256
|
-
qn="app:screen:rent-detail"
|
|
257
|
-
entityId="rent-1"
|
|
258
|
-
/>
|
|
259
|
-
</PrimitivesProvider>
|
|
260
|
-
</NavProvider>
|
|
261
|
-
</UserRolesProvider>
|
|
262
|
-
</AppFeaturesProvider>
|
|
263
|
-
</DispatcherProvider>
|
|
264
|
-
</LocaleProvider>,
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
describe("projectionDetail default edit action (fw#2166)", () => {
|
|
269
|
-
test("detailFor + matching entityEdit in the SAME feature → edit button navigates with the record id", async () => {
|
|
270
|
-
const schema: FeatureSchema = {
|
|
271
|
-
featureName: "app",
|
|
272
|
-
entities: {},
|
|
273
|
-
screens: [detailScreen(), editScreen("rent")],
|
|
274
|
-
};
|
|
275
|
-
let navigated: ScreenTarget | undefined;
|
|
276
|
-
const { getByTestId, queryByText } = renderDetail({
|
|
277
|
-
primarySchema: schema,
|
|
278
|
-
features: [schema],
|
|
279
|
-
userRoles: [],
|
|
280
|
-
onNavigate: (target) => {
|
|
281
|
-
navigated = target;
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
285
|
-
|
|
286
|
-
// trigger() is async (busy-state bookkeeping around onPress), so the
|
|
287
|
-
// click's state updates land after this tick — act() flushes them.
|
|
288
|
-
await act(async () => {
|
|
289
|
-
fireEvent.click(getByTestId("render-edit-action-edit"));
|
|
290
|
-
});
|
|
291
|
-
expect(navigated).toEqual({ screenId: "rent-edit", entityId: "rent-1" });
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("edit screen access denied → no edit button", async () => {
|
|
295
|
-
const schema: FeatureSchema = {
|
|
296
|
-
featureName: "app",
|
|
297
|
-
entities: {},
|
|
298
|
-
screens: [detailScreen(), editScreen("rent", { roles: ["admin"] })],
|
|
299
|
-
};
|
|
300
|
-
const { queryByTestId, queryByText } = renderDetail({
|
|
301
|
-
primarySchema: schema,
|
|
302
|
-
features: [schema],
|
|
303
|
-
userRoles: [],
|
|
304
|
-
});
|
|
305
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
306
|
-
|
|
307
|
-
expect(queryByTestId("render-edit-action-edit")).toBeNull();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
test("no entityEdit screen for the detailFor entity → no edit button", async () => {
|
|
311
|
-
const schema: FeatureSchema = {
|
|
312
|
-
featureName: "app",
|
|
313
|
-
entities: {},
|
|
314
|
-
screens: [detailScreen()],
|
|
315
|
-
};
|
|
316
|
-
const { queryByTestId, queryByText } = renderDetail({
|
|
317
|
-
primarySchema: schema,
|
|
318
|
-
features: [schema],
|
|
319
|
-
userRoles: [],
|
|
320
|
-
});
|
|
321
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
322
|
-
|
|
323
|
-
expect(queryByTestId("render-edit-action-edit")).toBeNull();
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
test('a declared actions entry with id: "edit" wins — exactly one edit button, the declared one', async () => {
|
|
327
|
-
const schema: FeatureSchema = {
|
|
328
|
-
featureName: "app",
|
|
329
|
-
entities: {},
|
|
330
|
-
screens: [
|
|
331
|
-
detailScreen({
|
|
332
|
-
actions: [
|
|
333
|
-
{ kind: "navigate", id: "edit", label: "custom-edit-label", screen: "rent-edit" },
|
|
334
|
-
],
|
|
335
|
-
}),
|
|
336
|
-
editScreen("rent"),
|
|
337
|
-
],
|
|
338
|
-
};
|
|
339
|
-
const { getAllByTestId, queryByText } = renderDetail({
|
|
340
|
-
primarySchema: schema,
|
|
341
|
-
features: [schema],
|
|
342
|
-
userRoles: [],
|
|
343
|
-
});
|
|
344
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
345
|
-
|
|
346
|
-
const editButtons = getAllByTestId("render-edit-action-edit");
|
|
347
|
-
expect(editButtons).toHaveLength(1);
|
|
348
|
-
// "custom-edit-label" has no translation registered, so translate()
|
|
349
|
-
// returns the raw key — proving this is the declared action, not the
|
|
350
|
-
// default (which renders the translated "kumiko.actions.edit").
|
|
351
|
-
expect(editButtons[0]?.textContent).toBe("custom-edit-label");
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
test("two entityEdit screens for the same entity, only the second accessible → button still renders, targeting the second (review finding 2)", async () => {
|
|
355
|
-
const lockedEdit: EntityEditScreenDefinition = {
|
|
356
|
-
id: "app:screen:rent-edit-locked",
|
|
357
|
-
type: "entityEdit",
|
|
358
|
-
entity: "rent",
|
|
359
|
-
layout: { sections: [{ columns: 1, fields: ["name"] }] },
|
|
360
|
-
access: { roles: ["admin"] },
|
|
361
|
-
};
|
|
362
|
-
const openEdit: EntityEditScreenDefinition = {
|
|
363
|
-
id: "app:screen:rent-edit-open",
|
|
364
|
-
type: "entityEdit",
|
|
365
|
-
entity: "rent",
|
|
366
|
-
layout: { sections: [{ columns: 1, fields: ["name"] }] },
|
|
367
|
-
};
|
|
368
|
-
const schema: FeatureSchema = {
|
|
369
|
-
featureName: "app",
|
|
370
|
-
entities: {},
|
|
371
|
-
screens: [detailScreen(), lockedEdit, openEdit],
|
|
372
|
-
};
|
|
373
|
-
let navigated: ScreenTarget | undefined;
|
|
374
|
-
const { getByTestId, queryByText } = renderDetail({
|
|
375
|
-
primarySchema: schema,
|
|
376
|
-
features: [schema],
|
|
377
|
-
userRoles: [],
|
|
378
|
-
onNavigate: (target) => {
|
|
379
|
-
navigated = target;
|
|
380
|
-
},
|
|
381
|
-
});
|
|
382
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
383
|
-
|
|
384
|
-
await act(async () => {
|
|
385
|
-
fireEvent.click(getByTestId("render-edit-action-edit"));
|
|
386
|
-
});
|
|
387
|
-
expect(navigated).toEqual({ screenId: "rent-edit-open", entityId: "rent-1" });
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("declared navigate action without entityId, targeting an entityEdit of the SAME entity → auto-fills the record id (review finding 3)", async () => {
|
|
391
|
-
const schema: FeatureSchema = {
|
|
392
|
-
featureName: "app",
|
|
393
|
-
entities: {},
|
|
394
|
-
screens: [
|
|
395
|
-
detailScreen({
|
|
396
|
-
actions: [
|
|
397
|
-
{
|
|
398
|
-
kind: "navigate",
|
|
399
|
-
id: "open-record",
|
|
400
|
-
label: "actions.openRecord",
|
|
401
|
-
screen: "rent-edit",
|
|
402
|
-
},
|
|
403
|
-
],
|
|
404
|
-
}),
|
|
405
|
-
editScreen("rent"),
|
|
406
|
-
],
|
|
407
|
-
};
|
|
408
|
-
let navigated: ScreenTarget | undefined;
|
|
409
|
-
const { getByTestId, queryByText } = renderDetail({
|
|
410
|
-
primarySchema: schema,
|
|
411
|
-
features: [schema],
|
|
412
|
-
userRoles: [],
|
|
413
|
-
onNavigate: (target) => {
|
|
414
|
-
navigated = target;
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
418
|
-
|
|
419
|
-
await act(async () => {
|
|
420
|
-
fireEvent.click(getByTestId("render-edit-action-open-record"));
|
|
421
|
-
});
|
|
422
|
-
expect(navigated).toEqual({ screenId: "rent-edit", entityId: "rent-1" });
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
test("declared action with a non-matching visible condition → no button rendered", async () => {
|
|
426
|
-
const schema: FeatureSchema = {
|
|
427
|
-
featureName: "app",
|
|
428
|
-
entities: {},
|
|
429
|
-
screens: [
|
|
430
|
-
detailScreen({
|
|
431
|
-
actions: [
|
|
432
|
-
{
|
|
433
|
-
kind: "navigate",
|
|
434
|
-
id: "archive",
|
|
435
|
-
label: "actions.archive",
|
|
436
|
-
screen: "rent-edit",
|
|
437
|
-
visible: { field: "status", eq: "closed" },
|
|
438
|
-
},
|
|
439
|
-
],
|
|
440
|
-
}),
|
|
441
|
-
],
|
|
442
|
-
};
|
|
443
|
-
const { queryByTestId, queryByText } = renderDetail({
|
|
444
|
-
primarySchema: schema,
|
|
445
|
-
features: [schema],
|
|
446
|
-
userRoles: [],
|
|
447
|
-
record: { id: "rent-1", description: "Rent for April", status: "open" },
|
|
448
|
-
});
|
|
449
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
450
|
-
|
|
451
|
-
expect(queryByTestId("render-edit-action-archive")).toBeNull();
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
test("entityEdit screen lives in ANOTHER feature → edit button is still there (cross-feature resolution)", async () => {
|
|
455
|
-
const appSchema: FeatureSchema = {
|
|
456
|
-
featureName: "app",
|
|
457
|
-
entities: {},
|
|
458
|
-
screens: [detailScreen()],
|
|
459
|
-
};
|
|
460
|
-
const billingSchema: FeatureSchema = {
|
|
461
|
-
featureName: "billing",
|
|
462
|
-
entities: {},
|
|
463
|
-
screens: [editScreen("rent", undefined, "billing")],
|
|
464
|
-
};
|
|
465
|
-
let navigated: ScreenTarget | undefined;
|
|
466
|
-
const { getByTestId, queryByText } = renderDetail({
|
|
467
|
-
primarySchema: appSchema,
|
|
468
|
-
features: [appSchema, billingSchema],
|
|
469
|
-
userRoles: [],
|
|
470
|
-
onNavigate: (target) => {
|
|
471
|
-
navigated = target;
|
|
472
|
-
},
|
|
473
|
-
});
|
|
474
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
475
|
-
|
|
476
|
-
// trigger() is async (busy-state bookkeeping around onPress), so the
|
|
477
|
-
// click's state updates land after this tick — act() flushes them.
|
|
478
|
-
await act(async () => {
|
|
479
|
-
fireEvent.click(getByTestId("render-edit-action-edit"));
|
|
480
|
-
});
|
|
481
|
-
expect(navigated).toEqual({ screenId: "rent-edit", entityId: "rent-1" });
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
test("two header actions → both render as buttons, no overflow trigger (fw bedienkonzept A7)", async () => {
|
|
485
|
-
const schema: FeatureSchema = {
|
|
486
|
-
featureName: "app",
|
|
487
|
-
entities: {},
|
|
488
|
-
screens: [
|
|
489
|
-
detailScreen({
|
|
490
|
-
actions: [
|
|
491
|
-
{ kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
|
|
492
|
-
{ kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
|
|
493
|
-
],
|
|
494
|
-
}),
|
|
495
|
-
editScreen("rent"),
|
|
496
|
-
],
|
|
497
|
-
};
|
|
498
|
-
const { getByTestId, queryByTestId, queryByText } = renderDetail({
|
|
499
|
-
primarySchema: schema,
|
|
500
|
-
features: [schema],
|
|
501
|
-
userRoles: [],
|
|
502
|
-
});
|
|
503
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
504
|
-
|
|
505
|
-
expect(getByTestId("render-edit-action-edit").textContent).toBe("actions.edit");
|
|
506
|
-
expect(getByTestId("render-edit-action-duplicate").textContent).toBe("actions.duplicate");
|
|
507
|
-
expect(queryByTestId("kumiko-screen-projection-detail-actions-overflow")).toBeNull();
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
test("three header actions → edit stays a text button, the rest collapse into the overflow menu (fw bedienkonzept A7)", async () => {
|
|
511
|
-
const schema: FeatureSchema = {
|
|
512
|
-
featureName: "app",
|
|
513
|
-
entities: {},
|
|
514
|
-
screens: [
|
|
515
|
-
detailScreen({
|
|
516
|
-
actions: [
|
|
517
|
-
{ kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
|
|
518
|
-
{ kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
|
|
519
|
-
{ kind: "navigate", id: "audit-log", label: "actions.auditLog", screen: "rent-edit" },
|
|
520
|
-
],
|
|
521
|
-
}),
|
|
522
|
-
editScreen("rent"),
|
|
523
|
-
],
|
|
524
|
-
};
|
|
525
|
-
const { getByTestId, queryByTestId, queryByText } = renderDetail({
|
|
526
|
-
primarySchema: schema,
|
|
527
|
-
features: [schema],
|
|
528
|
-
userRoles: [],
|
|
529
|
-
});
|
|
530
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
531
|
-
|
|
532
|
-
expect(getByTestId("render-edit-action-edit").textContent).toBe("actions.edit");
|
|
533
|
-
expect(queryByTestId("render-edit-action-duplicate")).toBeNull();
|
|
534
|
-
expect(queryByTestId("render-edit-action-audit-log")).toBeNull();
|
|
535
|
-
|
|
536
|
-
fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
|
|
537
|
-
expect(
|
|
538
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-duplicate").textContent,
|
|
539
|
-
).toBe("actions.duplicate");
|
|
540
|
-
expect(
|
|
541
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-audit-log").textContent,
|
|
542
|
-
).toBe("actions.auditLog");
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
test("four header actions incl. edit → edit stays a text button, the other three are only reachable via the overflow menu (fw bedienkonzept A7)", async () => {
|
|
546
|
-
const schema: FeatureSchema = {
|
|
547
|
-
featureName: "app",
|
|
548
|
-
entities: {},
|
|
549
|
-
screens: [
|
|
550
|
-
detailScreen({
|
|
551
|
-
actions: [
|
|
552
|
-
{ kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
|
|
553
|
-
{ kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
|
|
554
|
-
{ kind: "navigate", id: "audit-log", label: "actions.auditLog", screen: "rent-edit" },
|
|
555
|
-
{ kind: "navigate", id: "archive", label: "actions.archive", screen: "rent-edit" },
|
|
556
|
-
],
|
|
557
|
-
}),
|
|
558
|
-
editScreen("rent"),
|
|
559
|
-
],
|
|
560
|
-
};
|
|
561
|
-
const { getByTestId, queryByTestId, queryByText } = renderDetail({
|
|
562
|
-
primarySchema: schema,
|
|
563
|
-
features: [schema],
|
|
564
|
-
userRoles: [],
|
|
565
|
-
});
|
|
566
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
567
|
-
|
|
568
|
-
expect(getByTestId("render-edit-action-edit").textContent).toBe("actions.edit");
|
|
569
|
-
expect(queryByTestId("render-edit-action-duplicate")).toBeNull();
|
|
570
|
-
expect(queryByTestId("render-edit-action-audit-log")).toBeNull();
|
|
571
|
-
expect(queryByTestId("render-edit-action-archive")).toBeNull();
|
|
572
|
-
|
|
573
|
-
fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
|
|
574
|
-
expect(
|
|
575
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-duplicate").textContent,
|
|
576
|
-
).toBe("actions.duplicate");
|
|
577
|
-
expect(
|
|
578
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-audit-log").textContent,
|
|
579
|
-
).toBe("actions.auditLog");
|
|
580
|
-
expect(
|
|
581
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-archive").textContent,
|
|
582
|
-
).toBe("actions.archive");
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
test('header action without an id: "edit" entry → the style: "primary" action stays the visible button (fw bedienkonzept A7)', async () => {
|
|
586
|
-
const schema: FeatureSchema = {
|
|
587
|
-
featureName: "app",
|
|
588
|
-
entities: {},
|
|
589
|
-
screens: [
|
|
590
|
-
detailScreen({
|
|
591
|
-
actions: [
|
|
592
|
-
{ kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
|
|
593
|
-
{
|
|
594
|
-
kind: "navigate",
|
|
595
|
-
id: "approve",
|
|
596
|
-
label: "actions.approve",
|
|
597
|
-
screen: "rent-edit",
|
|
598
|
-
style: "primary",
|
|
599
|
-
},
|
|
600
|
-
{ kind: "navigate", id: "audit-log", label: "actions.auditLog", screen: "rent-edit" },
|
|
601
|
-
],
|
|
602
|
-
}),
|
|
603
|
-
],
|
|
604
|
-
};
|
|
605
|
-
const { getByTestId, queryByTestId, queryByText } = renderDetail({
|
|
606
|
-
primarySchema: schema,
|
|
607
|
-
features: [schema],
|
|
608
|
-
userRoles: [],
|
|
609
|
-
});
|
|
610
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
611
|
-
|
|
612
|
-
expect(getByTestId("render-edit-action-approve").textContent).toBe("actions.approve");
|
|
613
|
-
expect(queryByTestId("render-edit-action-duplicate")).toBeNull();
|
|
614
|
-
expect(queryByTestId("render-edit-action-audit-log")).toBeNull();
|
|
615
|
-
|
|
616
|
-
fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
|
|
617
|
-
expect(
|
|
618
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-duplicate").textContent,
|
|
619
|
-
).toBe("actions.duplicate");
|
|
620
|
-
expect(
|
|
621
|
-
getByTestId("kumiko-screen-projection-detail-actions-overflow-item-audit-log").textContent,
|
|
622
|
-
).toBe("actions.auditLog");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
test("a danger overflow-menu action still asks for confirmation before firing (fw bedienkonzept A7)", async () => {
|
|
626
|
-
const schema: FeatureSchema = {
|
|
627
|
-
featureName: "app",
|
|
628
|
-
entities: {},
|
|
629
|
-
screens: [
|
|
630
|
-
detailScreen({
|
|
631
|
-
actions: [
|
|
632
|
-
{ kind: "navigate", id: "edit", label: "actions.edit", screen: "rent-edit" },
|
|
633
|
-
{ kind: "navigate", id: "duplicate", label: "actions.duplicate", screen: "rent-edit" },
|
|
634
|
-
{
|
|
635
|
-
kind: "writeHandler",
|
|
636
|
-
id: "archive",
|
|
637
|
-
label: "actions.archive",
|
|
638
|
-
handler: "app:write:archive",
|
|
639
|
-
style: "danger",
|
|
640
|
-
},
|
|
641
|
-
],
|
|
642
|
-
}),
|
|
643
|
-
editScreen("rent"),
|
|
644
|
-
],
|
|
645
|
-
};
|
|
646
|
-
// `as DialogProps | null` widens the assignment so tsc doesn't narrow
|
|
647
|
-
// the property to the literal `null` type, which turns the later
|
|
648
|
-
// optional-chained reads below into `never` (compiler quirk).
|
|
649
|
-
TestDialog.lastProps = null as DialogProps | null;
|
|
650
|
-
const { getByTestId, queryByTestId, queryByText } = renderDetail({
|
|
651
|
-
primarySchema: schema,
|
|
652
|
-
features: [schema],
|
|
653
|
-
userRoles: [],
|
|
654
|
-
writeErrorMessage: "archive failed: rent is still active",
|
|
655
|
-
});
|
|
656
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
657
|
-
|
|
658
|
-
fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow"));
|
|
659
|
-
fireEvent.click(getByTestId("kumiko-screen-projection-detail-actions-overflow-item-archive"));
|
|
660
|
-
|
|
661
|
-
// Danger action must not fire on the menu click alone — it needs the
|
|
662
|
-
// confirm dialog first, same as a danger row action.
|
|
663
|
-
expect(queryByTestId("render-edit-action-error")).toBeNull();
|
|
664
|
-
expect(TestDialog.lastProps?.open).toBe(true);
|
|
665
|
-
expect(TestDialog.lastProps?.title).toBe("actions.archive");
|
|
666
|
-
|
|
667
|
-
await act(async () => {
|
|
668
|
-
await TestDialog.lastProps?.onConfirm();
|
|
669
|
-
});
|
|
670
|
-
|
|
671
|
-
const errorBanner = await waitFor(() => getByTestId("render-edit-action-error"));
|
|
672
|
-
expect(errorBanner.textContent).toBe("archive failed: rent is still active");
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
test("a failed writeHandler action shows its error in the head region, alongside the action button, NOT in Form's own actions/body regions (fw#2713)", async () => {
|
|
676
|
-
const schema: FeatureSchema = {
|
|
677
|
-
featureName: "app",
|
|
678
|
-
entities: {},
|
|
679
|
-
screens: [
|
|
680
|
-
detailScreen({
|
|
681
|
-
actions: [
|
|
682
|
-
{
|
|
683
|
-
kind: "writeHandler",
|
|
684
|
-
id: "archive",
|
|
685
|
-
label: "actions.archive",
|
|
686
|
-
handler: "app:write:archive",
|
|
687
|
-
},
|
|
688
|
-
],
|
|
689
|
-
}),
|
|
690
|
-
],
|
|
691
|
-
};
|
|
692
|
-
const { getByTestId, queryByText } = renderDetail({
|
|
693
|
-
primarySchema: schema,
|
|
694
|
-
features: [schema],
|
|
695
|
-
userRoles: [],
|
|
696
|
-
writeErrorMessage: "archive failed: rent is still active",
|
|
697
|
-
});
|
|
698
|
-
await waitFor(() => expect(queryByText("Loading…")).toBeNull());
|
|
699
|
-
|
|
700
|
-
await act(async () => {
|
|
701
|
-
fireEvent.click(getByTestId("render-edit-action-archive"));
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
const errorBanner = await waitFor(() => getByTestId("render-edit-action-error"));
|
|
705
|
-
expect(errorBanner.textContent).toBe("archive failed: rent is still active");
|
|
706
|
-
// Header actions (and their failure banner) render in the head Card via
|
|
707
|
-
// `headerRegion` now (fw#2713) — not through Form's own actions/body
|
|
708
|
-
// slots, so both a tab switch and Form's own footer leave them untouched.
|
|
709
|
-
expect(errorBanner.closest('[data-testid="form-header"]')).not.toBeNull();
|
|
710
|
-
expect(errorBanner.closest('[data-testid="form-body"]')).toBeNull();
|
|
711
|
-
expect(errorBanner.closest('[data-testid="form-actions"]')).toBeNull();
|
|
712
|
-
expect(
|
|
713
|
-
getByTestId("render-edit-action-archive").closest('[data-testid="form-header"]'),
|
|
714
|
-
).not.toBeNull();
|
|
715
|
-
expect(
|
|
716
|
-
getByTestId("render-edit-action-archive").closest('[data-testid="form-actions"]'),
|
|
717
|
-
).toBeNull();
|
|
718
|
-
});
|
|
719
|
-
});
|