-
Notifications
You must be signed in to change notification settings - Fork 0
Set up Playwright for local, manual e2e testing (LS-2335) #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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') }); | ||
|
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, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/specsRepository: 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.tsRepository: lightspeedwp/ls-theme Length of output: 4742 Require
🤖 Prompt for AI Agents |
||
|
|
||
| /* 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, | ||
| // }, | ||
| }); | ||
| 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); | ||
| } |
There was a problem hiding this comment.
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:
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.dirnameproperty is not available [1][2]. Theimport.meta.dirnameandimport.meta.filenameproperties 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 CommonJSdirnamevariable by deriving the path fromimport.meta.urlusing thenode:urlandnode:pathmodules [1][5]: import { fileURLToPath } from 'node:url'; import { dirname } from 'node:path'; const filename = fileURLToPath(import.meta.url); const dirname = dirname(filename);Citations:
import.meta.dirname/import.meta.filename(available since node20.11.0) compat-table/node-compat-table#92Support the declared Node.js version range.
package.jsonallows Node.js 20.0.0, butimport.meta.dirnameis only available from Node.js 20.11.0. On earlier supported Node.js 20 releases, Playwright cannot load this config. Usepath.dirname(fileURLToPath(import.meta.url)), or raise the engine floor. (nodejs.org)🤖 Prompt for AI Agents