QA Playbook | arbory-www

A comprehensive guide for quality assurance on the Arbory Digital AEM Edge Delivery Services site. Covers manual testing, automated checks, AI-assisted QA workflows, and PR review processes.

TABLE OF CONTENTS

  1. Environment Setup
  2. Block Inventory & Test Matrix
  3. Manual QA Checklist
  4. Automated Testing
  5. Linting & Code Quality
  6. Browser Testing with Playwright
  7. Performance & GitHub Checks
  8. PR Review Process
  9. AI-Assisted QA (Using .skills in VS Code)
  10. Design System Verification
  11. Cross-Origin & External Dependencies
  12. Troubleshooting

1. ENVIRONMENT SETUP

Prerequisites

- Node.js v20+ (check with: node --version)

- AEM CLI installed globally (npm install -g @adobe/aem-cli)

- Git with access to the arbory-digital-inc/arbory-www repo

<br>

First-Time Setup

Run in terminal:

<br>

Start Local Dev Server

Run in terminal:

aem up

This starts the local proxy at http://localhost:3000. All manual and browser testing runs against this server.

<br>

- ESLint - real-time JS lint feedback

- Stylelint - real-time CSS lint feedback

- Copilot / Cline / Continue / Windsurf - AI coding assistant with access to .skills/

2. BLOCK INVENTORY & TEST MATRIX

The site has 34 blocks currently. Every block should be tested when global styles or core scripts change. When a PR modifies a specific block, test that block and any blocks it interacts with.

<br>

What to Test Per Block

For each block, verify:

3. MANUAL QA CHECKLIST

Use this checklist for every QA pass - whether reviewing a PR or doing a full site sweep.

<br>

Page-Level Checks

<br>
Responsive Breakpoints

Test at these widths:

<br>
Dark Mode / Light Mode

The site uses light-dark() CSS functions with these tokens:

Toggle your OS or browser dark mode preference and verify:

4. AUTOMATED TESTING

Test Runner

The project uses Web Test Runner (@web/test-runner) with Chai assertions and Sinon for mocking.

Commands:

<br>

Existing Tests

test/scripts/scripts.test.js

Covers: Core loadPage() - verifies first image gets fetchPriority: high and loading removed

test/scripts/dapreview.test.js

Covers: DA preview mode - verifies ?dapreview=true loads tools/da/da.js

<br>

Writing New Tests

Tests use @esm-bundle/chai for assertions. Example:
import { expect } from '@esm-bundle/chai';
    describe('myFunction', () => {
      it('should do something', () => {
        expect(result).to.equal(expected);
      });
    });

<br>

When to add tests:

- New utility functions in scripts/utils/

- Data processing / transformation logic

- API integration helpers

- Complex conditional logic

<br>

Don't write unit tests for:

- Block decoration (DOM transforms) — validate these in the browser

- CSS styling — validate visually

- Simple getters/setters

5. LINTING & CODE QUALITY

Commands

<br>

Rules Overview

JavaScript (ESLint with @adobe/eslint-config-helix):

- ES6+ features required

- .js extension required in imports

- no-await-in-loop disabled

- no-param-reassign allows property mutation

- No license headers required

<br>

CSS (Stylelint with stylelint-config-standard):

- Block CSS must be scoped: selectors start with main .{block-name}

- Use CSS custom properties (not hard-coded colors/fonts)

- Mobile-first responsive: use @media (width >= 600px) range syntax

- Low specificity: 2–3 selector levels max

- No !important

<br>

Lint Must Pass Before Merge

Non-negotiable. Every PR must have clean lint output. Run npm run lint locally before pushing.

6. BROWSER TESTING WITH PLAYWRIGHT

For visual validation, responsive checks, and interactive behavior testing. These are throwaway tests - used to validate, capture screenshots, then discard.

<br>

Setup (One-Time)

npm install --save-dev playwright

npx playwright install chromium

<br>

Example: Test a Block at Multiple Viewports

// test-my-block.js (DO NOT COMMIT)

import {
    chromium
} from 'playwright';

import {
    mkdir
} from 'fs/promises';

async function test() {
    await mkdir('./test/tmp/screenshots', {
        recursive: true
    });
    const browser = await chromium.launch({
        headless: false
    });
    const page = await browser.newPage();
    const viewports = [
        {
            name: 'mobile',
          width: 375,
            height: 812
        },
        {
            name: 'tablet',
            width: 768,
            height: 1024
        },
        {
            name: 'desktop',
            width: 1440,
            height: 900
        },
    ];
    for (const vp of viewports) {
        await page.setViewportSize({
            width: vp.width,
            height: vp.height
        });
        await page.goto('http://localhost:3000/path/to/test-page');
        await page.waitForSelector('.my-block');
        await page.screenshot({
            path: './test/tmp/screenshots/my-block-' + vp.name + '.png',
            fullPage: true,
        });
    }
    await browser.close();
}

test().catch(console.error);

<br>

Run it:

node test-my-block.js

<br>
Tips

- Always wait for block decoration: await page.waitForSelector('.my-block')

- Test interactions: clicks, hovers, form fills, accordion expand/collapse

- Screenshot specific elements: await page.locator('.hero').screenshot(...)

- Include screenshots in PRs to help reviewers

- Keep throwaway scripts in test/tmp/ (gitignored)

7. PERFORMANCE & GITHUB CHECKS

Automatic CI Checks

When you push a branch and create a PR:

1. Linting runs automatically

2. PSI (PageSpeed Insights) runs if you include a test link in the PR description

<br>

Add this to your PR description:

## Testing

Preview: https://branch-name--arbory-www--arbory-digital-inc.aem.page/path/to/test

<br>

Monitor Checks

gh pr checks --watch     - Watch checks in real-time

gh pr checks             - Check once

<br>

Common Performance Issues

Too much eager JS

Fix: Move non-LCP code to loadLazy() or loadDelayed()

CSS blocking render

Fix: Move non-critical styles to lazy-styles.css

Large images

Fix: Use optimized formats, lazy load below-fold images

Third-party scripts in eager phase

Fix: Defer to loadDelayed()