]> git.immae.eu Git - perso/Immae/Projets/Cryptomonnaies/BIP39.git/blob - tests.js
Merge pull request #71 from LedgerHQ/master
[perso/Immae/Projets/Cryptomonnaies/BIP39.git] / tests.js
1 // Usage:
2 // $ phantomjs tests.js
3
4
5 var page = require('webpage').create();
6 var url = 'src/index.html';
7 var testMaxTime = 20000;
8
9 page.viewportSize = {
10 width: 1024,
11 height: 720
12 };
13
14 page.onResourceError = function(e) {
15 console.log("Error loading " + e.url);
16 phantom.exit();
17 }
18
19 function fail() {
20 console.log("Failed");
21 phantom.exit();
22 }
23
24 function waitForGenerate(fn, maxTime) {
25 if (!maxTime) {
26 maxTime = testMaxTime;
27 }
28 var start = new Date().getTime();
29 var prevAddressCount = -1;
30 var wait = function keepWaiting() {
31 var now = new Date().getTime();
32 var hasTimedOut = now - start > maxTime;
33 var addressCount = page.evaluate(function() {
34 return $(".address").length;
35 });
36 var hasFinished = addressCount > 0 && addressCount == prevAddressCount;
37 prevAddressCount = addressCount;
38 if (hasFinished) {
39 fn();
40 }
41 else if (hasTimedOut) {
42 console.log("Test timed out");
43 fn();
44 }
45 else {
46 setTimeout(keepWaiting, 100);
47 }
48 }
49 wait();
50 }
51
52 function waitForFeedback(fn, maxTime) {
53 if (!maxTime) {
54 maxTime = testMaxTime;
55 }
56 var start = new Date().getTime();
57 var wait = function keepWaiting() {
58 var now = new Date().getTime();
59 var hasTimedOut = now - start > maxTime;
60 if (hasTimedOut) {
61 console.log("Test timed out");
62 fn();
63 return;
64 }
65 var feedback = page.evaluate(function() {
66 var feedback = $(".feedback");
67 if (feedback.css("display") == "none") {
68 return "";
69 }
70 return feedback.text();
71 });
72 var hasFinished = feedback.length > 0 && feedback != "Calculating...";
73 if (hasFinished) {
74 fn();
75 }
76 else {
77 setTimeout(keepWaiting, 100);
78 }
79 }
80 wait();
81 }
82
83 function waitForEntropyFeedback(fn, maxTime) {
84 if (!maxTime) {
85 maxTime = testMaxTime;
86 }
87 var origFeedback = page.evaluate(function() {
88 return $(".entropy-container").text();
89 });
90 var start = new Date().getTime();
91 var wait = function keepWaiting() {
92 var now = new Date().getTime();
93 var hasTimedOut = now - start > maxTime;
94 if (hasTimedOut) {
95 console.log("Test timed out");
96 fn();
97 return;
98 }
99 var feedback = page.evaluate(function() {
100 return $(".entropy-container").text();
101 });
102 var hasFinished = feedback != origFeedback;
103 if (hasFinished) {
104 fn();
105 }
106 else {
107 setTimeout(keepWaiting, 100);
108 }
109 }
110 wait();
111 }
112
113 function next() {
114 if (tests.length > 0) {
115 var testsStr = tests.length == 1 ? "test" : "tests";
116 console.log(tests.length + " " + testsStr + " remaining");
117 tests.shift()();
118 }
119 else {
120 console.log("Finished with 0 failures");
121 phantom.exit();
122 }
123 }
124
125 /**
126 * Randomize array element order in-place.
127 * Using Durstenfeld shuffle algorithm.
128 * See http://stackoverflow.com/a/12646864
129 */
130 function shuffle(array) {
131 for (var i = array.length - 1; i > 0; i--) {
132 var j = Math.floor(Math.random() * (i + 1));
133 var temp = array[i];
134 array[i] = array[j];
135 array[j] = temp;
136 }
137 return array;
138 }
139
140 tests = [
141
142 // Page loads with status of 'success'
143 function() {
144 page.open(url, function(status) {
145 if (status != "success") {
146 console.log("Page did not load with status 'success'");
147 fail();
148 }
149 next();
150 });
151 },
152
153 // Page has text
154 function() {
155 page.open(url, function(status) {
156 var content = page.evaluate(function() {
157 return document.body.textContent.trim();
158 });
159 if (!content) {
160 console.log("Page does not have text");
161 fail();
162 }
163 next();
164 });
165 },
166
167 // Entering mnemonic generates addresses
168 function() {
169 page.open(url, function(status) {
170 // set the phrase
171 page.evaluate(function() {
172 $(".phrase").val("abandon abandon ability").trigger("input");
173 });
174 // get the address
175 waitForGenerate(function() {
176 var addressCount = page.evaluate(function() {
177 return $(".address").length;
178 });
179 if (addressCount != 20) {
180 console.log("Mnemonic did not generate addresses");
181 console.log("Expected: " + expected);
182 console.log("Got: " + actual);
183 fail();
184 }
185 next();
186 });
187 });
188 },
189
190 // Random button generates random mnemonic
191 function() {
192 page.open(url, function(status) {
193 // check initial phrase is empty
194 var phrase = page.evaluate(function() {
195 return $(".phrase").text();
196 });
197 if (phrase != "") {
198 console.log("Initial phrase is not blank");
199 fail();
200 }
201 // press the 'generate' button
202 page.evaluate(function() {
203 $(".generate").click();
204 });
205 // get the new phrase
206 waitForGenerate(function() {
207 var phrase = page.evaluate(function() {
208 return $(".phrase").val();
209 });
210 if (phrase.length <= 0) {
211 console.log("Phrase not generated by pressing button");
212 fail();
213 }
214 next();
215 });
216 });
217 },
218
219 // Mnemonic length can be customized
220 function() {
221 page.open(url, function(status) {
222 // set the length to 6
223 var expectedLength = "6";
224 page.evaluate(function() {
225 $(".strength option[selected]").removeAttr("selected");
226 $(".strength option[value=6]").prop("selected", true);
227 });
228 // press the 'generate' button
229 page.evaluate(function() {
230 $(".generate").click();
231 });
232 // check the new phrase is six words long
233 waitForGenerate(function() {
234 var actualLength = page.evaluate(function() {
235 var words = $(".phrase").val().split(" ");
236 return words.length;
237 });
238 if (actualLength != expectedLength) {
239 console.log("Phrase not generated with correct length");
240 console.log("Expected: " + expectedLength);
241 console.log("Actual: " + actualLength);
242 fail();
243 }
244 next();
245 });
246 });
247 },
248
249 // Passphrase can be set
250 function() {
251 page.open(url, function(status) {
252 // set the phrase and passphrase
253 var expected = "15pJzUWPGzR7avffV9nY5by4PSgSKG9rba";
254 page.evaluate(function() {
255 $(".phrase").val("abandon abandon ability");
256 $(".passphrase").val("secure_passphrase").trigger("input");
257 });
258 // check the address is generated correctly
259 waitForGenerate(function() {
260 var actual = page.evaluate(function() {
261 return $(".address:first").text();
262 });
263 if (actual != expected) {
264 console.log("Passphrase results in wrong address");
265 console.log("Expected: " + expected);
266 console.log("Actual: " + actual);
267 fail();
268 }
269 next();
270 });
271 });
272 },
273
274 // Network can be set to bitcoin testnet
275 function() {
276 page.open(url, function(status) {
277 // set the phrase and coin
278 var expected = "mucaU5iiDaJDb69BHLeDv8JFfGiyg2nJKi";
279 page.evaluate(function() {
280 $(".phrase").val("abandon abandon ability");
281 $(".phrase").trigger("input");
282 $(".network option[selected]").removeAttr("selected");
283 $(".network option").filter(function() {
284 return $(this).html() == "Bitcoin Testnet";
285 }).prop("selected", true);
286 $(".network").trigger("change");
287 });
288 // check the address is generated correctly
289 waitForGenerate(function() {
290 var actual = page.evaluate(function() {
291 return $(".address:first").text();
292 });
293 if (actual != expected) {
294 console.log("Bitcoin testnet address is incorrect");
295 console.log("Expected: " + expected);
296 console.log("Actual: " + actual);
297 fail();
298 }
299 next();
300 });
301 });
302 },
303
304 // Network can be set to litecoin
305 function() {
306 page.open(url, function(status) {
307 // set the phrase and coin
308 var expected = "LQ4XU8RX2ULPmPq9FcUHdVmPVchP9nwXdn";
309 page.evaluate(function() {
310 $(".phrase").val("abandon abandon ability");
311 $(".phrase").trigger("input");
312 $(".network option[selected]").removeAttr("selected");
313 $(".network option").filter(function() {
314 return $(this).html() == "Litecoin";
315 }).prop("selected", true);
316 $(".network").trigger("change");
317 });
318 // check the address is generated correctly
319 waitForGenerate(function() {
320 var actual = page.evaluate(function() {
321 return $(".address:first").text();
322 });
323 if (actual != expected) {
324 console.log("Litecoin address is incorrect");
325 console.log("Expected: " + expected);
326 console.log("Actual: " + actual);
327 fail();
328 }
329 next();
330 });
331 });
332 },
333
334 // Network can be set to ripple
335 function() {
336 page.open(url, function(status) {
337 // set the phrase and coin
338 var expected = "rLTFnqbmCVPGx6VfaygdtuKWJgcN4v1zRS";
339 page.evaluate(function() {
340 $(".phrase").val("ill clump only blind unit burden thing track silver cloth review awake useful craft whale all satisfy else trophy sunset walk vanish hope valve");
341 $(".phrase").trigger("input");
342 $(".network option[selected]").removeAttr("selected");
343 $(".network option").filter(function() {
344 return $(this).html() == "Ripple";
345 }).prop("selected", true);
346 $(".network").trigger("change");
347 });
348 // check the address is generated correctly
349 waitForGenerate(function() {
350 var actual = page.evaluate(function() {
351 return $(".address:first").text();
352 });
353 if (actual != expected) {
354 console.log("Litecoin address is incorrect");
355 console.log("Expected: " + expected);
356 console.log("Actual: " + actual);
357 fail();
358 }
359 next();
360 });
361 });
362 },
363
364 // Network can be set to dogecoin
365 function() {
366 page.open(url, function(status) {
367 // set the phrase and coin
368 var expected = "DPQH2AtuzkVSG6ovjKk4jbUmZ6iXLpgbJA";
369 page.evaluate(function() {
370 $(".phrase").val("abandon abandon ability");
371 $(".phrase").trigger("input");
372 $(".network option[selected]").removeAttr("selected");
373 $(".network option").filter(function() {
374 return $(this).html() == "Dogecoin";
375 }).prop("selected", true);
376 $(".network").trigger("change");
377 });
378 // check the address is generated correctly
379 waitForGenerate(function() {
380 var actual = page.evaluate(function() {
381 return $(".address:first").text();
382 });
383 if (actual != expected) {
384 console.log("Dogecoin address is incorrect");
385 console.log("Expected: " + expected);
386 console.log("Actual: " + actual);
387 fail();
388 }
389 next();
390 });
391 });
392 },
393
394 // Network can be set to shadowcash
395 function() {
396 page.open(url, function(status) {
397 // set the phrase and coin
398 var expected = "SiSZtfYAXEFvMm3XM8hmtkGDyViRwErtCG";
399 page.evaluate(function() {
400 $(".phrase").val("abandon abandon ability");
401 $(".phrase").trigger("input");
402 $(".network option[selected]").removeAttr("selected");
403 $(".network option").filter(function() {
404 return $(this).html() == "ShadowCash";
405 }).prop("selected", true);
406 $(".network").trigger("change");
407 });
408 // check the address is generated correctly
409 waitForGenerate(function() {
410 var actual = page.evaluate(function() {
411 return $(".address:first").text();
412 });
413 if (actual != expected) {
414 console.log("Shadowcash address is incorrect");
415 console.log("Expected: " + expected);
416 console.log("Actual: " + actual);
417 fail();
418 }
419 next();
420 });
421 });
422 },
423
424 // Network can be set to shadowcash testnet
425 function() {
426 page.open(url, function(status) {
427 // set the phrase and coin
428 var expected = "tM2EDpVKaTiEg2NZg3yKg8eqjLr55BErHe";
429 page.evaluate(function() {
430 $(".phrase").val("abandon abandon ability");
431 $(".phrase").trigger("input");
432 $(".network option[selected]").removeAttr("selected");
433 $(".network option").filter(function() {
434 return $(this).html() == "ShadowCash Testnet";
435 }).prop("selected", true);
436 $(".network").trigger("change");
437 });
438 // check the address is generated correctly
439 waitForGenerate(function() {
440 var actual = page.evaluate(function() {
441 return $(".address:first").text();
442 });
443 if (actual != expected) {
444 console.log("Shadowcash testnet address is incorrect");
445 console.log("Expected: " + expected);
446 console.log("Actual: " + actual);
447 fail();
448 }
449 next();
450 });
451 });
452 },
453
454 // Network can be set to viacoin
455 function() {
456 page.open(url, function(status) {
457 // set the phrase and coin
458 var expected = "Vq9Eq4N5SQnjqZvxtxzo7hZPW5XnyJsmXT";
459 page.evaluate(function() {
460 $(".phrase").val("abandon abandon ability");
461 $(".phrase").trigger("input");
462 $(".network option[selected]").removeAttr("selected");
463 $(".network option").filter(function() {
464 return $(this).html() == "Viacoin";
465 }).prop("selected", true);
466 $(".network").trigger("change");
467 });
468 // check the address is generated correctly
469 waitForGenerate(function() {
470 var actual = page.evaluate(function() {
471 return $(".address:first").text();
472 });
473 if (actual != expected) {
474 console.log("Viacoin address is incorrect");
475 console.log("Expected: " + expected);
476 console.log("Actual: " + actual);
477 fail();
478 }
479 next();
480 });
481 });
482 },
483
484 // Network can be set to viacoin testnet
485 function() {
486 page.open(url, function(status) {
487 // set the phrase and coin
488 var expected = "tM2EDpVKaTiEg2NZg3yKg8eqjLr55BErHe";
489 page.evaluate(function() {
490 $(".phrase").val("abandon abandon ability");
491 $(".phrase").trigger("input");
492 $(".network option[selected]").removeAttr("selected");
493 $(".network option").filter(function() {
494 return $(this).html() == "Viacoin Testnet";
495 }).prop("selected", true);
496 $(".network").trigger("change");
497 });
498 // check the address is generated correctly
499 waitForGenerate(function() {
500 var actual = page.evaluate(function() {
501 return $(".address:first").text();
502 });
503 if (actual != expected) {
504 console.log("Viacoin testnet address is incorrect");
505 console.log("Expected: " + expected);
506 console.log("Actual: " + actual);
507 fail();
508 }
509 next();
510 });
511 });
512 },
513
514 // Network can be set to jumbucks
515 function() {
516 page.open(url, function(status) {
517 // set the phrase and coin
518 var expected = "JLEXccwDXADK4RxBPkRez7mqsHVoJBEUew";
519 page.evaluate(function() {
520 $(".phrase").val("abandon abandon ability");
521 $(".phrase").trigger("input");
522 $(".network option[selected]").removeAttr("selected");
523 $(".network option").filter(function() {
524 return $(this).html() == "Jumbucks";
525 }).prop("selected", true);
526 $(".network").trigger("change");
527 });
528 // check the address is generated correctly
529 waitForGenerate(function() {
530 var actual = page.evaluate(function() {
531 return $(".address:first").text();
532 });
533 if (actual != expected) {
534 console.log("Jumbucks address is incorrect");
535 console.log("Expected: " + expected);
536 console.log("Actual: " + actual);
537 fail();
538 }
539 next();
540 });
541 });
542 },
543
544 // Network can be set to clam
545 function() {
546 page.open(url, function(status) {
547 // set the phrase and coin
548 var expected = "xCp4sakjVx4pUAZ6cBCtuin8Ddb6U1sk9y";
549 page.evaluate(function() {
550 $(".phrase").val("abandon abandon ability");
551 $(".phrase").trigger("input");
552 $(".network option[selected]").removeAttr("selected");
553 $(".network option").filter(function() {
554 return $(this).html() == "CLAM";
555 }).prop("selected", true);
556 $(".network").trigger("change");
557 });
558 // check the address is generated correctly
559 waitForGenerate(function() {
560 var actual = page.evaluate(function() {
561 return $(".address:first").text();
562 });
563 if (actual != expected) {
564 console.log("CLAM address is incorrect");
565 console.log("Expected: " + expected);
566 console.log("Actual: " + actual);
567 fail();
568 }
569 next();
570 });
571 });
572 },
573
574 // Network can be set to dash
575 function() {
576 page.open(url, function(status) {
577 // set the phrase and coin
578 var expected = "XdbhtMuGsPSkE6bPdNTHoFSszQKmK4S5LT";
579 page.evaluate(function() {
580 $(".phrase").val("abandon abandon ability");
581 $(".phrase").trigger("input");
582 $(".network option[selected]").removeAttr("selected");
583 $(".network option").filter(function() {
584 return $(this).html() == "DASH";
585 }).prop("selected", true);
586 $(".network").trigger("change");
587 });
588 // check the address is generated correctly
589 waitForGenerate(function() {
590 var actual = page.evaluate(function() {
591 return $(".address:first").text();
592 });
593 if (actual != expected) {
594 console.log("DASH address is incorrect");
595 console.log("Expected: " + expected);
596 console.log("Actual: " + actual);
597 fail();
598 }
599 next();
600 });
601 });
602 },
603
604 // Network can be set to game
605 function() {
606 page.open(url, function(status) {
607 // set the phrase and coin
608 var expected = "GSMY9bAp36cMR4zyT4uGVS7GFjpdXbao5Q";
609 page.evaluate(function() {
610 $(".phrase").val("abandon abandon ability");
611 $(".phrase").trigger("input");
612 $(".network option[selected]").removeAttr("selected");
613 $(".network option").filter(function() {
614 return $(this).html() == "GAME";
615 }).prop("selected", true);
616 $(".network").trigger("change");
617 });
618 // check the address is generated correctly
619 waitForGenerate(function() {
620 var actual = page.evaluate(function() {
621 return $(".address:first").text();
622 });
623 if (actual != expected) {
624 console.log("GAME address is incorrect");
625 console.log("Expected: " + expected);
626 console.log("Actual: " + actual);
627 fail();
628 }
629 next();
630 });
631 });
632 },
633
634 // Network can be set to namecoin
635 function() {
636 page.open(url, function(status) {
637 // set the phrase and coin
638 var expected = "Mw2vK2Bvex1yYtYF6sfbEg2YGoUc98YUD2";
639 page.evaluate(function() {
640 $(".phrase").val("abandon abandon ability");
641 $(".phrase").trigger("input");
642 $(".network option[selected]").removeAttr("selected");
643 $(".network option").filter(function() {
644 return $(this).html() == "Namecoin";
645 }).prop("selected", true);
646 $(".network").trigger("change");
647 });
648 // check the address is generated correctly
649 waitForGenerate(function() {
650 var actual = page.evaluate(function() {
651 return $(".address:first").text();
652 });
653 if (actual != expected) {
654 console.log("Namecoin address is incorrect");
655 console.log("Expected: " + expected);
656 console.log("Actual: " + actual);
657 fail();
658 }
659 next();
660 });
661 });
662 },
663
664 // Network can be set to peercoin
665 function() {
666 page.open(url, function(status) {
667 // set the phrase and coin
668 var expected = "PVAiioTaK2eDHSEo3tppT9AVdBYqxRTBAm";
669 page.evaluate(function() {
670 $(".phrase").val("abandon abandon ability");
671 $(".phrase").trigger("input");
672 $(".network option[selected]").removeAttr("selected");
673 $(".network option").filter(function() {
674 return $(this).html() == "Peercoin";
675 }).prop("selected", true);
676 $(".network").trigger("change");
677 });
678 // check the address is generated correctly
679 waitForGenerate(function() {
680 var actual = page.evaluate(function() {
681 return $(".address:first").text();
682 });
683 if (actual != expected) {
684 console.log("Peercoin address is incorrect");
685 console.log("Expected: " + expected);
686 console.log("Actual: " + actual);
687 fail();
688 }
689 next();
690 });
691 });
692 },
693
694 // Network can be set to ethereum
695 function() {
696
697 page.open(url, function(status) {
698
699 // set the phrase and coin
700 page.evaluate(function() {
701 $(".phrase").val("abandon abandon ability");
702 $(".phrase").trigger("input");
703 $(".network option[selected]").removeAttr("selected");
704 $(".network option").filter(function() {
705 return $(this).html() == "Ethereum";
706 }).prop("selected", true);
707 $(".network").trigger("change");
708 });
709 waitForGenerate(function() {
710 // check the address is generated correctly
711 // this value comes from
712 // https://www.myetherwallet.com/#view-wallet-info
713 // Unusual capitalization is due to checksum
714 var expected = "0xe5815d5902Ad612d49283DEdEc02100Bd44C2772";
715 var actual = page.evaluate(function() {
716 return $(".address:first").text();
717 });
718 if (actual != expected) {
719 console.log("Ethereum address is incorrect");
720 console.log("Expected: " + expected);
721 console.log("Actual: " + actual);
722 fail();
723 }
724 // check the private key is correct
725 // this private key can be imported into
726 // https://www.myetherwallet.com/#view-wallet-info
727 // and it should correlate to the address above
728 var expected = "8f253078b73d7498302bb78c171b23ce7a8fb511987d2b2702b731638a4a15e7";
729 var actual = page.evaluate(function() {
730 return $(".privkey:first").text();
731 });
732 if (actual != expected) {
733 console.log("Ethereum privkey is incorrect");
734 console.log("Expected: " + expected);
735 console.log("Actual: " + actual);
736 fail();
737 }
738 // check the public key is correct
739 // TODO
740 // don't have any third-party source to generate the expected value
741 //var expected = "?";
742 //var actual = page.evaluate(function() {
743 // return $(".pubkey:first").text();
744 //});
745 //if (actual != expected) {
746 // console.log("Ethereum privkey is incorrect");
747 // console.log("Expected: " + expected);
748 // console.log("Actual: " + actual);
749 // fail();
750 //}
751 next();
752 });
753 });
754 },
755
756 // Network can be set to Slimcoin
757 function() {
758 page.open(url, function(status) {
759 // set the phrase and coin
760 var expected = "SNzPi1CafHFm3WWjRo43aMgiaEEj3ogjww";
761 page.evaluate(function() {
762 $(".phrase").val("abandon abandon ability");
763 $(".phrase").trigger("input");
764 $(".network option[selected]").removeAttr("selected");
765 $(".network option").filter(function() {
766 return $(this).html() == "Slimcoin";
767 }).prop("selected", true);
768 $(".network").trigger("change");
769 });
770 // check the address is generated correctly
771 waitForGenerate(function() {
772 var actual = page.evaluate(function() {
773 return $(".address:first").text();
774 });
775 if (actual != expected) {
776 console.log("Slimcoin address is incorrect");
777 console.log("Expected: " + expected);
778 console.log("Actual: " + actual);
779 fail();
780 }
781 next();
782 });
783 });
784 },
785
786 // Network can be set to Slimcointn
787 function() {
788 page.open(url, function(status) {
789 // set the phrase and coin
790 var expected = "n3nMgWufTek5QQAr6uwMhg5xbzj8xqc4Dq";
791 page.evaluate(function() {
792 $(".phrase").val("abandon abandon ability");
793 $(".phrase").trigger("input");
794 $(".network option[selected]").removeAttr("selected");
795 $(".network option").filter(function() {
796 return $(this).html() == "Slimcoin Testnet";
797 }).prop("selected", true);
798 $(".network").trigger("change");
799 });
800 // check the address is generated correctly
801 waitForGenerate(function() {
802 var actual = page.evaluate(function() {
803 return $(".address:first").text();
804 });
805 if (actual != expected) {
806 console.log("Slimcoin testnet address is incorrect");
807 console.log("Expected: " + expected);
808 console.log("Actual: " + actual);
809 fail();
810 }
811 next();
812 });
813 });
814 },
815
816 // BIP39 seed is set from phrase
817 function() {
818 page.open(url, function(status) {
819 // set the phrase
820 var expected = "20da140d3dd1df8713cefcc4d54ce0e445b4151027a1ab567b832f6da5fcc5afc1c3a3f199ab78b8e0ab4652efd7f414ac2c9a3b81bceb879a70f377aa0a58f3";
821 page.evaluate(function() {
822 $(".phrase").val("abandon abandon ability");
823 $(".phrase").trigger("input");
824 });
825 // check the address is generated correctly
826 waitForGenerate(function() {
827 var actual = page.evaluate(function() {
828 return $(".seed").val();
829 });
830 if (actual != expected) {
831 console.log("BIP39 seed is incorrectly generated from mnemonic");
832 console.log("Expected: " + expected);
833 console.log("Actual: " + actual);
834 fail();
835 }
836 next();
837 });
838 });
839 },
840
841 // BIP32 root key is set from phrase
842 function() {
843 page.open(url, function(status) {
844 // set the phrase
845 var expected = "xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpi";
846 page.evaluate(function() {
847 $(".phrase").val("abandon abandon ability");
848 $(".phrase").trigger("input");
849 });
850 // check the address is generated correctly
851 waitForGenerate(function() {
852 var actual = page.evaluate(function() {
853 return $(".root-key").val();
854 });
855 if (actual != expected) {
856 console.log("Root key is incorrectly generated from mnemonic");
857 console.log("Expected: " + expected);
858 console.log("Actual: " + actual);
859 fail();
860 }
861 next();
862 });
863 });
864 },
865
866 // Tabs show correct addresses when changed
867 function() {
868 page.open(url, function(status) {
869 // set the phrase
870 var expected = "17uQ7s2izWPwBmEVFikTmZUjbBKWYdJchz";
871 page.evaluate(function() {
872 $(".phrase").val("abandon abandon ability");
873 $(".phrase").trigger("input");
874 });
875 // change tabs
876 waitForGenerate(function() {
877 page.evaluate(function() {
878 $("#bip32-tab a").click();
879 });
880 // check the address is generated correctly
881 waitForGenerate(function() {
882 var actual = page.evaluate(function() {
883 return $(".address:first").text();
884 });
885 if (actual != expected) {
886 console.log("Clicking tab generates incorrect address");
887 console.log("Expected: " + expected);
888 console.log("Actual: " + actual);
889 fail();
890 }
891 next();
892 });
893 });
894 });
895 },
896
897 // BIP44 derivation path is shown
898 function() {
899 page.open(url, function(status) {
900 // set the phrase
901 var expected = "m/44'/0'/0'/0";
902 page.evaluate(function() {
903 $(".phrase").val("abandon abandon ability");
904 $(".phrase").trigger("input");
905 });
906 // check the derivation path of the first address
907 waitForGenerate(function() {
908 var actual = page.evaluate(function() {
909 return $("#bip44 .path").val();
910 });
911 if (actual != expected) {
912 console.log("BIP44 derivation path is incorrect");
913 console.log("Expected: " + expected);
914 console.log("Actual: " + actual);
915 fail();
916 }
917 next();
918 });
919 });
920 },
921
922 // BIP44 extended private key is shown
923 function() {
924 page.open(url, function(status) {
925 // set the phrase
926 var expected = "xprvA2DxxvPZcyRvYgZMGS53nadR32mVDeCyqQYyFhrCVbJNjPoxMeVf7QT5g7mQASbTf9Kp4cryvcXnu2qurjWKcrdsr91jXymdCDNxKgLFKJG";
927 page.evaluate(function() {
928 $(".phrase").val("abandon abandon ability");
929 $(".phrase").trigger("input");
930 });
931 // check the BIP44 extended private key
932 waitForGenerate(function() {
933 var actual = page.evaluate(function() {
934 return $(".extended-priv-key").val();
935 });
936 if (actual != expected) {
937 console.log("BIP44 extended private key is incorrect");
938 console.log("Expected: " + expected);
939 console.log("Actual: " + actual);
940 fail();
941 }
942 next();
943 });
944 });
945 },
946
947 // BIP44 extended public key is shown
948 function() {
949 page.open(url, function(status) {
950 // set the phrase
951 var expected = "xpub6FDKNRvTTLzDmAdpNTc49ia9b4byd6vqCdUa46Fp3vqMcC96uBoufCmZXQLiN5AK3iSCJMhf9gT2sxkpyaPepRuA7W3MujV5tGmF5VfbueM";
952 page.evaluate(function() {
953 $(".phrase").val("abandon abandon ability");
954 $(".phrase").trigger("input");
955 });
956 // check the BIP44 extended public key
957 waitForGenerate(function() {
958 var actual = page.evaluate(function() {
959 return $(".extended-pub-key").val();
960 });
961 if (actual != expected) {
962 console.log("BIP44 extended public key is incorrect");
963 console.log("Expected: " + expected);
964 console.log("Actual: " + actual);
965 fail();
966 }
967 next();
968 });
969 });
970 },
971
972 // BIP44 purpose field changes address list
973 function() {
974 page.open(url, function(status) {
975 // set the phrase
976 var expected = "1JbDzRJ2cDT8aat2xwKd6Pb2zzavow5MhF";
977 page.evaluate(function() {
978 $(".phrase").val("abandon abandon ability");
979 $(".phrase").trigger("input");
980 });
981 waitForGenerate(function() {
982 // change the bip44 purpose field to 45
983 page.evaluate(function() {
984 $("#bip44 .purpose").val("45");
985 $("#bip44 .purpose").trigger("input");
986 });
987 waitForGenerate(function() {
988 // check the address for the new derivation path
989 var actual = page.evaluate(function() {
990 return $(".address:first").text();
991 });
992 if (actual != expected) {
993 console.log("BIP44 purpose field generates incorrect address");
994 console.log("Expected: " + expected);
995 console.log("Actual: " + actual);
996 fail();
997 }
998 next();
999 });
1000 });
1001 });
1002 },
1003
1004 // BIP44 coin field changes address list
1005 function() {
1006 page.open(url, function(status) {
1007 // set the phrase
1008 var expected = "1F6dB2djQYrxoyfZZmfr6D5voH8GkJTghk";
1009 page.evaluate(function() {
1010 $(".phrase").val("abandon abandon ability");
1011 $(".phrase").trigger("input");
1012 });
1013 waitForGenerate(function() {
1014 // change the bip44 purpose field to 45
1015 page.evaluate(function() {
1016 $("#bip44 .coin").val("1");
1017 $("#bip44 .coin").trigger("input");
1018 });
1019 waitForGenerate(function() {
1020 // check the address for the new derivation path
1021 var actual = page.evaluate(function() {
1022 return $(".address:first").text();
1023 });
1024 if (actual != expected) {
1025 console.log("BIP44 coin field generates incorrect address");
1026 console.log("Expected: " + expected);
1027 console.log("Actual: " + actual);
1028 fail();
1029 }
1030 next();
1031 });
1032 });
1033 });
1034 },
1035
1036 // BIP44 account field changes address list
1037 function() {
1038 page.open(url, function(status) {
1039 // set the phrase
1040 var expected = "1Nq2Wmu726XHCuGhctEtGmhxo3wzk5wZ1H";
1041 page.evaluate(function() {
1042 $(".phrase").val("abandon abandon ability");
1043 $(".phrase").trigger("input");
1044 });
1045 waitForGenerate(function() {
1046 // change the bip44 purpose field to 45
1047 page.evaluate(function() {
1048 $("#bip44 .account").val("1");
1049 $("#bip44 .account").trigger("input");
1050 });
1051 waitForGenerate(function() {
1052 // check the address for the new derivation path
1053 var actual = page.evaluate(function() {
1054 return $(".address:first").text();
1055 });
1056 if (actual != expected) {
1057 console.log("BIP44 account field generates incorrect address");
1058 console.log("Expected: " + expected);
1059 console.log("Actual: " + actual);
1060 fail();
1061 }
1062 next();
1063 });
1064 });
1065 });
1066 },
1067
1068 // BIP44 change field changes address list
1069 function() {
1070 page.open(url, function(status) {
1071 // set the phrase
1072 var expected = "1KAGfWgqfVbSSXY56fNQ7YnhyKuoskHtYo";
1073 page.evaluate(function() {
1074 $(".phrase").val("abandon abandon ability");
1075 $(".phrase").trigger("input");
1076 });
1077 waitForGenerate(function() {
1078 // change the bip44 purpose field to 45
1079 page.evaluate(function() {
1080 $("#bip44 .change").val("1");
1081 $("#bip44 .change").trigger("input");
1082 });
1083 waitForGenerate(function() {
1084 // check the address for the new derivation path
1085 var actual = page.evaluate(function() {
1086 return $(".address:first").text();
1087 });
1088 if (actual != expected) {
1089 console.log("BIP44 change field generates incorrect address");
1090 console.log("Expected: " + expected);
1091 console.log("Actual: " + actual);
1092 fail();
1093 }
1094 next();
1095 });
1096 });
1097 });
1098 },
1099
1100 // BIP32 derivation path can be set
1101 function() {
1102 page.open(url, function(status) {
1103 // set the phrase
1104 var expected = "16pYQQdLD1hH4hwTGLXBaZ9Teboi1AGL8L";
1105 page.evaluate(function() {
1106 $(".phrase").val("abandon abandon ability");
1107 $(".phrase").trigger("input");
1108 });
1109 // change tabs
1110 waitForGenerate(function() {
1111 page.evaluate(function() {
1112 $("#bip32-tab a").click();
1113 });
1114 // set the derivation path to m/1
1115 waitForGenerate(function() {
1116 page.evaluate(function() {
1117 $("#bip32 .path").val("m/1");
1118 $("#bip32 .path").trigger("input");
1119 });
1120 // check the address is generated correctly
1121 waitForGenerate(function() {
1122 var actual = page.evaluate(function() {
1123 return $(".address:first").text();
1124 });
1125 if (actual != expected) {
1126 console.log("Custom BIP32 path generates incorrect address");
1127 console.log("Expected: " + expected);
1128 console.log("Actual: " + actual);
1129 fail();
1130 }
1131 next();
1132 });
1133 });
1134 });
1135 });
1136 },
1137
1138 // BIP32 can use hardened derivation paths
1139 function() {
1140 page.open(url, function(status) {
1141 // set the phrase
1142 var expected = "14aXZeprXAE3UUKQc4ihvwBvww2LuEoHo4";
1143 page.evaluate(function() {
1144 $(".phrase").val("abandon abandon ability");
1145 $(".phrase").trigger("input");
1146 });
1147 // change tabs
1148 waitForGenerate(function() {
1149 page.evaluate(function() {
1150 $("#bip32-tab a").click();
1151 });
1152 // set the derivation path to m/0'
1153 waitForGenerate(function() {
1154 page.evaluate(function() {
1155 $("#bip32 .path").val("m/0'");
1156 $("#bip32 .path").trigger("input");
1157 });
1158 // check the address is generated correctly
1159 waitForGenerate(function() {
1160 var actual = page.evaluate(function() {
1161 return $(".address:first").text();
1162 });
1163 if (actual != expected) {
1164 console.log("Hardened BIP32 path generates incorrect address");
1165 console.log("Expected: " + expected);
1166 console.log("Actual: " + actual);
1167 fail();
1168 }
1169 next();
1170 });
1171 });
1172 });
1173 });
1174 },
1175
1176 // BIP32 extended private key is shown
1177 function() {
1178 page.open(url, function(status) {
1179 // set the phrase
1180 var expected = "xprv9va99uTVE5aLiutUVLTyfxfe8v8aaXjSQ1XxZbK6SezYVuikA9MnjQVTA8rQHpNA5LKvyQBpLiHbBQiiccKiBDs7eRmBogsvq3THFeLHYbe";
1181 page.evaluate(function() {
1182 $(".phrase").val("abandon abandon ability");
1183 $(".phrase").trigger("input");
1184 });
1185 // change tabs
1186 waitForGenerate(function() {
1187 page.evaluate(function() {
1188 $("#bip32-tab a").click();
1189 });
1190 // check the extended private key is generated correctly
1191 waitForGenerate(function() {
1192 var actual = page.evaluate(function() {
1193 return $(".extended-priv-key").val();
1194 });
1195 if (actual != expected) {
1196 console.log("BIP32 extended private key is incorrect");
1197 console.log("Expected: " + expected);
1198 console.log("Actual: " + actual);
1199 fail();
1200 }
1201 next();
1202 });
1203 });
1204 });
1205 },
1206
1207 // BIP32 extended public key is shown
1208 function() {
1209 page.open(url, function(status) {
1210 // set the phrase
1211 var expected = "xpub69ZVZQzP4T8dwPxwbMzz36cNgwy4yzTHmETZMyihzzXXNi3thgg3HCow1RtY252wdw5rS8369xKnraN5Q93y3FkFfJp2XEHWUrkyXsjS93P";
1212 page.evaluate(function() {
1213 $(".phrase").val("abandon abandon ability");
1214 $(".phrase").trigger("input");
1215 });
1216 // change tabs
1217 waitForGenerate(function() {
1218 page.evaluate(function() {
1219 $("#bip32-tab a").click();
1220 });
1221 // check the extended public key is generated correctly
1222 waitForGenerate(function() {
1223 var actual = page.evaluate(function() {
1224 return $(".extended-pub-key").val();
1225 });
1226 if (actual != expected) {
1227 console.log("BIP32 extended public key is incorrect");
1228 console.log("Expected: " + expected);
1229 console.log("Actual: " + actual);
1230 fail();
1231 }
1232 next();
1233 });
1234 });
1235 });
1236 },
1237
1238 // Derivation path is shown in table
1239 function() {
1240 page.open(url, function(status) {
1241 // set the phrase
1242 var expected = "m/44'/0'/0'/0/0";
1243 page.evaluate(function() {
1244 $(".phrase").val("abandon abandon ability");
1245 $(".phrase").trigger("input");
1246 });
1247 // check for derivation path in table
1248 waitForGenerate(function() {
1249 var actual = page.evaluate(function() {
1250 return $(".index:first").text();
1251 });
1252 if (actual != expected) {
1253 console.log("Derivation path shown incorrectly in table");
1254 console.log("Expected: " + expected);
1255 console.log("Actual: " + actual);
1256 fail();
1257 }
1258 next();
1259 });
1260 });
1261 },
1262
1263 // Derivation path for address can be hardened
1264 function() {
1265 page.open(url, function(status) {
1266 // set the phrase
1267 var expected = "18exLzUv7kfpiXRzmCjFDoC9qwNLFyvwyd";
1268 page.evaluate(function() {
1269 $(".phrase").val("abandon abandon ability");
1270 $(".phrase").trigger("input");
1271 });
1272 // change tabs
1273 waitForGenerate(function() {
1274 page.evaluate(function() {
1275 $("#bip32-tab a").click();
1276 });
1277 waitForGenerate(function() {
1278 // select the hardened addresses option
1279 page.evaluate(function() {
1280 $(".hardened-addresses").prop("checked", true);
1281 $(".hardened-addresses").trigger("change");
1282 });
1283 waitForGenerate(function() {
1284 // check the generated address is hardened
1285 var actual = page.evaluate(function() {
1286 return $(".address:first").text();
1287 });
1288 if (actual != expected) {
1289 console.log("Hardened address is incorrect");
1290 console.log("Expected: " + expected);
1291 console.log("Actual: " + actual);
1292 fail();
1293 }
1294 next();
1295 });
1296 });
1297 });
1298 });
1299 },
1300
1301 // Derivation path visibility can be toggled
1302 function() {
1303 page.open(url, function(status) {
1304 // set the phrase
1305 page.evaluate(function() {
1306 $(".phrase").val("abandon abandon ability");
1307 $(".phrase").trigger("input");
1308 });
1309 waitForGenerate(function() {
1310 // toggle path visibility
1311 page.evaluate(function() {
1312 $(".index-toggle").click();
1313 });
1314 // check the path is not visible
1315 var isInvisible = page.evaluate(function() {
1316 return $(".index:first span").hasClass("invisible");
1317 });
1318 if (!isInvisible) {
1319 console.log("Toggled derivation path is visible");
1320 fail();
1321 }
1322 next();
1323 });
1324 });
1325 },
1326
1327 // Address is shown
1328 function() {
1329 page.open(url, function(status) {
1330 var expected = "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug";
1331 // set the phrase
1332 page.evaluate(function() {
1333 $(".phrase").val("abandon abandon ability").trigger("input");
1334 });
1335 // get the address
1336 waitForGenerate(function() {
1337 var actual = page.evaluate(function() {
1338 return $(".address:first").text();
1339 });
1340 if (actual != expected) {
1341 console.log("Address is not shown");
1342 console.log("Expected: " + expected);
1343 console.log("Got: " + actual);
1344 fail();
1345 }
1346 next();
1347 });
1348 });
1349 },
1350
1351 // Addresses are shown in order of derivation path
1352 function() {
1353 page.open(url, function(status) {
1354 // set the phrase
1355 page.evaluate(function() {
1356 $(".phrase").val("abandon abandon ability").trigger("input");
1357 });
1358 // get the derivation paths
1359 waitForGenerate(function() {
1360 var paths = page.evaluate(function() {
1361 return $(".index").map(function(i, e) {
1362 return $(e).text();
1363 });
1364 });
1365 if (paths.length != 20) {
1366 console.log("Total paths is less than expected: " + paths.length);
1367 fail();
1368 }
1369 for (var i=0; i<paths.length; i++) {
1370 var expected = "m/44'/0'/0'/0/" + i;
1371 var actual = paths[i];
1372 if (actual != expected) {
1373 console.log("Path " + i + " is incorrect");
1374 console.log("Expected: " + expected);
1375 console.log("Actual: " + actual);
1376 fail();
1377 }
1378 }
1379 next();
1380 });
1381 });
1382 },
1383
1384 // Address visibility can be toggled
1385 function() {
1386 page.open(url, function(status) {
1387 // set the phrase
1388 page.evaluate(function() {
1389 $(".phrase").val("abandon abandon ability");
1390 $(".phrase").trigger("input");
1391 });
1392 waitForGenerate(function() {
1393 // toggle address visibility
1394 page.evaluate(function() {
1395 $(".address-toggle").click();
1396 });
1397 // check the address is not visible
1398 var isInvisible = page.evaluate(function() {
1399 return $(".address:first span").hasClass("invisible");
1400 });
1401 if (!isInvisible) {
1402 console.log("Toggled address is visible");
1403 fail();
1404 }
1405 next();
1406 });
1407 });
1408 },
1409
1410 // Public key is shown
1411 function() {
1412 page.open(url, function(status) {
1413 var expected = "033f5aed5f6cfbafaf223188095b5980814897295f723815fea5d3f4b648d0d0b3";
1414 // set the phrase
1415 page.evaluate(function() {
1416 $(".phrase").val("abandon abandon ability").trigger("input");
1417 });
1418 // get the address
1419 waitForGenerate(function() {
1420 var actual = page.evaluate(function() {
1421 return $(".pubkey:first").text();
1422 });
1423 if (actual != expected) {
1424 console.log("Public key is not shown");
1425 console.log("Expected: " + expected);
1426 console.log("Got: " + actual);
1427 fail();
1428 }
1429 next();
1430 });
1431 });
1432 },
1433
1434 // Public key visibility can be toggled
1435 function() {
1436 page.open(url, function(status) {
1437 // set the phrase
1438 page.evaluate(function() {
1439 $(".phrase").val("abandon abandon ability");
1440 $(".phrase").trigger("input");
1441 });
1442 waitForGenerate(function() {
1443 // toggle public key visibility
1444 page.evaluate(function() {
1445 $(".public-key-toggle").click();
1446 });
1447 // check the public key is not visible
1448 var isInvisible = page.evaluate(function() {
1449 return $(".pubkey:first span").hasClass("invisible");
1450 });
1451 if (!isInvisible) {
1452 console.log("Toggled public key is visible");
1453 fail();
1454 }
1455 next();
1456 });
1457 });
1458 },
1459
1460 // Private key is shown
1461 function() {
1462 page.open(url, function(status) {
1463 var expected = "L26cVSpWFkJ6aQkPkKmTzLqTdLJ923e6CzrVh9cmx21QHsoUmrEE";
1464 // set the phrase
1465 page.evaluate(function() {
1466 $(".phrase").val("abandon abandon ability").trigger("input");
1467 });
1468 // get the address
1469 waitForGenerate(function() {
1470 var actual = page.evaluate(function() {
1471 return $(".privkey:first").text();
1472 });
1473 if (actual != expected) {
1474 console.log("Private key is not shown");
1475 console.log("Expected: " + expected);
1476 console.log("Got: " + actual);
1477 fail();
1478 }
1479 next();
1480 });
1481 });
1482 },
1483
1484 // Private key visibility can be toggled
1485 function() {
1486 page.open(url, function(status) {
1487 // set the phrase
1488 page.evaluate(function() {
1489 $(".phrase").val("abandon abandon ability");
1490 $(".phrase").trigger("input");
1491 });
1492 waitForGenerate(function() {
1493 // toggle private key visibility
1494 page.evaluate(function() {
1495 $(".private-key-toggle").click();
1496 });
1497 // check the private key is not visible
1498 var isInvisible = page.evaluate(function() {
1499 return $(".privkey:first span").hasClass("invisible");
1500 });
1501 if (!isInvisible) {
1502 console.log("Toggled private key is visible");
1503 fail();
1504 }
1505 next();
1506 });
1507 });
1508 },
1509
1510 // More addresses can be generated
1511 function() {
1512 page.open(url, function(status) {
1513 // set the phrase
1514 page.evaluate(function() {
1515 $(".phrase").val("abandon abandon ability");
1516 $(".phrase").trigger("input");
1517 });
1518 waitForGenerate(function() {
1519 // generate more addresses
1520 page.evaluate(function() {
1521 $(".more").click();
1522 });
1523 waitForGenerate(function() {
1524 // check there are more addresses
1525 var addressCount = page.evaluate(function() {
1526 return $(".address").length;
1527 });
1528 if (addressCount != 40) {
1529 console.log("More addresses cannot be generated");
1530 fail();
1531 }
1532 next();
1533 });
1534 });
1535 });
1536 },
1537
1538 // A custom number of additional addresses can be generated
1539 function() {
1540 page.open(url, function(status) {
1541 // set the phrase
1542 page.evaluate(function() {
1543 $(".phrase").val("abandon abandon ability");
1544 $(".phrase").trigger("input");
1545 });
1546 waitForGenerate(function() {
1547 // get the current number of addresses
1548 var oldAddressCount = page.evaluate(function() {
1549 return $(".address").length;
1550 });
1551 // set a custom number of additional addresses
1552 page.evaluate(function() {
1553 $(".rows-to-add").val(1);
1554 });
1555 // generate more addresses
1556 page.evaluate(function() {
1557 $(".more").click();
1558 });
1559 waitForGenerate(function() {
1560 // check there are the correct number of addresses
1561 var newAddressCount = page.evaluate(function() {
1562 return $(".address").length;
1563 });
1564 if (newAddressCount - oldAddressCount != 1) {
1565 console.log("Number of additional addresses cannot be customized");
1566 console.log(newAddressCount)
1567 console.log(oldAddressCount)
1568 fail();
1569 }
1570 next();
1571 });
1572 });
1573 });
1574 },
1575
1576 // Additional addresses are shown in order of derivation path
1577 function() {
1578 page.open(url, function(status) {
1579 // set the phrase
1580 page.evaluate(function() {
1581 $(".phrase").val("abandon abandon ability").trigger("input");
1582 });
1583 waitForGenerate(function() {
1584 // generate more addresses
1585 page.evaluate(function() {
1586 $(".more").click();
1587 });
1588 // get the derivation paths
1589 waitForGenerate(function() {
1590 var paths = page.evaluate(function() {
1591 return $(".index").map(function(i, e) {
1592 return $(e).text();
1593 });
1594 });
1595 if (paths.length != 40) {
1596 console.log("Total additional paths is less than expected: " + paths.length);
1597 fail();
1598 }
1599 for (var i=0; i<paths.length; i++) {
1600 var expected = "m/44'/0'/0'/0/" + i;
1601 var actual = paths[i];
1602 if (actual != expected) {
1603 console.log("Path " + i + " is not in correct order");
1604 console.log("Expected: " + expected);
1605 console.log("Actual: " + actual);
1606 fail();
1607 }
1608 }
1609 next();
1610 });
1611 });
1612 });
1613 },
1614
1615 // BIP32 root key can be set by the user
1616 function() {
1617 page.open(url, function(status) {
1618 var expected = "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug";
1619 // set the root key
1620 page.evaluate(function() {
1621 $(".root-key").val("xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpi").trigger("input");
1622 });
1623 waitForGenerate(function() {
1624 var actual = page.evaluate(function() {
1625 return $(".address:first").text();
1626 });
1627 if (actual != expected) {
1628 console.log("Setting BIP32 root key results in wrong address");
1629 console.log("Expected: " + expected);
1630 console.log("Actual: " + actual);
1631 fail();
1632 }
1633 next();
1634 });
1635 });
1636 },
1637
1638 // Setting BIP32 root key clears the existing phrase, passphrase and seed
1639 function() {
1640 page.open(url, function(status) {
1641 var expected = "";
1642 // set a mnemonic
1643 page.evaluate(function() {
1644 $(".phrase").val("A non-blank but invalid value");
1645 });
1646 // Accept any confirm dialogs
1647 page.onConfirm = function() {
1648 return true;
1649 };
1650 // set the root key
1651 page.evaluate(function() {
1652 $(".root-key").val("xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpi").trigger("input");
1653 });
1654 waitForGenerate(function() {
1655 var actual = page.evaluate(function() {
1656 return $(".phrase").val();
1657 });
1658 if (actual != expected) {
1659 console.log("Phrase not cleared when setting BIP32 root key");
1660 console.log("Expected: " + expected);
1661 console.log("Actual: " + actual);
1662 fail();
1663 }
1664 next();
1665 });
1666 });
1667 },
1668
1669 // Clearing of phrase, passphrase and seed can be cancelled by user
1670 function() {
1671 page.open(url, function(status) {
1672 var expected = "abandon abandon ability";
1673 // set a mnemonic
1674 page.evaluate(function() {
1675 $(".phrase").val("abandon abandon ability");
1676 });
1677 // Cancel any confirm dialogs
1678 page.onConfirm = function() {
1679 return false;
1680 };
1681 // set the root key
1682 page.evaluate(function() {
1683 $(".root-key").val("xprv9s21ZrQH143K3d3vzEDD3KpSKmxsZ3y7CqhAL1tinwtP6wqK4TKEKjpBuo6P2hUhB6ZENo7TTSRytiP857hBZVpBdk8PooFuRspE1eywwNZ").trigger("input");
1684 });
1685 var actual = page.evaluate(function() {
1686 return $(".phrase").val();
1687 });
1688 if (actual != expected) {
1689 console.log("Phrase not retained when cancelling changes to BIP32 root key");
1690 console.log("Expected: " + expected);
1691 console.log("Actual: " + actual);
1692 fail();
1693 }
1694 next();
1695 });
1696 },
1697
1698 // Custom BIP32 root key is used when changing the derivation path
1699 function() {
1700 page.open(url, function(status) {
1701 var expected = "1Nq2Wmu726XHCuGhctEtGmhxo3wzk5wZ1H";
1702 // set the root key
1703 page.evaluate(function() {
1704 $(".root-key").val("xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpi").trigger("input");
1705 });
1706 waitForGenerate(function() {
1707 // change the derivation path
1708 page.evaluate(function() {
1709 $("#account").val("1").trigger("input");
1710 });
1711 // check the bip32 root key is used for derivation, not the blank phrase
1712 waitForGenerate(function() {
1713 var actual = page.evaluate(function() {
1714 return $(".address:first").text();
1715 });
1716 if (actual != expected) {
1717 console.log("Changing the derivation path does not use BIP32 root key");
1718 console.log("Expected: " + expected);
1719 console.log("Actual: " + actual);
1720 fail();
1721 }
1722 next();
1723 });
1724 });
1725 });
1726 },
1727
1728 // Incorrect mnemonic shows error
1729 function() {
1730 page.open(url, function(status) {
1731 // set the root key
1732 page.evaluate(function() {
1733 $(".phrase").val("abandon abandon abandon").trigger("input");
1734 });
1735 waitForFeedback(function() {
1736 // check there is an error shown
1737 var feedback = page.evaluate(function() {
1738 return $(".feedback").text();
1739 });
1740 if (feedback.length <= 0) {
1741 console.log("Invalid mnemonic does not show error");
1742 fail();
1743 }
1744 next();
1745 });
1746 });
1747 },
1748
1749 // Incorrect word shows suggested replacement
1750 function() {
1751 page.open(url, function(status) {
1752 // set the root key
1753 page.evaluate(function() {
1754 $(".phrase").val("abandon abandon abiliti").trigger("input");
1755 });
1756 // check there is a suggestion shown
1757 waitForFeedback(function() {
1758 var feedback = page.evaluate(function() {
1759 return $(".feedback").text();
1760 });
1761 if (feedback.indexOf("did you mean ability?") < 0) {
1762 console.log("Incorrect word does not show suggested replacement");
1763 console.log("Error: " + error);
1764 fail();
1765 }
1766 next();
1767 });
1768 });
1769 },
1770
1771 // Github pull request 48
1772 // First four letters of word shows that word, not closest
1773 // since first four letters gives unique word in BIP39 wordlist
1774 // eg ille should show illegal, not idle
1775 function() {
1776 page.open(url, function(status) {
1777 // set the incomplete word
1778 page.evaluate(function() {
1779 $(".phrase").val("ille").trigger("input");
1780 });
1781 // check there is a suggestion shown
1782 waitForFeedback(function() {
1783 var feedback = page.evaluate(function() {
1784 return $(".feedback").text();
1785 });
1786 if (feedback.indexOf("did you mean illegal?") < 0) {
1787 console.log("Start of word does not show correct suggestion");
1788 console.log("Error: " + error);
1789 fail();
1790 }
1791 next();
1792 });
1793 });
1794 },
1795
1796 // Incorrect BIP32 root key shows error
1797 function() {
1798 page.open(url, function(status) {
1799 // set the root key
1800 page.evaluate(function() {
1801 $(".root-key").val("xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpj").trigger("input");
1802 });
1803 // check there is an error shown
1804 waitForFeedback(function() {
1805 var feedback = page.evaluate(function() {
1806 return $(".feedback").text();
1807 });
1808 if (feedback != "Invalid root key") {
1809 console.log("Invalid root key does not show error");
1810 console.log("Error: " + error);
1811 fail();
1812 }
1813 next();
1814 });
1815 });
1816 },
1817
1818 // Derivation path not starting with m shows error
1819 function() {
1820 page.open(url, function(status) {
1821 // set the mnemonic phrase
1822 page.evaluate(function() {
1823 $(".phrase").val("abandon abandon ability").trigger("input");
1824 });
1825 waitForGenerate(function() {
1826 // select the bip32 tab so custom derivation path can be set
1827 page.evaluate(function() {
1828 $("#bip32-tab a").click();
1829 });
1830 waitForGenerate(function() {
1831 // set the incorrect derivation path
1832 page.evaluate(function() {
1833 $("#bip32 .path").val("n/0").trigger("input");
1834 });
1835 waitForFeedback(function() {
1836 var feedback = page.evaluate(function() {
1837 return $(".feedback").text();
1838 });
1839 if (feedback != "First character must be 'm'") {
1840 console.log("Derivation path not starting with m should show error");
1841 console.log("Error: " + error);
1842 fail();
1843 }
1844 next();
1845 });
1846 });
1847 });
1848 });
1849 },
1850
1851 // Derivation path containing invalid characters shows useful error
1852 function() {
1853 page.open(url, function(status) {
1854 // set the mnemonic phrase
1855 page.evaluate(function() {
1856 $(".phrase").val("abandon abandon ability").trigger("input");
1857 });
1858 waitForGenerate(function() {
1859 // select the bip32 tab so custom derivation path can be set
1860 page.evaluate(function() {
1861 $("#bip32-tab a").click();
1862 });
1863 waitForGenerate(function() {
1864 // set the incorrect derivation path
1865 page.evaluate(function() {
1866 $("#bip32 .path").val("m/1/0wrong1/1").trigger("input");
1867 });
1868 waitForFeedback(function() {
1869 var feedback = page.evaluate(function() {
1870 return $(".feedback").text();
1871 });
1872 if (feedback != "Invalid characters 0wrong1 found at depth 2") {
1873 console.log("Derivation path with invalid characters should show error");
1874 console.log("Error: " + error);
1875 fail();
1876 }
1877 next();
1878 });
1879 });
1880 });
1881 });
1882 },
1883
1884 // Github Issue 11: Default word length is 15
1885 // https://github.com/iancoleman/bip39/issues/11
1886 function() {
1887 page.open(url, function(status) {
1888 // get the word length
1889 var defaultLength = page.evaluate(function() {
1890 return $(".strength").val();
1891 });
1892 if (defaultLength != 15) {
1893 console.log("Default word length is not 15");
1894 fail();
1895 }
1896 next();
1897 });
1898 },
1899
1900
1901 // Github Issue 12: Generate more rows with private keys hidden
1902 // https://github.com/iancoleman/bip39/issues/12
1903 function() {
1904 page.open(url, function(status) {
1905 // set the phrase
1906 page.evaluate(function() {
1907 $(".phrase").val("abandon abandon ability");
1908 $(".phrase").trigger("input");
1909 });
1910 waitForGenerate(function() {
1911 // toggle private keys hidden, then generate more addresses
1912 page.evaluate(function() {
1913 $(".private-key-toggle").click();
1914 $(".more").click();
1915 });
1916 waitForGenerate(function() {
1917 // check more have been generated
1918 var expected = 40;
1919 var numPrivKeys = page.evaluate(function() {
1920 return $(".privkey").length;
1921 });
1922 if (numPrivKeys != expected) {
1923 console.log("Wrong number of addresses when clicking 'more' with hidden privkeys");
1924 console.log("Expected: " + expected);
1925 console.log("Actual: " + numPrivKeys);
1926 fail();
1927 }
1928 // check no private keys are shown
1929 var numHiddenPrivKeys = page.evaluate(function() {
1930 return $(".privkey span[class=invisible]").length;
1931 });
1932 if (numHiddenPrivKeys != expected) {
1933 console.log("Generating more does not retain hidden state of privkeys");
1934 console.log("Expected: " + expected);
1935 console.log("Actual: " + numHiddenPrivKeys);
1936 fail();
1937 }
1938 next();
1939 });
1940 });
1941 });
1942 },
1943
1944 // Github Issue 19: Mnemonic is not sensitive to whitespace
1945 // https://github.com/iancoleman/bip39/issues/19
1946 function() {
1947 page.open(url, function(status) {
1948 // set the phrase
1949 var expected = "xprv9s21ZrQH143K3isaZsWbKVoTtbvd34Y1ZGRugGdMeBGbM3AgBVzTH159mj1cbbtYSJtQr65w6L5xy5L9SFC7c9VJZWHxgAzpj4mun5LhrbC";
1950 page.evaluate(function() {
1951 var doubleSpace = " ";
1952 $(".phrase").val("urge cat" + doubleSpace + "bid");
1953 $(".phrase").trigger("input");
1954 });
1955 waitForGenerate(function() {
1956 // Check the bip32 root key is correct
1957 var actual = page.evaluate(function() {
1958 return $(".root-key").val();
1959 });
1960 if (actual != expected) {
1961 console.log("Mnemonic is sensitive to whitespace");
1962 console.log("Expected: " + expected);
1963 console.log("Actual: " + actual);
1964 fail();
1965 }
1966 next();
1967 });
1968 });
1969 },
1970
1971 // Github Issue 23: Part 1: Use correct derivation path when changing tabs
1972 // https://github.com/iancoleman/bip39/issues/23
1973 function() {
1974 page.open(url, function(status) {
1975 // 1) and 2) set the phrase
1976 page.evaluate(function() {
1977 $(".phrase").val("abandon abandon ability").trigger("input");
1978 });
1979 waitForGenerate(function() {
1980 // 3) select bip32 tab
1981 page.evaluate(function() {
1982 $("#bip32-tab a").click();
1983 });
1984 waitForGenerate(function() {
1985 // 4) switch from bitcoin to litecoin
1986 page.evaluate(function() {
1987 $(".network option").filter(function() {
1988 return $(this).html() == "Litecoin";
1989 }).prop("selected", true);
1990 $(".network").trigger("change");
1991 });
1992 waitForGenerate(function() {
1993 // 5) Check derivation path is displayed correctly
1994 var expected = "m/0/0";
1995 var actual = page.evaluate(function() {
1996 return $(".index:first").text();
1997 });
1998 if (actual != expected) {
1999 console.log("Github Issue 23 Part 1: derivation path display error");
2000 console.log("Expected: " + expected);
2001 console.log("Actual: " + actual);
2002 fail();
2003 }
2004 // 5) Check address is displayed correctly
2005 var expected = "LS8MP5LZ5AdzSZveRrjm3aYVoPgnfFh5T5";
2006 var actual = page.evaluate(function() {
2007 return $(".address:first").text();
2008 });
2009 if (actual != expected) {
2010 console.log("Github Issue 23 Part 1: address display error");
2011 console.log("Expected: " + expected);
2012 console.log("Actual: " + actual);
2013 fail();
2014 }
2015 next();
2016 });
2017 });
2018 });
2019 });
2020 },
2021
2022 // Github Issue 23 Part 2: Coin selection in derivation path
2023 // https://github.com/iancoleman/bip39/issues/23#issuecomment-238011920
2024 function() {
2025 page.open(url, function(status) {
2026 // set the phrase
2027 page.evaluate(function() {
2028 $(".phrase").val("abandon abandon ability").trigger("input");
2029 });
2030 waitForGenerate(function() {
2031 // switch from bitcoin to clam
2032 page.evaluate(function() {
2033 $(".network option").filter(function() {
2034 return $(this).html() == "CLAM";
2035 }).prop("selected", true);
2036 $(".network").trigger("change");
2037 });
2038 waitForGenerate(function() {
2039 // check derivation path is displayed correctly
2040 var expected = "m/44'/23'/0'/0/0";
2041 var actual = page.evaluate(function() {
2042 return $(".index:first").text();
2043 });
2044 if (actual != expected) {
2045 console.log("Github Issue 23 Part 2: Coin in BIP44 derivation path is incorrect");
2046 console.log("Expected: " + expected);
2047 console.log("Actual: " + actual);
2048 fail();
2049 }
2050 next();
2051 });
2052 });
2053 });
2054 },
2055
2056 // Github Issue 26: When using a Root key derrived altcoins are incorrect
2057 // https://github.com/iancoleman/bip39/issues/26
2058 function() {
2059 page.open(url, function(status) {
2060 // 1) 2) and 3) set the root key
2061 page.evaluate(function() {
2062 $(".root-key").val("xprv9s21ZrQH143K2jkGDCeTLgRewT9F2pH5JZs2zDmmjXes34geVnFiuNa8KTvY5WoYvdn4Ag6oYRoB6cXtc43NgJAEqDXf51xPm6fhiMCKwpi").trigger("input");
2063 });
2064 waitForGenerate(function() {
2065 // 4) switch from bitcoin to viacoin
2066 page.evaluate(function() {
2067 $(".network option").filter(function() {
2068 return $(this).html() == "Viacoin";
2069 }).prop("selected", true);
2070 $(".network").trigger("change");
2071 });
2072 waitForGenerate(function() {
2073 // 5) ensure the derived address is correct
2074 var expected = "Vq9Eq4N5SQnjqZvxtxzo7hZPW5XnyJsmXT";
2075 var actual = page.evaluate(function() {
2076 return $(".address:first").text();
2077 });
2078 if (actual != expected) {
2079 console.log("Github Issue 26: address is incorrect when changing networks and using root-key to derive");
2080 console.log("Expected: " + expected);
2081 console.log("Actual: " + actual);
2082 fail();
2083 }
2084 next();
2085 });
2086 });
2087 });
2088 },
2089
2090 // Selecting a language with no existing phrase should generate a phrase in
2091 // that language.
2092 function() {
2093 page.open(url, function(status) {
2094 // Select a language
2095 // Need to manually simulate hash being set due to quirk between
2096 // 'click' event triggered by javascript vs triggered by mouse.
2097 // Perhaps look into page.sendEvent
2098 // http://phantomjs.org/api/webpage/method/send-event.html
2099 page.evaluate(function() {
2100 window.location.hash = "#japanese";
2101 $("a[href='#japanese']").trigger("click");
2102 });
2103 waitForGenerate(function() {
2104 // Check the mnemonic is in Japanese
2105 var phrase = page.evaluate(function() {
2106 return $(".phrase").val();
2107 });
2108 if (phrase.length <= 0) {
2109 console.log("No Japanese phrase generated");
2110 fail();
2111 }
2112 if (phrase.charCodeAt(0) < 128) {
2113 console.log("First character of Japanese phrase is ascii");
2114 console.log("Phrase: " + phrase);
2115 fail();
2116 }
2117 next();
2118 });
2119 });
2120 },
2121
2122 // Selecting a language with existing phrase should update the phrase to use
2123 // that language.
2124 function() {
2125 page.open(url, function(status) {
2126 // Set the phrase to an English phrase.
2127 page.evaluate(function() {
2128 $(".phrase").val("abandon abandon ability").trigger("input");
2129 });
2130 waitForGenerate(function() {
2131 // Change to Italian
2132 // Need to manually simulate hash being set due to quirk between
2133 // 'click' event triggered by javascript vs triggered by mouse.
2134 // Perhaps look into page.sendEvent
2135 // http://phantomjs.org/api/webpage/method/send-event.html
2136 page.evaluate(function() {
2137 window.location.hash = "#italian";
2138 $("a[href='#italian']").trigger("click");
2139 });
2140 waitForGenerate(function() {
2141 // Check only the language changes, not the phrase
2142 var expected = "abaco abaco abbaglio";
2143 var actual = page.evaluate(function() {
2144 return $(".phrase").val();
2145 });
2146 if (actual != expected) {
2147 console.log("Changing language with existing phrase");
2148 console.log("Expected: " + expected);
2149 console.log("Actual: " + actual);
2150 fail();
2151 }
2152 // Check the address is correct
2153 var expected = "1Dz5TgDhdki9spa6xbPFbBqv5sjMrx3xgV";
2154 var actual = page.evaluate(function() {
2155 return $(".address:first").text();
2156 });
2157 if (actual != expected) {
2158 console.log("Changing language generates incorrect address");
2159 console.log("Expected: " + expected);
2160 console.log("Actual: " + actual);
2161 fail();
2162 }
2163 next();
2164 });
2165 });
2166 });
2167 },
2168
2169 // Suggested replacement for erroneous word in non-English language
2170 function() {
2171 page.open(url, function(status) {
2172 // Set an incorrect phrase in Italian
2173 page.evaluate(function() {
2174 $(".phrase").val("abaco abaco zbbaglio").trigger("input");
2175 });
2176 waitForFeedback(function() {
2177 // Check the suggestion is correct
2178 var feedback = page.evaluate(function() {
2179 return $(".feedback").text();
2180 });
2181 if (feedback.indexOf("did you mean abbaglio?") < 0) {
2182 console.log("Incorrect Italian word does not show suggested replacement");
2183 console.log("Error: " + error);
2184 fail();
2185 }
2186 next();
2187 });
2188 });
2189 },
2190
2191
2192 // Japanese word does not break across lines.
2193 // Point 2 from
2194 // https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md#japanese
2195 function() {
2196 page.open(url, function(status) {
2197 hasWordBreakCss = page.content.indexOf("word-break: keep-all;") > -1;
2198 if (!hasWordBreakCss) {
2199 console.log("Japanese words can break across lines mid-word");
2200 console.log("Check CSS for '.phrase { word-break: keep-all; }'");
2201 fail();
2202 }
2203 // Run the next test
2204 next();
2205 });
2206 },
2207
2208 // Language can be specified at page load using hash value in url
2209 function() {
2210 page.open(url, function(status) {
2211 // Set the page hash as if it were on a fresh page load
2212 page.evaluate(function() {
2213 window.location.hash = "#japanese";
2214 });
2215 // Generate a random phrase
2216 page.evaluate(function() {
2217 $(".generate").trigger("click");
2218 });
2219 waitForGenerate(function() {
2220 // Check the phrase is in Japanese
2221 var phrase = page.evaluate(function() {
2222 return $(".phrase").val();
2223 });
2224 if (phrase.length <= 0) {
2225 console.log("No phrase generated using url hash");
2226 fail();
2227 }
2228 if (phrase.charCodeAt(0) < 128) {
2229 console.log("Language not detected from url hash on page load.");
2230 console.log("Phrase: " + phrase);
2231 fail();
2232 }
2233 next();
2234 });
2235 });
2236 },
2237
2238 // Entropy unit tests
2239 function() {
2240 page.open(url, function(status) {
2241 var response = page.evaluate(function() {
2242 var e;
2243 // binary entropy is detected
2244 try {
2245 e = Entropy.fromString("01010101");
2246 if (e.base.str != "binary") {
2247 return "Binary entropy not detected correctly";
2248 }
2249 }
2250 catch (e) {
2251 return e.message;
2252 }
2253 // base6 entropy is detected
2254 try {
2255 e = Entropy.fromString("012345012345");
2256 if (e.base.str != "base 6") {
2257 return "base6 entropy not detected correctly";
2258 }
2259 }
2260 catch (e) {
2261 return e.message;
2262 }
2263 // dice entropy is detected
2264 try {
2265 e = Entropy.fromString("123456123456");
2266 if (e.base.str != "base 6 (dice)") {
2267 return "dice entropy not detected correctly";
2268 }
2269 }
2270 catch (e) {
2271 return e.message;
2272 }
2273 // base10 entropy is detected
2274 try {
2275 e = Entropy.fromString("0123456789");
2276 if (e.base.str != "base 10") {
2277 return "base10 entropy not detected correctly";
2278 }
2279 }
2280 catch (e) {
2281 return e.message;
2282 }
2283 // hex entropy is detected
2284 try {
2285 e = Entropy.fromString("0123456789ABCDEF");
2286 if (e.base.str != "hexadecimal") {
2287 return "hexadecimal entropy not detected correctly";
2288 }
2289 }
2290 catch (e) {
2291 return e.message;
2292 }
2293 // card entropy is detected
2294 try {
2295 e = Entropy.fromString("AC4DTHKS");
2296 if (e.base.str != "card") {
2297 return "card entropy not detected correctly";
2298 }
2299 }
2300 catch (e) {
2301 return e.message;
2302 }
2303 // entropy is case insensitive
2304 try {
2305 e = Entropy.fromString("aBcDeF");
2306 if (e.cleanStr != "aBcDeF") {
2307 return "Entropy should not be case sensitive";
2308 }
2309 }
2310 catch (e) {
2311 return e.message;
2312 }
2313 // dice entropy is converted to base6
2314 try {
2315 e = Entropy.fromString("123456");
2316 if (e.cleanStr != "123450") {
2317 return "Dice entropy is not automatically converted to base6";
2318 }
2319 }
2320 catch (e) {
2321 return e.message;
2322 }
2323 // dice entropy is preferred to base6 if ambiguous
2324 try {
2325 e = Entropy.fromString("12345");
2326 if (e.base.str != "base 6 (dice)") {
2327 return "dice not used as default over base 6";
2328 }
2329 }
2330 catch (e) {
2331 return e.message;
2332 }
2333 // unused characters are ignored
2334 try {
2335 e = Entropy.fromString("fghijkl");
2336 if (e.cleanStr != "f") {
2337 return "additional characters are not ignored";
2338 }
2339 }
2340 catch (e) {
2341 return e.message;
2342 }
2343 // the lowest base is used by default
2344 // 7 could be decimal or hexadecimal, but should be detected as decimal
2345 try {
2346 e = Entropy.fromString("7");
2347 if (e.base.str != "base 10") {
2348 return "lowest base is not used";
2349 }
2350 }
2351 catch (e) {
2352 return e.message;
2353 }
2354 // Leading zeros are retained
2355 try {
2356 e = Entropy.fromString("000A");
2357 if (e.cleanStr != "000A") {
2358 return "Leading zeros are not retained";
2359 }
2360 }
2361 catch (e) {
2362 return e.message;
2363 }
2364 // Leading zeros are correctly preserved for hex in binary string
2365 try {
2366 e = Entropy.fromString("2A");
2367 if (e.binaryStr != "00101010") {
2368 return "Hex leading zeros are not correct in binary";
2369 }
2370 }
2371 catch (e) {
2372 return e.message;
2373 }
2374 // Leading zeros for base 6 as binary string
2375 // 20 = 2 events at 2.58 bits per event = 5 bits
2376 // 20 in base 6 = 12 in base 10 = 1100 in base 2
2377 // so it needs 1 bit of padding to be the right bit length
2378 try {
2379 e = Entropy.fromString("20");
2380 if (e.binaryStr != "01100") {
2381 return "Base 6 as binary has leading zeros";
2382 }
2383 }
2384 catch (e) {
2385 return e.message;
2386 }
2387 // Leading zeros for base 10 as binary string
2388 try {
2389 e = Entropy.fromString("17");
2390 if (e.binaryStr != "010001") {
2391 return "Base 10 as binary has leading zeros";
2392 }
2393 }
2394 catch (e) {
2395 return e.message;
2396 }
2397 // Leading zeros for card entropy as binary string.
2398 // Card entropy is hashed so 2c does not necessarily produce leading zeros.
2399 try {
2400 e = Entropy.fromString("2c");
2401 if (e.binaryStr != "0010") {
2402 return "Card entropy as binary has leading zeros";
2403 }
2404 }
2405 catch (e) {
2406 return e.message;
2407 }
2408 // Keyboard mashing results in weak entropy
2409 // Despite being a long string, it's less than 30 bits of entropy
2410 try {
2411 e = Entropy.fromString("aj;se ifj; ask,dfv js;ifj");
2412 if (e.binaryStr.length >= 30) {
2413 return "Keyboard mashing should produce weak entropy";
2414 }
2415 }
2416 catch (e) {
2417 return e.message;
2418 }
2419 // Card entropy is used if every pair could be a card
2420 try {
2421 e = Entropy.fromString("4c3c2c");
2422 if (e.base.str != "card") {
2423 return "Card entropy not used if all pairs are cards";
2424 }
2425 }
2426 catch (e) {
2427 return e.message;
2428 }
2429 // Card entropy uses base 52
2430 // [ cards, binary ]
2431 try {
2432 var cards = [
2433 [ "ac", "0101" ],
2434 [ "acqs", "11011100" ],
2435 [ "acks", "01011100" ],
2436 [ "2cac", "11111000" ],
2437 [ "2c", "0010" ],
2438 [ "3d", "0001" ],
2439 [ "4h", "1001" ],
2440 [ "5s", "1001" ],
2441 [ "6c", "0000" ],
2442 [ "7d", "0001" ],
2443 [ "8h", "1011" ],
2444 [ "9s", "0010" ],
2445 [ "tc", "1001" ],
2446 [ "jd", "1111" ],
2447 [ "qh", "0010" ],
2448 [ "ks", "0101" ],
2449 [ "ks2c", "01010100" ],
2450 [ "KS2C", "01010100" ],
2451 ];
2452 for (var i=0; i<cards.length; i++) {
2453 var card = cards[i][0];
2454 var result = cards[i][1];
2455 e = Entropy.fromString(card);
2456 console.log(e.binary + " " + result);
2457 if (e.binaryStr !== result) {
2458 return "card entropy " + card + " not parsed correctly: " + result + " != " + e.binaryStr;
2459 }
2460 }
2461 }
2462 catch (e) {
2463 return e.message;
2464 }
2465 return "PASS";
2466 });
2467 if (response != "PASS") {
2468 console.log("Entropy unit tests");
2469 console.log(response);
2470 fail();
2471 };
2472 next();
2473 });
2474 },
2475
2476 // Entropy can be entered by the user
2477 function() {
2478 page.open(url, function(status) {
2479 expected = {
2480 mnemonic: "abandon abandon ability",
2481 address: "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug",
2482 }
2483 // use entropy
2484 page.evaluate(function() {
2485 $(".use-entropy").prop("checked", true).trigger("change");
2486 $(".entropy").val("00000000 00000000 00000000 00000000").trigger("input");
2487 });
2488 // check the mnemonic is set and address is correct
2489 waitForGenerate(function() {
2490 var actual = page.evaluate(function() {
2491 return {
2492 address: $(".address:first").text(),
2493 mnemonic: $(".phrase").val(),
2494 }
2495 });
2496 if (actual.mnemonic != expected.mnemonic) {
2497 console.log("Entropy does not generate correct mnemonic");
2498 console.log("Expected: " + expected.mnemonic);
2499 console.log("Got: " + actual.mnemonic);
2500 fail();
2501 }
2502 if (actual.address != expected.address) {
2503 console.log("Entropy does not generate correct address");
2504 console.log("Expected: " + expected.address);
2505 console.log("Got: " + actual.address);
2506 fail();
2507 }
2508 next();
2509 });
2510 });
2511 },
2512
2513 // A warning about entropy is shown to the user, with additional information
2514 function() {
2515 page.open(url, function(status) {
2516 // get text content from entropy sections of page
2517 var hasWarning = page.evaluate(function() {
2518 var entropyText = $(".entropy-container").text();
2519 var warning = "mnemonic may be insecure";
2520 if (entropyText.indexOf(warning) == -1) {
2521 return false;
2522 }
2523 var readMoreText = $("#entropy-notes").parent().text();
2524 var goodSources = "flipping a fair coin, rolling a fair dice, noise measurements etc";
2525 if (readMoreText.indexOf(goodSources) == -1) {
2526 return false;
2527 }
2528 return true;
2529 });
2530 // check the warnings and information are shown
2531 if (!hasWarning) {
2532 console.log("Page does not contain warning about using own entropy");
2533 fail();
2534 }
2535 next();
2536 });
2537 },
2538
2539 // The types of entropy available are described to the user
2540 function() {
2541 page.open(url, function(status) {
2542 // get placeholder text for entropy field
2543 var placeholder = page.evaluate(function() {
2544 return $(".entropy").attr("placeholder");
2545 });
2546 var options = [
2547 "binary",
2548 "base 6",
2549 "dice",
2550 "base 10",
2551 "hexadecimal",
2552 "cards",
2553 ];
2554 for (var i=0; i<options.length; i++) {
2555 var option = options[i];
2556 if (placeholder.indexOf(option) == -1) {
2557 console.log("Available entropy type is not shown to user: " + option);
2558 fail();
2559 }
2560 }
2561 next();
2562 });
2563 },
2564
2565 // The actual entropy used is shown to the user
2566 function() {
2567 page.open(url, function(status) {
2568 // use entropy
2569 var badEntropySource = page.evaluate(function() {
2570 var entropy = "Not A Very Good Entropy Source At All";
2571 $(".use-entropy").prop("checked", true).trigger("change");
2572 $(".entropy").val(entropy).trigger("input");
2573 });
2574 // check the actual entropy being used is shown
2575 waitForEntropyFeedback(function() {
2576 var expectedText = "AedEceAA";
2577 var entropyText = page.evaluate(function() {
2578 return $(".entropy-container").text();
2579 });
2580 if (entropyText.indexOf(expectedText) == -1) {
2581 console.log("Actual entropy used is not shown");
2582 fail();
2583 }
2584 next();
2585 });
2586 });
2587 },
2588
2589 // Binary entropy can be entered
2590 function() {
2591 page.open(url, function(status) {
2592 // use entropy
2593 page.evaluate(function() {
2594 $(".use-entropy").prop("checked", true).trigger("change");
2595 $(".entropy").val("01").trigger("input");
2596 });
2597 // check the entropy is shown to be the correct type
2598 waitForEntropyFeedback(function() {
2599 var entropyText = page.evaluate(function() {
2600 return $(".entropy-container").text();
2601 });
2602 if (entropyText.indexOf("binary") == -1) {
2603 console.log("Binary entropy is not detected and presented to user");
2604 fail();
2605 }
2606 next();
2607 });
2608 });
2609 },
2610
2611 // Base 6 entropy can be entered
2612 function() {
2613 page.open(url, function(status) {
2614 // use entropy
2615 page.evaluate(function() {
2616 $(".use-entropy").prop("checked", true).trigger("change");
2617 $(".entropy").val("012345").trigger("input");
2618 });
2619 // check the entropy is shown to be the correct type
2620 waitForEntropyFeedback(function() {
2621 var entropyText = page.evaluate(function() {
2622 return $(".entropy-container").text();
2623 });
2624 if (entropyText.indexOf("base 6") == -1) {
2625 console.log("Base 6 entropy is not detected and presented to user");
2626 fail();
2627 }
2628 next();
2629 });
2630 });
2631 },
2632
2633 // Base 6 dice entropy can be entered
2634 function() {
2635 page.open(url, function(status) {
2636 // use entropy
2637 page.evaluate(function() {
2638 $(".use-entropy").prop("checked", true).trigger("change");
2639 $(".entropy").val("123456").trigger("input");
2640 });
2641 // check the entropy is shown to be the correct type
2642 waitForEntropyFeedback(function() {
2643 var entropyText = page.evaluate(function() {
2644 return $(".entropy-container").text();
2645 });
2646 if (entropyText.indexOf("dice") == -1) {
2647 console.log("Dice entropy is not detected and presented to user");
2648 fail();
2649 }
2650 next();
2651 });
2652 });
2653 },
2654
2655 // Base 10 entropy can be entered
2656 function() {
2657 page.open(url, function(status) {
2658 // use entropy
2659 page.evaluate(function() {
2660 $(".use-entropy").prop("checked", true).trigger("change");
2661 $(".entropy").val("789").trigger("input");
2662 });
2663 // check the entropy is shown to be the correct type
2664 waitForEntropyFeedback(function() {
2665 var entropyText = page.evaluate(function() {
2666 return $(".entropy-container").text();
2667 });
2668 if (entropyText.indexOf("base 10") == -1) {
2669 console.log("Base 10 entropy is not detected and presented to user");
2670 fail();
2671 }
2672 next();
2673 });
2674 });
2675 },
2676
2677 // Hexadecimal entropy can be entered
2678 function() {
2679 page.open(url, function(status) {
2680 // use entropy
2681 page.evaluate(function() {
2682 $(".use-entropy").prop("checked", true).trigger("change");
2683 $(".entropy").val("abcdef").trigger("input");
2684 });
2685 // check the entropy is shown to be the correct type
2686 waitForEntropyFeedback(function() {
2687 var entropyText = page.evaluate(function() {
2688 return $(".entropy-container").text();
2689 });
2690 if (entropyText.indexOf("hexadecimal") == -1) {
2691 console.log("Hexadecimal entropy is not detected and presented to user");
2692 fail();
2693 }
2694 next();
2695 });
2696 });
2697 },
2698
2699 // Dice entropy value is shown as the converted base 6 value
2700 function() {
2701 page.open(url, function(status) {
2702 // use entropy
2703 page.evaluate(function() {
2704 $(".use-entropy").prop("checked", true).trigger("change");
2705 $(".entropy").val("123456").trigger("input");
2706 });
2707 // check the entropy is shown as base 6, not as the original dice value
2708 waitForEntropyFeedback(function() {
2709 var entropyText = page.evaluate(function() {
2710 return $(".entropy-container").text();
2711 });
2712 if (entropyText.indexOf("123450") == -1) {
2713 console.log("Dice entropy is not shown to user as base 6 value");
2714 fail();
2715 }
2716 if (entropyText.indexOf("123456") > -1) {
2717 console.log("Dice entropy value is shown instead of true base 6 value");
2718 fail();
2719 }
2720 next();
2721 });
2722 });
2723 },
2724
2725 // The number of bits of entropy accumulated is shown
2726 function() {
2727 page.open(url, function(status) {
2728 //[ entropy, bits ]
2729 var tests = [
2730 [ "0000 0000 0000 0000 0000", "20" ],
2731 [ "0", "1" ],
2732 [ "0000", "4" ],
2733 [ "6", "2" ], // 6 in card is 0 in base 6, 0 in base 6 is 2.6 bits (rounded down to 2 bits)
2734 [ "7", "3" ], // 7 in base 10 is 111 in base 2, no leading zeros
2735 [ "8", "4" ],
2736 [ "F", "4" ],
2737 [ "29", "6" ],
2738 [ "0A", "8" ],
2739 [ "1A", "8" ], // hex is always multiple of 4 bits of entropy
2740 [ "2A", "8" ],
2741 [ "4A", "8" ],
2742 [ "8A", "8" ],
2743 [ "FA", "8" ],
2744 [ "000A", "16" ],
2745 [ "5555", "11" ],
2746 [ "6666", "10" ], // uses dice, so entropy is actually 0000 in base 6, which is 4 lots of 2.58 bits, which is 10.32 bits (rounded down to 10 bits)
2747 [ "2227", "13" ], // Uses base 10, which is 4 lots of 3.32 bits, which is 13.3 bits (rounded down to 13)
2748 [ "222F", "16" ],
2749 [ "FFFF", "16" ],
2750 [ "0000101017", "33" ], // 10 events at 3.32 bits per event
2751 [ "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks", "225" ], // cards are not replaced, so a full deck is not 52^52 entropy which is 296 bits, it's 52!, which is 225 bits
2752 ]
2753 // use entropy
2754 page.evaluate(function(e) {
2755 $(".use-entropy").prop("checked", true).trigger("change");
2756 });
2757 // Run each test
2758 var nextTest = function runNextTest(i) {
2759 var entropy = tests[i][0];
2760 var expected = tests[i][1];
2761 // set entropy
2762 page.evaluate(function(e) {
2763 $(".entropy").val(e).trigger("input");
2764 }, entropy);
2765 // check the number of bits of entropy is shown
2766 waitForEntropyFeedback(function() {
2767 var entropyText = page.evaluate(function() {
2768 return $(".entropy-container").text();
2769 });
2770 if (entropyText.replace(/\s/g,"").indexOf("Bits" + expected) == -1) {
2771 console.log("Accumulated entropy is not shown correctly for " + entropy);
2772 fail();
2773 }
2774 var isLastTest = i == tests.length - 1;
2775 if (isLastTest) {
2776 next();
2777 }
2778 else {
2779 runNextTest(i+1);
2780 }
2781 });
2782 }
2783 nextTest(0);
2784 });
2785 },
2786
2787 // There is feedback provided about the supplied entropy
2788 function() {
2789 page.open(url, function(status) {
2790 var tests = [
2791 {
2792 entropy: "A",
2793 filtered: "A",
2794 type: "hexadecimal",
2795 events: 1,
2796 bits: 4,
2797 words: 0,
2798 strength: "extremely weak",
2799 },
2800 {
2801 entropy: "AAAAAAAA",
2802 filtered: "AAAAAAAA",
2803 type: "hexadecimal",
2804 events: 8,
2805 bits: 32,
2806 words: 3,
2807 strength: "extremely weak",
2808 },
2809 {
2810 entropy: "AAAAAAAA B",
2811 filtered: "AAAAAAAAB",
2812 type: "hexadecimal",
2813 events: 9,
2814 bits: 36,
2815 words: 3,
2816 strength: "extremely weak",
2817 },
2818 {
2819 entropy: "AAAAAAAA BBBBBBBB",
2820 filtered: "AAAAAAAABBBBBBBB",
2821 type: "hexadecimal",
2822 events: 16,
2823 bits: 64,
2824 words: 6,
2825 strength: "very weak",
2826 },
2827 {
2828 entropy: "AAAAAAAA BBBBBBBB CCCCCCCC",
2829 filtered: "AAAAAAAABBBBBBBBCCCCCCCC",
2830 type: "hexadecimal",
2831 events: 24,
2832 bits: 96,
2833 words: 9,
2834 strength: "weak",
2835 },
2836 {
2837 entropy: "AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD",
2838 filtered: "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD",
2839 type: "hexadecimal",
2840 events: 32,
2841 bits: 128,
2842 words: 12,
2843 strength: "easily cracked",
2844 },
2845 {
2846 entropy: "AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDA",
2847 filtered: "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDA",
2848 type: "hexadecimal",
2849 events: 32,
2850 bits: 128,
2851 words: 12,
2852 strength: "strong",
2853 },
2854 {
2855 entropy: "AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDA EEEEEEEE",
2856 filtered: "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDAEEEEEEEE",
2857 type: "hexadecimal",
2858 events: 40,
2859 bits: 160,
2860 words: 15,
2861 strength: "very strong",
2862 },
2863 {
2864 entropy: "AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDA EEEEEEEE FFFFFFFF",
2865 filtered: "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDAEEEEEEEEFFFFFFFF",
2866 type: "hexadecimal",
2867 events: 48,
2868 bits: 192,
2869 words: 18,
2870 strength: "extremely strong",
2871 },
2872 {
2873 entropy: "7d",
2874 type: "card",
2875 events: 1,
2876 bits: 5,
2877 words: 0,
2878 strength: "extremely weak",
2879 },
2880 {
2881 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks",
2882 type: "card (full deck)",
2883 events: 52,
2884 bits: 225,
2885 words: 21,
2886 strength: "extremely strong",
2887 },
2888 {
2889 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks3d",
2890 type: "card (full deck, 1 duplicate: 3d)",
2891 events: 53,
2892 bits: 254,
2893 words: 21,
2894 strength: "extremely strong",
2895 },
2896 {
2897 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqs3d4d",
2898 type: "card (2 duplicates: 3d 4d, 1 missing: KS)",
2899 events: 53,
2900 bits: 254,
2901 words: 21,
2902 strength: "extremely strong",
2903 },
2904 {
2905 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqs3d4d5d6d",
2906 type: "card (4 duplicates: 3d 4d 5d..., 1 missing: KS)",
2907 events: 53,
2908 bits: 264,
2909 words: 24,
2910 strength: "extremely strong",
2911 },
2912 // Next test was throwing uncaught error in zxcvbn
2913 // Also tests 451 bits, ie Math.log2(52!)*2 = 225.58 * 2
2914 {
2915 entropy: "ac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsksac2c3c4c5c6c7c8c9ctcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks",
2916 type: "card (full deck, 52 duplicates: ac 2c 3c...)",
2917 events: 104,
2918 bits: 499,
2919 words: 45,
2920 strength: "extremely strong",
2921 },
2922 // Case insensitivity to duplicate cards
2923 {
2924 entropy: "asAS",
2925 type: "card (1 duplicate: AS)",
2926 events: 2,
2927 bits: 9,
2928 words: 0,
2929 strength: "extremely weak",
2930 },
2931 {
2932 entropy: "ASas",
2933 type: "card (1 duplicate: as)",
2934 events: 2,
2935 bits: 9,
2936 words: 0,
2937 strength: "extremely weak",
2938 },
2939 // Missing cards are detected
2940 {
2941 entropy: "ac2c3c4c5c6c7c8c tcjcqckcad2d3d4d5d6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks",
2942 type: "card (1 missing: 9C)",
2943 events: 51,
2944 bits: 221,
2945 words: 18,
2946 strength: "extremely strong",
2947 },
2948 {
2949 entropy: "ac2c3c4c5c6c7c8c tcjcqckcad2d3d4d 6d7d8d9dtdjdqdkdah2h3h4h5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks",
2950 type: "card (2 missing: 9C 5D)",
2951 events: 50,
2952 bits: 216,
2953 words: 18,
2954 strength: "extremely strong",
2955 },
2956 {
2957 entropy: "ac2c3c4c5c6c7c8c tcjcqckcad2d3d4d 6d7d8d9dtdjd kdah2h3h 5h6h7h8h9hthjhqhkhas2s3s4s5s6s7s8s9stsjsqsks",
2958 type: "card (4 missing: 9C 5D QD...)",
2959 events: 48,
2960 bits: 208,
2961 words: 18,
2962 strength: "extremely strong",
2963 },
2964 // More than six missing cards does not show message
2965 {
2966 entropy: "ac2c3c4c5c6c7c8c tcjcqckcad2d3d4d 6d 8d9d jd kdah2h3h 5h6h7h8h9hthjhqhkh 2s3s4s5s6s7s8s9stsjsqsks",
2967 type: "card",
2968 events: 45,
2969 bits: 195,
2970 words: 18,
2971 strength: "extremely strong",
2972 },
2973 // Multiple decks of cards increases bits per event
2974 {
2975 entropy: "3d",
2976 events: 1,
2977 bits: 4,
2978 bitsPerEvent: 4.34,
2979 },
2980 {
2981 entropy: "3d3d",
2982 events: 2,
2983 bits: 9,
2984 bitsPerEvent: 4.80,
2985 },
2986 {
2987 entropy: "3d3d3d",
2988 events: 3,
2989 bits: 15,
2990 bitsPerEvent: 5.01,
2991 },
2992 {
2993 entropy: "3d3d3d3d",
2994 events: 4,
2995 bits: 20,
2996 bitsPerEvent: 5.14,
2997 },
2998 {
2999 entropy: "3d3d3d3d3d",
3000 events: 5,
3001 bits: 26,
3002 bitsPerEvent: 5.22,
3003 },
3004 {
3005 entropy: "3d3d3d3d3d3d",
3006 events: 6,
3007 bits: 31,
3008 bitsPerEvent: 5.28,
3009 },
3010 {
3011 entropy: "3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d3d",
3012 events: 33,
3013 bits: 184,
3014 bitsPerEvent: 5.59,
3015 strength: 'easily cracked - Repeats like "abcabcabc" are only slightly harder to guess than "abc"',
3016 },
3017 ];
3018 // use entropy
3019 page.evaluate(function() {
3020 $(".use-entropy").prop("checked", true).trigger("change");
3021 });
3022 var nextTest = function runNextTest(i) {
3023 function getFeedbackError(expected, actual) {
3024 if ("filtered" in expected && actual.indexOf(expected.filtered) == -1) {
3025 return "Filtered value not in feedback";
3026 }
3027 if ("type" in expected && actual.indexOf(expected.type) == -1) {
3028 return "Entropy type not in feedback";
3029 }
3030 if ("events" in expected && actual.indexOf(expected.events) == -1) {
3031 return "Event count not in feedback";
3032 }
3033 if ("bits" in expected && actual.indexOf(expected.bits) == -1) {
3034 return "Bit count not in feedback";
3035 }
3036 if ("strength" in expected && actual.indexOf(expected.strength) == -1) {
3037 return "Strength not in feedback";
3038 }
3039 if ("bitsPerEvent" in expected && actual.indexOf(expected.bitsPerEvent) == -1) {
3040 return "bitsPerEvent not in feedback";
3041 }
3042 return false;
3043 }
3044 test = tests[i];
3045 page.evaluate(function(e) {
3046 $(".addresses").empty();
3047 $(".phrase").val("");
3048 $(".entropy").val(e).trigger("input");
3049 }, test.entropy);
3050 waitForEntropyFeedback(function() {
3051 var mnemonic = page.evaluate(function() {
3052 return $(".phrase").val();
3053 });
3054 // Check mnemonic length
3055 if ("words" in test && test.words == 0) {
3056 if (mnemonic.length > 0) {
3057 console.log("Mnemonic length for " + test.strength + " strength is not " + test.words);
3058 console.log("Entropy: " + test.entropy);
3059 console.log("Mnemonic: " + mnemonic);
3060 fail();
3061 }
3062 }
3063 else if ("words" in test) {
3064 if (mnemonic.split(" ").length != test.words) {
3065 console.log("Mnemonic length for " + test.strength + " strength is not " + test.words);
3066 console.log("Entropy: " + test.entropy);
3067 console.log("Mnemonic: " + mnemonic);
3068 fail();
3069 }
3070 }
3071 // check feedback
3072 var feedback = page.evaluate(function() {
3073 return $(".entropy-container").text();
3074 });
3075 var feedbackError = getFeedbackError(test, feedback);
3076 if (feedbackError) {
3077 console.log("Entropy feedback for " + test.entropy + " returned error");
3078 console.log(feedbackError);
3079 fail();
3080 }
3081 // Run next test
3082 var isLastTest = i == tests.length - 1;
3083 if (isLastTest) {
3084 next();
3085 }
3086 else {
3087 runNextTest(i+1);
3088 }
3089 });
3090 }
3091 nextTest(0);
3092 });
3093 },
3094
3095 // Entropy is truncated from the left
3096 function() {
3097 page.open(url, function(status) {
3098 var expected = "avocado zoo zone";
3099 // use entropy
3100 page.evaluate(function() {
3101 $(".use-entropy").prop("checked", true).trigger("change");
3102 var entropy = "00000000 00000000 00000000 00000000";
3103 entropy += "11111111 11111111 11111111 1111"; // Missing last byte
3104 $(".entropy").val(entropy).trigger("input");
3105 });
3106 // check the entropy is truncated from the right
3107 waitForGenerate(function() {
3108 var actual = page.evaluate(function() {
3109 return $(".phrase").val();
3110 });
3111 if (actual != expected) {
3112 console.log("Entropy is not truncated from the right");
3113 console.log("Expected: " + expected);
3114 console.log("Got: " + actual);
3115 fail();
3116 }
3117 next();
3118 });
3119 });
3120 },
3121
3122 // Very large entropy results in very long mnemonics
3123 function() {
3124 page.open(url, function(status) {
3125 // use entropy
3126 page.evaluate(function() {
3127 $(".use-entropy").prop("checked", true).trigger("change");
3128 var entropy = "";
3129 // Generate a very long entropy string
3130 for (var i=0; i<33; i++) {
3131 entropy += "AAAAAAAA"; // 3 words * 33 iterations = 99 words
3132 }
3133 $(".entropy").val(entropy).trigger("input");
3134 });
3135 // check the mnemonic is very long
3136 waitForGenerate(function() {
3137 var wordCount = page.evaluate(function() {
3138 return $(".phrase").val().split(" ").length;
3139 });
3140 if (wordCount != 99) {
3141 console.log("Large entropy does not generate long mnemonic");
3142 console.log("Expected 99 words, got " + wordCount);
3143 fail();
3144 }
3145 next();
3146 });
3147 });
3148 },
3149
3150 // Is compatible with bip32jp entropy
3151 // https://bip32jp.github.io/english/index.html
3152 // NOTES:
3153 // Is incompatible with:
3154 // base 20
3155 function() {
3156 page.open(url, function(status) {
3157 var expected = "train then jungle barely whip fiber purpose puppy eagle cloud clump hospital robot brave balcony utility detect estate old green desk skill multiply virus";
3158 // use entropy
3159 page.evaluate(function() {
3160 $(".use-entropy").prop("checked", true).trigger("change");
3161 var entropy = "543210543210543210543210543210543210543210543210543210543210543210543210543210543210543210543210543";
3162 $(".entropy").val(entropy).trigger("input");
3163 });
3164 // check the mnemonic matches the expected value from bip32jp
3165 waitForGenerate(function() {
3166 var actual = page.evaluate(function() {
3167 return $(".phrase").val();
3168 });
3169 if (actual != expected) {
3170 console.log("Mnemonic does not match bip32jp for base 6 entropy");
3171 console.log("Expected: " + expected);
3172 console.log("Got: " + actual);
3173 fail();
3174 }
3175 next();
3176 });
3177 });
3178 },
3179
3180 // Blank entropy does not generate mnemonic or addresses
3181 function() {
3182 page.open(url, function(status) {
3183 // use entropy
3184 page.evaluate(function() {
3185 $(".use-entropy").prop("checked", true).trigger("change");
3186 $(".entropy").val("").trigger("input");
3187 });
3188 waitForFeedback(function() {
3189 // check there is no mnemonic
3190 var phrase = page.evaluate(function() {
3191 return $(".phrase").val();
3192 });
3193 if (phrase != "") {
3194 console.log("Blank entropy does not result in blank mnemonic");
3195 console.log("Got: " + phrase);
3196 fail();
3197 }
3198 // check there are no addresses displayed
3199 var addresses = page.evaluate(function() {
3200 return $(".address").length;
3201 });
3202 if (addresses != 0) {
3203 console.log("Blank entropy does not result in zero addresses");
3204 fail();
3205 }
3206 // Check the feedback says 'blank entropy'
3207 var feedback = page.evaluate(function() {
3208 return $(".feedback").text();
3209 });
3210 if (feedback != "Blank entropy") {
3211 console.log("Blank entropy does not show feedback message");
3212 fail();
3213 }
3214 next();
3215 });
3216 });
3217 },
3218
3219 // Mnemonic length can be selected even for weak entropy
3220 function() {
3221 page.open(url, function(status) {
3222 // use entropy
3223 page.evaluate(function() {
3224 $(".use-entropy").prop("checked", true).trigger("change");
3225 $(".entropy").val("012345");
3226 $(".mnemonic-length").val("18").trigger("change");
3227 });
3228 // check the mnemonic is the correct length
3229 waitForGenerate(function() {
3230 var phrase = page.evaluate(function() {
3231 return $(".phrase").val();
3232 });
3233 var numberOfWords = phrase.split(/\s/g).length;
3234 if (numberOfWords != 18) {
3235 console.log("Weak entropy cannot be overridden to give 18 word mnemonic");
3236 console.log(phrase);
3237 fail();
3238 }
3239 next();
3240 });
3241 });
3242 },
3243
3244 // Github issue 33
3245 // https://github.com/iancoleman/bip39/issues/33
3246 // Final cards should contribute entropy
3247 function() {
3248 page.open(url, function(status) {
3249 // use entropy
3250 page.evaluate(function() {
3251 $(".use-entropy").prop("checked", true).trigger("change");
3252 $(".entropy").val("7S 9H 9S QH 8C KS AS 7D 7C QD 4S 4D TC 2D 5S JS 3D 8S 8H 4C 3C AC 3S QC 9C JC 7H AD TD JD 6D KH 5C QS 2S 6S 6H JH KD 9D-6C TS TH 4H KC 5H 2H AH 2C 8D 3H 5D").trigger("input");
3253 });
3254 // get the mnemonic
3255 waitForGenerate(function() {
3256 var originalPhrase = page.evaluate(function() {
3257 return $(".phrase").val();
3258 });
3259 // Set the last 12 cards to be AS
3260 page.evaluate(function() {
3261 $(".addresses").empty();
3262 $(".entropy").val("7S 9H 9S QH 8C KS AS 7D 7C QD 4S 4D TC 2D 5S JS 3D 8S 8H 4C 3C AC 3S QC 9C JC 7H AD TD JD 6D KH 5C QS 2S 6S 6H JH KD 9D-AS AS AS AS AS AS AS AS AS AS AS AS").trigger("input");
3263 });
3264 // get the new mnemonic
3265 waitForGenerate(function() {
3266 var newPhrase = page.evaluate(function() {
3267 return $(".phrase").val();
3268 });
3269 // check the phrase has changed
3270 if (newPhrase == originalPhrase) {
3271 console.log("Changing last 12 cards does not change mnemonic");
3272 console.log("Original:");
3273 console.log(originalPhrase);
3274 console.log("New:");
3275 console.log(newPhrase);
3276 fail();
3277 }
3278 next();
3279 });
3280 });
3281 });
3282 },
3283
3284 // Github issue 35
3285 // https://github.com/iancoleman/bip39/issues/35
3286 // QR Code support
3287 function() {
3288 page.open(url, function(status) {
3289 // use entropy
3290 page.evaluate(function() {
3291 $(".generate").click();
3292 });
3293 waitForGenerate(function() {
3294 var p = page.evaluate(function() {
3295 // get position of mnemonic element
3296 return $(".phrase").offset();
3297 });
3298 p.top = Math.ceil(p.top);
3299 p.left = Math.ceil(p.left);
3300 // check the qr code shows
3301 page.sendEvent("mousemove", p.left+4, p.top+4);
3302 var qrShowing = page.evaluate(function() {
3303 return $(".qr-container").find("canvas").length > 0;
3304 });
3305 if (!qrShowing) {
3306 console.log("QR Code does not show");
3307 fail();
3308 }
3309 // check the qr code hides
3310 page.sendEvent("mousemove", p.left-4, p.top-4);
3311 var qrHidden = page.evaluate(function() {
3312 return $(".qr-container").find("canvas").length == 0;
3313 });
3314 if (!qrHidden) {
3315 console.log("QR Code does not hide");
3316 fail();
3317 }
3318 next();
3319 });
3320 });
3321 },
3322
3323 // BIP44 account extendend private key is shown
3324 // github issue 37 - compatibility with electrum
3325 function() {
3326 page.open(url, function(status) {
3327 // set the phrase
3328 var expected = "xprv9yzrnt4zWVJUr1k2VxSPy9ettKz5PpeDMgaVG7UKedhqnw1tDkxP2UyYNhuNSumk2sLE5ctwKZs9vwjsq3e1vo9egCK6CzP87H2cVYXpfwQ";
3329 page.evaluate(function() {
3330 $(".phrase").val("abandon abandon ability");
3331 $(".phrase").trigger("input");
3332 });
3333 // check the BIP44 account extended private key
3334 waitForGenerate(function() {
3335 var actual = page.evaluate(function() {
3336 return $(".account-xprv").val();
3337 });
3338 if (actual != expected) {
3339 console.log("BIP44 account extended private key is incorrect");
3340 console.log("Expected: " + expected);
3341 console.log("Actual: " + actual);
3342 fail();
3343 }
3344 next();
3345 });
3346 });
3347 },
3348
3349 // BIP44 account extendend public key is shown
3350 // github issue 37 - compatibility with electrum
3351 function() {
3352 page.open(url, function(status) {
3353 // set the phrase
3354 var expected = "xpub6CzDCPbtLrrn4VpVbyyQLHbdSMpZoHN4iuW64VswCyEpfjM2mJGdaHJ2DyuZwtst96E16VvcERb8BBeJdHSCVmAq9RhtRQg6eAZFrTKCNqf";
3355 page.evaluate(function() {
3356 $(".phrase").val("abandon abandon ability");
3357 $(".phrase").trigger("input");
3358 });
3359 // check the BIP44 account extended public key
3360 waitForGenerate(function() {
3361 var actual = page.evaluate(function() {
3362 return $(".account-xpub").val();
3363 });
3364 if (actual != expected) {
3365 console.log("BIP44 account extended public key is incorrect");
3366 console.log("Expected: " + expected);
3367 console.log("Actual: " + actual);
3368 fail();
3369 }
3370 next();
3371 });
3372 });
3373 },
3374
3375 // github issue 40
3376 // BIP32 root key can be set as an xpub
3377 function() {
3378 page.open(url, function(status) {
3379 // set the phrase
3380 page.evaluate(function() {
3381 // set xpub for account 0 of bip44 for 'abandon abandon ability'
3382 var bip44AccountXpub = "xpub6CzDCPbtLrrn4VpVbyyQLHbdSMpZoHN4iuW64VswCyEpfjM2mJGdaHJ2DyuZwtst96E16VvcERb8BBeJdHSCVmAq9RhtRQg6eAZFrTKCNqf";
3383 $("#root-key").val(bip44AccountXpub);
3384 $("#root-key").trigger("input");
3385 });
3386 waitForFeedback(function() {
3387 page.evaluate(function() {
3388 // Use bip32 tab
3389 $("#bip32-tab a").click();
3390 });
3391 waitForGenerate(function() {
3392 page.evaluate(function() {
3393 // derive external addresses for this xpub
3394 var firstAccountDerivationPath = "m/0";
3395 $("#bip32-path").val(firstAccountDerivationPath);
3396 $("#bip32-path").trigger("input");
3397 });
3398 waitForGenerate(function() {
3399 // check the addresses are generated
3400 var expected = "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug";
3401 var actual = page.evaluate(function() {
3402 return $(".address:first").text();
3403 });
3404 if (actual != expected) {
3405 console.log("xpub key does not generate addresses in table");
3406 console.log("Expected: " + expected);
3407 console.log("Actual: " + actual);
3408 fail();
3409 }
3410 // check the xprv key is not set
3411 var expected = "NA";
3412 var actual = page.evaluate(function() {
3413 return $(".extended-priv-key").val();
3414 });
3415 if (actual != expected) {
3416 console.log("xpub key as root shows derived bip32 xprv key");
3417 console.log("Expected: " + expected);
3418 console.log("Actual: " + actual);
3419 fail();
3420 }
3421 // check the private key is not set
3422 var expected = "NA";
3423 var actual = page.evaluate(function() {
3424 return $(".privkey:first").text();
3425 });
3426 if (actual != expected) {
3427 console.log("xpub key generates private key in addresses table");
3428 console.log("Expected: " + expected);
3429 console.log("Actual: " + actual);
3430 fail();
3431 }
3432 next();
3433 });
3434 });
3435 });
3436 });
3437 },
3438
3439 // github issue 40
3440 // xpub for bip32 root key will not work with hardened derivation paths
3441 function() {
3442 page.open(url, function(status) {
3443 // set the phrase
3444 page.evaluate(function() {
3445 // set xpub for account 0 of bip44 for 'abandon abandon ability'
3446 var bip44AccountXpub = "xpub6CzDCPbtLrrn4VpVbyyQLHbdSMpZoHN4iuW64VswCyEpfjM2mJGdaHJ2DyuZwtst96E16VvcERb8BBeJdHSCVmAq9RhtRQg6eAZFrTKCNqf";
3447 $("#root-key").val(bip44AccountXpub);
3448 $("#root-key").trigger("input");
3449 });
3450 waitForFeedback(function() {
3451 // Check feedback is correct
3452 var expected = "Hardened derivation path is invalid with xpub key";
3453 var actual = page.evaluate(function() {
3454 return $(".feedback").text();
3455 });
3456 if (actual != expected) {
3457 console.log("xpub key with hardened derivation path does not show feedback");
3458 console.log("Expected: " + expected);
3459 console.log("Actual: " + actual);
3460 fail();
3461 }
3462 // Check no addresses are shown
3463 var expected = 0;
3464 var actual = page.evaluate(function() {
3465 return $(".addresses tr").length;
3466 });
3467 if (actual != expected) {
3468 console.log("addresses still show after setting xpub key with hardened derivation path");
3469 console.log("Expected: " + expected);
3470 console.log("Actual: " + actual);
3471 fail();
3472 }
3473 next();
3474 });
3475 });
3476 },
3477
3478 // github issue 39
3479 // no root key shows feedback
3480 function() {
3481 page.open(url, function(status) {
3482 // click the bip32 tab on fresh page
3483 page.evaluate(function() {
3484 $("#bip32-tab a").click();
3485 });
3486 waitForFeedback(function() {
3487 // Check feedback is correct
3488 var expected = "No root key";
3489 var actual = page.evaluate(function() {
3490 return $(".feedback").text();
3491 });
3492 if (actual != expected) {
3493 console.log("Blank root key not detected");
3494 console.log("Expected: " + expected);
3495 console.log("Actual: " + actual);
3496 fail();
3497 }
3498 next();
3499 });
3500 });
3501 },
3502
3503 // Github issue 44
3504 // display error switching tabs while addresses are generating
3505 function() {
3506 page.open(url, function(status) {
3507 // set the phrase
3508 page.evaluate(function() {
3509 $(".phrase").val("abandon abandon ability").trigger("input");
3510 });
3511 waitForGenerate(function() {
3512 // set to generate 500 more addresses
3513 // generate more addresses
3514 // change tabs which should cancel the previous generating
3515 page.evaluate(function() {
3516 $(".rows-to-add").val("100");
3517 $(".more").click();
3518 $("#bip32-tab a").click();
3519 });
3520 // check the derivation paths are in order and of the right quantity
3521 waitForGenerate(function() {
3522 var paths = page.evaluate(function() {
3523 return $(".index").map(function(i, e) {
3524 return $(e).text();
3525 });
3526 });
3527 for (var i=0; i<paths.length; i++) {
3528 var expected = "m/0/" + i;
3529 var actual = paths[i];
3530 if (actual != expected) {
3531 console.log("Path " + i + " is not in correct order");
3532 console.log("Expected: " + expected);
3533 console.log("Actual: " + actual);
3534 fail();
3535 }
3536 }
3537 if (paths.length != 20) {
3538 console.log("Generation was not cancelled by new action");
3539 fail();
3540 }
3541 next();
3542 });
3543 });
3544 });
3545 },
3546
3547 // Github issue 49
3548 // padding for binary should give length with multiple of 256
3549 // hashed entropy 1111 is length 252, so requires 4 leading zeros
3550 // prior to issue 49 it would only generate 2 leading zeros, ie missing 2
3551 function() {
3552 page.open(url, function(status) {
3553 expected = "avocado valid quantum cross link predict excuse edit street able flame large galaxy ginger nuclear"
3554 // use entropy
3555 page.evaluate(function() {
3556 $(".use-entropy").prop("checked", true).trigger("change");
3557 $(".mnemonic-length").val("15");
3558 $(".entropy").val("1111").trigger("input");
3559 });
3560 waitForGenerate(function() {
3561 // get the mnemonic
3562 var actual = page.evaluate(function() {
3563 return $(".phrase").val();
3564 });
3565 // check the mnemonic is correct
3566 if (actual != expected) {
3567 console.log("Left padding error for entropy");
3568 console.log("Expected: " + expected);
3569 console.log("Actual: " + actual);
3570 fail();
3571 }
3572 next();
3573 });
3574 });
3575 },
3576
3577 // Github pull request 55
3578 // https://github.com/iancoleman/bip39/pull/55
3579 // Client select
3580 function() {
3581 page.open(url, function(status) {
3582 // set mnemonic and select bip32 tab
3583 page.evaluate(function() {
3584 $("#bip32-tab a").click();
3585 $(".phrase").val("abandon abandon ability").trigger("input");
3586 });
3587 waitForGenerate(function() {
3588 // BITCOIN CORE
3589 // set bip32 client to bitcoin core
3590 page.evaluate(function() {
3591 var bitcoinCoreIndex = "0";
3592 $("#bip32-client").val(bitcoinCoreIndex).trigger("change");
3593 });
3594 waitForGenerate(function() {
3595 // get the derivation path
3596 var actual = page.evaluate(function() {
3597 return $("#bip32-path").val();
3598 });
3599 // check the derivation path is correct
3600 expected = "m/0'/0'"
3601 if (actual != expected) {
3602 console.log("Selecting Bitcoin Core client does not set correct derivation path");
3603 console.log("Expected: " + expected);
3604 console.log("Actual: " + actual);
3605 fail();
3606 }
3607 // get hardened addresses
3608 var usesHardenedAddresses = page.evaluate(function() {
3609 return $(".hardened-addresses").prop("checked");
3610 });
3611 // check hardened addresses is selected
3612 if(!usesHardenedAddresses) {
3613 console.log("Selecting Bitcoin Core client does not use hardened addresses");
3614 fail();
3615 }
3616 // check input is readonly
3617 var pathIsReadonly = page.evaluate(function() {
3618 return $("#bip32-path").prop("readonly");
3619 });
3620 if (!pathIsReadonly) {
3621 console.log("Selecting Bitcoin Core client does not set derivation path to readonly");
3622 fail();
3623 }
3624 // MULTIBIT
3625 // set bip32 client to multibit
3626 page.evaluate(function() {
3627 var multibitIndex = "2";
3628 $("#bip32-client").val(multibitIndex).trigger("change");
3629 });
3630 waitForGenerate(function() {
3631 // get the derivation path
3632 var actual = page.evaluate(function() {
3633 return $("#bip32-path").val();
3634 });
3635 // check the derivation path is correct
3636 expected = "m/0'/0"
3637 if (actual != expected) {
3638 console.log("Selecting Multibit client does not set correct derivation path");
3639 console.log("Expected: " + expected);
3640 console.log("Actual: " + actual);
3641 fail();
3642 }
3643 // get hardened addresses
3644 var usesHardenedAddresses = page.evaluate(function() {
3645 return $(".hardened-addresses").prop("checked");
3646 });
3647 // check hardened addresses is selected
3648 if(usesHardenedAddresses) {
3649 console.log("Selecting Multibit client does not uncheck hardened addresses");
3650 fail();
3651 }
3652 // CUSTOM DERIVATION PATH
3653 // check input is not readonly
3654 page.evaluate(function() {
3655 $("#bip32-client").val("custom").trigger("change");
3656 });
3657 // do not wait for generate, since there is no change to the
3658 // derivation path there is no new generation performed
3659 var pathIsReadonly = page.evaluate(function() {
3660 return $("#bip32-path").prop("readonly");
3661 });
3662 if (pathIsReadonly) {
3663 console.log("Selecting Custom Derivation Path does not allow derivation path input");
3664 fail();
3665 }
3666 next();
3667 });
3668 });
3669 });
3670 });
3671 },
3672
3673 // github issue 58
3674 // https://github.com/iancoleman/bip39/issues/58
3675 // bip32 derivation is correct, does not drop leading zeros
3676 // see also
3677 // https://medium.com/@alexberegszaszi/why-do-my-bip32-wallets-disagree-6f3254cc5846
3678 function() {
3679 page.open(url, function(status) {
3680 // set the phrase and passphrase
3681 var expected = "17rxURoF96VhmkcEGCj5LNQkmN9HVhWb7F";
3682 // Note that bitcore generates an incorrect address
3683 // 13EuKhffWkBE2KUwcbkbELZb1MpzbimJ3Y
3684 // see the medium.com link above for more details
3685 page.evaluate(function() {
3686 $(".phrase").val("fruit wave dwarf banana earth journey tattoo true farm silk olive fence");
3687 $(".passphrase").val("banana").trigger("input");
3688 });
3689 // check the address is generated correctly
3690 waitForGenerate(function() {
3691 var actual = page.evaluate(function() {
3692 return $(".address:first").text();
3693 });
3694 if (actual != expected) {
3695 console.log("BIP32 derivation is incorrect");
3696 console.log("Expected: " + expected);
3697 console.log("Actual: " + actual);
3698 fail();
3699 }
3700 next();
3701 });
3702 });
3703 },
3704
3705
3706 // github issue 60
3707 // Japanese mnemonics generate incorrect bip32 seed
3708 // BIP39 seed is set from phrase
3709 function() {
3710 page.open(url, function(status) {
3711 // set the phrase
3712 var expected = "a262d6fb6122ecf45be09c50492b31f92e9beb7d9a845987a02cefda57a15f9c467a17872029a9e92299b5cbdf306e3a0ee620245cbd508959b6cb7ca637bd55";
3713 page.evaluate(function() {
3714 $(".phrase").val("あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あいこくしん あおぞら");
3715 $("#passphrase").val("メートルガバヴァぱばぐゞちぢ十人十色");
3716 $("#passphrase").trigger("input");
3717 });
3718 // check the seed is generated correctly
3719 waitForGenerate(function() {
3720 var actual = page.evaluate(function() {
3721 return $(".seed").val();
3722 });
3723 if (actual != expected) {
3724 console.log("BIP39 seed is incorrectly generated from Japanese mnemonic");
3725 console.log("Expected: " + expected);
3726 console.log("Actual: " + actual);
3727 fail();
3728 }
3729 next();
3730 });
3731 });
3732 },
3733
3734 // If you wish to add more tests, do so here...
3735
3736 // Here is a blank test template
3737 /*
3738
3739 function() {
3740 page.open(url, function(status) {
3741 // Do something on the page
3742 page.evaluate(function() {
3743 $(".phrase").val("abandon abandon ability").trigger("input");
3744 });
3745 waitForGenerate(function() {
3746 // Check the result of doing the thing
3747 var expected = "1Di3Vp7tBWtyQaDABLAjfWtF6V7hYKJtug";
3748 var actual = page.evaluate(function() {
3749 return $(".address:first").text();
3750 });
3751 if (actual != expected) {
3752 console.log("A specific message about what failed");
3753 console.log("Expected: " + expected);
3754 console.log("Actual: " + actual);
3755 fail();
3756 }
3757 // Run the next test
3758 next();
3759 });
3760 });
3761 },
3762
3763 */
3764
3765 ];
3766
3767 console.log("Running tests...");
3768 tests = shuffle(tests);
3769 next();