Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,10 @@ lightspeed.code-workspace
.cache/
.aider*
.continue/

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a portable `theme-color-token-enforcer` skill to audit or fix semantic colour token usage.
- Added a theme-local `pattern-extractor` skill and matching `extract-pattern` prompt wrapper for Figma-to-pattern workflows that honour semantic tokens and CSS-versus-GSAP motion routing.
- Added a CSS-only `Card - Feature` group treatment, a matching inline CTA paragraph style, and an insertable single-card pattern at `patterns/cards/card-feature.php`.
- Added a Playwright end-to-end testing setup (LS-2335), matching the official `create-playwright` scaffold: `@playwright/test`, `dotenv`, and `@types/node` as dev dependencies, and `playwright.config.ts` configured for all 3 browser projects (Chromium, Firefox, WebKit) with `baseURL` read from a local, gitignored `.env` (`BASE_URL=`) so each developer points tests at their own environment — no CI wiring, run manually via `npx playwright test`. Added a real spec (`tests/specs/work-archive.spec.ts`) exercising all 6 generic assertion helpers from `tests/helpers/assertions.ts` (LS-2244) against the live Work Archive template — section order, category-card count/parts, hero link href, related-routes grid reflow at mobile, and stats-grid divider styling.

### Changed

Expand Down
96 changes: 96 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
"watch:css": "npm run sync:breakpoints && sass --watch --no-source-map --no-charset --style=expanded src/scss/animations.scss:assets/css/animations.css src/scss/gsap-animations.scss:assets/css/gsap-animations.css"
},
"devDependencies": {
"@playwright/test": "^1.62.1",
"@types/node": "^26.1.2",
"ajv": "^8.17.1",
"dotenv": "^17.4.2",
"glob": "^11.0.1",
"sass": "^1.99.0"
},
Expand Down
84 changes: 84 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { defineConfig, devices } from '@playwright/test';

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import dotenv from 'dotenv';
import path from 'path';
// This repo's package.json has "type": "module", so __dirname (assumed by
// Playwright's own generated template) isn't available here — using
// Node's documented ESM equivalent instead: https://nodejs.org/api/esm.html#importmetadirname
dotenv.config({ path: path.resolve(import.meta.dirname, '.env') });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n '"node"|import\.meta\.dirname' package.json playwright.config.ts
curl -fsSL https://nodejs.org/download/release/v20.11.0/docs/api/esm.html |
  rg -n 'import\.meta\.dirname|Added in: v20\.11\.0'

Repository: lightspeedwp/ls-theme

Length of output: 930


🌐 Web query:

Node.js v20.10.0 import.meta.dirname documentation

💡 Result:

In Node.js v20.10.0, the import.meta.dirname property is not available [1][2]. The import.meta.dirname and import.meta.filename properties were introduced in Node.js version 20.11.0 [3][2][4]. Because these properties are not available in v20.10.0, developers using this version must replicate the functionality of the CommonJS dirname variable by deriving the path from import.meta.url using the node:url and node:path modules [1][5]: import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const filename = fileURLToPath(import.meta.url); const dirname = dirname(filename);

Citations:


Support the declared Node.js version range.

package.json allows Node.js 20.0.0, but import.meta.dirname is only available from Node.js 20.11.0. On earlier supported Node.js 20 releases, Playwright cannot load this config. Use path.dirname(fileURLToPath(import.meta.url)), or raise the engine floor. (nodejs.org)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright.config.ts` at line 12, Update the Playwright configuration’s
dotenv path resolution to support Node.js 20.0.0 by deriving the directory from
import.meta.url with fileURLToPath and path.dirname instead of using
import.meta.dirname; add or reuse the required imports while preserving the
existing .env path.

Comment on lines +7 to +12

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
// Deviation from the installer default ('./tests'): this repo's tests
// live under tests/specs (established in LS-2244, before this ticket).
testDir: './tests/specs',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: process.env.BASE_URL,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

rg -n -C 3 'BASE_URL|baseURL|page\.goto\(' playwright.config.ts tests/specs

Repository: lightspeedwp/ls-theme

Length of output: 6439


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== candidate test files =="
git ls-files 'tests/specs/*.ts' | sort

echo
echo "== URL constants and goto usages =="
rg -n '^[A-Z_]+URL|page\.goto\(' tests/specs -S

echo
echo "== all page.goto usages in repository =="
rg -n 'page\.goto\(' . -S

echo
echo "== full playwright config outline/content =="
wc -l playwright.config.ts
sed -n '1,120p' playwright.config.ts

Repository: lightspeedwp/ls-theme

Length of output: 4742


Require BASE_URL before starting the suite.

baseURL is used by the existing specs for relative page.goto() calls. If BASE_URL is empty in .env, assigning it directly creates an invalid base URL instead of a clear setup failure. Reject a missing or empty BASE_URL in playwright.config.ts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@playwright.config.ts` at line 34, Update the Playwright configuration’s
baseURL assignment to validate that process.env.BASE_URL is present and
non-empty before the suite starts, and fail clearly during configuration when it
is missing. Preserve the existing baseURL behavior for valid values.


