Repository navigation
Expand file tree
/
Copy pathtest_mathjax.js
More file actions
153 lines (144 loc) · 4.89 KB
/
Copy pathtest_mathjax.js
File metadata and controls
153 lines (144 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// Run with: node --test test_mathjax.js
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const vm = require('node:vm');
function deferred() {
let resolve;
const promise = new Promise(r => { resolve = r; });
return {promise, resolve};
}
function context(values = {}) {
const ctx = vm.createContext(values);
ctx.window = ctx;
return ctx;
}
function run(ctx, name) {
vm.runInContext(fs.readFileSync(path.join(__dirname, name), 'utf8'), ctx);
}
test('loader waits for startup and loads only the CDN runtime on success', async () => {
const startup = deferred();
const scripts = [];
const ctx = context({
MathJax: {startup: {promise: startup.promise}},
document: {
createElement: () => ({}),
head: {appendChild: script => scripts.push(script)}
}
});
run(ctx, 'mathjax-loader.js');
assert.equal(scripts.length, 1);
assert.match(scripts[0].src, /mathjax@4\.1\.3\/tex-chtml\.js$/);
let ready = false;
ctx.mathjaxReady.then(() => { ready = true; });
scripts[0].onload();
await Promise.resolve();
assert.equal(ready, false);
startup.resolve();
await ctx.mathjaxReady;
assert.equal(scripts.length, 1);
});
test('loader falls back to the local v4 runtime only on CDN failure', async () => {
const scripts = [];
let removed = 0;
const ctx = context({
MathJax: {startup: {promise: Promise.resolve()}},
document: {
createElement: () => ({remove: () => { removed++; }}),
head: {appendChild: script => scripts.push(script)}
}
});
run(ctx, 'mathjax-loader.js');
scripts[0].onerror();
await Promise.resolve();
assert.equal(removed, 1);
assert.equal(scripts.length, 2);
assert.equal(scripts[1].src, 'htmlbook/MathJax/node_modules/mathjax/tex-chtml.js');
scripts[1].onload();
await ctx.mathjaxReady;
});
test('loader reports failure if neither runtime can be loaded', async () => {
const scripts = [];
const ctx = context({MathJax: {}, document: {
createElement: () => ({remove() {}}),
head: {appendChild: script => scripts.push(script)}
}});
run(ctx, 'mathjax-loader.js');
const rejected = assert.rejects(ctx.mathjaxReady, /Unable to load MathJax/);
scripts[0].onerror();
await Promise.resolve();
scripts[1].onerror();
await rejected;
});
for (const useLoader of [false, true]) {
test(`typesetting waits for readiness and exposes completion (loader=${useLoader})`, async () => {
const ready = deferred();
const typeset = deferred();
let called = false;
const ctx = context({MathJax: {
startup: {promise: ready.promise},
typesetPromise: () => { called = true; return typeset.promise; }
}, document: {querySelectorAll: () => []}});
if (useLoader) ctx.mathjaxReady = ready.promise;
run(ctx, 'book.js');
const result = ctx.typesetMath();
assert.equal(result, ctx.mathTypesetPromise);
assert.equal(called, false);
ready.resolve();
await new Promise(setImmediate);
assert.equal(called, true);
let finished = false;
result.then(() => { finished = true; });
await Promise.resolve();
assert.equal(finished, false);
typeset.resolve();
await result;
assert.equal(finished, true);
});
}
function placeholderDocument() {
const created = [];
const placeholder = {
getAttribute: name => name === 'src' ? 'https://hypothes.is/embed.js' : null,
replacedWith: null,
replaceWith(node) { this.replacedWith = node; }
};
const document = {
querySelectorAll: selector => {
assert.equal(selector, 'script[data-run-after="mathjax"]');
return placeholder.replacedWith ? [] : [placeholder];
},
createElement: tag => { const el = {tag}; created.push(el); return el; }
};
return {document, placeholder, created};
}
for (const outcome of ['resolves', 'rejects']) {
test(`deferred scripts run only after typesetting ${outcome}`, async () => {
const typeset = deferred();
let rejectTypeset;
const failed = new Promise((_, reject) => { rejectTypeset = reject; });
const {document, placeholder, created} = placeholderDocument();
const ctx = context({MathJax: {
startup: {promise: Promise.resolve()},
typesetPromise: () => outcome === 'resolves' ? typeset.promise : failed
}, document});
run(ctx, 'book.js');
const result = ctx.typesetMath();
await new Promise(setImmediate);
assert.equal(placeholder.replacedWith, null);
if (outcome === 'resolves') {
typeset.resolve();
await result;
} else {
rejectTypeset(new Error('typeset failed'));
await assert.rejects(result, /typeset failed/);
}
await new Promise(setImmediate);
assert.equal(created.length, 1);
assert.equal(placeholder.replacedWith, created[0]);
assert.equal(created[0].tag, 'script');
assert.equal(created[0].src, 'https://hypothes.is/embed.js');
assert.equal(created[0].async, true);
});
}