@happyvertical/smrt-svelte 0.43.4 → 0.43.6
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 +23 -0
- package/README.md +23 -1
- package/dist/components/forms/AddressInput.svelte +155 -42
- package/dist/components/forms/AddressInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/CheckboxInput.svelte +40 -13
- package/dist/components/forms/CheckboxInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateRangeInput.svelte +132 -16
- package/dist/components/forms/DateRangeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/DateTimeInput.svelte +48 -14
- package/dist/components/forms/DateTimeInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +599 -70
- package/dist/components/forms/Form.svelte.d.ts +6 -2
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/MeasurementInput.svelte +137 -31
- package/dist/components/forms/MeasurementInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/MoneyInput.svelte +42 -23
- package/dist/components/forms/MoneyInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/NumberInput.svelte +59 -27
- package/dist/components/forms/NumberInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/PhoneInput.svelte +51 -22
- package/dist/components/forms/PhoneInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/SelectInput.svelte +38 -22
- package/dist/components/forms/SelectInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextInput.svelte +32 -18
- package/dist/components/forms/TextInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/TextareaInput.svelte +25 -18
- package/dist/components/forms/TextareaInput.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/AddressInput.behavior.test.js +94 -0
- package/dist/components/forms/__tests__/Form.behavior.test.js +1 -1
- package/dist/components/forms/__tests__/Form.extraction.test.js +68 -0
- package/dist/components/forms/__tests__/Form.test.js +28 -0
- package/dist/components/forms/__tests__/Form.webmcp.test.js +2123 -114
- package/dist/components/forms/__tests__/FormMicButton.test.js +0 -1
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte +30 -0
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts +7 -0
- package/dist/components/forms/__tests__/async-invalid-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte +19 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/async-validation-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte +21 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/async-validation-snapshot-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/compound-registration-wrapper.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte +48 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/custom-registration-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte +26 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-extraction-values.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte +52 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts +16 -0
- package/dist/components/forms/__tests__/form-registration-lifecycle.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte +108 -5
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts +34 -0
- package/dist/components/forms/__tests__/form-with-fields.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte +24 -0
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-with-policy-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte +69 -4
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts +32 -0
- package/dist/components/forms/__tests__/form-with-structured-fields.fixture.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte +44 -0
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/legacy-form-context.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte +35 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/policy-field.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/prepare-field-value.d.ts +5 -0
- package/dist/components/forms/prepare-field-value.d.ts.map +1 -0
- package/dist/components/forms/prepare-field-value.js +15 -0
- package/dist/state/form-context.d.ts +28 -6
- package/dist/state/form-context.d.ts.map +1 -1
- package/dist/state/form-context.js +75 -7
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte +62 -0
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts +8 -0
- package/dist/web/__tests__/webmcp-composed.child.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte +52 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts +12 -0
- package/dist/web/__tests__/webmcp-composed.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-composed.integration.svelte.test.js +247 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte +13 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts +19 -0
- package/dist/web/__tests__/webmcp-ssr.fixture.svelte.d.ts.map +1 -0
- package/dist/web/__tests__/webmcp-ui.test.js +209 -33
- package/dist/web/__tests__/webmcp.test.js +20 -0
- package/dist/web/webmcp-ui.d.ts.map +1 -1
- package/dist/web/webmcp-ui.js +50 -31
- package/dist/web/webmcp.svelte.d.ts.map +1 -1
- package/dist/web/webmcp.svelte.js +6 -1
- package/package.json +8 -6
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { Icon } from '@happyvertical/smrt-ui';
|
|
3
3
|
import { matchOption } from '@happyvertical/smrt-ui/utils/forms/formatters.js';
|
|
4
|
-
import { onDestroy, onMount } from 'svelte';
|
|
5
4
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
6
5
|
import {
|
|
7
6
|
type FieldDefinition,
|
|
8
7
|
tryGetFormContext,
|
|
9
8
|
} from '../../state/form-context.js';
|
|
9
|
+
import { prepareTextFieldValue } from './prepare-field-value.js';
|
|
10
10
|
import type { SelectOption } from './types.js';
|
|
11
11
|
|
|
12
12
|
export interface Props {
|
|
@@ -55,18 +55,21 @@ function updateValue(newValue: string) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Register with form context
|
|
58
|
-
|
|
58
|
+
$effect(() => {
|
|
59
59
|
if (formContext) {
|
|
60
|
-
const
|
|
61
|
-
const fullDescription = description
|
|
62
|
-
? `${description}. Options: ${optionsDesc}`
|
|
63
|
-
: `Options: ${optionsDesc}`;
|
|
64
|
-
|
|
60
|
+
const registeredName = name;
|
|
65
61
|
const fieldDef: FieldDefinition = {
|
|
66
|
-
name,
|
|
62
|
+
name: registeredName,
|
|
67
63
|
type: 'select',
|
|
68
|
-
label
|
|
69
|
-
|
|
64
|
+
get label() {
|
|
65
|
+
return label;
|
|
66
|
+
},
|
|
67
|
+
get description() {
|
|
68
|
+
const optionsDescription = options.map((o) => o.label).join(', ');
|
|
69
|
+
return description
|
|
70
|
+
? `${description}. Options: ${optionsDescription}`
|
|
71
|
+
: `Options: ${optionsDescription}`;
|
|
72
|
+
},
|
|
70
73
|
setValue: (v: unknown) => {
|
|
71
74
|
const strVal = String(v ?? '');
|
|
72
75
|
const matched = matchOption(strVal, options);
|
|
@@ -77,20 +80,33 @@ onMount(() => {
|
|
|
77
80
|
}
|
|
78
81
|
},
|
|
79
82
|
getValue: () => value,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
83
|
+
prepareValue: (candidate) => {
|
|
84
|
+
const next = prepareTextFieldValue(candidate);
|
|
85
|
+
if (next.trim() === '') return next;
|
|
86
|
+
return matchOption(next, options) ?? next;
|
|
87
|
+
},
|
|
88
|
+
clear: () => {
|
|
89
|
+
updateValue('');
|
|
90
|
+
return true;
|
|
91
|
+
},
|
|
92
|
+
getState: () => ({ disabled }),
|
|
93
|
+
get constraints() {
|
|
94
|
+
return { required };
|
|
95
|
+
},
|
|
96
|
+
get options() {
|
|
97
|
+
return options.map((option) => ({
|
|
98
|
+
value: option.value,
|
|
99
|
+
label: option.label,
|
|
100
|
+
}));
|
|
101
|
+
},
|
|
102
|
+
validateValue: (candidate) => {
|
|
103
|
+
const next = String(candidate ?? '');
|
|
104
|
+
if (next === '') return !required;
|
|
105
|
+
return options.some((option) => option.value === next);
|
|
106
|
+
},
|
|
85
107
|
validate: () => !required || value.trim().length > 0,
|
|
86
108
|
};
|
|
87
|
-
formContext.registerField(fieldDef);
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
onDestroy(() => {
|
|
92
|
-
if (formContext) {
|
|
93
|
-
formContext.unregisterField(name);
|
|
109
|
+
return formContext.registerField(fieldDef);
|
|
94
110
|
}
|
|
95
111
|
});
|
|
96
112
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/SelectInput.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"SelectInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/SelectInput.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wBAAwB;IACxB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAkID,QAAA,MAAM,WAAW,gDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
formatEmail,
|
|
6
6
|
formatText,
|
|
7
7
|
} from '@happyvertical/smrt-ui/utils/forms/formatters.js';
|
|
8
|
-
import { onDestroy, onMount } from 'svelte';
|
|
9
8
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
10
9
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
11
10
|
import { M } from '../../i18n/strings.forms.js';
|
|
@@ -14,6 +13,7 @@ import {
|
|
|
14
13
|
type FieldDefinition,
|
|
15
14
|
tryGetFormContext,
|
|
16
15
|
} from '../../state/form-context.js';
|
|
16
|
+
import { prepareTextFieldValue } from './prepare-field-value.js';
|
|
17
17
|
|
|
18
18
|
const { t } = useI18n();
|
|
19
19
|
|
|
@@ -78,10 +78,12 @@ const isInitializing = $derived(stt.isInitializing);
|
|
|
78
78
|
const downloadProgress = $derived(stt.downloadProgress);
|
|
79
79
|
|
|
80
80
|
// Validation
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
function emailValueIsValid(candidate: string): boolean {
|
|
82
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(candidate);
|
|
83
|
+
}
|
|
84
|
+
const isValidEmail = $derived(
|
|
85
|
+
type !== 'email' || !value || emailValueIsValid(value),
|
|
86
|
+
);
|
|
85
87
|
const showInvalid = $derived(type === 'email' && value && !isValidEmail);
|
|
86
88
|
|
|
87
89
|
// Helper to update value (works with $bindable)
|
|
@@ -91,29 +93,41 @@ function updateValue(newValue: string) {
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
// Register with form context
|
|
94
|
-
|
|
96
|
+
$effect(() => {
|
|
95
97
|
if (formContext) {
|
|
98
|
+
const registeredName = name;
|
|
96
99
|
const fieldDef: FieldDefinition = {
|
|
97
|
-
name,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
name: registeredName,
|
|
101
|
+
get type() {
|
|
102
|
+
return type === 'email' ? 'email' : 'text';
|
|
103
|
+
},
|
|
104
|
+
get label() {
|
|
105
|
+
return label;
|
|
106
|
+
},
|
|
107
|
+
get description() {
|
|
108
|
+
return description;
|
|
109
|
+
},
|
|
101
110
|
setValue: (v: unknown) => {
|
|
102
111
|
updateValue(String(v ?? ''));
|
|
103
112
|
},
|
|
104
113
|
getValue: () => value,
|
|
105
|
-
|
|
114
|
+
prepareValue: prepareTextFieldValue,
|
|
115
|
+
getState: () => ({ disabled }),
|
|
116
|
+
get constraints() {
|
|
117
|
+
return { required };
|
|
118
|
+
},
|
|
119
|
+
validateValue: (candidate) => {
|
|
120
|
+
const next = String(candidate ?? '');
|
|
121
|
+
return (
|
|
122
|
+
(!required || next.trim().length > 0) &&
|
|
123
|
+
(type !== 'email' || next.length === 0 || emailValueIsValid(next))
|
|
124
|
+
);
|
|
125
|
+
},
|
|
106
126
|
validate: () =>
|
|
107
127
|
(!required || value.trim().length > 0) &&
|
|
108
128
|
(type !== 'email' || value.length === 0 || isValidEmail),
|
|
109
129
|
};
|
|
110
|
-
formContext.registerField(fieldDef);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
onDestroy(() => {
|
|
115
|
-
if (formContext) {
|
|
116
|
-
formContext.unregisterField(name);
|
|
130
|
+
return formContext.registerField(fieldDef);
|
|
117
131
|
}
|
|
118
132
|
});
|
|
119
133
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextInput.svelte.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"TextInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextInput.svelte.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AA2QD,QAAA,MAAM,SAAS,gDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { Icon, ripple } from '@happyvertical/smrt-ui';
|
|
3
3
|
import { useI18n } from '@happyvertical/smrt-ui/i18n';
|
|
4
4
|
import { formatText } from '@happyvertical/smrt-ui/utils/forms/formatters.js';
|
|
5
|
-
import { onDestroy, onMount } from 'svelte';
|
|
6
5
|
import { useAppState } from '../../hooks/useAppState.svelte.js';
|
|
7
6
|
import { useSTT } from '../../hooks/useSTT.svelte.js';
|
|
8
7
|
import { M } from '../../i18n/strings.forms.js';
|
|
@@ -11,6 +10,7 @@ import {
|
|
|
11
10
|
type FieldDefinition,
|
|
12
11
|
tryGetFormContext,
|
|
13
12
|
} from '../../state/form-context.js';
|
|
13
|
+
import { prepareTextFieldValue } from './prepare-field-value.js';
|
|
14
14
|
|
|
15
15
|
const { t } = useI18n();
|
|
16
16
|
|
|
@@ -75,31 +75,38 @@ function updateValue(newValue: string) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// Register with form context
|
|
78
|
-
|
|
78
|
+
$effect(() => {
|
|
79
79
|
if (formContext) {
|
|
80
|
+
const registeredName = name;
|
|
80
81
|
const fieldDef: FieldDefinition = {
|
|
81
|
-
name,
|
|
82
|
+
name: registeredName,
|
|
82
83
|
type: 'textarea',
|
|
83
|
-
label
|
|
84
|
-
|
|
84
|
+
get label() {
|
|
85
|
+
return label;
|
|
86
|
+
},
|
|
87
|
+
get description() {
|
|
88
|
+
return description;
|
|
89
|
+
},
|
|
85
90
|
setValue: (v: unknown) => {
|
|
86
|
-
|
|
87
|
-
updateValue(`${value}\n${String(v ?? '')}`);
|
|
88
|
-
} else {
|
|
89
|
-
updateValue(String(v ?? ''));
|
|
90
|
-
}
|
|
91
|
+
updateValue(String(v ?? ''));
|
|
91
92
|
},
|
|
92
93
|
getValue: () => value,
|
|
93
|
-
|
|
94
|
+
prepareValue: (candidate) => {
|
|
95
|
+
const next = prepareTextFieldValue(candidate);
|
|
96
|
+
return appendMode && value ? `${value}\n${next}` : next;
|
|
97
|
+
},
|
|
98
|
+
// The review editor contains the complete displayed text, not a new
|
|
99
|
+
// append intent. Canonicalize its scalar shape without appending again.
|
|
100
|
+
prepareReviewedValue: prepareTextFieldValue,
|
|
101
|
+
getState: () => ({ disabled }),
|
|
102
|
+
get constraints() {
|
|
103
|
+
return { required };
|
|
104
|
+
},
|
|
105
|
+
validateValue: (candidate) =>
|
|
106
|
+
!required || String(candidate ?? '').trim().length > 0,
|
|
94
107
|
validate: () => !required || value.trim().length > 0,
|
|
95
108
|
};
|
|
96
|
-
formContext.registerField(fieldDef);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
onDestroy(() => {
|
|
101
|
-
if (formContext) {
|
|
102
|
-
formContext.unregisterField(name);
|
|
109
|
+
return formContext.registerField(fieldDef);
|
|
103
110
|
}
|
|
104
111
|
});
|
|
105
112
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TextareaInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextareaInput.svelte.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;
|
|
1
|
+
{"version":3,"file":"TextareaInput.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/TextareaInput.svelte.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,KAAK;IACpB,iBAAiB;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,kBAAkB;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qBAAqB;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sDAAsD;IACtD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAwOD,QAAA,MAAM,aAAa,gDAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -11,10 +11,21 @@
|
|
|
11
11
|
import { expectNoA11yViolations } from '@happyvertical/smrt-ui/test-support/a11y';
|
|
12
12
|
import { fireEvent, render, screen } from '@testing-library/svelte';
|
|
13
13
|
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import { tick } from 'svelte';
|
|
14
15
|
import { describe, expect, it, vi } from 'vitest';
|
|
16
|
+
const formCapture = vi.hoisted(() => ({
|
|
17
|
+
field: undefined,
|
|
18
|
+
}));
|
|
15
19
|
vi.mock('../../../hooks/useAppState.svelte.js', () => ({
|
|
16
20
|
useAppState: () => ({ state: { mode: 'default' }, setMode: vi.fn() }),
|
|
17
21
|
}));
|
|
22
|
+
vi.mock('../../../state/form-context.js', () => ({
|
|
23
|
+
tryGetFormContext: () => ({
|
|
24
|
+
registerField: (field) => {
|
|
25
|
+
formCapture.field = field;
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
18
29
|
import AddressInput from '../AddressInput.svelte';
|
|
19
30
|
describe('AddressInput — behavior', () => {
|
|
20
31
|
it('typing the street composes a value including the street', async () => {
|
|
@@ -102,6 +113,89 @@ describe('AddressInput — behavior', () => {
|
|
|
102
113
|
const countries = screen.getByLabelText('Country');
|
|
103
114
|
expect(countries.querySelector('option[value="US"]')?.textContent).toBe('United States');
|
|
104
115
|
});
|
|
116
|
+
it('parses spoken countries only from the live configured options', async () => {
|
|
117
|
+
const onchange = vi.fn();
|
|
118
|
+
render(AddressInput, {
|
|
119
|
+
props: {
|
|
120
|
+
name: 'addr',
|
|
121
|
+
label: 'Address',
|
|
122
|
+
fields: ['country'],
|
|
123
|
+
countries: [{ value: 'FR', label: 'France' }],
|
|
124
|
+
onchange,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
await tick();
|
|
128
|
+
const field = formCapture.field;
|
|
129
|
+
if (!field)
|
|
130
|
+
throw new Error('AddressInput did not register its field');
|
|
131
|
+
field.setValue('Canada');
|
|
132
|
+
expect(onchange).not.toHaveBeenCalled();
|
|
133
|
+
expect(field.getValue()).toEqual({ country: 'FR' });
|
|
134
|
+
field.setValue('France');
|
|
135
|
+
expect(onchange).toHaveBeenLastCalledWith({ country: 'FR' });
|
|
136
|
+
field.setValue('FR');
|
|
137
|
+
expect(onchange).toHaveBeenLastCalledWith({ country: 'FR' });
|
|
138
|
+
});
|
|
139
|
+
it('retains spoken Canada and United States defaults', async () => {
|
|
140
|
+
const onchange = vi.fn();
|
|
141
|
+
render(AddressInput, {
|
|
142
|
+
props: {
|
|
143
|
+
name: 'addr',
|
|
144
|
+
label: 'Address',
|
|
145
|
+
fields: ['country'],
|
|
146
|
+
onchange,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
await tick();
|
|
150
|
+
const field = formCapture.field;
|
|
151
|
+
if (!field)
|
|
152
|
+
throw new Error('AddressInput did not register its field');
|
|
153
|
+
field.setValue('Canada');
|
|
154
|
+
expect(onchange).toHaveBeenLastCalledWith({ country: 'CA' });
|
|
155
|
+
field.setValue('United States');
|
|
156
|
+
expect(onchange).toHaveBeenLastCalledWith({ country: 'US' });
|
|
157
|
+
});
|
|
158
|
+
it('accepts valid incremental setters for a required address', async () => {
|
|
159
|
+
const onchange = vi.fn();
|
|
160
|
+
render(AddressInput, {
|
|
161
|
+
props: {
|
|
162
|
+
name: 'addr',
|
|
163
|
+
label: 'Address',
|
|
164
|
+
required: true,
|
|
165
|
+
countries: [{ value: 'FR', label: 'France' }],
|
|
166
|
+
onchange,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
await tick();
|
|
170
|
+
const field = formCapture.field;
|
|
171
|
+
if (!field)
|
|
172
|
+
throw new Error('AddressInput did not register its field');
|
|
173
|
+
field.setValue({ city: 'Paris' });
|
|
174
|
+
expect(onchange).toHaveBeenLastCalledWith({
|
|
175
|
+
street: '',
|
|
176
|
+
city: 'Paris',
|
|
177
|
+
province: '',
|
|
178
|
+
postalCode: '',
|
|
179
|
+
country: 'FR',
|
|
180
|
+
});
|
|
181
|
+
expect(field.validateValue?.({ city: 'Paris' })).toBe(false);
|
|
182
|
+
expect(field.validate?.()).toBe(false);
|
|
183
|
+
expect(field.webMcpSchema).toMatchObject({
|
|
184
|
+
required: ['street', 'city', 'province', 'postalCode', 'country'],
|
|
185
|
+
});
|
|
186
|
+
field.setValue('France');
|
|
187
|
+
expect(onchange).toHaveBeenLastCalledWith(expect.objectContaining({ city: 'Paris', country: 'FR' }));
|
|
188
|
+
onchange.mockClear();
|
|
189
|
+
field.setValue({ country: 'CA' });
|
|
190
|
+
expect(onchange).not.toHaveBeenCalled();
|
|
191
|
+
expect(field.getValue()).toEqual({
|
|
192
|
+
street: '',
|
|
193
|
+
city: 'Paris',
|
|
194
|
+
province: '',
|
|
195
|
+
postalCode: '',
|
|
196
|
+
country: 'FR',
|
|
197
|
+
});
|
|
198
|
+
});
|
|
105
199
|
it('marks every field invalid and links one alert when error is set', () => {
|
|
106
200
|
render(AddressInput, {
|
|
107
201
|
props: { name: 'addr', label: 'Address', error: 'Address required' },
|
|
@@ -94,7 +94,7 @@ describe('Form — submit value collection', () => {
|
|
|
94
94
|
// Both fields registered → both collected.
|
|
95
95
|
await userEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
|
96
96
|
expect(onsubmit).toHaveBeenLastCalledWith({ fullname: 'Keep', age: 7 });
|
|
97
|
-
// Unmount the age field;
|
|
97
|
+
// Unmount the age field; its generation-bound disposer drops it from submit.
|
|
98
98
|
await rerender({
|
|
99
99
|
onsubmit,
|
|
100
100
|
textValue: 'Keep',
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/svelte';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
const sttStub = {
|
|
5
|
+
isListening: false,
|
|
6
|
+
isInitializing: false,
|
|
7
|
+
isReady: true,
|
|
8
|
+
adapterType: 'browser-speech',
|
|
9
|
+
downloadProgress: 0,
|
|
10
|
+
lastResult: '',
|
|
11
|
+
initialize: vi.fn(async () => { }),
|
|
12
|
+
start: vi.fn(async () => { }),
|
|
13
|
+
stop: vi.fn(async () => { }),
|
|
14
|
+
};
|
|
15
|
+
vi.mock('../../../hooks/useAppState.svelte.js', () => ({
|
|
16
|
+
useAppState: () => ({ state: { mode: 'smrt' }, setMode: vi.fn() }),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock('../../../hooks/useSTT.svelte.js', () => ({
|
|
19
|
+
useSTT: () => sttStub,
|
|
20
|
+
}));
|
|
21
|
+
import FormExtractionValues from './form-extraction-values.fixture.svelte';
|
|
22
|
+
describe('Form voice extraction', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
sttStub.lastResult = '';
|
|
25
|
+
sttStub.start.mockClear();
|
|
26
|
+
sttStub.stop.mockClear();
|
|
27
|
+
});
|
|
28
|
+
it('skips an invalid earlier field and applies a later valid field', async () => {
|
|
29
|
+
const ageChanged = vi.fn();
|
|
30
|
+
const textChanged = vi.fn();
|
|
31
|
+
render(FormExtractionValues, {
|
|
32
|
+
props: { onagechange: ageChanged, ontextchange: textChanged },
|
|
33
|
+
});
|
|
34
|
+
const listen = screen.getByRole('button', { name: 'Speak all fields' });
|
|
35
|
+
await userEvent.click(listen);
|
|
36
|
+
sttStub.lastResult = 'age nonsense full name Ada Lovelace';
|
|
37
|
+
await userEvent.click(listen);
|
|
38
|
+
await waitFor(() => expect(screen.getByRole('textbox', { name: 'Full name' })).toHaveValue('Ada Lovelace'));
|
|
39
|
+
expect(ageChanged).not.toHaveBeenCalled();
|
|
40
|
+
expect(textChanged).toHaveBeenLastCalledWith('Ada Lovelace');
|
|
41
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
42
|
+
expect(screen.queryByText('staged_value_invalid')).not.toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
it('parses a spoken date range and continues to a later field', async () => {
|
|
45
|
+
const datesChanged = vi.fn();
|
|
46
|
+
const textChanged = vi.fn();
|
|
47
|
+
render(FormExtractionValues, {
|
|
48
|
+
props: {
|
|
49
|
+
showDateRange: true,
|
|
50
|
+
ondateschange: datesChanged,
|
|
51
|
+
ontextchange: textChanged,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const listen = screen.getByRole('button', { name: 'Speak all fields' });
|
|
55
|
+
await userEvent.click(listen);
|
|
56
|
+
sttStub.lastResult =
|
|
57
|
+
'dates from January 15 2026 to January 20 2026 full name Grace Hopper';
|
|
58
|
+
await userEvent.click(listen);
|
|
59
|
+
await waitFor(() => expect(datesChanged).toHaveBeenLastCalledWith({
|
|
60
|
+
startDate: '2026-01-15',
|
|
61
|
+
endDate: '2026-01-20',
|
|
62
|
+
}));
|
|
63
|
+
expect(screen.getByRole('textbox', { name: 'Full name' })).toHaveValue('Grace Hopper');
|
|
64
|
+
expect(textChanged).toHaveBeenLastCalledWith('Grace Hopper');
|
|
65
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
66
|
+
expect(screen.queryByText('staged_value_invalid')).not.toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* The aria-live status region is L1's deliverable (#1420); here we cover the
|
|
8
8
|
* form element, children rendering, the onsubmit contract, and axe.
|
|
9
9
|
*/
|
|
10
|
+
import { createControlInteractionRegistry } from '@happyvertical/smrt-ui/forms';
|
|
10
11
|
import { expectNoA11yViolations } from '@happyvertical/smrt-ui/test-support/a11y';
|
|
11
12
|
import { render, screen } from '@testing-library/svelte';
|
|
12
13
|
import userEvent from '@testing-library/user-event';
|
|
@@ -55,4 +56,31 @@ describe('Form', () => {
|
|
|
55
56
|
const { container } = render(Form, { props: { children: submitButton() } });
|
|
56
57
|
await expectNoA11yViolations(container);
|
|
57
58
|
});
|
|
59
|
+
it('uses the shared staged-edit review contract for local human apply', async () => {
|
|
60
|
+
let value = 'Ada';
|
|
61
|
+
const registry = createControlInteractionRegistry({
|
|
62
|
+
isLocalGesture: () => true,
|
|
63
|
+
});
|
|
64
|
+
const identity = { formId: 'profile', controlId: 'display-name' };
|
|
65
|
+
registry.register({
|
|
66
|
+
identity,
|
|
67
|
+
metadata: { kind: 'text', label: 'Display name' },
|
|
68
|
+
getValue: () => value,
|
|
69
|
+
setValue: (next) => {
|
|
70
|
+
value = String(next);
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
render(Form, {
|
|
74
|
+
props: {
|
|
75
|
+
children: submitButton(),
|
|
76
|
+
formId: 'profile',
|
|
77
|
+
interactionRegistry: registry,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
await registry.execute({ action: 'stage', identity, value: 'Grace' }, { source: 'agent' });
|
|
81
|
+
expect(value).toBe('Ada');
|
|
82
|
+
expect(screen.getByRole('region', { name: 'Review proposed changes' })).toBeInTheDocument();
|
|
83
|
+
await userEvent.click(screen.getByRole('button', { name: 'Apply Display name' }));
|
|
84
|
+
expect(value).toBe('Grace');
|
|
85
|
+
});
|
|
58
86
|
});
|