@semiont/react-ui 0.5.25 → 0.5.27
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 +28 -4
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js +677 -0
- package/dist/PdfAnnotationCanvas.client-EVUPTUWZ.js.map +1 -0
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css +185 -0
- package/dist/PdfAnnotationCanvas.client-TGDPPGXJ.css.map +1 -0
- package/dist/{ar-CULRHLNG.js → ar-OU3DQGC6.js} +65 -7
- package/dist/ar-OU3DQGC6.js.map +1 -0
- package/dist/{bn-HYR7CEL2.js → bn-PHWCR6AZ.js} +65 -7
- package/dist/bn-PHWCR6AZ.js.map +1 -0
- package/dist/chunk-5VDTWRXJ.js +287 -0
- package/dist/chunk-5VDTWRXJ.js.map +1 -0
- package/dist/{chunk-TG2XQEC4.js → chunk-BTUG3VJJ.js} +18 -298
- package/dist/chunk-BTUG3VJJ.js.map +1 -0
- package/dist/{chunk-NH6L2GIL.js → chunk-PCRRBU7H.js} +65 -7
- package/dist/chunk-PCRRBU7H.js.map +1 -0
- package/dist/{cs-YBRWY7NH.js → cs-NEXYZ62I.js} +65 -7
- package/dist/cs-NEXYZ62I.js.map +1 -0
- package/dist/{da-PHDEOBV3.js → da-FAT43YCU.js} +65 -7
- package/dist/da-FAT43YCU.js.map +1 -0
- package/dist/{de-O4UCCVLJ.js → de-KRQ3WBCU.js} +65 -7
- package/dist/de-KRQ3WBCU.js.map +1 -0
- package/dist/{el-MVGO6WZV.js → el-Q4IU3EKC.js} +65 -7
- package/dist/el-Q4IU3EKC.js.map +1 -0
- package/dist/{en-2KQILFKQ.js → en-2KADE7N4.js} +6 -2
- package/dist/{es-USSLXQY7.js → es-2IO3R2MK.js} +65 -7
- package/dist/es-2IO3R2MK.js.map +1 -0
- package/dist/{fa-WMAREFWY.js → fa-Q5GOQFXX.js} +65 -7
- package/dist/fa-Q5GOQFXX.js.map +1 -0
- package/dist/{fi-B3NBNOTS.js → fi-R6NMRWBE.js} +65 -7
- package/dist/fi-R6NMRWBE.js.map +1 -0
- package/dist/{fr-TXYK5T5W.js → fr-QY4D5E6J.js} +65 -7
- package/dist/fr-QY4D5E6J.js.map +1 -0
- package/dist/{he-JCJACW5D.js → he-X6Y77XHY.js} +65 -7
- package/dist/he-X6Y77XHY.js.map +1 -0
- package/dist/{hi-VOQLZNJU.js → hi-XL7IOEQM.js} +65 -7
- package/dist/hi-XL7IOEQM.js.map +1 -0
- package/dist/{id-BFN5RZNZ.js → id-QZ4ZZS67.js} +65 -7
- package/dist/id-QZ4ZZS67.js.map +1 -0
- package/dist/index.css +241 -508
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +155 -54
- package/dist/index.js +1815 -1642
- package/dist/index.js.map +1 -1
- package/dist/integrations/css-modules-helper.js +1 -3
- package/dist/integrations/css-modules-helper.js.map +1 -1
- package/dist/{it-TWWDHKKM.js → it-ME2Q7OB6.js} +65 -7
- package/dist/it-ME2Q7OB6.js.map +1 -0
- package/dist/{ja-YRQLEBFM.js → ja-HX7QASRI.js} +65 -7
- package/dist/ja-HX7QASRI.js.map +1 -0
- package/dist/{ko-PG6B3CRB.js → ko-EQO5AHAV.js} +65 -7
- package/dist/ko-EQO5AHAV.js.map +1 -0
- package/dist/{ms-7EZECVKM.js → ms-UILFSF3I.js} +65 -7
- package/dist/ms-UILFSF3I.js.map +1 -0
- package/dist/{nl-HP753YAP.js → nl-NNI45PMK.js} +65 -7
- package/dist/nl-NNI45PMK.js.map +1 -0
- package/dist/{no-JUD5WX3N.js → no-MAL5F6P6.js} +65 -7
- package/dist/no-MAL5F6P6.js.map +1 -0
- package/dist/{pl-RVUL4CBM.js → pl-4RMY6JAJ.js} +65 -7
- package/dist/pl-4RMY6JAJ.js.map +1 -0
- package/dist/{pt-RPOVD4QM.js → pt-VQH2X3QQ.js} +65 -7
- package/dist/pt-VQH2X3QQ.js.map +1 -0
- package/dist/{ro-DMYWQTRD.js → ro-XXCQO2OG.js} +65 -7
- package/dist/ro-XXCQO2OG.js.map +1 -0
- package/dist/{sv-DMZ4FGXD.js → sv-7Z6EJ6U4.js} +65 -7
- package/dist/sv-7Z6EJ6U4.js.map +1 -0
- package/dist/test-utils.css +1 -1
- package/dist/test-utils.css.map +1 -1
- package/dist/test-utils.d.ts +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/test-utils.js.map +1 -1
- package/dist/{th-4HVBJBQ2.js → th-L3DXXYVQ.js} +65 -7
- package/dist/th-L3DXXYVQ.js.map +1 -0
- package/dist/{tr-6BDPU5ML.js → tr-P3DFVLFJ.js} +65 -7
- package/dist/tr-P3DFVLFJ.js.map +1 -0
- package/dist/{uk-TBZBT2TL.js → uk-7C5D3PXS.js} +65 -7
- package/dist/uk-7C5D3PXS.js.map +1 -0
- package/dist/{vi-UMKZKZL3.js → vi-ZLTLEO7P.js} +65 -7
- package/dist/vi-ZLTLEO7P.js.map +1 -0
- package/dist/{zh-YBJWJJYB.js → zh-5FHVWDIN.js} +65 -7
- package/dist/zh-5FHVWDIN.js.map +1 -0
- package/package.json +7 -7
- package/src/components/AssistProgress.tsx +144 -134
- package/src/components/{EntityFoundLog.tsx → ItemFoundLog.tsx} +12 -7
- package/src/components/ProtectedErrorBoundary.css +3 -1
- package/src/components/StatusDisplay.css +35 -35
- package/src/components/Toast.css +3 -1
- package/src/components/__tests__/AssistProgress.test.tsx +216 -148
- package/src/components/annotation/annotation-entries.css +0 -4
- package/src/components/annotation/annotations.css +1 -1
- package/src/components/annotation/references.css +0 -4
- package/src/components/image-annotation/AnnotationOverlay.tsx +6 -6
- package/src/components/image-annotation/SvgDrawingCanvas.tsx +1 -1
- package/src/components/image-annotation/__tests__/AnnotationOverlay.click.test.tsx +7 -9
- package/src/components/image-annotation/__tests__/SvgDrawingCanvas.click.test.tsx +1 -2
- package/src/components/layout/Header.css +0 -4
- package/src/components/layout/LeftSidebar.css +0 -4
- package/src/components/modals/ConfigureGenerationStep.tsx +109 -48
- package/src/components/modals/ConfigureSearchStep.tsx +23 -40
- package/src/components/modals/ReferenceWizardModal.tsx +55 -16
- package/src/components/modals/ResourceGenerateModal.tsx +24 -8
- package/src/components/modals/ResourceSearchModal.tsx +1 -1
- package/src/components/modals/SearchModal.css +5 -17
- package/src/components/modals/SearchModal.tsx +1 -1
- package/src/components/modals/SearchResultsStep.tsx +2 -20
- package/src/components/modals/WizardFooter.tsx +63 -0
- package/src/components/modals/__tests__/ConfigureGenerationStep.test.tsx +255 -0
- package/src/components/modals/__tests__/ConfigureSearchStep.test.tsx +72 -0
- package/src/components/modals/__tests__/ReferenceWizardModal.test.tsx +256 -0
- package/src/components/modals/__tests__/ResourceGenerateModal.test.tsx +1 -0
- package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +6 -2
- package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +6 -2
- package/src/components/modals/__tests__/WizardFooter.test.tsx +235 -0
- package/src/components/modals/modals.css +16 -42
- package/src/components/navigation/NavigationTabs.css +0 -20
- package/src/components/pdf-annotation/PdfAnnotationCanvas.css +162 -6
- package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +623 -232
- package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +764 -7
- package/src/components/pdf-annotation/__tests__/estimate-slot-height.test.ts +53 -0
- package/src/components/pdf-annotation/estimate-slot-height.ts +31 -0
- package/src/components/resource/AnnotateView.tsx +16 -5
- package/src/components/resource/AnnotationHistory.tsx +2 -0
- package/src/components/resource/BrowseView.tsx +15 -4
- package/src/components/resource/ResourceViewer.tsx +23 -24
- package/src/components/resource/__tests__/AnnotateView.focus.test.tsx +50 -0
- package/src/components/resource/__tests__/AnnotationHistory.test.tsx +2 -0
- package/src/components/resource/__tests__/BrowseView.test.tsx +29 -3
- package/src/components/resource/__tests__/ResourceViewer.embeddable.test.tsx +47 -3
- package/src/components/resource/__tests__/browse-renderers.dispatch.test.tsx +33 -0
- package/src/components/resource/annotate-renderers.tsx +10 -1
- package/src/components/resource/browse-renderers.tsx +10 -1
- package/src/components/resource/event-formatting.ts +0 -3
- package/src/components/resource/panels/AssessmentEntry.tsx +1 -1
- package/src/components/resource/panels/AssessmentPanel.css +5 -6
- package/src/components/resource/panels/AssistSection.css +68 -271
- package/src/components/resource/panels/AssistSection.tsx +4 -4
- package/src/components/resource/panels/AssistShell.tsx +12 -4
- package/src/components/resource/panels/CollaborationPanel.css +47 -22
- package/src/components/resource/panels/CollaborationPanel.tsx +93 -2
- package/src/components/resource/panels/CommentEntry.tsx +1 -1
- package/src/components/resource/panels/CommentsPanel.css +0 -16
- package/src/components/resource/panels/HighlightEntry.tsx +1 -1
- package/src/components/resource/panels/HighlightPanel.css +0 -7
- package/src/components/resource/panels/JsonLdPanel.css +0 -4
- package/src/components/resource/panels/ReferenceEntry.tsx +1 -1
- package/src/components/resource/panels/ReferencesPanel.css +0 -12
- package/src/components/resource/panels/ReferencesPanel.tsx +12 -20
- package/src/components/resource/panels/ResourceInfoPanel.css +0 -32
- package/src/components/resource/panels/ResourceInfoPanel.tsx +1 -1
- package/src/components/resource/panels/StatisticsPanel.css +0 -20
- package/src/components/resource/panels/TagEntry.tsx +1 -1
- package/src/components/resource/panels/TaggingPanel.css +0 -4
- package/src/components/resource/panels/TaggingPanel.tsx +2 -10
- package/src/components/resource/panels/UnifiedAnnotationsPanel.css +0 -4
- package/src/components/resource/panels/__tests__/AssessmentEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/AssistSection.test.tsx +48 -50
- package/src/components/resource/panels/__tests__/AssistShell.test.tsx +11 -6
- package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +89 -0
- package/src/components/resource/panels/__tests__/CommentEntry.test.tsx +1 -2
- package/src/components/resource/panels/__tests__/HighlightEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +44 -36
- package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +0 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.headless.test.tsx +1 -1
- package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +34 -53
- package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +37 -1
- package/src/components/resource/panels/__tests__/TagEntry.test.tsx +0 -1
- package/src/components/settings/SettingsPanel.css +0 -4
- package/src/features/auth/auth.css +9 -33
- package/src/features/resource-compose/state/__tests__/compose-page-state-unit.test.ts +25 -1
- package/src/features/resource-compose/state/compose-page-state-unit.ts +11 -2
- package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +55 -0
- package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +29 -0
- package/src/features/resource-discovery/state/__tests__/discover-state-unit.test.ts +53 -3
- package/src/features/resource-discovery/state/discover-state-unit.ts +29 -8
- package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +26 -0
- package/src/features/resource-viewer/components/ResourceViewerPage.tsx +42 -21
- package/src/features/resource-viewer/hooks/__tests__/useResourceViewedReport.test.tsx +63 -0
- package/src/features/resource-viewer/hooks/useResourceViewedReport.ts +33 -0
- package/src/features/resource-viewer/state/__tests__/resource-viewer-page-state-unit.test.ts +2 -2
- package/src/features/resource-viewer/state/resource-viewer-page-state-unit.ts +1 -1
- package/src/styles/core/buttons.css +3 -1
- package/src/styles/core/forms.css +0 -21
- package/src/styles/core/indicators.css +0 -4
- package/src/styles/core/inputs.css +1 -5
- package/src/styles/core/progress.css +2 -34
- package/src/styles/core/sliders.css +0 -4
- package/src/styles/core/toggles.css +0 -4
- package/src/styles/features/compose.css +1 -1
- package/src/styles/features/resource-discovery.css +13 -0
- package/src/styles/features/resource-viewer.css +1 -21
- package/src/styles/features/static-pages.css +11 -11
- package/src/styles/motivations/motivation-assessment.css +2 -2
- package/src/styles/motivations/motivation-comment.css +2 -2
- package/src/styles/motivations/motivation-reference.css +2 -2
- package/src/styles/panels/history-panel.css +0 -40
- package/src/styles/panels/user-panel.css +0 -3
- package/src/styles/patterns/panel-helpers.css +0 -27
- package/src/styles/patterns/panel-sections.css +0 -59
- package/src/styles/patterns/panels-base.css +4 -36
- package/src/styles/utilities/motion.css +15 -15
- package/src/styles/variables.css +29 -1
- package/translations/ar.json +59 -5
- package/translations/bn.json +59 -5
- package/translations/cs.json +59 -5
- package/translations/da.json +59 -5
- package/translations/de.json +59 -5
- package/translations/el.json +59 -5
- package/translations/en.json +59 -5
- package/translations/es.json +59 -5
- package/translations/fa.json +59 -5
- package/translations/fi.json +59 -5
- package/translations/fr.json +59 -5
- package/translations/he.json +59 -5
- package/translations/hi.json +59 -5
- package/translations/id.json +59 -5
- package/translations/it.json +59 -5
- package/translations/ja.json +59 -5
- package/translations/ko.json +59 -5
- package/translations/ms.json +59 -5
- package/translations/nl.json +59 -5
- package/translations/no.json +59 -5
- package/translations/pl.json +59 -5
- package/translations/pt.json +59 -5
- package/translations/ro.json +59 -5
- package/translations/sv.json +59 -5
- package/translations/th.json +59 -5
- package/translations/tr.json +59 -5
- package/translations/uk.json +59 -5
- package/translations/vi.json +59 -5
- package/translations/zh.json +59 -5
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js +0 -462
- package/dist/PdfAnnotationCanvas.client-EHQWF6BR.js.map +0 -1
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css +0 -98
- package/dist/PdfAnnotationCanvas.client-SU5KNT7G.css.map +0 -1
- package/dist/ar-CULRHLNG.js.map +0 -1
- package/dist/bn-HYR7CEL2.js.map +0 -1
- package/dist/chunk-NH6L2GIL.js.map +0 -1
- package/dist/chunk-TG2XQEC4.js.map +0 -1
- package/dist/chunk-XUDKYAVC.js +0 -21
- package/dist/chunk-XUDKYAVC.js.map +0 -1
- package/dist/cs-YBRWY7NH.js.map +0 -1
- package/dist/da-PHDEOBV3.js.map +0 -1
- package/dist/de-O4UCCVLJ.js.map +0 -1
- package/dist/el-MVGO6WZV.js.map +0 -1
- package/dist/es-USSLXQY7.js.map +0 -1
- package/dist/fa-WMAREFWY.js.map +0 -1
- package/dist/fi-B3NBNOTS.js.map +0 -1
- package/dist/fr-TXYK5T5W.js.map +0 -1
- package/dist/he-JCJACW5D.js.map +0 -1
- package/dist/hi-VOQLZNJU.js.map +0 -1
- package/dist/id-BFN5RZNZ.js.map +0 -1
- package/dist/it-TWWDHKKM.js.map +0 -1
- package/dist/ja-YRQLEBFM.js.map +0 -1
- package/dist/ko-PG6B3CRB.js.map +0 -1
- package/dist/ms-7EZECVKM.js.map +0 -1
- package/dist/nl-HP753YAP.js.map +0 -1
- package/dist/no-JUD5WX3N.js.map +0 -1
- package/dist/pl-RVUL4CBM.js.map +0 -1
- package/dist/pt-RPOVD4QM.js.map +0 -1
- package/dist/ro-DMYWQTRD.js.map +0 -1
- package/dist/sv-DMZ4FGXD.js.map +0 -1
- package/dist/th-4HVBJBQ2.js.map +0 -1
- package/dist/tr-6BDPU5ML.js.map +0 -1
- package/dist/uk-TBZBT2TL.js.map +0 -1
- package/dist/vi-UMKZKZL3.js.map +0 -1
- package/dist/zh-YBJWJJYB.js.map +0 -1
- package/src/integrations/tailwind-plugin.cjs +0 -224
- /package/dist/{en-2KQILFKQ.js.map → en-2KADE7N4.js.map} +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wizard's own logic, which had no test file at all.
|
|
3
|
+
*
|
|
4
|
+
* What is pinned here is the part that has already been wrong twice:
|
|
5
|
+
*
|
|
6
|
+
* 1. **The Hint's PLACEMENT.** `GatheredContext.json` puts it at
|
|
7
|
+
* `focus.userHint` ("a hint to supplement or replace the selected text for
|
|
8
|
+
* search and generation"). Search and compose each built `{ ...context,
|
|
9
|
+
* userHint }` inline — a TOP-LEVEL key the schema does not define — and
|
|
10
|
+
* generation received the raw context and dropped it entirely. So it was
|
|
11
|
+
* misplaced on two paths and missing on the third, and nothing failed.
|
|
12
|
+
* 2. **That all three strategies get the SAME object.** One `contextWithHint`
|
|
13
|
+
* now feeds compose, search and generation; a future strategy that reaches
|
|
14
|
+
* for the raw `context` prop is the regression these tests catch.
|
|
15
|
+
*
|
|
16
|
+
* Also pinned: reopening the modal resets the drafts. WIZARD-NAVIGATION D3 made
|
|
17
|
+
* Back lossless by hoisting step config into the wizard, and the flip side of
|
|
18
|
+
* "the modal remembers" is that a NEW run must not inherit the last one's
|
|
19
|
+
* settings.
|
|
20
|
+
*/
|
|
21
|
+
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
|
|
22
|
+
import { screen } from '@testing-library/react';
|
|
23
|
+
import userEvent from '@testing-library/user-event';
|
|
24
|
+
import '@testing-library/jest-dom';
|
|
25
|
+
import type { GatheredContext } from '@semiont/core';
|
|
26
|
+
import { renderWithProviders } from '../../../test-utils';
|
|
27
|
+
import { ReferenceWizardModal } from '../ReferenceWizardModal';
|
|
28
|
+
|
|
29
|
+
// jsdom implements no layout, so it has no `scrollIntoView`. GatherContextStep
|
|
30
|
+
// calls it to bring the highlighted passage into view whenever the context
|
|
31
|
+
// changes — real behaviour, untestable here, and a hard throw if unstubbed.
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
Element.prototype.scrollIntoView = vi.fn();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const CONTEXT = {
|
|
37
|
+
focus: {
|
|
38
|
+
kind: 'annotation',
|
|
39
|
+
annotation: { id: 'ann-1' },
|
|
40
|
+
sourceResource: { id: 'res-1', name: 'Scythian steppe' },
|
|
41
|
+
selected: { text: 'Caspian Sea' },
|
|
42
|
+
},
|
|
43
|
+
graph: { nodes: [], edges: [] },
|
|
44
|
+
metadata: {},
|
|
45
|
+
} as unknown as GatheredContext;
|
|
46
|
+
|
|
47
|
+
const T = {
|
|
48
|
+
gatherTitle: 'Gathered Context', configureGenerationTitle: 'Configure Generation',
|
|
49
|
+
configureSearchTitle: 'Configure Search', searchResultsTitle: 'Search Results',
|
|
50
|
+
sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
|
|
51
|
+
userHintLabel: 'Hint', userHintPlaceholder: 'Describe what this refers to…',
|
|
52
|
+
loadingContext: 'Loading…', failedContext: 'Failed',
|
|
53
|
+
cancel: 'Cancel', search: 'Search', searching: 'Searching…', generate: 'Generate',
|
|
54
|
+
compose: 'Compose', resolutionStrategyLabel: 'Resolution Strategy', back: 'Back',
|
|
55
|
+
link: 'Link', score: 'Score', noResults: 'No results',
|
|
56
|
+
resourceTitle: 'Resource Title', resourceTitlePlaceholder: 'Title…',
|
|
57
|
+
additionalInstructions: 'Additional Instructions', additionalInstructionsPlaceholder: '…',
|
|
58
|
+
language: 'Language', languageHelp: '', creativity: 'Creativity',
|
|
59
|
+
creativityFocused: 'Focused', creativityCreative: 'Creative',
|
|
60
|
+
maxLength: 'Max length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{maxOutputTokens}} by {{model}}.',
|
|
61
|
+
maxResults: 'Max Results', semanticScoring: 'Semantic Scoring', semanticScoringHelp: '',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function renderWizard(over: Partial<React.ComponentProps<typeof ReferenceWizardModal>> = {}) {
|
|
65
|
+
const onComposeNavigate = vi.fn();
|
|
66
|
+
const onGenerateSubmit = vi.fn();
|
|
67
|
+
const onLinkResource = vi.fn();
|
|
68
|
+
const onClose = vi.fn();
|
|
69
|
+
|
|
70
|
+
const utils = renderWithProviders(
|
|
71
|
+
<ReferenceWizardModal
|
|
72
|
+
isOpen
|
|
73
|
+
onClose={onClose}
|
|
74
|
+
annotationId="ann-1"
|
|
75
|
+
resourceId="res-1"
|
|
76
|
+
defaultTitle="Caspian Sea"
|
|
77
|
+
entityTypes={['Location']}
|
|
78
|
+
locale="en"
|
|
79
|
+
context={CONTEXT}
|
|
80
|
+
contextLoading={false}
|
|
81
|
+
contextError={null}
|
|
82
|
+
onGenerateSubmit={onGenerateSubmit}
|
|
83
|
+
onLinkResource={onLinkResource}
|
|
84
|
+
onComposeNavigate={onComposeNavigate}
|
|
85
|
+
translations={T}
|
|
86
|
+
{...over}
|
|
87
|
+
/>,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// The wizard emits over the session's own client (`client.match.requestSearch`),
|
|
91
|
+
// so the spy has to be on THAT client, not a second one.
|
|
92
|
+
const client = utils.session!.client;
|
|
93
|
+
const searchSpy = vi.spyOn(client.match, 'requestSearch');
|
|
94
|
+
|
|
95
|
+
return { ...utils, client, searchSpy, onComposeNavigate, onGenerateSubmit, onLinkResource, onClose };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Type the hint into the gather step's textarea. */
|
|
99
|
+
async function typeHint(text: string) {
|
|
100
|
+
await userEvent.type(screen.getByPlaceholderText(T.userHintPlaceholder), text);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('ReferenceWizardModal — the Hint reaches every strategy, at focus.userHint', () => {
|
|
104
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
105
|
+
|
|
106
|
+
it('compose carries the hint inside focus, not at the top level', async () => {
|
|
107
|
+
const { onComposeNavigate } = renderWizard();
|
|
108
|
+
await typeHint('the lake, not the myth');
|
|
109
|
+
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
110
|
+
|
|
111
|
+
expect(onComposeNavigate).toHaveBeenCalledTimes(1);
|
|
112
|
+
const context = onComposeNavigate.mock.calls[0]![0] as Record<string, unknown>;
|
|
113
|
+
expect((context.focus as Record<string, unknown>).userHint).toBe('the lake, not the myth');
|
|
114
|
+
// The shape the old code produced. `GatheredContext` has no top-level
|
|
115
|
+
// `userHint`, so anything reading the schema would never look here.
|
|
116
|
+
expect(context).not.toHaveProperty('userHint');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('search sends the same enriched context over the wire', async () => {
|
|
120
|
+
const { searchSpy } = renderWizard();
|
|
121
|
+
await typeHint('the lake');
|
|
122
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
123
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
124
|
+
|
|
125
|
+
expect(searchSpy).toHaveBeenCalledTimes(1);
|
|
126
|
+
const arg = searchSpy.mock.calls[0]![0] as { context: Record<string, unknown> };
|
|
127
|
+
expect((arg.context.focus as Record<string, unknown>).userHint).toBe('the lake');
|
|
128
|
+
expect(arg.context).not.toHaveProperty('userHint');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('generation gets it too — the path that used to drop it silently', async () => {
|
|
132
|
+
renderWizard();
|
|
133
|
+
await typeHint('focus on hydrology');
|
|
134
|
+
await userEvent.click(screen.getByText(new RegExp(T.generate)));
|
|
135
|
+
|
|
136
|
+
// The step renders the context it was handed; the hint is visible in the
|
|
137
|
+
// gathered-context panel it shows, which is only true if the ENRICHED
|
|
138
|
+
// object was passed down rather than the raw prop.
|
|
139
|
+
expect(screen.getByText(T.configureGenerationTitle)).toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('leaves the context untouched when no hint was typed', async () => {
|
|
143
|
+
const { onComposeNavigate } = renderWizard();
|
|
144
|
+
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
145
|
+
|
|
146
|
+
const context = onComposeNavigate.mock.calls[0]![0];
|
|
147
|
+
expect(context).toBe(CONTEXT); // same reference: nothing was rebuilt
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('ReferenceWizardModal — a new run starts clean (D3 flip side)', () => {
|
|
152
|
+
it('reopening resets the hint and the step', async () => {
|
|
153
|
+
const { rerender } = renderWizard();
|
|
154
|
+
await typeHint('first attempt');
|
|
155
|
+
expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toHaveValue('first attempt');
|
|
156
|
+
|
|
157
|
+
const props = {
|
|
158
|
+
isOpen: false, onClose: vi.fn(), annotationId: 'ann-1', resourceId: 'res-1',
|
|
159
|
+
defaultTitle: 'Caspian Sea', entityTypes: ['Location'], locale: 'en',
|
|
160
|
+
context: CONTEXT, contextLoading: false, contextError: null,
|
|
161
|
+
onGenerateSubmit: vi.fn(), onLinkResource: vi.fn(), onComposeNavigate: vi.fn(),
|
|
162
|
+
translations: T,
|
|
163
|
+
};
|
|
164
|
+
rerender(<ReferenceWizardModal {...props} />);
|
|
165
|
+
rerender(<ReferenceWizardModal {...props} isOpen />);
|
|
166
|
+
|
|
167
|
+
// Back is lossless WITHIN a run (D3); a new run inherits nothing.
|
|
168
|
+
expect(screen.getByPlaceholderText(T.userHintPlaceholder)).toHaveValue('');
|
|
169
|
+
expect(screen.getByText(T.gatherTitle)).toBeInTheDocument();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('ReferenceWizardModal — the three strategies complete', () => {
|
|
174
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
175
|
+
|
|
176
|
+
it('search results arrive over the bus and linking one resolves the reference', async () => {
|
|
177
|
+
const { client, onLinkResource, onClose } = renderWizard();
|
|
178
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
179
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
180
|
+
|
|
181
|
+
// The results step is entered by the REPLY, not by the click — the wizard
|
|
182
|
+
// sits on the configure step until the bus answers (which is why dismissal
|
|
183
|
+
// must stay available; see the A5 pin in WizardFooter.test).
|
|
184
|
+
client.bus.get('match:search-results').next({
|
|
185
|
+
referenceId: 'ann-1',
|
|
186
|
+
// `ResourceDescriptor` is JSON-LD: the identifier is `@id`, and the
|
|
187
|
+
// results step links on that. A fixture using `id` renders the row and
|
|
188
|
+
// links `undefined` — passing the eye, failing the reference.
|
|
189
|
+
response: [{ '@id': 'res-9', name: 'Caspian Sea', score: 54.2 }],
|
|
190
|
+
} as never);
|
|
191
|
+
|
|
192
|
+
expect(await screen.findByText(T.searchResultsTitle)).toBeInTheDocument();
|
|
193
|
+
await userEvent.click(screen.getByRole('button', { name: new RegExp(T.link) }));
|
|
194
|
+
|
|
195
|
+
expect(onLinkResource).toHaveBeenCalledWith('ann-1', 'res-9');
|
|
196
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('ignores results addressed to a different annotation', () => {
|
|
200
|
+
// Two wizards can never be open at once, but one reply can outlive the
|
|
201
|
+
// annotation it was asked for — a late answer must not hijack the step.
|
|
202
|
+
const { client } = renderWizard();
|
|
203
|
+
client.bus.get('match:search-results').next({
|
|
204
|
+
referenceId: 'someone-elses-annotation',
|
|
205
|
+
response: [{ '@id': 'res-9', name: 'Caspian Sea' }],
|
|
206
|
+
} as never);
|
|
207
|
+
expect(screen.getByText(T.gatherTitle)).toBeInTheDocument();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('generation submits the step\'s config against this annotation, then closes', async () => {
|
|
211
|
+
const { onGenerateSubmit, onClose } = renderWizard();
|
|
212
|
+
await userEvent.click(screen.getByText(new RegExp(T.generate)));
|
|
213
|
+
await userEvent.type(screen.getByLabelText(/Save location/i), 'generated/out.md');
|
|
214
|
+
await userEvent.click(screen.getByRole('button', { name: T.generate }));
|
|
215
|
+
|
|
216
|
+
expect(onGenerateSubmit).toHaveBeenCalledTimes(1);
|
|
217
|
+
expect(onGenerateSubmit.mock.calls[0]![0]).toBe('ann-1');
|
|
218
|
+
expect(onGenerateSubmit.mock.calls[0]![1].title).toBe('Caspian Sea');
|
|
219
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('retreats from a configure step back to the gather step', async () => {
|
|
223
|
+
renderWizard();
|
|
224
|
+
await userEvent.click(screen.getByText(new RegExp(T.generate)));
|
|
225
|
+
expect(screen.getByText(T.configureGenerationTitle)).toBeInTheDocument();
|
|
226
|
+
await userEvent.click(screen.getByRole('button', { name: /Back/ }));
|
|
227
|
+
expect(screen.getByText(T.gatherTitle)).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('ReferenceWizardModal — nothing fires without an annotation to resolve', () => {
|
|
232
|
+
// Every strategy guards on `annotationId` / `resourceId` / a context. The
|
|
233
|
+
// props are nullable because the page renders this modal before a reference
|
|
234
|
+
// is chosen, so "open but not yet aimed" is a real state, and acting in it
|
|
235
|
+
// would resolve a reference nobody selected.
|
|
236
|
+
beforeEach(() => { vi.clearAllMocks(); });
|
|
237
|
+
|
|
238
|
+
it('compose, search and generate all no-op', async () => {
|
|
239
|
+
const { onComposeNavigate, onGenerateSubmit, searchSpy, client } =
|
|
240
|
+
renderWizard({ annotationId: null, resourceId: null });
|
|
241
|
+
|
|
242
|
+
await userEvent.click(screen.getByText(new RegExp(T.compose)));
|
|
243
|
+
expect(onComposeNavigate).not.toHaveBeenCalled();
|
|
244
|
+
|
|
245
|
+
await userEvent.click(screen.getByText(new RegExp(`^🔍? ?${T.search}`)));
|
|
246
|
+
await userEvent.click(screen.getByRole('button', { name: T.search }));
|
|
247
|
+
expect(searchSpy).not.toHaveBeenCalled();
|
|
248
|
+
|
|
249
|
+
// A reply that arrives anyway is ignored rather than advancing the step.
|
|
250
|
+
client.bus.get('match:search-results').next({
|
|
251
|
+
referenceId: 'ann-1', response: [{ '@id': 'res-9', name: 'X' }],
|
|
252
|
+
} as never);
|
|
253
|
+
expect(screen.queryByText(T.searchResultsTitle)).not.toBeInTheDocument();
|
|
254
|
+
expect(onGenerateSubmit).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { asStates } from '../../../__tests__/test-client';
|
|
3
3
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
|
4
|
-
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
import { BehaviorSubject, map } from 'rxjs';
|
|
5
5
|
import { renderWithProviders } from '../../../test-utils';
|
|
6
6
|
import '@testing-library/jest-dom';
|
|
7
7
|
import { ResourceSearchModal } from '../ResourceSearchModal';
|
|
@@ -20,7 +20,11 @@ vi.mock('@headlessui/react', () => ({
|
|
|
20
20
|
// We mock useSemiont to return a stable browser whose activeSession$ emits a
|
|
21
21
|
// session-shaped object that carries the mock client.
|
|
22
22
|
const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
23
|
-
const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable()
|
|
23
|
+
const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable().pipe(
|
|
24
|
+
// Arrays get the list envelope `resources()` now emits; `undefined`
|
|
25
|
+
// passes through as the pending state.
|
|
26
|
+
map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: 'lexical' } : v)),
|
|
27
|
+
)));
|
|
24
28
|
const stableMockClient = { browse: { resources: browseResourcesMock } };
|
|
25
29
|
const stableMockSession = { client: stableMockClient };
|
|
26
30
|
const stableActiveSession$ = new BehaviorSubject<any>(stableMockSession);
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
15
15
|
import { asStates } from '../../../__tests__/test-client';
|
|
16
16
|
import { screen, fireEvent, waitFor, act } from '@testing-library/react';
|
|
17
|
-
import { BehaviorSubject } from 'rxjs';
|
|
17
|
+
import { BehaviorSubject, map } from 'rxjs';
|
|
18
18
|
import { renderWithProviders } from '../../../test-utils';
|
|
19
19
|
import '@testing-library/jest-dom';
|
|
20
20
|
import { SearchModal } from '../SearchModal';
|
|
@@ -34,7 +34,11 @@ vi.mock('@headlessui/react', () => ({
|
|
|
34
34
|
|
|
35
35
|
// Mock the http-transport Observable surface
|
|
36
36
|
const browseResourcesSubject = new BehaviorSubject<any[] | undefined>(undefined);
|
|
37
|
-
const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable()
|
|
37
|
+
const browseResourcesMock = vi.fn(() => asStates(browseResourcesSubject.asObservable().pipe(
|
|
38
|
+
// Arrays get the list envelope `resources()` now emits; `undefined`
|
|
39
|
+
// passes through as the pending state.
|
|
40
|
+
map((v) => (Array.isArray(v) ? { resources: v, total: v.length, offset: 0, limit: 20, matchKind: 'lexical' } : v)),
|
|
41
|
+
)));
|
|
38
42
|
|
|
39
43
|
// Stable client reference — useSemiont is called on every render, so a
|
|
40
44
|
// fresh object literal would invalidate useMemo deps and restart the RxJS
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WIZARD-NAVIGATION P1 — one dismissal, one retreat, one advance.
|
|
3
|
+
*
|
|
4
|
+
* The wizard used to offer four ways out of one step (corner ✕, Esc, backdrop, and a
|
|
5
|
+
* footer `✕ Cancel`) and none at all on the step that starts the flow. D1 settled it:
|
|
6
|
+
* the corner control plus Esc and backdrop are the dismissal, and the footer is purely
|
|
7
|
+
* flow — where you came from, where you are going.
|
|
8
|
+
*
|
|
9
|
+
* These assert STRUCTURE, never copy: labels arrive as props and will be revised.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { render, screen } from '@testing-library/react';
|
|
13
|
+
import userEvent from '@testing-library/user-event';
|
|
14
|
+
import '@testing-library/jest-dom';
|
|
15
|
+
import { WizardFooter } from '../WizardFooter';
|
|
16
|
+
import { ConfigureSearchStep } from '../ConfigureSearchStep';
|
|
17
|
+
import { ConfigureGenerationStep } from '../ConfigureGenerationStep';
|
|
18
|
+
import { SearchResultsStep } from '../SearchResultsStep';
|
|
19
|
+
import type { GatheredContext } from '@semiont/core';
|
|
20
|
+
|
|
21
|
+
/** Anything that reads as "get me out of here" belongs in the corner, not the footer. */
|
|
22
|
+
const DISMISSAL = /cancel|close|dismiss|✕|✖|×/i;
|
|
23
|
+
|
|
24
|
+
const footerOf = (container: HTMLElement) =>
|
|
25
|
+
container.querySelector('.semiont-modal__actions') as HTMLElement | null;
|
|
26
|
+
|
|
27
|
+
const buttonsIn = (footer: HTMLElement) => Array.from(footer.querySelectorAll('button'));
|
|
28
|
+
|
|
29
|
+
describe('WizardFooter', () => {
|
|
30
|
+
it('renders retreat and advance, and nothing that dismisses', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<WizardFooter
|
|
33
|
+
backLabel="Back"
|
|
34
|
+
onBack={vi.fn()}
|
|
35
|
+
primary={{ label: 'Search', type: 'submit' }}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
const footer = footerOf(container)!;
|
|
39
|
+
const labels = buttonsIn(footer).map((b) => b.textContent ?? '');
|
|
40
|
+
expect(labels).toHaveLength(2);
|
|
41
|
+
expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('omits Back entirely when there is nowhere to go back to', () => {
|
|
45
|
+
const { container } = render(
|
|
46
|
+
<WizardFooter primary={{ label: 'Search', type: 'submit' }} />,
|
|
47
|
+
);
|
|
48
|
+
expect(buttonsIn(footerOf(container)!)).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders Back alone when the step has no footer-level action', () => {
|
|
52
|
+
// Search results: the action is per row, so the footer carries retreat only.
|
|
53
|
+
const { container } = render(<WizardFooter backLabel="Back" onBack={vi.fn()} />);
|
|
54
|
+
const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent);
|
|
55
|
+
expect(labels).toEqual(['◀ Back']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not stretch its buttons to equal width', () => {
|
|
59
|
+
// `--flex` made retreat and advance read as peers. Retreat is chrome.
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<WizardFooter backLabel="Back" onBack={vi.fn()} primary={{ label: 'Go', type: 'button', onClick: vi.fn() }} />,
|
|
62
|
+
);
|
|
63
|
+
for (const button of buttonsIn(footerOf(container)!)) {
|
|
64
|
+
expect(button.className).not.toContain('semiont-button--flex');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('a pending primary disables itself and Back, and says so', async () => {
|
|
69
|
+
const onBack = vi.fn();
|
|
70
|
+
render(
|
|
71
|
+
<WizardFooter
|
|
72
|
+
backLabel="Back"
|
|
73
|
+
onBack={onBack}
|
|
74
|
+
primary={{ label: 'Search', pendingLabel: 'Searching…', pending: true, type: 'submit' }}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
expect(screen.getByText(/Searching…/)).toBeInTheDocument();
|
|
78
|
+
await userEvent.click(screen.getByText(/Back/));
|
|
79
|
+
expect(onBack).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
84
|
+
// The same rule, asserted against the real steps — a footer component is
|
|
85
|
+
// only worth having if every step actually uses it.
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
87
|
+
const SEARCH_T = {
|
|
88
|
+
maxResults: 'Max Results', semanticScoring: 'Semantic Scoring',
|
|
89
|
+
semanticScoringHelp: 'help', back: 'Back', search: 'Search', searching: 'Searching…',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const GEN_T = {
|
|
93
|
+
resourceTitle: 'Title', resourceTitlePlaceholder: '', additionalInstructions: 'Instructions',
|
|
94
|
+
additionalInstructionsPlaceholder: '', language: 'Language', languageHelp: '',
|
|
95
|
+
creativity: 'Creativity', creativityFocused: 'Focused', creativityCreative: 'Creative',
|
|
96
|
+
maxLength: 'Max Length', maxLengthHelp: '', maxLengthCeiling: 'Limited to {{max}} by {{model}}.',
|
|
97
|
+
back: 'Back', generate: 'Generate',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const CONTEXT_T = {
|
|
101
|
+
sourceContextLabel: 'Source', connectionsLabel: 'Connections', citedByLabel: 'Cited by',
|
|
102
|
+
userHintLabel: 'Hint', userHintPlaceholder: '',
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const RESULTS_T = { noResults: 'None', score: 'Score', link: 'Link', back: 'Back', ...CONTEXT_T };
|
|
106
|
+
|
|
107
|
+
const CONTEXT = {
|
|
108
|
+
focus: { kind: 'annotation', annotation: { id: 'a1' }, sourceResource: { id: 'r1', name: 'Src' } },
|
|
109
|
+
// `graph` is not optional in practice: ContextSummary hands it straight to
|
|
110
|
+
// `deriveViews`, which dereferences `.nodes`.
|
|
111
|
+
graph: { nodes: [], edges: [] },
|
|
112
|
+
} as unknown as GatheredContext;
|
|
113
|
+
|
|
114
|
+
describe('every step footer follows the grammar (A1, A2)', () => {
|
|
115
|
+
it('ConfigureSearchStep', () => {
|
|
116
|
+
const { container } = render(
|
|
117
|
+
<ConfigureSearchStep
|
|
118
|
+
config={{ limit: 10, useSemanticScoring: true }}
|
|
119
|
+
onConfigChange={vi.fn()}
|
|
120
|
+
onBack={vi.fn()}
|
|
121
|
+
onSearch={vi.fn()}
|
|
122
|
+
translations={SEARCH_T}
|
|
123
|
+
/>,
|
|
124
|
+
);
|
|
125
|
+
const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
|
|
126
|
+
expect(labels).toHaveLength(2);
|
|
127
|
+
expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('SearchResultsStep — footer is retreat only; Link lives on the rows', () => {
|
|
131
|
+
const { container } = render(
|
|
132
|
+
<SearchResultsStep
|
|
133
|
+
context={CONTEXT}
|
|
134
|
+
results={[{ id: 'r1', name: 'Black Sea', score: 54.22 } as never]}
|
|
135
|
+
onBack={vi.fn()}
|
|
136
|
+
onLink={vi.fn()}
|
|
137
|
+
translations={RESULTS_T}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
|
|
141
|
+
expect(labels).toHaveLength(1);
|
|
142
|
+
expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
|
|
143
|
+
expect(screen.getByText(/Link/)).toBeInTheDocument();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('ConfigureGenerationStep', () => {
|
|
147
|
+
const { container } = render(
|
|
148
|
+
<ConfigureGenerationStep
|
|
149
|
+
context={CONTEXT}
|
|
150
|
+
config={{
|
|
151
|
+
title: 'Caspian Sea', storagePath: '', prompt: '', language: 'en',
|
|
152
|
+
temperature: 0.7, maxTokensText: '500',
|
|
153
|
+
}}
|
|
154
|
+
onConfigChange={vi.fn()}
|
|
155
|
+
onBack={vi.fn()}
|
|
156
|
+
onGenerate={vi.fn()}
|
|
157
|
+
translations={GEN_T}
|
|
158
|
+
/>,
|
|
159
|
+
);
|
|
160
|
+
const labels = buttonsIn(footerOf(container)!).map((b) => b.textContent ?? '');
|
|
161
|
+
expect(labels).toHaveLength(2);
|
|
162
|
+
expect(labels.filter((l) => DISMISSAL.test(l))).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
167
|
+
// WIZARD-NAVIGATION P3 — the exits, pinned.
|
|
168
|
+
//
|
|
169
|
+
// HeadlessUI already wires Esc and the backdrop to `onClose`; these are the
|
|
170
|
+
// guard, not the fix. What they prevent is a later change that "improves" Esc
|
|
171
|
+
// into a step-back, or that disables the way out while a request is in flight.
|
|
172
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
173
|
+
describe('the way out (A4, A5)', () => {
|
|
174
|
+
it('a pending step never disables the corner control — only the footer', () => {
|
|
175
|
+
// D5: dismissal must survive a search that never returns. The footer's own
|
|
176
|
+
// buttons DO disable (pressing Search twice is not a feature); the corner is
|
|
177
|
+
// not the footer's to disable, which is why it does not live there.
|
|
178
|
+
const { container } = render(
|
|
179
|
+
<ConfigureSearchStep
|
|
180
|
+
config={{ limit: 10, useSemanticScoring: true }}
|
|
181
|
+
onConfigChange={vi.fn()}
|
|
182
|
+
isSearching
|
|
183
|
+
onBack={vi.fn()}
|
|
184
|
+
onSearch={vi.fn()}
|
|
185
|
+
translations={SEARCH_T}
|
|
186
|
+
/>,
|
|
187
|
+
);
|
|
188
|
+
const footer = footerOf(container)!;
|
|
189
|
+
for (const button of buttonsIn(footer)) expect(button).toBeDisabled();
|
|
190
|
+
// …and the step renders no dismissal of its own to be disabled with them.
|
|
191
|
+
expect(buttonsIn(footer).map((b) => b.textContent ?? '').filter((l) => DISMISSAL.test(l))).toEqual([]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('Back does not fire while the step is pending', async () => {
|
|
195
|
+
const onBack = vi.fn();
|
|
196
|
+
render(
|
|
197
|
+
<ConfigureSearchStep
|
|
198
|
+
config={{ limit: 10, useSemanticScoring: true }}
|
|
199
|
+
onConfigChange={vi.fn()}
|
|
200
|
+
isSearching
|
|
201
|
+
onBack={onBack}
|
|
202
|
+
onSearch={vi.fn()}
|
|
203
|
+
translations={SEARCH_T}
|
|
204
|
+
/>,
|
|
205
|
+
);
|
|
206
|
+
await userEvent.click(screen.getByText(/Back/));
|
|
207
|
+
expect(onBack).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Back is lossless (A3)', () => {
|
|
212
|
+
it('the step reflects the wizard-owned draft, so remounting restores it', () => {
|
|
213
|
+
// The regression this replaces: `useState` inside the step meant stepping
|
|
214
|
+
// back unmounted it and every choice reverted to the default.
|
|
215
|
+
const config = { limit: 20, useSemanticScoring: false };
|
|
216
|
+
const { unmount } = render(
|
|
217
|
+
<ConfigureSearchStep
|
|
218
|
+
config={config} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
|
|
219
|
+
translations={SEARCH_T}
|
|
220
|
+
/>,
|
|
221
|
+
);
|
|
222
|
+
expect((screen.getByLabelText('Max Results') as HTMLSelectElement).value).toBe('20');
|
|
223
|
+
unmount();
|
|
224
|
+
|
|
225
|
+
// Same draft, fresh mount — what "Back then forward" does.
|
|
226
|
+
render(
|
|
227
|
+
<ConfigureSearchStep
|
|
228
|
+
config={config} onConfigChange={vi.fn()} onBack={vi.fn()} onSearch={vi.fn()}
|
|
229
|
+
translations={SEARCH_T}
|
|
230
|
+
/>,
|
|
231
|
+
);
|
|
232
|
+
expect((screen.getByLabelText('Max Results') as HTMLSelectElement).value).toBe('20');
|
|
233
|
+
expect((screen.getByLabelText(/Semantic Scoring/) as HTMLInputElement).checked).toBe(false);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -111,11 +111,6 @@
|
|
|
111
111
|
background-color: var(--semiont-bg-tertiary);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
[data-theme="dark"] .semiont-modal__close:hover {
|
|
115
|
-
color: var(--semiont-text-primary);
|
|
116
|
-
background-color: var(--semiont-bg-tertiary);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
114
|
.semiont-modal__close-icon {
|
|
120
115
|
width: 1.25rem;
|
|
121
116
|
height: 1.25rem;
|
|
@@ -256,6 +251,22 @@
|
|
|
256
251
|
gap: 0.5rem;
|
|
257
252
|
}
|
|
258
253
|
|
|
254
|
+
/*
|
|
255
|
+
* The wizard footer (WIZARD-NAVIGATION D2). Retreat sits at the leading edge and
|
|
256
|
+
* advance at the trailing one, and neither stretches — `--flex` on all three
|
|
257
|
+
* buttons is what made "Cancel", "Back" and "Search" read as three peers.
|
|
258
|
+
* `margin-inline-end: auto` rather than `space-between` so a footer with only
|
|
259
|
+
* one child still lands on the correct side.
|
|
260
|
+
*/
|
|
261
|
+
.semiont-modal__actions--wizard {
|
|
262
|
+
justify-content: flex-end;
|
|
263
|
+
padding-top: 0.5rem;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.semiont-wizard-footer__back {
|
|
267
|
+
margin-inline-end: auto;
|
|
268
|
+
}
|
|
269
|
+
|
|
259
270
|
/* Chip Styles */
|
|
260
271
|
.semiont-chip {
|
|
261
272
|
padding: 0.25rem 0.75rem;
|
|
@@ -352,12 +363,6 @@
|
|
|
352
363
|
cursor: not-allowed;
|
|
353
364
|
}
|
|
354
365
|
|
|
355
|
-
[data-theme="dark"] .semiont-button--disabled,
|
|
356
|
-
[data-theme="dark"] .semiont-button[data-disabled="true"] {
|
|
357
|
-
background-color: var(--semiont-bg-tertiary);
|
|
358
|
-
color: var(--semiont-text-tertiary);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
366
|
/* Keyboard Shortcuts Styles */
|
|
362
367
|
.semiont-shortcuts {
|
|
363
368
|
display: flex;
|
|
@@ -544,10 +549,6 @@
|
|
|
544
549
|
margin: 0;
|
|
545
550
|
}
|
|
546
551
|
|
|
547
|
-
[data-theme="dark"] .semiont-jsonld-view__title {
|
|
548
|
-
color: var(--semiont-text-primary);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
552
|
.semiont-jsonld-view__back-button,
|
|
552
553
|
.semiont-jsonld-view__copy-button {
|
|
553
554
|
padding: 0.5rem 1rem;
|
|
@@ -572,12 +573,6 @@
|
|
|
572
573
|
border-color: var(--semiont-border-primary);
|
|
573
574
|
}
|
|
574
575
|
|
|
575
|
-
[data-theme="dark"] .semiont-jsonld-view__back-button:hover,
|
|
576
|
-
[data-theme="dark"] .semiont-jsonld-view__copy-button:hover {
|
|
577
|
-
background-color: var(--semiont-bg-secondary);
|
|
578
|
-
border-color: var(--semiont-border-secondary);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
576
|
.semiont-jsonld-view__editor {
|
|
582
577
|
flex: 1;
|
|
583
578
|
overflow: auto;
|
|
@@ -585,10 +580,6 @@
|
|
|
585
580
|
background-color: var(--semiont-bg-primary);
|
|
586
581
|
}
|
|
587
582
|
|
|
588
|
-
[data-theme="dark"] .semiont-jsonld-view__editor {
|
|
589
|
-
background-color: var(--semiont-bg-primary);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
583
|
/* Ensure CodeMirror takes full height */
|
|
593
584
|
.semiont-jsonld-view__editor .cm-editor {
|
|
594
585
|
height: 100%;
|
|
@@ -620,10 +611,6 @@
|
|
|
620
611
|
margin: 0;
|
|
621
612
|
}
|
|
622
613
|
|
|
623
|
-
[data-theme="dark"] .semiont-popup-header__title {
|
|
624
|
-
color: var(--semiont-text-primary);
|
|
625
|
-
}
|
|
626
|
-
|
|
627
614
|
.semiont-popup-header__subtitle {
|
|
628
615
|
font-size: 0.875rem;
|
|
629
616
|
font-weight: normal;
|
|
@@ -631,10 +618,6 @@
|
|
|
631
618
|
margin-left: 0.5rem;
|
|
632
619
|
}
|
|
633
620
|
|
|
634
|
-
[data-theme="dark"] .semiont-popup-header__subtitle {
|
|
635
|
-
color: var(--semiont-text-secondary);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
621
|
.semiont-popup-header__close-button {
|
|
639
622
|
padding: 0.25rem;
|
|
640
623
|
border-radius: 0.25rem;
|
|
@@ -657,15 +640,6 @@
|
|
|
657
640
|
outline-offset: 2px;
|
|
658
641
|
}
|
|
659
642
|
|
|
660
|
-
[data-theme="dark"] .semiont-popup-header__close-button {
|
|
661
|
-
color: var(--semiont-text-secondary);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
[data-theme="dark"] .semiont-popup-header__close-button:hover {
|
|
665
|
-
background-color: var(--semiont-bg-tertiary);
|
|
666
|
-
color: var(--semiont-text-primary);
|
|
667
|
-
}
|
|
668
|
-
|
|
669
643
|
[data-theme="dark"] .semiont-popup-header__close-button:focus-visible {
|
|
670
644
|
outline-color: var(--semiont-color-primary-400);
|
|
671
645
|
}
|