@happyvertical/smrt-fields 0.40.60 → 0.40.62
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 +143 -42
- 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/chunks/FieldPolicyCollection--RxukfCX.js +1218 -0
- package/dist/chunks/FieldPolicyCollection--RxukfCX.js.map +1 -0
- package/dist/collections/FieldPolicyCollection.d.ts +95 -0
- package/dist/collections/FieldPolicyCollection.d.ts.map +1 -0
- package/dist/collections/FieldPolicySuggestionCollection.d.ts +160 -0
- package/dist/collections/FieldPolicySuggestionCollection.d.ts.map +1 -0
- package/dist/collections/FieldUsageCounterCollection.d.ts +143 -0
- package/dist/collections/FieldUsageCounterCollection.d.ts.map +1 -0
- package/dist/deterministic-id.d.ts +18 -0
- package/dist/deterministic-id.d.ts.map +1 -0
- package/dist/field-definitions.d.ts +85 -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 +15 -532
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1813 -628
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1487 -191
- package/dist/models/FieldPolicy.d.ts +163 -0
- package/dist/models/FieldPolicy.d.ts.map +1 -0
- package/dist/models/FieldPolicySuggestion.d.ts +132 -0
- package/dist/models/FieldPolicySuggestion.d.ts.map +1 -0
- package/dist/models/FieldUsageCounter.d.ts +178 -0
- package/dist/models/FieldUsageCounter.d.ts.map +1 -0
- package/dist/models/FieldUsageReportReceipt.d.ts +25 -0
- package/dist/models/FieldUsageReportReceipt.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 +449 -9
- package/dist/svelte/__tests__/FieldPolicyControlPanel.test.js +277 -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 +687 -0
- package/dist/svelte/__tests__/ObjectFormGeneratedApi.integration.test.js +436 -0
- package/dist/svelte/__tests__/PolicyField.test.js +373 -0
- package/dist/svelte/__tests__/UsageLearning.test.js +140 -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 +37 -0
- package/dist/svelte/__tests__/fixtures/ObjectFormActionsFixture.svelte.d.ts +15 -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/SuggestionGearFixture.svelte +23 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.svelte.d.ts +10 -0
- package/dist/svelte/__tests__/fixtures/SuggestionGearFixture.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 +288 -0
- package/dist/svelte/components/FieldPolicyControlPanel.svelte.d.ts +29 -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 +24 -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 +169 -0
- package/dist/svelte/components/FieldPolicyGearProvider.svelte.d.ts +20 -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/FieldPolicySuggestionQueue.svelte +135 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.svelte.d.ts +11 -0
- package/dist/svelte/components/FieldPolicySuggestionQueue.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 +440 -0
- package/dist/svelte/components/ObjectForm.svelte.d.ts +50 -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 +16 -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 +63 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +41 -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/suggestions.d.ts +38 -0
- package/dist/svelte/suggestions.d.ts.map +1 -0
- package/dist/svelte/suggestions.js +68 -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/svelte/usage-capture.d.ts +63 -0
- package/dist/svelte/usage-capture.d.ts.map +1 -0
- package/dist/svelte/usage-capture.js +61 -0
- package/dist/types.d.ts +316 -203
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/dist/usage-learning.d.ts +201 -0
- package/dist/usage-learning.d.ts.map +1 -0
- package/dist/usage-schedules.d.ts +110 -0
- package/dist/usage-schedules.d.ts.map +1 -0
- package/dist/users-module.d.ts +30 -0
- package/dist/users-module.d.ts.map +1 -0
- package/package.json +33 -8
|
@@ -0,0 +1,687 @@
|
|
|
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('reports only after a synchronous host persistence acknowledgement', async () => {
|
|
258
|
+
const onsubmit = vi.fn(() => true);
|
|
259
|
+
const reportUsage = vi.fn(() => {
|
|
260
|
+
throw new Error('metrics unavailable');
|
|
261
|
+
});
|
|
262
|
+
render(ObjectFormActionsFixture, {
|
|
263
|
+
props: { fields, policy, onsubmit, usageReporter: { reportUsage } },
|
|
264
|
+
});
|
|
265
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'Deluxe');
|
|
266
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save product' }));
|
|
267
|
+
expect(onsubmit).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(reportUsage).toHaveBeenCalledWith({
|
|
269
|
+
entries: [{ objectRef: '@test:Product', fieldName: 'name' }],
|
|
270
|
+
});
|
|
271
|
+
const metadata = screen.getByRole('textbox', {
|
|
272
|
+
name: 'Metadata',
|
|
273
|
+
});
|
|
274
|
+
metadata.value = '{not json}';
|
|
275
|
+
await fireEvent.input(metadata);
|
|
276
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save product' }));
|
|
277
|
+
expect(reportUsage).toHaveBeenCalledTimes(1);
|
|
278
|
+
});
|
|
279
|
+
it('reports after an asynchronous host persistence acknowledgement', async () => {
|
|
280
|
+
const onsubmit = vi.fn(async () => true);
|
|
281
|
+
const reportUsage = vi.fn();
|
|
282
|
+
render(ObjectFormActionsFixture, {
|
|
283
|
+
props: { fields, policy, onsubmit, usageReporter: { reportUsage } },
|
|
284
|
+
});
|
|
285
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'Deluxe');
|
|
286
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save product' }));
|
|
287
|
+
await vi.waitFor(() => expect(reportUsage).toHaveBeenCalledTimes(1));
|
|
288
|
+
});
|
|
289
|
+
it('reports the submitted values when a successful create clears its bound record', async () => {
|
|
290
|
+
const reportUsage = vi.fn();
|
|
291
|
+
render(ObjectFormActionsFixture, {
|
|
292
|
+
props: {
|
|
293
|
+
fields,
|
|
294
|
+
policy,
|
|
295
|
+
onsubmit: async () => true,
|
|
296
|
+
usageReporter: { reportUsage },
|
|
297
|
+
resetAfterSuccessfulSubmit: true,
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'Deluxe');
|
|
301
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save product' }));
|
|
302
|
+
await vi.waitFor(() => expect(reportUsage).toHaveBeenCalledWith({
|
|
303
|
+
entries: [{ objectRef: '@test:Product', fieldName: 'name' }],
|
|
304
|
+
}));
|
|
305
|
+
});
|
|
306
|
+
it('keeps named void and Promise<void> persistence handlers compatible without treating either as success', async () => {
|
|
307
|
+
function legacyVoidHandler(_event) { }
|
|
308
|
+
async function legacyAsyncVoidHandler(_event) { }
|
|
309
|
+
const handlers = [
|
|
310
|
+
legacyVoidHandler,
|
|
311
|
+
legacyAsyncVoidHandler,
|
|
312
|
+
];
|
|
313
|
+
for (const onsubmit of handlers) {
|
|
314
|
+
const reportUsage = vi.fn();
|
|
315
|
+
const view = render(ObjectFormActionsFixture, {
|
|
316
|
+
props: { fields, policy, onsubmit, usageReporter: { reportUsage } },
|
|
317
|
+
});
|
|
318
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'Deluxe');
|
|
319
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save product' }));
|
|
320
|
+
await Promise.resolve();
|
|
321
|
+
expect(reportUsage).not.toHaveBeenCalled();
|
|
322
|
+
view.unmount();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
it('marks a count-only text default as matched after persistence', async () => {
|
|
326
|
+
const policyWithTextDefault = {
|
|
327
|
+
...policy,
|
|
328
|
+
fields: {
|
|
329
|
+
...policy.fields,
|
|
330
|
+
name: {
|
|
331
|
+
...policy.fields.name,
|
|
332
|
+
hasDefault: true,
|
|
333
|
+
defaultValue: 'Default product name',
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
const reportUsage = vi.fn();
|
|
338
|
+
render(ObjectFormActionsFixture, {
|
|
339
|
+
props: {
|
|
340
|
+
fields,
|
|
341
|
+
policy: policyWithTextDefault,
|
|
342
|
+
onsubmit: () => true,
|
|
343
|
+
usageReporter: { reportUsage },
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
await tick();
|
|
347
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save product' }));
|
|
348
|
+
expect(reportUsage).toHaveBeenCalledWith({
|
|
349
|
+
entries: [
|
|
350
|
+
{
|
|
351
|
+
objectRef: '@test:Product',
|
|
352
|
+
fieldName: 'name',
|
|
353
|
+
matchedDefault: true,
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
it('does not report when the host cancels, omits success, rejects, or local validation fails', async () => {
|
|
359
|
+
let attempt = 0;
|
|
360
|
+
const onsubmit = vi.fn(async () => {
|
|
361
|
+
attempt++;
|
|
362
|
+
if (attempt === 1)
|
|
363
|
+
return undefined;
|
|
364
|
+
if (attempt === 2)
|
|
365
|
+
return false;
|
|
366
|
+
throw new Error('persistence failed');
|
|
367
|
+
});
|
|
368
|
+
const reportUsage = vi.fn();
|
|
369
|
+
render(ObjectFormActionsFixture, {
|
|
370
|
+
props: { fields, policy, onsubmit, usageReporter: { reportUsage } },
|
|
371
|
+
});
|
|
372
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Name' }), 'Deluxe');
|
|
373
|
+
const submit = screen.getByRole('button', { name: 'Save product' });
|
|
374
|
+
await userEvent.click(submit);
|
|
375
|
+
await userEvent.click(submit);
|
|
376
|
+
await userEvent.click(submit);
|
|
377
|
+
await vi.waitFor(() => expect(onsubmit).toHaveBeenCalledTimes(3));
|
|
378
|
+
expect(reportUsage).not.toHaveBeenCalled();
|
|
379
|
+
const metadata = screen.getByRole('textbox', {
|
|
380
|
+
name: 'Metadata',
|
|
381
|
+
});
|
|
382
|
+
metadata.value = '{not json}';
|
|
383
|
+
await fireEvent.input(metadata);
|
|
384
|
+
await userEvent.click(submit);
|
|
385
|
+
expect(onsubmit).toHaveBeenCalledTimes(3);
|
|
386
|
+
expect(reportUsage).not.toHaveBeenCalled();
|
|
387
|
+
});
|
|
388
|
+
it('is axe-clean for its generated basic form', async () => {
|
|
389
|
+
const { container } = render(ObjectFormFixture, {
|
|
390
|
+
props: { fields, policy },
|
|
391
|
+
});
|
|
392
|
+
await expectNoA11yViolations(container);
|
|
393
|
+
});
|
|
394
|
+
it('gives separately mounted forms distinct control and help ids', () => {
|
|
395
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
396
|
+
render(ObjectFormFixture, { props: { fields, policy } });
|
|
397
|
+
const names = screen.getAllByRole('textbox', { name: 'Name' });
|
|
398
|
+
expect(names[0].id).not.toBe(names[1].id);
|
|
399
|
+
expect(names[0].getAttribute('aria-describedby')).not.toBe(names[1].getAttribute('aria-describedby'));
|
|
400
|
+
});
|
|
401
|
+
it('surfaces invalid JSON without emitting the stale parsed value', async () => {
|
|
402
|
+
const values = [];
|
|
403
|
+
const validity = [];
|
|
404
|
+
render(FieldInput, {
|
|
405
|
+
props: {
|
|
406
|
+
id: 'payload',
|
|
407
|
+
name: 'payload',
|
|
408
|
+
field: {
|
|
409
|
+
name: 'payload',
|
|
410
|
+
definition: { type: 'json' },
|
|
411
|
+
group: null,
|
|
412
|
+
order: null,
|
|
413
|
+
},
|
|
414
|
+
value: { ok: true },
|
|
415
|
+
required: false,
|
|
416
|
+
disabled: false,
|
|
417
|
+
onvaluechange: (value) => values.push(value),
|
|
418
|
+
onvaliditychange: (valid) => validity.push(valid),
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
const input = screen.getByRole('textbox');
|
|
422
|
+
input.value = '{bad';
|
|
423
|
+
await fireEvent.input(input);
|
|
424
|
+
expect(input).toHaveAttribute('aria-invalid', 'true');
|
|
425
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/valid JSON/i);
|
|
426
|
+
expect(values).toEqual([]);
|
|
427
|
+
expect(validity.at(-1)).toBe(false);
|
|
428
|
+
});
|
|
429
|
+
it('treats an empty optional JSON input as valid and emits undefined', async () => {
|
|
430
|
+
const values = [];
|
|
431
|
+
const validity = [];
|
|
432
|
+
render(FieldInput, {
|
|
433
|
+
props: {
|
|
434
|
+
id: 'payload-empty',
|
|
435
|
+
name: 'payload',
|
|
436
|
+
field: {
|
|
437
|
+
name: 'payload',
|
|
438
|
+
definition: { type: 'json' },
|
|
439
|
+
group: null,
|
|
440
|
+
order: null,
|
|
441
|
+
},
|
|
442
|
+
value: { ok: true },
|
|
443
|
+
required: false,
|
|
444
|
+
disabled: false,
|
|
445
|
+
onvaluechange: (value) => values.push(value),
|
|
446
|
+
onvaliditychange: (valid) => validity.push(valid),
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
const input = screen.getByRole('textbox');
|
|
450
|
+
input.value = '';
|
|
451
|
+
await fireEvent.input(input);
|
|
452
|
+
expect(input).not.toHaveAttribute('aria-invalid');
|
|
453
|
+
expect(values.at(-1)).toBeUndefined();
|
|
454
|
+
expect(validity.at(-1)).toBe(true);
|
|
455
|
+
});
|
|
456
|
+
it('does not emit NaN while a numeric control holds an invalid intermediate value', async () => {
|
|
457
|
+
const values = [];
|
|
458
|
+
render(FieldInput, {
|
|
459
|
+
props: {
|
|
460
|
+
id: 'price',
|
|
461
|
+
name: 'price',
|
|
462
|
+
field: {
|
|
463
|
+
name: 'price',
|
|
464
|
+
definition: { type: 'decimal' },
|
|
465
|
+
group: null,
|
|
466
|
+
order: null,
|
|
467
|
+
},
|
|
468
|
+
value: 12.5,
|
|
469
|
+
required: false,
|
|
470
|
+
disabled: false,
|
|
471
|
+
onvaluechange: (value) => values.push(value),
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
const input = screen.getByRole('spinbutton');
|
|
475
|
+
let raw = '1e';
|
|
476
|
+
Object.defineProperty(input, 'value', {
|
|
477
|
+
configurable: true,
|
|
478
|
+
get: () => raw,
|
|
479
|
+
set: (next) => {
|
|
480
|
+
raw = next;
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
await fireEvent.input(input);
|
|
484
|
+
expect(values).toEqual([]);
|
|
485
|
+
});
|
|
486
|
+
it('does not treat a browser badInput empty number value as a deliberate clear', async () => {
|
|
487
|
+
const values = [];
|
|
488
|
+
render(FieldInput, {
|
|
489
|
+
props: {
|
|
490
|
+
id: 'quantity',
|
|
491
|
+
name: 'quantity',
|
|
492
|
+
field: {
|
|
493
|
+
name: 'quantity',
|
|
494
|
+
definition: { type: 'integer' },
|
|
495
|
+
group: null,
|
|
496
|
+
order: null,
|
|
497
|
+
},
|
|
498
|
+
value: 3,
|
|
499
|
+
required: false,
|
|
500
|
+
disabled: false,
|
|
501
|
+
onvaluechange: (value) => values.push(value),
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
const input = screen.getByRole('spinbutton');
|
|
505
|
+
let raw = '';
|
|
506
|
+
Object.defineProperty(input, 'value', {
|
|
507
|
+
configurable: true,
|
|
508
|
+
get: () => raw,
|
|
509
|
+
set: (next) => {
|
|
510
|
+
raw = next;
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
Object.defineProperty(input, 'validity', {
|
|
514
|
+
configurable: true,
|
|
515
|
+
value: { badInput: true },
|
|
516
|
+
});
|
|
517
|
+
await fireEvent.input(input);
|
|
518
|
+
expect(values).toEqual([]);
|
|
519
|
+
});
|
|
520
|
+
it('converts ISO datetimes for datetime-local editing and emits an ISO DTO', async () => {
|
|
521
|
+
const values = [];
|
|
522
|
+
render(FieldInput, {
|
|
523
|
+
props: {
|
|
524
|
+
id: 'starts-at',
|
|
525
|
+
name: 'startsAt',
|
|
526
|
+
field: {
|
|
527
|
+
name: 'startsAt',
|
|
528
|
+
definition: { type: 'datetime' },
|
|
529
|
+
group: null,
|
|
530
|
+
order: null,
|
|
531
|
+
},
|
|
532
|
+
value: '2025-01-02T03:04:00.000Z',
|
|
533
|
+
required: false,
|
|
534
|
+
disabled: false,
|
|
535
|
+
onvaluechange: (value) => values.push(value),
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
const input = document.querySelector('#starts-at');
|
|
539
|
+
if (!input)
|
|
540
|
+
throw new Error('datetime input missing');
|
|
541
|
+
input.value = '2025-01-03T04:05';
|
|
542
|
+
await fireEvent.input(input);
|
|
543
|
+
expect(values.at(-1)).toMatch(/Z$/);
|
|
544
|
+
});
|
|
545
|
+
it('uses the resolved display label for generated boolean toggle accessibility', () => {
|
|
546
|
+
render(FieldInput, {
|
|
547
|
+
props: {
|
|
548
|
+
id: 'is-published',
|
|
549
|
+
name: 'isPublished',
|
|
550
|
+
label: 'Published publicly',
|
|
551
|
+
field: {
|
|
552
|
+
name: 'isPublished',
|
|
553
|
+
definition: { type: 'boolean' },
|
|
554
|
+
group: null,
|
|
555
|
+
order: null,
|
|
556
|
+
},
|
|
557
|
+
value: false,
|
|
558
|
+
required: false,
|
|
559
|
+
disabled: false,
|
|
560
|
+
onvaluechange: () => undefined,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
expect(screen.getByRole('checkbox', { name: 'Published publicly' })).toBeInTheDocument();
|
|
564
|
+
});
|
|
565
|
+
it('humanizes a generated boolean name when policy has no display label', () => {
|
|
566
|
+
const booleanFields = { isPublished: { type: 'boolean' } };
|
|
567
|
+
const booleanPolicy = {
|
|
568
|
+
objectRef: '@test:Boolean',
|
|
569
|
+
fields: {
|
|
570
|
+
isPublished: {
|
|
571
|
+
fieldName: 'isPublished',
|
|
572
|
+
hasDefault: false,
|
|
573
|
+
defaultValue: undefined,
|
|
574
|
+
visibility: 'basic',
|
|
575
|
+
help: null,
|
|
576
|
+
label: null,
|
|
577
|
+
order: 1,
|
|
578
|
+
group: null,
|
|
579
|
+
locked: false,
|
|
580
|
+
required: false,
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
render(ObjectFormFixture, {
|
|
585
|
+
props: { fields: booleanFields, policy: booleanPolicy },
|
|
586
|
+
});
|
|
587
|
+
expect(screen.getByRole('checkbox', { name: 'Is Published' })).toBeInTheDocument();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
describe('ObjectForm generated source acceptance', () => {
|
|
591
|
+
const productRef = objectRef(AcceptanceProduct);
|
|
592
|
+
const eventRef = objectRef(AcceptanceEvent);
|
|
593
|
+
const productFields = {
|
|
594
|
+
name: { type: 'text', required: true },
|
|
595
|
+
price: { type: 'decimal' },
|
|
596
|
+
published: { type: 'boolean' },
|
|
597
|
+
};
|
|
598
|
+
const eventFields = {
|
|
599
|
+
title: { type: 'text', required: true },
|
|
600
|
+
startsAt: { type: 'datetime' },
|
|
601
|
+
metadata: { type: 'json' },
|
|
602
|
+
};
|
|
603
|
+
const definitions = [
|
|
604
|
+
{ objectRef: productRef, fields: productFields },
|
|
605
|
+
{ objectRef: eventRef, fields: eventFields },
|
|
606
|
+
];
|
|
607
|
+
const policies = {
|
|
608
|
+
[productRef]: sourcePolicy(productRef, productFields, { published: false }),
|
|
609
|
+
[eventRef]: sourcePolicy(eventRef, eventFields),
|
|
610
|
+
};
|
|
611
|
+
function createSource(response = { policies }) {
|
|
612
|
+
const source = new ObjectFormSourceRegistry({
|
|
613
|
+
resolveBatch: vi.fn(async () => response),
|
|
614
|
+
});
|
|
615
|
+
for (const definition of definitions)
|
|
616
|
+
source.register(definition);
|
|
617
|
+
return source;
|
|
618
|
+
}
|
|
619
|
+
it('drives two REST-exposed decorated objects through one source, including create and edit semantics', async () => {
|
|
620
|
+
const productRegistration = ObjectRegistry.getClassByConstructor(AcceptanceProduct);
|
|
621
|
+
const eventRegistration = ObjectRegistry.getClassByConstructor(AcceptanceEvent);
|
|
622
|
+
expect(productRegistration?.config.api).toEqual(expect.objectContaining({
|
|
623
|
+
include: expect.arrayContaining(['create', 'update']),
|
|
624
|
+
}));
|
|
625
|
+
expect(eventRegistration?.config.api).toEqual(expect.objectContaining({
|
|
626
|
+
include: expect.arrayContaining(['create', 'update']),
|
|
627
|
+
}));
|
|
628
|
+
expect(ObjectRegistry.getFields(productRef).get('price')?.type).toBe('decimal');
|
|
629
|
+
expect(ObjectRegistry.getFields(eventRef).get('metadata')?.type).toBe('json');
|
|
630
|
+
const createView = render(ObjectFormSourceSnippetFixture, {
|
|
631
|
+
props: { source: createSource(), objectRef: productRef },
|
|
632
|
+
});
|
|
633
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Custom generated name' })).toBeInTheDocument());
|
|
634
|
+
expect(screen.getByTestId('record')).toHaveTextContent('"published":false');
|
|
635
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Custom generated name' }), 'deluxe');
|
|
636
|
+
expect(screen.getByTestId('record')).toHaveTextContent('"DELUXE"');
|
|
637
|
+
await expectNoA11yViolations(createView.container);
|
|
638
|
+
createView.unmount();
|
|
639
|
+
render(ObjectFormSourceFixture, {
|
|
640
|
+
props: {
|
|
641
|
+
source: createSource(),
|
|
642
|
+
objectRef: eventRef,
|
|
643
|
+
isNewRecord: false,
|
|
644
|
+
initialValue: {
|
|
645
|
+
title: 'Existing event',
|
|
646
|
+
startsAt: '2026-08-08T10:00',
|
|
647
|
+
metadata: { venue: 'Hall A' },
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
await vi.waitFor(() => expect(screen.getByRole('textbox', { name: 'Title' })).toHaveValue('Existing event'));
|
|
652
|
+
expect(screen.getByRole('textbox', { name: 'Metadata' })
|
|
653
|
+
.value).toContain('Hall A');
|
|
654
|
+
await userEvent.clear(screen.getByRole('textbox', { name: 'Title' }));
|
|
655
|
+
await userEvent.type(screen.getByRole('textbox', { name: 'Title' }), 'Updated event');
|
|
656
|
+
expect(screen.getByTestId('record')).toHaveTextContent('Updated event');
|
|
657
|
+
});
|
|
658
|
+
it('fails closed with an accessible error for malformed generated action responses', async () => {
|
|
659
|
+
render(ObjectFormSourceFixture, {
|
|
660
|
+
props: {
|
|
661
|
+
source: createSource({
|
|
662
|
+
policies: { [productRef]: { objectRef: eventRef } },
|
|
663
|
+
}),
|
|
664
|
+
objectRef: productRef,
|
|
665
|
+
},
|
|
666
|
+
});
|
|
667
|
+
await vi.waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent(/Invalid ObjectForm source response/));
|
|
668
|
+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
669
|
+
});
|
|
670
|
+
it('fails closed with an accessible error for an unregistered objectRef', async () => {
|
|
671
|
+
render(ObjectFormSourceFixture, {
|
|
672
|
+
props: { source: createSource(), objectRef: '@test/missing:Missing' },
|
|
673
|
+
});
|
|
674
|
+
await vi.waitFor(() => expect(screen.getByRole('alert')).toHaveTextContent(/has no definition/));
|
|
675
|
+
expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
describe('DataTable policy consumer acceptance', () => {
|
|
679
|
+
it('uses the real DataTable visible-column API without reviving static-hidden columns', () => {
|
|
680
|
+
render(PolicyDataTableFixture, { props: { policy } });
|
|
681
|
+
expect(screen.getByRole('columnheader', { name: 'Name' })).toBeInTheDocument();
|
|
682
|
+
expect(screen.queryByRole('columnheader', { name: 'Published' })).not.toBeInTheDocument();
|
|
683
|
+
expect(screen.queryByRole('columnheader', { name: 'Metadata' })).not.toBeInTheDocument();
|
|
684
|
+
expect(screen.getByRole('columnheader', { name: 'Computed' })).toBeInTheDocument();
|
|
685
|
+
expect(screen.getByRole('columnheader', { name: 'Actions' })).toBeInTheDocument();
|
|
686
|
+
});
|
|
687
|
+
});
|