@eeacms/volto-cca-policy 1.0.23 → 1.0.24
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/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.jsx +23 -5
- package/src/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem.test.jsx +44 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/DocumentCard.jsx +37 -122
- package/src/components/manage/Blocks/ChatbotCatalogue/DocumentCard.test.jsx +162 -73
- package/src/components/manage/Blocks/ChatbotCatalogue/astUtils.js +249 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/astUtils.test.js +207 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/catalogue-chat.less +0 -56
- package/src/components/manage/Blocks/ChatbotCatalogue/docCards.js +92 -29
- package/src/components/manage/Blocks/ChatbotCatalogue/docCards.test.js +107 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/nextSteps.js +22 -20
- package/src/components/manage/Blocks/ChatbotCatalogue/nextSteps.test.js +69 -0
- package/src/components/manage/Blocks/ChatbotCatalogue/useCatalogueDoc.js +6 -2
- package/src/components/manage/Blocks/ChatbotCatalogue/useCatalogueDoc.test.js +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
### [1.0.24](https://github.com/eea/volto-cca-policy/compare/1.0.23...1.0.24) - 17 September 2026
|
|
8
|
+
|
|
9
|
+
#### :bug: Bug Fixes
|
|
10
|
+
|
|
11
|
+
- fix: unwrap accidental code blocks and add resilience against varied assistant formats [Tiberiu Ichim - [`b245de7`](https://github.com/eea/volto-cca-policy/commit/b245de770a2e6d9a5741d240dc6649d2df7674bc)]
|
|
12
|
+
- fix: hide document card when Elasticsearch lookup returns empty or not found [Tiberiu Ichim - [`6acb0ac`](https://github.com/eea/volto-cca-policy/commit/6acb0acf1ad558cf259a61ed25c6860a08edc1ee)]
|
|
13
|
+
- fix: make catalogue cards and date formatting resilient against malformed data [Tiberiu Ichim - [`858ec4e`](https://github.com/eea/volto-cca-policy/commit/858ec4e2b7c8857ec43f080b9f5368e50bad40d4)]
|
|
14
|
+
|
|
7
15
|
### [1.0.23](https://github.com/eea/volto-cca-policy/compare/1.0.22...1.0.23) - 16 September 2026
|
|
8
16
|
|
|
9
17
|
#### :rocket: New Features
|
package/package.json
CHANGED
|
@@ -54,6 +54,23 @@ const publicationDateFormatter = new Intl.DateTimeFormat('en-GB', {
|
|
|
54
54
|
timeZone: 'UTC',
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
+
const formatPublicationDate = (value) => {
|
|
58
|
+
if (!value) return '';
|
|
59
|
+
const raw =
|
|
60
|
+
typeof value === 'object' && value !== null && 'raw' in value
|
|
61
|
+
? value.raw
|
|
62
|
+
: value;
|
|
63
|
+
const val = Array.isArray(raw) ? raw[0] : raw;
|
|
64
|
+
if (!val || typeof val === 'object') return '';
|
|
65
|
+
try {
|
|
66
|
+
const d = new Date(val);
|
|
67
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
68
|
+
return publicationDateFormatter.format(d);
|
|
69
|
+
} catch {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
57
74
|
const TagGroup = ({ typeLabel, values, type }) => {
|
|
58
75
|
const visible = values.slice(0, 3);
|
|
59
76
|
const hidden = values.slice(3);
|
|
@@ -117,7 +134,7 @@ const CycleElements = ({ intl, values }) => {
|
|
|
117
134
|
};
|
|
118
135
|
|
|
119
136
|
const NavigatorCatalogueCardItem = (props) => {
|
|
120
|
-
const { result } = props;
|
|
137
|
+
const { result = {} } = props;
|
|
121
138
|
const intl = useIntl();
|
|
122
139
|
const sectors = rawValueAsArray(result.cca_adaptation_sectors);
|
|
123
140
|
const hazards = rawValueAsArray(result.cca_climate_impacts);
|
|
@@ -131,13 +148,14 @@ const NavigatorCatalogueCardItem = (props) => {
|
|
|
131
148
|
const adaptationSupportCycleSteps = rawValueAsArray(
|
|
132
149
|
result.adaptation_support_cycle_step,
|
|
133
150
|
)
|
|
134
|
-
.map((value) =>
|
|
151
|
+
.map((value) => {
|
|
152
|
+
const title = value?.title || value;
|
|
153
|
+
return typeof title === 'string' ? title.split(':')[0] : title;
|
|
154
|
+
})
|
|
135
155
|
.filter(Boolean);
|
|
136
156
|
const publicationDate =
|
|
137
157
|
result.publication_date?.raw || result.publication_date;
|
|
138
|
-
const formattedPublicationDate = publicationDate
|
|
139
|
-
? publicationDateFormatter.format(new Date(publicationDate))
|
|
140
|
-
: '';
|
|
158
|
+
const formattedPublicationDate = formatPublicationDate(publicationDate);
|
|
141
159
|
const sectorLabel = intl.formatMessage(messages.sector);
|
|
142
160
|
const hazardLabel = intl.formatMessage(messages.hazard);
|
|
143
161
|
const compareTool = {
|
|
@@ -175,6 +175,50 @@ describe('NavigatorCatalogueCardItem', () => {
|
|
|
175
175
|
expect(screen.queryByText('License:')).not.toBeInTheDocument();
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
+
it('safely handles invalid, array, or malformed publication dates without throwing RangeError', () => {
|
|
179
|
+
// Malformed/invalid date string that would throw RangeError: Invalid time value
|
|
180
|
+
const { rerender } = renderCard({
|
|
181
|
+
title: 'Invalid date tool',
|
|
182
|
+
publication_date: 'N/A',
|
|
183
|
+
});
|
|
184
|
+
expect(screen.getByText('Invalid date tool')).toBeInTheDocument();
|
|
185
|
+
expect(screen.queryByText('N/A')).not.toBeInTheDocument();
|
|
186
|
+
|
|
187
|
+
// Array publication date
|
|
188
|
+
rerender(
|
|
189
|
+
<IntlProvider locale="en">
|
|
190
|
+
<NavigatorCatalogueCardItem
|
|
191
|
+
result={{
|
|
192
|
+
title: 'Array date tool',
|
|
193
|
+
publication_date: { raw: ['2026-07-24'] },
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
</IntlProvider>,
|
|
197
|
+
);
|
|
198
|
+
expect(screen.getByText('24 Jul 26')).toBeInTheDocument();
|
|
199
|
+
|
|
200
|
+
// Malformed object publication date
|
|
201
|
+
rerender(
|
|
202
|
+
<IntlProvider locale="en">
|
|
203
|
+
<NavigatorCatalogueCardItem
|
|
204
|
+
result={{
|
|
205
|
+
title: 'Object date tool',
|
|
206
|
+
publication_date: { raw: { invalid: true } },
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
</IntlProvider>,
|
|
210
|
+
);
|
|
211
|
+
expect(screen.getByText('Object date tool')).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('safely handles non-string adaptationSupportCycleSteps without throwing', () => {
|
|
215
|
+
renderCard({
|
|
216
|
+
title: 'Numeric cycle tool',
|
|
217
|
+
adaptation_support_cycle_step: { raw: [123, null, { title: 456 }] },
|
|
218
|
+
});
|
|
219
|
+
expect(screen.getByText('Numeric cycle tool')).toBeInTheDocument();
|
|
220
|
+
});
|
|
221
|
+
|
|
178
222
|
describe('thumbnail rendering and fallback', () => {
|
|
179
223
|
it('renders thumbnail image and smoothly transitions from placeholder on load', () => {
|
|
180
224
|
const { container } = renderCard({
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import React, { useContext, memo } from 'react';
|
|
2
|
-
import { Icon } from 'semantic-ui-react';
|
|
3
|
-
import ExternalLink from '@eeacms/search/components/Result/ExternalLink';
|
|
4
2
|
|
|
5
3
|
import NavigatorCatalogueCardItem from '@eeacms/volto-cca-policy/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem';
|
|
6
4
|
// The `ChatMessageContext` seam only exists in @eeacms/volto-eea-chatbot from
|
|
@@ -54,137 +52,58 @@ export function matchesDocumentTitle(docTitle, searchTitle) {
|
|
|
54
52
|
return false;
|
|
55
53
|
}
|
|
56
54
|
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
const d = new Date(value);
|
|
60
|
-
if (Number.isNaN(d.getTime())) return null;
|
|
61
|
-
return d.toLocaleDateString('en-GB', {
|
|
62
|
-
day: '2-digit',
|
|
63
|
-
month: 'short',
|
|
64
|
-
year: '2-digit',
|
|
65
|
-
});
|
|
55
|
+
function sourcePropsEqual(prev, next) {
|
|
56
|
+
return prev.source?.link === next.source?.link;
|
|
66
57
|
}
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
prevSource?.semantic_identifier === nextSource?.semantic_identifier &&
|
|
74
|
-
prevSource?.blurb === nextSource?.blurb &&
|
|
75
|
-
prevSource?.updated_at === nextSource?.updated_at &&
|
|
76
|
-
prevSource?.source_type === nextSource?.source_type &&
|
|
77
|
-
prevSource?.link === nextSource?.link
|
|
78
|
-
);
|
|
79
|
-
}
|
|
59
|
+
export class DocCardErrorBoundary extends React.Component {
|
|
60
|
+
constructor(props) {
|
|
61
|
+
super(props);
|
|
62
|
+
this.state = { hasError: false };
|
|
63
|
+
}
|
|
80
64
|
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
static getDerivedStateFromError() {
|
|
66
|
+
return { hasError: true };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
componentDidCatch(error, info) {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.warn('DocCard failed to render rich catalogue card:', error, info);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
render() {
|
|
75
|
+
if (this.state.hasError) {
|
|
76
|
+
return this.props.fallback ?? null;
|
|
77
|
+
}
|
|
78
|
+
return this.props.children;
|
|
79
|
+
}
|
|
83
80
|
}
|
|
84
81
|
|
|
85
82
|
/**
|
|
86
|
-
* Document card
|
|
87
|
-
*
|
|
88
|
-
*
|
|
83
|
+
* Document card rendered once the globalsearch ES lookup (by document URL)
|
|
84
|
+
* resolves as the full Navigator catalogue card.
|
|
85
|
+
*
|
|
86
|
+
* If the document is not found in Elasticsearch (or lookup fails), no card
|
|
87
|
+
* is displayed.
|
|
89
88
|
*/
|
|
90
|
-
const
|
|
91
|
-
|
|
89
|
+
const EnhancedDocCard = memo(function EnhancedDocCard({ source }) {
|
|
90
|
+
const { result } = useCatalogueDoc(source?.link);
|
|
91
|
+
|
|
92
|
+
if (!result) {
|
|
92
93
|
return null;
|
|
93
94
|
}
|
|
94
|
-
const {
|
|
95
|
-
semantic_identifier: rawTitle,
|
|
96
|
-
blurb,
|
|
97
|
-
updated_at,
|
|
98
|
-
source_type,
|
|
99
|
-
link,
|
|
100
|
-
} = source;
|
|
101
|
-
const isWeb = source_type === 'web';
|
|
102
|
-
const title = cleanDocumentTitle(rawTitle);
|
|
103
|
-
const formattedDate = formatDate(updated_at);
|
|
104
|
-
const typeLabel = isWeb ? 'Web' : 'Document';
|
|
105
95
|
|
|
106
96
|
return (
|
|
107
|
-
<
|
|
108
|
-
<div className="
|
|
109
|
-
<div className="
|
|
110
|
-
<
|
|
111
|
-
</div>
|
|
112
|
-
|
|
113
|
-
<div className="catalogue-item-main">
|
|
114
|
-
<div className="catalogue-item-top">
|
|
115
|
-
<div className="navigator-tool-provider">
|
|
116
|
-
{typeof index === 'number' && (
|
|
117
|
-
<span className="chat-citation">{index} </span>
|
|
118
|
-
)}
|
|
119
|
-
{typeLabel}
|
|
120
|
-
</div>
|
|
121
|
-
{formattedDate && (
|
|
122
|
-
<span className="catalogue-date">{formattedDate}</span>
|
|
123
|
-
)}
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div className="catalogue-item-heading">
|
|
127
|
-
<h4>
|
|
128
|
-
{link ? (
|
|
129
|
-
<ExternalLink href={link} title={title}>
|
|
130
|
-
{title}
|
|
131
|
-
</ExternalLink>
|
|
132
|
-
) : (
|
|
133
|
-
<span title={title}>{title}</span>
|
|
134
|
-
)}
|
|
135
|
-
</h4>
|
|
136
|
-
</div>
|
|
137
|
-
|
|
138
|
-
{blurb && <p className="catalogue-description">{blurb}</p>}
|
|
139
|
-
|
|
140
|
-
<div className="catalogue-item-footer">
|
|
141
|
-
<div className="catalogue-meta license-type">
|
|
142
|
-
<span className="catalogue-type">Type: {typeLabel}</span>
|
|
143
|
-
</div>
|
|
144
|
-
|
|
145
|
-
<div className="catalogue-actions">
|
|
146
|
-
{link && (
|
|
147
|
-
<ExternalLink
|
|
148
|
-
href={link}
|
|
149
|
-
className="ui button primary icon"
|
|
150
|
-
labelPosition="left"
|
|
151
|
-
>
|
|
152
|
-
View
|
|
153
|
-
<Icon className="ri-arrow-right-line" />
|
|
154
|
-
</ExternalLink>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
97
|
+
<DocCardErrorBoundary fallback={null}>
|
|
98
|
+
<div className="catalogue-chat-card-inline">
|
|
99
|
+
<div className="catalogue-chat-navigator-card">
|
|
100
|
+
<NavigatorCatalogueCardItem result={result} />
|
|
158
101
|
</div>
|
|
159
102
|
</div>
|
|
160
|
-
</
|
|
103
|
+
</DocCardErrorBoundary>
|
|
161
104
|
);
|
|
162
105
|
}, sourcePropsEqual);
|
|
163
106
|
|
|
164
|
-
/**
|
|
165
|
-
* Document card that upgrades itself: while it has only the Onyx-provided
|
|
166
|
-
* fields it renders the basic `DocumentCard`; once the globalsearch ES
|
|
167
|
-
* lookup (by document URL) resolves it re-renders as the full Navigator
|
|
168
|
-
* catalogue card.
|
|
169
|
-
*
|
|
170
|
-
* Memoized: the answer markdown is re-rendered on every streaming tick, but
|
|
171
|
-
* the (already-shown, potentially heavy) card should only re-render when its
|
|
172
|
-
* own data changes, not on every tick. Its own basic→full upgrade is driven
|
|
173
|
-
* by internal state, which still re-renders it exactly once.
|
|
174
|
-
*/
|
|
175
|
-
const EnhancedDocCard = memo(function EnhancedDocCard({ source, index }) {
|
|
176
|
-
const { result } = useCatalogueDoc(source?.link);
|
|
177
|
-
|
|
178
|
-
if (result) {
|
|
179
|
-
return (
|
|
180
|
-
<div className="catalogue-chat-navigator-card">
|
|
181
|
-
<NavigatorCatalogueCardItem result={result} />
|
|
182
|
-
</div>
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
return <DocumentCard source={source} index={index} />;
|
|
186
|
-
}, sourcePropsEqual);
|
|
187
|
-
|
|
188
107
|
/**
|
|
189
108
|
* Inline document card, rendered in the message text where the assistant
|
|
190
109
|
* emitted a `![[doc: Title]]` marker (see docCards.js). Tries to match the
|
|
@@ -204,11 +123,7 @@ export function InlineDocCard({ title, documents = [] }) {
|
|
|
204
123
|
source_type: doc?.source_type,
|
|
205
124
|
link: doc?.link,
|
|
206
125
|
};
|
|
207
|
-
return
|
|
208
|
-
<div className="catalogue-chat-card-inline">
|
|
209
|
-
<EnhancedDocCard source={source} />
|
|
210
|
-
</div>
|
|
211
|
-
);
|
|
126
|
+
return <EnhancedDocCard source={source} />;
|
|
212
127
|
}
|
|
213
128
|
|
|
214
129
|
/**
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import '@testing-library/jest-dom';
|
|
3
|
-
import {
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
4
|
|
|
5
5
|
import { ChatMessageContext } from '@eeacms/volto-eea-chatbot/ChatBlock/chat';
|
|
6
6
|
import { useCatalogueDoc } from './useCatalogueDoc';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
CcaDocCard,
|
|
9
|
+
DocCardErrorBoundary,
|
|
10
|
+
InlineDocCard,
|
|
11
|
+
cleanDocumentTitle,
|
|
12
|
+
matchesDocumentTitle,
|
|
13
|
+
} from './DocumentCard';
|
|
8
14
|
|
|
9
15
|
jest.mock('@eeacms/volto-eea-chatbot/ChatBlock/chat', () => ({
|
|
10
16
|
ChatMessageContext:
|
|
@@ -12,14 +18,20 @@ jest.mock('@eeacms/volto-eea-chatbot/ChatBlock/chat', () => ({
|
|
|
12
18
|
require('react').createContext(null),
|
|
13
19
|
}));
|
|
14
20
|
|
|
21
|
+
let mockNavigatorShouldThrow = false;
|
|
22
|
+
|
|
15
23
|
jest.mock(
|
|
16
24
|
'@eeacms/volto-cca-policy/components/Search/NavigatorCatalogue/NavigatorCatalogueCardItem',
|
|
17
|
-
() =>
|
|
18
|
-
(
|
|
25
|
+
() => (props) => {
|
|
26
|
+
if (mockNavigatorShouldThrow) {
|
|
27
|
+
throw new RangeError('Invalid time value');
|
|
28
|
+
}
|
|
29
|
+
return <div data-testid="navigator-card" />;
|
|
30
|
+
},
|
|
19
31
|
);
|
|
20
32
|
|
|
21
33
|
jest.mock('./useCatalogueDoc', () => ({
|
|
22
|
-
useCatalogueDoc: jest.fn(() => ({ result: null, loading:
|
|
34
|
+
useCatalogueDoc: jest.fn(() => ({ result: null, loading: false })),
|
|
23
35
|
}));
|
|
24
36
|
|
|
25
37
|
const doc = {
|
|
@@ -30,66 +42,75 @@ const doc = {
|
|
|
30
42
|
link: 'https://example.com/france-nas',
|
|
31
43
|
};
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
45
|
+
describe('cleanDocumentTitle', () => {
|
|
46
|
+
it('strips pipe-separated suffixes', () => {
|
|
47
|
+
expect(
|
|
48
|
+
cleanDocumentTitle('Climate policy radar | Tools | Something else'),
|
|
49
|
+
).toBe('Climate policy radar');
|
|
50
|
+
});
|
|
40
51
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
useCatalogueDoc.mockReset();
|
|
44
|
-
useCatalogueDoc.mockReturnValue({ result: null, loading: true });
|
|
52
|
+
it('returns trimmed title when no pipe is present', () => {
|
|
53
|
+
expect(cleanDocumentTitle(' Some Title ')).toBe('Some Title');
|
|
45
54
|
});
|
|
46
55
|
|
|
47
|
-
it('
|
|
48
|
-
|
|
49
|
-
expect(
|
|
50
|
-
expect(
|
|
51
|
-
expect(screen.queryByText('The French NAS.')).not.toBeInTheDocument();
|
|
56
|
+
it('handles null and undefined gracefully', () => {
|
|
57
|
+
expect(cleanDocumentTitle(null)).toBe('');
|
|
58
|
+
expect(cleanDocumentTitle(undefined)).toBe('');
|
|
59
|
+
expect(cleanDocumentTitle(123)).toBe('');
|
|
52
60
|
});
|
|
61
|
+
});
|
|
53
62
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
title=" france: NATIONAL adaptation strategy "
|
|
58
|
-
documents={[doc]}
|
|
59
|
-
/>,
|
|
60
|
-
);
|
|
61
|
-
expect(screen.getByText('The French NAS.')).toBeInTheDocument();
|
|
62
|
-
expect(screen.getByText('01 May 23')).toBeInTheDocument();
|
|
63
|
+
describe('matchesDocumentTitle', () => {
|
|
64
|
+
it('matches identical titles', () => {
|
|
65
|
+
expect(matchesDocumentTitle('Same Title', 'Same Title')).toBe(true);
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
it('
|
|
66
|
-
|
|
67
|
-
const link = screen.getByRole('link', { name: 'Some Web Source' });
|
|
68
|
-
expect(link).toHaveAttribute('href', 'https://example.com/web');
|
|
69
|
-
expect(link).toHaveAttribute('target', '_blank');
|
|
70
|
-
expect(screen.getByText('Web')).toBeInTheDocument();
|
|
68
|
+
it('matches case-insensitively and with trimmed whitespace', () => {
|
|
69
|
+
expect(matchesDocumentTitle(' Title One ', 'title one')).toBe(true);
|
|
71
70
|
});
|
|
72
71
|
|
|
73
|
-
it('
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
it('matches noisy document titles with clean search titles', () => {
|
|
73
|
+
expect(
|
|
74
|
+
matchesDocumentTitle(
|
|
75
|
+
'Nature DEMO | Tools | Discover the key services',
|
|
76
|
+
'Nature DEMO',
|
|
77
|
+
),
|
|
78
|
+
).toBe(true);
|
|
79
|
+
});
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
it('returns false for non-matching or empty titles', () => {
|
|
82
|
+
expect(matchesDocumentTitle('Alpha', 'Beta')).toBe(false);
|
|
83
|
+
expect(matchesDocumentTitle(null, 'Beta')).toBe(false);
|
|
84
|
+
expect(matchesDocumentTitle('Alpha', '')).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('InlineDocCard', () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
useCatalogueDoc.mockReset();
|
|
91
|
+
useCatalogueDoc.mockReturnValue({ result: null, loading: false });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('renders nothing when no document matches', () => {
|
|
95
|
+
const { container } = render(
|
|
96
|
+
<InlineDocCard title="Unknown Title" documents={[doc]} />,
|
|
84
97
|
);
|
|
85
|
-
expect(
|
|
86
|
-
|
|
98
|
+
expect(container.firstChild).toBeNull();
|
|
99
|
+
});
|
|
87
100
|
|
|
88
|
-
|
|
89
|
-
|
|
101
|
+
it('renders nothing when ES lookup returns empty (not found in ES)', () => {
|
|
102
|
+
useCatalogueDoc.mockReturnValue({
|
|
103
|
+
result: null,
|
|
104
|
+
loading: false,
|
|
105
|
+
});
|
|
106
|
+
const { container } = render(
|
|
107
|
+
<InlineDocCard title={doc.semantic_identifier} documents={[doc]} />,
|
|
108
|
+
);
|
|
109
|
+
expect(container.firstChild).toBeNull();
|
|
110
|
+
expect(screen.queryByTestId('navigator-card')).not.toBeInTheDocument();
|
|
90
111
|
});
|
|
91
112
|
|
|
92
|
-
it('
|
|
113
|
+
it('renders the Navigator card once the ES lookup resolves with a document', () => {
|
|
93
114
|
useCatalogueDoc.mockReturnValue({
|
|
94
115
|
result: { found: true, uid: 'https://example.com/france-nas' },
|
|
95
116
|
loading: false,
|
|
@@ -99,55 +120,123 @@ describe('InlineDocCard', () => {
|
|
|
99
120
|
expect(useCatalogueDoc).toHaveBeenCalledWith(doc.link);
|
|
100
121
|
});
|
|
101
122
|
|
|
102
|
-
it('
|
|
123
|
+
it('matches title case-insensitively and renders card when ES lookup resolves', () => {
|
|
124
|
+
useCatalogueDoc.mockReturnValue({
|
|
125
|
+
result: { found: true, uid: 'https://example.com/france-nas' },
|
|
126
|
+
loading: false,
|
|
127
|
+
});
|
|
128
|
+
render(
|
|
129
|
+
<InlineDocCard
|
|
130
|
+
title=" france: NATIONAL adaptation strategy "
|
|
131
|
+
documents={[doc]}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
expect(screen.getByTestId('navigator-card')).toBeInTheDocument();
|
|
135
|
+
expect(useCatalogueDoc).toHaveBeenCalledWith(doc.link);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('cleans pipeline title noise and matches cleanly when ES lookup resolves', () => {
|
|
103
139
|
const noisyDoc = {
|
|
104
140
|
...doc,
|
|
105
141
|
semantic_identifier:
|
|
106
142
|
'Climate policy radar | Tools | Discover the key services, thematic features and tools of Climate-ADAPT Climate-ADAPT',
|
|
107
143
|
};
|
|
144
|
+
useCatalogueDoc.mockReturnValue({
|
|
145
|
+
result: { found: true, uid: doc.link },
|
|
146
|
+
loading: false,
|
|
147
|
+
});
|
|
108
148
|
render(
|
|
109
149
|
<InlineDocCard title="Climate policy radar" documents={[noisyDoc]} />,
|
|
110
150
|
);
|
|
111
|
-
expect(screen.
|
|
112
|
-
expect(
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('renders a View button when link is present', () => {
|
|
116
|
-
render(<InlineDocCard title="Some Web Source" documents={[webDoc]} />);
|
|
117
|
-
const viewButton = screen.getByRole('link', { name: 'View' });
|
|
118
|
-
expect(viewButton).toHaveAttribute('href', 'https://example.com/web');
|
|
119
|
-
expect(viewButton).toHaveClass('ui button primary icon');
|
|
151
|
+
expect(screen.getByTestId('navigator-card')).toBeInTheDocument();
|
|
152
|
+
expect(useCatalogueDoc).toHaveBeenCalledWith(doc.link);
|
|
120
153
|
});
|
|
121
154
|
|
|
122
|
-
it('calls
|
|
123
|
-
|
|
124
|
-
|
|
155
|
+
it('calls useCatalogueDoc with undefined when no match is found, rendering nothing', () => {
|
|
156
|
+
const { container } = render(
|
|
157
|
+
<InlineDocCard title="No Match" documents={[]} />,
|
|
158
|
+
);
|
|
125
159
|
expect(useCatalogueDoc).toHaveBeenCalledWith(undefined);
|
|
160
|
+
expect(container.firstChild).toBeNull();
|
|
126
161
|
});
|
|
127
162
|
});
|
|
128
163
|
|
|
129
164
|
describe('CcaDocCard', () => {
|
|
130
|
-
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
useCatalogueDoc.mockReset();
|
|
167
|
+
useCatalogueDoc.mockReturnValue({ result: null, loading: false });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('matches the marker against owning message documents and renders when ES resolves', () => {
|
|
171
|
+
useCatalogueDoc.mockReturnValue({
|
|
172
|
+
result: { found: true, uid: doc.link },
|
|
173
|
+
loading: false,
|
|
174
|
+
});
|
|
131
175
|
render(
|
|
132
176
|
<ChatMessageContext.Provider value={{ documents: [doc] }}>
|
|
133
177
|
<CcaDocCard title={doc.semantic_identifier} />
|
|
134
178
|
</ChatMessageContext.Provider>,
|
|
135
179
|
);
|
|
136
|
-
expect(screen.
|
|
180
|
+
expect(screen.getByTestId('navigator-card')).toBeInTheDocument();
|
|
137
181
|
});
|
|
138
182
|
|
|
139
|
-
it('
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
183
|
+
it('renders nothing when message context is absent', () => {
|
|
184
|
+
const { container } = render(
|
|
185
|
+
<CcaDocCard title={doc.semantic_identifier} />,
|
|
186
|
+
);
|
|
187
|
+
expect(container.firstChild).toBeNull();
|
|
143
188
|
});
|
|
144
189
|
|
|
145
|
-
it('
|
|
146
|
-
render(
|
|
190
|
+
it('renders nothing when message has no documents', () => {
|
|
191
|
+
const { container } = render(
|
|
147
192
|
<ChatMessageContext.Provider value={{ documents: [] }}>
|
|
148
193
|
<CcaDocCard title={doc.semantic_identifier} />
|
|
149
194
|
</ChatMessageContext.Provider>,
|
|
150
195
|
);
|
|
151
|
-
expect(
|
|
196
|
+
expect(container.firstChild).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('renders nothing when the rich catalogue card throws an error (caught by error boundary)', () => {
|
|
200
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
201
|
+
|
|
202
|
+
useCatalogueDoc.mockReturnValue({
|
|
203
|
+
result: { title: 'Crashing Tool' },
|
|
204
|
+
loading: false,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
mockNavigatorShouldThrow = true;
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const { container } = render(
|
|
211
|
+
<ChatMessageContext.Provider value={{ documents: [doc] }}>
|
|
212
|
+
<CcaDocCard title={doc.semantic_identifier} />
|
|
213
|
+
</ChatMessageContext.Provider>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(container.firstChild).toBeNull();
|
|
217
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
218
|
+
} finally {
|
|
219
|
+
mockNavigatorShouldThrow = false;
|
|
220
|
+
warnSpy.mockRestore();
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('DocCardErrorBoundary', () => {
|
|
226
|
+
it('renders custom fallback when provided and an error occurs', () => {
|
|
227
|
+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
228
|
+
const ThrowingComponent = () => {
|
|
229
|
+
throw new Error('Test error');
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const { getByText } = render(
|
|
233
|
+
<DocCardErrorBoundary fallback={<div>Fallback Content</div>}>
|
|
234
|
+
<ThrowingComponent />
|
|
235
|
+
</DocCardErrorBoundary>,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(getByText('Fallback Content')).toBeInTheDocument();
|
|
239
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
240
|
+
warnSpy.mockRestore();
|
|
152
241
|
});
|
|
153
242
|
});
|