@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
|
@@ -1,79 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GENERATE-FROM-BUTTON P2/P4
|
|
2
|
+
* GENERATE-FROM-BUTTON P2/P4 → FLOW-LIFECYCLE-CONVERGENCE P3 →
|
|
3
|
+
* GATHER-AT-THE-TOP P2 — the resource-generate flow modal.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* One composite stack, no step machine: the gather controls sit at the TOP,
|
|
6
|
+
* the gathered evidence appears below them once gather fires, and the
|
|
7
|
+
* generation params render beneath the evidence once context arrives —
|
|
8
|
+
* [gather zone] → [evidence] → [act zone], the same grammar the resolve
|
|
9
|
+
* wizard expresses. Gather state arrives as PROPS (the page reads
|
|
10
|
+
* `gather.resourceContext$` and friends off the state unit) and the gather
|
|
11
|
+
* itself is an `onGather` callback — the modal mocks no hook and reaches for
|
|
12
|
+
* no provider (FLC A6). Entity-type options are owner-supplied, so a failed
|
|
13
|
+
* load cannot surface as an empty vocabulary.
|
|
8
14
|
*/
|
|
9
15
|
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
10
16
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
17
|
import '@testing-library/jest-dom';
|
|
12
|
-
import { BehaviorSubject, of } from 'rxjs';
|
|
13
18
|
import type { GatheredContext } from '@semiont/core';
|
|
19
|
+
import type { ResourceGatherOptions } from '@semiont/sdk';
|
|
14
20
|
import type { GenerationConfig } from '../ConfigureGenerationStep';
|
|
21
|
+
import { ResourceGenerateModal } from '../ResourceGenerateModal';
|
|
15
22
|
|
|
16
23
|
const RESOURCE_CONTEXT = {
|
|
17
|
-
focus: { kind: 'resource', resource: { id: 'res-1', name: 'My Resource' }, summary: 'A short summary' },
|
|
24
|
+
focus: { kind: 'resource', resource: { '@id': 'res-1', name: 'My Resource' }, summary: 'A short summary' },
|
|
18
25
|
graph: { nodes: [{ id: 'res-1', type: 'resource', label: 'My Resource' }], edges: [] },
|
|
19
26
|
metadata: {},
|
|
20
27
|
} as unknown as GatheredContext;
|
|
21
28
|
|
|
22
|
-
// Mutable hook state + spies, shared with the hoisted vi.mock factory.
|
|
23
|
-
const h = vi.hoisted(() => ({
|
|
24
|
-
gather: vi.fn(),
|
|
25
|
-
reset: vi.fn(),
|
|
26
|
-
state: { context: null as unknown, loading: false, error: null as Error | null },
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
vi.mock('../../../hooks/useResourceGather', () => ({
|
|
30
|
-
useResourceGather: () => ({
|
|
31
|
-
context: h.state.context,
|
|
32
|
-
loading: h.state.loading,
|
|
33
|
-
error: h.state.error,
|
|
34
|
-
gather: h.gather,
|
|
35
|
-
reset: h.reset,
|
|
36
|
-
}),
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
// The modal no longer fetches entity types; the owner supplies them, so a
|
|
40
|
-
// failed load cannot surface as an empty vocabulary. The stable-observable
|
|
41
|
-
// note below is kept because the client mock still backs other calls.
|
|
42
|
-
// See .plans/PANEL-FAILURE-STATES.md
|
|
43
|
-
// Stable observable instance — the modal calls `entityTypes()` inline in
|
|
44
|
-
// render, so a fresh observable each call would re-subscribe every render and
|
|
45
|
-
// loop. The real SDK returns a cached observable; mirror that here.
|
|
46
|
-
const entityTypes$ = of<string[]>(['Person', 'Topic']);
|
|
47
|
-
const mockClient = { browse: { entityTypes: () => entityTypes$ } };
|
|
48
|
-
const activeSession$ = new BehaviorSubject<unknown>({ client: mockClient });
|
|
49
|
-
vi.mock('../../../session/SemiontProvider', async () => {
|
|
50
|
-
const actual = await vi.importActual<typeof import('../../../session/SemiontProvider')>('../../../session/SemiontProvider');
|
|
51
|
-
return { ...actual, useSemiont: () => ({ activeSession$ }) };
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
import { ResourceGenerateModal } from '../ResourceGenerateModal';
|
|
55
|
-
|
|
56
29
|
const T = {
|
|
57
|
-
|
|
58
|
-
reviewTitle: 'Review Context',
|
|
59
|
-
configureTitle: 'Configure Generation',
|
|
60
|
-
next: 'Next',
|
|
61
|
-
back: 'Back',
|
|
62
|
-
cancel: 'Cancel',
|
|
30
|
+
title: 'Generate from this Resource',
|
|
63
31
|
gatherIntro: 'Choose what to include.',
|
|
64
32
|
includeContent: 'Include content',
|
|
65
33
|
includeSummary: 'Include summary',
|
|
66
34
|
gatherDepth: 'Depth',
|
|
67
35
|
gatherMaxResources: 'Max resources',
|
|
68
36
|
gatherButton: 'Gather',
|
|
69
|
-
|
|
37
|
+
recallLabel: 'Included in recall — deselect to exclude',
|
|
70
38
|
loadingContext: 'Gathering…',
|
|
71
39
|
failedContext: 'Failed',
|
|
72
40
|
sourceContextLabel: 'Resource',
|
|
73
41
|
connectionsLabel: 'Connections',
|
|
74
42
|
citedByLabel: 'Cited by',
|
|
43
|
+
graphPaneTitle: 'In the graph',
|
|
44
|
+
graphEmpty: 'No links yet.', resourceLinkLabel: 'Resource link',
|
|
45
|
+
corpusPaneTitle: 'In the corpus',
|
|
46
|
+
corpusEmpty: 'Nothing similar in the corpus.',
|
|
47
|
+
excludedReceipt: '{{types}} excluded from this recall',
|
|
48
|
+
machineRead: 'OCR',
|
|
49
|
+
score: 'Score',
|
|
75
50
|
resourceTitle: 'New resource title',
|
|
76
51
|
resourceTitlePlaceholder: 'Title…',
|
|
52
|
+
saveLocation: 'Save location',
|
|
77
53
|
additionalInstructions: 'Additional Instructions',
|
|
78
54
|
additionalInstructionsPlaceholder: 'Optional…',
|
|
79
55
|
language: 'Language',
|
|
@@ -84,11 +60,18 @@ const T = {
|
|
|
84
60
|
maxLength: 'Max Length',
|
|
85
61
|
maxLengthHelp: 'Max help',
|
|
86
62
|
maxLengthCeiling: 'Limited to {{maxOutputTokens}} tokens by {{model}}.',
|
|
63
|
+
outputFormat: 'Format',
|
|
64
|
+
formatExtensionMismatch: 'Save location must end in {{extension}} to match the selected format.',
|
|
87
65
|
generate: 'Generate',
|
|
66
|
+
discardDraftPrompt: 'Discard this draft?',
|
|
67
|
+
discardDraft: 'Discard',
|
|
68
|
+
keepEditing: 'Keep editing',
|
|
69
|
+
editGather: 'Change gather settings',
|
|
88
70
|
};
|
|
89
71
|
|
|
90
72
|
let onClose: Mock<() => void>;
|
|
91
73
|
let onGenerateSubmit: Mock<(resourceId: string, config: GenerationConfig) => void>;
|
|
74
|
+
let onGather: Mock<(options: ResourceGatherOptions) => void>;
|
|
92
75
|
|
|
93
76
|
function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerateModal>> = {}) {
|
|
94
77
|
return render(
|
|
@@ -100,6 +83,10 @@ function renderModal(props: Partial<React.ComponentProps<typeof ResourceGenerate
|
|
|
100
83
|
locale="en"
|
|
101
84
|
entityTypeOptions={['Person', 'Topic']}
|
|
102
85
|
onGenerateSubmit={onGenerateSubmit}
|
|
86
|
+
gatherContext={null}
|
|
87
|
+
gatherLoading={false}
|
|
88
|
+
gatherError={null}
|
|
89
|
+
onGather={onGather}
|
|
103
90
|
translations={T}
|
|
104
91
|
{...props}
|
|
105
92
|
/>,
|
|
@@ -111,39 +98,81 @@ describe('ResourceGenerateModal', () => {
|
|
|
111
98
|
vi.clearAllMocks();
|
|
112
99
|
onClose = vi.fn<() => void>();
|
|
113
100
|
onGenerateSubmit = vi.fn<(resourceId: string, config: GenerationConfig) => void>();
|
|
114
|
-
|
|
115
|
-
activeSession$.next({ client: mockClient });
|
|
101
|
+
onGather = vi.fn<(options: ResourceGatherOptions) => void>();
|
|
116
102
|
// jsdom doesn't implement scrollIntoView; GatherContextStep may call it.
|
|
117
103
|
Element.prototype.scrollIntoView = vi.fn();
|
|
118
104
|
});
|
|
119
105
|
|
|
120
106
|
it('renders nothing when closed', () => {
|
|
121
107
|
renderModal({ isOpen: false });
|
|
122
|
-
expect(screen.queryByText(
|
|
108
|
+
expect(screen.queryByText(T.title)).not.toBeInTheDocument();
|
|
123
109
|
});
|
|
124
110
|
|
|
125
|
-
it('opens
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
111
|
+
it('opens with the gather controls at the top and nothing below them yet', () => {
|
|
112
|
+
// Stale slots must not leak a previous run's evidence into a fresh open:
|
|
113
|
+
// even with a context PROP already present, nothing renders below the
|
|
114
|
+
// controls until THIS run's gather fires.
|
|
115
|
+
const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
116
|
+
expect(screen.getByText(T.title)).toBeInTheDocument(); // the one modal title
|
|
117
|
+
expect(screen.getByText(T.gatherIntro)).toBeInTheDocument();
|
|
118
|
+
expect(screen.getByLabelText(T.includeContent)).toBeInTheDocument();
|
|
130
119
|
expect(screen.getByRole('button', { name: 'Person' })).toBeInTheDocument();
|
|
131
120
|
expect(screen.getByRole('button', { name: 'Topic' })).toBeInTheDocument();
|
|
121
|
+
expect(baseElement.querySelector('.semiont-wizard__step-scroll')).not.toBeNull();
|
|
122
|
+
expect(screen.queryByText(T.graphPaneTitle)).not.toBeInTheDocument();
|
|
123
|
+
expect(screen.queryByLabelText(T.resourceTitle)).not.toBeInTheDocument();
|
|
132
124
|
});
|
|
133
125
|
|
|
134
|
-
it('
|
|
126
|
+
it('Gather emits onGather; the spent controls collapse to a receipt of what fired', () => {
|
|
127
|
+
// The modal no longer knows how to gather — it says WHAT to gather and the
|
|
128
|
+
// page wires the state unit (FLC D3). No resourceId in the payload: the
|
|
129
|
+
// owner already holds it.
|
|
130
|
+
const { baseElement } = renderModal({ gatherLoading: true });
|
|
131
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
132
|
+
expect(onGather).toHaveBeenCalledWith({ includeContent: true, includeSummary: true, depth: 2, maxResources: 10 });
|
|
133
|
+
expect(baseElement.querySelector('.semiont-gather__loading')).not.toBeNull();
|
|
134
|
+
// The form's job is done: it folds into a one-line receipt of what THIS
|
|
135
|
+
// gather asked for, and the evidence leads the pane. The form stays
|
|
136
|
+
// mounted (hidden) so its values survive for the expand path.
|
|
137
|
+
const receipt = screen.getByTitle(T.editGather);
|
|
138
|
+
expect(receipt.textContent).toContain(`${T.gatherDepth} 2`);
|
|
139
|
+
expect(receipt.textContent).toContain(`${T.gatherMaxResources} 10`);
|
|
140
|
+
expect(receipt.textContent).toContain(T.includeContent);
|
|
141
|
+
expect(screen.getByText(T.gatherIntro)).not.toBeVisible();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('the receipt names the exclusions of THIS recall', () => {
|
|
135
145
|
renderModal();
|
|
146
|
+
fireEvent.click(screen.getByRole('button', { name: 'Person' }));
|
|
136
147
|
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
137
|
-
expect(
|
|
138
|
-
|
|
139
|
-
expect(screen.getByRole('button', { name: /Next/ })).toBeDisabled(); // no context yet
|
|
148
|
+
expect(screen.getByTitle(T.editGather).textContent)
|
|
149
|
+
.toContain('Person excluded from this recall');
|
|
140
150
|
});
|
|
141
151
|
|
|
142
|
-
it('
|
|
152
|
+
it('the receipt expands back to the form — values preserved — and re-gather collapses again', () => {
|
|
153
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
154
|
+
fireEvent.change(screen.getByLabelText(T.gatherDepth), { target: { value: '4' } });
|
|
155
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
156
|
+
expect(screen.getByTitle(T.editGather).textContent).toContain(`${T.gatherDepth} 4`);
|
|
157
|
+
|
|
158
|
+
fireEvent.click(screen.getByTitle(T.editGather));
|
|
159
|
+
const depthInput = screen.getByLabelText(T.gatherDepth);
|
|
160
|
+
expect(depthInput).toBeVisible();
|
|
161
|
+
expect(depthInput).toHaveValue(4); // not re-initialized — the form never unmounted
|
|
162
|
+
expect(screen.queryByTitle(T.editGather)).not.toBeInTheDocument();
|
|
163
|
+
|
|
164
|
+
fireEvent.change(depthInput, { target: { value: '3' } });
|
|
165
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
166
|
+
expect(onGather).toHaveBeenCalledTimes(2);
|
|
167
|
+
expect(onGather).toHaveBeenLastCalledWith(expect.objectContaining({ depth: 3 }));
|
|
168
|
+
expect(screen.getByTitle(T.editGather).textContent).toContain(`${T.gatherDepth} 3`);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('threads crossed-off entity types into the gather options as the exclude list', () => {
|
|
143
172
|
renderModal();
|
|
144
|
-
fireEvent.click(screen.getByRole('button', { name: 'Person' })); //
|
|
173
|
+
fireEvent.click(screen.getByRole('button', { name: 'Person' })); // cross off the recall
|
|
145
174
|
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
146
|
-
expect(
|
|
175
|
+
expect(onGather).toHaveBeenCalledWith({
|
|
147
176
|
includeContent: true,
|
|
148
177
|
includeSummary: true,
|
|
149
178
|
depth: 2,
|
|
@@ -152,17 +181,34 @@ describe('ResourceGenerateModal', () => {
|
|
|
152
181
|
});
|
|
153
182
|
});
|
|
154
183
|
|
|
155
|
-
it('
|
|
156
|
-
|
|
184
|
+
it('every entity type starts INCLUDED — deselecting is what excludes (recall chips)', () => {
|
|
185
|
+
// The SDK's wire is an exclude list, but the natural posture is
|
|
186
|
+
// "everything is in the recall until you say otherwise": chips render
|
|
187
|
+
// included by default and a click crosses one off.
|
|
157
188
|
renderModal();
|
|
189
|
+
expect(screen.getByText(T.recallLabel)).toBeInTheDocument();
|
|
190
|
+
const person = screen.getByRole('button', { name: 'Person' });
|
|
191
|
+
const topic = screen.getByRole('button', { name: 'Topic' });
|
|
192
|
+
expect(person).toHaveAttribute('aria-pressed', 'true');
|
|
193
|
+
expect(person).toHaveAttribute('data-included', 'true');
|
|
194
|
+
expect(person.className).toContain('semiont-form__recall-chip');
|
|
195
|
+
|
|
196
|
+
fireEvent.click(person);
|
|
197
|
+
expect(person).toHaveAttribute('aria-pressed', 'false');
|
|
198
|
+
expect(person).toHaveAttribute('data-included', 'false');
|
|
199
|
+
expect(topic).toHaveAttribute('aria-pressed', 'true'); // the rest stay in
|
|
158
200
|
|
|
159
|
-
fireEvent.click(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
fireEvent.click(person); // back in — the toggle round-trips
|
|
202
|
+
expect(person).toHaveAttribute('aria-pressed', 'true');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('params render beneath the evidence once context arrives; Generate emits and closes', () => {
|
|
206
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT }); // gather resolves immediately
|
|
163
207
|
|
|
164
|
-
fireEvent.
|
|
165
|
-
|
|
208
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
209
|
+
|
|
210
|
+
fireEvent.change(screen.getByLabelText(T.resourceTitle), { target: { value: 'Generated Doc' } });
|
|
211
|
+
fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.md' } });
|
|
166
212
|
fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
|
|
167
213
|
|
|
168
214
|
expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
|
|
@@ -170,29 +216,66 @@ describe('ResourceGenerateModal', () => {
|
|
|
170
216
|
'res-1',
|
|
171
217
|
expect.objectContaining({
|
|
172
218
|
title: 'Generated Doc',
|
|
173
|
-
|
|
219
|
+
// D8: the submitted payload names this what it IS — a full storage
|
|
220
|
+
// URI, built here by prefixing `file://`. It was called `storagePath`
|
|
221
|
+
// while holding a URI, which is why the page handlers had to rename
|
|
222
|
+
// it field-by-field on the way out.
|
|
223
|
+
storageUri: 'file://generated/out.md',
|
|
224
|
+
// D2: the default is sent, not inherited from the worker.
|
|
225
|
+
outputMediaType: 'text/markdown',
|
|
174
226
|
context: RESOURCE_CONTEXT,
|
|
175
227
|
}),
|
|
176
228
|
);
|
|
177
229
|
expect(onClose).toHaveBeenCalled();
|
|
178
230
|
});
|
|
179
231
|
|
|
180
|
-
it('
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fireEvent.
|
|
185
|
-
|
|
186
|
-
fireEvent.
|
|
187
|
-
|
|
232
|
+
it('a chosen output format survives the host to onGenerateSubmit', () => {
|
|
233
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
234
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
235
|
+
|
|
236
|
+
fireEvent.change(screen.getByLabelText(T.resourceTitle), { target: { value: 'Generated Doc' } });
|
|
237
|
+
fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.pdf' } });
|
|
238
|
+
fireEvent.change(screen.getByLabelText(T.outputFormat), { target: { value: 'application/pdf' } });
|
|
239
|
+
fireEvent.click(screen.getByRole('button', { name: /Generate/ }));
|
|
240
|
+
|
|
241
|
+
expect(onGenerateSubmit).toHaveBeenCalledWith(
|
|
242
|
+
'res-1',
|
|
243
|
+
expect.objectContaining({ outputMediaType: 'application/pdf', storageUri: 'file://generated/out.pdf' }),
|
|
244
|
+
);
|
|
188
245
|
});
|
|
189
246
|
|
|
190
|
-
it('
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
fireEvent.click(screen.getByRole('button', { name: /
|
|
195
|
-
|
|
247
|
+
it('a failed gather shows its failure in place', () => {
|
|
248
|
+
// The props are the state unit's slots verbatim; the modal renders them
|
|
249
|
+
// without owning them (FLC A6).
|
|
250
|
+
renderModal({ gatherLoading: true });
|
|
251
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
252
|
+
|
|
253
|
+
render(
|
|
254
|
+
<ResourceGenerateModal
|
|
255
|
+
isOpen onClose={onClose} resourceId="res-1" defaultTitle="Default Title"
|
|
256
|
+
locale="en" entityTypeOptions={['Person', 'Topic']} onGenerateSubmit={onGenerateSubmit}
|
|
257
|
+
gatherContext={null} gatherLoading={false} gatherError={new Error('boom')}
|
|
258
|
+
onGather={onGather} translations={T}
|
|
259
|
+
/>,
|
|
260
|
+
);
|
|
261
|
+
fireEvent.click(screen.getAllByRole('button', { name: /Gather/ }).at(-1)!);
|
|
262
|
+
expect(screen.getByText(T.failedContext)).toBeInTheDocument();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('re-Gather refreshes the evidence in place — the stack stays', () => {
|
|
266
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
267
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
268
|
+
fireEvent.click(screen.getByTitle(T.editGather)); // expand the receipt
|
|
269
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // tweak-and-regather
|
|
270
|
+
expect(onGather).toHaveBeenCalledTimes(2);
|
|
271
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument();
|
|
272
|
+
expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('no Back anywhere — a single stack has nothing to go back to (D6)', () => {
|
|
276
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
277
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
278
|
+
expect(screen.queryAllByRole('button', { name: /Back/ })).toHaveLength(0);
|
|
196
279
|
});
|
|
197
280
|
|
|
198
281
|
it('the header close button calls onClose', () => {
|
|
@@ -200,4 +283,135 @@ describe('ResourceGenerateModal', () => {
|
|
|
200
283
|
fireEvent.click(screen.getByLabelText('Close'));
|
|
201
284
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
202
285
|
});
|
|
286
|
+
|
|
287
|
+
it('opening seeds the proposed title from defaultTitle, even one that arrived after mount (GFR A4)', () => {
|
|
288
|
+
// The page passes the source resource's name, which loads asynchronously —
|
|
289
|
+
// a draft seeded only in the useState initializer holds whatever was there
|
|
290
|
+
// at FIRST render, which for the real page was ''. Opening is the moment
|
|
291
|
+
// that matters: the draft re-seeds then.
|
|
292
|
+
const { rerender } = render(
|
|
293
|
+
<ResourceGenerateModal
|
|
294
|
+
isOpen={false} onClose={onClose} resourceId="res-1" defaultTitle=""
|
|
295
|
+
locale="en" entityTypeOptions={[]} onGenerateSubmit={onGenerateSubmit}
|
|
296
|
+
gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
|
|
297
|
+
onGather={onGather} translations={T}
|
|
298
|
+
/>,
|
|
299
|
+
);
|
|
300
|
+
rerender(
|
|
301
|
+
<ResourceGenerateModal
|
|
302
|
+
isOpen onClose={onClose} resourceId="res-1" defaultTitle="PB"
|
|
303
|
+
locale="en" entityTypeOptions={[]} onGenerateSubmit={onGenerateSubmit}
|
|
304
|
+
gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
|
|
305
|
+
onGather={onGather} translations={T}
|
|
306
|
+
/>,
|
|
307
|
+
);
|
|
308
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
309
|
+
expect(screen.getByLabelText(T.resourceTitle)).toHaveValue('PB');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('evidence stays in view above the params — ONE scroll pane, nothing scrolls alone (A3)', () => {
|
|
313
|
+
const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
314
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
315
|
+
|
|
316
|
+
const scroll = baseElement.querySelector('.semiont-wizard__step-scroll');
|
|
317
|
+
expect(scroll).not.toBeNull();
|
|
318
|
+
expect(scroll!.querySelector('.semiont-gather__outer')).not.toBeNull(); // the evidence
|
|
319
|
+
expect(screen.getByText(T.graphPaneTitle)).toBeInTheDocument(); // graph pane up
|
|
320
|
+
expect(screen.getByLabelText(T.resourceTitle)).toBeInTheDocument(); // the form, below
|
|
321
|
+
for (const form of Array.from(scroll!.querySelectorAll('form.semiont-form'))) {
|
|
322
|
+
expect(form.className).not.toContain('semiont-form--scrollable');
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('the panel carries the widened class (GEP P2, D2)', () => {
|
|
327
|
+
const { baseElement } = renderModal();
|
|
328
|
+
const panel = baseElement.querySelector('.semiont-search-modal__panel--gather');
|
|
329
|
+
expect(panel).not.toBeNull();
|
|
330
|
+
expect(panel!.className).toContain('semiont-search-modal__panel--wide');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('every footer is the wizard footer — no dismissal, no flex (GFR A5)', () => {
|
|
334
|
+
// The modal renders via a HeadlessUI portal, so query the whole document.
|
|
335
|
+
const { baseElement } = renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
336
|
+
const footerPins = () => {
|
|
337
|
+
expect(baseElement.querySelector('.semiont-modal__actions--wizard')).not.toBeNull();
|
|
338
|
+
expect(baseElement.querySelectorAll('.semiont-button--flex')).toHaveLength(0);
|
|
339
|
+
const footerButtons = Array.from(
|
|
340
|
+
baseElement.querySelectorAll('.semiont-modal__actions button'),
|
|
341
|
+
).map((b) => b.textContent ?? '');
|
|
342
|
+
expect(footerButtons.filter((l) => /cancel|✕/i.test(l))).toEqual([]);
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
footerPins(); // controls alone
|
|
346
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
347
|
+
footerPins(); // the full stack: Gather footer + Generate footer
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// GATHER-AT-THE-TOP P1: dismissal guards typed work. D5 — typed text only
|
|
352
|
+
// (title beyond the seeded default, save location, instructions); checkbox,
|
|
353
|
+
// depth, and exclusion state is cheap to redo and never nags.
|
|
354
|
+
describe('ResourceGenerateModal — dismissal guards typed work (GATHER-AT-THE-TOP P1)', () => {
|
|
355
|
+
beforeEach(() => {
|
|
356
|
+
onClose = vi.fn();
|
|
357
|
+
onGenerateSubmit = vi.fn();
|
|
358
|
+
onGather = vi.fn();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('a typed save location guards dismissal; Keep editing stays, Discard closes', () => {
|
|
362
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
363
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ })); // reveal the stack
|
|
364
|
+
fireEvent.change(screen.getByLabelText(T.saveLocation), { target: { value: 'generated/out.md' } });
|
|
365
|
+
|
|
366
|
+
fireEvent.click(screen.getByLabelText('Close'));
|
|
367
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
368
|
+
expect(screen.getByText(T.discardDraftPrompt)).toBeInTheDocument();
|
|
369
|
+
|
|
370
|
+
fireEvent.click(screen.getByRole('button', { name: T.keepEditing }));
|
|
371
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
372
|
+
expect(screen.getByLabelText(T.saveLocation)).toHaveValue('generated/out.md');
|
|
373
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
374
|
+
|
|
375
|
+
fireEvent.click(screen.getByLabelText('Close'));
|
|
376
|
+
fireEvent.click(screen.getByRole('button', { name: T.discardDraft }));
|
|
377
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('a clean draft dismisses without any prompt', () => {
|
|
381
|
+
renderModal({ gatherContext: RESOURCE_CONTEXT });
|
|
382
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
383
|
+
|
|
384
|
+
fireEvent.click(screen.getByLabelText('Close'));
|
|
385
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
386
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('a defaultTitle arriving mid-flow does not fake a dirty draft', () => {
|
|
390
|
+
// The source resource's name loads asynchronously: the baseline moves
|
|
391
|
+
// while the modal is open, the user has typed nothing. Dirtiness compares
|
|
392
|
+
// against what was SEEDED, not the live prop.
|
|
393
|
+
const { rerender } = renderModal({ defaultTitle: '', gatherContext: RESOURCE_CONTEXT });
|
|
394
|
+
fireEvent.click(screen.getByRole('button', { name: /Gather/ }));
|
|
395
|
+
rerender(
|
|
396
|
+
<ResourceGenerateModal
|
|
397
|
+
isOpen onClose={onClose} resourceId="res-1" defaultTitle="Loaded Name"
|
|
398
|
+
locale="en" entityTypeOptions={['Person', 'Topic']} onGenerateSubmit={onGenerateSubmit}
|
|
399
|
+
gatherContext={RESOURCE_CONTEXT} gatherLoading={false} gatherError={null}
|
|
400
|
+
onGather={onGather} translations={T}
|
|
401
|
+
/>,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
fireEvent.click(screen.getByLabelText('Close'));
|
|
405
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
406
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('non-text state alone never nags — exclusions and depth are cheap to redo (D5)', () => {
|
|
410
|
+
renderModal();
|
|
411
|
+
fireEvent.click(screen.getByRole('button', { name: 'Person' })); // cross off the recall
|
|
412
|
+
|
|
413
|
+
fireEvent.click(screen.getByLabelText('Close'));
|
|
414
|
+
expect(screen.queryByText(T.discardDraftPrompt)).not.toBeInTheDocument();
|
|
415
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
416
|
+
});
|
|
203
417
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GATHER-EVIDENCE-PANES D10 (amended 2026-08-19) — `SearchResultsStep` is pure
|
|
3
|
+
* RESULTS: ranked rows with scores and Link buttons, nothing else.
|
|
4
|
+
*
|
|
5
|
+
* The evidence lives with the HOST: the wizard stacks the full display-only
|
|
6
|
+
* `GatherContextStep` (quotation, graph pane, corpus pane, collapsed strategy
|
|
7
|
+
* band) above this step, exactly as on the configure steps. The original D10
|
|
8
|
+
* concern — corpus matches beside real results reading as a second, staler
|
|
9
|
+
* ranking — dissolved with the stacking: evidence sits above the strategy
|
|
10
|
+
* band, results below it, no competing side-by-side columns. This step
|
|
11
|
+
* therefore renders NO context of its own; a gather pane appearing here means
|
|
12
|
+
* someone re-embedded the minimal context redux this amendment deleted.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
15
|
+
import { render, screen } from '@testing-library/react';
|
|
16
|
+
import '@testing-library/jest-dom';
|
|
17
|
+
import { SearchResultsStep } from '../SearchResultsStep';
|
|
18
|
+
|
|
19
|
+
const T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back' };
|
|
20
|
+
|
|
21
|
+
const RESULTS = [
|
|
22
|
+
{ '@id': 'r1', name: 'Martin Pauls', score: 57.46, matchReason: 'connected; semantic match' },
|
|
23
|
+
{ '@id': 'r2', name: 'Arthur Fisher', score: 25.46, description: 'a citing resource' },
|
|
24
|
+
] as never[];
|
|
25
|
+
|
|
26
|
+
describe('SearchResultsStep — pure results (D10 amended)', () => {
|
|
27
|
+
it('renders ranked rows: name, score, reason, and a Link button each', () => {
|
|
28
|
+
render(
|
|
29
|
+
<SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
|
|
30
|
+
);
|
|
31
|
+
expect(screen.getByText('Martin Pauls')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText(`${T.score}: 57.46`)).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText('connected; semantic match')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('a citing resource')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getAllByRole('button', { name: `🔗 ${T.link}` })).toHaveLength(2);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('links the row that was clicked, by @id', () => {
|
|
39
|
+
const onLink = vi.fn();
|
|
40
|
+
render(
|
|
41
|
+
<SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={onLink} translations={T} />,
|
|
42
|
+
);
|
|
43
|
+
screen.getAllByRole('button', { name: `🔗 ${T.link}` })[1]!.click();
|
|
44
|
+
expect(onLink).toHaveBeenCalledWith('r2');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('shows the empty state when nothing matched', () => {
|
|
48
|
+
render(
|
|
49
|
+
<SearchResultsStep results={[]} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
|
|
50
|
+
);
|
|
51
|
+
expect(screen.getByText(T.noResults)).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders no context of its own — the host owns the evidence', () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<SearchResultsStep results={RESULTS} onBack={vi.fn()} onLink={vi.fn()} translations={T} />,
|
|
57
|
+
);
|
|
58
|
+
expect(container.querySelector('.semiont-gather-pane')).toBeNull();
|
|
59
|
+
expect(container.querySelector('.semiont-search-results__two-pane')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|