testaro 60.1.0 → 60.2.1

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/UPGRADES.md +349 -0
  2. package/package.json +1 -1
  3. package/run.js +27 -9
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.1.0",
3
+ "version": "60.2.1",
4
4
  "description": "Run 1000 web accessibility tests from 11 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/run.js CHANGED
@@ -204,11 +204,20 @@ const goTo = async (report, page, url, timeout, waitUntil) => {
204
204
  }
205
205
  // Otherwise, if the response status was rejection of excessive requests:
206
206
  else if (httpStatus === 429) {
207
+ const retryHeader = response.headers()['retry-after'];
208
+ let waitSeconds = 5;
209
+ if (retryHeader) {
210
+ waitSeconds = Number.isNaN(Number(retryHeader))
211
+ ? Math.ceil((new Date(retryHeader) - new Date()) / 1000)
212
+ : Number(retryHeader);
213
+ }
207
214
  // Return this.
208
- console.log(`ERROR: Visit to ${url} prevented by request frequency limit (status 429)`);
215
+ console.log(
216
+ `ERROR: Visit to ${url} rate-limited (status 429); retry after ${waitSeconds} sec.`
217
+ );
209
218
  return {
210
219
  success: false,
211
- error: 'status429'
220
+ error: `status429/retryAfterSeconds=${waitSeconds}`
212
221
  };
213
222
  }
214
223
  // Otherwise, i.e. if the response status was otherwise abnormal:
@@ -423,11 +432,6 @@ const launch = exports.launch = async (
423
432
  report.jobData.lastScriptNonce = scriptNonce;
424
433
  }
425
434
  }
426
- // Otherwise, i.e. if the navigation was prevented by a request frequency restriction:
427
- else if (navResult.error === 'status429') {
428
- // Report this.
429
- addError(true, false, report, actIndex, 'status429');
430
- }
431
435
  // Otherwise, i.e. if the launch or navigation failed for another reason:
432
436
  else {
433
437
  // Cause another attempt to launch and navigate, if retries remain.
@@ -438,8 +442,21 @@ const launch = exports.launch = async (
438
442
  catch(error) {
439
443
  // If retries remain:
440
444
  if (retries > 0) {
441
- console.log(`WARNING: Retrying launch (${retries} retries left)`);
442
- await wait(2000);
445
+ // Prepare to wait 1 second before a retry.
446
+ let waitSeconds = 1;
447
+ // If the error was a visit failure due to rate limiting:
448
+ if (error.message.includes('status429/retryAfterSeconds=')) {
449
+ // Change the wait to the requested time, if less than 10 seconds.
450
+ const waitSecondsRequest = Number(error.message.replace(/^.+=|\)$/g, ''));
451
+ if (! Number.isNaN(waitSecondsRequest) && waitSecondsRequest < 10) {
452
+ waitSeconds = waitSecondsRequest;
453
+ }
454
+ }
455
+ console.log(
456
+ `WARNING: Waiting ${waitSeconds} sec. before retrying (retries left: ${retries})`
457
+ );
458
+ await wait(1000 * waitSeconds + 100);
459
+ // Then retry the launch and navigation.
443
460
  return launch(report, debug, waits, tempBrowserID, tempURL, retries - 1);
444
461
  }
445
462
  // Otherwise, i.e. if no retries remain:
@@ -804,6 +821,7 @@ const doActs = async (report, opts = {}) => {
804
821
  if (act.data && ! act.data.prevented) {
805
822
  // If standardization is required:
806
823
  if (['also', 'only'].includes(standard)) {
824
+ console.log('>>>>>> Standardizing');
807
825
  // Initialize the standard result.
808
826
  act.standardResult = {
809
827
  totals: [0, 0, 0, 0],