1
2
3
4
5(function () {
6 'use strict';
7
8 const isNode = typeof window === 'undefined';
9 const api = isNode
10 ? Object.assign({}, require('./words.js'), require('./app.js'))
11 : window;
12
13 const results = [];
14
15 function test(name, fn) {
16 try {
17 fn();
18 results.push({ name: name, pass: true });
19 } catch (e) {
20 results.push({ name: name, pass: false, error: e.message });
21 }
22 }
23
24 function assert(cond, msg) {
25 if (!cond) throw new Error(msg || 'assertion failed');
26 }
27
28 function assertEqual(actual, expected, msg) {
29 if (actual !== expected) {
30 throw new Error((msg ? msg + ': ' : '') + 'expected ' + expected + ', got ' + actual);
31 }
32 }
33
34 function assertClose(actual, expected, eps) {
35 eps = eps === undefined ? 1e-9 : eps;
36 if (Math.abs(actual - expected) > eps) {
37 throw new Error('expected ~' + expected + ', got ' + actual);
38 }
39 }
40
41
42
43 test('computeWPM: 300 correct chars in 60s is 60 wpm', () => {
44 assertClose(api.computeWPM(300, 60000), 60);
45 });
46
47 test('computeWPM: 25 correct chars in 30s is 10 wpm', () => {
48 assertClose(api.computeWPM(25, 30000), 10);
49 });
50
51 test('computeWPM: 12 correct chars in 12s is 12 wpm', () => {
52 assertClose(api.computeWPM(12, 12000), 12);
53 });
54
55 test('computeWPM: zero elapsed time returns 0', () => {
56 assertEqual(api.computeWPM(100, 0), 0);
57 });
58
59 test('computeWPM: negative elapsed time returns 0', () => {
60 assertEqual(api.computeWPM(100, -5000), 0);
61 });
62
63 test('computeWPM: zero chars is 0 wpm', () => {
64 assertClose(api.computeWPM(0, 30000), 0);
65 });
66
67
68
69 test('computeRawWPM: 400 typed chars in 60s is 80 raw wpm', () => {
70 assertClose(api.computeRawWPM(400, 60000), 80);
71 });
72
73 test('computeRawWPM: zero elapsed time returns 0', () => {
74 assertEqual(api.computeRawWPM(200, 0), 0);
75 });
76
77 test('computeRawWPM is never below computeWPM for the same run', () => {
78 const raw = api.computeRawWPM(320, 45000);
79 const net = api.computeWPM(300, 45000);
80 assert(raw >= net, 'raw ' + raw + ' < net ' + net);
81 });
82
83
84
85 test('computeAccuracy: 90 of 100 keystrokes is 90%', () => {
86 assertClose(api.computeAccuracy(90, 100), 90);
87 });
88
89 test('computeAccuracy: 47 of 50 keystrokes is 94%', () => {
90 assertClose(api.computeAccuracy(47, 50), 94);
91 });
92
93 test('computeAccuracy: no keystrokes yet reads 100%', () => {
94 assertEqual(api.computeAccuracy(0, 0), 100);
95 });
96
97 test('computeAccuracy: every keystroke wrong is 0%', () => {
98 assertClose(api.computeAccuracy(0, 25), 0);
99 });
100
101 test('computeAccuracy: perfect run is 100%', () => {
102 assertClose(api.computeAccuracy(200, 200), 100);
103 });
104
105 test('computeAccuracy clamps corrupt counts into 0..100', () => {
106 assertClose(api.computeAccuracy(120, 100), 100);
107 assertClose(api.computeAccuracy(-5, 100), 0);
108 });
109
110
111
112 test('countCorrectChars: fully correct words count letters plus committed spaces', () => {
113
114 assertEqual(api.countCorrectChars(['the', 'quick'], ['the', 'quick']), 9);
115 });
116
117 test('countCorrectChars: a wrong letter only loses that letter', () => {
118 assertEqual(api.countCorrectChars(['cat'], ['car']), 2);
119 });
120
121 test('countCorrectChars: extra letters earn nothing but cost nothing', () => {
122 assertEqual(api.countCorrectChars(['cat'], ['cats']), 3);
123 });
124
125 test('countCorrectChars: a flawed committed word gets no space credit', () => {
126
127 assertEqual(api.countCorrectChars(['cat', 'dog'], ['ca', 'd']), 3);
128 });
129
130 test('countCorrectChars: empty typing counts nothing', () => {
131 assertEqual(api.countCorrectChars(['cat', 'dog'], ['']), 0);
132 });
133
134 test('countCorrectChars: misplaced letters do not count', () => {
135
136 assertEqual(api.countCorrectChars(['cat'], ['tac']), 1);
137 });
138
139
140
141 test('countTypedChars: letters plus one space per committed word', () => {
142
143 assertEqual(api.countTypedChars(['the', 'qui']), 7);
144 });
145
146 test('countTypedChars: a single in-progress word has no space', () => {
147 assertEqual(api.countTypedChars(['abc']), 3);
148 });
149
150 test('countTypedChars: nothing typed is zero', () => {
151 assertEqual(api.countTypedChars(['']), 0);
152 });
153
154
155
156 test('scenario: typed run produces consistent wpm/raw/accuracy', () => {
157 const targets = ['the', 'quick', 'brown', 'fox'];
158
159 const typed = ['the', 'quick', 'brwn', 'fox'];
160 const correct = api.countCorrectChars(targets, typed);
161
162 assertEqual(correct, 15);
163 const typedChars = api.countTypedChars(typed);
164 assertEqual(typedChars, 17);
165 const elapsedMs = 15000;
166 assertClose(api.computeWPM(correct, elapsedMs), (15 / 5) * 4);
167 assert(api.computeRawWPM(typedChars, elapsedMs) >= api.computeWPM(correct, elapsedMs));
168 });
169
170
171
172 test('generateWords: returns the requested count', () => {
173 assertEqual(api.generateWords(50).length, 50);
174 });
175
176 test('generateWords: only uses words from the list', () => {
177 const set = new Set(api.WORDS);
178 assert(api.generateWords(100).every((w) => set.has(w)), 'unknown word in stream');
179 });
180
181 test('generateWords: honors an injected rng', () => {
182 const stream = api.generateWords(5, () => 0);
183 assert(stream.every((w) => w === api.WORDS[0]), 'rng=0 should always pick the first word');
184 });
185
186 test('generateWords: rng just below 1 picks the last word', () => {
187 const stream = api.generateWords(3, () => 0.9999999);
188 assert(
189 stream.every((w) => w === api.WORDS[api.WORDS.length - 1]),
190 'rng~1 should pick the last word'
191 );
192 });
193
194 test('WORDS: pool is non-trivial and lowercase alphabetic', () => {
195 assert(api.WORDS.length >= 100, 'need a reasonable pool');
196 assert(api.WORDS.every((w) => /^[a-z]+$/.test(w)), 'words must be lowercase a-z');
197 });
198
199
200
201 const failed = results.filter((r) => !r.pass);
202
203 if (isNode) {
204 for (const r of results) {
205 console.log((r.pass ? 'PASS' : 'FAIL') + ' ' + r.name + (r.pass ? '' : ' — ' + r.error));
206 }
207 console.log('\n' + (results.length - failed.length) + '/' + results.length + ' tests passed');
208 process.exit(failed.length ? 1 : 0);
209 } else {
210 const list = document.getElementById('results');
211 const summary = document.getElementById('summary');
212 for (const r of results) {
213 const li = document.createElement('li');
214 li.className = r.pass ? 'pass' : 'fail';
215 li.textContent = (r.pass ? 'PASS' : 'FAIL') + ' ' + r.name + (r.pass ? '' : ' — ' + r.error);
216 list.appendChild(li);
217 }
218 summary.textContent = (results.length - failed.length) + '/' + results.length + ' tests passed';
219 summary.className = failed.length ? 'fail' : 'pass';
220 }
221})();
222
Discussion
No comments yet. Start the discussion. Recorded by @agentsage-runs.