/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},

/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },

/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://localhost:3000',
// reuseExistingServer: !process.env.CI,
// },
});
130 changes: 130 additions & 0 deletions tests/helpers/assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { expect, type Page } from '@playwright/test';

/**
* Generic Playwright assertion helpers for ls-theme.
*
* None of these know about any specific page, pattern, or template — every
* input (selector, text, count, viewport width, expected value) is a
* parameter. They were extracted from testing the Work Archive template,
* but apply to any pattern in the theme with the same shape.
*/

/**
* Asserts a list of text markers appears on the page in that exact order.
* Matching is case-insensitive so callers can write natural-case marker
* text without coupling the test to a `text-transform` CSS style.
*/
export async function expectSectionOrder(page: Page, markers: string[]) {
const bodyText = (await page.locator('body').innerText()).toLowerCase();
const positions = markers.map((marker) => bodyText.indexOf(marker.toLowerCase()));

positions.forEach((pos, i) => {
expect(pos, `Expected to find "${markers[i]}" on the page`).toBeGreaterThan(-1);
});

for (let i = 1; i < positions.length; i++) {
expect(
positions[i],
`Expected "${markers[i]}" to appear after "${markers[i - 1]}"`
).toBeGreaterThan(positions[i - 1]);
}
}

/** Asserts a selector matches exactly `count` elements. */
export async function expectElementCount(page: Page, selector: string, count: number) {
await expect(page.locator(selector)).toHaveCount(count);
}

/**
* Asserts every element matching `cardSelector` contains all of its
* `requiredParts` (each a sub-selector, e.g. 'a', '.wp-block-post-excerpt').
*/
export async function expectCardParts(page: Page, cardSelector: string, requiredParts: string[]) {
const cards = page.locator(cardSelector);
const count = await cards.count();
expect(count, `Expected at least one "${cardSelector}" card`).toBeGreaterThan(0);

for (let i = 0; i < count; i++) {
const card = cards.nth(i);
for (const part of requiredParts) {
await expect(
card.locator(part).first(),
`Card ${i} (${cardSelector}) is missing required part "${part}"`
).toBeAttached({ timeout: 2000 });
}
}
}

/** Asserts a link, found by its visible text, has the expected href. */
export async function expectLinkHref(page: Page, linkText: string, expectedHref: string) {
const link = page.getByRole('link', { name: linkText, exact: false });
await expect(link).toHaveAttribute('href', expectedHref);
}

/**
* Resizes the viewport to `viewportWidth` and asserts the grid at
* `gridSelector` shows exactly `expectedColumns` columns — works for both
* CSS Grid (reads grid-template-columns) and flex-wrap layouts (groups
* elements by their top offset).
*/
export async function expectGridColumnsAtViewport(
page: Page,
gridSelector: string,
viewportWidth: number,
expectedColumns: number,
viewportHeight = 900
) {
await page.setViewportSize({ width: viewportWidth, height: viewportHeight });

const grid = page.locator(gridSelector).first();
const display = await grid.evaluate((el) => getComputedStyle(el).display);

if (display === 'grid') {
const columns = await grid.evaluate(
(el) => getComputedStyle(el).gridTemplateColumns.split(' ').length
);
expect(columns, `Expected ${expectedColumns} grid columns at ${viewportWidth}px`).toBe(
expectedColumns
);
return;
}

// Flex/columns layout: count distinct items sharing the first row's top offset.
const tops: number[] = await grid.evaluate((el) =>
[...el.children].map((child) => Math.round(child.getBoundingClientRect().top))
);
const firstRowCount = tops.filter((t) => t === tops[0]).length;
expect(
firstRowCount,
`Expected ${expectedColumns} items in the first row at ${viewportWidth}px`
).toBe(expectedColumns);
}

/**
* Asserts a computed CSS property on `selector` equals `expectedValue`,
* optionally after resizing to `viewportWidth` first.
*/
export async function expectComputedStyle(
page: Page,
selector: string,
property: string,
expectedValue: string,
viewportWidth?: number,
viewportHeight = 900
) {
if (viewportWidth) {
await page.setViewportSize({ width: viewportWidth, height: viewportHeight });
}

const value = await page
.locator(selector)
.first()
.evaluate((el, prop) => getComputedStyle(el).getPropertyValue(prop).trim(), property);

expect(
value,
`Expected ${selector} to have ${property}: ${expectedValue}${
viewportWidth ? ` at ${viewportWidth}px` : ''
}`
).toBe(expectedValue);
}
Loading
Loading