@pie-lib/text-select 2.1.1-next.0 → 2.2.0-next.0

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.
Files changed (38) hide show
  1. package/lib/index.js +0 -7
  2. package/lib/index.js.map +1 -1
  3. package/lib/legend.js +0 -12
  4. package/lib/legend.js.map +1 -1
  5. package/lib/text-select.js +0 -14
  6. package/lib/text-select.js.map +1 -1
  7. package/lib/token-select/index.js +3 -24
  8. package/lib/token-select/index.js.map +1 -1
  9. package/lib/token-select/token.js +0 -17
  10. package/lib/token-select/token.js.map +1 -1
  11. package/lib/tokenizer/builder.js +3 -14
  12. package/lib/tokenizer/builder.js.map +1 -1
  13. package/lib/tokenizer/controls.js +0 -2
  14. package/lib/tokenizer/controls.js.map +1 -1
  15. package/lib/tokenizer/index.js +4 -19
  16. package/lib/tokenizer/index.js.map +1 -1
  17. package/lib/tokenizer/selection-utils.js +0 -4
  18. package/lib/tokenizer/selection-utils.js.map +1 -1
  19. package/lib/tokenizer/token-text.js +0 -11
  20. package/lib/tokenizer/token-text.js.map +1 -1
  21. package/lib/utils.js +0 -9
  22. package/lib/utils.js.map +1 -1
  23. package/package.json +11 -8
  24. package/src/__tests__/legend.test.jsx +211 -0
  25. package/src/token-select/__tests__/index.test.jsx +264 -2
  26. package/src/token-select/__tests__/token.test.jsx +207 -1
  27. package/src/token-select/index.jsx +1 -2
  28. package/src/token-select/token.jsx +1 -5
  29. package/src/tokenizer/__tests__/builder.test.js +1 -1
  30. package/src/tokenizer/__tests__/controls.test.jsx +1 -1
  31. package/src/tokenizer/__tests__/index.test.jsx +289 -7
  32. package/src/tokenizer/__tests__/selection-utils.test.js +122 -3
  33. package/src/tokenizer/__tests__/token-text.test.jsx +285 -9
  34. package/src/tokenizer/builder.js +2 -3
  35. package/src/tokenizer/controls.jsx +3 -18
  36. package/src/tokenizer/index.jsx +2 -4
  37. package/src/tokenizer/token-text.jsx +2 -2
  38. package/NEXT.CHANGELOG.json +0 -1
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
- import { render } from '@testing-library/react';
3
- import TokenText from '../token-text';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import TokenText, { Text } from '../token-text';
4
4
 
5
5
  const tokens = () => [
6
6
  {
7
7
  start: 0,
8
- end: 7,
9
- text: `lorem\nfoo bar`,
8
+ end: 5,
9
+ text: 'lorem',
10
10
  },
11
11
  ];
12
12
 
