@semiont/react-ui 0.5.27 → 0.5.29
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/README.md +3 -3
- package/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-LSH3PZPY.js} +32 -5
- package/dist/{ar-OU3DQGC6.js → ar-AZBRADEB.js} +43 -23
- package/dist/{bn-PHWCR6AZ.js → bn-CCEW6AOS.js} +43 -23
- package/dist/{chunk-VXASXU4K.js → chunk-3ZBKCWUZ.js} +1 -1
- package/dist/{chunk-5VDTWRXJ.js → chunk-G4VHL4UA.js} +31 -31
- package/dist/{chunk-PCRRBU7H.js → chunk-SDKFZLEX.js} +43 -23
- package/dist/{cs-NEXYZ62I.js → cs-HXPF2CTE.js} +43 -23
- package/dist/{da-FAT43YCU.js → da-K5NTK7LH.js} +43 -23
- package/dist/{de-KRQ3WBCU.js → de-GMRQEDZI.js} +43 -23
- package/dist/{el-Q4IU3EKC.js → el-EUB4CI4G.js} +43 -23
- package/dist/{en-2KADE7N4.js → en-R5XA26NF.js} +2 -2
- package/dist/{es-2IO3R2MK.js → es-Z32DAZ6W.js} +43 -23
- package/dist/{fa-Q5GOQFXX.js → fa-G7HN6B32.js} +43 -23
- package/dist/{fi-R6NMRWBE.js → fi-3YBLZO7I.js} +43 -23
- package/dist/{fr-QY4D5E6J.js → fr-VUBH7BBS.js} +43 -23
- package/dist/{he-X6Y77XHY.js → he-WQQEY26B.js} +43 -23
- package/dist/{hi-XL7IOEQM.js → hi-HJZ6W4JB.js} +43 -23
- package/dist/{id-QZ4ZZS67.js → id-ZOGZ37MI.js} +43 -23
- package/dist/index.css +279 -74
- package/dist/index.d.ts +318 -393
- package/dist/index.js +2619 -2687
- package/dist/{it-ME2Q7OB6.js → it-DT6UW66F.js} +43 -23
- package/dist/{ja-HX7QASRI.js → ja-RUKQCOLO.js} +43 -23
- package/dist/{ko-EQO5AHAV.js → ko-ZH5ADKZM.js} +43 -23
- package/dist/{ms-UILFSF3I.js → ms-Y2AFPZTU.js} +43 -23
- package/dist/{nl-NNI45PMK.js → nl-OQEWIXNA.js} +43 -23
- package/dist/{no-MAL5F6P6.js → no-KPAYJZWY.js} +43 -23
- package/dist/{pl-4RMY6JAJ.js → pl-WSXK2UUD.js} +43 -23
- package/dist/{pt-VQH2X3QQ.js → pt-NWRJRUCK.js} +43 -23
- package/dist/{ro-XXCQO2OG.js → ro-3CGVMOC6.js} +43 -23
- package/dist/{sv-7Z6EJ6U4.js → sv-HGSEBA7S.js} +43 -23
- package/dist/test-utils.js +16 -7
- package/dist/{th-L3DXXYVQ.js → th-NZ4G352Z.js} +43 -23
- package/dist/{tr-P3DFVLFJ.js → tr-LGGS4DLW.js} +43 -23
- package/dist/{uk-7C5D3PXS.js → uk-AIX536L4.js} +43 -23
- package/dist/{vi-ZLTLEO7P.js → vi-V6CVQZLQ.js} +43 -23
- package/dist/{zh-5FHVWDIN.js → zh-AGKPGI3L.js} +43 -23
- package/package.json +16 -15
- package/src/components/AssistProgress.tsx +36 -2
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/StatusDisplay.tsx +13 -13
- package/src/components/__tests__/AssistProgress.test.tsx +59 -0
- package/src/components/__tests__/StatusDisplay.test.tsx +9 -9
- package/src/components/modals/ComposeStep.tsx +233 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +178 -34
- package/src/components/modals/ConfigureSearchStep.tsx +19 -0
- package/src/components/modals/ContextSummary.tsx +185 -72
- package/src/components/modals/CorpusPane.tsx +75 -0
- package/src/components/modals/DiscardPrompt.tsx +38 -0
- package/src/components/modals/GatherContextStep.tsx +237 -107
- package/src/components/modals/ReferenceWizardModal.tsx +252 -59
- package/src/components/modals/ResourceGenerateModal.tsx +225 -112
- package/src/components/modals/SearchModal.css +337 -101
- package/src/components/modals/SearchResultsStep.tsx +61 -68
- package/src/components/modals/WizardFooter.tsx +7 -1
- package/src/components/modals/__tests__/ComposeStep.test.tsx +152 -0
- package/src/components/modals/__tests__/ConfigureGatherStep.test.tsx +14 -26
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +206 -8
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +24 -1
- package/src/components/modals/__tests__/GatherContextStep.resource.test.tsx +463 -25
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +442 -39
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +300 -86
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +116 -9
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +34 -1
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +38 -1
- package/src/components/resource/AnnotateView.tsx +3 -3
- package/src/components/resource/BrowseView.tsx +8 -8
- package/src/components/resource/DownloadFileLink.tsx +1 -1
- package/src/components/resource/ResourceViewer.tsx +4 -4
- package/src/components/resource/__tests__/AnnotateView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.embeddable.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.reference-hover.test.tsx +1 -1
- package/src/components/resource/__tests__/BrowseView.test.tsx +11 -11
- package/src/components/resource/__tests__/download-link.test.tsx +6 -6
- package/src/components/resource/annotate-renderers.tsx +3 -3
- package/src/components/resource/panels/AssistSection.css +35 -0
- package/src/components/resource/panels/ReferenceEntry.tsx +11 -4
- package/src/components/resource/panels/ReferencesPanel.tsx +9 -1
- package/src/components/resource/panels/ResourceInfoPanel.tsx +89 -20
- package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +7 -2
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +41 -0
- package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +84 -3
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +149 -7
- package/src/features/auth/__tests__/SignInForm.a11y.test.tsx +12 -12
- package/src/features/auth/components/SignInForm.tsx +53 -53
- package/src/features/auth-welcome/state/__tests__/welcome-state-unit.test.ts +2 -2
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +35 -133
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +1 -100
- package/src/features/resource-compose/state/compose-page-state-unit.ts +5 -57
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +1 -1
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +2 -2
- package/src/features/resource-discovery/state/discover-state-unit.ts +1 -1
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +73 -17
- package/src/features/resource-viewer/__tests__/generation-options.test.ts +53 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +148 -83
- package/src/features/resource-viewer/generation-options.ts +29 -0
- package/src/styles/core/forms.css +35 -8
- package/src/styles/features/resource-viewer.css +0 -22
- package/src/styles/features/resource.css +0 -4
- package/src/styles/index.css +0 -1
- package/translations/ar.json +42 -22
- package/translations/bn.json +42 -22
- package/translations/cs.json +42 -22
- package/translations/da.json +42 -22
- package/translations/de.json +42 -22
- package/translations/el.json +42 -22
- package/translations/en.json +42 -22
- package/translations/es.json +42 -22
- package/translations/fa.json +42 -22
- package/translations/fi.json +42 -22
- package/translations/fr.json +42 -22
- package/translations/he.json +42 -22
- package/translations/hi.json +42 -22
- package/translations/id.json +42 -22
- package/translations/it.json +42 -22
- package/translations/ja.json +42 -22
- package/translations/ko.json +42 -22
- package/translations/ms.json +42 -22
- package/translations/nl.json +42 -22
- package/translations/no.json +42 -22
- package/translations/pl.json +42 -22
- package/translations/pt.json +42 -22
- package/translations/ro.json +42 -22
- package/translations/sv.json +42 -22
- package/translations/th.json +42 -22
- package/translations/tr.json +42 -22
- package/translations/uk.json +42 -22
- package/translations/vi.json +42 -22
- package/translations/zh.json +42 -22
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +0 -1
- package/dist/ar-OU3DQGC6.js.map +0 -1
- package/dist/bn-PHWCR6AZ.js.map +0 -1
- package/dist/chunk-5VDTWRXJ.js.map +0 -1
- package/dist/chunk-BTUG3VJJ.js.map +0 -1
- package/dist/chunk-O23TLLXW.js.map +0 -1
- package/dist/chunk-PCRRBU7H.js.map +0 -1
- package/dist/chunk-QT7LYM72.js.map +0 -1
- package/dist/chunk-VXASXU4K.js.map +0 -1
- package/dist/cs-NEXYZ62I.js.map +0 -1
- package/dist/da-FAT43YCU.js.map +0 -1
- package/dist/de-KRQ3WBCU.js.map +0 -1
- package/dist/el-Q4IU3EKC.js.map +0 -1
- package/dist/en-2KADE7N4.js.map +0 -1
- package/dist/es-2IO3R2MK.js.map +0 -1
- package/dist/fa-Q5GOQFXX.js.map +0 -1
- package/dist/fi-R6NMRWBE.js.map +0 -1
- package/dist/fr-QY4D5E6J.js.map +0 -1
- package/dist/he-X6Y77XHY.js.map +0 -1
- package/dist/hi-XL7IOEQM.js.map +0 -1
- package/dist/id-QZ4ZZS67.js.map +0 -1
- package/dist/index.css.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/css-modules-helper.js.map +0 -1
- package/dist/integrations/styled-components-theme.js.map +0 -1
- package/dist/it-ME2Q7OB6.js.map +0 -1
- package/dist/ja-HX7QASRI.js.map +0 -1
- package/dist/ko-EQO5AHAV.js.map +0 -1
- package/dist/ms-UILFSF3I.js.map +0 -1
- package/dist/nl-NNI45PMK.js.map +0 -1
- package/dist/no-MAL5F6P6.js.map +0 -1
- package/dist/pl-4RMY6JAJ.js.map +0 -1
- package/dist/pt-VQH2X3QQ.js.map +0 -1
- package/dist/ro-XXCQO2OG.js.map +0 -1
- package/dist/sv-7Z6EJ6U4.js.map +0 -1
- package/dist/test-utils.css.map +0 -1
- package/dist/test-utils.js.map +0 -1
- package/dist/th-L3DXXYVQ.js.map +0 -1
- package/dist/tr-P3DFVLFJ.js.map +0 -1
- package/dist/uk-7C5D3PXS.js.map +0 -1
- package/dist/vi-ZLTLEO7P.js.map +0 -1
- package/dist/zh-5FHVWDIN.js.map +0 -1
- package/src/components/modals/ResourceSearchModal.tsx +0 -224
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +0 -163
- package/src/features/admin-exchange/__tests__/AdminExchangePage.test.tsx +0 -137
- package/src/features/admin-exchange/__tests__/ExportCard.test.tsx +0 -41
- package/src/features/admin-exchange/__tests__/ImportCard.test.tsx +0 -148
- package/src/features/admin-exchange/__tests__/ImportProgress.test.tsx +0 -117
- package/src/features/admin-exchange/components/AdminExchangePage.tsx +0 -117
- package/src/features/admin-exchange/components/ExportCard.tsx +0 -35
- package/src/features/admin-exchange/components/ImportCard.tsx +0 -183
- package/src/features/admin-exchange/components/ImportProgress.tsx +0 -80
- package/src/features/admin-exchange/state/__tests__/exchange-state-unit.test.ts +0 -185
- package/src/features/admin-exchange/state/exchange-state-unit.ts +0 -133
- package/src/features/moderation-linked-data/__tests__/LinkedDataPage.test.tsx +0 -118
- package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +0 -118
- package/src/styles/features/exchange.css +0 -374
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPOSE-IN-MODAL P1 — the compose strategy's form, as a pure step.
|
|
3
|
+
*
|
|
4
|
+
* The from-reference slice of the compose page, lifted: name, save location,
|
|
5
|
+
* entity types (read-only tags when the reference fixed them, picker
|
|
6
|
+
* otherwise), language, and the editor. Deliberately NONE of the page's
|
|
7
|
+
* upload/format/encoding machinery — the fences reference mode already
|
|
8
|
+
* proved are here as absence pins. The draft is CONTROLLED by the host
|
|
9
|
+
* (WIZARD-NAVIGATION D3: stepping Back must not discard typed work), and
|
|
10
|
+
* the evidence display is the host's job (plan A3: no context redux here).
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
13
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
14
|
+
import '@testing-library/jest-dom';
|
|
15
|
+
import { ComposeStep, type ComposeDraft } from '../ComposeStep';
|
|
16
|
+
|
|
17
|
+
// Mock CodeMirrorRenderer to avoid CodeMirror dependencies (same mock the
|
|
18
|
+
// compose page's own tests use).
|
|
19
|
+
vi.mock('../../CodeMirrorRenderer', () => ({
|
|
20
|
+
CodeMirrorRenderer: ({ content, onChange, editable }: any) => (
|
|
21
|
+
<textarea
|
|
22
|
+
data-testid="code-editor"
|
|
23
|
+
value={content}
|
|
24
|
+
disabled={!editable}
|
|
25
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
26
|
+
/>
|
|
27
|
+
),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const T = {
|
|
31
|
+
resourceTitle: 'New resource title',
|
|
32
|
+
resourceTitlePlaceholder: 'Title…',
|
|
33
|
+
saveLocation: 'Save location',
|
|
34
|
+
entityTypes: 'Entity types',
|
|
35
|
+
language: 'Language',
|
|
36
|
+
contentLabel: 'Content',
|
|
37
|
+
back: 'Back',
|
|
38
|
+
createAndLink: 'Create & Link',
|
|
39
|
+
creatingAndLinking: 'Creating…',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const DRAFT: ComposeDraft = {
|
|
43
|
+
name: 'Black Hawk',
|
|
44
|
+
storagePath: 'people/black-hawk.md',
|
|
45
|
+
content: 'Sauk leader.',
|
|
46
|
+
entityTypes: [],
|
|
47
|
+
language: 'en',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function renderStep(over: Partial<React.ComponentProps<typeof ComposeStep>> = {}) {
|
|
51
|
+
const onDraftChange = vi.fn();
|
|
52
|
+
const onBack = vi.fn();
|
|
53
|
+
const onCompose = vi.fn<(p: unknown) => Promise<void>>().mockResolvedValue(undefined);
|
|
54
|
+
const utils = render(
|
|
55
|
+
<ComposeStep
|
|
56
|
+
draft={DRAFT}
|
|
57
|
+
onDraftChange={onDraftChange}
|
|
58
|
+
referenceEntityTypes={['Person']}
|
|
59
|
+
entityTypeOptions={['Person', 'Topic', 'Location']}
|
|
60
|
+
showLineNumbers={false}
|
|
61
|
+
hoverDelayMs={300}
|
|
62
|
+
onBack={onBack}
|
|
63
|
+
onCompose={onCompose}
|
|
64
|
+
translations={T}
|
|
65
|
+
{...over}
|
|
66
|
+
/>,
|
|
67
|
+
);
|
|
68
|
+
return { ...utils, onDraftChange, onBack, onCompose };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('ComposeStep — the from-reference slice, and nothing else', () => {
|
|
72
|
+
it('renders the draft: name, save location, content, language', () => {
|
|
73
|
+
renderStep();
|
|
74
|
+
expect(screen.getByLabelText(T.resourceTitle)).toHaveValue('Black Hawk');
|
|
75
|
+
expect(screen.getByLabelText(T.saveLocation)).toHaveValue('people/black-hawk.md');
|
|
76
|
+
expect(screen.getByTestId('code-editor')).toHaveValue('Sauk leader.');
|
|
77
|
+
expect(screen.getByLabelText(T.language)).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('edits flow through onDraftChange — the host owns the draft (D3)', () => {
|
|
81
|
+
const { onDraftChange } = renderStep();
|
|
82
|
+
fireEvent.change(screen.getByLabelText(T.resourceTitle), { target: { value: 'Black Hawk (Sauk)' } });
|
|
83
|
+
expect(onDraftChange).toHaveBeenCalledWith({ name: 'Black Hawk (Sauk)' });
|
|
84
|
+
fireEvent.change(screen.getByTestId('code-editor'), { target: { value: 'A Sauk leader.' } });
|
|
85
|
+
expect(onDraftChange).toHaveBeenCalledWith({ content: 'A Sauk leader.' });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('reference-fixed entity types render as read-only tags, not a picker (D6)', () => {
|
|
89
|
+
renderStep(); // referenceEntityTypes: ['Person']
|
|
90
|
+
expect(screen.getByText('Person')).toBeInTheDocument();
|
|
91
|
+
// No toggling: the types were chosen when the reference was created.
|
|
92
|
+
expect(screen.queryByRole('button', { name: /Person/ })).not.toBeInTheDocument();
|
|
93
|
+
expect(screen.queryByRole('button', { name: /Topic/ })).not.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('an empty reference set falls back to the picker over the owner-supplied options (D6)', () => {
|
|
97
|
+
const { onDraftChange } = renderStep({ referenceEntityTypes: [] });
|
|
98
|
+
const topic = screen.getByRole('button', { name: /Topic/ });
|
|
99
|
+
fireEvent.click(topic);
|
|
100
|
+
expect(onDraftChange).toHaveBeenCalledWith({ entityTypes: ['Topic'] });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('carries NONE of the page-only machinery: no upload, no format, no encoding', () => {
|
|
104
|
+
const { container } = renderStep();
|
|
105
|
+
expect(container.querySelector('.semiont-form__upload-dropzone')).toBeNull();
|
|
106
|
+
expect(container.querySelector('.semiont-form__content-source-toggle')).toBeNull();
|
|
107
|
+
expect(container.querySelector('#format-select')).toBeNull();
|
|
108
|
+
expect(container.querySelector('#charset-select')).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('renders no context of its own — the evidence is host-stacked (A3)', () => {
|
|
112
|
+
const { container } = renderStep();
|
|
113
|
+
expect(container.querySelector('.semiont-gather-pane')).toBeNull();
|
|
114
|
+
expect(container.querySelector('.semiont-gather__outer')).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('submit emits the full params with the file:// prefix applied at the seam', async () => {
|
|
118
|
+
const { onCompose } = renderStep({ referenceEntityTypes: ['Person'] });
|
|
119
|
+
fireEvent.click(screen.getByRole('button', { name: T.createAndLink }));
|
|
120
|
+
await waitFor(() => expect(onCompose).toHaveBeenCalledTimes(1));
|
|
121
|
+
expect(onCompose).toHaveBeenCalledWith({
|
|
122
|
+
name: 'Black Hawk',
|
|
123
|
+
storagePath: 'file://people/black-hawk.md',
|
|
124
|
+
content: 'Sauk leader.',
|
|
125
|
+
entityTypes: ['Person'],
|
|
126
|
+
language: 'en',
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('the footer is a wizard footer: Back + Create, pending while in flight, re-enabled on rejection (A4)', async () => {
|
|
131
|
+
let reject!: (e: Error) => void;
|
|
132
|
+
const onCompose = vi.fn(() => new Promise<void>((_, r) => { reject = r; }));
|
|
133
|
+
const { container } = renderStep({ onCompose });
|
|
134
|
+
|
|
135
|
+
expect(container.querySelector('.semiont-modal__actions--wizard')).not.toBeNull();
|
|
136
|
+
fireEvent.click(screen.getByRole('button', { name: T.createAndLink }));
|
|
137
|
+
expect(await screen.findByRole('button', { name: T.creatingAndLinking })).toBeDisabled();
|
|
138
|
+
|
|
139
|
+
reject(new Error('boom'));
|
|
140
|
+
expect(await screen.findByRole('button', { name: T.createAndLink })).toBeEnabled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('Back is the only retreat and dismissal never lives in the footer (A4)', () => {
|
|
144
|
+
const { container, onBack } = renderStep();
|
|
145
|
+
const footerButtons = Array.from(
|
|
146
|
+
container.querySelectorAll('.semiont-modal__actions button'),
|
|
147
|
+
).map((b) => b.textContent ?? '');
|
|
148
|
+
expect(footerButtons.filter((l) => /cancel|✕/i.test(l))).toEqual([]);
|
|
149
|
+
fireEvent.click(screen.getByRole('button', { name: `◀ ${T.back}` }));
|
|
150
|
+
expect(onBack).toHaveBeenCalledTimes(1);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -16,13 +16,12 @@ const t = {
|
|
|
16
16
|
includeSummary: 'Include summary',
|
|
17
17
|
depth: 'Depth',
|
|
18
18
|
maxResources: 'Max resources',
|
|
19
|
-
cancel: 'Cancel',
|
|
20
19
|
gather: 'Gather',
|
|
21
20
|
};
|
|
22
21
|
|
|
23
22
|
describe('ConfigureGatherStep', () => {
|
|
24
23
|
it('renders the intro and the default option values', () => {
|
|
25
|
-
render(<ConfigureGatherStep onGather={vi.fn()}
|
|
24
|
+
render(<ConfigureGatherStep onGather={vi.fn()} translations={t} />);
|
|
26
25
|
expect(screen.getByText('Choose what to include.')).toBeInTheDocument();
|
|
27
26
|
expect(screen.getByLabelText('Include content')).toBeChecked();
|
|
28
27
|
expect(screen.getByLabelText('Include summary')).toBeChecked();
|
|
@@ -32,14 +31,14 @@ describe('ConfigureGatherStep', () => {
|
|
|
32
31
|
|
|
33
32
|
it('emits the default config on submit', () => {
|
|
34
33
|
const onGather = vi.fn();
|
|
35
|
-
render(<ConfigureGatherStep onGather={onGather}
|
|
34
|
+
render(<ConfigureGatherStep onGather={onGather} translations={t} />);
|
|
36
35
|
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
37
36
|
expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
it('reflects edits in the emitted config', () => {
|
|
41
40
|
const onGather = vi.fn();
|
|
42
|
-
render(<ConfigureGatherStep onGather={onGather}
|
|
41
|
+
render(<ConfigureGatherStep onGather={onGather} translations={t} />);
|
|
43
42
|
fireEvent.click(screen.getByLabelText('Include content')); // uncheck
|
|
44
43
|
fireEvent.click(screen.getByLabelText('Include summary')); // uncheck
|
|
45
44
|
fireEvent.change(screen.getByLabelText('Depth'), { target: { value: '4' } });
|
|
@@ -48,32 +47,21 @@ describe('ConfigureGatherStep', () => {
|
|
|
48
47
|
expect(onGather).toHaveBeenCalledWith({ includeContent: false, includeSummary: false, depth: 4, maxResources: 25 });
|
|
49
48
|
});
|
|
50
49
|
|
|
51
|
-
it('
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
);
|
|
61
|
-
expect(screen.getByLabelText('Include summary')).not.toBeChecked();
|
|
62
|
-
expect(screen.getByLabelText('Depth')).toHaveValue(1);
|
|
63
|
-
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
64
|
-
expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: false, depth: 1, maxResources: 5 });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('calls onCancel from the cancel button', () => {
|
|
68
|
-
const onCancel = vi.fn();
|
|
69
|
-
render(<ConfigureGatherStep onGather={vi.fn()} onCancel={onCancel} translations={t} />);
|
|
70
|
-
fireEvent.click(screen.getByRole('button', { name: /Cancel/ }));
|
|
71
|
-
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
50
|
+
it('the footer is the wizard footer: advance only, no dismissal, no flex (GFR A5)', () => {
|
|
51
|
+
// Dismissal lives on the modal's corner ✕/Esc/backdrop, never in a step
|
|
52
|
+
// footer (WIZARD-NAVIGATION D1); this is the first step, so no retreat either.
|
|
53
|
+
const { container } = render(<ConfigureGatherStep onGather={vi.fn()} translations={t} />);
|
|
54
|
+
const footer = container.querySelector('.semiont-modal__actions--wizard');
|
|
55
|
+
expect(footer).not.toBeNull();
|
|
56
|
+
const buttons = Array.from(footer!.querySelectorAll('button'));
|
|
57
|
+
expect(buttons).toHaveLength(1);
|
|
58
|
+
expect(buttons[0]!.textContent).not.toMatch(/cancel|✕/i);
|
|
59
|
+
expect(buttons[0]!.className).not.toContain('semiont-button--flex');
|
|
72
60
|
});
|
|
73
61
|
|
|
74
62
|
it('renders the children slot (exclusion picker)', () => {
|
|
75
63
|
render(
|
|
76
|
-
<ConfigureGatherStep onGather={vi.fn()}
|
|
64
|
+
<ConfigureGatherStep onGather={vi.fn()} translations={t}>
|
|
77
65
|
<div>EXCLUDE-SLOT</div>
|
|
78
66
|
</ConfigureGatherStep>,
|
|
79
67
|
);
|
|
@@ -12,11 +12,14 @@ import { useState } from 'react';
|
|
|
12
12
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
13
13
|
import '@testing-library/jest-dom';
|
|
14
14
|
import type { CollaboratorEntry, GatheredContext } from '@semiont/core';
|
|
15
|
-
import {
|
|
15
|
+
import { GENERATABLE_MEDIA_TYPES, capabilitiesOf } from '@semiont/core';
|
|
16
|
+
import { ConfigureGenerationStep, freshGenerationDraft } from '../ConfigureGenerationStep';
|
|
17
|
+
import type { GenerationDraft } from '../ConfigureGenerationStep';
|
|
16
18
|
|
|
17
19
|
const translations = {
|
|
18
20
|
resourceTitle: 'Title',
|
|
19
21
|
resourceTitlePlaceholder: 'Title…',
|
|
22
|
+
saveLocation: 'Save location',
|
|
20
23
|
additionalInstructions: 'Instructions',
|
|
21
24
|
additionalInstructionsPlaceholder: 'Instructions…',
|
|
22
25
|
language: 'Language',
|
|
@@ -28,6 +31,8 @@ const translations = {
|
|
|
28
31
|
maxLengthHelp: 'How long the generated resource may be.',
|
|
29
32
|
// Named model + ceiling, so the bound is legible rather than mysterious (D6).
|
|
30
33
|
maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
|
|
34
|
+
outputFormat: 'Format',
|
|
35
|
+
formatExtensionMismatch: 'Save location must end in {{extension}} to match the selected format.',
|
|
31
36
|
back: 'Back',
|
|
32
37
|
generate: 'Generate',
|
|
33
38
|
};
|
|
@@ -35,10 +40,7 @@ const translations = {
|
|
|
35
40
|
const context = { resources: [], annotations: [] } as unknown as GatheredContext;
|
|
36
41
|
|
|
37
42
|
/** The wizard's initial draft (WIZARD-NAVIGATION D3). */
|
|
38
|
-
const DRAFT =
|
|
39
|
-
title: 'Untitled', storagePath: '', prompt: '', language: 'en',
|
|
40
|
-
temperature: 0.7, maxTokensText: '500',
|
|
41
|
-
};
|
|
43
|
+
const DRAFT: GenerationDraft = freshGenerationDraft('Untitled', 'en');
|
|
42
44
|
|
|
43
45
|
const agentWithCeiling = (maxOutputTokens: number): CollaboratorEntry =>
|
|
44
46
|
({
|
|
@@ -58,12 +60,13 @@ const agentWithCeiling = (maxOutputTokens: number): CollaboratorEntry =>
|
|
|
58
60
|
* than the bare component: a `vi.fn()` for `onConfigChange` would swallow every edit
|
|
59
61
|
* and quietly turn each assertion below into a test of nothing.
|
|
60
62
|
*/
|
|
61
|
-
function Harness({ generationAgent }: { generationAgent?: CollaboratorEntry }) {
|
|
63
|
+
function Harness({ generationAgent, defaultFolder }: { generationAgent?: CollaboratorEntry; defaultFolder?: string }) {
|
|
62
64
|
const [draft, setDraft] = useState(DRAFT);
|
|
63
65
|
return (
|
|
64
66
|
<ConfigureGenerationStep
|
|
65
67
|
config={draft}
|
|
66
68
|
onConfigChange={setDraft}
|
|
69
|
+
{...(defaultFolder !== undefined ? { defaultFolder } : {})}
|
|
67
70
|
context={context}
|
|
68
71
|
onBack={vi.fn()}
|
|
69
72
|
onGenerate={onGenerateSpy}
|
|
@@ -75,9 +78,14 @@ function Harness({ generationAgent }: { generationAgent?: CollaboratorEntry }) {
|
|
|
75
78
|
|
|
76
79
|
let onGenerateSpy = vi.fn();
|
|
77
80
|
|
|
78
|
-
function renderStep(generationAgent?: CollaboratorEntry) {
|
|
81
|
+
function renderStep(generationAgent?: CollaboratorEntry, defaultFolder?: string) {
|
|
79
82
|
onGenerateSpy = vi.fn();
|
|
80
|
-
const utils = render(
|
|
83
|
+
const utils = render(
|
|
84
|
+
<Harness
|
|
85
|
+
{...(generationAgent ? { generationAgent } : {})}
|
|
86
|
+
{...(defaultFolder !== undefined ? { defaultFolder } : {})}
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
81
89
|
const input = () => screen.getByLabelText('Max length') as HTMLInputElement;
|
|
82
90
|
return { ...utils, input, props: { onGenerate: onGenerateSpy } };
|
|
83
91
|
}
|
|
@@ -96,6 +104,17 @@ describe('ConfigureGenerationStep — ceiling awareness', () => {
|
|
|
96
104
|
expect(screen.getByText(/Limited to 64000 tokens by claude-sonnet-5\./)).toBeInTheDocument();
|
|
97
105
|
});
|
|
98
106
|
|
|
107
|
+
it('the ceiling sentence is a row-level footnote, never a column child', () => {
|
|
108
|
+
// A full sentence inside the 5.5rem Max Length column wraps one word per
|
|
109
|
+
// line and its height taxes the whole flex row — the void under the other
|
|
110
|
+
// controls IS this caption. It renders full-width under the row instead.
|
|
111
|
+
renderStep(agentWithCeiling(64_000));
|
|
112
|
+
|
|
113
|
+
const help = screen.getByText(/Limited to 64000 tokens/);
|
|
114
|
+
expect(help).toHaveClass('semiont-form__help--row');
|
|
115
|
+
expect(help.closest('.semiont-form__field--narrow')).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
99
118
|
it('will not commit a value above the ceiling', () => {
|
|
100
119
|
// `max` on a number input does not prevent TYPING an over-value — it only
|
|
101
120
|
// marks the field invalid. D6 says the value cannot be entered, so assert
|
|
@@ -253,3 +272,182 @@ describe('ConfigureGenerationStep — the draft is the owner\'s', () => {
|
|
|
253
272
|
expect(props.onGenerate.mock.calls[0]![0]).not.toHaveProperty('prompt');
|
|
254
273
|
});
|
|
255
274
|
});
|
|
275
|
+
|
|
276
|
+
// ── GENERATION-OUTPUT-FORMAT P2 — the output format is a choice ─────────────
|
|
277
|
+
// The wire, the worker and the registry have carried three generatable types
|
|
278
|
+
// the whole time; only the control was missing.
|
|
279
|
+
|
|
280
|
+
describe('ConfigureGenerationStep — the output format (D1, D2)', () => {
|
|
281
|
+
const formatSelect = () => screen.getByLabelText(translations.outputFormat) as HTMLSelectElement;
|
|
282
|
+
|
|
283
|
+
it('offers exactly the registry-generatable types, in registry order, by their registry labels', () => {
|
|
284
|
+
// DERIVED from GENERATABLE_MEDIA_TYPES, never a literal list: promoting a
|
|
285
|
+
// fourth row must light it up here for free, and a hand-written array
|
|
286
|
+
// would silently lie the moment the registry moves.
|
|
287
|
+
renderStep();
|
|
288
|
+
const options = Array.from(formatSelect().options);
|
|
289
|
+
expect(options.map((o) => o.value)).toEqual([...GENERATABLE_MEDIA_TYPES]);
|
|
290
|
+
expect(options.map((o) => o.textContent)).toEqual(
|
|
291
|
+
GENERATABLE_MEDIA_TYPES.map((t) => capabilitiesOf(t)!.label),
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('defaults to markdown, and SENDS what it shows (D2)', () => {
|
|
296
|
+
// The worker already defaults to markdown when the key is absent, so an
|
|
297
|
+
// unsent field would produce the right artifact by accident. The point is
|
|
298
|
+
// that the displayed value is the transmitted one.
|
|
299
|
+
const { props } = renderStep();
|
|
300
|
+
expect(formatSelect().value).toBe('text/markdown');
|
|
301
|
+
fillRequired();
|
|
302
|
+
fireEvent.click(screen.getByText(new RegExp(translations.generate)));
|
|
303
|
+
expect(props.onGenerate.mock.calls[0]![0]).toMatchObject({ outputMediaType: 'text/markdown' });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('carries a chosen format into the submitted config', () => {
|
|
307
|
+
const { props } = renderStep();
|
|
308
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.pdf' } });
|
|
309
|
+
fireEvent.change(formatSelect(), { target: { value: 'application/pdf' } });
|
|
310
|
+
fireEvent.click(screen.getByText(new RegExp(translations.generate)));
|
|
311
|
+
expect(props.onGenerate.mock.calls[0]![0]).toMatchObject({ outputMediaType: 'application/pdf' });
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe('ConfigureGenerationStep — the GUI refuses a format/extension mismatch (D7)', () => {
|
|
316
|
+
const formatSelect = () => screen.getByLabelText(translations.outputFormat) as HTMLSelectElement;
|
|
317
|
+
const generateButton = () => screen.getByText(new RegExp(translations.generate)).closest('button')!;
|
|
318
|
+
|
|
319
|
+
it('blocks submission and explains, naming the extension the format expects', () => {
|
|
320
|
+
// D7: the worker is faithful and incurious — it will write a PDF to a .md
|
|
321
|
+
// path and only log it — so this form is the ONLY refusal in the chain.
|
|
322
|
+
const { props } = renderStep();
|
|
323
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.md' } });
|
|
324
|
+
fireEvent.change(formatSelect(), { target: { value: 'application/pdf' } });
|
|
325
|
+
|
|
326
|
+
expect(generateButton()).toBeDisabled();
|
|
327
|
+
expect(screen.getByText(/must end in \.pdf/)).toBeInTheDocument();
|
|
328
|
+
fireEvent.click(generateButton());
|
|
329
|
+
expect(props.onGenerate).not.toHaveBeenCalled();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('clears when EITHER control is corrected', () => {
|
|
333
|
+
const { props } = renderStep();
|
|
334
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.md' } });
|
|
335
|
+
fireEvent.change(formatSelect(), { target: { value: 'application/pdf' } });
|
|
336
|
+
expect(generateButton()).toBeDisabled();
|
|
337
|
+
|
|
338
|
+
// Fix the path…
|
|
339
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/out.pdf' } });
|
|
340
|
+
expect(generateButton()).not.toBeDisabled();
|
|
341
|
+
expect(screen.queryByText(/must end in/)).toBeNull();
|
|
342
|
+
|
|
343
|
+
// …or, from a fresh mismatch, fix the format instead.
|
|
344
|
+
fireEvent.change(formatSelect(), { target: { value: 'text/plain' } });
|
|
345
|
+
expect(generateButton()).toBeDisabled();
|
|
346
|
+
fireEvent.change(formatSelect(), { target: { value: 'application/pdf' } });
|
|
347
|
+
expect(generateButton()).not.toBeDisabled();
|
|
348
|
+
|
|
349
|
+
fireEvent.click(generateButton());
|
|
350
|
+
expect(props.onGenerate).toHaveBeenCalledTimes(1);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('never greets an untouched form with a refusal — the default state is coherent', () => {
|
|
354
|
+
// markdown + the placeholder's `.md` shape: a user who has typed nothing
|
|
355
|
+
// must not be told they are wrong.
|
|
356
|
+
renderStep();
|
|
357
|
+
expect(screen.queryByText(/must end in/)).toBeNull();
|
|
358
|
+
fillRequired(); // generated/out.md, markdown selected
|
|
359
|
+
expect(generateButton()).not.toBeDisabled();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('refuses an extensionless path, and accepts a differently-cased one', () => {
|
|
363
|
+
// Open question 2, both edges: the rule is "ends with the registry
|
|
364
|
+
// extension", so no extension fails it; case is not a real mismatch.
|
|
365
|
+
renderStep();
|
|
366
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/notes' } });
|
|
367
|
+
expect(generateButton()).toBeDisabled();
|
|
368
|
+
|
|
369
|
+
fireEvent.change(screen.getByLabelText('Save location'), { target: { value: 'generated/NOTES.MD' } });
|
|
370
|
+
expect(generateButton()).not.toBeDisabled();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
describe('ConfigureGenerationStep — the hint echo (GEP P1c, D8)', () => {
|
|
375
|
+
it('echoes a non-empty hint, and renders nothing without one', () => {
|
|
376
|
+
const { container, rerender } = render(
|
|
377
|
+
<ConfigureGenerationStep
|
|
378
|
+
context={context} config={DRAFT} onConfigChange={vi.fn()}
|
|
379
|
+
onBack={vi.fn()} onGenerate={vi.fn()} translations={translations}
|
|
380
|
+
hintEcho={{ label: 'Hint', value: 'the ancient city, not the modern province' }}
|
|
381
|
+
/>,
|
|
382
|
+
);
|
|
383
|
+
const echo = container.querySelector('.semiont-wizard__hint-echo');
|
|
384
|
+
expect(echo).not.toBeNull();
|
|
385
|
+
expect(echo!.textContent).toContain('the ancient city, not the modern province');
|
|
386
|
+
|
|
387
|
+
rerender(
|
|
388
|
+
<ConfigureGenerationStep
|
|
389
|
+
context={context} config={DRAFT} onConfigChange={vi.fn()}
|
|
390
|
+
onBack={vi.fn()} onGenerate={vi.fn()} translations={translations}
|
|
391
|
+
/>,
|
|
392
|
+
);
|
|
393
|
+
expect(container.querySelector('.semiont-wizard__hint-echo')).toBeNull();
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('the Save location proposes a path and stops following once touched (D11)', () => {
|
|
398
|
+
const path = () => screen.getByLabelText('Save location') as HTMLInputElement;
|
|
399
|
+
const title = () => screen.getByLabelText('Title') as HTMLInputElement;
|
|
400
|
+
|
|
401
|
+
it('opens PRE-FILLED beside the source, named for the title', () => {
|
|
402
|
+
renderStep(undefined, 'research');
|
|
403
|
+
expect(path().value).toBe('research/untitled.md');
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('proposes a bare filename when the source has no folder', () => {
|
|
407
|
+
renderStep(undefined, '');
|
|
408
|
+
expect(path().value).toBe('untitled.md');
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('FOLLOWS the title while untouched', () => {
|
|
412
|
+
renderStep(undefined, 'research');
|
|
413
|
+
fireEvent.change(title(), { target: { value: 'A New Name' } });
|
|
414
|
+
expect(path().value).toBe('research/a-new-name.md');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('FOLLOWS the format while untouched — extension included', () => {
|
|
418
|
+
renderStep(undefined, 'research');
|
|
419
|
+
fireEvent.change(screen.getByLabelText('Format'), { target: { value: 'application/pdf' } });
|
|
420
|
+
expect(path().value).toBe('research/untitled.pdf');
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it('STOPS following once the path is edited — the title then moves freely', () => {
|
|
424
|
+
renderStep(undefined, 'research');
|
|
425
|
+
fireEvent.change(path(), { target: { value: 'elsewhere/mine.md' } });
|
|
426
|
+
fireEvent.change(title(), { target: { value: 'A New Name' } });
|
|
427
|
+
expect(path().value).toBe('elsewhere/mine.md');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it('CLEARING the field un-touches it, restoring the proposal', () => {
|
|
431
|
+
renderStep(undefined, 'research');
|
|
432
|
+
fireEvent.change(path(), { target: { value: 'elsewhere/mine.md' } });
|
|
433
|
+
fireEvent.change(path(), { target: { value: '' } });
|
|
434
|
+
expect(path().value).toBe('research/untitled.md');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('submits the PROPOSED path when the user never touched it', () => {
|
|
438
|
+
const { props } = renderStep(undefined, 'research');
|
|
439
|
+
fireEvent.click(screen.getByRole('button', { name: 'Generate' }));
|
|
440
|
+
expect(props.onGenerate).toHaveBeenCalledWith(
|
|
441
|
+
expect.objectContaining({ storageUri: 'file://research/untitled.md' }),
|
|
442
|
+
);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('a proposed path never trips the D7 mismatch refusal — it carries the right extension', () => {
|
|
446
|
+
const { props } = renderStep(undefined, 'research');
|
|
447
|
+
fireEvent.change(screen.getByLabelText('Format'), { target: { value: 'application/pdf' } });
|
|
448
|
+
fireEvent.click(screen.getByRole('button', { name: 'Generate' }));
|
|
449
|
+
expect(props.onGenerate).toHaveBeenCalledWith(
|
|
450
|
+
expect.objectContaining({ storageUri: 'file://research/untitled.pdf' }),
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -16,7 +16,7 @@ import { ConfigureSearchStep, type SearchConfig } from '../ConfigureSearchStep';
|
|
|
16
16
|
const T = {
|
|
17
17
|
maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
|
|
18
18
|
semanticScoringHelp: 'Use AI to score results by semantic relevance',
|
|
19
|
-
back: 'Back', search: 'Search', searching: 'Searching…',
|
|
19
|
+
back: 'Back', search: 'Search', searching: 'Searching…', searchFailed: 'Search failed',
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const CONFIG: SearchConfig = { limit: 10, useSemanticScoring: true };
|
|
@@ -50,6 +50,29 @@ describe('ConfigureSearchStep', () => {
|
|
|
50
50
|
expect(onConfigChange).toHaveBeenCalledWith({ limit: 10, useSemanticScoring: false });
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it('echoes the hint being steered by, and stays silent without one (GEP P1c, D8)', () => {
|
|
54
|
+
// The thing the hint steers goes invisible behind the step transition —
|
|
55
|
+
// one quiet line keeps it visible and advertises that the hint did something.
|
|
56
|
+
const { container, rerender } = render(
|
|
57
|
+
<ConfigureSearchStep
|
|
58
|
+
config={CONFIG} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
|
|
59
|
+
translations={T}
|
|
60
|
+
hintEcho={{ label: 'Hint', value: 'the ancient city, not the modern province' }}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
const echo = container.querySelector('.semiont-wizard__hint-echo');
|
|
64
|
+
expect(echo).not.toBeNull();
|
|
65
|
+
expect(echo!.textContent).toContain('the ancient city, not the modern province');
|
|
66
|
+
|
|
67
|
+
rerender(
|
|
68
|
+
<ConfigureSearchStep
|
|
69
|
+
config={CONFIG} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
|
|
70
|
+
translations={T}
|
|
71
|
+
/>,
|
|
72
|
+
);
|
|
73
|
+
expect(container.querySelector('.semiont-wizard__hint-echo')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
53
76
|
it('submits whatever the owner currently holds, not a default', async () => {
|
|
54
77
|
// The point of controlled state: a step that kept its own copy could submit
|
|
55
78
|
// something the wizard never saw.
|