@pie-lib/text-select 3.0.3-next.37 → 3.0.3-next.51
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.json +1 -0
- package/CHANGELOG.md +946 -0
- package/LICENSE.md +5 -0
- package/lib/index.js +57 -0
- package/lib/index.js.map +1 -0
- package/lib/legend.js +119 -0
- package/lib/legend.js.map +1 -0
- package/lib/text-select.js +105 -0
- package/lib/text-select.js.map +1 -0
- package/lib/token-select/index.js +267 -0
- package/lib/token-select/index.js.map +1 -0
- package/lib/token-select/token.js +236 -0
- package/lib/token-select/token.js.map +1 -0
- package/lib/tokenizer/builder.js +265 -0
- package/lib/tokenizer/builder.js.map +1 -0
- package/lib/tokenizer/controls.js +106 -0
- package/lib/tokenizer/controls.js.map +1 -0
- package/lib/tokenizer/index.js +147 -0
- package/lib/tokenizer/index.js.map +1 -0
- package/lib/tokenizer/selection-utils.js +55 -0
- package/lib/tokenizer/selection-utils.js.map +1 -0
- package/lib/tokenizer/token-text.js +176 -0
- package/lib/tokenizer/token-text.js.map +1 -0
- package/lib/utils.js +51 -0
- package/lib/utils.js.map +1 -0
- package/package.json +30 -33
- package/src/__tests__/legend.test.jsx +211 -0
- package/src/__tests__/text-select.test.jsx +44 -0
- package/src/__tests__/utils.test.jsx +27 -0
- package/src/index.js +8 -0
- package/src/legend.js +102 -0
- package/src/text-select.jsx +79 -0
- package/src/token-select/__tests__/index.test.jsx +623 -0
- package/src/token-select/__tests__/token.test.jsx +236 -0
- package/src/token-select/index.jsx +242 -0
- package/src/token-select/token.jsx +223 -0
- package/src/tokenizer/__tests__/builder.test.js +256 -0
- package/src/tokenizer/__tests__/controls.test.jsx +27 -0
- package/src/tokenizer/__tests__/index.test.jsx +329 -0
- package/src/tokenizer/__tests__/selection-utils.test.js +145 -0
- package/src/tokenizer/__tests__/token-text.test.jsx +318 -0
- package/src/tokenizer/builder.js +258 -0
- package/src/tokenizer/controls.jsx +71 -0
- package/src/tokenizer/index.jsx +144 -0
- package/src/tokenizer/selection-utils.js +49 -0
- package/src/tokenizer/token-text.jsx +135 -0
- package/src/utils.js +56 -0
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -7
- package/dist/legend.d.ts +0 -13
- package/dist/legend.js +0 -64
- package/dist/node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.js +0 -16
- package/dist/text-select.d.ts +0 -34
- package/dist/text-select.js +0 -53
- package/dist/token-select/index.d.ts +0 -44
- package/dist/token-select/index.js +0 -170
- package/dist/token-select/token.d.ts +0 -32
- package/dist/token-select/token.js +0 -134
- package/dist/tokenizer/builder.d.ts +0 -27
- package/dist/tokenizer/builder.js +0 -124
- package/dist/tokenizer/controls.d.ts +0 -23
- package/dist/tokenizer/controls.js +0 -68
- package/dist/tokenizer/index.d.ts +0 -35
- package/dist/tokenizer/index.js +0 -91
- package/dist/tokenizer/selection-utils.d.ts +0 -10
- package/dist/tokenizer/selection-utils.js +0 -18
- package/dist/tokenizer/token-text.d.ts +0 -27
- package/dist/tokenizer/token-text.js +0 -85
- package/dist/utils.d.ts +0 -12
- package/dist/utils.js +0 -21
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import { TokenSelect } from '../index';
|
|
4
|
+
|
|
5
|
+
describe('token-select', () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
tokens: [
|
|
8
|
+
{
|
|
9
|
+
text: 'foo bar',
|
|
10
|
+
start: 0,
|
|
11
|
+
end: 7,
|
|
12
|
+
predefined: true,
|
|
13
|
+
selectable: true,
|
|
14
|
+
selected: false,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
onChange: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
describe('rendering', () => {
|
|
21
|
+
it('renders with default props', () => {
|
|
22
|
+
const { container } = render(<TokenSelect {...defaultProps} />);
|
|
23
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders sentences with newlines', () => {
|
|
27
|
+
const tokens = [
|
|
28
|
+
{
|
|
29
|
+
text: 'foo,',
|
|
30
|
+
start: 0,
|
|
31
|
+
end: 4,
|
|
32
|
+
predefined: true,
|
|
33
|
+
selectable: true,
|
|
34
|
+
selected: false,
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
text: '\n',
|
|
38
|
+
start: 4,
|
|
39
|
+
end: 5,
|
|
40
|
+
selected: false,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
text: 'bar',
|
|
44
|
+
start: 5,
|
|
45
|
+
end: 8,
|
|
46
|
+
predefined: true,
|
|
47
|
+
selectable: true,
|
|
48
|
+
selected: false,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
52
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('renders paragraphs with double newlines', () => {
|
|
56
|
+
const tokens = [
|
|
57
|
+
{
|
|
58
|
+
text: 'foo,',
|
|
59
|
+
start: 0,
|
|
60
|
+
end: 4,
|
|
61
|
+
predefined: true,
|
|
62
|
+
selectable: true,
|
|
63
|
+
selected: false,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
text: '\n\n',
|
|
67
|
+
start: 4,
|
|
68
|
+
end: 5,
|
|
69
|
+
selected: false,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
text: 'bar',
|
|
73
|
+
start: 5,
|
|
74
|
+
end: 8,
|
|
75
|
+
predefined: true,
|
|
76
|
+
selectable: true,
|
|
77
|
+
selected: false,
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
81
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders in disabled mode with selected tokens', () => {
|
|
85
|
+
const tokens = [
|
|
86
|
+
{
|
|
87
|
+
text: 'foo,',
|
|
88
|
+
start: 0,
|
|
89
|
+
end: 4,
|
|
90
|
+
predefined: true,
|
|
91
|
+
selectable: true,
|
|
92
|
+
selected: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
text: '\n',
|
|
96
|
+
start: 4,
|
|
97
|
+
end: 5,
|
|
98
|
+
selected: false,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
text: 'bar',
|
|
102
|
+
start: 5,
|
|
103
|
+
end: 8,
|
|
104
|
+
predefined: true,
|
|
105
|
+
selectable: true,
|
|
106
|
+
selected: true,
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
const { container } = render(<TokenSelect {...defaultProps} disabled tokens={tokens} />);
|
|
110
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('renders with maxNoOfSelections', () => {
|
|
114
|
+
const { container } = render(<TokenSelect {...defaultProps} maxNoOfSelections={5} />);
|
|
115
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders with highlightChoices enabled', () => {
|
|
119
|
+
const { container } = render(<TokenSelect {...defaultProps} highlightChoices={true} />);
|
|
120
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('strips HTML tags from token text', () => {
|
|
124
|
+
const tokens = [
|
|
125
|
+
{
|
|
126
|
+
text: '<b>bold text</b>',
|
|
127
|
+
start: 0,
|
|
128
|
+
end: 16,
|
|
129
|
+
predefined: true,
|
|
130
|
+
selectable: true,
|
|
131
|
+
selected: false,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
135
|
+
expect(container.textContent).toContain('bold text');
|
|
136
|
+
// The selectable token should have HTML tags stripped before rendering
|
|
137
|
+
// Look specifically in the main content div (not the hidden primer)
|
|
138
|
+
const mainDiv = container.querySelectorAll('div[class*="css-"]')[1];
|
|
139
|
+
expect(mainDiv.textContent).toContain('bold text');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('renders with custom className', () => {
|
|
143
|
+
const { container } = render(<TokenSelect {...defaultProps} className="custom-token-select" />);
|
|
144
|
+
// The second div with Emotion class should have the custom className
|
|
145
|
+
const styledDivs = container.querySelectorAll('div[class*="css-"]');
|
|
146
|
+
const mainDiv = styledDivs[styledDivs.length - 1];
|
|
147
|
+
expect(mainDiv).toHaveClass('custom-token-select');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('renders tokens with correct and incorrect states', () => {
|
|
151
|
+
const tokens = [
|
|
152
|
+
{
|
|
153
|
+
text: 'correct',
|
|
154
|
+
start: 0,
|
|
155
|
+
end: 7,
|
|
156
|
+
predefined: true,
|
|
157
|
+
selectable: true,
|
|
158
|
+
selected: true,
|
|
159
|
+
correct: true,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
text: 'incorrect',
|
|
163
|
+
start: 8,
|
|
164
|
+
end: 17,
|
|
165
|
+
predefined: true,
|
|
166
|
+
selectable: true,
|
|
167
|
+
selected: true,
|
|
168
|
+
correct: false,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
172
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
173
|
+
const checkIcon = container.querySelector('svg[data-testid="CheckIcon"]');
|
|
174
|
+
const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
|
|
175
|
+
expect(checkIcon).toBeInTheDocument();
|
|
176
|
+
expect(closeIcon).toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('renders tokens with isMissing state', () => {
|
|
180
|
+
const tokens = [
|
|
181
|
+
{
|
|
182
|
+
text: 'missing answer',
|
|
183
|
+
start: 0,
|
|
184
|
+
end: 14,
|
|
185
|
+
predefined: true,
|
|
186
|
+
selectable: true,
|
|
187
|
+
selected: false,
|
|
188
|
+
isMissing: true,
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
192
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
193
|
+
const closeIcon = container.querySelector('svg[data-testid="CloseIcon"]');
|
|
194
|
+
expect(closeIcon).toBeInTheDocument();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('token interaction', () => {
|
|
199
|
+
it('calls onChange when clicking a selectable token', () => {
|
|
200
|
+
const onChange = jest.fn();
|
|
201
|
+
const tokens = [
|
|
202
|
+
{
|
|
203
|
+
text: 'foo bar',
|
|
204
|
+
start: 0,
|
|
205
|
+
end: 7,
|
|
206
|
+
predefined: true,
|
|
207
|
+
selectable: true,
|
|
208
|
+
selected: false,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
|
|
212
|
+
|
|
213
|
+
const tokenElements = container.querySelectorAll('.tokenRootClass');
|
|
214
|
+
// Find the first token with data-indexkey >= 0 (skip primer tokens at -1)
|
|
215
|
+
let tokenElement = null;
|
|
216
|
+
for (const el of tokenElements) {
|
|
217
|
+
if (parseInt(el.getAttribute('data-indexkey'), 10) >= 0) {
|
|
218
|
+
tokenElement = el;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (tokenElement) {
|
|
223
|
+
fireEvent.click(tokenElement);
|
|
224
|
+
expect(onChange).toHaveBeenCalled();
|
|
225
|
+
const updatedTokens = onChange.mock.calls[0][0];
|
|
226
|
+
expect(updatedTokens[0].selected).toBe(true);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('handles maxNoOfSelections of 1 by deselecting previous token', () => {
|
|
231
|
+
const onChange = jest.fn();
|
|
232
|
+
const tokens = [
|
|
233
|
+
{
|
|
234
|
+
text: 'first',
|
|
235
|
+
start: 0,
|
|
236
|
+
end: 5,
|
|
237
|
+
predefined: true,
|
|
238
|
+
selectable: true,
|
|
239
|
+
selected: true,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
text: 'second',
|
|
243
|
+
start: 6,
|
|
244
|
+
end: 12,
|
|
245
|
+
predefined: true,
|
|
246
|
+
selectable: true,
|
|
247
|
+
selected: false,
|
|
248
|
+
},
|
|
249
|
+
];
|
|
250
|
+
const { container } = render(
|
|
251
|
+
<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} maxNoOfSelections={1} />,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const tokenElements = container.querySelectorAll('.tokenRootClass');
|
|
255
|
+
// Find tokens with data-indexkey >= 0 (skip primer tokens)
|
|
256
|
+
const realTokens = Array.from(tokenElements).filter(
|
|
257
|
+
(el) => parseInt(el.getAttribute('data-indexkey'), 10) >= 0,
|
|
258
|
+
);
|
|
259
|
+
if (realTokens.length > 1) {
|
|
260
|
+
fireEvent.click(realTokens[1]);
|
|
261
|
+
expect(onChange).toHaveBeenCalled();
|
|
262
|
+
const updatedTokens = onChange.mock.calls[0][0];
|
|
263
|
+
expect(updatedTokens[0].selected).toBe(false);
|
|
264
|
+
expect(updatedTokens[1].selected).toBe(true);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('prevents selecting more tokens when maxNoOfSelections is reached', () => {
|
|
269
|
+
const onChange = jest.fn();
|
|
270
|
+
const tokens = [
|
|
271
|
+
{
|
|
272
|
+
text: 'first',
|
|
273
|
+
start: 0,
|
|
274
|
+
end: 5,
|
|
275
|
+
predefined: true,
|
|
276
|
+
selectable: true,
|
|
277
|
+
selected: true,
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
text: 'second',
|
|
281
|
+
start: 6,
|
|
282
|
+
end: 12,
|
|
283
|
+
predefined: true,
|
|
284
|
+
selectable: true,
|
|
285
|
+
selected: true,
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
text: 'third',
|
|
289
|
+
start: 13,
|
|
290
|
+
end: 18,
|
|
291
|
+
predefined: true,
|
|
292
|
+
selectable: true,
|
|
293
|
+
selected: false,
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
const { container } = render(
|
|
297
|
+
<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} maxNoOfSelections={2} />,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
const tokenElements = container.querySelectorAll('.tokenRootClass');
|
|
301
|
+
const realTokens = Array.from(tokenElements).filter(
|
|
302
|
+
(el) => parseInt(el.getAttribute('data-indexkey'), 10) >= 0,
|
|
303
|
+
);
|
|
304
|
+
if (realTokens.length > 2) {
|
|
305
|
+
fireEvent.click(realTokens[2]);
|
|
306
|
+
// onChange should not be called because max is reached
|
|
307
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('does not toggle token when in animationsDisabled mode', () => {
|
|
312
|
+
const onChange = jest.fn();
|
|
313
|
+
const tokens = [
|
|
314
|
+
{
|
|
315
|
+
text: 'foo bar',
|
|
316
|
+
start: 0,
|
|
317
|
+
end: 7,
|
|
318
|
+
predefined: true,
|
|
319
|
+
selectable: true,
|
|
320
|
+
selected: false,
|
|
321
|
+
},
|
|
322
|
+
];
|
|
323
|
+
const { container } = render(
|
|
324
|
+
<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} animationsDisabled={true} />,
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const tokenElement = container.querySelector('.tokenRootClass');
|
|
328
|
+
if (tokenElement) {
|
|
329
|
+
fireEvent.click(tokenElement);
|
|
330
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('does not toggle token when correct is defined', () => {
|
|
335
|
+
const onChange = jest.fn();
|
|
336
|
+
const tokens = [
|
|
337
|
+
{
|
|
338
|
+
text: 'foo bar',
|
|
339
|
+
start: 0,
|
|
340
|
+
end: 7,
|
|
341
|
+
predefined: true,
|
|
342
|
+
selectable: true,
|
|
343
|
+
selected: false,
|
|
344
|
+
correct: true,
|
|
345
|
+
},
|
|
346
|
+
];
|
|
347
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
|
|
348
|
+
|
|
349
|
+
const tokenElement = container.querySelector('.tokenRootClass');
|
|
350
|
+
if (tokenElement) {
|
|
351
|
+
fireEvent.click(tokenElement);
|
|
352
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('does not toggle token when isMissing is true', () => {
|
|
357
|
+
const onChange = jest.fn();
|
|
358
|
+
const tokens = [
|
|
359
|
+
{
|
|
360
|
+
text: 'foo bar',
|
|
361
|
+
start: 0,
|
|
362
|
+
end: 7,
|
|
363
|
+
predefined: true,
|
|
364
|
+
selectable: true,
|
|
365
|
+
selected: false,
|
|
366
|
+
isMissing: true,
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
|
|
370
|
+
|
|
371
|
+
const tokenElement = container.querySelector('.tokenRootClass');
|
|
372
|
+
if (tokenElement) {
|
|
373
|
+
fireEvent.click(tokenElement);
|
|
374
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('allows toggling token off when selected', () => {
|
|
379
|
+
const onChange = jest.fn();
|
|
380
|
+
const tokens = [
|
|
381
|
+
{
|
|
382
|
+
text: 'foo bar',
|
|
383
|
+
start: 0,
|
|
384
|
+
end: 7,
|
|
385
|
+
predefined: true,
|
|
386
|
+
selectable: true,
|
|
387
|
+
selected: true,
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} onChange={onChange} />);
|
|
391
|
+
|
|
392
|
+
const tokenElements = container.querySelectorAll('.tokenRootClass');
|
|
393
|
+
let tokenElement = null;
|
|
394
|
+
for (const el of tokenElements) {
|
|
395
|
+
if (parseInt(el.getAttribute('data-indexkey'), 10) >= 0) {
|
|
396
|
+
tokenElement = el;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (tokenElement) {
|
|
401
|
+
fireEvent.click(tokenElement);
|
|
402
|
+
expect(onChange).toHaveBeenCalled();
|
|
403
|
+
const updatedTokens = onChange.mock.calls[0][0];
|
|
404
|
+
expect(updatedTokens[0].selected).toBe(false);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('CSS injection and styling', () => {
|
|
410
|
+
it('renders HiddenCssPrimer to inject CSS for all Token variants', () => {
|
|
411
|
+
const { container } = render(<TokenSelect {...defaultProps} />);
|
|
412
|
+
// Check that the component renders without errors; the HiddenCssPrimer
|
|
413
|
+
// ensures CSS is injected into the document for all token states.
|
|
414
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
415
|
+
// The HiddenCssPrimer should have aria-hidden to mark it as invisible
|
|
416
|
+
const primer = container.querySelector('[aria-hidden="true"]');
|
|
417
|
+
expect(primer).toBeInTheDocument();
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('renders tokens with Emotion CSS class names', () => {
|
|
421
|
+
const { container } = render(<TokenSelect {...defaultProps} />);
|
|
422
|
+
const tokenElement = container.querySelector('.tokenRootClass');
|
|
423
|
+
expect(tokenElement).toBeInTheDocument();
|
|
424
|
+
// Should have Emotion-generated class name like css-xxxxx
|
|
425
|
+
const hasEmotionClass = Array.from(tokenElement.classList).some((cls) => cls.startsWith('css-'));
|
|
426
|
+
expect(hasEmotionClass).toBe(true);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe('HTML preservation', () => {
|
|
431
|
+
it('preserves non-selectable HTML content', () => {
|
|
432
|
+
const tokens = [
|
|
433
|
+
{
|
|
434
|
+
text: '<table><tr><td>table content</td></tr></table>',
|
|
435
|
+
start: 0,
|
|
436
|
+
end: 46,
|
|
437
|
+
predefined: false,
|
|
438
|
+
selectable: false,
|
|
439
|
+
selected: false,
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
443
|
+
// Non-selectable HTML should be rendered as-is
|
|
444
|
+
const html = container.innerHTML;
|
|
445
|
+
expect(html).toContain('table');
|
|
446
|
+
expect(html).toContain('table content');
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('preserves non-breaking spaces in HTML', () => {
|
|
450
|
+
const tokens = [
|
|
451
|
+
{
|
|
452
|
+
text: 'word space',
|
|
453
|
+
start: 0,
|
|
454
|
+
end: 14,
|
|
455
|
+
predefined: false,
|
|
456
|
+
selectable: false,
|
|
457
|
+
selected: false,
|
|
458
|
+
},
|
|
459
|
+
];
|
|
460
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
461
|
+
// should be converted to non-breaking space character
|
|
462
|
+
expect(container.textContent).toContain('word');
|
|
463
|
+
expect(container.textContent).toContain('space');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('handles mixed selectable and non-selectable tokens', () => {
|
|
467
|
+
const tokens = [
|
|
468
|
+
{
|
|
469
|
+
text: 'prefix ',
|
|
470
|
+
start: 0,
|
|
471
|
+
end: 7,
|
|
472
|
+
predefined: false,
|
|
473
|
+
selectable: false,
|
|
474
|
+
selected: false,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
text: 'selectable token',
|
|
478
|
+
start: 7,
|
|
479
|
+
end: 22,
|
|
480
|
+
predefined: true,
|
|
481
|
+
selectable: true,
|
|
482
|
+
selected: false,
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
text: ' suffix',
|
|
486
|
+
start: 22,
|
|
487
|
+
end: 29,
|
|
488
|
+
predefined: false,
|
|
489
|
+
selectable: false,
|
|
490
|
+
selected: false,
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
494
|
+
expect(container.textContent).toContain('prefix');
|
|
495
|
+
expect(container.textContent).toContain('selectable token');
|
|
496
|
+
expect(container.textContent).toContain('suffix');
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe('newline handling', () => {
|
|
501
|
+
it('renders single newlines as <br> tags', () => {
|
|
502
|
+
const tokens = [
|
|
503
|
+
{
|
|
504
|
+
text: 'line1',
|
|
505
|
+
start: 0,
|
|
506
|
+
end: 5,
|
|
507
|
+
predefined: true,
|
|
508
|
+
selectable: true,
|
|
509
|
+
selected: false,
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
text: '\n',
|
|
513
|
+
start: 5,
|
|
514
|
+
end: 6,
|
|
515
|
+
selected: false,
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
text: 'line2',
|
|
519
|
+
start: 6,
|
|
520
|
+
end: 11,
|
|
521
|
+
predefined: true,
|
|
522
|
+
selectable: true,
|
|
523
|
+
selected: false,
|
|
524
|
+
},
|
|
525
|
+
];
|
|
526
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
527
|
+
const html = container.innerHTML;
|
|
528
|
+
expect(html).toContain('<br>');
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('renders double newlines as paragraph breaks', () => {
|
|
532
|
+
const tokens = [
|
|
533
|
+
{
|
|
534
|
+
text: 'paragraph1',
|
|
535
|
+
start: 0,
|
|
536
|
+
end: 10,
|
|
537
|
+
predefined: true,
|
|
538
|
+
selectable: true,
|
|
539
|
+
selected: false,
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
text: '\n\n',
|
|
543
|
+
start: 10,
|
|
544
|
+
end: 12,
|
|
545
|
+
selected: false,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
text: 'paragraph2',
|
|
549
|
+
start: 12,
|
|
550
|
+
end: 22,
|
|
551
|
+
predefined: true,
|
|
552
|
+
selectable: true,
|
|
553
|
+
selected: false,
|
|
554
|
+
},
|
|
555
|
+
];
|
|
556
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
557
|
+
const html = container.innerHTML;
|
|
558
|
+
expect(html).toContain('</p>');
|
|
559
|
+
expect(html).toContain('<p>');
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
describe('edge cases', () => {
|
|
564
|
+
it('handles empty token list', () => {
|
|
565
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={[]} />);
|
|
566
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('handles tokens with special characters', () => {
|
|
570
|
+
const tokens = [
|
|
571
|
+
{
|
|
572
|
+
text: 'special & < > " chars',
|
|
573
|
+
start: 0,
|
|
574
|
+
end: 21,
|
|
575
|
+
predefined: true,
|
|
576
|
+
selectable: true,
|
|
577
|
+
selected: false,
|
|
578
|
+
},
|
|
579
|
+
];
|
|
580
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
581
|
+
expect(container.textContent).toContain('special');
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it('renders with disabled and highlightChoices together', () => {
|
|
585
|
+
const tokens = [
|
|
586
|
+
{
|
|
587
|
+
text: 'token',
|
|
588
|
+
start: 0,
|
|
589
|
+
end: 5,
|
|
590
|
+
predefined: true,
|
|
591
|
+
selectable: true,
|
|
592
|
+
selected: false,
|
|
593
|
+
},
|
|
594
|
+
];
|
|
595
|
+
const { container } = render(
|
|
596
|
+
<TokenSelect {...defaultProps} tokens={tokens} disabled highlightChoices />,
|
|
597
|
+
);
|
|
598
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('handles very long token text', () => {
|
|
602
|
+
const longText = 'Lorem ipsum '.repeat(100);
|
|
603
|
+
const tokens = [
|
|
604
|
+
{
|
|
605
|
+
text: longText,
|
|
606
|
+
start: 0,
|
|
607
|
+
end: longText.length,
|
|
608
|
+
predefined: true,
|
|
609
|
+
selectable: true,
|
|
610
|
+
selected: false,
|
|
611
|
+
},
|
|
612
|
+
];
|
|
613
|
+
const { container } = render(<TokenSelect {...defaultProps} tokens={tokens} />);
|
|
614
|
+
expect(container.textContent).toContain('Lorem ipsum');
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Note: Tests for internal methods (selectedCount, canSelectMore, toggleToken) are
|
|
619
|
+
// implementation details and cannot be directly tested with RTL. The original tests
|
|
620
|
+
// used wrapper.instance() to test these methods, which tests implementation rather
|
|
621
|
+
// than user-facing behavior. Token selection logic and user interactions should be
|
|
622
|
+
// tested through integration/e2e tests.
|
|
623
|
+
});
|