testaro 60.2.0 → 60.3.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.
package/UPGRADES.md CHANGED
@@ -2453,3 +2453,352 @@ describe('Database queries', () => {
2453
2453
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2454
2454
  SOFTWARE.
2455
2455
  */
2456
+
2457
+ ## Performance improvements
2458
+
2459
+ ### Browser-Node iteration prevention
2460
+
2461
+ It is suspected that some Testaro tests are unnecessarily inefficient because they iterate across elements in a page, performing a browser operation on each and then returning a result for processing by Node. For example, in `lineHeight`:
2462
+
2463
+ ```
2464
+ const all = await init(100, page, 'body *', {hasText: /[^\s]/});
2465
+ // For each locator:
2466
+ for (const loc of all.allLocs) {
2467
+ // Get whether its element violates the rule.
2468
+ const data = await loc.evaluate(el => {
2469
+ const styleDec = window.getComputedStyle(el);
2470
+ const {fontSize, lineHeight} = styleDec;
2471
+ return {
2472
+ fontSize: Number.parseFloat(fontSize),
2473
+ lineHeight: Number.parseFloat(lineHeight)
2474
+ };
2475
+ });
2476
+ ```
2477
+
2478
+ The inefficiency has necessitating sampling.
2479
+
2480
+ However, there is evidence that moving the processing of Playwright locators into the browser and returning the final result to Node could be 1 or 2 orders of magnitude faster and eliminate the need to sample, thereby improving speed and delivering accuracy and consistency.
2481
+
2482
+ One proposal for a rewrite of `lineHeight`, still doing sampling, was:
2483
+
2484
+ ```
2485
+ // Reports a test.
2486
+ exports.reporter = async (page, report) => {
2487
+ // Get data on elements with potentially invalid line heights.
2488
+ const data = await page.evaluate(() => {
2489
+ const instances = [];
2490
+ // Get all visible elements in the body with any text.
2491
+ const elements = Array
2492
+ .from(document.querySelectorAll('body *'))
2493
+ .filter(el => {
2494
+ const styleDec = window.getComputedStyle(el);
2495
+ const {display, visibility} = styleDec;
2496
+ return display !== 'none' && visibility !== 'hidden' && el.textContent.trim().length;
2497
+ });
2498
+ // Get a sample of them.
2499
+ const sampleIndexes = window.getSample(elements, 100);
2500
+ const sample = sampleIndexes.map(index => elements[index]);
2501
+ // For each element in the sample:
2502
+ sample.forEach(el => {
2503
+ const styleDec = window.getComputedStyle(el);
2504
+ const {lineHeight, fontSize} = styleDec;
2505
+ // If the line height is absolute:
2506
+ if (['px', 'pt'].some(unit => lineHeight.endsWith(unit))) {
2507
+ const lhValue = parseFloat(lineHeight);
2508
+ const fsValue = parseFloat(fontSize);
2509
+ // If the line height is less than 1.2 times the font size:
2510
+ if (fsValue > 0 && lhValue / fsValue < 1.2) {
2511
+ const box = el.getBoundingClientRect();
2512
+ // Add an instance to the result.
2513
+ instances.push({
2514
+ ruleID: 'lineHeight',
2515
+ what: 'Line height is less than 1.2 times the font size',
2516
+ ordinalSeverity: 2,
2517
+ tagName: el.tagName,
2518
+ id: el.id || '',
2519
+ location: {
2520
+ doc: 'dom',
2521
+ type: 'box',
2522
+ spec: {
2523
+ x: box.x,
2524
+ y: box.y,
2525
+ width: box.width,
2526
+ height: box.height
2527
+ }
2528
+ },
2529
+ excerpt: el.textContent.trim().replace(/\s+/g, ' ').slice(0, 100)
2530
+ });
2531
+ }
2532
+ }
2533
+ });
2534
+ return {
2535
+ totals: [0, 0, instances.length, 0],
2536
+ instances
2537
+ };
2538
+ });
2539
+
2540
+ // Add the result to the report.
2541
+ try {
2542
+ report.acts[report.actIndex].result.data = data;
2543
+ }
2544
+ catch(error) {
2545
+ console.log(`ERROR: Could not add result to report (${error.message})`);
2546
+ }
2547
+ };
2548
+ ```
2549
+
2550
+ To reuse a sampling (or any other) function in multiple tests, it was proposed to store the function in a file and then inject it into the page as follows:
2551
+
2552
+ ```
2553
+ const getSample = require(path.join(__dirname, 'procs/sample.js')).getSample;
2554
+ const initScript = `window.getSample = ${getSample.toString()};`;
2555
+ await browserContext.addInitScript(initScript);
2556
+ ```
2557
+
2558
+ ## Version management for service
2559
+
2560
+ Proposals:
2561
+
2562
+ Prevent Future Conflicts
2563
+ Option 1: Don't commit package-lock.json on the server (Recommended)
2564
+
2565
+ On the server, tell git to ignore local changes to package-lock.json:
2566
+
2567
+ ```
2568
+ # Assume package-lock.json is unchanged locally
2569
+ git update-index --assume-unchanged package-lock.json
2570
+ ```
2571
+
2572
+ This prevents git from seeing the file as modified when npm update changes it.
2573
+
2574
+ Option 2: Exclude package-lock.json from version control
2575
+
2576
+ If you don't need package-lock.json in your repo (since you use * for all versions in package.json), add it to .gitignore:
2577
+
2578
+ ```
2579
+ echo "package-lock.json" >> .gitignore
2580
+ git rm --cached package-lock.json
2581
+ git commit -m "Stop tracking package-lock.json"
2582
+ git push
2583
+ ```
2584
+
2585
+ Then on the server:
2586
+
2587
+ ```
2588
+ git pull
2589
+ npm update # Will create package-lock.json locally but git won't track it
2590
+ ```
2591
+
2592
+ Option 3: Proper deployment workflow (Best Practice)
2593
+
2594
+ On the server, use a deployment script instead of git pull:
2595
+
2596
+ ```
2597
+ #!/bin/bash
2598
+ # deploy.sh
2599
+
2600
+ # Stash any local changes
2601
+ git stash
2602
+
2603
+ # Fetch latest from origin
2604
+ git fetch origin
2605
+
2606
+ # Reset to origin/main (discard all local changes)
2607
+ git reset --hard origin/main
2608
+
2609
+ # Clean untracked files
2610
+ git clean -fd
2611
+
2612
+ # Install dependencies
2613
+ npm ci # Use 'ci' instead of 'update' - it's deterministic
2614
+
2615
+ # Restart app
2616
+ pm2 restart kilotest
2617
+ ```
2618
+
2619
+ Make it executable and use it:
2620
+
2621
+ ```
2622
+ chmod +x deploy.sh
2623
+ ./deploy.sh
2624
+ ```
2625
+
2626
+ Why This Happens
2627
+
2628
+ With "testaro": "*" in your package.json:
2629
+
2630
+ - Local dev: npm update locks to version X in package-lock.json
2631
+ - Server: npm update locks to version Y (newer) in package-lock.json
2632
+ - Git: Sees conflicting versions when you pull
2633
+
2634
+ Recommended Approach
2635
+
2636
+ Use exact versions to ensure consistency across environments:
2637
+
2638
+ ```
2639
+ {
2640
+ "dependencies": {
2641
+ "dotenv": "^17.2.3",
2642
+ "testaro": "^2.1.0",
2643
+ "testilo": "^1.5.0"
2644
+ }
2645
+ }
2646
+ ```
2647
+
2648
+ Then:
2649
+
2650
+ ```
2651
+ # Lock versions
2652
+ npm install
2653
+
2654
+ # Commit the lock file
2655
+ git add package.json package-lock.json
2656
+ git commit -m "Lock dependency versions"
2657
+ git push
2658
+
2659
+ # On server
2660
+ git pull
2661
+ npm ci # Install exact versions from package-lock.json
2662
+ pm2 restart kilotest
2663
+ ```
2664
+
2665
+ This ensures identical dependencies in both environments and prevents conflicts.
2666
+
2667
+ Automate Version Updates in package.json
2668
+
2669
+ You can use npm-check-updates (ncu) to automatically update version numbers in package.json:
2670
+
2671
+ Installation
2672
+
2673
+ ```
2674
+ npm install -g npm-check-updates
2675
+ ```
2676
+
2677
+ Usage
2678
+
2679
+ ```
2680
+ # Check what would be updated
2681
+ ncu
2682
+
2683
+ # Update package.json with latest versions
2684
+ ncu -u
2685
+
2686
+ # Install the new versions
2687
+ npm install
2688
+
2689
+ # Commit and push
2690
+ git add package.json package-lock.json
2691
+ git commit -m "chore: update dependencies"
2692
+ git push
2693
+ ```
2694
+
2695
+ Automate with a Script
2696
+ Add to your package.json:
2697
+
2698
+ ```
2699
+ {
2700
+ "scripts": {
2701
+ "update-deps": "ncu -u && npm install && git add package.json package-lock.json"
2702
+ }
2703
+ }
2704
+ ```
2705
+
2706
+ Then use:
2707
+
2708
+ ```
2709
+ npm run update-deps
2710
+ git commit -m "chore: update dependencies"
2711
+ git push
2712
+ ```
2713
+
2714
+ More Control with Target Versions
2715
+ Instead of *, use caret ranges for automatic minor/patch updates:
2716
+
2717
+ ```
2718
+ {
2719
+ "dependencies": {
2720
+ "dotenv": "^17.2.3", // allows 17.x.x
2721
+ "testaro": "^2.1.0", // allows 2.x.x
2722
+ "testilo": "^1.5.0" // allows 1.x.x
2723
+ }
2724
+ }
2725
+ ```
2726
+
2727
+ Then npm update will:
2728
+
2729
+ - Update to latest minor/patch versions within the range
2730
+ - Update package-lock.json with exact versions
2731
+ - Not change package.json unless you use ncu -u
2732
+
2733
+ Workflow
2734
+
2735
+ Local development:
2736
+
2737
+ ```
2738
+ # When you want to update dependencies
2739
+ ncu -u # Update package.json to latest versions
2740
+ npm install # Install and lock exact versions
2741
+ npm test # Verify everything works
2742
+ git add package.json package-lock.json
2743
+ git commit -m "chore: update dependencies"
2744
+ git push
2745
+ ```
2746
+
2747
+ Server deployment:
2748
+
2749
+ ```
2750
+ git pull # Gets updated package.json and package-lock.json
2751
+ npm ci # Installs exact locked versions
2752
+ pm2 restart kilotest
2753
+ ```
2754
+
2755
+ Alternative: Use npm's Built-in Commands
2756
+
2757
+ For major version updates with interactive prompts:
2758
+
2759
+ ```
2760
+ # Check outdated packages
2761
+ npm outdated
2762
+
2763
+ # Update interactively (choose which to update)
2764
+ npx npm-check -u
2765
+
2766
+ # Or update specific packages
2767
+ npm install testaro@latest
2768
+ npm install testilo@latest
2769
+
2770
+ # Commit the changes
2771
+ git add package.json package-lock.json
2772
+ git commit -m "chore: update testaro and testilo"
2773
+ git push
2774
+ ```
2775
+
2776
+ Pre-commit Hook (Optional)
2777
+ Ensure package-lock.json is always in sync:
2778
+
2779
+ ```
2780
+ # .husky/pre-commit or .git/hooks/pre-commit
2781
+ #!/bin/sh
2782
+ if git diff --cached --name-only | grep -q "package.json"; then
2783
+ if ! git diff --cached --name-only | grep -q "package-lock.json"; then
2784
+ echo "Error: package.json changed but package-lock.json didn't"
2785
+ echo "Run: npm install"
2786
+ exit 1
2787
+ fi
2788
+ fi
2789
+ ```
2790
+
2791
+ Recommended Setup
2792
+ 1. Change * to ^ ranges in package.json
2793
+ 2. Install npm-check-updates globally: npm i -g npm-check-updates
2794
+ 3. Add update script to package.json
2795
+ 4. Use npm ci on server (never npm update or npm install)
2796
+
2797
+ This gives you:
2798
+
2799
+ ✅ Automatic patch/minor updates with npm update
2800
+ ✅ Controlled major updates with ncu -u
2801
+ ✅ Cross-environment consistency via package-lock.json
2802
+ ✅ No git conflicts
2803
+
2804
+ Claude Sonnet 4.5 • 1x
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "60.2.0",
3
+ "version": "60.3.0",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -50,9 +50,10 @@ const actIndex = Number.parseInt(process.argv[2]);
50
50
 
51
51
  // FUNCTIONS
52
52
 
53
+ // Performs the tests of the act specified by the caller.
53
54
  const doTestAct = async () => {
54
55
  const reportPath = `${tmpDir}/report.json`;
55
- // Get the saved report.
56
+ // Get the report from the temporary directory.
56
57
  const reportJSON = await fs.readFile(reportPath, 'utf8');
57
58
  const report = JSON.parse(reportJSON);
58
59
  // Get a reference to the act in the report.
@@ -1,5 +1,6 @@
1
1
  /*
2
2
  © 2023–2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+ © 2025 Jonathan Robert Pool. All rights reserved.
3
4
 
4
5
  MIT License
5
6
 
@@ -705,7 +706,7 @@ const convert = (toolName, data, result, standardResult) => {
705
706
  // Round the totals of the standard result.
706
707
  standardResult.totals = standardResult.totals.map(total => Math.round(total));
707
708
  };
708
- // Converts the results.
709
+ // Converts the results of a test act.
709
710
  exports.standardize = act => {
710
711
  const {which, data, result, standardResult} = act;
711
712
  if (which && result && standardResult) {
package/run.js CHANGED
@@ -668,6 +668,11 @@ const abortActs = (report, actIndex) => {
668
668
  // Report that the job is aborted.
669
669
  console.log(`ERROR: Job aborted on act ${actIndex}`);
670
670
  };
671
+ // Returns the combination of browser ID and target URL of an act.
672
+ const launchSpecs = (act, report) => [
673
+ act.browserID || report.browserID || '',
674
+ act.target && act.target.url || report.target && report.target.url || ''
675
+ ];
671
676
  // Performs the acts in a report and adds the results to the report.
672
677
  const doActs = async (report, opts = {}) => {
673
678
  const {acts} = report;
@@ -752,13 +757,14 @@ const doActs = async (report, opts = {}) => {
752
757
  }
753
758
  // Otherwise, if the act is a launch:
754
759
  else if (type === 'launch') {
760
+ const actLaunchSpecs = launchSpecs(act, report);
755
761
  // Launch a browser, navigate to a page, and add the result to the act.
756
762
  await launch(
757
763
  report,
758
764
  debug,
759
765
  waits,
760
- act.browserID || report.browserID || '',
761
- act.target && act.target.url || report.target && report.target.url || ''
766
+ actLaunchSpecs[0],
767
+ actLaunchSpecs[1]
762
768
  );
763
769
  // If this failed:
764
770
  if (page.prevented) {
@@ -819,50 +825,6 @@ const doActs = async (report, opts = {}) => {
819
825
  toolTimes[act.which] += time;
820
826
  // If the act was not prevented:
821
827
  if (act.data && ! act.data.prevented) {
822
- // If standardization is required:
823
- if (['also', 'only'].includes(standard)) {
824
- console.log('>>>>>> Standardizing');
825
- // Initialize the standard result.
826
- act.standardResult = {
827
- totals: [0, 0, 0, 0],
828
- instances: []
829
- };
830
- // Populate it.
831
- standardize(act);
832
- // Launch a browser and navigate to the page.
833
- await launch(
834
- report,
835
- debug,
836
- waits,
837
- act.browserID || report.browserID || '',
838
- act.target && act.target.url || report.target && report.target.url || ''
839
- );
840
- // If this failed:
841
- if (page.prevented) {
842
- // Add this to the act.
843
- act.data ??= {};
844
- act.data.prevented = true;
845
- act.data.error = page.error || '';
846
- }
847
- // Otherwise, i.e. if it succeeded:
848
- else {
849
- // Add a box ID and a path ID to each of its standard instances if missing.
850
- for (const instance of act.standardResult.instances) {
851
- const elementID = await identify(instance, page);
852
- if (! instance.boxID) {
853
- instance.boxID = elementID ? elementID.boxID : '';
854
- }
855
- if (! instance.pathID) {
856
- instance.pathID = elementID ? elementID.pathID : '';
857
- }
858
- };
859
- }
860
- // If the original-format result is not to be included in the report:
861
- if (standard === 'only') {
862
- // Remove it.
863
- delete act.result;
864
- }
865
- }
866
828
  // If the act has expectations:
867
829
  const expectations = act.expect;
868
830
  if (expectations) {
@@ -1501,7 +1463,71 @@ const doActs = async (report, opts = {}) => {
1501
1463
  }
1502
1464
  }
1503
1465
  console.log('Acts completed');
1466
+ // If standardization is required:
1467
+ if (['also', 'only'].includes(standard)) {
1468
+ console.log('>>>> Standardizing results of test acts');
1469
+ const launchSpecActs = {};
1470
+ // For each act:
1471
+ report.acts.forEach((act, index) => {
1472
+ // If it is a test act:
1473
+ if (act.type === 'test') {
1474
+ // Classify it by its browser ID and target URL.
1475
+ const specs = launchSpecs(act, report);
1476
+ const specString = `${specs[0]}>${specs[1]}`;
1477
+ if (launchSpecActs[specString]) {
1478
+ launchSpecActs[specString].push(index);
1479
+ }
1480
+ else {
1481
+ launchSpecActs[specString] = [index];
1482
+ }
1483
+ }
1484
+ });
1485
+ // For each browser ID/target URL class:
1486
+ for (const specString of Object.keys(launchSpecActs)) {
1487
+ const specs = specString.split('>');
1488
+ // Launch a browser and navigate to the page.
1489
+ await launch(
1490
+ report,
1491
+ debug,
1492
+ waits,
1493
+ specs[0],
1494
+ specs[1]
1495
+ );
1496
+ // For each test act with the class:
1497
+ for (const specActIndex of launchSpecActs[specString]) {
1498
+ const act = report.acts[specActIndex];
1499
+ // Initialize the standard result.
1500
+ act.standardResult = {
1501
+ totals: [0, 0, 0, 0],
1502
+ instances: []
1503
+ };
1504
+ // Populate it.
1505
+ standardize(act);
1506
+ // If the launch and navigation succeeded:
1507
+ if (! page.prevented) {
1508
+ // Add a box ID and a path ID to each of its standard instances if missing.
1509
+ for (const instance of act.standardResult.instances) {
1510
+ const elementID = await identify(instance, page);
1511
+ if (! instance.boxID) {
1512
+ instance.boxID = elementID ? elementID.boxID : '';
1513
+ }
1514
+ if (! instance.pathID) {
1515
+ instance.pathID = elementID ? elementID.pathID : '';
1516
+ }
1517
+ };
1518
+ }
1519
+ // If the original-format result is not to be included in the report:
1520
+ if (standard === 'only') {
1521
+ // Remove it.
1522
+ delete act.result;
1523
+ }
1524
+ };
1525
+ };
1526
+ console.log('>>>> Standardization completed');
1527
+ }
1528
+ // Close the browser.
1504
1529
  await browserClose();
1530
+ // Delete the temporary report file.
1505
1531
  await fs.rm(reportPath, {force: true});
1506
1532
  return report;
1507
1533
  };