@@ -14,10 +14,19 @@ describe('token-text', () => {
14
14
  const defaultProps = {
15
15
  onTokenClick: jest.fn(),
16
16
  onSelectToken: jest.fn(),
17
- text: `lorem\nfoo bar`,
17
+ text: `lorem ipsum dolor`,
18
18
  tokens: tokens(),
19
19
  };
20
20
 
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ if (typeof global.window !== 'undefined') {
24
+ global.window.getSelection = jest.fn().mockReturnValue({
25
+ toString: () => '',
26
+ });
27
+ }
28
+ });
29
+
21
30
  describe('rendering', () => {
22
31
  it('renders with text and tokens', () => {
23
32
  const { container } = render(<TokenText {...defaultProps} />);
@@ -33,10 +42,277 @@ describe('token-text', () => {
33
42
  const { container } = render(<TokenText {...defaultProps} text="Line 1\nLine 2\nLine 3" />);
34
43
  expect(container.firstChild).toBeInTheDocument();
35
44
  });
45
+
46
+ it('renders with custom className', () => {
47
+ const { container } = render(<TokenText {...defaultProps} className="custom-class" />);
48
+ expect(container.firstChild).toHaveClass('custom-class');
49
+ });
50
+
51
+ it('renders normalized tokens', () => {
52
+ const { container } = render(<TokenText {...defaultProps} />);
53
+ const spans = container.querySelectorAll('span');
54
+ expect(spans.length).toBeGreaterThan(0);
55
+ });
56
+ });
57
+
58
+ describe('Text component', () => {
59
+ it('renders plain text when not predefined', () => {
60
+ const { container } = render(<Text text="hello" predefined={false} />);
61
+ expect(container.querySelector('span')).toBeInTheDocument();
62
+ expect(container.querySelector('span')).not.toHaveClass('predefined');
63
+ });
64
+
65
+ it('renders predefined text with correct class', () => {
66
+ const { container } = render(<Text text="hello" predefined={true} onClick={jest.fn()} />);
67
+ expect(container.querySelector('span')).toHaveClass('predefined');
68
+ });
69
+
70
+ it('renders correct text with both classes', () => {
71
+ const { container } = render(<Text text="hello" predefined={true} correct={true} onClick={jest.fn()} />);
72
+ const span = container.querySelector('span');
73
+ expect(span).toHaveClass('predefined');
74
+ expect(span).toHaveClass('correct');
75
+ });
76
+
77
+ it('calls onClick when predefined text is clicked', () => {
78
+ const onClick = jest.fn();
79
+ const { container } = render(<Text text="hello" predefined={true} onClick={onClick} />);
80
+ const span = container.querySelector('span');
81
+ fireEvent.click(span);
82
+ expect(onClick).toHaveBeenCalled();
83
+ });
84
+ });
85
+
86
+ describe('token interaction', () => {
87
+ it('calls onTokenClick when clicking on a predefined token', () => {
88
+ const onTokenClick = jest.fn();
89
+ const { container } = render(<TokenText {...defaultProps} onTokenClick={onTokenClick} />);
90
+ const predefinedSpan = container.querySelector('.predefined');
91
+ if (predefinedSpan) {
92
+ fireEvent.click(predefinedSpan);
93
+ expect(onTokenClick).toHaveBeenCalled();
94
+ }
95
+ });
96
+
97
+ it('handles click without window object (SSR)', () => {
98
+ const originalWindow = global.window;
99
+ global.window = undefined;
100
+
101
+ const { container } = render(<TokenText {...defaultProps} />);
102
+ expect(() => fireEvent.click(container.firstChild)).not.toThrow();
103
+
104
+ global.window = originalWindow;
105
+ });
36
106
  });
37
107
 
38
- // Note: Tests for internal methods (onClick, mouseup event handling) are implementation
39
- // details and cannot be directly tested with RTL. The original tests used wrapper.instance()
40
- // to test onClick method logic, which tests implementation rather than user-facing behavior.
41
- // User interactions with text selection should be tested through integration/e2e tests.
108
+ describe('text selection', () => {
109
+ beforeEach(() => {
110
+ global.window.getSelection = jest.fn();
111
+ });
112
+
113
+ it('does not call onSelectToken when no text is selected', () => {
114
+ const onSelectToken = jest.fn();
115
+ global.window.getSelection.mockReturnValue({
116
+ toString: () => '',
117
+ });
118
+
119
+ const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
120
+ fireEvent.click(container.firstChild);
121
+
122
+ expect(onSelectToken).not.toHaveBeenCalled();
123
+ });
124
+
125
+ it('does not call onSelectToken for newline character selection', () => {
126
+ const onSelectToken = jest.fn();
127
+ global.window.getSelection.mockReturnValue({
128
+ toString: () => '\n',
129
+ });
130
+
131
+ const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
132
+ fireEvent.click(container.firstChild);
133
+
134
+ expect(onSelectToken).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('does not call onSelectToken for space character selection', () => {
138
+ const onSelectToken = jest.fn();
139
+ global.window.getSelection.mockReturnValue({
140
+ toString: () => ' ',
141
+ });
142
+
143
+ const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
144
+ fireEvent.click(container.firstChild);
145
+
146
+ expect(onSelectToken).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it('does not call onSelectToken for tab character selection', () => {
150
+ const onSelectToken = jest.fn();
151
+ global.window.getSelection.mockReturnValue({
152
+ toString: () => '\t',
153
+ });
154
+
155
+ const { container } = render(<TokenText {...defaultProps} onSelectToken={onSelectToken} />);
156
+ fireEvent.click(container.firstChild);
157
+
158
+ expect(onSelectToken).not.toHaveBeenCalled();
159
+ });
160
+ });
161
+
162
+ describe('props handling', () => {
163
+ it('accepts and uses all required props', () => {
164
+ const props = {
165
+ text: 'test text',
166
+ tokens: [],
167
+ onTokenClick: jest.fn(),
168
+ onSelectToken: jest.fn(),
169
+ };
170
+
171
+ const { container } = render(<TokenText {...props} />);
172
+ expect(container.firstChild).toBeInTheDocument();
173
+ });
174
+
175
+ it('handles multiple tokens', () => {
176
+ const multipleTokens = [
177
+ { start: 0, end: 5, text: 'lorem' },
178
+ { start: 6, end: 11, text: 'ipsum' },
179
+ ];
180
+
181
+ const { container } = render(<TokenText {...defaultProps} tokens={multipleTokens} />);
182
+ expect(container.firstChild).toBeInTheDocument();
183
+ });
184
+ });
185
+
186
+ describe('text selection with valid text', () => {
187
+ let mockGetCaretCharacterOffsetWithin;
188
+
189
+ beforeEach(() => {
190
+ mockGetCaretCharacterOffsetWithin = jest.fn().mockReturnValue(6);
191
+ jest.mock('../selection-utils', () => ({
192
+ clearSelection: jest.fn(),
193
+ getCaretCharacterOffsetWithin: mockGetCaretCharacterOffsetWithin,
194
+ }));
195
+ });
196
+
197
+ it('calls onSelectToken when valid text is selected without overlap', () => {
198
+ const onSelectToken = jest.fn();
199
+ const clearSelectionMock = jest.fn();
200
+
201
+ const selectionUtils = require('../selection-utils');
202
+ selectionUtils.clearSelection = clearSelectionMock;
203
+ selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(6);
204
+
205
+ global.window.getSelection.mockReturnValue({
206
+ toString: () => 'ipsum',
207
+ });
208
+
209
+ const { container } = render(
210
+ <TokenText {...defaultProps} text="lorem ipsum dolor" tokens={[]} onSelectToken={onSelectToken} />,
211
+ );
212
+
213
+ fireEvent.click(container.firstChild);
214
+
215
+ expect(onSelectToken).toHaveBeenCalledWith(
216
+ expect.objectContaining({
217
+ text: 'ipsum',
218
+ start: expect.any(Number),
219
+ end: expect.any(Number),
220
+ }),
221
+ expect.any(Array),
222
+ );
223
+ });
224
+
225
+ it('handles text selection at the end of text', () => {
226
+ const onSelectToken = jest.fn();
227
+ const selectionUtils = require('../selection-utils');
228
+ selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(12);
229
+
230
+ global.window.getSelection.mockReturnValue({
231
+ toString: () => 'dolor',
232
+ });
233
+
234
+ const { container } = render(
235
+ <TokenText {...defaultProps} text="lorem ipsum dolor" tokens={[]} onSelectToken={onSelectToken} />,
236
+ );
237
+
238
+ fireEvent.click(container.firstChild);
239
+
240
+ expect(onSelectToken).toHaveBeenCalled();
241
+ });
242
+
243
+ it('handles newline offset calculation', () => {
244
+ const onSelectToken = jest.fn();
245
+ const selectionUtils = require('../selection-utils');
246
+ selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(0);
247
+
248
+ global.window.getSelection.mockReturnValue({
249
+ toString: () => 'ipsum',
250
+ });
251
+
252
+ const { container } = render(
253
+ <TokenText {...defaultProps} text="lorem\nipsum dolor" tokens={[]} onSelectToken={onSelectToken} />,
254
+ );
255
+
256
+ fireEvent.click(container.firstChild);
257
+
258
+ if (onSelectToken.mock.calls.length > 0) {
259
+ const token = onSelectToken.mock.calls[0][0];
260
+ expect(token.text).toBe('ipsum');
261
+ }
262
+ });
263
+
264
+ it('calls onSelectToken with tokensToRemove for surrounded tokens', () => {
265
+ const onSelectToken = jest.fn();
266
+ const selectionUtils = require('../selection-utils');
267
+ selectionUtils.getCaretCharacterOffsetWithin = jest.fn().mockReturnValue(0);
268
+
269
+ global.window.getSelection.mockReturnValue({
270
+ toString: () => 'lorem ipsum',
271
+ });
272
+
273
+ const existingTokens = [{ start: 6, end: 11, text: 'ipsum' }];
274
+
275
+ const { container } = render(
276
+ <TokenText {...defaultProps} text="lorem ipsum dolor" tokens={existingTokens} onSelectToken={onSelectToken} />,
277
+ );
278
+
279
+ fireEvent.click(container.firstChild);
280
+
281
+ expect(onSelectToken).toHaveBeenCalled();
282
+ });
283
+
284
+ it('does not call onSelectToken when root is not available', () => {
285
+ const onSelectToken = jest.fn();
286
+
287
+ global.window.getSelection.mockReturnValue({
288
+ toString: () => 'ipsum',
289
+ });
290
+
291
+ const { container } = render(
292
+ <TokenText {...defaultProps} text="lorem ipsum" tokens={[]} onSelectToken={onSelectToken} />,
293
+ );
294
+
295
+ const instance = container.querySelector('div');
296
+ if (instance) {
297
+ fireEvent.click(instance);
298
+ }
299
+ });
300
+ });
301
+
302
+ describe('Text component edge cases', () => {
303
+ it('handles null text gracefully', () => {
304
+ const { container } = render(<Text text={null} predefined={false} />);
305
+ expect(container.querySelector('span')).toBeInTheDocument();
306
+ });
307
+
308
+ it('handles undefined text gracefully', () => {
309
+ const { container } = render(<Text text={undefined} predefined={false} />);
310
+ expect(container.querySelector('span')).toBeInTheDocument();
311
+ });
312
+
313
+ it('renders empty string text', () => {
314
+ const { container } = render(<Text text="" predefined={false} />);
315
+ expect(container.querySelector('span')).toBeInTheDocument();
316
+ });
317
+ });
42
318
  });
@@ -1,6 +1,5 @@
1
- import compact from 'lodash/compact';
1
+ import { clone, compact } from 'lodash-es';
2
2
  import English from '@pie-framework/parse-english';
3
- import clone from 'lodash/clone';
4
3
 
5
4
  const g = (str, node) => {
6
5
  if (node.children) {
@@ -41,7 +40,7 @@ export const paragraphs = (text) => {
41
40
  export const handleSentence = (child, acc) => {
42
41
  const sentenceChilds = [];
43
42
  // we parse the children of the sentence
44
- let newAcc = child.children.reduce(function(acc, child) {
43
+ let newAcc = child.children.reduce(function (acc, child) {
45
44
  // if we find a whitespace node that's \n, we end the sentence
46
45
  if (child.type === 'WhiteSpaceNode' && child.value === '\n') {
47
46
  if (sentenceChilds.length) {
@@ -50,20 +50,10 @@ export class Controls extends React.Component {
50
50
  <StyledButton onClick={onWords} size="small" color="primary" disabled={setCorrectMode}>
51
51
  Words
52
52
  </StyledButton>
53
- <StyledButton
54
- onClick={onSentences}
55
- size="small"
56
- color="primary"
57
- disabled={setCorrectMode}
58
- >
53
+ <StyledButton onClick={onSentences} size="small" color="primary" disabled={setCorrectMode}>
59
54
  Sentences
60
55
  </StyledButton>
61
- <StyledButton
62
- onClick={onParagraphs}
63
- size="small"
64
- color="primary"
65
- disabled={setCorrectMode}
66
- >
56
+ <StyledButton onClick={onParagraphs} size="small" color="primary" disabled={setCorrectMode}>
67
57
  Paragraphs
68
58
  </StyledButton>
69
59
  <StyledButton size="small" color="secondary" onClick={onClear} disabled={setCorrectMode}>
@@ -71,12 +61,7 @@ export class Controls extends React.Component {
71
61
  </StyledButton>
72
62
  </div>
73
63
  <FormControlLabel
74
- control={
75
- <StyledSwitch
76
- checked={setCorrectMode}
77
- onChange={onToggleCorrectMode}
78
- />
79
- }
64
+ control={<StyledSwitch checked={setCorrectMode} onChange={onToggleCorrectMode} />}
80
65
  label="Set correct answers"
81
66
  />
82
67
  </StyledControls>
@@ -2,10 +2,8 @@ import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import Controls from './controls';
4
4
  import { styled } from '@mui/material/styles';
5
- import { words, sentences, paragraphs } from './builder';
6
- import clone from 'lodash/clone';
7
- import isEqual from 'lodash/isEqual';
8
- import differenceWith from 'lodash/differenceWith';
5
+ import { paragraphs, sentences, words } from './builder';
6
+ import { clone, differenceWith, isEqual } from 'lodash-es';
9
7
  import { noSelect } from '@pie-lib/style-utils';
10
8
  import TokenText from './token-text';
11
9
 
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { styled } from '@mui/material/styles';
4
- import { normalize, intersection } from './builder';
4
+ import { intersection, normalize } from './builder';
5
5
  import debug from 'debug';
6
6
  import classNames from 'classnames';
7
7
 
8
8
  import { clearSelection, getCaretCharacterOffsetWithin } from './selection-utils';
9
9
 
10
- import { yellow, green } from '@mui/material/colors';
10
+ import { green, yellow } from '@mui/material/colors';
11
11
 
12
12
  const log = debug('@pie-lib:text-select:token-text');
13
13
 
@@ -1 +0,0 @@
1
- []