testaro 4.4.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/commands.js +2 -2
  2. package/package.json +1 -1
  3. package/run.js +153 -145
package/commands.js CHANGED
@@ -100,8 +100,8 @@ exports.commands = {
100
100
  state: [
101
101
  'Wait until the page reaches a load state',
102
102
  {
103
- which: [true, 'string', 'hasLength', '“loaded” or “idle”'],
104
- what: [false, 'string', 'isState', 'comment']
103
+ which: [true, 'string', 'isState', '“loaded” or “idle”'],
104
+ what: [false, 'string', 'hasLength', 'comment']
105
105
  }
106
106
  ],
107
107
  tenonRequest: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -256,7 +256,7 @@ const launch = async typeName => {
256
256
  // Make its console messages appear in the Playwright console.
257
257
  page.on('console', msg => {
258
258
  const msgText = msg.text();
259
- console.log(msgText);
259
+ console.log(`[${msgText}]`);
260
260
  logCount++;
261
261
  logSize += msgText.length;
262
262
  const msgLC = msgText.toLowerCase();
@@ -369,69 +369,79 @@ const textOf = async (page, element) => {
369
369
  return null;
370
370
  }
371
371
  };
372
- // Returns an element case-insensitively matching a text.
372
+ // Returns an element of a type case-insensitively matching a text.
373
373
  const matchElement = async (page, selector, matchText, index = 0) => {
374
- // If the page still exists:
375
- if (page) {
376
- // Wait 2 seconds until the body contains any text to be matched.
377
- const slimText = debloat(matchText);
378
- const bodyText = await page.textContent('body');
379
- const slimBody = debloat(bodyText);
380
- const textInBodyJSHandle = await page.waitForFunction(
381
- args => {
382
- const matchText = args[0];
383
- const bodyText = args[1];
384
- return ! matchText || bodyText.includes(matchText);
385
- },
386
- [slimText, slimBody],
387
- {timeout: 4000}
388
- )
389
- .catch(async error => {
390
- console.log(`ERROR: text “${matchText}” not in body (${error.message})`);
391
- });
392
- // If there is no text to be matched or the body contained it:
393
- if (textInBodyJSHandle) {
394
- const lcText = matchText ? matchText.toLowerCase() : '';
395
- // Identify the selected elements.
396
- const selections = await page.$$(`body ${selector}`);
374
+ if (matchText) {
375
+ // If the page still exists:
376
+ if (page) {
377
+ const slimText = debloat(matchText);
378
+ // Identify the elements of the specified type.
379
+ const selections = await page.$$(selector);
397
380
  // If there are any:
398
381
  if (selections.length) {
399
382
  // If there are enough to make a match possible:
400
383
  if (index < selections.length) {
401
- // Return the nth one including any specified text, or the count of candidates if none.
402
- const elementTexts = [];
403
- let nth = 0;
404
- for (const element of selections) {
405
- const elementText = await textOf(page, element);
406
- elementTexts.push(elementText);
407
- if ((! lcText || elementText.includes(lcText)) && nth++ === index) {
408
- return element;
384
+ // Return the specified one, if any.
385
+ let matchCount = 0;
386
+ const selectionTexts = [];
387
+ for (const selection of selections) {
388
+ const selectionText = await textOf(page, selection);
389
+ selectionTexts.push(selectionText);
390
+ if (selectionText.includes(slimText)) {
391
+ if (matchCount++ === index) {
392
+ return {
393
+ success: true,
394
+ matchingElement: selection
395
+ };
396
+ }
409
397
  }
410
398
  }
411
- return elementTexts;
399
+ // None satisfied the specifications, so return a failure.
400
+ return {
401
+ success: false,
402
+ error: 'exhausted',
403
+ message: `Text found in only ${matchCount} (not ${index + 1}) of ${selections.length}`,
404
+ candidateTexts: selectionTexts
405
+ };
412
406
  }
413
- // Otherwise, i.e. if there are too few to make a match possible:
407
+ // Otherwise, i.e. if there are too few such elements to make a match possible:
414
408
  else {
415
- // Return the count of candidates.
416
- return selections.length;
409
+ // Return a failure.
410
+ return {
411
+ success: false,
412
+ error: 'fewer',
413
+ message: `Count of '${selector}' elements only ${selections.length}`
414
+ };
417
415
  }
418
416
  }
419
- // Otherwise, i.e. if there are no selected elements, return 0.
417
+ // Otherwise, i.e. if there are no elements of the specified type:
420
418
  else {
421
- return 0;
419
+ // Return a failure.
420
+ return {
421
+ success: false,
422
+ error: 'none',
423
+ message: `No '${selector}' elements found`
424
+ };
422
425
  }
423
426
  }
424
- // Otherwise, i.e. if the body did not contain it:
427
+ // Otherwise, i.e. if the page no longer exists:
425
428
  else {
426
- // Return the failure.
427
- return -1;
429
+ // Return a failure.
430
+ return {
431
+ success: false,
432
+ error: 'gone',
433
+ message: 'Page gone'
434
+ };
428
435
  }
429
436
  }
430
- // Otherwise, i.e. if the page no longer exists:
437
+ // Otherwise, i.e. if no text was specified:
431
438
  else {
432
- // Return null.
433
- console.log('ERROR: Page gone');
434
- return null;
439
+ // Return a failure.
440
+ return {
441
+ success: false,
442
+ error: 'text',
443
+ message: 'No text specified'
444
+ };
435
445
  }
436
446
  };
437
447
  // Returns a string with any final slash removed.
@@ -502,8 +512,9 @@ const visit = async (act, page, isStrict) => {
502
512
  // If the visit fails:
503
513
  if (response === 'error') {
504
514
  // Give up.
505
- console.log(`ERROR: Visits to ${requestedURL} failed`);
506
- act.result = `ERROR: Visit to ${requestedURL} failed`;
515
+ const errorMsg = `ERROR: Attemts to visit ${requestedURL} failed`;
516
+ console.log(errorMsg);
517
+ act.result = errorMsg;
507
518
  await page.goto('about:blank')
508
519
  .catch(error => {
509
520
  console.log(`ERROR: Navigation to blank page failed (${error.message})`);
@@ -563,6 +574,14 @@ const waitError = (page, act, error, what) => {
563
574
  act.result.error = `ERROR waiting for ${what}`;
564
575
  return false;
565
576
  };
577
+ // Waits.
578
+ const wait = ms => {
579
+ return new Promise(resolve => {
580
+ setTimeout(() => {
581
+ resolve('');
582
+ }, ms);
583
+ });
584
+ };
566
585
  // Recursively performs the acts in a report.
567
586
  const doActs = async (report, actIndex, page) => {
568
587
  const {acts} = report;
@@ -597,12 +616,12 @@ const doActs = async (report, actIndex, page) => {
597
616
  if (truth[1]) {
598
617
  // If the performance of commands is to stop:
599
618
  if (act.jump === 0) {
600
- // Set the command index to cause a stop.
619
+ // Set the act index to cause a stop.
601
620
  actIndex = -2;
602
621
  }
603
622
  // Otherwise, if there is a numerical jump:
604
623
  else if (act.jump) {
605
- // Set the command index accordingly.
624
+ // Set the act index accordingly.
606
625
  actIndex += act.jump - 1;
607
626
  }
608
627
  // Otherwise, if there is a named next command:
@@ -630,76 +649,65 @@ const doActs = async (report, actIndex, page) => {
630
649
  else if (act.type === 'wait') {
631
650
  const {what, which} = act;
632
651
  console.log(`>> for ${what} to include “${which}”`);
633
- // Wait 5 or 10 seconds for the specified text, and quit if it does not.
634
- let successJSHandle;
652
+ // Wait 5 or 10 seconds for the specified text, and quit if it does not appear.
635
653
  if (act.what === 'url') {
636
- successJSHandle = await page.waitForFunction(
637
- text => document.URL.includes(text), act.which, {timeout: 5000}
638
- )
654
+ await page.waitForURL(act.which, {timeout: 15000})
639
655
  .catch(error => {
640
- actIndex = acts.length;
641
- return waitError(page, act, error, 'URL');
656
+ actIndex = -2;
657
+ waitError(page, act, error, 'URL');
642
658
  });
643
659
  }
644
660
  else if (act.what === 'title') {
645
- successJSHandle = await page.waitForFunction(
646
- text => document.title.includes(text), act.which, {timeout: 5000}
661
+ await page.waitForFunction(
662
+ text => document && document.title && document.title.includes(text),
663
+ act.which,
664
+ {
665
+ polling: 1000,
666
+ timeout: 5000
667
+ }
647
668
  )
648
669
  .catch(error => {
649
- actIndex = acts.length;
650
- return waitError(page, act, error, 'title');
670
+ actIndex = -2;
671
+ waitError(page, act, error, 'title');
651
672
  });
652
673
  }
653
674
  else if (act.what === 'body') {
654
- successJSHandle = await page.waitForFunction(
655
- matchText => {
656
- const innerText = document && document.body && document.body.innerText;
657
- if (innerText) {
658
- return innerText.includes(matchText);
659
- }
660
- else {
661
- actIndex = acts.length;
662
- console.log('ERROR finding document body');
663
- return false;
664
- }
665
- }, which, {timeout: 20000}
675
+ await page.waitForFunction(
676
+ text => document && document.body && document.body.innerText.includes(text),
677
+ act.which,
678
+ {
679
+ polling: 2000,
680
+ timeout: 10000
681
+ }
666
682
  )
667
- .catch(error => {
668
- actIndex = acts.length;
669
- return waitError(page, act, error, 'body');
683
+ .catch(async error => {
684
+ actIndex = -2;
685
+ waitError(page, act, error, 'body');
670
686
  });
671
687
  }
672
- if (successJSHandle) {
688
+ // If the text was found:
689
+ if (actIndex > -2) {
690
+ // Add this to the report.
673
691
  act.result = {url: page.url()};
674
692
  if (act.what === 'title') {
675
693
  act.result.title = await page.title();
676
694
  }
677
- await page.waitForLoadState('networkidle', {timeout: 10000})
678
- .catch(error => {
679
- console.log(`ERROR waiting for stability after ${act.what} (${error.message})`);
680
- act.result.error = `ERROR waiting for stability after ${act.what}`;
681
- });
682
695
  }
683
696
  }
684
697
  // Otherwise, if the act is a wait for a state:
685
698
  else if (act.type === 'state') {
686
- // If the state is valid:
699
+ // Wait for it, and quit if it does not appear.
687
700
  const stateIndex = ['loaded', 'idle'].indexOf(act.which);
688
- if (stateIndex !== -1) {
689
- // Wait for it, and quit if it does not appear.
690
- await page.waitForLoadState(
691
- ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 5000][stateIndex]}
692
- )
693
- .catch(error => {
694
- console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
695
- act.result = `ERROR waiting for page to be ${act.which}`;
696
- actIndex = acts.length;
697
- });
698
- }
699
- else {
700
- console.log('ERROR: invalid state');
701
- act.result = 'ERROR: invalid state';
702
- actIndex = acts.length;
701
+ await page.waitForLoadState(
702
+ ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 5000][stateIndex]}
703
+ )
704
+ .catch(error => {
705
+ console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
706
+ act.result = `ERROR waiting for page to be ${act.which}`;
707
+ actIndex = -2;
708
+ });
709
+ if (actIndex > -2) {
710
+ act.result = `Page became ${act.which}`;
703
711
  }
704
712
  }
705
713
  // Otherwise, if the act is a page switch:
@@ -889,48 +897,34 @@ const doActs = async (report, actIndex, page) => {
889
897
  // Otherwise, if the act is a move:
890
898
  else if (moves[act.type]) {
891
899
  const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
892
- // Identify the element to perform the move on.
893
- const whichElement = await matchElement(page, selector, act.which || '', act.index);
894
- // If there were enough candidates but no text match:
895
- if (Array.isArray(whichElement)) {
896
- // Add the result to the act.
897
- act.result = {
898
- candidateCount: whichElement.length,
899
- error: 'ERROR: no element with matching text found',
900
- candidateTexts: whichElement
901
- };
902
- }
903
- // Otherwise, if the body did not contain the text:
904
- else if (whichElement === -1) {
905
- // Add the failure to the act.
906
- act.result = 'ERROR: body did not contain text to match';
907
- }
908
- // Otherwise, if there were not enough candidates:
909
- else if (typeof whichElement === 'number') {
910
- // Add the failure to the act.
911
- act.result = {
912
- candidateCount: whichElement,
913
- error: 'ERROR: too few such elements to allow a match'
914
- };
900
+ // Wait 10 seconds until the element to perform the move on is identified.
901
+ let matchResult = {success: false};
902
+ let tries = 0;
903
+ while (tries++ < 5 && ! matchResult.success) {
904
+ matchResult = await matchElement(page, selector, act.which || '', act.index);
905
+ if (! matchResult.success) {
906
+ await wait(2000);
907
+ }
915
908
  }
916
- // Otherwise, if a match was found:
917
- else if (whichElement !== null) {
909
+ // If a match was found:
910
+ if (matchResult.success) {
911
+ const {matchingElement} = matchResult;
918
912
  // If the move is a button click, perform it.
919
913
  if (act.type === 'button') {
920
- await whichElement.click({timeout: 3000});
914
+ await matchingElement.click({timeout: 3000});
921
915
  act.result = 'clicked';
922
916
  }
923
917
  // Otherwise, if it is checking a radio button or checkbox, perform it.
924
918
  else if (['checkbox', 'radio'].includes(act.type)) {
925
- await whichElement.waitForElementState('stable', {timeout: 2000})
919
+ await matchingElement.waitForElementState('stable', {timeout: 2000})
926
920
  .catch(error => {
927
921
  console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
928
922
  act.result = `ERROR waiting for stable ${act.type}`;
929
923
  });
930
924
  if (! act.result) {
931
- const isEnabled = await whichElement.isEnabled();
925
+ const isEnabled = await matchingElement.isEnabled();
932
926
  if (isEnabled) {
933
- await whichElement.check({
927
+ await matchingElement.check({
934
928
  force: true,
935
929
  timeout: 2000
936
930
  })
@@ -951,29 +945,37 @@ const doActs = async (report, actIndex, page) => {
951
945
  }
952
946
  // Otherwise, if it is focusing the element, perform it.
953
947
  else if (act.type === 'focus') {
954
- await whichElement.focus({timeout: 2000});
948
+ await matchingElement.focus({timeout: 2000});
955
949
  act.result = 'focused';
956
950
  }
957
951
  // Otherwise, if it is clicking a link, perform it.
958
952
  else if (act.type === 'link') {
959
- const href = await whichElement.getAttribute('href');
960
- const target = await whichElement.getAttribute('target');
961
- await whichElement.click({timeout: 2000})
953
+ const href = await matchingElement.getAttribute('href');
954
+ const target = await matchingElement.getAttribute('target');
955
+ await matchingElement.click({timeout: 2000})
962
956
  .catch(async () => {
963
- await whichElement.click({
957
+ console.log('ERROR: First attempt to click link timed out');
958
+ await matchingElement.click({
964
959
  force: true,
965
960
  timeout: 10000
961
+ })
962
+ .catch(() => {
963
+ actIndex = -2;
964
+ console.log('ERROR: Second (forced) attempt to click link timed out');
965
+ act.result = 'ERROR: Normal and forced click attempts timed out';
966
966
  });
967
967
  });
968
- act.result = {
969
- href: href || 'NONE',
970
- target: target || 'NONE',
971
- move: 'clicked'
972
- };
968
+ if (actIndex > -2) {
969
+ act.result = {
970
+ href: href || 'NONE',
971
+ target: target || 'NONE',
972
+ move: 'clicked'
973
+ };
974
+ }
973
975
  }
974
976
  // Otherwise, if it is selecting an option in a select list, perform it.
975
977
  else if (act.type === 'select') {
976
- const options = await whichElement.$$('option');
978
+ const options = await matchingElement.$$('option');
977
979
  let optionText = '';
978
980
  if (options && Array.isArray(options) && options.length) {
979
981
  const optionTexts = [];
@@ -984,12 +986,12 @@ const doActs = async (report, actIndex, page) => {
984
986
  const matchTexts = optionTexts.map((text, index) => text.includes(act.what) ? index : -1);
985
987
  const index = matchTexts.filter(text => text > -1)[act.index || 0];
986
988
  if (index !== undefined) {
987
- await whichElement.selectOption({index});
989
+ await matchingElement.selectOption({index});
988
990
  optionText = optionTexts[index];
989
991
  }
990
992
  }
991
993
  act.result = optionText
992
- ? `&ldquo;${optionText}}&rdquo; selected`
994
+ ? `“${optionText} selected`
993
995
  : 'ERROR: option not found';
994
996
  }
995
997
  // Otherwise, if it is entering text on the element:
@@ -1003,19 +1005,23 @@ const doActs = async (report, actIndex, page) => {
1003
1005
  what = what.replace(/__[A-Z]+__/, envValue);
1004
1006
  }
1005
1007
  // Enter the text.
1006
- await whichElement.type(act.what);
1008
+ await matchingElement.type(act.what);
1007
1009
  report.presses += act.what.length;
1008
1010
  act.result = 'entered';
1009
1011
  }
1010
1012
  // Otherwise, i.e. if the move is unknown, add the failure to the act.
1011
1013
  else {
1012
1014
  // Report the error.
1013
- act.result = 'ERROR: move unknown';
1015
+ const report = 'ERROR: move unknown';
1016
+ act.result = report;
1017
+ console.log(report);
1014
1018
  }
1015
1019
  }
1016
- // Otherwise, i.e. if the page was gone:
1020
+ // Otherwise, i.e. if no match was found:
1017
1021
  else {
1018
- act.result = 'ERROR: page gone, so matching element not found';
1022
+ const report = 'ERROR: Specified element not found';
1023
+ act.result = report;
1024
+ console.log(report);
1019
1025
  }
1020
1026
  }
1021
1027
  // Otherwise, if the act is a keypress:
@@ -1203,7 +1209,9 @@ const doActs = async (report, actIndex, page) => {
1203
1209
  // Otherwise, i.e. if the command is invalid:
1204
1210
  else {
1205
1211
  // Add an error result to the act.
1206
- act.result = `ERROR: Invalid command of type ${act.type}`;
1212
+ const errorMsg = `ERROR: Invalid command of type ${act.type}`;
1213
+ act.result = errorMsg;
1214
+ console.log(errorMsg);
1207
1215
  }
1208
1216
  // Perform the remaining acts.
1209
1217
  await doActs(report, actIndex + 1, page);