@happyvertical/smrt-fields 0.40.60 → 0.40.61
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/AGENTS.md +58 -12
- package/README.md +90 -2
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/cache.d.ts +18 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/collections/FieldPolicyCollection.d.ts +95 -0
- package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
- package/dist/field-definitions.d.ts +77 -0
- package/dist/field-definitions.d.ts.map +1 -0
- package/dist/field-policy-resolver.d.ts +46 -0
- package/dist/field-policy-resolver.d.ts.map +1 -0
- package/dist/index.d.ts +9 -532
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +752 -55
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +45 -3
- package/dist/models/FieldPolicy.d.ts +163 -0
- package/dist/models/FieldPolicy.d.ts.map +1 -0
- package/dist/permissions.d.ts +9 -0
- package/dist/permissions.d.ts.map +1 -0
- package/dist/playground.d.ts +2 -0
- package/dist/playground.d.ts.map +1 -0
- package/dist/playground.js +39 -0
- package/dist/playground.js.map +1 -0
- package/dist/settings-catalog.d.ts +73 -0
- package/dist/settings-catalog.d.ts.map +1 -0
- package/dist/smrt-knowledge.json +50 -7
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +223 -0
- package/dist/svelte/__tests__/FieldPolicyGear.test.js +503 -0
- package/dist/svelte/__tests__/FormHelp.test.js +138 -0
- package/dist/svelte/__tests__/ModeUX.test.js +109 -0
- package/dist/svelte/__tests__/ObjectForm.test.js +556 -0
- package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
- package/dist/svelte/__tests__/PolicyField.test.js +373 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte +30 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/AdvancedFieldsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/DomPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte +19 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/FormHelpFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/GeneratedApiGearFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte +11 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/GracefulDegradeFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte +18 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/ModeSwitchFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/NoIdFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte +20 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte +17 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormRegistryFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte +34 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts +11 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte +33 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts +9 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormSourceSnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte +40 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts +8 -0
- package/dist/svelte/__tests__/fixtures/PolicyDataTableFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte +26 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/PrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte +39 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts +17 -0
- package/dist/svelte/__tests__/fixtures/ProviderFieldFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte +15 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts +5 -0
- package/dist/svelte/__tests__/fixtures/RegistryInput.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte +29 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/RevealPrefillFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte +24 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/SettingsCatalogStub.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte +28 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts +13 -0
- package/dist/svelte/__tests__/fixtures/SnippetFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte +14 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts +19 -0
- package/dist/svelte/__tests__/fixtures/ToggleModeButton.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte +25 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts +14 -0
- package/dist/svelte/__tests__/fixtures/TooltipFixture.svelte.d.ts.map +1 -0
- package/dist/svelte/__tests__/settings-catalog.test.js +161 -0
- package/dist/svelte/components/AdvancedFields.svelte +49 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts +17 -0
- package/dist/svelte/components/AdvancedFields.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldInput.svelte +159 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts +5 -0
- package/dist/svelte/components/FieldInput.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte +261 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +26 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte +417 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts +18 -0
- package/dist/svelte/components/FieldPolicyEditor.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte +17 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts +8 -0
- package/dist/svelte/components/FieldPolicyGearButton.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte +135 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +17 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte +89 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts +35 -0
- package/dist/svelte/components/FieldPolicyProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/FormHelp.svelte +157 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts +15 -0
- package/dist/svelte/components/FormHelp.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ModeSwitch.svelte +47 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts +10 -0
- package/dist/svelte/components/ModeSwitch.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectForm.svelte +351 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts +40 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts.map +1 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte +18 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts +11 -0
- package/dist/svelte/components/ObjectFormSourceProvider.svelte.d.ts.map +1 -0
- package/dist/svelte/components/PolicyField.svelte +338 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts +80 -0
- package/dist/svelte/components/PolicyField.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.d.ts +54 -0
- package/dist/svelte/context.svelte.d.ts.map +1 -0
- package/dist/svelte/context.svelte.js +55 -0
- package/dist/svelte/data-table.d.ts +17 -0
- package/dist/svelte/data-table.d.ts.map +1 -0
- package/dist/svelte/data-table.js +19 -0
- package/dist/svelte/field-policy-editor.d.ts +81 -0
- package/dist/svelte/field-policy-editor.d.ts.map +1 -0
- package/dist/svelte/field-policy-editor.js +120 -0
- package/dist/svelte/gear-context.svelte.d.ts +14 -0
- package/dist/svelte/gear-context.svelte.d.ts.map +1 -0
- package/dist/svelte/gear-context.svelte.js +15 -0
- package/dist/svelte/index.d.ts +58 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +38 -0
- package/dist/svelte/input-registry.d.ts +21 -0
- package/dist/svelte/input-registry.d.ts.map +1 -0
- package/dist/svelte/input-registry.js +26 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts +4 -0
- package/dist/svelte/object-form-source-context.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source-context.svelte.js +8 -0
- package/dist/svelte/object-form-source.svelte.d.ts +18 -0
- package/dist/svelte/object-form-source.svelte.d.ts.map +1 -0
- package/dist/svelte/object-form-source.svelte.js +99 -0
- package/dist/svelte/object-form.d.ts +6 -0
- package/dist/svelte/object-form.d.ts.map +1 -0
- package/dist/svelte/object-form.js +38 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte +212 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/FieldPolicyFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte +129 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/ObjectFormPreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts +20 -0
- package/dist/svelte/playground.d.ts.map +1 -0
- package/dist/svelte/playground.js +38 -0
- package/dist/svelte/settings-catalog.d.ts +54 -0
- package/dist/svelte/settings-catalog.d.ts.map +1 -0
- package/dist/svelte/settings-catalog.js +116 -0
- package/dist/svelte/types.d.ts +101 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +4 -0
- package/dist/types.d.ts +276 -203
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +1 -1
- package/package.json +33 -8
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* Component tests for ModeSwitch and AdvancedFields (#2048).
|
|
4
|
+
*/
|
|
5
|
+
import { expectNoA11yViolations, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
6
|
+
import { tick } from 'svelte';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import AdvancedFields from '../components/AdvancedFields.svelte';
|
|
9
|
+
import FieldPolicyProvider from '../components/FieldPolicyProvider.svelte';
|
|
10
|
+
import ModeSwitch from '../components/ModeSwitch.svelte';
|
|
11
|
+
import AdvancedFieldsFixture from './fixtures/AdvancedFieldsFixture.svelte';
|
|
12
|
+
import ModeSwitchFixture from './fixtures/ModeSwitchFixture.svelte';
|
|
13
|
+
function makePolicy() {
|
|
14
|
+
return {
|
|
15
|
+
objectRef: '@test:Widget',
|
|
16
|
+
fields: {
|
|
17
|
+
name: {
|
|
18
|
+
fieldName: 'name',
|
|
19
|
+
hasDefault: false,
|
|
20
|
+
defaultValue: undefined,
|
|
21
|
+
visibility: 'basic',
|
|
22
|
+
help: 'Name',
|
|
23
|
+
label: 'Name',
|
|
24
|
+
order: 1,
|
|
25
|
+
group: null,
|
|
26
|
+
locked: false,
|
|
27
|
+
required: true,
|
|
28
|
+
},
|
|
29
|
+
secretKey: {
|
|
30
|
+
fieldName: 'secretKey',
|
|
31
|
+
hasDefault: false,
|
|
32
|
+
defaultValue: undefined,
|
|
33
|
+
visibility: 'advanced',
|
|
34
|
+
help: 'Secret key',
|
|
35
|
+
label: 'Secret Key',
|
|
36
|
+
order: 2,
|
|
37
|
+
group: null,
|
|
38
|
+
locked: false,
|
|
39
|
+
required: false,
|
|
40
|
+
},
|
|
41
|
+
another: {
|
|
42
|
+
fieldName: 'another',
|
|
43
|
+
hasDefault: false,
|
|
44
|
+
defaultValue: undefined,
|
|
45
|
+
visibility: 'advanced',
|
|
46
|
+
help: 'Another',
|
|
47
|
+
label: 'Another',
|
|
48
|
+
order: 3,
|
|
49
|
+
group: null,
|
|
50
|
+
locked: false,
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
describe('ModeSwitch', () => {
|
|
57
|
+
it('renders basic and advanced options', () => {
|
|
58
|
+
render(ModeSwitchFixture, { props: { policy: makePolicy() } });
|
|
59
|
+
// SegmentedControl renders as a radiogroup with radio options
|
|
60
|
+
expect(screen.getByRole('radiogroup', { name: 'View mode' })).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
it('switching to advanced reveals advanced fields', async () => {
|
|
63
|
+
render(ModeSwitchFixture, { props: { policy: makePolicy() } });
|
|
64
|
+
expect(screen.queryByRole('textbox', { name: /Secret Key/ })).not.toBeInTheDocument();
|
|
65
|
+
// SegmentedControl renders radio buttons — click the Advanced label
|
|
66
|
+
await userEvent.click(screen.getByText('Advanced'));
|
|
67
|
+
await tick();
|
|
68
|
+
expect(screen.getByRole('textbox', { name: /Secret Key/ })).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
it('is axe-clean', async () => {
|
|
71
|
+
const { container } = render(ModeSwitchFixture, {
|
|
72
|
+
props: { policy: makePolicy() },
|
|
73
|
+
});
|
|
74
|
+
await expectNoA11yViolations(container);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('AdvancedFields', () => {
|
|
78
|
+
it('shows disclosure with field count in advanced mode', async () => {
|
|
79
|
+
render(AdvancedFieldsFixture, {
|
|
80
|
+
props: { policy: makePolicy(), mode: 'advanced' },
|
|
81
|
+
});
|
|
82
|
+
await tick();
|
|
83
|
+
expect(screen.getByText(/Advanced · 2 fields/)).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
it('does not render in basic mode', () => {
|
|
86
|
+
render(AdvancedFieldsFixture, {
|
|
87
|
+
props: { policy: makePolicy(), mode: 'basic' },
|
|
88
|
+
});
|
|
89
|
+
expect(screen.queryByText(/Advanced ·/)).not.toBeInTheDocument();
|
|
90
|
+
expect(screen.queryByRole('textbox', { name: /Secret/ })).not.toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
it('uses singular "field" for one advanced field', async () => {
|
|
93
|
+
const policy = makePolicy();
|
|
94
|
+
// Remove one advanced field
|
|
95
|
+
delete policy.fields.another;
|
|
96
|
+
render(AdvancedFieldsFixture, {
|
|
97
|
+
props: { policy, mode: 'advanced' },
|
|
98
|
+
});
|
|
99
|
+
await tick();
|
|
100
|
+
expect(screen.getByText(/Advanced · 1 field$/)).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
it('is axe-clean', async () => {
|
|
103
|
+
const { container } = render(AdvancedFieldsFixture, {
|
|
104
|
+
props: { policy: makePolicy(), mode: 'advanced' },
|
|
105
|
+
});
|
|
106
|
+
await tick();
|
|
107
|
+
await expectNoA11yViolations(container);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
import { field, ObjectRegistry, SmrtObject, smrt, } from '@happyvertical/smrt-core';
|
|
9
|
+
import { expectNoA11yViolations, fireEvent, render, screen, userEvent, } from '@happyvertical/smrt-vitest/svelte';
|
|
10
|
+
import { tick } from 'svelte';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import FieldInput from '../components/FieldInput.svelte';
|
|
13
|
+
import { policyToVisibleColumnIds } from '../data-table.js';
|
|
14
|
+
import { ObjectFormSourceRegistry, } from '../index.js';
|
|
15
|
+
import { FieldInputRegistry } from '../input-registry.js';
|
|
16
|
+
import { resolveObjectFormFields } from '../object-form.js';
|
|
17
|
+
import ObjectFormActionsFixture from './fixtures/ObjectFormActionsFixture.svelte';
|
|
18
|
+
import ObjectFormFixture from './fixtures/ObjectFormFixture.svelte';
|
|
19
|
+
import ObjectFormRegistryFixture from './fixtures/ObjectFormRegistryFixture.svelte';
|
|
20
|
+
import ObjectFormSnippetFixture from './fixtures/ObjectFormSnippetFixture.svelte';
|
|
21
|
+
import ObjectFormSourceFixture from './fixtures/ObjectFormSourceFixture.svelte';
|
|
22
|
+
import ObjectFormSourceSnippetFixture from './fixtures/ObjectFormSourceSnippetFixture.svelte';
|
|
23
|
+
import PolicyDataTableFixture from './fixtures/PolicyDataTableFixture.svelte';
|
|
24
|
+
let AcceptanceProduct = class AcceptanceProduct extends SmrtObject {
|
|
25
|
+
name = '';
|
|
26
|
+
price = 0;
|
|
27
|
+
published = false;
|
|
28
|
+
};
|
|
29
|
+
__decorate([
|
|
30
|
+
field({ required: true })
|
|
31
|
+
], AcceptanceProduct.prototype, "name", void 0);
|
|
32
|
+
__decorate([
|
|
33
|
+
field({ type: 'decimal' })
|
|
34
|
+
], AcceptanceProduct.prototype, "price", void 0);
|
|
35
|
+
__decorate([
|
|
36
|
+
field({ type: 'boolean', default: false })
|
|
37
|
+
], AcceptanceProduct.prototype, "published", void 0);
|
|
38
|
+
AcceptanceProduct = __decorate([
|
|
39
|
+
smrt({
|
|
40
|
+
packageName: '@test/smrt-fields-object-form',
|
|
41
|
+
visibility: 'internal',
|
|
42
|
+
api: { include: ['list', 'get', 'create', 'update'] },
|
|
43
|
+
cli: false,
|
|
44
|
+
mcp: false,
|
|
45
|
+
})
|
|
46
|
+
], AcceptanceProduct);
|
|
47
|
+
let AcceptanceEvent = class AcceptanceEvent extends SmrtObject {
|
|
48
|
+
title = '';
|
|
49
|
+
startsAt = '';
|
|
50
|
+
metadata = {};
|
|
51
|
+
};
|
|
52
|
+
__decorate([
|
|
53
|
+
field({ required: true })
|
|
54
|
+
], AcceptanceEvent.prototype, "title", void 0);
|
|
55
|
+
__decorate([
|
|
56
|
+
field({ type: 'datetime' })
|
|
57
|
+
], AcceptanceEvent.prototype, "startsAt", void 0);
|
|
58
|
+
__decorate([
|
|
59
|
+
field({ type: 'json' })
|
|
60
|
+
], AcceptanceEvent.prototype, "metadata", void 0);
|
|
61
|
+
AcceptanceEvent = __decorate([
|
|
62
|
+
smrt({
|
|
63
|
+
packageName: '@test/smrt-fields-object-form',
|
|
64
|
+
visibility: 'internal',
|
|
65
|
+
api: { include: ['list', 'get', 'create', 'update'] },
|
|
66
|
+
cli: false,
|
|
67
|
+
mcp: false,
|
|
68
|
+
})
|
|
69
|
+
], AcceptanceEvent);
|
|
70
|
+
function objectRef(ctor) {
|
|
71
|
+
const registered = ObjectRegistry.getClassByConstructor(ctor);
|
|
72
|
+
if (!registered?.qualifiedName)
|
|
73
|
+
throw new Error('acceptance fixture did not register');
|
|
74
|
+
return registered.qualifiedName;
|
|
75
|
+
}
|
|
76
|
+
function sourcePolicy(objectRef, definitions, defaults = {}) {
|
|
77
|
+
return {
|
|
78
|
+
objectRef,
|
|
79
|
+
fields: Object.fromEntries(Object.entries(definitions).map(([fieldName, definition], index) => [
|
|
80
|
+
fieldName,
|
|
81
|
+
{
|
|
82
|
+
fieldName,
|
|
83
|
+
hasDefault: fieldName in defaults,
|
|
84
|
+
defaultValue: defaults[fieldName],
|
|
85
|
+
visibility: 'basic',
|
|
86
|
+
help: definition.description ?? null,
|
|
87
|
+
label: fieldName
|
|
88
|
+
.replace(/([A-Z])/g, ' $1')
|
|
89
|
+
.replace(/^./, (letter) => letter.toUpperCase()),
|
|
90
|
+
order: index,
|
|
91
|
+
group: null,
|
|
92
|
+
locked: false,
|
|
93
|
+
required: definition.required === true,
|
|
94
|
+
},
|
|
95
|
+
])),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const fields = {
|
|
99
|
+
name: { type: 'text', required: true },
|
|
100
|
+
price: { type: 'decimal', ui: { group: 'Pricing', order: 2 } },
|
|
101
|
+
published: { type: 'boolean' },
|
|
102
|
+
metadata: { type: 'json' },
|
|
103
|
+
id: { type: 'text' },
|
|
104
|
+
};
|
|
105
|
+
const policy = {
|
|
106
|
+
objectRef: '@test:Product',
|
|
107
|
+
fields: {
|
|
108
|
+
name: {
|
|
109
|
+
fieldName: 'name',
|
|
110
|
+
hasDefault: false,
|
|
111
|
+
defaultValue: undefined,
|
|
112
|
+
visibility: 'basic',
|
|
113
|
+
help: 'Public display name',
|
|
114
|
+
label: 'Name',
|
|
115
|
+
order: 1,
|
|
116
|
+
group: null,
|
|
117
|
+
locked: false,
|
|
118
|
+
required: true,
|
|
119
|
+
},
|
|
120
|
+
price: {
|
|
121
|
+
fieldName: 'price',
|
|
122
|
+
hasDefault: false,
|
|
123
|
+
defaultValue: undefined,
|
|
124
|
+
visibility: 'advanced',
|
|
125
|
+
help: null,
|
|
126
|
+
label: 'Price',
|
|
127
|
+
order: 2,
|
|
128
|
+
group: 'Pricing',
|
|
129
|
+
locked: false,
|
|
130
|
+
required: false,
|
|
131
|
+
},
|
|
132
|
+
published: {
|
|
133
|
+
fieldName: 'published',
|
|
134
|
+
hasDefault: true,
|
|
135
|
+
defaultValue: false,
|
|
136
|
+
visibility: 'hidden',
|
|
137
|
+
help: null,
|
|
138
|
+
label: 'Published',
|
|
139
|
+
order: 3,
|
|
140
|
+
group: null,
|
|
141
|
+
locked: false,
|
|
142
|
+
required: false,
|
|
143
|
+
},
|
|
144
|
+
metadata: {
|
|
145
|
+
fieldName: 'metadata',
|
|
146
|
+
hasDefault: false,
|
|
147
|
+
defaultValue: undefined,
|
|
148
|
+
visibility: 'basic',
|
|
149
|
+
help: null,
|
|
150
|
+
label: 'Metadata',
|
|
151
|
+
order: 4,
|
|
152
|
+
group: null,
|
|
153
|
+
locked: false,
|
|
154
|
+
required: false,
|
|
155
|
+
},
|
|
156
|
+
serverOnly: {
|
|
157
|
+
fieldName: 'serverOnly',
|
|
158
|
+
hasDefault: false,
|
|
159
|
+
defaultValue: undefined,
|
|
160
|
+
visibility: 'basic',
|
|
161
|
+
help: null,
|
|
162
|
+
label: 'Server only',
|
|
163
|
+
order: 5,
|
|
164
|
+
group: null,
|
|
165
|
+
locked: false,
|
|
166
|
+
required: false,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
describe('ObjectForm selection and adapters', () => {
|
|
171
|
+
it('uses only the safe web definition × policy intersection, ordered by policy', () => {
|
|
172
|
+
expect(resolveObjectFormFields(fields, policy).map((field) => field.name)).toEqual(['name', 'price', 'published', 'metadata']);
|
|
173
|
+
});
|
|
174
|
+
it('keeps unmapped/action columns while filtering policy-hidden and static hidden columns', () => {
|
|
175
|
+
expect([
|
|
176
|
+
...policyToVisibleColumnIds(policy, [
|
|
177
|
+
{ id: 'name' },
|
|
178
|
+
{ id: 'published' },
|
|
179
|
+
{ id: 'computed' },
|
|
180
|
+
{ id: 'actions' },
|
|
181
|
+
{ id: 'metadata', hidden: true },
|
|
182
|
+
]),
|
|
183
|
+
]).toEqual(['name', 'computed', 'actions']);
|
|
184
|
+
});
|
|
185
|
+
it('prefers field overrides to type overrides and keeps registries isolated per app', () => {
|
|
186
|
+
const registry = new FieldInputRegistry();
|
|
187
|
+
const text = (() => ({}));
|
|
188
|
+
const field = (() => ({}));
|
|
189
|
+
registry.registerType('text', text);
|
|
190
|
+
registry.registerField('@test:Product', 'name', field);
|
|
191
|
+
expect(registry.resolve('@test:Product', 'name', 'text')).toBe(field);
|
|
192
|
+
expect(registry.resolve('@test:Product', 'other', 'text')).toBe(text);
|
|
193
|
+
expect(new FieldInputRegistry().resolve('@test:Product', 'name', 'text')).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('ObjectForm component', () => {
|
|
197
|
+
it('renders basic fields, omits hidden and non-formable fields, and reveals advanced groups', async () => {
|
|
198
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
199
|
+
expect(screen.getByRole('textbox', { name: 'Name' })).toBeInTheDocument();
|
|
200
|
+
expect(screen.getByRole('textbox', { name: 'Metadata' })).toBeInTheDocument();
|
|
201
|
+
expect(screen.queryByRole('checkbox', { name: 'Published' })).not.toBeInTheDocument();
|
|
202
|
+
expect(screen.queryByRole('spinbutton', { name: 'Price' })).not.toBeInTheDocument();
|
|
203
|
+
expect(screen.queryByRole('group', { name: 'Pricing' })).not.toBeInTheDocument();
|
|
204
|
+
expect(screen.queryByText('Server only')).not.toBeInTheDocument();
|
|
205
|
+
await userEvent.click(screen.getByRole('radio', { name: /Advanced/ }));
|
|
206
|
+
expect(screen.getByRole('group', { name: 'Pricing' })).toBeInTheDocument();
|
|
207
|
+
expect(screen.getByRole('spinbutton', { name: 'Price' })).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
it('updates the bound create record through built-in text and decimal adapters', async () => {
|
|
210
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
211
|
+
const name = screen.getByRole('textbox', { name: 'Name' });
|
|
212
|
+
await userEvent.type(name, 'Deluxe');
|
|
213
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Deluxe');
|
|
214
|
+
await userEvent.click(screen.getByRole('radio', { name: /Advanced/ }));
|
|
215
|
+
await userEvent.type(screen.getByRole('spinbutton', { name: 'Price' }), '19.5');
|
|
216
|
+
expect(screen.getByTestId('record')).toHaveTextContent('19.5');
|
|
217
|
+
});
|
|
218
|
+
it('seeds policy defaults into create records, including hidden fields', async () => {
|
|
219
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
220
|
+
await tick();
|
|
221
|
+
expect(screen.getByTestId('record')).toHaveTextContent('"published":false');
|
|
222
|
+
});
|
|
223
|
+
it('reapplies defaults after a host resets the bound record for a second create session', async () => {
|
|
224
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
225
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'First');
|
|
226
|
+
await userEvent.click(screen.getByRole('button', { name: 'Start another record' }));
|
|
227
|
+
await tick();
|
|
228
|
+
expect(screen.getByTestId('record')).toHaveTextContent('{"published":false}');
|
|
229
|
+
});
|
|
230
|
+
it('uses an app registry field override ahead of the built-in type adapter', () => {
|
|
231
|
+
render(ObjectFormRegistryFixture, { props: { fields, policy } });
|
|
232
|
+
expect(screen.getByRole('textbox', { name: 'Registry name' })).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
it('allows a per-field snippet renderer to replace the resolved input', async () => {
|
|
235
|
+
render(ObjectFormSnippetFixture, { props: { fields, policy } });
|
|
236
|
+
const custom = screen.getByRole('textbox', { name: 'Custom product name' });
|
|
237
|
+
await userEvent.type(custom, 'Custom');
|
|
238
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Custom');
|
|
239
|
+
expect(screen.queryByRole('textbox', { name: 'Name' })).not.toBeInTheDocument();
|
|
240
|
+
});
|
|
241
|
+
it('renders host actions inside the form and keeps submit validation and callbacks intact', async () => {
|
|
242
|
+
const onsubmit = vi.fn();
|
|
243
|
+
render(ObjectFormActionsFixture, { props: { fields, policy, onsubmit } });
|
|
244
|
+
const submit = screen.getByRole('button', { name: 'Save product' });
|
|
245
|
+
expect(submit).toHaveAttribute('type', 'submit');
|
|
246
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'Deluxe');
|
|
247
|
+
await userEvent.click(submit);
|
|
248
|
+
expect(onsubmit).toHaveBeenCalledTimes(1);
|
|
249
|
+
const metadata = screen.getByRole('textbox', {
|
|
250
|
+
name: 'Metadata',
|
|
251
|
+
});
|
|
252
|
+
metadata.value = '{not json}';
|
|
253
|
+
await fireEvent.input(metadata);
|
|
254
|
+
await userEvent.click(submit);
|
|
255
|
+
expect(onsubmit).toHaveBeenCalledTimes(1);
|
|
256
|
+
});
|
|
257
|
+
it('is axe-clean for its generated basic form', async () => {
|
|
258
|
+
const { container } = render(ObjectFormFixture, {
|
|
259
|
+
props: { fields, policy },
|
|
260
|
+
});
|
|
261
|
+
await expectNoA11yViolations(container);
|
|
262
|
+
});
|
|
263
|
+
it('gives separately mounted forms distinct control and help ids', () => {
|
|
264
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
265
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
266
|
+
const names = screen.getAllByRole('textbox', { name: 'Name' });
|
|
267
|
+
expect(names[0].id).not.toBe(names[1].id);
|
|
268
|
+
expect(names[0].getAttribute('aria-describedby')).not.toBe(names[1].getAttribute('aria-describedby'));
|
|
269
|
+
});
|
|
270
|
+
it('surfaces invalid JSON without emitting the stale parsed value', async () => {
|
|
271
|
+
const values = [];
|
|
272
|
+
const validity = [];
|
|
273
|
+
render(FieldInput, {
|
|
274
|
+
props: {
|
|
275
|
+
id: 'payload',
|
|
276
|
+
name: 'payload',
|
|
277
|
+
field: {
|
|
278
|
+
name: 'payload',
|
|
279
|
+
definition: { type: 'json' },
|
|
280
|
+
group: null,
|
|
281
|
+
order: null,
|
|
282
|
+
},
|
|
283
|
+
value: { ok: true },
|
|
284
|
+
required: false,
|
|
285
|
+
disabled: false,
|
|
286
|
+
onvaluechange: (value) => values.push(value),
|
|
287
|
+
onvaliditychange: (valid) => validity.push(valid),
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
const input = screen.getByRole('textbox');
|
|
291
|
+
input.value = '{bad';
|
|
292
|
+
await fireEvent.input(input);
|
|
293
|
+
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
294
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/valid JSON/i);
|
|
295
|
+
expect(values).toEqual([]);
|
|
296
|
+
expect(validity.at(-1)).toBe(false);
|
|
297
|
+
});
|
|
298
|
+
it('treats an empty optional JSON input as valid and emits undefined', async () => {
|
|
299
|
+
const values = [];
|
|
300
|
+
const validity = [];
|
|
301
|
+
render(FieldInput, {
|
|
302
|
+
props: {
|
|
303
|
+
id: 'payload-empty',
|
|
304
|
+
name: 'payload',
|
|
305
|
+
field: {
|
|
306
|
+
name: 'payload',
|
|
307
|
+
definition: { type: 'json' },
|
|
308
|
+
group: null,
|
|
309
|
+
order: null,
|
|
310
|
+
},
|
|
311
|
+
value: { ok: true },
|
|
312
|
+
required: false,
|
|
313
|
+
disabled: false,
|
|
314
|
+
onvaluechange: (value) => values.push(value),
|
|
315
|
+
onvaliditychange: (valid) => validity.push(valid),
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const input = screen.getByRole('textbox');
|
|
319
|
+
input.value = '';
|
|
320
|
+
await fireEvent.input(input);
|
|
321
|
+
expect(input).not.toHaveAttribute('aria-invalid');
|
|
322
|
+
expect(values.at(-1)).toBeUndefined();
|
|
323
|
+
expect(validity.at(-1)).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
it('does not emit NaN while a numeric control holds an invalid intermediate value', async () => {
|
|
326
|
+
const values = [];
|
|
327
|
+
render(FieldInput, {
|
|
328
|
+
props: {
|
|
329
|
+
id: 'price',
|
|
330
|
+
name: 'price',
|
|
331
|
+
field: {
|
|
332
|
+
name: 'price',
|
|
333
|
+
definition: { type: 'decimal' },
|
|
334
|
+
group: null,
|
|
335
|
+
order: null,
|
|
336
|
+
},
|
|
337
|
+
value: 12.5,
|
|
338
|
+
required: false,
|
|
339
|
+
disabled: false,
|
|
340
|
+
onvaluechange: (value) => values.push(value),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const input = screen.getByRole('spinbutton');
|
|
344
|
+
let raw = '1e';
|
|
345
|
+
Object.defineProperty(input, 'value', {
|
|
346
|
+
configurable: true,
|
|
347
|
+
get: () => raw,
|
|
348
|
+
set: (next) => {
|
|
349
|
+
raw = next;
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
await fireEvent.input(input);
|
|
353
|
+
expect(values).toEqual([]);
|
|
354
|
+
});
|
|
355
|
+
it('does not treat a browser badInput empty number value as a deliberate clear', async () => {
|
|
356
|
+
const values = [];
|
|
357
|
+
render(FieldInput, {
|
|
358
|
+
props: {
|
|
359
|
+
id: 'quantity',
|
|
360
|
+
name: 'quantity',
|
|
361
|
+
field: {
|
|
362
|
+
name: 'quantity',
|
|
363
|
+
definition: { type: 'integer' },
|
|
364
|
+
group: null,
|
|
365
|
+
order: null,
|
|
366
|
+
},
|
|
367
|
+
value: 3,
|
|
368
|
+
required: false,
|
|
369
|
+
disabled: false,
|
|
370
|
+
onvaluechange: (value) => values.push(value),
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
const input = screen.getByRole('spinbutton');
|
|
374
|
+
let raw = '';
|
|
375
|
+
Object.defineProperty(input, 'value', {
|
|
376
|
+
configurable: true,
|
|
377
|
+
get: () => raw,
|
|
378
|
+
set: (next) => {
|
|
379
|
+
raw = next;
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
Object.defineProperty(input, 'validity', {
|
|
383
|
+
configurable: true,
|
|
384
|
+
value: { badInput: true },
|
|
385
|
+
});
|
|
386
|
+
await fireEvent.input(input);
|
|
387
|
+
expect(values).toEqual([]);
|
|
388
|
+
});
|
|
389
|
+
it('converts ISO datetimes for datetime-local editing and emits an ISO DTO', async () => {
|
|
390
|
+
const values = [];
|
|
391
|
+
render(FieldInput, {
|
|
392
|
+
props: {
|
|
393
|
+
id: 'starts-at',
|
|
394
|
+
name: 'startsAt',
|
|
395
|
+
field: {
|
|
396
|
+
name: 'startsAt',
|
|
397
|
+
definition: { type: 'datetime' },
|
|
398
|
+
group: null,
|
|
399
|
+
order: null,
|
|
400
|
+
},
|
|
401
|
+
value: '2025-01-02T03:04:00.000Z',
|
|
402
|
+
required: false,
|
|
403
|
+
disabled: false,
|
|
404
|
+
onvaluechange: (value) => values.push(value),
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
const input = document.querySelector('#starts-at');
|
|
408
|
+
if (!input)
|
|
409
|
+
throw new Error('datetime input missing');
|
|
410
|
+
input.value = '2025-01-03T04:05';
|
|
411
|
+
await fireEvent.input(input);
|
|
412
|
+
expect(values.at(-1)).toMatch(/Z$/);
|
|
413
|
+
});
|
|
414
|
+
it('uses the resolved display label for generated boolean toggle accessibility', () => {
|
|
415
|
+
render(FieldInput, {
|
|
416
|
+
props: {
|
|
417
|
+
id: 'is-published',
|
|
418
|
+
name: 'isPublished',
|
|
419
|
+
label: 'Published publicly',
|
|
420
|
+
field: {
|
|
421
|
+
name: 'isPublished',
|
|
422
|
+
definition: { type: 'boolean' },
|
|
423
|
+
group: null,
|
|
424
|
+
order: null,
|
|
425
|
+
},
|
|
426
|
+
value: false,
|
|
427
|
+
required: false,
|
|
428
|
+
disabled: false,
|
|
429
|
+
onvaluechange: () => undefined,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
expect(screen.getByRole('checkbox', { name: 'Published publicly' })).toBeInTheDocument();
|
|
433
|
+
});
|
|
434
|
+
it('humanizes a generated boolean name when policy has no display label', () => {
|
|
435
|
+
const booleanFields = { isPublished: { type: 'boolean' } };
|
|
436
|
+
const booleanPolicy = {
|
|
437
|
+
objectRef: '@test:Boolean',
|
|
438
|
+
fields: {
|
|
439
|
+
isPublished: {
|
|
440
|
+
fieldName: 'isPublished',
|
|
441
|
+
hasDefault: false,
|
|
442
|
+
defaultValue: undefined,
|
|
443
|
+
visibility: 'basic',
|
|
444
|
+
help: null,
|
|
445
|
+
label: null,
|
|
446
|
+
order: 1,
|
|
447
|
+
group: null,
|
|
448
|
+
locked: false,
|
|
449
|
+
required: false,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
};
|
|
453
|
+
render(ObjectFormFixture, {
|
|
454
|
+
props: { fields: booleanFields, policy: booleanPolicy },
|
|
455
|
+
});
|
|
456
|
+
expect(screen.getByRole('checkbox', { name: 'Is Published' })).toBeInTheDocument();
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
describe('ObjectForm generated source acceptance', () => {
|
|
460
|
+
const productRef = objectRef(AcceptanceProduct);
|
|
461
|
+
const eventRef = objectRef(AcceptanceEvent);
|
|
462
|
+
const productFields = {
|
|
463
|
+
name: { type: 'text', required: true },
|
|
464
|
+
price: { type: 'decimal' },
|
|
465
|
+
published: { type: 'boolean' },
|
|
466
|
+
};
|
|
467
|
+
const eventFields = {
|
|
468
|
+
title: { type: 'text', required: true },
|
|
469
|
+
startsAt: { type: 'datetime' },
|
|
470
|
+
metadata: { type: 'json' },
|
|
471
|
+
};
|
|
472
|
+
const definitions = [
|
|
473
|
+
{ objectRef: productRef, fields: productFields },
|
|
474
|
+
{ objectRef: eventRef, fields: eventFields },
|
|
475
|
+
];
|
|
476
|
+
const policies = {
|
|
477
|
+
[productRef]: sourcePolicy(productRef, productFields, { published: false }),
|
|
478
|
+
[eventRef]: sourcePolicy(eventRef, eventFields),
|
|
479
|
+
};
|
|
480
|
+
function createSource(response = { policies }) {
|
|
481
|
+
const source = new ObjectFormSourceRegistry({
|
|
482
|
+
resolveBatch: vi.fn(async () => response),
|
|
483
|
+
});
|
|
484
|
+
for (const definition of definitions)
|
|
485
|
+
source.register(definition);
|
|
486
|
+
return source;
|
|
487
|
+
}
|
|
488
|
+
it('drives two REST-exposed decorated objects through one source, including create and edit semantics', async () => {
|
|
489
|
+
const productRegistration = ObjectRegistry.getClassByConstructor(AcceptanceProduct);
|
|
490
|
+
const eventRegistration = ObjectRegistry.getClassByConstructor(AcceptanceEvent);
|
|
491
|
+
expect(productRegistration?.config.api).toEqual(expect.objectContaining({
|
|
492
|
+
include: expect.arrayContaining(['create', 'update']),
|
|
493
|
+
}));
|
|
494
|
+
expect(eventRegistration?.config.api).toEqual(expect.objectContaining({
|
|
495
|
+
include: expect.arrayContaining(['create', 'update']),
|
|
496
|
+
}));
|
|
497
|
+
expect(ObjectRegistry.getFields(productRef).get('price')?.type).toBe('decimal');
|
|
498
|
+
expect(ObjectRegistry.getFields(eventRef).get('metadata')?.type).toBe('json');
|
|
499
|
+
const createView = render(ObjectFormSourceSnippetFixture, {
|
|
500
|
+
props: { source: createSource(), objectRef: productRef },
|
|
501
|
+
});
|
|
502
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Custom generated name' })).toBeInTheDocument());
|
|
503
|
+
expect(screen.getByTestId('record')).toHaveTextContent('"published":false');
|
|
504
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Custom generated name' }), 'deluxe');
|
|
505
|
+
expect(screen.getByTestId('record')).toHaveTextContent('"DELUXE"');
|
|
506
|
+
await expectNoA11yViolations(createView.container);
|
|
507
|
+
createView.unmount();
|
|
508
|
+
render(ObjectFormSourceFixture, {
|
|
509
|
+
props: {
|
|
510
|
+
source: createSource(),
|
|
511
|
+
objectRef: eventRef,
|
|
512
|
+
isNewRecord: false,
|
|
513
|
+
initialValue: {
|
|
514
|
+
title: 'Existing event',
|
|
515
|
+
startsAt: '2026-08-08T10:00',
|
|
516
|
+
metadata: { venue: 'Hall A' },
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Title' })).toHaveValue('Existing event'));
|
|
521
|
+
expect(screen.getByRole('textbox', { name: 'Metadata' })
|
|
522
|
+
.value).toContain('Hall A');
|
|
523
|
+
await userEvent.clear(screen.getByRole('textbox', { name: 'Title' }));
|
|
524
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Title' }), 'Updated event');
|
|
525
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Updated event');
|
|
526
|
+
});
|
|
527
|
+
it('fails closed with an accessible error for malformed generated action responses', async () => {
|
|
528
|
+
render(ObjectFormSourceFixture, {
|
|
529
|
+
props: {
|
|
530
|
+
source: createSource({
|
|
531
|
+
policies: { [productRef]: { objectRef: eventRef } },
|
|
532
|
+
}),
|
|
533
|
+
objectRef: productRef,
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
await vi.waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent(/Invalid ObjectForm source response/));
|
|
537
|
+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
538
|
+
});
|
|
539
|
+
it('fails closed with an accessible error for an unregistered objectRef', async () => {
|
|
540
|
+
render(ObjectFormSourceFixture, {
|
|
541
|
+
props: { source: createSource(), objectRef: '@test/missing:Missing' },
|
|
542
|
+
});
|
|
543
|
+
await vi.waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent(/has no definition/));
|
|
544
|
+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
describe('DataTable policy consumer acceptance', () => {
|
|
548
|
+
it('uses the real DataTable visible-column API without reviving static-hidden columns', () => {
|
|
549
|
+
render(PolicyDataTableFixture, { props: { policy } });
|
|
550
|
+
expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
|
|
551
|
+
expect(screen.queryByRole('columnheader', { name: 'Published' })).not.toBeInTheDocument();
|
|
552
|
+
expect(screen.queryByRole('columnheader', { name: 'Metadata' })).not.toBeInTheDocument();
|
|
553
|
+
expect(screen.getByRole('columnheader', { name: 'Computed' })).toBeInTheDocument();
|
|
554
|
+
expect(screen.getByRole('columnheader', { name: 'Actions' })).toBeInTheDocument();
|
|
555
|
+
});
|
|
556
|
+
});
|