@pie-lib/math-input-mathlive 1.0.9-next.6637 → 1.0.10
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/README.md +14 -5
- package/lib/__tests__/composites.test.js +255 -0
- package/lib/__tests__/composites.test.js.map +1 -0
- package/lib/__tests__/drop-in-compat.test.js +133 -0
- package/lib/__tests__/drop-in-compat.test.js.map +1 -0
- package/lib/__tests__/index.test.js +118 -0
- package/lib/__tests__/index.test.js.map +1 -0
- package/lib/__tests__/input-imperative.test.js +155 -0
- package/lib/__tests__/input-imperative.test.js.map +1 -0
- package/lib/__tests__/keypad-label.test.js +118 -0
- package/lib/__tests__/keypad-label.test.js.map +1 -0
- package/lib/__tests__/keypad-render.test.js +206 -0
- package/lib/__tests__/keypad-render.test.js.map +1 -0
- package/lib/__tests__/latex-bridge.test.js +280 -0
- package/lib/__tests__/latex-bridge.test.js.map +1 -0
- package/lib/__tests__/mathlive-instance.test.js +279 -0
- package/lib/__tests__/mathlive-instance.test.js.map +1 -0
- package/lib/__tests__/mount-race.test.js +195 -0
- package/lib/__tests__/mount-race.test.js.map +1 -0
- package/lib/__tests__/static-prompts.test.js +282 -0
- package/lib/__tests__/static-prompts.test.js.map +1 -0
- package/lib/keypad/index.js +11 -13
- package/lib/keypad/index.js.map +1 -1
- package/lib/keys/geometry.js +7 -4
- package/lib/keys/geometry.js.map +1 -1
- package/lib/latex-bridge.js +18 -3
- package/lib/latex-bridge.js.map +1 -1
- package/lib/mathlive-instance.js +103 -11
- package/lib/mathlive-instance.js.map +1 -1
- package/lib/mf/common-styles.js +33 -4
- package/lib/mf/common-styles.js.map +1 -1
- package/lib/mf/input.js +22 -5
- package/lib/mf/input.js.map +1 -1
- package/lib/mf/static.js +21 -4
- package/lib/mf/static.js.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/composites.test.jsx +220 -0
- package/src/__tests__/keypad-label.test.jsx +81 -14
- package/src/__tests__/keypad-render.test.jsx +123 -0
- package/src/__tests__/latex-bridge.test.js +21 -0
- package/src/__tests__/mathlive-instance.test.js +87 -0
- package/src/__tests__/mount-race.test.jsx +130 -0
- package/src/__tests__/static-prompts.test.jsx +251 -0
- package/src/keypad/index.jsx +11 -13
- package/src/keys/geometry.js +7 -2
- package/src/latex-bridge.js +18 -3
- package/src/mathlive-instance.js +111 -11
- package/src/mf/common-styles.js +33 -4
- package/src/mf/input.jsx +23 -5
- package/src/mf/static.jsx +24 -4
- package/LICENSE.md +0 -5
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import MathInput from '../math-input';
|
|
5
|
+
import HorizontalKeypad from '../horizontal-keypad';
|
|
6
|
+
import Input from '../mf/input';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The pieces that sit between the keypad and the field: how a pressed key is
|
|
10
|
+
* routed into the field, and the legacy `{value,type}` shape math-toolbar
|
|
11
|
+
* consumes.
|
|
12
|
+
*/
|
|
13
|
+
describe('MathInput.keypadPress routing', () => {
|
|
14
|
+
const withInput = () => {
|
|
15
|
+
const mi = new MathInput({});
|
|
16
|
+
const calls = { write: [], keystroke: [] };
|
|
17
|
+
|
|
18
|
+
mi.input = {
|
|
19
|
+
write: (v) => calls.write.push(v),
|
|
20
|
+
keystroke: (v) => calls.keystroke.push(v),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return { mi, calls };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
it('routes an insert action to write()', () => {
|
|
27
|
+
const { mi, calls } = withInput();
|
|
28
|
+
|
|
29
|
+
mi.keypadPress({ latex: '\\pi' });
|
|
30
|
+
|
|
31
|
+
expect(calls.write).toEqual(['\\pi']);
|
|
32
|
+
expect(calls.keystroke).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('expands an argument-taking command before writing', () => {
|
|
36
|
+
const { mi, calls } = withInput();
|
|
37
|
+
|
|
38
|
+
mi.keypadPress({ command: '\\frac' });
|
|
39
|
+
|
|
40
|
+
expect(calls.write).toEqual(['\\frac{#?}{#?}']);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('routes a cursor keystroke to keystroke()', () => {
|
|
44
|
+
const { mi, calls } = withInput();
|
|
45
|
+
|
|
46
|
+
mi.keypadPress({ keystroke: 'Left' });
|
|
47
|
+
|
|
48
|
+
expect(calls.keystroke).toEqual(['moveToPreviousChar']);
|
|
49
|
+
expect(calls.write).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('ignores keys with no action', () => {
|
|
53
|
+
const { mi, calls } = withInput();
|
|
54
|
+
|
|
55
|
+
mi.keypadPress({});
|
|
56
|
+
mi.keypadPress({ keystroke: 'Nonsense' });
|
|
57
|
+
|
|
58
|
+
expect(calls.write).toEqual([]);
|
|
59
|
+
expect(calls.keystroke).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('is a no-op before the field ref exists', () => {
|
|
63
|
+
const mi = new MathInput({});
|
|
64
|
+
|
|
65
|
+
expect(() => mi.keypadPress({ latex: '\\pi' })).not.toThrow();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('changeLatex only fires onChange when the value differs', () => {
|
|
69
|
+
const onChange = jest.fn();
|
|
70
|
+
const mi = new MathInput({ latex: 'x', onChange });
|
|
71
|
+
|
|
72
|
+
mi.changeLatex('x');
|
|
73
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
74
|
+
|
|
75
|
+
mi.changeLatex('y');
|
|
76
|
+
expect(onChange).toHaveBeenCalledWith('y');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('tracks focus state', () => {
|
|
80
|
+
const mi = new MathInput({});
|
|
81
|
+
|
|
82
|
+
mi.setState = (s) => Object.assign(mi.state, s);
|
|
83
|
+
mi.inputFocus();
|
|
84
|
+
expect(mi.state.focused).toBe(true);
|
|
85
|
+
|
|
86
|
+
mi.inputBlur();
|
|
87
|
+
expect(mi.state.focused).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('HorizontalKeypad legacy {value,type} model', () => {
|
|
92
|
+
const pressed = (key) => {
|
|
93
|
+
const onClick = jest.fn();
|
|
94
|
+
const hk = new HorizontalKeypad({ onClick });
|
|
95
|
+
|
|
96
|
+
hk.keypadPress(key);
|
|
97
|
+
|
|
98
|
+
return onClick.mock.calls[0][0];
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
it('maps a command to {value, type: command}', () => {
|
|
102
|
+
expect(pressed({ command: '\\frac' })).toEqual({ value: '\\frac', type: 'command' });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('maps a write to {value}', () => {
|
|
106
|
+
expect(pressed({ write: 'x' })).toEqual({ value: 'x' });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('maps a keystroke to {type: cursor}', () => {
|
|
110
|
+
expect(pressed({ keystroke: 'Left' })).toEqual({ type: 'cursor', value: 'Left' });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('renders without throwing', () => {
|
|
114
|
+
expect(() => render(<HorizontalKeypad onClick={() => {}} />)).not.toThrow();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Input: controlled-value sync and events', () => {
|
|
119
|
+
const withField = () => {
|
|
120
|
+
const input = new Input({});
|
|
121
|
+
|
|
122
|
+
input.mathField = {
|
|
123
|
+
value: '',
|
|
124
|
+
getValue: () => input.mathField.value,
|
|
125
|
+
insert(v) {
|
|
126
|
+
input.mathField.value += v;
|
|
127
|
+
},
|
|
128
|
+
executeCommand() {},
|
|
129
|
+
focus() {},
|
|
130
|
+
blur() {},
|
|
131
|
+
addEventListener() {},
|
|
132
|
+
removeEventListener() {},
|
|
133
|
+
remove() {},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
return input;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
it('emits stored-form latex on edit', () => {
|
|
140
|
+
const onChange = jest.fn();
|
|
141
|
+
const input = withField();
|
|
142
|
+
|
|
143
|
+
input.props = { onChange };
|
|
144
|
+
input.mathField.value = '\\longdiv{\\placeholder{}}';
|
|
145
|
+
input.onInputEdit();
|
|
146
|
+
|
|
147
|
+
// \placeholder is MathLive-internal and must not reach the model
|
|
148
|
+
expect(onChange).toHaveBeenCalledWith('\\longdiv{}');
|
|
149
|
+
expect(input.lastEmitted).toEqual('\\longdiv{}');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not push a prop echo back into the field', () => {
|
|
153
|
+
const input = withField();
|
|
154
|
+
|
|
155
|
+
input.mathField.value = 'x';
|
|
156
|
+
input.lastEmitted = 'x';
|
|
157
|
+
input.props = { latex: 'x' };
|
|
158
|
+
|
|
159
|
+
input.componentDidUpdate({ latex: '' });
|
|
160
|
+
|
|
161
|
+
expect(input.mathField.value).toEqual('x');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('pushes a genuinely new latex prop into the field', () => {
|
|
165
|
+
const input = withField();
|
|
166
|
+
|
|
167
|
+
input.mathField.value = 'x';
|
|
168
|
+
input.lastEmitted = 'x';
|
|
169
|
+
input.props = { latex: 'y' };
|
|
170
|
+
|
|
171
|
+
input.componentDidUpdate({ latex: 'x' });
|
|
172
|
+
|
|
173
|
+
expect(input.mathField.value).toEqual('y');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('bridges stored latex on the way in', () => {
|
|
177
|
+
const input = withField();
|
|
178
|
+
|
|
179
|
+
input.props = { latex: '\\longdiv{5}' };
|
|
180
|
+
input.componentDidUpdate({ latex: '' });
|
|
181
|
+
|
|
182
|
+
// must become native \enclose so the content stays editable
|
|
183
|
+
expect(input.mathField.value).toEqual('\\enclose{longdiv}{5}');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('toggles readOnly', () => {
|
|
187
|
+
const input = withField();
|
|
188
|
+
|
|
189
|
+
input.props = { readOnly: true };
|
|
190
|
+
input.componentDidUpdate({ readOnly: false });
|
|
191
|
+
|
|
192
|
+
expect(input.mathField.readOnly).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('clear() empties the field', () => {
|
|
196
|
+
const input = withField();
|
|
197
|
+
|
|
198
|
+
input.mathField.value = 'abc';
|
|
199
|
+
expect(input.clear()).toEqual('');
|
|
200
|
+
expect(input.mathField.value).toEqual('');
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('suppresses Enter so it cannot insert a line break', () => {
|
|
204
|
+
const input = withField();
|
|
205
|
+
const preventDefault = jest.fn();
|
|
206
|
+
|
|
207
|
+
input.onKeyDown({ key: 'Enter', preventDefault });
|
|
208
|
+
expect(preventDefault).toHaveBeenCalled();
|
|
209
|
+
|
|
210
|
+
preventDefault.mockClear();
|
|
211
|
+
input.onKeyDown({ key: 'a', preventDefault });
|
|
212
|
+
expect(preventDefault).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('componentDidUpdate is safe before the field exists', () => {
|
|
216
|
+
const input = new Input({ latex: 'x' });
|
|
217
|
+
|
|
218
|
+
expect(() => input.componentDidUpdate({ latex: '' })).not.toThrow();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -11,24 +11,18 @@ describe('keypad label display normalisation', () => {
|
|
|
11
11
|
const out = latexForDisplay('\\frac{}{}');
|
|
12
12
|
|
|
13
13
|
expect(out).not.toEqual('\\frac{}{}');
|
|
14
|
-
|
|
14
|
+
// a \rule, not a glyph: font metrics for a substituted glyph do not match
|
|
15
|
+
// MathLive's, which left stretchy accents narrower than the box
|
|
16
|
+
expect(out).toContain('\\rule');
|
|
15
17
|
expect(out).toContain('pie-empty');
|
|
16
18
|
});
|
|
17
19
|
|
|
18
20
|
it('handles every empty-group key shape', () => {
|
|
19
|
-
[
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'\\sqrt{}',
|
|
25
|
-
'\\sqrt[{}]{}',
|
|
26
|
-
'\\abs{}',
|
|
27
|
-
'\\log_{}',
|
|
28
|
-
'\\longdiv{}',
|
|
29
|
-
].forEach((latex) => {
|
|
30
|
-
expect(latexForDisplay(latex)).not.toContain('{}');
|
|
31
|
-
});
|
|
21
|
+
['\\frac{}{}', '\\overline{}', 'x^{}', 'x_{}', '\\sqrt{}', '\\sqrt[{}]{}', '\\abs{}', '\\log_{}', '\\longdiv{}'].forEach(
|
|
22
|
+
(latex) => {
|
|
23
|
+
expect(latexForDisplay(latex)).not.toContain('{}');
|
|
24
|
+
},
|
|
25
|
+
);
|
|
32
26
|
});
|
|
33
27
|
|
|
34
28
|
it('treats a whitespace-only group as empty', () => {
|
|
@@ -36,6 +30,79 @@ describe('keypad label display normalisation', () => {
|
|
|
36
30
|
expect(latexForDisplay('\\frac{x}{ }')).toContain('pie-empty');
|
|
37
31
|
});
|
|
38
32
|
|
|
33
|
+
// MathQuill needed a nested \overline{} to draw the empty box. MathLive
|
|
34
|
+
// renders that as a real line, so those labels showed a spurious bar (a line
|
|
35
|
+
// under the arrows, and a straight line instead of the arc).
|
|
36
|
+
it('accent keys do not carry the nested-overline box hack', () => {
|
|
37
|
+
// eslint-disable-next-line global-require
|
|
38
|
+
const geometry = require('../keys/geometry');
|
|
39
|
+
|
|
40
|
+
Object.values(geometry).forEach((key) => {
|
|
41
|
+
if (key && typeof key.latex === 'string') {
|
|
42
|
+
expect(key.latex).not.toContain('\\overline{}}');
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('an accent over an empty slot still gets a visible box', () => {
|
|
48
|
+
['\\overleftrightarrow{}', '\\overarc{}'].forEach((latex) => {
|
|
49
|
+
const out = latexForDisplay(latex);
|
|
50
|
+
|
|
51
|
+
expect(out).toContain('pie-empty');
|
|
52
|
+
expect(out).not.toContain('\\overline');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// A glyph's width comes from font metrics; U+25AB is not in the KaTeX fonts,
|
|
57
|
+
// so the browser substituted a wider one and stretchy accents came out
|
|
58
|
+
// narrower than the box. A rule has an explicit width.
|
|
59
|
+
it('the empty slot has an explicit width, not font-dependent metrics', () => {
|
|
60
|
+
const out = latexForDisplay('\\overarc{}');
|
|
61
|
+
|
|
62
|
+
expect(out).toContain('\\rule');
|
|
63
|
+
expect(out).toMatch(/rule\{[\d.]+em\}\{[\d.]+em\}/);
|
|
64
|
+
expect(out).not.toContain('unicode');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Measured in the browser: the accent's immediate wrapper computes to 0px
|
|
68
|
+
// (its .ML__stretchy child is absolutely positioned), so the svg width has to
|
|
69
|
+
// resolve against .ML__base - whose width equals the content. Without pinning
|
|
70
|
+
// that, the arc collapsed to a spike and sat right of the box.
|
|
71
|
+
it('pins the accent containing block and drops the centering shift', () => {
|
|
72
|
+
// eslint-disable-next-line global-require
|
|
73
|
+
const { placeholderStyles } = require('../mf/common-styles');
|
|
74
|
+
|
|
75
|
+
expect(placeholderStyles['& .ML__base'].position).toEqual('relative');
|
|
76
|
+
expect(placeholderStyles['& .ML__stretchy']).toEqual({ position: 'absolute', left: 0, width: '100%' });
|
|
77
|
+
// must be !important: MathLive writes margin-left as an inline style, and a
|
|
78
|
+
// class cannot override that. Leaving it un-important also halved the arc's
|
|
79
|
+
// width, because .ML__center is the stretchy's containing block.
|
|
80
|
+
expect(placeholderStyles['& .ML__center:has(.ML__stretchy)'].marginLeft).toEqual('0 !important');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('applies those rules wherever static math renders', () => {
|
|
84
|
+
// eslint-disable-next-line global-require
|
|
85
|
+
const cs = require('../mf/common-styles');
|
|
86
|
+
|
|
87
|
+
['commonKeyboardStyles', 'commonMathLiveStyles'].forEach((name) => {
|
|
88
|
+
expect(cs[name]['& .ML__stretchy']).toBeDefined();
|
|
89
|
+
expect(cs[name]['& .ML__base']).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// A bare `& svg` rule re-parents every svg in the subtree; MathLive's own rule
|
|
94
|
+
// is descendant-scoped to .ML__latex .ML__stretchy svg.
|
|
95
|
+
it('does not use a blanket svg rule', () => {
|
|
96
|
+
// eslint-disable-next-line global-require
|
|
97
|
+
const cs = require('../mf/common-styles');
|
|
98
|
+
|
|
99
|
+
Object.values(cs).forEach((style) => {
|
|
100
|
+
if (style && typeof style === 'object') {
|
|
101
|
+
expect(Object.keys(style)).not.toContain('& svg');
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
39
106
|
it('leaves latex without empty groups alone', () => {
|
|
40
107
|
['\\pi', '\\sin', 'x^2', '\\overline{AB}'].forEach((latex) => {
|
|
41
108
|
expect(latexForDisplay(latex)).toEqual(latex);
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import KeyPad, { LatexLabel } from '../keypad';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keypad behaviour that is new in this package: labels are static markup from
|
|
8
|
+
* convertLatexToMarkup (the MathQuill version created a live StaticMath per
|
|
9
|
+
* button), with a raw-latex fallback when the engine is unavailable.
|
|
10
|
+
*/
|
|
11
|
+
describe('LatexLabel', () => {
|
|
12
|
+
it('falls back to raw latex when MathLive is unavailable', () => {
|
|
13
|
+
// jsdom cannot load MathLive, so latexToMarkup returns '' - the button must
|
|
14
|
+
// still show something rather than render blank.
|
|
15
|
+
const { container } = render(<LatexLabel latex={'\\pi'} />);
|
|
16
|
+
|
|
17
|
+
expect(container.textContent).toContain('\\pi');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('re-renders when the latex prop changes', () => {
|
|
21
|
+
const { container, rerender } = render(<LatexLabel latex={'\\pi'} />);
|
|
22
|
+
|
|
23
|
+
rerender(<LatexLabel latex={'\\theta'} />);
|
|
24
|
+
|
|
25
|
+
expect(container.textContent).toContain('\\theta');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does not warn more than once about a missing engine', async () => {
|
|
29
|
+
LatexLabel.warned = false;
|
|
30
|
+
|
|
31
|
+
const a = new LatexLabel({ latex: '\\pi' });
|
|
32
|
+
const b = new LatexLabel({ latex: '\\theta' });
|
|
33
|
+
|
|
34
|
+
a.state = { markup: '' };
|
|
35
|
+
b.state = { markup: '' };
|
|
36
|
+
a.setState = () => {};
|
|
37
|
+
b.setState = () => {};
|
|
38
|
+
|
|
39
|
+
await a.componentDidMount();
|
|
40
|
+
await b.componentDidMount();
|
|
41
|
+
|
|
42
|
+
expect(LatexLabel.warned).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('KeyPad', () => {
|
|
47
|
+
const key = (over = {}) => ({ name: 'x', label: 'x', write: 'x', category: 'vars', ...over });
|
|
48
|
+
|
|
49
|
+
it('renders a button per key', () => {
|
|
50
|
+
const { container } = render(<KeyPad onPress={() => {}} baseSet={[[key(), key({ name: 'y', label: 'y' })]]} />);
|
|
51
|
+
|
|
52
|
+
expect(container.querySelectorAll('button').length).toBe(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('calls onPress with the whole key definition', () => {
|
|
56
|
+
const onPress = jest.fn();
|
|
57
|
+
const k = key();
|
|
58
|
+
const { container } = render(<KeyPad onPress={onPress} baseSet={[[k]]} />);
|
|
59
|
+
|
|
60
|
+
container.querySelector('button').click();
|
|
61
|
+
|
|
62
|
+
expect(onPress).toHaveBeenCalledWith(k);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('renders a latex key through LatexLabel', () => {
|
|
66
|
+
const { container } = render(<KeyPad onPress={() => {}} baseSet={[[{ name: 'pi', latex: '\\pi' }]]} />);
|
|
67
|
+
|
|
68
|
+
expect(container.textContent).toContain('\\pi');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('disables the decimal separators when noDecimal is set', () => {
|
|
72
|
+
const dot = key({ name: '.', label: '.', write: '.' });
|
|
73
|
+
const comma = key({ name: ',', label: ',', write: ',' });
|
|
74
|
+
const { container } = render(<KeyPad onPress={() => {}} noDecimal baseSet={[[dot, comma]]} />);
|
|
75
|
+
|
|
76
|
+
const disabled = Array.from(container.querySelectorAll('button')).filter((b) => b.disabled);
|
|
77
|
+
|
|
78
|
+
expect(disabled.length).toBe(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('leaves the separators enabled otherwise', () => {
|
|
82
|
+
const dot = key({ name: '.', label: '.', write: '.' });
|
|
83
|
+
const { container } = render(<KeyPad onPress={() => {}} baseSet={[[dot]]} />);
|
|
84
|
+
|
|
85
|
+
expect(container.querySelector('button').disabled).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('renders a placeholder for empty grid slots', () => {
|
|
89
|
+
expect(() => render(<KeyPad onPress={() => {}} baseSet={[[null]]} />)).not.toThrow();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('notifies setKeypadInteraction on pointer interaction', () => {
|
|
93
|
+
const setKeypadInteraction = jest.fn();
|
|
94
|
+
const { container } = render(
|
|
95
|
+
<KeyPad onPress={() => {}} setKeypadInteraction={setKeypadInteraction} baseSet={[[key()]]} />,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
container.firstChild.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
99
|
+
|
|
100
|
+
expect(setKeypadInteraction).toHaveBeenCalledWith(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('uses the mode as a class so mode-specific styling applies', () => {
|
|
104
|
+
const { container } = render(<KeyPad onPress={() => {}} mode="language" baseSet={[[key()]]} />);
|
|
105
|
+
|
|
106
|
+
expect(container.firstChild.className).toContain('language');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('drops the base set for the modes that supply their own keys', () => {
|
|
110
|
+
const { container } = render(
|
|
111
|
+
<KeyPad onPress={() => {}} mode="language" baseSet={[[key(), key()]]} additionalKeys={[[key()]]} />,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// base set ignored, only the additional key renders
|
|
115
|
+
expect(container.querySelectorAll('button').length).toBe(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('unmounts without throwing', () => {
|
|
119
|
+
const { unmount } = render(<KeyPad onPress={() => {}} controlledKeypadMode baseSet={[[key()]]} />);
|
|
120
|
+
|
|
121
|
+
expect(() => unmount()).not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -190,6 +190,27 @@ describe('latex-bridge', () => {
|
|
|
190
190
|
expect(keyToAction({ command: '^' })).toEqual({ type: 'insert', value: '^{#?}' });
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
// MathQuill's `/` turned the preceding expression into the numerator: type
|
|
194
|
+
// 5, press the key, get 5/_ with the caret in the denominator. `#@` is
|
|
195
|
+
// MathLive's "selection, or the item before the cursor" token, and is what
|
|
196
|
+
// MathLive binds to `/` itself.
|
|
197
|
+
it('division takes the preceding item as the numerator', () => {
|
|
198
|
+
expect(keyToAction({ command: '/' })).toEqual({ type: 'insert', value: '\\frac{#@}{#?}' });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('an explicit blank-over-blank fraction stays empty', () => {
|
|
202
|
+
// distinct from `/`: this key is meant to insert two empty slots
|
|
203
|
+
expect(keyToAction({ command: '\\frac' })).toEqual({ type: 'insert', value: '\\frac{#?}{#?}' });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('the x-over-blank key routes through division', () => {
|
|
207
|
+
// eslint-disable-next-line global-require
|
|
208
|
+
const { xOverBlank } = require('../keys/fractions');
|
|
209
|
+
|
|
210
|
+
expect(xOverBlank.command).toEqual('/');
|
|
211
|
+
expect(keyToAction(xOverBlank).value).toEqual('\\frac{#@}{#?}');
|
|
212
|
+
});
|
|
213
|
+
|
|
193
214
|
it('passes argument-less commands through', () => {
|
|
194
215
|
expect(keyToAction({ command: '\\pi' })).toEqual({ type: 'insert', value: '\\pi' });
|
|
195
216
|
});
|
|
@@ -150,6 +150,46 @@ describe('mathlive-instance', () => {
|
|
|
150
150
|
expect(MathfieldElement.fontsDirectory.startsWith('http')).toBe(true);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
// \neq, \nsim and \ncong are built from Private Use Area glyphs that only
|
|
154
|
+
// the KaTeX fonts contain, so "no fonts" renders garbage rather than
|
|
155
|
+
// degrading gracefully - hence a CDN fallback by default.
|
|
156
|
+
it('falls back to a version-pinned CDN when nothing is configured', () => {
|
|
157
|
+
const MathfieldElement = {};
|
|
158
|
+
|
|
159
|
+
instance.__setMathLiveForTest({ MathfieldElement, version: { mathlive: '9.9.9' } });
|
|
160
|
+
instance.configureFonts(undefined);
|
|
161
|
+
|
|
162
|
+
expect(MathfieldElement.fontsDirectory).toEqual('https://unpkg.com/mathlive@9.9.9/fonts');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('uses /fonts, not /dist/fonts (the latter 404s on unpkg)', () => {
|
|
166
|
+
const MathfieldElement = {};
|
|
167
|
+
|
|
168
|
+
instance.__setMathLiveForTest({ MathfieldElement, version: { mathlive: '0.110.0' } });
|
|
169
|
+
instance.configureFonts(undefined);
|
|
170
|
+
|
|
171
|
+
expect(MathfieldElement.fontsDirectory).toEqual('https://unpkg.com/mathlive@0.110.0/fonts');
|
|
172
|
+
expect(MathfieldElement.fontsDirectory).not.toContain('/dist/');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('a configured path wins over the CDN', () => {
|
|
176
|
+
const MathfieldElement = {};
|
|
177
|
+
|
|
178
|
+
instance.__setMathLiveForTest({ MathfieldElement, version: { mathlive: '0.110.0' } });
|
|
179
|
+
instance.configureFonts('/mathlive-fonts');
|
|
180
|
+
|
|
181
|
+
expect(MathfieldElement.fontsDirectory).not.toContain('unpkg.com');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('null still opts out entirely', () => {
|
|
185
|
+
const MathfieldElement = {};
|
|
186
|
+
|
|
187
|
+
instance.__setMathLiveForTest({ MathfieldElement, version: { mathlive: '0.110.0' } });
|
|
188
|
+
instance.configureFonts(null);
|
|
189
|
+
|
|
190
|
+
expect(MathfieldElement.fontsDirectory).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
153
193
|
it('leaves a fully-qualified url untouched', () => {
|
|
154
194
|
const MathfieldElement = {};
|
|
155
195
|
|
|
@@ -169,6 +209,53 @@ describe('mathlive-instance', () => {
|
|
|
169
209
|
});
|
|
170
210
|
});
|
|
171
211
|
|
|
212
|
+
// A live <math-field> renders in shadow DOM, so nothing in the page's
|
|
213
|
+
// stylesheets reaches it - the stretchy-accent fix has to be injected there.
|
|
214
|
+
describe('applyShadowStyles', () => {
|
|
215
|
+
const host = () => {
|
|
216
|
+
const el = document.createElement('div');
|
|
217
|
+
|
|
218
|
+
el.attachShadow({ mode: 'open' });
|
|
219
|
+
|
|
220
|
+
return el;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
it('is a no-op without a shadow root', () => {
|
|
224
|
+
expect(() => instance.applyShadowStyles(undefined)).not.toThrow();
|
|
225
|
+
expect(() => instance.applyShadowStyles(document.createElement('div'))).not.toThrow();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('injects the accent fix into the shadow root', () => {
|
|
229
|
+
const el = host();
|
|
230
|
+
|
|
231
|
+
instance.applyShadowStyles(el);
|
|
232
|
+
|
|
233
|
+
const viaSheet = el.shadowRoot.__pieAccentSheet;
|
|
234
|
+
const viaStyle = el.shadowRoot.querySelector('style[data-pie-accent]');
|
|
235
|
+
const css = viaSheet ? 'adopted' : (viaStyle && viaStyle.textContent) || '';
|
|
236
|
+
|
|
237
|
+
expect(viaSheet || viaStyle).toBeTruthy();
|
|
238
|
+
|
|
239
|
+
if (viaStyle) {
|
|
240
|
+
expect(css).toContain('margin-left:0 !important');
|
|
241
|
+
// scoped so single-glyph accents keep their centring offset
|
|
242
|
+
expect(css).toContain(':has(.ML__stretchy)');
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('does not inject twice', () => {
|
|
247
|
+
const el = host();
|
|
248
|
+
|
|
249
|
+
instance.applyShadowStyles(el);
|
|
250
|
+
instance.applyShadowStyles(el);
|
|
251
|
+
|
|
252
|
+
const styles = el.shadowRoot.querySelectorAll('style[data-pie-accent]');
|
|
253
|
+
|
|
254
|
+
// either one adopted sheet, or exactly one style node
|
|
255
|
+
expect(el.shadowRoot.__pieAccentSheet ? 1 : styles.length).toBe(1);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
172
259
|
describe('latexToMarkup', () => {
|
|
173
260
|
it('returns empty string when MathLive is not loaded', () => {
|
|
174
261
|
expect(instance.latexToMarkup('\\pi')).toEqual('');
|