
Testing Blocks
- 25 installs
- 45 repo stars
- Updated August 4, 2026
- adobe/helix-website
testing-blocks is a Claude Code skill that guides testing of AEM Edge Delivery Services blocks, scripts, and styles before opening a pull request.
About
testing-blocks guides a developer through testing code changes in AEM Edge Delivery Services projects: blocks, core scripts, and styles. It applies a value-versus-cost philosophy, recommending durable unit tests for logic and throwaway browser tests for DOM and visual validation. A developer follows it after making changes and before opening a pull request.
- Guides testing of AEM Edge Delivery blocks, scripts, and styles before opening a pull request
- Splits work into keeper unit tests (Vitest) and throwaway browser tests (Playwright/Puppeteer) by value vs cost
- Provides an 8-item pre-PR testing checklist covering linting, responsive, and CI checks
Testing Blocks by the numbers
- 25 all-time installs (skills.sh)
- Ranked #1,392 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
testing blocks capabilities & compatibility
- Capabilities
- testing · unit testing · browser testing · code review
- Works with
- github · playwright
- Use cases
- testing · code review · ci cd
What testing blocks says it does
This skill guides you through testing code changes in AEM Edge Delivery Services projects.
Create and maintain tests when the value they bring exceeds the cost of creation and maintenance.
**Linting passes** - `npm run lint` completes without errors
npx skills add https://github.com/adobe/helix-website --skill testing-blocksAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25 |
|---|---|
| repo stars | ★ 45 |
| Last updated | August 4, 2026 |
| Repository | adobe/helix-website ↗ |
What it does
Test AEM Edge Delivery blocks, scripts, and styles and pass a pre-PR checklist before opening a pull request.
Who is it for?
AEM Edge Delivery developers validating block changes with the right mix of unit and browser tests before a PR
Skip if: Non-AEM projects or writing the block code itself (use building-blocks)
When should I use this skill?
After implementing or modifying AEM blocks, scripts, or styles and before opening a pull request
What you get
Validated block changes with keeper unit tests, browser screenshots, and a passing pre-PR checklist
- Keeper unit tests for logic-heavy utilities
- Browser validation with screenshots for PR review
- Completed pre-PR testing checklist
By the numbers
- 8-item pre-PR testing checklist
- Two-tier model: keeper unit tests vs throwaway browser tests
Files
Testing Blocks
This skill guides you through testing code changes in AEM Edge Delivery Services projects. Testing follows a value-versus-cost philosophy: create and maintain tests when the value they bring exceeds the cost of creation and maintenance.
Related Skills
- content-driven-development: Test content created during CDD serves as the basis for testing
- building-blocks: This skill is automatically invoked after block implementation
- block-collection-and-party: May provide reference test patterns from similar blocks
When to Use This Skill
Use this skill:
- ✅ After implementing or modifying blocks
- ✅ After changes to core scripts (scripts.js, delayed.js, aem.js)
- ✅ After style changes (styles.css, lazy-styles.css)
- ✅ After configuration changes that affect functionality
- ✅ Before opening any pull request with code changes
This skill should be automatically invoked by the building-blocks skill after implementation is complete.
Testing Philosophy: Value vs Cost
The Principle: Create and maintain tests when the value they bring exceeds the cost of creation and maintenance.
Keeper Tests (High Value, Worth Maintaining)
✅ Write unit tests for:
- Logic-heavy utility functions used across multiple blocks
- Data processing and transformation logic
- API integrations and external service interactions
- Complex algorithms or business logic
- Shared libraries and helper functions
These tests provide lasting value because they catch regressions in reused code, serve as living documentation, and are fast and easy to maintain.
Throwaway Tests (Lower Value, Use Once)
⚠️ Use browser tests for:
- Block decoration logic (DOM transformations)
- Specific DOM structures or UI layouts
- Visual appearance validation
- Block-specific rendering behavior
These tests are better done in a browser because DOM structures change frequently, visual validation requires human judgment, and maintaining UI tests is expensive relative to their value.
Important: Even throwaway tests have value! Use them to: 1. Validate your implementation works correctly 2. Take screenshots to evaluate visual correctness 3. Show screenshots to humans for feedback 4. Include screenshots in PRs to aid review
Organization: Keep throwaway tests in test/tmp/ and test content in drafts/tmp/. Both directories should be gitignored so temporary test artifacts aren't committed.
Testing Checklist
Before opening a pull request, complete ALL of the following:
- [ ] Existing tests pass - All keeper tests still pass with your changes
- [ ] Unit tests written - New keeper tests for any logic-heavy utilities or data processing
- [ ] Browser validation - Feature tested in local dev server, screenshots captured
- [ ] All variants tested - Each variant/configuration of blocks validated
- [ ] Responsive behavior - Tested on mobile, tablet, desktop viewports
- [ ] Linting passes -
npm run lintcompletes without errors - [ ] Branch pushed - Code committed and pushed to feature branch
- [ ] GitHub checks verified - Use
gh checksto confirm all CI checks pass
Testing Methods Overview
1. Unit Tests (KEEPER TESTS)
When to use: Logic-heavy functions, utilities, data processing, API integrations
Quick start:
# Verify test setup (see resources/vitest-setup.md if not configured)
npm test
# Write test for utility function
# test/utils/my-utility.test.js
import { describe, it, expect } from 'vitest';
import { myUtility } from '../../scripts/utils/my-utility.js';
describe('myUtility', () => {
it('should transform input correctly', () => {
expect(myUtility('input')).toBe('OUTPUT');
});
});
# Run tests during development
npm run test:watchDetailed guide: See resources/unit-testing.md
2. Browser Testing (THROWAWAY TESTS)
When to use: Block decoration, visual validation, DOM structure, responsive design
Organization:
- Test scripts:
test/tmp/test-{block}-browser.js - Test content:
drafts/tmp/{block}.html - Screenshots:
test/tmp/screenshots/ - Both
test/tmp/anddrafts/tmp/should be gitignored
Quick start:
# Install Playwright
npm install --save-dev playwright
npx playwright install chromium
# Create test content
# drafts/tmp/my-block.html (copy head.html content, add test markup)
# Start dev server with drafts folder
aem up --html-folder drafts
# Create throwaway test script in test/tmp/
# test/tmp/test-my-block.js
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();
await page.goto('http://localhost:3000/drafts/tmp/my-block');
await page.waitForSelector('.my-block');
await page.screenshot({
path: './test/tmp/screenshots/my-block.png',
fullPage: true
});
await browser.close();
}
test().catch(console.error);
# Run the test
node test/tmp/test-my-block.js
# Clean up when done (optional - gitignored either way)
rm -rf test/tmp/*Detailed guide: See resources/browser-testing.md
3. Linting (ALWAYS)
When to use: Before every commit
Quick start:
# Run linting
npm run lint
# Auto-fix issues
npm run lint:fixLinting MUST pass before opening a PR. Non-negotiable.
4. Performance Testing (AUTOMATED)
When to use: After pushing branch, automatically via GitHub checks
Quick start:
# Push branch
git push -u origin your-branch
# Create PR with test link
# PR description MUST include:
# Preview: https://branch--repo--owner.aem.page/path/to/test
# Monitor checks
gh pr checks --watchPerformance tests run automatically when you include a test link in your PR description.
Complete Workflow
For detailed step-by-step workflow, see resources/testing-workflow.md.
Quick summary:
During Development
1. Write unit tests for new utilities 2. Run npm run test:watch 3. Manually test in browser
Before Committing
4. Run npm test - all tests pass 5. Run npm run lint - linting passes 6. Write throwaway browser test in test/tmp/ 7. Create test content in drafts/tmp/ 8. Review screenshots from test/tmp/screenshots/ 9. Manual validation in browser
Before Opening PR
10. Commit and push to feature branch (test/tmp/ won't be included) 11. Verify branch preview loads 12. Run gh checks 13. Create PR with test link 14. Monitor gh pr checks
After PR Review
15. Address feedback 16. Re-test 17. Verify checks pass
Troubleshooting
For detailed troubleshooting guide, see resources/troubleshooting.md.
Common issues:
Tests fail
- Read error message carefully
- Run single test:
npm test -- path/to/test.js - Fix code or update test
Linting fails
- Run
npm run lint:fix - Manually fix remaining issues
GitHub checks fail
- Ensure PR has test link
- Check
gh pr checksfor details - Fix performance issues if PSI fails
Browser tests fail
- Verify dev server running:
aem up --html-folder drafts - Check test content exists in
drafts/tmp/ - Verify URL uses
/tmp/path:http://localhost:3000/drafts/tmp/my-block - Add waits:
await page.waitForSelector('.block')
Resources
- Unit Testing:
resources/unit-testing.md- Complete guide to writing and maintaining unit tests - Browser Testing:
resources/browser-testing.md- Playwright/Puppeteer workflows and best practices - Testing Workflow:
resources/testing-workflow.md- Step-by-step workflow from dev to PR - Troubleshooting:
resources/troubleshooting.md- Solutions to common testing issues - Vitest Setup:
resources/vitest-setup.md- One-time configuration guide
Integration with Building Blocks Skill
The building-blocks skill automatically invokes this skill after implementation.
Expected flow: 1. Building blocks completes implementation 2. Invokes testing-blocks skill 3. This skill guides testing process 4. Returns control when testing complete
Building blocks provides:
- Block name being tested
- Test content URL from CDD process
- Any variants that need testing
This skill returns:
- Confirmation all tests pass
- Screenshots from browser testing (if requested)
- Any issues discovered during testing
Summary
Testing in AEM Edge Delivery follows a pragmatic value-versus-cost approach:
Create keeper tests for:
- Logic-heavy utilities
- Data processing and transformations
- API integrations
- Shared libraries
Use throwaway browser tests for:
- Block decoration validation
- Visual appearance
- DOM structure
- Interactive behavior
Always do:
- Run linting before commits
- Test manually in browser
- Verify GitHub checks pass
- Include test links in PRs
Remember: The goal is confidence that your code works correctly, not achieving 100% test coverage. Write tests that provide value, and validate everything else in a browser.
Browser Testing Guide
Browser testing validates that blocks, DOM transformations, and visual elements work correctly in a real browser environment. These are throwaway tests - use them once to validate, capture screenshots, then discard them.
When to Use Browser Testing
Use browser testing for:
- Block decoration validation - Does the block transform HTML correctly?
- Visual appearance - Does it look right at different screen sizes?
- Interactive behavior - Do click handlers, forms, and interactions work?
- DOM structure - Is the final rendered HTML correct?
- Responsive design - Does it work on mobile, tablet, desktop?
Browser Testing Tools
Option 1: Playwright (Recommended)
Playwright provides a full browser automation API with excellent developer experience.
Setup:
npm install --save-dev playwright
npx playwright install chromiumExample throwaway test script:
// test-hero-block.js (DO NOT COMMIT)
import { chromium } from 'playwright';
async function testHeroBlock() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// Navigate to test content
await page.goto('http://localhost:3000/drafts/hero-test');
// Wait for block decoration
await page.waitForSelector('.hero');
// Take screenshot for validation
await page.screenshot({ path: 'hero-desktop.png', fullPage: true });
// Test mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: 'hero-mobile.png', fullPage: true });
// Test tablet viewport
await page.setViewportSize({ width: 768, height: 1024 });
await page.screenshot({ path: 'hero-tablet.png', fullPage: true });
// Validate DOM structure
const heroTitle = await page.textContent('.hero h1');
console.log('Hero title:', heroTitle);
// Test interactions
const button = page.locator('.hero .button');
await button.click();
await page.waitForTimeout(1000); // Wait for any animations
await browser.close();
}
testHeroBlock().catch(console.error);Run the test:
node test-hero-block.jsOption 2: Puppeteer
Similar to Playwright but older. Use if Playwright isn't suitable.
npm install --save-dev puppeteerAPI is very similar to Playwright. Substitute puppeteer for playwright in import statements.
Option 3: Browser MCP
If you have access to a Browser MCP server, use it for interactive browser testing through Claude's tools.
Browser Testing Workflow
1. Ensure dev server is running
aem upNote the port (usually 3000).
2. Write throwaway test script
Create a temporary script file (e.g., test-my-block.js) with:
- Navigation to test content URL
- Waiting for block decoration
- Taking screenshots at multiple viewports
- Validating DOM structure or behavior
- Testing interactions
3. Run the script
node test-my-block.js4. Review screenshots
- Examine screenshots visually to validate appearance
- Show screenshots to the user for feedback if needed
- Include screenshots in PR description to aid review
5. Clean up
- Delete the test script (don't commit)
- Keep screenshots temporarily for PR, then delete
Common Testing Scenarios
Testing Multiple Variants
async function testBlockVariants() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const variants = ['default', 'dark', 'light', 'wide'];
for (const variant of variants) {
await page.goto(`http://localhost:3000/drafts/hero-${variant}`);
await page.waitForSelector('.hero');
await page.screenshot({
path: `hero-${variant}.png`,
fullPage: true
});
}
await browser.close();
}Testing Interactive Elements
async function testForm() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000/drafts/contact-form');
await page.waitForSelector('.form');
// Fill out form
await page.fill('input[name="name"]', 'Test User');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('textarea[name="message"]', 'Test message');
// Take screenshot of filled form
await page.screenshot({ path: 'form-filled.png' });
// Submit form
await page.click('button[type="submit"]');
// Wait for success message
await page.waitForSelector('.form-success');
await page.screenshot({ path: 'form-success.png' });
await browser.close();
}Testing Animations and Transitions
async function testCarousel() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000/drafts/carousel');
await page.waitForSelector('.carousel');
// Initial state
await page.screenshot({ path: 'carousel-1.png' });
// Click next button
await page.click('.carousel-next');
await page.waitForTimeout(500); // Wait for animation
await page.screenshot({ path: 'carousel-2.png' });
// Click next again
await page.click('.carousel-next');
await page.waitForTimeout(500);
await page.screenshot({ path: 'carousel-3.png' });
await browser.close();
}Testing Responsive Behavior
async function testResponsive() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000/drafts/header');
await page.waitForSelector('.header');
// Desktop
await page.setViewportSize({ width: 1920, height: 1080 });
await page.screenshot({ path: 'header-desktop.png' });
// Tablet landscape
await page.setViewportSize({ width: 1024, height: 768 });
await page.screenshot({ path: 'header-tablet-landscape.png' });
// Tablet portrait
await page.setViewportSize({ width: 768, height: 1024 });
await page.screenshot({ path: 'header-tablet-portrait.png' });
// Mobile
await page.setViewportSize({ width: 375, height: 812 });
await page.screenshot({ path: 'header-mobile.png' });
await browser.close();
}Browser Testing Best Practices
DO:
- ✅ Test all viewport sizes (mobile, tablet, desktop)
- ✅ Take screenshots for visual validation
- ✅ Test all block variants in one script
- ✅ Wait for block decoration before capturing state
- ✅ Test interactive elements (clicks, forms, etc.)
- ✅ Show screenshots to humans for feedback
- ✅ Include screenshots in PRs to help reviewers
DON'T:
- ❌ Commit throwaway test scripts to the repository
- ❌ Try to automate visual regression testing (not worth the maintenance)
- ❌ Write brittle assertions about specific DOM structure
- ❌ Spend time making these tests maintainable (they're throwaway)
- ❌ Test the same thing in both unit tests and browser tests
Playwright Tips and Tricks
Waiting for elements
// Wait for selector
await page.waitForSelector('.my-block');
// Wait for specific text
await page.waitForSelector('text=Click me');
// Wait for network idle
await page.waitForLoadState('networkidle');Taking targeted screenshots
// Screenshot of specific element
await page.locator('.hero').screenshot({ path: 'hero-only.png' });
// Full page screenshot
await page.screenshot({ path: 'full-page.png', fullPage: true });
// Screenshot with specific viewport
await page.screenshot({
path: 'mobile.png',
fullPage: true,
clip: { x: 0, y: 0, width: 375, height: 812 }
});Debugging
// Launch browser in non-headless mode
const browser = await chromium.launch({ headless: false });
// Slow down actions
const browser = await chromium.launch({ slowMo: 500 });
// Pause execution
await page.pause();Extracting data
// Get text content
const text = await page.textContent('.selector');
// Get attribute value
const href = await page.getAttribute('a', 'href');
// Check if element exists
const exists = await page.locator('.selector').count() > 0;
// Get all matching elements
const items = await page.$$eval('.item', els => els.map(el => el.textContent));When Browser Tests Are Worth Keeping
In rare cases, browser tests might be worth committing and maintaining:
1. Critical user flows - Checkout process, authentication, critical forms 2. Cross-browser compatibility - When you need to test in multiple browsers 3. Accessibility testing - Using specialized tools like axe-core
Even in these cases, keep tests focused on critical functionality only. The cost of maintaining browser tests is high.
Next Steps
After browser testing: 1. Review all screenshots carefully 2. Show screenshots to stakeholders if needed 3. Include key screenshots in your PR 4. Delete the test script (don't commit it) 5. Move on to other testing methods (linting, unit tests, etc.)
Remember: Browser tests are a validation tool, not a regression prevention tool. Use them to confirm your implementation works, then move on.
Complete Testing Workflow
This guide provides a step-by-step workflow for testing code changes from development through pull request approval.
Workflow Phases
Phase 1: During Development
1. Write unit tests as you code
For any new utility functions or logic-heavy code, write unit tests alongside implementation:
# Open test in watch mode for immediate feedback
npm run test:watchThis gives you instant feedback as you develop. Tests should be passing before you consider code complete.
2. Run tests in watch mode
Keep npm run test:watch running during development to catch regressions immediately.
3. Manually test in browser
View your changes in the local dev server as you work:
# Start dev server if not already running
aem upNavigate to your test content and verify the implementation looks and behaves correctly.
Phase 2: Before Committing
4. Run full test suite
Ensure all keeper tests pass:
npm testIf tests fail:
- Review the error messages
- Fix broken functionality OR update tests if requirements changed
- Re-run
npm testuntil all pass
5. Run linting
npm run lintIf linting fails:
# Auto-fix what can be fixed
npm run lint:fix
# Manually fix remaining issues
# Re-run to verify
npm run lintLinting MUST pass before committing. Non-negotiable.
6. Write throwaway browser test
Create a temporary test script (e.g., test-my-block.js) to validate block behavior:
// test-my-block.js (DO NOT COMMIT)
import { chromium } from 'playwright';
async function testMyBlock() {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
// Test desktop
await page.goto('http://localhost:3000/drafts/my-block-test');
await page.waitForSelector('.my-block');
await page.screenshot({ path: 'my-block-desktop.png', fullPage: true });
// Test mobile
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: 'my-block-mobile.png', fullPage: true });
await browser.close();
}
testMyBlock().catch(console.error);7. Run browser test
Execute the script and review screenshots:
node test-my-block.jsReview each screenshot:
- Does it look correct?
- Are all variants working?
- Is responsive behavior correct?
- Do interactions work?
8. Manual validation
Double-check test content in browser yourself. Don't rely solely on screenshots. Click around, test interactions, check on real devices if possible.
Phase 3: Before Opening PR
9. Commit and push
git add .
git commit -m "Your commit message"
git push -u origin your-feature-branch10. Verify branch is accessible
Check that your branch preview loads:
https://your-branch--repo--owner.aem.page/path/to/test-contentVisit this URL in your browser to ensure the content is accessible for PSI checks.
11. Run gh checks
Verify CI checks pass:
gh pr checksOr if you haven't created the PR yet, you can check branch status:
gh pr status12. Create PR with test link
Your PR description MUST include a test link for PSI checks:
## Related Issues
Fixes #123
## Summary
Added new hero block with support for multiple variants.
## Testing
Preview: https://hero-block--shsteimer-com--shsteimer.aem.page/drafts/hero-testThe test link is required for automatic performance testing.
13. Monitor GitHub checks
After creating the PR, monitor checks:
# Watch checks in real-time
gh pr checks --watch
# Or check once
gh pr checksWait for all checks to pass, especially PSI performance checks.
Phase 4: After PR Review
14. Address feedback
If reviewers request changes:
- Make the requested modifications
- Follow the same workflow (test, lint, browser test)
15. Re-test
After making changes:
# Run unit tests
npm test
# Run linting
npm run lint
# Create new browser test if needed
node test-updated-block.js16. Verify checks pass
gh pr checksEnsure all CI checks pass on the latest commit before requesting re-review.
Quick Reference Checklist
Use this checklist for every code change:
During Development:
- [ ] Write unit tests for new utilities/logic
- [ ] Run
npm run test:watchduring development - [ ] Manually test in browser at
http://localhost:3000
Before Committing:
- [ ] Run
npm test- all tests pass - [ ] Run
npm run lint- linting passes - [ ] Write throwaway browser test script
- [ ] Run browser test and review screenshots
- [ ] Manually validate in browser
Before Opening PR:
- [ ] Commit and push to feature branch
- [ ] Verify branch preview loads
- [ ] Run
gh checksto verify CI passes - [ ] Create PR with test link for PSI checks
- [ ] Monitor
gh pr checksuntil all pass
After PR Review:
- [ ] Address all feedback
- [ ] Re-run tests and linting
- [ ] Verify
gh pr checkspass on latest commit
Integration with Other Skills
This workflow integrates with other AEM skills:
content-driven-development provides:
- Test content URLs for validation
- Content model for testing against
building-blocks invokes testing-blocks:
- After block implementation
- With block name and test URLs
This skill returns:
- Confirmation tests pass
- Screenshots for validation
- Any issues discovered
Common Workflow Variations
For Bug Fixes
1. Write a failing test that reproduces the bug 2. Fix the bug 3. Verify the test now passes 4. Follow standard workflow
For Refactoring
1. Ensure existing tests pass before starting 2. Make refactoring changes 3. Verify all tests still pass 4. No new tests needed if behavior unchanged
For New Features
1. Follow content-driven-development first 2. Create test content 3. Implement feature with unit tests 4. Browser test to validate 5. Follow standard workflow
Tips for Efficient Testing
Use watch mode during development:
npm run test:watchRun single test file:
npm test -- test/utils/my-utility.test.jsRun tests matching pattern:
npm test -- --grep "checkDomain"Generate coverage report:
npm run test:coverageMonitor GitHub checks continuously:
gh pr checks --watchNext Steps
After completing this workflow: 1. Delete throwaway browser test scripts 2. Keep screenshots for PR, then delete 3. Celebrate passing tests! 🎉 4. Monitor PR for review feedback
Remember: The goal is confidence your code works, not perfection. Follow the workflow, fix what breaks, and ship with confidence.
Testing Troubleshooting Guide
Common issues encountered during testing and how to resolve them.
Tests Fail After Changes
Unit Tests Fail
Symptoms:
- Test suite runs but some tests fail
- Error messages about unexpected values or behaviors
Diagnosis:
1. Read the error message carefully
- What assertion failed?
- What was expected vs actual?
- Which test file and line number?
2. Determine the cause:
- Did you break existing functionality?
- Did requirements change?
- Is the test incorrect?
Solutions:
If you broke functionality:
# Fix the code, not the test
# Re-run tests
npm testIf requirements changed:
# Update the test to match new requirements
# Ensure the change is intentional
npm testIf testing a specific file:
# Run just one test file for faster feedback
npm test -- test/utils/my-utility.test.jsIf debugging a specific test:
# Use test.only to run just one test
it.only('should do something', () => {
// This test will be the only one that runs
});Browser Tests Fail
Symptoms:
- Browser test script throws errors
- Screenshots not generated
- Timeouts occur
Common Issues and Solutions:
Issue: Dev server not running
# Start the dev server
aem up
# Verify it's accessible
curl http://localhost:3000Issue: Test content doesn't exist
- Verify the URL manually in a browser
- Check that content is published/previewed
- Ensure path in test script is correct
Issue: Block not decorated
// Add wait for specific selector
await page.waitForSelector('.my-block');
// Or wait for network idle
await page.waitForLoadState('networkidle');
// Or increase timeout
await page.waitForSelector('.my-block', { timeout: 10000 });Issue: Timing problems
// Wait for animations to complete
await page.waitForTimeout(500);
// Wait for specific state
await page.waitForFunction(() => {
return document.querySelector('.my-block').classList.contains('loaded');
});Issue: Elements not clickable
// Wait for element to be clickable
await page.locator('.button').click({ force: false });
// Or scroll into view first
await page.locator('.button').scrollIntoViewIfNeeded();
await page.locator('.button').click();Linting Fails
Common Linting Issues
Unused variables
// Error: 'foo' is defined but never used
const foo = 'bar';
// Fix: Remove unused variable
// Or use itMissing semicolons
// Error: Missing semicolon
const foo = 'bar'
// Fix: Add semicolon
const foo = 'bar';Incorrect indentation
# Auto-fix indentation issues
npm run lint:fixConsole.log statements
// Error: Unexpected console statement
console.log('debug info');
// Fix Option 1: Remove it
// (remove the line)
// Fix Option 2: Disable rule for this line
// eslint-disable-next-line no-console
console.log('debug info');Import issues
// Error: Unable to resolve path to module
import { foo } from './utils';
// Fix: Add .js extension
import { foo } from './utils.js';Airbnb style violations
// Error: Expected a line break after this opening brace
import { foo, bar } from './utils.js';
// Fix: Let lint:fix handle it
// Or manually format:
import {
foo,
bar,
} from './utils.js';Quick Fixes
Auto-fix everything possible:
npm run lint:fixCheck what would be fixed:
npm run lint:js -- --fix-dry-runLint specific files:
npm run lint:js -- test/utils/my-test.test.jsGitHub Checks Fail
Checks Don't Run
Issue: PSI checks not running
Cause: Missing test link in PR description
Solution:
## Testing
Preview: https://branch--repo--owner.aem.page/path/to/testThe test link MUST be in the PR description for PSI checks to run.
Issue: Actions workflow disabled
Check repository settings → Actions → ensure workflows are enabled.
PSI Checks Fail
Issue: Poor performance score
Common causes:
- Too much JavaScript loaded eagerly
- CSS blocking render
- Large unoptimized images
- Third-party scripts in eager phase
Solutions:
Move JavaScript to lazy or delayed phase:
// In scripts.js, move non-LCP code to loadLazy() or loadDelayed()Optimize images:
- Use WebP format
- Lazy load below-the-fold images
- Use appropriate dimensions
Defer non-critical CSS:
/* Move non-LCP styles to lazy-styles.css */Review performance report:
# Check details in GitHub PR checks
gh pr checks
# Click on failed check for detailsLinting Checks Fail in CI
Issue: Tests pass locally but fail in CI
Cause: Linting not run locally or different config
Solution:
# Always run lint before committing
npm run lint
# Ensure .eslintrc.js is committed
git add .eslintrc.js
git commit -m "Add eslint config"Tests Pass Locally But Fail in CI
Different Node Version
Symptoms:
- Syntax errors in CI
- Missing features
Solution:
Check CI Node version matches local:
# Check local version
node --version
# Add .nvmrc if needed
echo "20" > .nvmrc
git add .nvmrc
git commit -m "Add nvmrc for consistent Node version"Missing Dependencies
Symptoms:
- Module not found errors in CI
- Dependencies work locally
Solution:
# Ensure package.json is up to date
git add package.json package-lock.json
git commit -m "Update dependencies"
# Or regenerate lock file
rm package-lock.json
npm install
git add package-lock.jsonEnvironment Differences
Symptoms:
- Tests dependent on local files fail
- Absolute paths don't work
Solution:
Use relative paths and environment-agnostic code:
// Bad: Absolute path
const path = '/Users/me/project/file.js';
// Good: Relative path
const path = './file.js';Race Conditions
Symptoms:
- Tests sometimes pass, sometimes fail
- Timing-dependent failures
Solution:
Add proper waits:
// Bad: No wait
it('should update text', async () => {
await updateText();
expect(getText()).toBe('updated');
});
// Good: Wait for update
it('should update text', async () => {
await updateText();
await waitFor(() => getText() === 'updated');
expect(getText()).toBe('updated');
});Vitest-Specific Issues
Module Resolution Errors
Error:
Failed to resolve import './utils.js'Solution:
Check import paths are correct:
// From test/utils/my-test.test.js
// Importing from scripts/utils/my-utility.js
// Correct:
import { myUtility } from '../../scripts/utils/my-utility.js';
// Wrong (missing ../../):
import { myUtility } from 'scripts/utils/my-utility.js';jsdom Errors
Error:
document is not definedSolution:
Ensure vitest.config.js has jsdom environment:
export default defineConfig({
test: {
environment: 'jsdom', // Important!
},
});Coverage Not Generated
Error:
Coverage provider not foundSolution:
Install coverage package:
npm install --save-dev @vitest/coverage-v8Getting Help
If you're stuck:
1. Read the error message completely - Often contains the solution 2. Check the documentation - vitest.dev, playwright.dev 3. Search existing issues - GitHub issues for Vitest/Playwright 4. Simplify - Create minimal reproduction of the issue 5. Ask for help - Include error messages and context
Preventive Measures
To avoid issues:
- ✅ Run
npm testbefore every commit - ✅ Run
npm run lintbefore every commit - ✅ Use
npm run test:watchduring development - ✅ Test in browser manually before opening PR
- ✅ Include test links in PR descriptions
- ✅ Monitor
gh pr checksafter creating PR
Don't:
- ❌ Skip testing locally
- ❌ Commit failing tests
- ❌ Ignore linting errors
- ❌ Force push without re-running tests
- ❌ Merge PRs with failing checks
Quick Diagnostic Commands
# Check Node version
node --version
# Check npm version
npm --version
# Verify Vitest installed
npm list vitest
# Verify test config
cat vitest.config.js
# Check dev server running
curl http://localhost:3000
# Verify git status
git status
# Check GitHub PR status
gh pr checksNext Steps
After resolving issues:
1. Re-run all checks 2. Verify everything passes 3. Commit fixes if needed 4. Update documentation if issue was unclear 5. Help others avoid the same issue
Remember: Every error is a learning opportunity. Take time to understand why something failed, not just how to fix it.
Unit Testing Guide
Unit tests are keeper tests for logic-heavy code that benefits from automated regression testing. This guide covers when to write unit tests, how to structure them, and best practices for maintainable test suites.
When to Write Unit Tests
Write unit tests for:
- Pure functions - Functions with no side effects that transform inputs to outputs
- Utility libraries - Shared helper functions used across blocks
- Data processors - Code that parses, transforms, or validates data
- API integrations - Functions that interact with external services
- Complex algorithms - Business logic, calculations, or conditional flows
Do NOT write unit tests for:
- Block decoration functions (test these in browser)
- DOM manipulation logic (test in browser)
- CSS styles (test in browser)
- Simple getters/setters
- Code that primarily renders UI
Prerequisites
This guide assumes Vitest is already configured in the project. If not, see vitest-setup.md for one-time setup instructions.
Verify test setup exists:
npm test # Should run without errors (even if no tests exist yet)If the command fails or Vitest is not installed, consult vitest-setup.md.
Important: Ensure test files are not served to production by adding them to .hlxignore:
# .hlxignore
test/
*.test.jsThis prevents test files from being accessible on your live site.
Writing Unit Tests
File Location and Naming
Place test files next to the code they test:
scripts/utils/my-utility.js→test/utils/my-utility.test.jsblocks/hero/utils.js→test/blocks/hero/utils.test.js
Naming convention: {filename}.test.js
Test Structure
import { describe, it, expect } from 'vitest';
import { myUtility } from '../../scripts/utils/my-utility.js';
describe('myUtility', () => {
it('should transform input correctly', () => {
const input = { foo: 'bar' };
const result = myUtility(input);
expect(result).toEqual({ foo: 'BAR' });
});
it('should handle edge cases', () => {
expect(myUtility(null)).toBeNull();
expect(myUtility({})).toEqual({});
});
});Running Tests
npm test # Run all tests once
npm run test:watch # Run tests in watch mode
npm run test:ui # Open interactive UI
npm run test:coverage # Generate coverage reportWhat Makes a Good Unit Test
Good unit tests are:
- Fast - Run in milliseconds
- Isolated - Test one function/unit at a time
- Repeatable - Same input always produces same output
- Self-validating - Pass or fail clearly, no manual inspection
- Focused - Test one behavior per test case
Complete Example
Here's an example of a keeper test that's worth maintaining:
// scripts/utils/url-helpers.js
export function normalizeUrl(url, base) {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
if (url.startsWith('/')) return `${base}${url}`;
return `${base}/${url}`;
}
// test/utils/url-helpers.test.js
import { describe, it, expect } from 'vitest';
import { normalizeUrl } from '../../scripts/utils/url-helpers.js';
describe('normalizeUrl', () => {
const base = 'https://example.com';
it('returns empty string for null/undefined', () => {
expect(normalizeUrl(null, base)).toBe('');
expect(normalizeUrl(undefined, base)).toBe('');
});
it('returns absolute URLs unchanged', () => {
expect(normalizeUrl('https://other.com/path', base)).toBe('https://other.com/path');
expect(normalizeUrl('http://other.com/path', base)).toBe('http://other.com/path');
});
it('prepends base to root-relative URLs', () => {
expect(normalizeUrl('/path/to/page', base)).toBe('https://example.com/path/to/page');
});
it('prepends base with slash to relative URLs', () => {
expect(normalizeUrl('path/to/page', base)).toBe('https://example.com/path/to/page');
});
});Why this test is worth maintaining:
- URL normalization is used across many blocks
- Bugs here would break multiple features
- Logic is complex enough to benefit from regression tests
- Test is fast and easy to maintain
Testing with jsdom
When testing DOM-dependent code, jsdom provides a browser-like environment:
import { describe, it, expect, beforeEach } from 'vitest';
import { JSDOM } from 'jsdom';
describe('DOM manipulation', () => {
let document;
beforeEach(() => {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
document = dom.window.document;
global.document = document;
});
it('should create elements', () => {
const div = document.createElement('div');
div.textContent = 'Hello';
expect(div.textContent).toBe('Hello');
});
});Integration Tests
Integration tests validate that multiple components work together correctly. These fall into the "keeper" category if they test critical workflows.
When to write integration tests:
- Auto-blocking logic that depends on multiple functions
- Complex workflows spanning multiple utilities
- Critical user journeys that depend on multiple blocks
Integration tests use the same Vitest setup as unit tests but test multiple components together.
Example integration test:
// test/integration/auto-blocking.test.js
import { describe, it, expect, beforeEach } from 'vitest';
import { JSDOM } from 'jsdom';
import { buildAutoBlocks } from '../../scripts/scripts.js';
describe('Auto-blocking integration', () => {
let document;
beforeEach(() => {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
document = dom.window.document;
global.document = document;
});
it('should auto-block hero from first section with image', () => {
const main = document.createElement('main');
const section = document.createElement('div');
const picture = document.createElement('picture');
section.appendChild(picture);
main.appendChild(section);
buildAutoBlocks(main);
const hero = main.querySelector('.hero');
expect(hero).toBeTruthy();
expect(hero.querySelector('picture')).toBeTruthy();
});
});Integration tests are worth maintaining if:
- The workflow is critical to site functionality
- Multiple teams/developers work on related code
- Bugs in this integration would be expensive to fix
Best Practices
1. Test behavior, not implementation - Focus on what the function does, not how it does it 2. Use descriptive test names - Test names should explain what they're testing 3. One assertion per test - Or multiple assertions testing the same behavior 4. Avoid test interdependence - Each test should be able to run independently 5. Keep tests simple - Tests should be easier to understand than the code they test 6. Mock external dependencies - API calls, file system access, etc. 7. Test edge cases - Null, undefined, empty strings, boundary values
Common Patterns
Testing async functions
it('should fetch data asynchronously', async () => {
const result = await fetchData();
expect(result).toBeDefined();
});Using beforeEach for setup
describe('Calculator', () => {
let calculator;
beforeEach(() => {
calculator = new Calculator();
});
it('should add numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
});Testing error conditions
it('should throw error for invalid input', () => {
expect(() => {
validateInput('invalid');
}).toThrow('Invalid input');
});Next Steps
Once you've written unit tests: 1. Run tests during development with npm run test:watch 2. Run full test suite before commits with npm test 3. Monitor coverage with npm run test:coverage 4. Keep tests updated as code evolves
Vitest Setup Guide
This guide covers the one-time setup of Vitest for unit testing in AEM Edge Delivery projects. Once configured, you won't need to repeat these steps.
Installation
Install Vitest and required dependencies:
npm install --save-dev vitest @vitest/ui jsdomOptional but recommended for coverage reports:
npm install --save-dev @vitest/coverage-v8Configuration
Create vitest.config.js in the project root:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom',
globals: true,
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: [
'node_modules/',
'test/',
'**/*.config.js',
],
},
},
});Configuration Options Explained
- environment: 'jsdom' - Provides browser-like environment for testing DOM code
- globals: true - Makes
describe,it,expectavailable without imports - coverage - Configures code coverage reporting (optional but useful)
Package.json Scripts
Add test scripts to your package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest run --coverage"
}
}Script Descriptions
- `npm test` - Run all tests once (use in CI or before commits)
- `npm run test:watch` - Run tests in watch mode (use during development)
- `npm run test:ui` - Open interactive web UI for tests
- `npm run test:coverage` - Generate code coverage report
Directory Structure
Create directories for test files:
mkdir -p test/utils
mkdir -p test/blocksRecommended Structure
project-root/
├── scripts/
│ └── utils/
│ └── my-utility.js
├── blocks/
│ └── hero/
│ ├── hero.js
│ ├── hero.css
│ └── utils.js
└── test/
├── utils/
│ └── my-utility.test.js
└── blocks/
└── hero/
└── utils.test.jsTest file naming: {filename}.test.js
Verify Installation
Run tests to verify setup:
npm testIf no tests exist yet, you should see:
No test files found, exiting with code 0Create a simple test to verify everything works:
// test/example.test.js
import { describe, it, expect } from 'vitest';
describe('Vitest setup', () => {
it('should be configured correctly', () => {
expect(true).toBe(true);
});
});Run tests again:
npm testYou should see the test pass. Once verified, delete test/example.test.js.
Usage with jsdom
When testing DOM-dependent code, jsdom provides a browser-like environment:
import { describe, it, expect, beforeEach } from 'vitest';
import { JSDOM } from 'jsdom';
describe('DOM manipulation', () => {
let document;
beforeEach(() => {
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>');
document = dom.window.document;
global.document = document;
});
it('should create elements', () => {
const div = document.createElement('div');
div.textContent = 'Hello';
expect(div.textContent).toBe('Hello');
});
});CI Integration
For GitHub Actions or other CI, tests run automatically with:
npm testConsider adding to your CI workflow:
- name: Run tests
run: npm test
- name: Generate coverage
run: npm run test:coverageTroubleshooting
"vitest: command not found"
- Ensure
vitestis indevDependenciesin package.json - Run
npm install
"Cannot find module 'jsdom'"
- Install jsdom:
npm install --save-dev jsdom
Tests not finding imports
- Check import paths are correct relative to test file location
- Ensure files being tested export the functions properly
Coverage not working
- Install coverage provider:
npm install --save-dev @vitest/coverage-v8
Next Steps
Once setup is complete: 1. Write unit tests following the patterns in the main testing-blocks skill 2. Run tests during development with npm run test:watch 3. Run full test suite before commits with npm test 4. Monitor coverage with npm run test:coverage
This is a one-time setup. Once configured, focus on writing valuable tests rather than configuration.