@semiont/react-ui 0.5.27 → 0.5.28
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/dist/{PdfAnnotationCanvas.client-EVUPTUWZ.js → PdfAnnotationCanvas.client-C2IGG4SW.js} +31 -4
- package/dist/{ar-OU3DQGC6.js → ar-FABPTJVB.js} +39 -23
- package/dist/{bn-PHWCR6AZ.js → bn-LKP6BDMH.js} +39 -23
- package/dist/{chunk-PCRRBU7H.js → chunk-2GOHTTHW.js} +39 -23
- package/dist/{chunk-5VDTWRXJ.js → chunk-2YHEE63S.js} +31 -31
- package/dist/{cs-NEXYZ62I.js → cs-YQYNWHRG.js} +39 -23
- package/dist/{da-FAT43YCU.js → da-QHZOYKRY.js} +39 -23
- package/dist/{de-KRQ3WBCU.js → de-GKA7RD7J.js} +39 -23
- package/dist/{el-Q4IU3EKC.js → el-45T257AL.js} +39 -23
- package/dist/{en-2KADE7N4.js → en-RVW5EQ66.js} +2 -2
- package/dist/{es-2IO3R2MK.js → es-AVER7RU4.js} +39 -23
- package/dist/{fa-Q5GOQFXX.js → fa-K3VPK3O2.js} +39 -23
- package/dist/{fi-R6NMRWBE.js → fi-5NEKF7EB.js} +39 -23
- package/dist/{fr-QY4D5E6J.js → fr-FUMNDLBZ.js} +39 -23
- package/dist/{he-X6Y77XHY.js → he-3JGNA3PR.js} +39 -23
- package/dist/{hi-XL7IOEQM.js → hi-IJY375DF.js} +39 -23
- package/dist/{id-QZ4ZZS67.js → id-LWHKJ7ZD.js} +39 -23
- package/dist/index.css +279 -74
- package/dist/index.d.ts +170 -145
- package/dist/index.js +2743 -2365
- package/dist/{it-ME2Q7OB6.js → it-EBVOV4RW.js} +39 -23
- package/dist/{ja-HX7QASRI.js → ja-KWRIX2NF.js} +39 -23
- package/dist/{ko-EQO5AHAV.js → ko-L5CJMFDS.js} +39 -23
- package/dist/{ms-UILFSF3I.js → ms-LSNMJDR2.js} +39 -23
- package/dist/{nl-NNI45PMK.js → nl-ZB32MB7U.js} +39 -23
- package/dist/{no-MAL5F6P6.js → no-4GTNY7M5.js} +39 -23
- package/dist/{pl-4RMY6JAJ.js → pl-LUHBLTHB.js} +39 -23
- package/dist/{pt-VQH2X3QQ.js → pt-C4H3UNQ6.js} +39 -23
- package/dist/{ro-XXCQO2OG.js → ro-GGITS5V4.js} +39 -23
- package/dist/{sv-7Z6EJ6U4.js → sv-T56RTLBP.js} +39 -23
- package/dist/test-utils.js +2 -2
- package/dist/{th-L3DXXYVQ.js → th-22X2IWCB.js} +39 -23
- package/dist/{tr-P3DFVLFJ.js → tr-77TFG62H.js} +39 -23
- package/dist/{uk-7C5D3PXS.js → uk-MIIL6NXT.js} +39 -23
- package/dist/{vi-ZLTLEO7P.js → vi-7JYOIGNV.js} +39 -23
- package/dist/{zh-5FHVWDIN.js → zh-ID6DRYU4.js} +39 -23
- package/package.json +9 -8
- package/src/components/AssistProgress.tsx +36 -2
- package/src/components/CodeMirrorRenderer.tsx +8 -8
- package/src/components/__tests__/AssistProgress.test.tsx +59 -0
- package/src/components/modals/ComposeStep.tsx +218 -0
- package/src/components/modals/ConfigureGatherStep.tsx +10 -18
- package/src/components/modals/ConfigureGenerationStep.tsx +35 -19
- 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 +238 -50
- package/src/components/modals/ResourceGenerateModal.tsx +216 -108
- 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 +35 -0
- 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 +417 -38
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +276 -85
- package/src/components/modals/__tests__/SearchResultsStep.test.tsx +61 -0
- package/src/components/modals/__tests__/WizardFooter.test.tsx +113 -4
- 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 +5 -5
- 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.test.tsx +9 -9
- 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 +9 -2
- package/src/components/resource/panels/ReferencesPanel.tsx +4 -0
- 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.test.tsx +33 -1
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +148 -6
- package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +0 -65
- package/src/features/resource-compose/components/ResourceComposePage.tsx +17 -130
- 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-viewer/__tests__/ResourceViewerPage.test.tsx +71 -15
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +133 -59
- 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/translations/ar.json +38 -22
- package/translations/bn.json +38 -22
- package/translations/cs.json +38 -22
- package/translations/da.json +38 -22
- package/translations/de.json +38 -22
- package/translations/el.json +38 -22
- package/translations/en.json +38 -22
- package/translations/es.json +38 -22
- package/translations/fa.json +38 -22
- package/translations/fi.json +38 -22
- package/translations/fr.json +38 -22
- package/translations/he.json +38 -22
- package/translations/hi.json +38 -22
- package/translations/id.json +38 -22
- package/translations/it.json +38 -22
- package/translations/ja.json +38 -22
- package/translations/ko.json +38 -22
- package/translations/ms.json +38 -22
- package/translations/nl.json +38 -22
- package/translations/no.json +38 -22
- package/translations/pl.json +38 -22
- package/translations/pt.json +38 -22
- package/translations/ro.json +38 -22
- package/translations/sv.json +38 -22
- package/translations/th.json +38 -22
- package/translations/tr.json +38 -22
- package/translations/uk.json +38 -22
- package/translations/vi.json +38 -22
- package/translations/zh.json +38 -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
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* settings.
|
|
20
20
|
*/
|
|
21
21
|
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
|
|
22
|
-
import { screen } from '@testing-library/react';
|
|
22
|
+
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
23
23
|
import userEvent from '@testing-library/user-event';
|
|
24
24
|
import '@testing-library/jest-dom';
|
|
25
25
|
import type { GatheredContext } from '@semiont/core';
|
|
@@ -37,7 +37,7 @@ const CONTEXT = {
|
|
|
37
37
|
focus: {
|
|
38
38
|
kind: 'annotation',
|
|
39
39
|
annotation: { id: 'ann-1' },
|
|
40
|
-
sourceResource: { id: 'res-1', name: 'Scythian steppe' },
|
|
40
|
+
sourceResource: { '@id': 'res-1', name: 'Scythian steppe' },
|
|
41
41
|
selected: { text: 'Caspian Sea' },
|
|
42
42
|
},
|
|
43
43
|
graph: { nodes: [], edges: [] },
|
|
@@ -45,24 +45,43 @@ const CONTEXT = {
|
|
|
45
45
|
} as unknown as GatheredContext;
|
|
46
46
|
|
|
47
47
|
const T = {
|
|
48
|
-
|
|
49
|
-
configureSearchTitle: 'Configure Search', searchResultsTitle: 'Search Results',
|
|
48
|
+
resolveTitle: 'Resolve Reference',
|
|
50
49
|
sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
|
|
51
|
-
userHintLabel: 'Hint',
|
|
50
|
+
userHintLabel: 'Hint', userHintEffect: 'steers Search and Generate',
|
|
51
|
+
userHintPlaceholder: 'Describe what this refers to…',
|
|
52
|
+
graphPaneTitle: 'In the graph', graphEmpty: 'No links yet.', resourceLinkLabel: 'Resource link',
|
|
53
|
+
corpusPaneTitle: 'In the corpus', corpusEmpty: 'Nothing similar.',
|
|
54
|
+
excludedReceipt: '{{types}} excluded', machineRead: 'OCR',
|
|
52
55
|
loadingContext: 'Loading…', failedContext: 'Failed',
|
|
53
|
-
|
|
56
|
+
search: 'Search', searching: 'Searching…', generate: 'Generate',
|
|
54
57
|
compose: 'Compose', resolutionStrategyLabel: 'Resolution Strategy', back: 'Back',
|
|
55
58
|
link: 'Link', score: 'Score', noResults: 'No results',
|
|
56
|
-
resourceTitle: 'Resource Title', resourceTitlePlaceholder: 'Title…',
|
|
59
|
+
resourceTitle: 'Resource Title', resourceTitlePlaceholder: 'Title…', saveLocation: 'Save location',
|
|
57
60
|
additionalInstructions: 'Additional Instructions', additionalInstructionsPlaceholder: '…',
|
|
58
61
|
language: 'Language', languageHelp: '', creativity: 'Creativity',
|
|
59
62
|
creativityFocused: 'Focused', creativityCreative: 'Creative',
|
|
60
63
|
maxLength: 'Max length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{maxOutputTokens}} by {{model}}.',
|
|
61
64
|
maxResults: 'Max Results', semanticScoring: 'Semantic Scoring', semanticScoringHelp: '',
|
|
65
|
+
searchFailed: 'Search failed',
|
|
66
|
+
contentLabel: 'Content', entityTypes: 'Entity types',
|
|
67
|
+
createAndLink: 'Create & Link', creatingAndLinking: 'Creating…',
|
|
68
|
+
discardDraftPrompt: 'Discard this draft?', discardDraft: 'Discard', keepEditing: 'Keep editing',
|
|
62
69
|
};
|
|
63
70
|
|
|
71
|
+
// ComposeStep embeds the editor; same mock the page's own tests use.
|
|
72
|
+
vi.mock('../../CodeMirrorRenderer', () => ({
|
|
73
|
+
CodeMirrorRenderer: ({ content, onChange, editable }: any) => (
|
|
74
|
+
<textarea
|
|
75
|
+
data-testid="code-editor"
|
|
76
|
+
value={content}
|
|
77
|
+
disabled={!editable}
|
|
78
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
79
|
+
/>
|
|
80
|
+
),
|
|
81
|
+
}));
|
|
82
|
+
|
|
64
83
|
function renderWizard(over: Partial<React.ComponentProps<typeof ReferenceWizardModal>> = {}) {
|
|
65
|
-
const
|
|
84
|
+
const onComposeSubmit = vi.fn<(referenceId: string, params: unknown) => Promise<void>>().mockResolvedValue(undefined);
|
|
66
85
|
const onGenerateSubmit = vi.fn();
|
|
67
86
|
const onLinkResource = vi.fn();
|
|
68
87
|
const onClose = vi.fn();
|
|
@@ -75,13 +94,14 @@ function renderWizard(over: Partial<React.ComponentProps<typeof ReferenceWizardM
|
|
|
75
94
|
resourceId="res-1"
|
|
76
95
|
defaultTitle="Caspian Sea"
|
|
77
96
|
entityTypes={['Location']}
|
|
97
|
+
entityTypeOptions={['Person', 'Topic', 'Location']}
|
|
78
98
|
locale="en"
|
|
79
99
|
context={CONTEXT}
|
|
80
100
|
contextLoading={false}
|
|
81
101
|
contextError={null}
|
|
82
102
|
onGenerateSubmit={onGenerateSubmit}
|
|
83
103
|
onLinkResource={onLinkResource}
|
|
84
|
-
|
|
104
|
+
onComposeSubmit={onComposeSubmit}
|
|
85
105
|
translations={T}
|
|
86
106
|
{...over}
|
|
87
107
|
/>,
|
|
@@ -92,7 +112,7 @@ function renderWizard(over: Partial<React.ComponentProps<typeof ReferenceWizardM
|
|
|
92
112
|
const client = utils.session!.client;
|
|
93
113
|
const searchSpy = vi.spyOn(client.match, 'requestSearch');
|
|
94
114
|
|
|
95
|
-
return { ...utils, client, searchSpy,
|
|
115
|
+
return { ...utils, client, searchSpy, onComposeSubmit, onGenerateSubmit, onLinkResource, onClose };
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
/** Type the hint into the gather step's textarea. */
|
|
@@ -103,17 +123,32 @@ async function typeHint(text: string) {
|
|
|
103
123
|
describe('ReferenceWizardModal — the Hint reaches every strategy, at focus.userHint', () => {
|
|
104
124
|
beforeEach(() => { vi.clearAllMocks(); });
|
|
105
125
|
|
|
106
|
-
it('
|
|
107
|
-
const {
|
|
108
|
-
|
|
109
|
-
|
|
126
|
+
it('the gather panel carries the widened class (GEP P2, D2)', () => {
|
|
127
|
+
const { baseElement } = renderWizard();
|
|
128
|
+
const panel = baseElement.querySelector('.semiont-search-modal__panel--gather');
|
|
129
|
+
expect(panel).not.toBeNull();
|
|
130
|
+
expect(panel!.className).toContain('semiont-search-modal__panel--wide');
|
|
131
|
+
});
|
|
110
132
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
it('the typed hint stays visible after stepping into configure-search (GEP P1c, D8)', async () => {
|
|
134
|
+
// The thing being steered must not vanish while you steer it.
|
|
135
|
+
const { baseElement } = renderWizard();
|
|
136
|
+
await typeHint('the ancient city');
|
|
137
|
+
await userEvent.click(screen.getByRole('button', { name: /Search…/ }));
|
|
138
|
+
const echo = baseElement.querySelector('.semiont-wizard__hint-echo');
|
|
139
|
+
expect(echo).not.toBeNull();
|
|
140
|
+
expect(echo!.textContent).toContain('the ancient city');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('compose is a step now — choosing it navigates nowhere (COMPOSE-IN-MODAL D1)', async () => {
|
|
144
|
+
// The old flow stashed the context in sessionStorage and navigated to the
|
|
145
|
+
// compose page; both are gone. Choosing ✍️ Compose unfolds the compose
|
|
146
|
+
// form below the evidence like the other strategies.
|
|
147
|
+
renderWizard();
|
|
148
|
+
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
149
|
+
expect(screen.getByRole('button', { name: T.createAndLink })).toBeInTheDocument(); // the compose form is open
|
|
150
|
+
expect(screen.getByText(T.resolveTitle)).toBeInTheDocument(); // ONE title, every step
|
|
151
|
+
expect(screen.getByLabelText(T.resourceTitle)).toHaveValue('Caspian Sea'); // seeded draft
|
|
117
152
|
});
|
|
118
153
|
|
|
119
154
|
it('search sends the same enriched context over the wire', async () => {
|
|
@@ -131,20 +166,311 @@ describe('ReferenceWizardModal — the Hint reaches every strategy, at focus.use
|
|
|
131
166
|
it('generation gets it too — the path that used to drop it silently', async () => {
|
|
132
167
|
renderWizard();
|
|
133
168
|
await typeHint('focus on hydrology');
|
|
134
|
-
await userEvent.click(screen.
|
|
169
|
+
await userEvent.click(screen.getByRole('button', { name: `✨ ${T.generate}…` }));
|
|
135
170
|
|
|
136
171
|
// The step renders the context it was handed; the hint is visible in the
|
|
137
172
|
// gathered-context panel it shows, which is only true if the ENRICHED
|
|
138
173
|
// object was passed down rather than the raw prop.
|
|
139
|
-
expect(screen.
|
|
174
|
+
expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('ReferenceWizardModal — the context stays in view on the strategy steps', () => {
|
|
180
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
181
|
+
|
|
182
|
+
// Choosing Generate or Search unfolds that strategy's form BELOW the
|
|
183
|
+
// evidence — it must not navigate away from it. The gather step stays the
|
|
184
|
+
// decision surface (chooser footer, hint textarea); the strategy steps keep
|
|
185
|
+
// the RESOLUTION STRATEGY band for continuity, collapsed from three choices
|
|
186
|
+
// to the chosen one — a passive echo, not a second chooser: the OTHER
|
|
187
|
+
// strategies' buttons are gone, and there is no second hint textarea.
|
|
188
|
+
it('configure-search renders the evidence above the form, the strategy band collapsed to Search', async () => {
|
|
189
|
+
const { baseElement } = renderWizard();
|
|
190
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
191
|
+
|
|
192
|
+
expect(baseElement.querySelector('.semiont-gather__source-box')).not.toBeNull(); // quotation strip
|
|
193
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument(); // graph pane
|
|
194
|
+
expect(screen.getByText(T.maxResults)).toBeInTheDocument(); // the form, below
|
|
195
|
+
// The band survives the step: header + the chosen strategy, nothing clickable.
|
|
196
|
+
const footer = baseElement.querySelector('.semiont-gather__footer');
|
|
197
|
+
expect(footer).not.toBeNull();
|
|
198
|
+
expect(footer!.textContent).toContain(T.resolutionStrategyLabel);
|
|
199
|
+
expect(footer!.textContent).toContain(`🔍 ${T.search}`);
|
|
200
|
+
expect(footer!.querySelectorAll('button')).toHaveLength(0);
|
|
201
|
+
expect(screen.queryByRole('button', { name: `✨ ${T.generate}…` })).not.toBeInTheDocument();
|
|
202
|
+
expect(screen.queryByRole('button', { name: `✍️ ${T.compose}` })).not.toBeInTheDocument();
|
|
203
|
+
expect(screen.queryByPlaceholderText(T.userHintPlaceholder)).not.toBeInTheDocument();
|
|
204
|
+
const panel = baseElement.querySelector('.semiont-search-modal__panel')!;
|
|
205
|
+
expect(panel.className).toContain('semiont-search-modal__panel--wide'); // evidence needs the width
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('search-results keeps the evidence and the collapsed band above the results', async () => {
|
|
209
|
+
// Same grammar as the configure steps — the results replace the FORM
|
|
210
|
+
// region, not the evidence. The step itself is pure results now (D10
|
|
211
|
+
// amended); the host stacks the full display above it.
|
|
212
|
+
const { client, baseElement } = renderWizard();
|
|
213
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
214
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
215
|
+
client.bus.get('match:search-results').next({
|
|
216
|
+
referenceId: 'ann-1',
|
|
217
|
+
response: [{ '@id': 'res-9', name: 'Caspian Sea', score: 54.2 }],
|
|
218
|
+
} as never);
|
|
219
|
+
await screen.findByRole('button', { name: new RegExp(T.link) }); // results are up
|
|
220
|
+
|
|
221
|
+
expect(baseElement.querySelector('.semiont-gather__source-box')).not.toBeNull(); // quotation strip
|
|
222
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument(); // graph pane
|
|
223
|
+
expect(screen.getByText(T.corpusPaneTitle)).toBeInTheDocument(); // full display, not the redux
|
|
224
|
+
const footer = baseElement.querySelector('.semiont-gather__footer');
|
|
225
|
+
expect(footer).not.toBeNull();
|
|
226
|
+
expect(footer!.textContent).toContain(`🔍 ${T.search}`);
|
|
227
|
+
expect(footer!.querySelectorAll('button')).toHaveLength(0);
|
|
228
|
+
expect(screen.getByRole('button', { name: `🔗 ${T.link}` })).toBeInTheDocument(); // the results
|
|
229
|
+
const panel = baseElement.querySelector('.semiont-search-modal__panel')!;
|
|
230
|
+
expect(panel.className).toContain('semiont-search-modal__panel--gather');
|
|
140
231
|
});
|
|
141
232
|
|
|
142
|
-
it('
|
|
143
|
-
const {
|
|
233
|
+
it('configure-generation renders the evidence above the form, the strategy band collapsed to Generate', async () => {
|
|
234
|
+
const { baseElement } = renderWizard();
|
|
235
|
+
await userEvent.click(screen.getByRole('button', { name: `✨ ${T.generate}…` }));
|
|
236
|
+
|
|
237
|
+
expect(baseElement.querySelector('.semiont-gather__source-box')).not.toBeNull();
|
|
238
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument();
|
|
239
|
+
expect(screen.getByText(T.resourceTitle)).toBeInTheDocument();
|
|
240
|
+
const footer = baseElement.querySelector('.semiont-gather__footer');
|
|
241
|
+
expect(footer).not.toBeNull();
|
|
242
|
+
expect(footer!.textContent).toContain(T.resolutionStrategyLabel);
|
|
243
|
+
expect(footer!.textContent).toContain(`✨ ${T.generate}`);
|
|
244
|
+
expect(footer!.querySelectorAll('button')).toHaveLength(0);
|
|
245
|
+
// The echo is a receipt LINE, not a control row: label and value share
|
|
246
|
+
// the --echo band, and the chooser's button-row wrapper is gone — the
|
|
247
|
+
// value must never sit where controls sit (it wore a control's costume:
|
|
248
|
+
// inherited base size + link blue).
|
|
249
|
+
expect(footer!.className).toContain('semiont-gather__footer--echo');
|
|
250
|
+
expect(footer!.querySelector('.semiont-gather__actions')).toBeNull();
|
|
251
|
+
expect(footer!.querySelector('.semiont-gather__chosen-strategy')).not.toBeNull();
|
|
252
|
+
expect(screen.queryByText(new RegExp(`^🔍 ${T.search}…`))).not.toBeInTheDocument();
|
|
253
|
+
const panel = baseElement.querySelector('.semiont-search-modal__panel')!;
|
|
254
|
+
expect(panel.className).toContain('semiont-search-modal__panel--wide');
|
|
255
|
+
|
|
256
|
+
// ONE scroll pane for the whole step: evidence and parameters scroll
|
|
257
|
+
// together, the form in full at the bottom, the evidence tucking up under
|
|
258
|
+
// the modal top. An independently-scrollable form inside the stack is what
|
|
259
|
+
// squeezed the parameters out of view.
|
|
260
|
+
const scroll = baseElement.querySelector('.semiont-wizard__step-scroll');
|
|
261
|
+
expect(scroll).not.toBeNull();
|
|
262
|
+
expect(scroll!.querySelector('.semiont-gather__outer')).not.toBeNull();
|
|
263
|
+
const form = scroll!.querySelector('form.semiont-form');
|
|
264
|
+
expect(form).not.toBeNull();
|
|
265
|
+
expect(form!.className).not.toContain('semiont-form--scrollable');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('ReferenceWizardModal — compose is the fourth in-modal strategy (COMPOSE-IN-MODAL P2)', () => {
|
|
270
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
271
|
+
|
|
272
|
+
async function enterCompose() {
|
|
144
273
|
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
it('compose renders the evidence above the form, the strategy band collapsed to Compose', async () => {
|
|
277
|
+
const { baseElement } = renderWizard();
|
|
278
|
+
await enterCompose();
|
|
279
|
+
|
|
280
|
+
expect(baseElement.querySelector('.semiont-gather__source-box')).not.toBeNull();
|
|
281
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument();
|
|
282
|
+
const footer = baseElement.querySelector('.semiont-gather__footer');
|
|
283
|
+
expect(footer).not.toBeNull();
|
|
284
|
+
expect(footer!.textContent).toContain(T.resolutionStrategyLabel);
|
|
285
|
+
expect(footer!.textContent).toContain(`✍️ ${T.compose}`);
|
|
286
|
+
expect(footer!.querySelectorAll('button')).toHaveLength(0);
|
|
287
|
+
// ONE scroll pane holding display + form (same grammar as the others).
|
|
288
|
+
const scroll = baseElement.querySelector('.semiont-wizard__step-scroll');
|
|
289
|
+
expect(scroll).not.toBeNull();
|
|
290
|
+
expect(scroll!.querySelector('.semiont-gather__outer')).not.toBeNull();
|
|
291
|
+
expect(scroll!.querySelector('form.semiont-form')).not.toBeNull();
|
|
292
|
+
});
|
|
145
293
|
|
|
146
|
-
|
|
147
|
-
|
|
294
|
+
it('the draft survives Back and re-entry (WIZARD-NAVIGATION D3)', async () => {
|
|
295
|
+
renderWizard();
|
|
296
|
+
await enterCompose();
|
|
297
|
+
fireEvent.change(screen.getByTestId('code-editor'), { target: { value: 'A Sauk leader.' } });
|
|
298
|
+
await userEvent.click(screen.getByRole('button', { name: `◀ ${T.back}` }));
|
|
299
|
+
expect(screen.getByText(new RegExp(T.compose))).toBeInTheDocument(); // back on gather
|
|
300
|
+
await enterCompose();
|
|
301
|
+
expect(screen.getByTestId('code-editor')).toHaveValue('A Sauk leader.');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('submit emits against the annotation with the reference-fixed entity types, then closes', async () => {
|
|
305
|
+
const { onComposeSubmit, onClose } = renderWizard();
|
|
306
|
+
await enterCompose();
|
|
307
|
+
fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'people/caspian-sea.md' } });
|
|
308
|
+
fireEvent.change(screen.getByTestId('code-editor'), { target: { value: 'The lake.' } });
|
|
309
|
+
await userEvent.click(screen.getByRole('button', { name: T.createAndLink }));
|
|
310
|
+
|
|
311
|
+
await waitFor(() => expect(onComposeSubmit).toHaveBeenCalledTimes(1));
|
|
312
|
+
expect(onComposeSubmit).toHaveBeenCalledWith('ann-1', {
|
|
313
|
+
name: 'Caspian Sea',
|
|
314
|
+
storagePath: 'file://people/caspian-sea.md',
|
|
315
|
+
content: 'The lake.',
|
|
316
|
+
entityTypes: ['Location'],
|
|
317
|
+
language: 'en',
|
|
318
|
+
});
|
|
319
|
+
await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1));
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('a rejected submit keeps the modal open with the footer re-enabled', async () => {
|
|
323
|
+
const onComposeSubmit = vi.fn<(referenceId: string, params: unknown) => Promise<void>>()
|
|
324
|
+
.mockRejectedValue(new Error('boom'));
|
|
325
|
+
const { onClose } = renderWizard({ onComposeSubmit });
|
|
326
|
+
await enterCompose();
|
|
327
|
+
fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'people/x.md' } });
|
|
328
|
+
await userEvent.click(screen.getByRole('button', { name: T.createAndLink }));
|
|
329
|
+
|
|
330
|
+
await waitFor(() => expect(onComposeSubmit).toHaveBeenCalled());
|
|
331
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
332
|
+
expect(await screen.findByRole('button', { name: T.createAndLink })).toBeEnabled();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// D4: a modal dies on ✕/Escape/backdrop; a non-empty draft must not die
|
|
336
|
+
// with it. The guard is an INLINE prompt — never window.confirm — and the
|
|
337
|
+
// footer stays dismissal-free (A4).
|
|
338
|
+
it('dismissing a dirty draft asks first; Keep editing stays, Discard closes', async () => {
|
|
339
|
+
const { onClose } = renderWizard();
|
|
340
|
+
await enterCompose();
|
|
341
|
+
fireEvent.change(screen.getByTestId('code-editor'), { target: { value: 'typed work' } });
|
|
342
|
+
|
|
343
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
344
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
345
|
+
expect(screen.getByText(T.discardDraftPrompt)).toBeInTheDocument();
|
|
346
|
+
|
|
347
|
+
await userEvent.click(screen.getByRole('button', { name: T.keepEditing }));
|
|
348
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
349
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
350
|
+
|
|
351
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
352
|
+
await userEvent.click(screen.getByRole('button', { name: T.discardDraft }));
|
|
353
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('a clean draft dismisses without any prompt', async () => {
|
|
357
|
+
const { onClose } = renderWizard();
|
|
358
|
+
await enterCompose();
|
|
359
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
360
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
361
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// GATHER-AT-THE-TOP P1: the dirty guard widens. D4 — dirtiness is
|
|
366
|
+
// step-independent (typed work guards dismissal wherever the user currently
|
|
367
|
+
// is); D5 — typed text only (title beyond seed, save location, instructions,
|
|
368
|
+
// content). The generation draft gets the same protection compose has.
|
|
369
|
+
describe('ReferenceWizardModal — the dirty guard widens (GATHER-AT-THE-TOP D4/D5)', () => {
|
|
370
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
371
|
+
|
|
372
|
+
it('a typed generation draft guards dismissal', async () => {
|
|
373
|
+
const { onClose } = renderWizard();
|
|
374
|
+
await userEvent.click(screen.getByRole('button', { name: /Generate…/ }));
|
|
375
|
+
fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'people/x.md' } });
|
|
376
|
+
|
|
377
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
378
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
379
|
+
expect(screen.getByText(T.discardDraftPrompt)).toBeInTheDocument();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('a defaultTitle drifting while open does not fake a dirty draft', async () => {
|
|
383
|
+
// The wizard's defaultTitle derives from observable state and can re-emit
|
|
384
|
+
// while the modal is open. The baseline moves, the user has typed nothing —
|
|
385
|
+
// dirtiness compares against what was SEEDED, not the live prop.
|
|
386
|
+
const { rerender, onClose, onComposeSubmit, onGenerateSubmit, onLinkResource } = renderWizard();
|
|
387
|
+
rerender(
|
|
388
|
+
<ReferenceWizardModal
|
|
389
|
+
isOpen
|
|
390
|
+
onClose={onClose}
|
|
391
|
+
annotationId="ann-1"
|
|
392
|
+
resourceId="res-1"
|
|
393
|
+
defaultTitle="Caspian Sea (moved)"
|
|
394
|
+
entityTypes={['Location']}
|
|
395
|
+
entityTypeOptions={['Person', 'Topic', 'Location']}
|
|
396
|
+
locale="en"
|
|
397
|
+
context={CONTEXT}
|
|
398
|
+
contextLoading={false}
|
|
399
|
+
contextError={null}
|
|
400
|
+
onGenerateSubmit={onGenerateSubmit}
|
|
401
|
+
onLinkResource={onLinkResource}
|
|
402
|
+
onComposeSubmit={onComposeSubmit}
|
|
403
|
+
translations={T}
|
|
404
|
+
/>,
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
408
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
409
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('a compose draft still guards after stepping Back to the evidence — D4 kills the step gate', async () => {
|
|
413
|
+
const { onClose } = renderWizard();
|
|
414
|
+
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
415
|
+
fireEvent.change(screen.getByTestId('code-editor'), { target: { value: 'typed work' } });
|
|
416
|
+
await userEvent.click(screen.getByRole('button', { name: new RegExp(T.back) }));
|
|
417
|
+
|
|
418
|
+
await userEvent.click(screen.getByLabelText('Close'));
|
|
419
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
420
|
+
expect(screen.getByText(T.discardDraftPrompt)).toBeInTheDocument();
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// The gather-loading state is not a blank modal: the annotation's exact text
|
|
425
|
+
// and entity types are wizard props — known before any wire round-trip — and
|
|
426
|
+
// the pane headers are static. Render them immediately with skeleton bars so
|
|
427
|
+
// the loading screen is the loaded screen minus the data.
|
|
428
|
+
describe('ReferenceWizardModal — the gather skeleton', () => {
|
|
429
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
430
|
+
|
|
431
|
+
it('while gathering, the known annotation facts and per-cell animations render immediately', () => {
|
|
432
|
+
const { baseElement } = renderWizard({ context: null, contextLoading: true });
|
|
433
|
+
|
|
434
|
+
expect(screen.getByText(/"Caspian Sea"/)).toBeInTheDocument(); // quoted exact text
|
|
435
|
+
expect(screen.getByText('Location')).toBeInTheDocument(); // entity chip
|
|
436
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument(); // pane headers in place
|
|
437
|
+
expect(screen.getByText(T.corpusPaneTitle)).toBeInTheDocument();
|
|
438
|
+
// One dots animation PER zone — source excerpt, Neighborhood, Similar
|
|
439
|
+
// passages — each where its content will land; no central block, no bars.
|
|
440
|
+
expect(baseElement.querySelectorAll('.semiont-gather__skeleton-dots')).toHaveLength(3);
|
|
441
|
+
expect(baseElement.querySelectorAll('.semiont-gather__skeleton-bar')).toHaveLength(0);
|
|
442
|
+
// The sentence survives for screen readers only.
|
|
443
|
+
expect(screen.getByText(T.loadingContext)).toHaveClass('semiont-sr-only');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('the source line, Hint, and strategy chooser render during loading — buttons grayed until context lands', () => {
|
|
447
|
+
// The resource name is a page fact (the wizard opens FROM the source
|
|
448
|
+
// resource); the Hint is wizard state, safely typed while the gather
|
|
449
|
+
// runs; the chooser's buttons already gate on contextReady.
|
|
450
|
+
renderWizard({ context: null, contextLoading: true, resourceName: 'Cedar County, Iowa' });
|
|
451
|
+
|
|
452
|
+
expect(screen.getByText(/Source "Cedar County, Iowa"/)).toBeInTheDocument();
|
|
453
|
+
expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toBeInTheDocument();
|
|
454
|
+
expect(screen.getByRole('button', { name: /Search…/ })).toBeDisabled();
|
|
455
|
+
expect(screen.getByRole('button', { name: /Generate…/ })).toBeDisabled();
|
|
456
|
+
expect(screen.getByRole('button', { name: new RegExp(T.compose) })).toBeDisabled();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('a hint typed during the gather survives context arrival', () => {
|
|
460
|
+
const { rerender, onClose, onComposeSubmit, onGenerateSubmit, onLinkResource } = renderWizard({ context: null, contextLoading: true });
|
|
461
|
+
fireEvent.change(screen.getByPlaceholderText(T.userHintPlaceholder), { target: { value: 'early steer' } });
|
|
462
|
+
|
|
463
|
+
rerender(
|
|
464
|
+
<ReferenceWizardModal
|
|
465
|
+
isOpen onClose={onClose} annotationId="ann-1" resourceId="res-1"
|
|
466
|
+
defaultTitle="Caspian Sea" entityTypes={['Location']}
|
|
467
|
+
entityTypeOptions={['Person', 'Topic', 'Location']} locale="en"
|
|
468
|
+
context={CONTEXT} contextLoading={false} contextError={null}
|
|
469
|
+
onGenerateSubmit={onGenerateSubmit} onLinkResource={onLinkResource}
|
|
470
|
+
onComposeSubmit={onComposeSubmit} translations={T}
|
|
471
|
+
/>,
|
|
472
|
+
);
|
|
473
|
+
expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toHaveValue('early steer');
|
|
148
474
|
});
|
|
149
475
|
});
|
|
150
476
|
|
|
@@ -158,7 +484,8 @@ describe('ReferenceWizardModal — a new run starts clean (D3 flip side)', () =>
|
|
|
158
484
|
isOpen: false, onClose: vi.fn(), annotationId: 'ann-1', resourceId: 'res-1',
|
|
159
485
|
defaultTitle: 'Caspian Sea', entityTypes: ['Location'], locale: 'en',
|
|
160
486
|
context: CONTEXT, contextLoading: false, contextError: null,
|
|
161
|
-
onGenerateSubmit: vi.fn(), onLinkResource: vi.fn(),
|
|
487
|
+
onGenerateSubmit: vi.fn(), onLinkResource: vi.fn(),
|
|
488
|
+
onComposeSubmit: vi.fn<(r: string, p: unknown) => Promise<void>>().mockResolvedValue(undefined),
|
|
162
489
|
translations: T,
|
|
163
490
|
};
|
|
164
491
|
rerender(<ReferenceWizardModal {...props} />);
|
|
@@ -166,7 +493,7 @@ describe('ReferenceWizardModal — a new run starts clean (D3 flip side)', () =>
|
|
|
166
493
|
|
|
167
494
|
// Back is lossless WITHIN a run (D3); a new run inherits nothing.
|
|
168
495
|
expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toHaveValue('');
|
|
169
|
-
expect(screen.
|
|
496
|
+
expect(screen.getByRole('button', { name: `🔍 ${T.search}…` })).toBeInTheDocument(); // back on the gather step
|
|
170
497
|
});
|
|
171
498
|
});
|
|
172
499
|
|
|
@@ -189,13 +516,65 @@ describe('ReferenceWizardModal — the three strategies complete', () => {
|
|
|
189
516
|
response: [{ '@id': 'res-9', name: 'Caspian Sea', score: 54.2 }],
|
|
190
517
|
} as never);
|
|
191
518
|
|
|
192
|
-
|
|
193
|
-
await userEvent.click(screen.getByRole('button', { name: new RegExp(T.link) }));
|
|
519
|
+
await userEvent.click(await screen.findByRole('button', { name: new RegExp(T.link) }));
|
|
194
520
|
|
|
195
521
|
expect(onLinkResource).toHaveBeenCalledWith('ann-1', 'res-9');
|
|
196
522
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
197
523
|
});
|
|
198
524
|
|
|
525
|
+
it('a search failure over the bus settles the spinner and surfaces the error — no eternal "Searching…"', async () => {
|
|
526
|
+
// The measured hang (.plans/bugs/match-search-hangs-on-neo4j-datetime-annotations.md):
|
|
527
|
+
// /bus/emit 400s the request, the match machinery publishes
|
|
528
|
+
// match:search-failed — and the wizard listened only for results, so the
|
|
529
|
+
// failure fired into an empty room while the button spun forever.
|
|
530
|
+
const { client } = renderWizard();
|
|
531
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
532
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
533
|
+
expect(screen.getByRole('button', { name: T.searching })).toBeDisabled();
|
|
534
|
+
|
|
535
|
+
client.bus.get('match:search-failed').next({
|
|
536
|
+
correlationId: 'c-1',
|
|
537
|
+
referenceId: 'ann-1',
|
|
538
|
+
error: '/bus/emit 400: Bus emit validation failed',
|
|
539
|
+
} as never);
|
|
540
|
+
|
|
541
|
+
// Still on configure-search, failure visible, retry available.
|
|
542
|
+
expect(await screen.findByText(new RegExp(T.searchFailed))).toBeInTheDocument();
|
|
543
|
+
expect(screen.getByText(/Bus emit validation failed/)).toBeInTheDocument();
|
|
544
|
+
expect(screen.getByRole('button', { name: T.search })).toBeEnabled();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('a failure addressed to a different annotation changes nothing', async () => {
|
|
548
|
+
// Same scoping rule as results: a late failure from another annotation's
|
|
549
|
+
// run must not settle THIS spinner or paint an error that isn't ours.
|
|
550
|
+
const { client } = renderWizard();
|
|
551
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
552
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
553
|
+
|
|
554
|
+
client.bus.get('match:search-failed').next({
|
|
555
|
+
correlationId: 'c-2',
|
|
556
|
+
referenceId: 'someone-elses-annotation',
|
|
557
|
+
error: 'not ours',
|
|
558
|
+
} as never);
|
|
559
|
+
|
|
560
|
+
expect(screen.getByRole('button', { name: T.searching })).toBeDisabled();
|
|
561
|
+
expect(screen.queryByText(new RegExp(T.searchFailed))).not.toBeInTheDocument();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('retrying after a failure clears the error while the new search runs', async () => {
|
|
565
|
+
const { client } = renderWizard();
|
|
566
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
567
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
568
|
+
client.bus.get('match:search-failed').next({
|
|
569
|
+
correlationId: 'c-1', referenceId: 'ann-1', error: 'boom',
|
|
570
|
+
} as never);
|
|
571
|
+
expect(await screen.findByText(new RegExp(T.searchFailed))).toBeInTheDocument();
|
|
572
|
+
|
|
573
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
574
|
+
expect(screen.queryByText(new RegExp(T.searchFailed))).not.toBeInTheDocument();
|
|
575
|
+
expect(screen.getByRole('button', { name: T.searching })).toBeDisabled();
|
|
576
|
+
});
|
|
577
|
+
|
|
199
578
|
it('ignores results addressed to a different annotation', () => {
|
|
200
579
|
// Two wizards can never be open at once, but one reply can outlive the
|
|
201
580
|
// annotation it was asked for — a late answer must not hijack the step.
|
|
@@ -204,12 +583,12 @@ describe('ReferenceWizardModal — the three strategies complete', () => {
|
|
|
204
583
|
referenceId: 'someone-elses-annotation',
|
|
205
584
|
response: [{ '@id': 'res-9', name: 'Caspian Sea' }],
|
|
206
585
|
} as never);
|
|
207
|
-
expect(screen.
|
|
586
|
+
expect(screen.getByRole('button', { name: `🔍 ${T.search}…` })).toBeInTheDocument(); // still the gather step
|
|
208
587
|
});
|
|
209
588
|
|
|
210
589
|
it('generation submits the step\'s config against this annotation, then closes', async () => {
|
|
211
590
|
const { onGenerateSubmit, onClose } = renderWizard();
|
|
212
|
-
await userEvent.click(screen.
|
|
591
|
+
await userEvent.click(screen.getByRole('button', { name: `✨ ${T.generate}…` }));
|
|
213
592
|
await userEvent.type(screen.getByLabelText(/Save location/i), 'generated/out.md');
|
|
214
593
|
await userEvent.click(screen.getByRole('button', { name: T.generate }));
|
|
215
594
|
|
|
@@ -221,10 +600,10 @@ describe('ReferenceWizardModal — the three strategies complete', () => {
|
|
|
221
600
|
|
|
222
601
|
it('retreats from a configure step back to the gather step', async () => {
|
|
223
602
|
renderWizard();
|
|
224
|
-
await userEvent.click(screen.
|
|
225
|
-
expect(screen.
|
|
603
|
+
await userEvent.click(screen.getByRole('button', { name: `✨ ${T.generate}…` }));
|
|
604
|
+
expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument();
|
|
226
605
|
await userEvent.click(screen.getByRole('button', { name: /Back/ }));
|
|
227
|
-
expect(screen.
|
|
606
|
+
expect(screen.getByRole('button', { name: `🔍 ${T.search}…` })).toBeInTheDocument();
|
|
228
607
|
});
|
|
229
608
|
});
|
|
230
609
|
|
|
@@ -236,11 +615,11 @@ describe('ReferenceWizardModal — nothing fires without an annotation to resolv
|
|
|
236
615
|
beforeEach(() => { vi.clearAllMocks(); });
|
|
237
616
|
|
|
238
617
|
it('compose, search and generate all no-op', async () => {
|
|
239
|
-
const {
|
|
618
|
+
const { onGenerateSubmit, searchSpy, client } =
|
|
240
619
|
renderWizard({ annotationId: null, resourceId: null });
|
|
241
620
|
|
|
242
621
|
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
243
|
-
expect(
|
|
622
|
+
expect(screen.queryByRole('button', { name: T.createAndLink })).not.toBeInTheDocument();
|
|
244
623
|
|
|
245
624
|
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
246
625
|
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
@@ -250,7 +629,7 @@ describe('ReferenceWizardModal — nothing fires without an annotation to resolv
|
|
|
250
629
|
client.bus.get('match:search-results').next({
|
|
251
630
|
referenceId: 'ann-1', response: [{ '@id': 'res-9', name: 'X' }],
|
|
252
631
|
} as never);
|
|
253
|
-
expect(screen.
|
|
632
|
+
expect(screen.queryByRole('button', { name: new RegExp(T.link) })).not.toBeInTheDocument();
|
|
254
633
|
expect(onGenerateSubmit).not.toHaveBeenCalled();
|
|
255
634
|
});
|
|
256
635
|
});
|