@pie-lib/mask-markup 3.0.4-next.33 → 3.0.4-next.34
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 +17 -0
- package/CHANGELOG.md +1256 -0
- package/LICENSE.md +5 -0
- package/lib/choices/choice.js +116 -0
- package/lib/choices/choice.js.map +1 -0
- package/lib/choices/index.js +103 -0
- package/lib/choices/index.js.map +1 -0
- package/lib/componentize.js +21 -0
- package/lib/componentize.js.map +1 -0
- package/lib/components/blank.js +371 -0
- package/lib/components/blank.js.map +1 -0
- package/lib/components/correct-input.js +94 -0
- package/lib/components/correct-input.js.map +1 -0
- package/lib/components/dropdown.js +483 -0
- package/lib/components/dropdown.js.map +1 -0
- package/lib/components/input.js +50 -0
- package/lib/components/input.js.map +1 -0
- package/lib/constructed-response.js +101 -0
- package/lib/constructed-response.js.map +1 -0
- package/lib/customizable.js +42 -0
- package/lib/customizable.js.map +1 -0
- package/lib/drag-in-the-blank.js +254 -0
- package/lib/drag-in-the-blank.js.map +1 -0
- package/lib/index.js +55 -0
- package/lib/index.js.map +1 -0
- package/lib/inline-dropdown.js +40 -0
- package/lib/inline-dropdown.js.map +1 -0
- package/lib/mask.js +198 -0
- package/lib/mask.js.map +1 -0
- package/lib/serialization.js +261 -0
- package/lib/serialization.js.map +1 -0
- package/lib/with-mask.js +97 -0
- package/lib/with-mask.js.map +1 -0
- package/package.json +20 -39
- package/src/__tests__/drag-in-the-blank.test.js +111 -0
- package/src/__tests__/index.test.js +38 -0
- package/src/__tests__/mask.test.js +381 -0
- package/src/__tests__/serialization.test.js +54 -0
- package/src/__tests__/utils.js +1 -0
- package/src/__tests__/with-mask.test.js +76 -0
- package/src/choices/__tests__/index.test.js +75 -0
- package/src/choices/choice.jsx +97 -0
- package/src/choices/index.jsx +64 -0
- package/src/componentize.js +13 -0
- package/src/components/__tests__/blank.test.js +199 -0
- package/src/components/__tests__/correct-input.test.js +90 -0
- package/src/components/__tests__/dropdown.test.js +129 -0
- package/src/components/__tests__/input.test.js +102 -0
- package/src/components/blank.jsx +386 -0
- package/src/components/correct-input.jsx +82 -0
- package/src/components/dropdown.jsx +423 -0
- package/src/components/input.jsx +48 -0
- package/src/constructed-response.jsx +87 -0
- package/src/customizable.jsx +34 -0
- package/src/drag-in-the-blank.jsx +241 -0
- package/src/index.js +16 -0
- package/src/inline-dropdown.jsx +29 -0
- package/src/mask.jsx +172 -0
- package/src/serialization.js +260 -0
- package/src/with-mask.jsx +75 -0
- package/dist/_virtual/_rolldown/runtime.js +0 -4
- package/dist/choices/choice.d.ts +0 -24
- package/dist/choices/choice.js +0 -77
- package/dist/choices/index.d.ts +0 -25
- package/dist/choices/index.js +0 -49
- package/dist/componentize.d.ts +0 -12
- package/dist/componentize.js +0 -4
- package/dist/components/blank.d.ts +0 -39
- package/dist/components/blank.js +0 -240
- package/dist/components/correct-input.d.ts +0 -11
- package/dist/components/dropdown.d.ts +0 -37
- package/dist/components/dropdown.js +0 -320
- package/dist/components/input.d.ts +0 -37
- package/dist/constructed-response.d.ts +0 -44
- package/dist/constructed-response.js +0 -55
- package/dist/customizable.d.ts +0 -43
- package/dist/customizable.js +0 -8
- package/dist/drag-in-the-blank.d.ts +0 -37
- package/dist/drag-in-the-blank.js +0 -164
- package/dist/index.d.ts +0 -15
- package/dist/index.js +0 -7
- package/dist/inline-dropdown.d.ts +0 -44
- package/dist/inline-dropdown.js +0 -24
- package/dist/mask.d.ts +0 -30
- package/dist/mask.js +0 -99
- package/dist/node_modules/.bun/clsx@2.1.1/node_modules/clsx/dist/clsx.js +0 -16
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/index.js +0 -17
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/cssPrefix.js +0 -9
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/cssUnitless.js +0 -26
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/hasOwn.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/isFunction.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/isObject.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/prefixInfo.js +0 -24
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/prefixProperties.js +0 -32
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/prefixer.js +0 -29
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/camelize.js +0 -14
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/hyphenRe.js +0 -8
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/hyphenate.js +0 -12
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/separate.js +0 -11
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/toLowerFirst.js +0 -10
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/stringUtils/toUpperFirst.js +0 -10
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/toStyleObject.js +0 -55
- package/dist/node_modules/.bun/to-style@1.3.3/node_modules/to-style/src/toStyleString.js +0 -16
- package/dist/serialization.d.ts +0 -34
- package/dist/serialization.js +0 -132
- package/dist/with-mask.d.ts +0 -55
- package/dist/with-mask.js +0 -45
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import Mask from '../mask';
|
|
4
|
+
|
|
5
|
+
describe('Mask', () => {
|
|
6
|
+
// Don't mock renderChildren - let the component render naturally
|
|
7
|
+
const onChange = jest.fn();
|
|
8
|
+
const defaultProps = {
|
|
9
|
+
onChange,
|
|
10
|
+
layout: {
|
|
11
|
+
nodes: [
|
|
12
|
+
{
|
|
13
|
+
object: 'text',
|
|
14
|
+
leaves: [
|
|
15
|
+
{
|
|
16
|
+
text: 'Foo',
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
value: {},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
onChange.mockClear();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('rendering', () => {
|
|
30
|
+
it('renders with default props', () => {
|
|
31
|
+
const { container } = render(<Mask {...defaultProps} />);
|
|
32
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders text content', () => {
|
|
36
|
+
render(<Mask {...defaultProps} />);
|
|
37
|
+
expect(screen.getByText('Foo')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders a paragraph element', () => {
|
|
41
|
+
const { container } = render(
|
|
42
|
+
<Mask
|
|
43
|
+
{...defaultProps}
|
|
44
|
+
layout={{
|
|
45
|
+
nodes: [
|
|
46
|
+
{
|
|
47
|
+
type: 'p',
|
|
48
|
+
nodes: [
|
|
49
|
+
{
|
|
50
|
+
object: 'text',
|
|
51
|
+
leaves: [
|
|
52
|
+
{
|
|
53
|
+
text: 'Foo',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
}}
|
|
61
|
+
/>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Paragraph is rendered as a styled div, not a <p> tag
|
|
65
|
+
expect(screen.getByText('Foo')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('renders nested div and paragraph', () => {
|
|
69
|
+
const { container } = render(
|
|
70
|
+
<Mask
|
|
71
|
+
{...defaultProps}
|
|
72
|
+
layout={{
|
|
73
|
+
nodes: [
|
|
74
|
+
{
|
|
75
|
+
type: 'div',
|
|
76
|
+
data: {
|
|
77
|
+
attributes: {},
|
|
78
|
+
},
|
|
79
|
+
nodes: [
|
|
80
|
+
{
|
|
81
|
+
type: 'p',
|
|
82
|
+
data: {
|
|
83
|
+
attributes: {},
|
|
84
|
+
},
|
|
85
|
+
nodes: [
|
|
86
|
+
{
|
|
87
|
+
object: 'text',
|
|
88
|
+
leaves: [
|
|
89
|
+
{
|
|
90
|
+
text: 'Foo',
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
}}
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(container.querySelector('div')).toBeInTheDocument();
|
|
104
|
+
// Paragraph is rendered as a styled div, not a <p> tag
|
|
105
|
+
expect(screen.getByText('Foo')).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('renders text with italic marks', () => {
|
|
109
|
+
const { container } = render(
|
|
110
|
+
<Mask
|
|
111
|
+
{...defaultProps}
|
|
112
|
+
layout={{
|
|
113
|
+
nodes: [
|
|
114
|
+
{
|
|
115
|
+
leaves: [{ text: 'Foo ' }],
|
|
116
|
+
object: 'text',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
leaves: [
|
|
120
|
+
{
|
|
121
|
+
marks: [
|
|
122
|
+
{
|
|
123
|
+
data: undefined,
|
|
124
|
+
type: 'italic',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
text: 'x',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
object: 'text',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
leaves: [{ text: ' bar' }],
|
|
134
|
+
object: 'text',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
object: 'block',
|
|
138
|
+
type: 'div',
|
|
139
|
+
}}
|
|
140
|
+
/>,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Text "Foo " is split with spaces, use regex
|
|
144
|
+
expect(screen.getByText(/Foo/)).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByText('x')).toBeInTheDocument();
|
|
146
|
+
expect(screen.getByText(/bar/)).toBeInTheDocument();
|
|
147
|
+
// Check for italic/em element
|
|
148
|
+
const em = container.querySelector('em, i');
|
|
149
|
+
expect(em).toBeInTheDocument();
|
|
150
|
+
expect(em.textContent).toBe('x');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('renders tbody without extra space', () => {
|
|
154
|
+
const da = () => ({ data: { attributes: {} } });
|
|
155
|
+
const { container } = render(
|
|
156
|
+
<Mask
|
|
157
|
+
{...defaultProps}
|
|
158
|
+
layout={{
|
|
159
|
+
nodes: [
|
|
160
|
+
{
|
|
161
|
+
type: 'table',
|
|
162
|
+
...da(),
|
|
163
|
+
nodes: [
|
|
164
|
+
{
|
|
165
|
+
type: 'tbody',
|
|
166
|
+
...da(),
|
|
167
|
+
nodes: [
|
|
168
|
+
{
|
|
169
|
+
object: 'text',
|
|
170
|
+
leaves: [{ text: ' ' }],
|
|
171
|
+
},
|
|
172
|
+
{ type: 'tr', ...da(), nodes: [] },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
}}
|
|
179
|
+
/>,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(container.querySelector('table')).toBeInTheDocument();
|
|
183
|
+
expect(container.querySelector('tbody')).toBeInTheDocument();
|
|
184
|
+
expect(container.querySelector('tr')).toBeInTheDocument();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('spacer rendering for DnD components', () => {
|
|
189
|
+
it('adds spacers before and after DnD blank components', () => {
|
|
190
|
+
const mockRenderChildren = jest.fn((n) => {
|
|
191
|
+
if (n.data?.dataset?.component === 'blank') {
|
|
192
|
+
return <span data-testid="blank-component">Blank</span>;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { container } = render(
|
|
198
|
+
<Mask
|
|
199
|
+
{...defaultProps}
|
|
200
|
+
renderChildren={mockRenderChildren}
|
|
201
|
+
layout={{
|
|
202
|
+
nodes: [
|
|
203
|
+
{
|
|
204
|
+
type: 'div',
|
|
205
|
+
data: {
|
|
206
|
+
dataset: { component: 'blank' },
|
|
207
|
+
attributes: {},
|
|
208
|
+
},
|
|
209
|
+
nodes: [],
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
}}
|
|
213
|
+
/>,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Check that renderChildren was called and spacers are present
|
|
217
|
+
// Count all children in the container - should be: spacer + blank + spacer = 3 elements
|
|
218
|
+
const maskContainer = container.firstChild;
|
|
219
|
+
expect(maskContainer.childNodes.length).toBe(3);
|
|
220
|
+
expect(screen.getByTestId('blank-component')).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('does not add spacers for non-DnD components', () => {
|
|
224
|
+
const mockRenderChildren = jest.fn((n) => {
|
|
225
|
+
return <span data-testid="regular-component">Regular</span>;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const { container } = render(
|
|
229
|
+
<Mask
|
|
230
|
+
{...defaultProps}
|
|
231
|
+
renderChildren={mockRenderChildren}
|
|
232
|
+
layout={{
|
|
233
|
+
nodes: [
|
|
234
|
+
{
|
|
235
|
+
type: 'div',
|
|
236
|
+
data: {
|
|
237
|
+
attributes: {},
|
|
238
|
+
},
|
|
239
|
+
nodes: [],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
}}
|
|
243
|
+
/>,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// Should not have spacers - only the regular component
|
|
247
|
+
const maskContainer = container.firstChild;
|
|
248
|
+
expect(maskContainer.childNodes.length).toBe(1);
|
|
249
|
+
expect(screen.getByTestId('regular-component')).toBeInTheDocument();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('adds spacers regardless of parent node type', () => {
|
|
253
|
+
const mockRenderChildren = jest.fn((n) => {
|
|
254
|
+
if (n.data?.dataset?.component === 'blank') {
|
|
255
|
+
return <span data-testid="blank-in-td">Blank in TD</span>;
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const { container } = render(
|
|
261
|
+
<Mask
|
|
262
|
+
{...defaultProps}
|
|
263
|
+
renderChildren={mockRenderChildren}
|
|
264
|
+
elementType="drag-in-the-blank"
|
|
265
|
+
layout={{
|
|
266
|
+
nodes: [
|
|
267
|
+
{
|
|
268
|
+
type: 'table',
|
|
269
|
+
data: { attributes: {} },
|
|
270
|
+
nodes: [
|
|
271
|
+
{
|
|
272
|
+
type: 'tbody',
|
|
273
|
+
data: { attributes: {} },
|
|
274
|
+
nodes: [
|
|
275
|
+
{
|
|
276
|
+
type: 'tr',
|
|
277
|
+
data: { attributes: {} },
|
|
278
|
+
nodes: [
|
|
279
|
+
{
|
|
280
|
+
type: 'td',
|
|
281
|
+
data: { attributes: {} },
|
|
282
|
+
nodes: [
|
|
283
|
+
{
|
|
284
|
+
type: 'div',
|
|
285
|
+
data: {
|
|
286
|
+
dataset: { component: 'blank' },
|
|
287
|
+
attributes: {},
|
|
288
|
+
},
|
|
289
|
+
nodes: [],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
}}
|
|
301
|
+
/>,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Should have spacers even inside td element
|
|
305
|
+
const td = container.querySelector('td');
|
|
306
|
+
expect(td.childNodes.length).toBe(3); // spacer + blank + spacer
|
|
307
|
+
expect(screen.getByTestId('blank-in-td')).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('does not add spacers for text content', () => {
|
|
311
|
+
const { container } = render(
|
|
312
|
+
<Mask
|
|
313
|
+
{...defaultProps}
|
|
314
|
+
elementType="drag-in-the-blank"
|
|
315
|
+
layout={{
|
|
316
|
+
nodes: [
|
|
317
|
+
{
|
|
318
|
+
object: 'text',
|
|
319
|
+
leaves: [
|
|
320
|
+
{
|
|
321
|
+
text: 'Some text',
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
}}
|
|
327
|
+
/>,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Should not have spacers for plain text - just text node
|
|
331
|
+
const maskContainer = container.firstChild;
|
|
332
|
+
expect(maskContainer.childNodes.length).toBe(1);
|
|
333
|
+
expect(maskContainer.childNodes[0].nodeType).toBe(Node.TEXT_NODE);
|
|
334
|
+
expect(screen.getByText('Some text')).toBeInTheDocument();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it('handles multiple DnD components with correct spacer placement', () => {
|
|
338
|
+
const mockRenderChildren = jest.fn((n) => {
|
|
339
|
+
if (n.data?.dataset?.component === 'blank') {
|
|
340
|
+
return <span data-testid={`blank-${n.data.testId}`}>Blank</span>;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
const { container } = render(
|
|
346
|
+
<Mask
|
|
347
|
+
{...defaultProps}
|
|
348
|
+
renderChildren={mockRenderChildren}
|
|
349
|
+
layout={{
|
|
350
|
+
nodes: [
|
|
351
|
+
{
|
|
352
|
+
type: 'div',
|
|
353
|
+
data: {
|
|
354
|
+
dataset: { component: 'blank' },
|
|
355
|
+
attributes: {},
|
|
356
|
+
testId: '1',
|
|
357
|
+
},
|
|
358
|
+
nodes: [],
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
type: 'div',
|
|
362
|
+
data: {
|
|
363
|
+
dataset: { component: 'blank' },
|
|
364
|
+
attributes: {},
|
|
365
|
+
testId: '2',
|
|
366
|
+
},
|
|
367
|
+
nodes: [],
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
}}
|
|
371
|
+
/>,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Should have 2 spacers per component = 4 spacers + 2 blanks = 6 total children
|
|
375
|
+
const maskContainer = container.firstChild;
|
|
376
|
+
expect(maskContainer.childNodes.length).toBe(6);
|
|
377
|
+
expect(screen.getByTestId('blank-1')).toBeInTheDocument();
|
|
378
|
+
expect(screen.getByTestId('blank-2')).toBeInTheDocument();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { deserialize } from '../serialization';
|
|
2
|
+
|
|
3
|
+
describe('serialization', () => {
|
|
4
|
+
it('ignores comments', () => {
|
|
5
|
+
const out = deserialize(`<!-- hi -->`);
|
|
6
|
+
expect(out.document.nodes[0]).toEqual(expect.objectContaining({ type: 'span' }));
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('ignores comments', () => {
|
|
10
|
+
const out = deserialize(`<!-- hi --><div>foo</div>`);
|
|
11
|
+
expect(out.document.nodes[0]).toEqual(
|
|
12
|
+
expect.objectContaining({
|
|
13
|
+
type: 'div',
|
|
14
|
+
nodes: [
|
|
15
|
+
expect.objectContaining({
|
|
16
|
+
object: 'text',
|
|
17
|
+
leaves: [{ text: 'foo' }],
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('deserializes an em', () => {
|
|
25
|
+
const out = deserialize(`<!-- hi --><div> <em>x</em> </div>`);
|
|
26
|
+
expect(out.document.nodes[0]).toEqual(
|
|
27
|
+
expect.objectContaining({
|
|
28
|
+
type: 'div',
|
|
29
|
+
nodes: [
|
|
30
|
+
expect.objectContaining({
|
|
31
|
+
object: 'text',
|
|
32
|
+
}),
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
leaves: [
|
|
35
|
+
{
|
|
36
|
+
marks: [
|
|
37
|
+
{
|
|
38
|
+
data: undefined,
|
|
39
|
+
type: 'italic',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
text: 'x',
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
object: 'text',
|
|
46
|
+
}),
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
object: 'text',
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const choice = (v, id) => ({ label: v, value: v, id });
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { withMask } from '../with-mask';
|
|
5
|
+
|
|
6
|
+
describe('WithMask', () => {
|
|
7
|
+
const onChange = jest.fn();
|
|
8
|
+
const defaultProps = {
|
|
9
|
+
markup: '<p>Foo bar {{0}} over the moon;</p>',
|
|
10
|
+
value: {
|
|
11
|
+
0: 'blank',
|
|
12
|
+
},
|
|
13
|
+
onChange,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Masked = withMask('foo', (props) => (node) => {
|
|
17
|
+
const dataset = node.data ? node.data.dataset || {} : {};
|
|
18
|
+
|
|
19
|
+
if (dataset.component === 'foo') {
|
|
20
|
+
return <input type="text" data-testid="masked-input" defaultValue="Foo" onChange={props.onChange} />;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
onChange.mockClear();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('rendering', () => {
|
|
29
|
+
it('renders with default props', () => {
|
|
30
|
+
const { container } = render(<Masked {...defaultProps} />);
|
|
31
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders markup content', () => {
|
|
35
|
+
render(<Masked {...defaultProps} />);
|
|
36
|
+
expect(screen.getByText(/Foo bar/)).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders paragraph content', () => {
|
|
40
|
+
const { container } = render(<Masked {...defaultProps} />);
|
|
41
|
+
// Paragraph is rendered as a styled div, not a <p> tag
|
|
42
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText(/Foo bar/)).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('onChange handler', () => {
|
|
48
|
+
it('calls onChange when value changes', async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
render(<Masked {...defaultProps} />);
|
|
51
|
+
|
|
52
|
+
const input = screen.queryByTestId('masked-input');
|
|
53
|
+
if (input) {
|
|
54
|
+
await user.clear(input);
|
|
55
|
+
await user.type(input, 'ceva');
|
|
56
|
+
|
|
57
|
+
expect(onChange).toHaveBeenCalled();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('passes event to onChange', async () => {
|
|
62
|
+
const user = userEvent.setup();
|
|
63
|
+
render(<Masked {...defaultProps} />);
|
|
64
|
+
|
|
65
|
+
const input = screen.queryByTestId('masked-input');
|
|
66
|
+
if (input) {
|
|
67
|
+
await user.clear(input);
|
|
68
|
+
await user.type(input, 'test');
|
|
69
|
+
|
|
70
|
+
expect(onChange).toHaveBeenCalled();
|
|
71
|
+
const lastCall = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
|
72
|
+
expect(lastCall).toHaveProperty('target');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import Choice from '../choice';
|
|
4
|
+
import { choice } from '../../__tests__/utils';
|
|
5
|
+
import Choices from '../index';
|
|
6
|
+
|
|
7
|
+
// Mock @dnd-kit hooks to avoid DndContext requirement
|
|
8
|
+
jest.mock('@dnd-kit/core', () => ({
|
|
9
|
+
useDraggable: jest.fn(() => ({
|
|
10
|
+
attributes: {},
|
|
11
|
+
listeners: {},
|
|
12
|
+
setNodeRef: jest.fn(),
|
|
13
|
+
isDragging: false,
|
|
14
|
+
})),
|
|
15
|
+
useDroppable: jest.fn(() => ({
|
|
16
|
+
setNodeRef: jest.fn(),
|
|
17
|
+
isOver: false,
|
|
18
|
+
active: null,
|
|
19
|
+
})),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('index', () => {
|
|
23
|
+
describe('Choices', () => {
|
|
24
|
+
const defaultProps = {
|
|
25
|
+
disabled: false,
|
|
26
|
+
choices: [choice('Jumped', '0'), choice('Laughed', '1'), choice('Spoon', '2')],
|
|
27
|
+
choicePosition: 'below',
|
|
28
|
+
instanceId: 'test-instance',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
it('renders correctly with default props', () => {
|
|
32
|
+
const { container } = render(<Choices {...defaultProps} />);
|
|
33
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Jumped')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('Laughed')).toBeInTheDocument();
|
|
36
|
+
expect(screen.getByText('Spoon')).toBeInTheDocument();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('renders correctly with disabled prop as true', () => {
|
|
40
|
+
const { container } = render(<Choices {...defaultProps} disabled={true} />);
|
|
41
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders without duplicates', () => {
|
|
45
|
+
const { container } = render(<Choices {...defaultProps} duplicates={undefined} value={{ 0: '0', 1: '1' }} />);
|
|
46
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('renders with duplicates', () => {
|
|
50
|
+
const { container } = render(<Choices {...defaultProps} duplicates={true} value={{ 0: '0', 1: '1' }} />);
|
|
51
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('Choice', () => {
|
|
56
|
+
const defaultProps = {
|
|
57
|
+
disabled: false,
|
|
58
|
+
choice: choice('Label', '1'),
|
|
59
|
+
instanceId: 'test-instance',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
describe('render', () => {
|
|
63
|
+
it('renders correctly with default props', () => {
|
|
64
|
+
const { container } = render(<Choice {...defaultProps} />);
|
|
65
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText('Label')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders correctly with disabled prop as true', () => {
|
|
70
|
+
const { container } = render(<Choice {...defaultProps} disabled={true} />);
|
|
71
|
+
expect(container.firstChild).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { useDraggable } from '@dnd-kit/core';
|
|
4
|
+
import { styled } from '@mui/material/styles';
|
|
5
|
+
import Chip from '@mui/material/Chip';
|
|
6
|
+
import { renderMath } from '@pie-lib/math-rendering';
|
|
7
|
+
import { color } from '@pie-lib/render-ui';
|
|
8
|
+
|
|
9
|
+
export const DRAG_TYPE = 'MaskBlank';
|
|
10
|
+
|
|
11
|
+
const StyledChoice = styled('span')(({ theme, disabled }) => ({
|
|
12
|
+
border: `solid 0px ${theme.palette.primary.main}`,
|
|
13
|
+
borderRadius: theme.spacing(2),
|
|
14
|
+
margin: theme.spacing(0.5),
|
|
15
|
+
transform: 'translate(0, 0)',
|
|
16
|
+
display: 'inline-flex',
|
|
17
|
+
...(disabled && {}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const StyledChip = styled(Chip)(() => ({
|
|
21
|
+
backgroundColor: color.white(),
|
|
22
|
+
border: `1px solid ${color.text()}`,
|
|
23
|
+
color: color.text(),
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
display: 'inline-flex',
|
|
26
|
+
height: 'initial',
|
|
27
|
+
minHeight: '32px',
|
|
28
|
+
fontSize: 'inherit',
|
|
29
|
+
whiteSpace: 'pre-wrap',
|
|
30
|
+
maxWidth: '374px',
|
|
31
|
+
// Added for touch devices, for image content.
|
|
32
|
+
// This will prevent the context menu from appearing and not allowing other interactions with the image.
|
|
33
|
+
// If interactions with the image in the token will be requested we should handle only the context Menu.
|
|
34
|
+
pointerEvents: 'none',
|
|
35
|
+
borderRadius: '3px',
|
|
36
|
+
paddingTop: '12px',
|
|
37
|
+
paddingBottom: '12px',
|
|
38
|
+
|
|
39
|
+
'&.Mui-disabled': {
|
|
40
|
+
opacity: 1,
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const StyledChipLabel = styled('span')(() => ({
|
|
45
|
+
whiteSpace: 'normal',
|
|
46
|
+
'& img': {
|
|
47
|
+
display: 'block',
|
|
48
|
+
padding: '2px 0',
|
|
49
|
+
},
|
|
50
|
+
'& mjx-frac': {
|
|
51
|
+
fontSize: '120% !important',
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
export default function Choice({ choice, disabled, instanceId }) {
|
|
56
|
+
const rootRef = useRef(null);
|
|
57
|
+
|
|
58
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
59
|
+
id: `choice-${choice.id}`,
|
|
60
|
+
data: { choice, instanceId, fromChoice: true, type: DRAG_TYPE },
|
|
61
|
+
disabled,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
renderMath(rootRef.current);
|
|
66
|
+
}, [choice.value]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<StyledChoice
|
|
70
|
+
ref={setNodeRef}
|
|
71
|
+
style={
|
|
72
|
+
isDragging
|
|
73
|
+
? {
|
|
74
|
+
width: rootRef.current?.offsetWidth,
|
|
75
|
+
height: rootRef.current?.offsetHeight,
|
|
76
|
+
}
|
|
77
|
+
: {}
|
|
78
|
+
}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
{...listeners}
|
|
81
|
+
{...attributes}
|
|
82
|
+
>
|
|
83
|
+
<StyledChip
|
|
84
|
+
clickable={false}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
ref={rootRef}
|
|
87
|
+
label={<StyledChipLabel dangerouslySetInnerHTML={{ __html: choice.value }} />}
|
|
88
|
+
/>
|
|
89
|
+
</StyledChoice>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
Choice.propTypes = {
|
|
94
|
+
choice: PropTypes.object.isRequired,
|
|
95
|
+
disabled: PropTypes.bool,
|
|
96
|
+
instanceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
97
|
+
};
|