@pie-lib/text-select 3.0.3-next.38 → 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,256 @@
|
|
|
1
|
+
import { intersection, normalize, paragraphs, sentences, sort, words } from '../builder';
|
|
2
|
+
|
|
3
|
+
const token = (start, end, text) => ({ start, end, text });
|
|
4
|
+
|
|
5
|
+
const selection = (start, end) => ({ start, end });
|
|
6
|
+
|
|
7
|
+
const o = (strings, ...exp) => {
|
|
8
|
+
return strings.reduce((acc, v, index) => {
|
|
9
|
+
const e = exp[index];
|
|
10
|
+
const s = typeof e === 'object' ? JSON.stringify(e) : e;
|
|
11
|
+
return `${acc}${v}${s || ''}`;
|
|
12
|
+
}, '');
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('builder', () => {
|
|
16
|
+
describe('intersection', () => {
|
|
17
|
+
const assert = (selection, tokens, expected) => {
|
|
18
|
+
it(o`${selection}, ${tokens} => ${expected}`, () => {
|
|
19
|
+
const received = intersection(selection, tokens);
|
|
20
|
+
expect(received.results).toEqual(expected);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
assert(selection(2, 3), [token(0, 1)], []);
|
|
25
|
+
assert(selection(1, 2), [token(0, 1)], []);
|
|
26
|
+
assert(selection(0, 1), [token(0, 1)], [{ token: token(0, 1), type: 'exact-fit' }]);
|
|
27
|
+
assert(selection(0, 2), [token(0, 1)], [{ token: token(0, 1), type: 'within-selection' }]);
|
|
28
|
+
assert(selection(0, 2), [token(1, 2)], [{ token: token(1, 2), type: 'within-selection' }]);
|
|
29
|
+
assert(
|
|
30
|
+
selection(0, 10),
|
|
31
|
+
[token(1, 2), token(2, 3)],
|
|
32
|
+
[
|
|
33
|
+
{ token: token(1, 2), type: 'within-selection' },
|
|
34
|
+
{ token: token(2, 3), type: 'within-selection' },
|
|
35
|
+
],
|
|
36
|
+
);
|
|
37
|
+
assert(
|
|
38
|
+
selection(0, 10),
|
|
39
|
+
[token(1, 2), token(2, 11)],
|
|
40
|
+
[
|
|
41
|
+
{ token: token(1, 2), type: 'within-selection' },
|
|
42
|
+
{ token: token(2, 11), type: 'overlap' },
|
|
43
|
+
],
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('sort', () => {
|
|
48
|
+
it('sorts', () => {
|
|
49
|
+
const out = sort([token(1, 2), token(0, 1)]);
|
|
50
|
+
expect(out).toEqual([token(0, 1), token(1, 2)]);
|
|
51
|
+
});
|
|
52
|
+
it('sorts', () => {
|
|
53
|
+
const out = sort([token(0, 1), token(1, 2)]);
|
|
54
|
+
expect(out).toEqual([token(0, 1), token(1, 2)]);
|
|
55
|
+
});
|
|
56
|
+
xit('sorts', () => {
|
|
57
|
+
expect(() => sort([token(0, 2), token(1, 2)])).toThrow(Error);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('normalize', () => {
|
|
61
|
+
const assert = (input, tokens, expected) => {
|
|
62
|
+
it(`${input} + ${JSON.stringify(tokens)} -> ${JSON.stringify(expected)}`, () => {
|
|
63
|
+
const out = normalize(input, tokens);
|
|
64
|
+
expect(out).toEqual(expected);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
assert(
|
|
68
|
+
null,
|
|
69
|
+
[],
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
text: '',
|
|
73
|
+
start: 0,
|
|
74
|
+
end: 0,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
);
|
|
78
|
+
assert(
|
|
79
|
+
'abcde',
|
|
80
|
+
[
|
|
81
|
+
{ text: 'b', start: 1, end: 2 },
|
|
82
|
+
{ text: 'd', start: 3, end: 4 },
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
{ text: 'a', start: 0, end: 1 },
|
|
86
|
+
{ text: 'b', start: 1, end: 2, predefined: true },
|
|
87
|
+
{ text: 'c', start: 2, end: 3 },
|
|
88
|
+
{ text: 'd', start: 3, end: 4, predefined: true },
|
|
89
|
+
{ text: 'e', start: 4, end: 5 },
|
|
90
|
+
],
|
|
91
|
+
);
|
|
92
|
+
assert(
|
|
93
|
+
'abc',
|
|
94
|
+
[{ text: 'c', start: 2, end: 3 }],
|
|
95
|
+
[
|
|
96
|
+
{ text: 'ab', start: 0, end: 2 },
|
|
97
|
+
{ text: 'c', start: 2, end: 3, predefined: true },
|
|
98
|
+
],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
assert(
|
|
102
|
+
'abc',
|
|
103
|
+
[
|
|
104
|
+
{ text: 'c', start: 2, end: 3 },
|
|
105
|
+
{ text: 'b', start: 1, end: 2 },
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
{ text: 'a', start: 0, end: 1 },
|
|
109
|
+
{ text: 'b', start: 1, end: 2, predefined: true },
|
|
110
|
+
{ text: 'c', start: 2, end: 3, predefined: true },
|
|
111
|
+
],
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
assert(
|
|
115
|
+
'abc',
|
|
116
|
+
[{ text: 'a', start: 0, end: 1 }],
|
|
117
|
+
[
|
|
118
|
+
{ text: 'a', start: 0, end: 1, predefined: true },
|
|
119
|
+
{ text: 'bc', start: 1, end: 3 },
|
|
120
|
+
],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
assert(
|
|
124
|
+
'abcd',
|
|
125
|
+
[
|
|
126
|
+
{ text: 'b', start: 1, end: 2 },
|
|
127
|
+
{ text: 'c', start: 2, end: 3 },
|
|
128
|
+
{ text: 'd', start: 3, end: 4 },
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
{ text: 'a', start: 0, end: 1 },
|
|
132
|
+
{ text: 'b', start: 1, end: 2, predefined: true },
|
|
133
|
+
{ text: 'c', start: 2, end: 3, predefined: true },
|
|
134
|
+
{ text: 'd', start: 3, end: 4, predefined: true },
|
|
135
|
+
],
|
|
136
|
+
);
|
|
137
|
+
assert(
|
|
138
|
+
'abcde',
|
|
139
|
+
[
|
|
140
|
+
{ text: 'b', start: 1, end: 2 },
|
|
141
|
+
{ text: 'c', start: 2, end: 3 },
|
|
142
|
+
{ text: 'd', start: 3, end: 4 },
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
{ text: 'a', start: 0, end: 1 },
|
|
146
|
+
{ text: 'b', start: 1, end: 2, predefined: true },
|
|
147
|
+
{ text: 'c', start: 2, end: 3, predefined: true },
|
|
148
|
+
{ text: 'd', start: 3, end: 4, predefined: true },
|
|
149
|
+
{ text: 'e', start: 4, end: 5 },
|
|
150
|
+
],
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// same token defined multiple times
|
|
154
|
+
assert(
|
|
155
|
+
'abcde',
|
|
156
|
+
[
|
|
157
|
+
{ text: 'c', start: 2, end: 3 },
|
|
158
|
+
{ text: 'b', start: 1, end: 2 },
|
|
159
|
+
{ text: 'c', start: 2, end: 3 },
|
|
160
|
+
{ text: 'd', start: 3, end: 4 },
|
|
161
|
+
{ text: 'c', start: 2, end: 3 },
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
{ text: 'a', start: 0, end: 1 },
|
|
165
|
+
{ text: 'b', start: 1, end: 2, predefined: true },
|
|
166
|
+
{ text: 'c', start: 2, end: 3, predefined: true },
|
|
167
|
+
{ text: 'd', start: 3, end: 4, predefined: true },
|
|
168
|
+
{ text: 'e', start: 4, end: 5 },
|
|
169
|
+
],
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('words', () => {
|
|
174
|
+
it('works', () => {
|
|
175
|
+
const out = words('foo. bar');
|
|
176
|
+
expect(out).toEqual([
|
|
177
|
+
{
|
|
178
|
+
text: 'foo.',
|
|
179
|
+
start: 0,
|
|
180
|
+
end: 4,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
text: 'bar',
|
|
184
|
+
start: 5,
|
|
185
|
+
end: 8,
|
|
186
|
+
},
|
|
187
|
+
]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('sentences', () => {
|
|
192
|
+
it('foobar', () => {
|
|
193
|
+
const text = 'This is foo. This is bar.';
|
|
194
|
+
const out = sentences(text);
|
|
195
|
+
expect(out[0]).toEqual({ text: 'This is foo.', start: 0, end: 12 });
|
|
196
|
+
expect(out[1]).toEqual({ text: 'This is bar.', start: 13, end: 25 });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('works', () => {
|
|
200
|
+
const text =
|
|
201
|
+
'On Jan. 20, former Sen. Barack Obama became the 44th President of the USA. Millions attended the Inauguration.';
|
|
202
|
+
|
|
203
|
+
const out = sentences(text);
|
|
204
|
+
expect(out.length).toEqual(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('works for sentences separated by \n', () => {
|
|
208
|
+
const text = 'This is foo\nThis is bar';
|
|
209
|
+
const out = sentences(text);
|
|
210
|
+
expect(out[0]).toEqual({ text: 'This is foo', start: 0, end: 11 });
|
|
211
|
+
expect(out[1]).toEqual({ text: 'This is bar', start: 12, end: 23 });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('works for sentences ending in one-character-words', () => {
|
|
215
|
+
const text =
|
|
216
|
+
"This is Sentence 1. This is Sentence 2. This is Sentence 3. This is Sentence 4. Dr. A. said he'll call in 5.";
|
|
217
|
+
|
|
218
|
+
const out = sentences(text);
|
|
219
|
+
|
|
220
|
+
expect(out.length).toEqual(5);
|
|
221
|
+
|
|
222
|
+
expect(out).toEqual([
|
|
223
|
+
{ text: 'This is Sentence 1.', start: 0, end: 19 },
|
|
224
|
+
{ text: 'This is Sentence 2.', start: 20, end: 39 },
|
|
225
|
+
{ text: 'This is Sentence 3.', start: 40, end: 59 },
|
|
226
|
+
{ text: 'This is Sentence 4.', start: 60, end: 79 },
|
|
227
|
+
{ text: "Dr. A. said he'll call in 5.", start: 80, end: 108 },
|
|
228
|
+
]);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('paragraphs', () => {
|
|
233
|
+
it('foobar', () => {
|
|
234
|
+
const text = 'This is foo. This is bar.\nThis is foobar. This is barfoo.';
|
|
235
|
+
const out = paragraphs(text);
|
|
236
|
+
expect(out[0]).toEqual({
|
|
237
|
+
text: 'This is foo. This is bar.',
|
|
238
|
+
start: 0,
|
|
239
|
+
end: 25,
|
|
240
|
+
});
|
|
241
|
+
expect(out[1]).toEqual({
|
|
242
|
+
text: 'This is foobar. This is barfoo.',
|
|
243
|
+
start: 26,
|
|
244
|
+
end: 57,
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
it('works', () => {
|
|
248
|
+
const text =
|
|
249
|
+
'On Jan. 20, former Sen. Barack Obama became the 44th President of the USA. Millions attended the Inauguration.' +
|
|
250
|
+
'\\ndadadadadadadadada.';
|
|
251
|
+
|
|
252
|
+
const out = paragraphs(text);
|
|
253
|
+
expect(out.length).toEqual(1);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Controls } from '../controls';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
|
|
5
|
+
describe('controls', () => {
|
|
6
|
+
const defaultProps = {
|
|
7
|
+
classes: { button: 'button' },
|
|
8
|
+
onClear: jest.fn(),
|
|
9
|
+
onWords: jest.fn(),
|
|
10
|
+
onSentences: jest.fn(),
|
|
11
|
+
onParagraphs: jest.fn(),
|
|
12
|
+
setCorrectMode: false,
|
|
13
|
+
onToggleCorrectMode: jest.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('rendering', () => {
|
|
17
|
+
it('renders with all controls', () => {
|
|
18
|
+
const { container } = render(<Controls {...defaultProps} />);
|
|
19
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders in setCorrectMode', () => {
|
|
23
|
+
const { container } = render(<Controls {...defaultProps} setCorrectMode={true} />);
|
|
24
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import { Tokenizer } from '../index';
|
|
4
|
+
|
|
5
|
+
const tokens = () => [
|
|
6
|
+
{
|
|
7
|
+
start: 0,
|
|
8
|
+
end: 1,
|
|
9
|
+
text: 'f',
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('tokenizer', () => {
|
|
14
|
+
const defaultProps = {
|
|
15
|
+
text: 'foo',
|
|
16
|
+
classes: {},
|
|
17
|
+
onChange: jest.fn(),
|
|
18
|
+
tokens: tokens(),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.clearAllMocks();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('rendering', () => {
|
|
26
|
+
it('renders with default props', () => {
|
|
27
|
+
const { container } = render(<Tokenizer {...defaultProps} />);
|
|
28
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders with empty tokens', () => {
|
|
32
|
+
const { container } = render(<Tokenizer {...defaultProps} tokens={[]} />);
|
|
33
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders with multiple tokens', () => {
|
|
37
|
+
const multipleTokens = [
|
|
38
|
+
{ start: 0, end: 3, text: 'foo' },
|
|
39
|
+
{ start: 4, end: 7, text: 'bar' },
|
|
40
|
+
];
|
|
41
|
+
const { container } = render(<Tokenizer {...defaultProps} tokens={multipleTokens} />);
|
|
42
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders Controls component', () => {
|
|
46
|
+
const { getByText } = render(<Tokenizer {...defaultProps} />);
|
|
47
|
+
expect(getByText(/clear/i)).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders TokenText with text content', () => {
|
|
51
|
+
const { container } = render(<Tokenizer {...defaultProps} text="Hello world" />);
|
|
52
|
+
expect(container.textContent).toContain('Hello world');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('control interactions', () => {
|
|
57
|
+
it('calls onChange when clear button is clicked', () => {
|
|
58
|
+
const onChange = jest.fn();
|
|
59
|
+
const { getByText } = render(<Tokenizer {...defaultProps} onChange={onChange} />);
|
|
60
|
+
|
|
61
|
+
const clearButton = getByText(/clear/i);
|
|
62
|
+
fireEvent.click(clearButton);
|
|
63
|
+
|
|
64
|
+
expect(onChange).toHaveBeenCalledWith([], '');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('calls onChange when words button is clicked', () => {
|
|
68
|
+
const onChange = jest.fn();
|
|
69
|
+
const { getByText } = render(<Tokenizer {...defaultProps} text="hello world" onChange={onChange} />);
|
|
70
|
+
|
|
71
|
+
const wordsButton = getByText(/words/i);
|
|
72
|
+
fireEvent.click(wordsButton);
|
|
73
|
+
|
|
74
|
+
expect(onChange).toHaveBeenCalled();
|
|
75
|
+
expect(onChange.mock.calls[0][1]).toBe('words');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('calls onChange when sentences button is clicked', () => {
|
|
79
|
+
const onChange = jest.fn();
|
|
80
|
+
const { getByText } = render(<Tokenizer {...defaultProps} text="Hello. World." onChange={onChange} />);
|
|
81
|
+
|
|
82
|
+
const sentencesButton = getByText(/sentences/i);
|
|
83
|
+
fireEvent.click(sentencesButton);
|
|
84
|
+
|
|
85
|
+
expect(onChange).toHaveBeenCalled();
|
|
86
|
+
expect(onChange.mock.calls[0][1]).toBe('sentence');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('calls onChange when paragraphs button is clicked', () => {
|
|
90
|
+
const onChange = jest.fn();
|
|
91
|
+
const { getByText } = render(<Tokenizer {...defaultProps} text="Para 1\n\nPara 2" onChange={onChange} />);
|
|
92
|
+
|
|
93
|
+
const paragraphsButton = getByText(/paragraphs/i);
|
|
94
|
+
fireEvent.click(paragraphsButton);
|
|
95
|
+
|
|
96
|
+
expect(onChange).toHaveBeenCalled();
|
|
97
|
+
expect(onChange.mock.calls[0][1]).toBe('paragraphs');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('correct mode toggle', () => {
|
|
102
|
+
it('toggles correct mode when toggle button is clicked', () => {
|
|
103
|
+
const { container } = render(<Tokenizer {...defaultProps} />);
|
|
104
|
+
const toggleButton = container.querySelector('[type="checkbox"]');
|
|
105
|
+
|
|
106
|
+
expect(toggleButton).not.toBeChecked();
|
|
107
|
+
|
|
108
|
+
fireEvent.click(toggleButton);
|
|
109
|
+
expect(toggleButton).toBeChecked();
|
|
110
|
+
|
|
111
|
+
fireEvent.click(toggleButton);
|
|
112
|
+
expect(toggleButton).not.toBeChecked();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('token handling', () => {
|
|
117
|
+
it('renders with tokens containing correct property', () => {
|
|
118
|
+
const tokensWithCorrect = [
|
|
119
|
+
{ start: 0, end: 3, text: 'foo', correct: true },
|
|
120
|
+
{ start: 4, end: 7, text: 'bar', correct: false },
|
|
121
|
+
];
|
|
122
|
+
const { container } = render(<Tokenizer {...defaultProps} tokens={tokensWithCorrect} />);
|
|
123
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('handles tokens with overlapping positions', () => {
|
|
127
|
+
const overlappingTokens = [
|
|
128
|
+
{ start: 0, end: 5, text: 'hello' },
|
|
129
|
+
{ start: 3, end: 8, text: 'low' },
|
|
130
|
+
];
|
|
131
|
+
const { container } = render(<Tokenizer {...defaultProps} tokens={overlappingTokens} />);
|
|
132
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('text variations', () => {
|
|
137
|
+
it('handles multiline text', () => {
|
|
138
|
+
const multilineText = 'Line 1\nLine 2\nLine 3';
|
|
139
|
+
const { container } = render(<Tokenizer {...defaultProps} text={multilineText} />);
|
|
140
|
+
expect(container.textContent).toContain('Line 1');
|
|
141
|
+
expect(container.textContent).toContain('Line 2');
|
|
142
|
+
expect(container.textContent).toContain('Line 3');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('handles empty text', () => {
|
|
146
|
+
const { container } = render(<Tokenizer {...defaultProps} text="" tokens={[]} />);
|
|
147
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles text with special characters', () => {
|
|
151
|
+
const specialText = 'Hello! @#$ %^& *() world?';
|
|
152
|
+
const { container } = render(<Tokenizer {...defaultProps} text={specialText} />);
|
|
153
|
+
expect(container.textContent).toContain(specialText);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('token click interactions', () => {
|
|
158
|
+
it('removes token when clicked in normal mode', () => {
|
|
159
|
+
const onChange = jest.fn();
|
|
160
|
+
const tokensWithPredefined = [{ start: 0, end: 5, text: 'hello', predefined: true }];
|
|
161
|
+
const { container } = render(
|
|
162
|
+
<Tokenizer {...defaultProps} text="hello world" tokens={tokensWithPredefined} onChange={onChange} />,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const predefinedToken = container.querySelector('.predefined');
|
|
166
|
+
if (predefinedToken) {
|
|
167
|
+
fireEvent.click(predefinedToken);
|
|
168
|
+
expect(onChange).toHaveBeenCalled();
|
|
169
|
+
expect(onChange.mock.calls[0][0]).toEqual([]);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('toggles correct property when token clicked in correct mode', () => {
|
|
174
|
+
const onChange = jest.fn();
|
|
175
|
+
const tokensWithPredefined = [{ start: 0, end: 5, text: 'hello', predefined: true, correct: false }];
|
|
176
|
+
const { container } = render(
|
|
177
|
+
<Tokenizer {...defaultProps} text="hello world" tokens={tokensWithPredefined} onChange={onChange} />,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const toggleButton = container.querySelector('[type="checkbox"]');
|
|
181
|
+
fireEvent.click(toggleButton);
|
|
182
|
+
onChange.mockClear();
|
|
183
|
+
|
|
184
|
+
const predefinedToken = container.querySelector('.predefined');
|
|
185
|
+
if (predefinedToken) {
|
|
186
|
+
fireEvent.click(predefinedToken);
|
|
187
|
+
expect(onChange).toHaveBeenCalled();
|
|
188
|
+
const updatedToken = onChange.mock.calls[0][0][0];
|
|
189
|
+
expect(updatedToken.correct).toBe(true);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('toggles correct from true to false when clicked in correct mode', () => {
|
|
194
|
+
const onChange = jest.fn();
|
|
195
|
+
const tokensWithPredefined = [{ start: 0, end: 5, text: 'hello', predefined: true, correct: true }];
|
|
196
|
+
const { container } = render(
|
|
197
|
+
<Tokenizer {...defaultProps} text="hello world" tokens={tokensWithPredefined} onChange={onChange} />,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const toggleButton = container.querySelector('[type="checkbox"]');
|
|
201
|
+
fireEvent.click(toggleButton);
|
|
202
|
+
onChange.mockClear();
|
|
203
|
+
|
|
204
|
+
const predefinedToken = container.querySelector('.predefined');
|
|
205
|
+
if (predefinedToken) {
|
|
206
|
+
fireEvent.click(predefinedToken);
|
|
207
|
+
expect(onChange).toHaveBeenCalled();
|
|
208
|
+
const updatedToken = onChange.mock.calls[0][0][0];
|
|
209
|
+
expect(updatedToken.correct).toBe(false);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('does not modify tokens when clicking non-existent token', () => {
|
|
214
|
+
const onChange = jest.fn();
|
|
215
|
+
const { container } = render(<Tokenizer {...defaultProps} text="hello world" tokens={[]} onChange={onChange} />);
|
|
216
|
+
|
|
217
|
+
fireEvent.click(container.querySelector('div > div'));
|
|
218
|
+
|
|
219
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('token removal', () => {
|
|
224
|
+
it('removes multiple different tokens', () => {
|
|
225
|
+
const onChange = jest.fn();
|
|
226
|
+
const multipleTokens = [
|
|
227
|
+
{ start: 0, end: 5, text: 'hello', predefined: true },
|
|
228
|
+
{ start: 6, end: 11, text: 'world', predefined: true },
|
|
229
|
+
];
|
|
230
|
+
const { container } = render(
|
|
231
|
+
<Tokenizer {...defaultProps} text="hello world" tokens={multipleTokens} onChange={onChange} />,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const predefinedTokens = container.querySelectorAll('.predefined');
|
|
235
|
+
expect(predefinedTokens.length).toBeGreaterThan(0);
|
|
236
|
+
|
|
237
|
+
fireEvent.click(predefinedTokens[0]);
|
|
238
|
+
expect(onChange).toHaveBeenCalled();
|
|
239
|
+
expect(onChange.mock.calls[0][0].length).toBe(1); // One token left
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('handles removing token by matching text, start, and end', () => {
|
|
243
|
+
const onChange = jest.fn();
|
|
244
|
+
const tokensWithSameText = [
|
|
245
|
+
{ start: 0, end: 5, text: 'hello', predefined: true },
|
|
246
|
+
{ start: 12, end: 17, text: 'hello', predefined: true },
|
|
247
|
+
];
|
|
248
|
+
const { container } = render(
|
|
249
|
+
<Tokenizer {...defaultProps} text="hello world hello" tokens={tokensWithSameText} onChange={onChange} />,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
const predefinedTokens = container.querySelectorAll('.predefined');
|
|
253
|
+
if (predefinedTokens.length > 0) {
|
|
254
|
+
fireEvent.click(predefinedTokens[0]);
|
|
255
|
+
expect(onChange).toHaveBeenCalled();
|
|
256
|
+
expect(onChange.mock.calls[0][0].length).toBe(1);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('text selection and token creation', () => {
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
if (typeof global.window !== 'undefined') {
|
|
264
|
+
global.window.getSelection = jest.fn().mockReturnValue({
|
|
265
|
+
toString: () => '',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('creates new token when text is selected', () => {
|
|
271
|
+
const onChange = jest.fn();
|
|
272
|
+
const { container } = render(<Tokenizer {...defaultProps} text="hello world" tokens={[]} onChange={onChange} />);
|
|
273
|
+
|
|
274
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('selectToken removes overlapping tokens when creating new token', () => {
|
|
278
|
+
const onChange = jest.fn();
|
|
279
|
+
const existingTokens = [{ start: 0, end: 5, text: 'hello' }];
|
|
280
|
+
const { container } = render(
|
|
281
|
+
<Tokenizer {...defaultProps} text="hello world" tokens={existingTokens} onChange={onChange} />,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('mode state management', () => {
|
|
289
|
+
it('maintains mode state after buildTokens', () => {
|
|
290
|
+
const onChange = jest.fn();
|
|
291
|
+
const { getByText } = render(<Tokenizer {...defaultProps} text="hello world test" onChange={onChange} />);
|
|
292
|
+
|
|
293
|
+
const wordsButton = getByText(/words/i);
|
|
294
|
+
fireEvent.click(wordsButton);
|
|
295
|
+
|
|
296
|
+
expect(onChange).toHaveBeenCalled();
|
|
297
|
+
expect(onChange.mock.calls[0][1]).toBe('words');
|
|
298
|
+
|
|
299
|
+
onChange.mockClear();
|
|
300
|
+
fireEvent.click(wordsButton);
|
|
301
|
+
expect(onChange).toHaveBeenCalled();
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('maintains mode state after clear', () => {
|
|
305
|
+
const onChange = jest.fn();
|
|
306
|
+
const { getByText } = render(<Tokenizer {...defaultProps} text="hello world" onChange={onChange} />);
|
|
307
|
+
|
|
308
|
+
fireEvent.click(getByText(/words/i));
|
|
309
|
+
onChange.mockClear();
|
|
310
|
+
|
|
311
|
+
fireEvent.click(getByText(/clear/i));
|
|
312
|
+
expect(onChange).toHaveBeenCalledWith([], '');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('disabled state styling', () => {
|
|
317
|
+
it('applies disabled styling when in correct mode', () => {
|
|
318
|
+
const { container } = render(<Tokenizer {...defaultProps} />);
|
|
319
|
+
|
|
320
|
+
const styledDiv = container.querySelector('div > div');
|
|
321
|
+
expect(styledDiv).toBeInTheDocument();
|
|
322
|
+
|
|
323
|
+
const toggleButton = container.querySelector('[type="checkbox"]');
|
|
324
|
+
fireEvent.click(toggleButton);
|
|
325
|
+
|
|
326
|
+
expect(styledDiv).toBeInTheDocument();
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|