@tai42/studio-sdk 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +15 -0
- package/README.md +318 -0
- package/SECURITY.md +51 -0
- package/dist/components/badge.d.ts +13 -0
- package/dist/components/badge.d.ts.map +1 -0
- package/dist/components/badge.js +14 -0
- package/dist/components/badge.js.map +1 -0
- package/dist/components/checkbox.d.ts +24 -0
- package/dist/components/checkbox.d.ts.map +1 -0
- package/dist/components/checkbox.js +31 -0
- package/dist/components/checkbox.js.map +1 -0
- package/dist/components/code-block.d.ts +7 -0
- package/dist/components/code-block.d.ts.map +1 -0
- package/dist/components/code-block.js +23 -0
- package/dist/components/code-block.js.map +1 -0
- package/dist/components/completion-input.d.ts +31 -0
- package/dist/components/completion-input.d.ts.map +1 -0
- package/dist/components/completion-input.js +61 -0
- package/dist/components/completion-input.js.map +1 -0
- package/dist/components/components.css +2212 -0
- package/dist/components/confirm-dialog.d.ts +40 -0
- package/dist/components/confirm-dialog.d.ts.map +1 -0
- package/dist/components/confirm-dialog.js +11 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/control-styles.d.ts +12 -0
- package/dist/components/control-styles.d.ts.map +1 -0
- package/dist/components/control-styles.js +14 -0
- package/dist/components/control-styles.js.map +1 -0
- package/dist/components/copy-field.d.ts +8 -0
- package/dist/components/copy-field.d.ts.map +1 -0
- package/dist/components/copy-field.js +106 -0
- package/dist/components/copy-field.js.map +1 -0
- package/dist/components/dialog.d.ts +21 -0
- package/dist/components/dialog.d.ts.map +1 -0
- package/dist/components/dialog.js +25 -0
- package/dist/components/dialog.js.map +1 -0
- package/dist/components/drawer.d.ts +22 -0
- package/dist/components/drawer.d.ts.map +1 -0
- package/dist/components/drawer.js +32 -0
- package/dist/components/drawer.js.map +1 -0
- package/dist/components/error-boundary.d.ts +33 -0
- package/dist/components/error-boundary.d.ts.map +1 -0
- package/dist/components/error-boundary.js +36 -0
- package/dist/components/error-boundary.js.map +1 -0
- package/dist/components/extension-combo-builder.d.ts +70 -0
- package/dist/components/extension-combo-builder.d.ts.map +1 -0
- package/dist/components/extension-combo-builder.js +154 -0
- package/dist/components/extension-combo-builder.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +65 -0
- package/dist/components/extension-grouping.d.ts.map +1 -0
- package/dist/components/extension-grouping.js +78 -0
- package/dist/components/extension-grouping.js.map +1 -0
- package/dist/components/extension-picker.d.ts +35 -0
- package/dist/components/extension-picker.d.ts.map +1 -0
- package/dist/components/extension-picker.js +31 -0
- package/dist/components/extension-picker.js.map +1 -0
- package/dist/components/external-link.d.ts +19 -0
- package/dist/components/external-link.d.ts.map +1 -0
- package/dist/components/external-link.js +12 -0
- package/dist/components/external-link.js.map +1 -0
- package/dist/components/field.d.ts +64 -0
- package/dist/components/field.d.ts.map +1 -0
- package/dist/components/field.js +56 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/fleet-report.d.ts +39 -0
- package/dist/components/fleet-report.d.ts.map +1 -0
- package/dist/components/fleet-report.js +50 -0
- package/dist/components/fleet-report.js.map +1 -0
- package/dist/components/fonts.css +22 -0
- package/dist/components/icons.d.ts +142 -0
- package/dist/components/icons.d.ts.map +1 -0
- package/dist/components/icons.js +139 -0
- package/dist/components/icons.js.map +1 -0
- package/dist/components/inputs.d.ts +52 -0
- package/dist/components/inputs.d.ts.map +1 -0
- package/dist/components/inputs.js +35 -0
- package/dist/components/inputs.js.map +1 -0
- package/dist/components/json-diff.d.ts +40 -0
- package/dist/components/json-diff.d.ts.map +1 -0
- package/dist/components/json-diff.js +119 -0
- package/dist/components/json-diff.js.map +1 -0
- package/dist/components/json-tree.d.ts +8 -0
- package/dist/components/json-tree.d.ts.map +1 -0
- package/dist/components/json-tree.js +70 -0
- package/dist/components/json-tree.js.map +1 -0
- package/dist/components/modal-focus.d.ts +15 -0
- package/dist/components/modal-focus.d.ts.map +1 -0
- package/dist/components/modal-focus.js +136 -0
- package/dist/components/modal-focus.js.map +1 -0
- package/dist/components/page-header.d.ts +56 -0
- package/dist/components/page-header.d.ts.map +1 -0
- package/dist/components/page-header.js +24 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/primitives.d.ts +186 -0
- package/dist/components/primitives.d.ts.map +1 -0
- package/dist/components/primitives.js +204 -0
- package/dist/components/primitives.js.map +1 -0
- package/dist/components/progress-bar.d.ts +29 -0
- package/dist/components/progress-bar.d.ts.map +1 -0
- package/dist/components/progress-bar.js +48 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/radio-group.d.ts +37 -0
- package/dist/components/radio-group.d.ts.map +1 -0
- package/dist/components/radio-group.js +56 -0
- package/dist/components/radio-group.js.map +1 -0
- package/dist/components/reveal-input.d.ts +13 -0
- package/dist/components/reveal-input.d.ts.map +1 -0
- package/dist/components/reveal-input.js +31 -0
- package/dist/components/reveal-input.js.map +1 -0
- package/dist/components/scroll-region.d.ts +85 -0
- package/dist/components/scroll-region.d.ts.map +1 -0
- package/dist/components/scroll-region.js +574 -0
- package/dist/components/scroll-region.js.map +1 -0
- package/dist/components/select.d.ts +53 -0
- package/dist/components/select.d.ts.map +1 -0
- package/dist/components/select.js +33 -0
- package/dist/components/select.js.map +1 -0
- package/dist/components/table.d.ts +59 -0
- package/dist/components/table.d.ts.map +1 -0
- package/dist/components/table.js +20 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/tabs.d.ts +15 -0
- package/dist/components/tabs.d.ts.map +1 -0
- package/dist/components/tabs.js +13 -0
- package/dist/components/tabs.js.map +1 -0
- package/dist/components/tags.d.ts +46 -0
- package/dist/components/tags.d.ts.map +1 -0
- package/dist/components/tags.js +68 -0
- package/dist/components/tags.js.map +1 -0
- package/dist/components/tokens.css +392 -0
- package/dist/components/tokens.d.ts +15 -0
- package/dist/components/tokens.d.ts.map +1 -0
- package/dist/components/tokens.js +101 -0
- package/dist/components/tokens.js.map +1 -0
- package/dist/components/tool-picker.d.ts +24 -0
- package/dist/components/tool-picker.d.ts.map +1 -0
- package/dist/components/tool-picker.js +97 -0
- package/dist/components/tool-picker.js.map +1 -0
- package/dist/components/tooltip.d.ts +17 -0
- package/dist/components/tooltip.d.ts.map +1 -0
- package/dist/components/tooltip.js +18 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/version-history-panel.d.ts +71 -0
- package/dist/components/version-history-panel.d.ts.map +1 -0
- package/dist/components/version-history-panel.js +148 -0
- package/dist/components/version-history-panel.js.map +1 -0
- package/dist/download.d.ts +7 -0
- package/dist/download.d.ts.map +1 -0
- package/dist/download.js +18 -0
- package/dist/download.js.map +1 -0
- package/dist/element-slot.d.ts +4 -0
- package/dist/element-slot.d.ts.map +1 -0
- package/dist/element-slot.js +20 -0
- package/dist/element-slot.js.map +1 -0
- package/dist/elicitation/ElicitationForm.d.ts +34 -0
- package/dist/elicitation/ElicitationForm.d.ts.map +1 -0
- package/dist/elicitation/ElicitationForm.js +24 -0
- package/dist/elicitation/ElicitationForm.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +15 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension-combos.d.ts +13 -0
- package/dist/extension-combos.d.ts.map +1 -0
- package/dist/extension-combos.js +9 -0
- package/dist/extension-combos.js.map +1 -0
- package/dist/hooks/useApi.d.ts +1222 -0
- package/dist/hooks/useApi.d.ts.map +1 -0
- package/dist/hooks/useApi.js +22 -0
- package/dist/hooks/useApi.js.map +1 -0
- package/dist/hooks/useAuth.d.ts +13 -0
- package/dist/hooks/useAuth.d.ts.map +1 -0
- package/dist/hooks/useAuth.js +53 -0
- package/dist/hooks/useAuth.js.map +1 -0
- package/dist/hooks/useBreakpoint.d.ts +10 -0
- package/dist/hooks/useBreakpoint.d.ts.map +1 -0
- package/dist/hooks/useBreakpoint.js +66 -0
- package/dist/hooks/useBreakpoint.js.map +1 -0
- package/dist/hooks/useCapabilities.d.ts +88 -0
- package/dist/hooks/useCapabilities.d.ts.map +1 -0
- package/dist/hooks/useCapabilities.js +203 -0
- package/dist/hooks/useCapabilities.js.map +1 -0
- package/dist/hooks/useSse.d.ts +13 -0
- package/dist/hooks/useSse.d.ts.map +1 -0
- package/dist/hooks/useSse.js +298 -0
- package/dist/hooks/useSse.js.map +1 -0
- package/dist/hooks/useTheme.d.ts +20 -0
- package/dist/hooks/useTheme.d.ts.map +1 -0
- package/dist/hooks/useTheme.js +97 -0
- package/dist/hooks/useTheme.js.map +1 -0
- package/dist/hooks/useUnauthorized.d.ts +3 -0
- package/dist/hooks/useUnauthorized.d.ts.map +1 -0
- package/dist/hooks/useUnauthorized.js +18 -0
- package/dist/hooks/useUnauthorized.js.map +1 -0
- package/dist/host.d.ts +11 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +10 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +91 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/context.d.ts +25 -0
- package/dist/navigation/context.d.ts.map +1 -0
- package/dist/navigation/context.js +45 -0
- package/dist/navigation/context.js.map +1 -0
- package/dist/navigation/index.d.ts +4 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +2 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/navigation/types.d.ts +97 -0
- package/dist/navigation/types.d.ts.map +1 -0
- package/dist/navigation/types.js +16 -0
- package/dist/navigation/types.js.map +1 -0
- package/dist/plugin/host-state.d.ts +45 -0
- package/dist/plugin/host-state.d.ts.map +1 -0
- package/dist/plugin/host-state.js +62 -0
- package/dist/plugin/host-state.js.map +1 -0
- package/dist/plugin/registry.d.ts +35 -0
- package/dist/plugin/registry.d.ts.map +1 -0
- package/dist/plugin/registry.js +129 -0
- package/dist/plugin/registry.js.map +1 -0
- package/dist/plugin/types.d.ts +165 -0
- package/dist/plugin/types.d.ts.map +1 -0
- package/dist/plugin/types.js +2 -0
- package/dist/plugin/types.js.map +1 -0
- package/dist/plugin/version.d.ts +34 -0
- package/dist/plugin/version.d.ts.map +1 -0
- package/dist/plugin/version.js +37 -0
- package/dist/plugin/version.js.map +1 -0
- package/dist/query-keys.d.ts +31 -0
- package/dist/query-keys.d.ts.map +1 -0
- package/dist/query-keys.js +31 -0
- package/dist/query-keys.js.map +1 -0
- package/dist/schema-editor/SchemaEditor.d.ts +43 -0
- package/dist/schema-editor/SchemaEditor.d.ts.map +1 -0
- package/dist/schema-editor/SchemaEditor.js +106 -0
- package/dist/schema-editor/SchemaEditor.js.map +1 -0
- package/dist/schema-editor/index.d.ts +10 -0
- package/dist/schema-editor/index.d.ts.map +1 -0
- package/dist/schema-editor/index.js +8 -0
- package/dist/schema-editor/index.js.map +1 -0
- package/dist/schema-editor/lint.d.ts +37 -0
- package/dist/schema-editor/lint.d.ts.map +1 -0
- package/dist/schema-editor/lint.js +49 -0
- package/dist/schema-editor/lint.js.map +1 -0
- package/dist/schema-form/SchemaForm.d.ts +70 -0
- package/dist/schema-form/SchemaForm.d.ts.map +1 -0
- package/dist/schema-form/SchemaForm.js +13 -0
- package/dist/schema-form/SchemaForm.js.map +1 -0
- package/dist/schema-form/array-field.d.ts +20 -0
- package/dist/schema-form/array-field.d.ts.map +1 -0
- package/dist/schema-form/array-field.js +39 -0
- package/dist/schema-form/array-field.js.map +1 -0
- package/dist/schema-form/classify.d.ts +87 -0
- package/dist/schema-form/classify.d.ts.map +1 -0
- package/dist/schema-form/classify.js +199 -0
- package/dist/schema-form/classify.js.map +1 -0
- package/dist/schema-form/context.d.ts +4 -0
- package/dist/schema-form/context.d.ts.map +1 -0
- package/dist/schema-form/context.js +14 -0
- package/dist/schema-form/context.js.map +1 -0
- package/dist/schema-form/default-value.d.ts +8 -0
- package/dist/schema-form/default-value.d.ts.map +1 -0
- package/dist/schema-form/default-value.js +64 -0
- package/dist/schema-form/default-value.js.map +1 -0
- package/dist/schema-form/enum-field.d.ts +19 -0
- package/dist/schema-form/enum-field.d.ts.map +1 -0
- package/dist/schema-form/enum-field.js +26 -0
- package/dist/schema-form/enum-field.js.map +1 -0
- package/dist/schema-form/field-group.d.ts +9 -0
- package/dist/schema-form/field-group.d.ts.map +1 -0
- package/dist/schema-form/field-group.js +32 -0
- package/dist/schema-form/field-group.js.map +1 -0
- package/dist/schema-form/field-node.d.ts +24 -0
- package/dist/schema-form/field-node.d.ts.map +1 -0
- package/dist/schema-form/field-node.js +51 -0
- package/dist/schema-form/field-node.js.map +1 -0
- package/dist/schema-form/index.d.ts +13 -0
- package/dist/schema-form/index.d.ts.map +1 -0
- package/dist/schema-form/index.js +11 -0
- package/dist/schema-form/index.js.map +1 -0
- package/dist/schema-form/media-field.d.ts +24 -0
- package/dist/schema-form/media-field.d.ts.map +1 -0
- package/dist/schema-form/media-field.js +169 -0
- package/dist/schema-form/media-field.js.map +1 -0
- package/dist/schema-form/media.d.ts +40 -0
- package/dist/schema-form/media.d.ts.map +1 -0
- package/dist/schema-form/media.js +65 -0
- package/dist/schema-form/media.js.map +1 -0
- package/dist/schema-form/object-fields.d.ts +20 -0
- package/dist/schema-form/object-fields.d.ts.map +1 -0
- package/dist/schema-form/object-fields.js +24 -0
- package/dist/schema-form/object-fields.js.map +1 -0
- package/dist/schema-form/resolve.d.ts +41 -0
- package/dist/schema-form/resolve.d.ts.map +1 -0
- package/dist/schema-form/resolve.js +102 -0
- package/dist/schema-form/resolve.js.map +1 -0
- package/dist/schema-form/string-field.d.ts +20 -0
- package/dist/schema-form/string-field.d.ts.map +1 -0
- package/dist/schema-form/string-field.js +68 -0
- package/dist/schema-form/string-field.js.map +1 -0
- package/dist/schema-form/styles.d.ts +18 -0
- package/dist/schema-form/styles.d.ts.map +1 -0
- package/dist/schema-form/styles.js +18 -0
- package/dist/schema-form/styles.js.map +1 -0
- package/dist/schema-form/types.d.ts +67 -0
- package/dist/schema-form/types.d.ts.map +1 -0
- package/dist/schema-form/types.js +10 -0
- package/dist/schema-form/types.js.map +1 -0
- package/dist/schema-form/union-field.d.ts +24 -0
- package/dist/schema-form/union-field.d.ts.map +1 -0
- package/dist/schema-form/union-field.js +55 -0
- package/dist/schema-form/union-field.js.map +1 -0
- package/dist/schema-form/union.d.ts +4 -0
- package/dist/schema-form/union.d.ts.map +1 -0
- package/dist/schema-form/union.js +68 -0
- package/dist/schema-form/union.js.map +1 -0
- package/dist/schema-form/unsupported-notice.d.ts +18 -0
- package/dist/schema-form/unsupported-notice.d.ts.map +1 -0
- package/dist/schema-form/unsupported-notice.js +8 -0
- package/dist/schema-form/unsupported-notice.js.map +1 -0
- package/dist/schema-form/validate.d.ts +18 -0
- package/dist/schema-form/validate.d.ts.map +1 -0
- package/dist/schema-form/validate.js +162 -0
- package/dist/schema-form/validate.js.map +1 -0
- package/dist/structured-output/StructuredOutput.d.ts +27 -0
- package/dist/structured-output/StructuredOutput.d.ts.map +1 -0
- package/dist/structured-output/StructuredOutput.js +41 -0
- package/dist/structured-output/StructuredOutput.js.map +1 -0
- package/dist/testing.d.ts +80 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +207 -0
- package/dist/testing.js.map +1 -0
- package/package.json +101 -0
- package/src/components/badge.tsx +31 -0
- package/src/components/checkbox.tsx +93 -0
- package/src/components/code-block.tsx +37 -0
- package/src/components/completion-input.tsx +137 -0
- package/src/components/components.css +2212 -0
- package/src/components/confirm-dialog.tsx +77 -0
- package/src/components/control-styles.ts +14 -0
- package/src/components/copy-field.tsx +178 -0
- package/src/components/dialog.tsx +76 -0
- package/src/components/drawer.tsx +83 -0
- package/src/components/error-boundary.tsx +56 -0
- package/src/components/extension-combo-builder.tsx +305 -0
- package/src/components/extension-grouping.ts +116 -0
- package/src/components/extension-picker.tsx +97 -0
- package/src/components/external-link.tsx +40 -0
- package/src/components/field.tsx +169 -0
- package/src/components/fleet-report.tsx +136 -0
- package/src/components/fonts.css +22 -0
- package/src/components/icons.tsx +504 -0
- package/src/components/inputs.tsx +125 -0
- package/src/components/json-diff.tsx +192 -0
- package/src/components/json-tree.tsx +129 -0
- package/src/components/modal-focus.ts +150 -0
- package/src/components/page-header.tsx +107 -0
- package/src/components/primitives.tsx +489 -0
- package/src/components/progress-bar.tsx +100 -0
- package/src/components/radio-group.tsx +160 -0
- package/src/components/reveal-input.tsx +87 -0
- package/src/components/scroll-region.tsx +703 -0
- package/src/components/select.tsx +149 -0
- package/src/components/table.tsx +94 -0
- package/src/components/tabs.tsx +51 -0
- package/src/components/tags.tsx +137 -0
- package/src/components/tokens.css +392 -0
- package/src/components/tokens.ts +102 -0
- package/src/components/tool-picker.tsx +175 -0
- package/src/components/tooltip.tsx +46 -0
- package/src/components/version-history-panel.tsx +407 -0
- package/src/css.d.ts +2 -0
- package/src/download.ts +17 -0
- package/src/element-slot.ts +22 -0
- package/src/elicitation/ElicitationForm.tsx +87 -0
- package/src/errors.ts +12 -0
- package/src/extension-combos.ts +18 -0
- package/src/hooks/useApi.ts +27 -0
- package/src/hooks/useAuth.ts +67 -0
- package/src/hooks/useBreakpoint.ts +78 -0
- package/src/hooks/useCapabilities.ts +242 -0
- package/src/hooks/useSse.ts +314 -0
- package/src/hooks/useTheme.ts +133 -0
- package/src/hooks/useUnauthorized.ts +20 -0
- package/src/host.ts +15 -0
- package/src/index.ts +267 -0
- package/src/navigation/context.tsx +90 -0
- package/src/navigation/index.ts +9 -0
- package/src/navigation/types.ts +90 -0
- package/src/plugin/host-state.ts +93 -0
- package/src/plugin/registry.ts +174 -0
- package/src/plugin/types.ts +178 -0
- package/src/plugin/version.ts +44 -0
- package/src/query-keys.ts +34 -0
- package/src/schema-editor/SchemaEditor.tsx +186 -0
- package/src/schema-editor/index.ts +9 -0
- package/src/schema-editor/lint.ts +70 -0
- package/src/schema-form/SchemaForm.tsx +107 -0
- package/src/schema-form/array-field.tsx +110 -0
- package/src/schema-form/classify.ts +302 -0
- package/src/schema-form/context.ts +17 -0
- package/src/schema-form/default-value.ts +65 -0
- package/src/schema-form/enum-field.tsx +65 -0
- package/src/schema-form/field-group.tsx +71 -0
- package/src/schema-form/field-node.tsx +161 -0
- package/src/schema-form/index.ts +12 -0
- package/src/schema-form/media-field.tsx +281 -0
- package/src/schema-form/media.ts +69 -0
- package/src/schema-form/object-fields.tsx +74 -0
- package/src/schema-form/resolve.ts +114 -0
- package/src/schema-form/string-field.tsx +161 -0
- package/src/schema-form/styles.ts +20 -0
- package/src/schema-form/types.ts +71 -0
- package/src/schema-form/union-field.tsx +122 -0
- package/src/schema-form/union.ts +78 -0
- package/src/schema-form/unsupported-notice.tsx +39 -0
- package/src/schema-form/validate.ts +183 -0
- package/src/structured-output/StructuredOutput.tsx +81 -0
- package/src/testing.ts +235 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validateAgainstSchema` — a loud, structured pre-submit check. It walks the
|
|
3
|
+
* schema against a value and returns a path-keyed `SchemaFormErrors` bag:
|
|
4
|
+
* required-presence, type/format mismatches, enum/const violations, and any
|
|
5
|
+
* `unsupported` construct (surfaced, never hidden). It never throws on a
|
|
6
|
+
* malformed value — a malformed value IS the error it reports.
|
|
7
|
+
*/
|
|
8
|
+
import { classifySchema } from './classify';
|
|
9
|
+
import { decodedByteSize, effectiveMaxBytes, overCapMessage } from './media';
|
|
10
|
+
import { scalarLabel } from './resolve';
|
|
11
|
+
import { activeVariantIndex } from './union';
|
|
12
|
+
import type { JsonSchema, SchemaFormErrors } from './types';
|
|
13
|
+
|
|
14
|
+
/** Options for {@link validateAgainstSchema}. */
|
|
15
|
+
export interface ValidateOptions {
|
|
16
|
+
/**
|
|
17
|
+
* The host's default byte cap for media-upload fields, mirroring the renderer's
|
|
18
|
+
* `maxUploadBytes` prop. A field's own `contentMaxBytes` still overrides it;
|
|
19
|
+
* absent both, the shared default applies.
|
|
20
|
+
*/
|
|
21
|
+
readonly maxUploadBytes?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
25
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
26
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
27
|
+
|
|
28
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function joinPath(path: string, key: string): string {
|
|
33
|
+
return path === '' ? key : `${path}.${key}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatError(format: string, value: string): string | undefined {
|
|
37
|
+
switch (format) {
|
|
38
|
+
case 'email':
|
|
39
|
+
return EMAIL_RE.test(value) ? undefined : 'must be a valid email address';
|
|
40
|
+
case 'uri':
|
|
41
|
+
case 'uri-reference':
|
|
42
|
+
try {
|
|
43
|
+
new URL(value, 'https://base.invalid');
|
|
44
|
+
return undefined;
|
|
45
|
+
} catch {
|
|
46
|
+
return 'must be a valid URI';
|
|
47
|
+
}
|
|
48
|
+
case 'uuid':
|
|
49
|
+
return UUID_RE.test(value) ? undefined : 'must be a valid UUID';
|
|
50
|
+
case 'date':
|
|
51
|
+
return DATE_RE.test(value) && !Number.isNaN(Date.parse(value))
|
|
52
|
+
? undefined
|
|
53
|
+
: 'must be a valid date (YYYY-MM-DD)';
|
|
54
|
+
case 'date-time':
|
|
55
|
+
return Number.isNaN(Date.parse(value)) ? 'must be a valid date-time' : undefined;
|
|
56
|
+
default:
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walk(
|
|
62
|
+
schema: JsonSchema,
|
|
63
|
+
value: unknown,
|
|
64
|
+
path: string,
|
|
65
|
+
root: JsonSchema,
|
|
66
|
+
errors: Record<string, string>,
|
|
67
|
+
maxUploadBytes: number | undefined,
|
|
68
|
+
): void {
|
|
69
|
+
const classified = classifySchema(schema, root);
|
|
70
|
+
|
|
71
|
+
if (value === null) {
|
|
72
|
+
if (!classified.nullable) errors[path] = 'must not be null';
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { model } = classified;
|
|
77
|
+
switch (model.kind) {
|
|
78
|
+
case 'unsupported':
|
|
79
|
+
errors[path] = `unsupported schema (${model.reason})`;
|
|
80
|
+
return;
|
|
81
|
+
case 'const':
|
|
82
|
+
if (value !== model.value) errors[path] = `must equal ${scalarLabel(model.value)}`;
|
|
83
|
+
return;
|
|
84
|
+
case 'enum': {
|
|
85
|
+
const allowed = model.options.some((option) => option.value === value);
|
|
86
|
+
if (!allowed) errors[path] = 'must be one of the allowed values';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case 'string': {
|
|
90
|
+
if (typeof value !== 'string') {
|
|
91
|
+
errors[path] = 'must be a string';
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// A media-annotated string carries a byte cap; enforce it on the value's
|
|
95
|
+
// DECODED size so a value that arrived by any route is caught here too. The
|
|
96
|
+
// precedence matches the renderer: field `contentMaxBytes` → host cap →
|
|
97
|
+
// shared default.
|
|
98
|
+
if (model.media !== undefined) {
|
|
99
|
+
const cap = effectiveMaxBytes(model.media.maxBytes, maxUploadBytes);
|
|
100
|
+
const size = decodedByteSize(value);
|
|
101
|
+
if (size > cap) {
|
|
102
|
+
errors[path] = overCapMessage('The value', size, cap);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (value.length > 0 && model.format !== undefined) {
|
|
107
|
+
const message = formatError(model.format, value);
|
|
108
|
+
if (message !== undefined) errors[path] = message;
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
case 'number': {
|
|
113
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
114
|
+
errors[path] = model.integer ? 'must be an integer' : 'must be a number';
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (model.integer && !Number.isInteger(value)) errors[path] = 'must be an integer';
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
case 'boolean':
|
|
121
|
+
if (typeof value !== 'boolean') errors[path] = 'must be true or false';
|
|
122
|
+
return;
|
|
123
|
+
case 'array': {
|
|
124
|
+
if (!Array.isArray(value)) {
|
|
125
|
+
errors[path] = 'must be an array';
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
value.forEach((item, index) => {
|
|
129
|
+
walk(model.items, item, `${path}[${String(index)}]`, root, errors, maxUploadBytes);
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case 'object': {
|
|
134
|
+
if (!isPlainObject(value)) {
|
|
135
|
+
errors[path] = 'must be an object';
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const propMap = new Map(model.properties);
|
|
139
|
+
for (const [name, propSchema] of model.properties) {
|
|
140
|
+
const childPath = joinPath(path, name);
|
|
141
|
+
const childValue = value[name];
|
|
142
|
+
if (childValue === undefined) {
|
|
143
|
+
if (model.required.has(name)) errors[childPath] = `"${name}" is required`;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
walk(propSchema, childValue, childPath, root, errors, maxUploadBytes);
|
|
147
|
+
}
|
|
148
|
+
// Required keys naming a property that somehow isn't declared still count.
|
|
149
|
+
for (const name of model.required) {
|
|
150
|
+
if (!propMap.has(name) && value[name] === undefined) {
|
|
151
|
+
errors[joinPath(path, name)] = `"${name}" is required`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
case 'union': {
|
|
157
|
+
const index = activeVariantIndex(value, model.variants, model.discriminator, root);
|
|
158
|
+
const variant = index === -1 ? undefined : model.variants[index];
|
|
159
|
+
if (variant === undefined) {
|
|
160
|
+
errors[path] = 'does not match any allowed variant';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
walk(variant.schema, value, path, root, errors, maxUploadBytes);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate `value` against `schema`, returning a per-path error bag (empty =
|
|
171
|
+
* valid). The caller runs this before submit and feeds the result back to
|
|
172
|
+
* `SchemaForm` for display. `options.maxUploadBytes` mirrors the renderer's
|
|
173
|
+
* `maxUploadBytes` prop so a media field's byte cap is enforced identically here.
|
|
174
|
+
*/
|
|
175
|
+
export function validateAgainstSchema(
|
|
176
|
+
schema: JsonSchema,
|
|
177
|
+
value: unknown,
|
|
178
|
+
options?: ValidateOptions,
|
|
179
|
+
): SchemaFormErrors {
|
|
180
|
+
const errors: Record<string, string> = {};
|
|
181
|
+
walk(schema, value, '', schema, errors, options?.maxUploadBytes);
|
|
182
|
+
return errors;
|
|
183
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StructuredOutput` — the READ-ONLY output side of the schema-driven system:
|
|
3
|
+
* it renders a tool's `structured_content` THROUGH its declared MCP
|
|
4
|
+
* `output_schema` (property titles + declared order), and falls back to a raw
|
|
5
|
+
* `JsonTree` when the tool declares no output schema (or the content does not
|
|
6
|
+
* match an object schema).
|
|
7
|
+
*
|
|
8
|
+
* Reusable by any feature/plugin run surface. SAFETY: every schema-derived and
|
|
9
|
+
* content-derived value renders as React TEXT (through `JsonTree` / plain text
|
|
10
|
+
* nodes), so a payload carrying markup is escaped, never interpreted.
|
|
11
|
+
*/
|
|
12
|
+
import type { ReactNode } from 'react';
|
|
13
|
+
|
|
14
|
+
import { JsonTree } from '../components/json-tree';
|
|
15
|
+
import { resolveRef } from '../schema-form/resolve';
|
|
16
|
+
import type { JsonSchema } from '../schema-form/types';
|
|
17
|
+
|
|
18
|
+
export interface StructuredOutputProps {
|
|
19
|
+
/** The tool's declared MCP output schema, if any. */
|
|
20
|
+
readonly schema?: JsonSchema;
|
|
21
|
+
/** The `structured_content` the tool returned. */
|
|
22
|
+
readonly content: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The pane's name when the payload has no schema to name it by. */
|
|
26
|
+
const RAW_OUTPUT_LABEL = 'Structured output';
|
|
27
|
+
|
|
28
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
29
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isObjectSchema(
|
|
33
|
+
schema: JsonSchema | undefined,
|
|
34
|
+
root: JsonSchema | undefined,
|
|
35
|
+
): schema is JsonSchema {
|
|
36
|
+
if (!schema || !root) return false;
|
|
37
|
+
const resolved = schema.$ref ? resolveRef(schema, root) : schema;
|
|
38
|
+
const type = resolved.type;
|
|
39
|
+
const isObject = type === 'object' || (Array.isArray(type) && type.includes('object'));
|
|
40
|
+
return isObject && isRecord(resolved.properties);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Render `content` through `schema`. An object schema drives a labeled,
|
|
45
|
+
* ordered field list (each value shown with a `JsonTree`); anything else — no
|
|
46
|
+
* schema, a non-object schema, or content that is not a matching object —
|
|
47
|
+
* renders the raw content through a single `JsonTree`.
|
|
48
|
+
*/
|
|
49
|
+
export function StructuredOutput({ schema, content }: StructuredOutputProps): ReactNode {
|
|
50
|
+
if (!isObjectSchema(schema, schema) || !isRecord(content)) {
|
|
51
|
+
return (
|
|
52
|
+
// A deeply nested payload outruns its column; the tree's own pane scrolls
|
|
53
|
+
// it and is a keyboard target only while it actually does.
|
|
54
|
+
<div data-testid="structured-output-raw">
|
|
55
|
+
<JsonTree data={content} defaultExpanded label={RAW_OUTPUT_LABEL} />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// `schema` is narrowed to a defined object schema here.
|
|
61
|
+
const resolved = schema.$ref ? resolveRef(schema, schema) : schema;
|
|
62
|
+
// `isObjectSchema` guarantees `properties` is a record; the fallback only
|
|
63
|
+
// satisfies the optional type.
|
|
64
|
+
const properties = resolved.properties ?? {};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className="tai-stack tai-stack-2" data-testid="structured-output">
|
|
68
|
+
{Object.entries(properties).map(([key, propSchema]) => {
|
|
69
|
+
const field = propSchema.$ref ? resolveRef(propSchema, schema) : propSchema;
|
|
70
|
+
const name = field.title ?? key;
|
|
71
|
+
return (
|
|
72
|
+
<div key={key} className="tai-field">
|
|
73
|
+
<span className="tai-field-label">{name}</span>
|
|
74
|
+
{/* A wide value scrolls inside its own pane rather than widening the row. */}
|
|
75
|
+
<JsonTree data={content[key]} defaultExpanded label={name} />
|
|
76
|
+
</div>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/** Test-only registry reset, used to clear module-level state between cases; never shipped to the browser. */
|
|
2
|
+
export { __resetContributions } from './plugin/registry';
|
|
3
|
+
/** Test-only plugin host-state reset, cleared between cases beside the registry reset. */
|
|
4
|
+
export { __resetPluginHostState } from './plugin/host-state';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Every `ResizeObserver` the stub has handed out and not yet had disconnected,
|
|
8
|
+
* with the elements each one is watching. `flushResizeObservers` drives the
|
|
9
|
+
* callbacks from here.
|
|
10
|
+
*/
|
|
11
|
+
interface LiveResizeObserver {
|
|
12
|
+
readonly callback: ResizeObserverCallback;
|
|
13
|
+
readonly targets: Set<Element>;
|
|
14
|
+
readonly observer: ResizeObserver;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const liveResizeObservers = new Set<LiveResizeObserver>();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A full `ResizeObserverEntry` shape for `target`. Radix's `useSize` reads
|
|
21
|
+
* `borderBoxSize[0].inlineSize`, so a `target`-only entry would throw there;
|
|
22
|
+
* jsdom reports zero geometry, which is exactly what a real headless observer
|
|
23
|
+
* would report for an unlaid-out element.
|
|
24
|
+
*/
|
|
25
|
+
function resizeObserverEntry(target: Element): ResizeObserverEntry {
|
|
26
|
+
const rect = target.getBoundingClientRect();
|
|
27
|
+
const size: ResizeObserverSize = { inlineSize: rect.width, blockSize: rect.height };
|
|
28
|
+
return {
|
|
29
|
+
target,
|
|
30
|
+
contentRect: rect,
|
|
31
|
+
borderBoxSize: [size],
|
|
32
|
+
contentBoxSize: [size],
|
|
33
|
+
devicePixelContentBoxSize: [size],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Fires every live `ResizeObserver` stub's callback for the elements it is
|
|
39
|
+
* observing. Pair it with `setElementOverflow` to drive a component that
|
|
40
|
+
* measures itself (`ScrollRegion` and `useProseScrollRegions` re-read
|
|
41
|
+
* `scrollWidth`/`clientWidth` from their observer callbacks):
|
|
42
|
+
*
|
|
43
|
+
* setElementOverflow(region, true);
|
|
44
|
+
* flushResizeObservers();
|
|
45
|
+
*
|
|
46
|
+
* Only meaningful once `installJsdomStubs` has installed the stub; with a real
|
|
47
|
+
* `ResizeObserver` present (a browser environment) there is nothing to flush.
|
|
48
|
+
*/
|
|
49
|
+
export function flushResizeObservers(): void {
|
|
50
|
+
for (const live of [...liveResizeObservers]) {
|
|
51
|
+
if (live.targets.size === 0) continue;
|
|
52
|
+
live.callback([...live.targets].map(resizeObserverEntry), live.observer);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Delivers a resize for ONE element, to every stub observing it — the narrow
|
|
58
|
+
* form of {@link flushResizeObservers}. Use it to prove which element a
|
|
59
|
+
* component is watching: a measurement driven by a resize of a CHILD only
|
|
60
|
+
* arrives if that child is observed, so the assertion fails the moment a
|
|
61
|
+
* component narrows its observation to its own box.
|
|
62
|
+
*
|
|
63
|
+
* @param target - the element whose resize to deliver.
|
|
64
|
+
*/
|
|
65
|
+
export function flushResizeObserversFor(target: Element): void {
|
|
66
|
+
for (const live of [...liveResizeObservers]) {
|
|
67
|
+
if (!live.targets.has(target)) continue;
|
|
68
|
+
live.callback([resizeObserverEntry(target)], live.observer);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The faked widths: equal reads as "fits", unequal as "scrolls". */
|
|
73
|
+
const FITTING_WIDTH = 100;
|
|
74
|
+
const OVERFLOWING_SCROLL_WIDTH = 400;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Makes `element` report itself as horizontally overflowing (or not). jsdom runs
|
|
78
|
+
* no layout, so `scrollWidth` and `clientWidth` are both 0 and every
|
|
79
|
+
* overflow test reads as "fits"; this shadows the prototype getters with own,
|
|
80
|
+
* configurable values so a test can drive either state. Call it again with the
|
|
81
|
+
* opposite flag to flip the element back.
|
|
82
|
+
*
|
|
83
|
+
* @param element - the element whose scroll metrics to fake.
|
|
84
|
+
* @param overflowing - true to report content wider than the box.
|
|
85
|
+
*/
|
|
86
|
+
export function setElementOverflow(element: HTMLElement, overflowing: boolean): void {
|
|
87
|
+
Object.defineProperty(element, 'clientWidth', { configurable: true, value: FITTING_WIDTH });
|
|
88
|
+
Object.defineProperty(element, 'scrollWidth', {
|
|
89
|
+
configurable: true,
|
|
90
|
+
value: overflowing ? OVERFLOWING_SCROLL_WIDTH : FITTING_WIDTH,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* jsdom omits a handful of browser APIs the Studio components reach for while a
|
|
96
|
+
* test drives them. Installing every stub in one place keeps each feature's
|
|
97
|
+
* `test-setup.ts` identical and lets interaction tests exercise the real
|
|
98
|
+
* components instead of mocking them out:
|
|
99
|
+
*
|
|
100
|
+
* - Radix primitives observe element size (`ResizeObserver`), scroll the active
|
|
101
|
+
* item into view, and capture the pointer while opening.
|
|
102
|
+
* - Export/download paths create and revoke object URLs.
|
|
103
|
+
* - File-import flows read the picked file via `Blob.text()`.
|
|
104
|
+
* - Router scroll restoration calls `scrollTo` on navigation.
|
|
105
|
+
*
|
|
106
|
+
* The `ResizeObserver` stub is a WORKING observer, not an inert one: it keeps a
|
|
107
|
+
* registry of the observers still in use, delivers an entry to the callback as
|
|
108
|
+
* each element is observed (as a real observer does for its initial
|
|
109
|
+
* observation), and lets a test re-deliver on demand via
|
|
110
|
+
* `flushResizeObservers`. Components that size themselves — `ScrollRegion` and
|
|
111
|
+
* `useProseScrollRegions` — are therefore exercised for real, with
|
|
112
|
+
* `setElementOverflow` supplying the scroll metrics jsdom cannot compute.
|
|
113
|
+
*
|
|
114
|
+
* Each stub only fills a MISSING API, so installing the whole set everywhere is
|
|
115
|
+
* harmless — the one exception is `window.scrollTo`, which jsdom ships as a "Not
|
|
116
|
+
* implemented" stub that logs loudly on every navigation, so it is replaced
|
|
117
|
+
* unconditionally. Call once from a package's Vitest `setupFiles` entry.
|
|
118
|
+
*/
|
|
119
|
+
export function installJsdomStubs(): void {
|
|
120
|
+
if (typeof globalThis.ResizeObserver !== 'function') {
|
|
121
|
+
class ResizeObserverStub implements ResizeObserver {
|
|
122
|
+
readonly #live: LiveResizeObserver;
|
|
123
|
+
|
|
124
|
+
constructor(callback: ResizeObserverCallback) {
|
|
125
|
+
this.#live = { callback, targets: new Set<Element>(), observer: this };
|
|
126
|
+
liveResizeObservers.add(this.#live);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
observe(target: Element): void {
|
|
130
|
+
// `disconnect()` stops observation; it does not destroy the observer, so
|
|
131
|
+
// observing again re-registers it exactly as the real API does.
|
|
132
|
+
liveResizeObservers.add(this.#live);
|
|
133
|
+
this.#live.targets.add(target);
|
|
134
|
+
this.#live.callback([resizeObserverEntry(target)], this);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
unobserve(target: Element): void {
|
|
138
|
+
this.#live.targets.delete(target);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
disconnect(): void {
|
|
142
|
+
this.#live.targets.clear();
|
|
143
|
+
liveResizeObservers.delete(this.#live);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
globalThis.ResizeObserver = ResizeObserverStub;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (typeof Element.prototype.scrollIntoView !== 'function') {
|
|
150
|
+
Element.prototype.scrollIntoView = function scrollIntoView(): void {
|
|
151
|
+
/* jsdom stub */
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (typeof Element.prototype.hasPointerCapture !== 'function') {
|
|
155
|
+
Element.prototype.hasPointerCapture = function hasPointerCapture(): boolean {
|
|
156
|
+
return false;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (typeof Element.prototype.setPointerCapture !== 'function') {
|
|
160
|
+
Element.prototype.setPointerCapture = function setPointerCapture(): void {
|
|
161
|
+
/* jsdom stub */
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (typeof Element.prototype.releasePointerCapture !== 'function') {
|
|
165
|
+
Element.prototype.releasePointerCapture = function releasePointerCapture(): void {
|
|
166
|
+
/* jsdom stub */
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// TanStack Router's scroll restoration calls scrollTo on every navigation.
|
|
171
|
+
// jsdom omits `Element.prototype.scrollTo`, so fill it like the others; but it
|
|
172
|
+
// DOES ship `window.scrollTo` as a "Not implemented" stub that logs loudly on
|
|
173
|
+
// every call, so that one is REPLACED unconditionally to silence the noise.
|
|
174
|
+
if (typeof Element.prototype.scrollTo !== 'function') {
|
|
175
|
+
Element.prototype.scrollTo = function scrollTo(): void {
|
|
176
|
+
/* jsdom stub */
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
window.scrollTo = function scrollTo(): void {
|
|
180
|
+
/* jsdom stub */
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// jsdom does not implement object URLs; export/download paths create and
|
|
184
|
+
// revoke one.
|
|
185
|
+
if (typeof URL.createObjectURL !== 'function') {
|
|
186
|
+
URL.createObjectURL = (): string => 'blob:stub';
|
|
187
|
+
}
|
|
188
|
+
if (typeof URL.revokeObjectURL !== 'function') {
|
|
189
|
+
URL.revokeObjectURL = (): void => {
|
|
190
|
+
/* jsdom stub */
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// jsdom's Blob/File omit the `text()` reader browsers ship, so file-import
|
|
195
|
+
// flows that call `file.text()` cannot run. Back it with the FileReader jsdom
|
|
196
|
+
// does provide.
|
|
197
|
+
if (typeof Blob.prototype.text !== 'function') {
|
|
198
|
+
Blob.prototype.text = function readAsText(this: Blob): Promise<string> {
|
|
199
|
+
return new Promise<string>((resolve, reject) => {
|
|
200
|
+
const reader = new FileReader();
|
|
201
|
+
reader.onload = (): void => {
|
|
202
|
+
resolve(typeof reader.result === 'string' ? reader.result : '');
|
|
203
|
+
};
|
|
204
|
+
reader.onerror = (): void => {
|
|
205
|
+
reject(reader.error ?? new Error('FileReader failed'));
|
|
206
|
+
};
|
|
207
|
+
reader.readAsText(this);
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* A promise whose settlement the test controls, returned alongside its own
|
|
215
|
+
* `resolve`/`reject`. It pins a component's loading state for assertion: hand the
|
|
216
|
+
* unsettled `promise` to the code under test (e.g. as a mocked api call's return
|
|
217
|
+
* value), assert the in-flight UI while it hangs, then call `resolve`/`reject` to
|
|
218
|
+
* drive the success or failure branch and assert the settled UI. Each call
|
|
219
|
+
* returns a fresh, independent deferred.
|
|
220
|
+
*
|
|
221
|
+
* @typeParam T - the value the promise resolves to.
|
|
222
|
+
*/
|
|
223
|
+
export function deferred<T>(): {
|
|
224
|
+
promise: Promise<T>;
|
|
225
|
+
resolve: (value: T) => void;
|
|
226
|
+
reject: (reason: unknown) => void;
|
|
227
|
+
} {
|
|
228
|
+
let resolve!: (value: T) => void;
|
|
229
|
+
let reject!: (reason: unknown) => void;
|
|
230
|
+
const promise = new Promise<T>((res, rej) => {
|
|
231
|
+
resolve = res;
|
|
232
|
+
reject = rej;
|
|
233
|
+
});
|
|
234
|
+
return { promise, resolve, reject };
|
|
235
|
+
}
|