Skip to content

Commit

Permalink
feat(playwright): file loading, snapshots, and browsers (oh my!)
Browse files Browse the repository at this point in the history
feat: added basic visual regression testing
feat: added choice of browser: chromium, firefox, webkit
feat: added options:
    - host: the base url host
    - port: the base url port
    - screenshotDir: the directory for sceenshots
    - nojsTags: tags to start the browser without js
    - showBrowserTags: tags to show a browser for that test
    - slowMoTags: tags to run that test with slowMo enabled
    - slowMoMs: number of milliseconds to run slowMo
feat: added world.baseUrl getter
fix: remove timeouts for all step defintions (use slowmo instead)
fix: fixed expression for outcome step "I should see {string} element with {string}"
feat: added outcome step "the user agent should contain/be {string}"
feat: added outcome step "the screenshot should match"
feat: added outcome step "the screenshot {string} should match"
fix: make the action steps for navigation respect the config
feat: added action step "I load the file {string}"
fix: fixed the screenshot steps

test: added test html files for faster loading
test: removed old tests dependent on internet
test: tests for concurrent testing in different browsers
test: tests for showing brorwser and slowmo
test: tests for visual regression testing
  • Loading branch information
dnotes committed Oct 13, 2024
1 parent ed86abd commit 120d2eb
Show file tree
Hide file tree
Showing 16 changed files with 742 additions and 81 deletions.
24 changes: 24 additions & 0 deletions .changeset/ninety-dragons-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
"@quickpickle/playwright": patch
---

feat(playwright): file loading, snapshots, and browsers (oh my!)

feat: added basic visual regression testing
feat: added choice of browser: chromium, firefox, webkit
feat: added options: - host: the base url host - port: the base url port - screenshotDir: the directory for sceenshots - nojsTags: tags to start the browser without js - showBrowserTags: tags to show a browser for that test - slowMoTags: tags to run that test with slowMo enabled - slowMoMs: number of milliseconds to run slowMo
feat: added world.baseUrl getter
fix: remove timeouts for all step defintions (use slowmo instead)
fix: fixed expression for outcome step "I should see {string} element with {string}"
feat: added outcome step "the user agent should contain/be {string}"
feat: added outcome step "the screenshot should match"
feat: added outcome step "the screenshot {string} should match"
fix: make the action steps for navigation respect the config
feat: added action step "I load the file {string}"
fix: fixed the screenshot steps

test: added test html files for faster loading
test: removed old tests dependent on internet
test: tests for concurrent testing in different browsers
test: tests for showing brorwser and slowmo
test: tests for visual regression testing
7 changes: 7 additions & 0 deletions packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,23 @@
"author": "David Hunt",
"dependencies": {
"@playwright/test": "^1.48.0",
"jest-image-snapshot": "^6.4.0",
"lodash-es": "^4.17.21",
"pixelmatch": "^6.0.0",
"playwright": "^1.48.0",
"quickpickle": "workspace:^",
"vite": "^5.0.11"
},
"devDependencies": {
"@rollup/plugin-replace": "^6.0.1",
"@rollup/plugin-typescript": "^12.1.0",
"@types/jest-image-snapshot": "^6.4.0",
"@types/lodash-es": "^4.17.12",
"@types/pixelmatch": "^5.2.6",
"@types/pngjs": "^6.0.5",
"fast-glob": "^3.3.2",
"playwright-core": "^1.48.0",
"pngjs": "^7.0.0",
"rollup": "^3.20.7",
"typescript": "^5.6.2",
"vitest": "^2.1.2"
Expand Down
3 changes: 3 additions & 0 deletions packages/playwright/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export default {
'vite',
'node:path',
'node:url',
'node:fs',
'lodash-es',
'pngjs',
'pixelmatch',
]
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 41 additions & 14 deletions packages/playwright/src/PlaywrightWorld.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
import { chromium, firefox, webkit, type Browser, type BrowserContext, type Page } from 'playwright';
import { normalizeTags, QuickPickleWorld, QuickPickleWorldInterface } from 'quickpickle';
import { After } from 'quickpickle';
import type { TestContext } from 'vitest';
import { defaults, intersection } from 'lodash-es'
import { defaultsDeep } from 'lodash-es'

export type PlaywrightWorldConfigSetting = {
nojsTags?: string|string[]
const browsers = { chromium, firefox, webkit }

export type PlaywrightWorldConfigSetting = Partial<{
host: string,
port: number,
screenshotDir: string,
nojsTags: string|string[]
showBrowserTags: string|string[]
slowMoTags: string|string[]
headless: boolean
sloMo: number
}
slowMo: boolean|number
slowMoMs: number
}>

export const defaultPlaywrightWorldConfig = {
host: 'http://localhost',
screenshotDir: 'screenshots',
nojsTags: ['@nojs', '@noscript'],
showBrowserTags: ['@browser','@show-browser','@showbrowser'],
slowMoTags: ['@slowmo'],
headless: true,
slowMo: 0,
slowMo: false,
slowMoMs: 500,
}

export type PlaywrightWorldConfig = typeof defaultPlaywrightWorldConfig
export type PlaywrightWorldConfig = typeof defaultPlaywrightWorldConfig & { port?:number }

export class PlaywrightWorld extends QuickPickleWorld {
browser!: Browser
browserContext!: BrowserContext
page!: Page
playwrightConfig:PlaywrightWorldConfig = defaultPlaywrightWorldConfig

constructor(context:TestContext, info:QuickPickleWorldInterface['info']|undefined, worldConfig?:PlaywrightWorldConfigSetting) {
constructor(context:TestContext, info:QuickPickleWorldInterface['info']|undefined, worldConfig:PlaywrightWorldConfigSetting = {}) {
super(context, info)
let newConfig = defaults(defaultPlaywrightWorldConfig, worldConfig || {})
let newConfig = defaultsDeep(worldConfig || {}, defaultPlaywrightWorldConfig, )
newConfig.nojsTags = normalizeTags(newConfig.nojsTags)
newConfig.showBrowserTags = normalizeTags(newConfig.showBrowserTags)
newConfig.slowMoTags = normalizeTags(newConfig.slowMoTags)
if (typeof newConfig.slowMo === 'number') {
newConfig.slowMoMs = newConfig.slowMo
newConfig.slowMo = newConfig.slowMoMs > 0
}
this.playwrightConfig = newConfig
}

async init() {
this.browser = await chromium.launch({
headless: this.playwrightConfig.headless,
slowMo: this.playwrightConfig.slowMo,
let browserName = this.info.tags.find(t => t.match(
/^@(?:chromium|firefox|webkit)$/
))?.replace(/^@/, '') as 'chromium'|'firefox'|'webkit' ?? 'chromium'
this.browser = await browsers[browserName].launch({

Check failure on line 57 in packages/playwright/src/PlaywrightWorld.ts

View workflow job for this annotation

GitHub Actions / Release

tests/playwright.feature > Feature: Basic tests of Playwright browser and steps > Rule: Playwright should support testing with multiple browsers > Example: Running firefox tests

Error: browserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/firefox-1465/firefox/firefox ╔═════════════════════════════════════════════════════════════════════════╗ ║ Looks like Playwright Test or Playwright was just installed or updated. ║ ║ Please run the following command to download new browsers: ║ ║ ║ ║ pnpm exec playwright install ║ ║ ║ ║ <3 Playwright Team ║ ╚═════════════════════════════════════════════════════════════════════════╝ ❯ PlaywrightWorld.init src/PlaywrightWorld.ts:57:48 ❯ initScenario tests/playwright.feature:30:15 ❯ initRuleScenario tests/playwright.feature:59:25 ❯ tests/playwright.feature:73:25

Check failure on line 57 in packages/playwright/src/PlaywrightWorld.ts

View workflow job for this annotation

GitHub Actions / Release

tests/playwright.feature > Feature: Basic tests of Playwright browser and steps > Rule: Playwright should support testing with multiple browsers > Example: Running webkit tests

Error: browserType.launch: Executable doesn't exist at /home/runner/.cache/ms-playwright/webkit-2083/pw_run.sh ╔═════════════════════════════════════════════════════════════════════════╗ ║ Looks like Playwright Test or Playwright was just installed or updated. ║ ║ Please run the following command to download new browsers: ║ ║ ║ ║ pnpm exec playwright install ║ ║ ║ ║ <3 Playwright Team ║ ╚═════════════════════════════════════════════════════════════════════════╝ ❯ PlaywrightWorld.init src/PlaywrightWorld.ts:57:48 ❯ initScenario tests/playwright.feature:30:15 ❯ initRuleScenario tests/playwright.feature:59:25 ❯ tests/playwright.feature:79:25

Check failure on line 57 in packages/playwright/src/PlaywrightWorld.ts

View workflow job for this annotation

GitHub Actions / Release

tests/playwright.feature > Feature: Basic tests of Playwright browser and steps > Rule: It should be possible to set a tag and see the browser with slowMo > Example: Opening a page

Error: browserType.launch: Target page, context or browser has been closed Browser logs: ╔════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ Looks like you launched a headed browser without having a XServer running. ║ ║ Set either 'headless: true' or use 'xvfb-run <your-playwright-app>' before running Playwright. ║ ║ ║ ║ <3 Playwright Team ║ ╚════════════════════════════════════════════════════════════════════════════════════════════════╝ Call log: - <launching> /home/runner/.cache/ms-playwright/chromium-1140/chrome-linux/chrome --disable-field-trial-config --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-back-forward-cache --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --no-default-browser-check --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=ImprovedCookieControls,LazyFrameLoading,GlobalMediaControls,DestroyProfileOnBrowserClose,MediaRouter,DialMediaRouteProvider,AcceptCHFrame,AutoExpandDetailsElement,CertificateTransparencyComponentUpdater,AvoidUnnecessaryBeforeUnloadCheckSync,Translate,HttpsUpgrades,PaintHolding,ThirdPartyStoragePartitioning,LensOverlay --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --force-color-profile=srgb --metrics-recording-only --no-first-run --enable-automation --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-65EE2z --remote-debugging-pipe --no-startup-window - <launched> pid=3269 - [pid=3269][err] [3269:3269:1013/020816.024790:ERROR:ozone_platform_x11.cc(244)] Missing X server or $DISPLAY - [pid=3269][err] [3269:3269:1013/020816.024813:ERROR:env.cc(258)] The platform failed to initialize. Exiting. ❯ PlaywrightWorld.init src/PlaywrightWorld.ts:57:48 ❯ initScenario tests/playwright.feature:30:15 ❯ initRuleScenario tests/playwright.feature:90:25 ❯ tests/playwright.feature:98:25
headless: this.tagsMatch(this.playwrightConfig.showBrowserTags) ? false : this.playwrightConfig.headless,
slowMo: (this.playwrightConfig.slowMo || this.tagsMatch(this.playwrightConfig.slowMoTags)) ? this.playwrightConfig.slowMoMs : 0
})
this.browserContext = await this.browser.newContext({
serviceWorkers: 'block',
javaScriptEnabled: intersection(this.info.tags, this.playwrightConfig.nojsTags)?.length ? false : true,
javaScriptEnabled: this.tagsMatch(this.playwrightConfig.nojsTags) ? false : true,
})
this.page = await this.browserContext.newPage()
}
Expand All @@ -55,6 +77,11 @@ export class PlaywrightWorld extends QuickPickleWorld {
async close() {
await this.browser.close()
}

get baseUrl() {
if (this.playwrightConfig.port) return new URL(`${this.playwrightConfig.host}:${this.playwrightConfig.port}`)
else return new URL(this.playwrightConfig.host)
}
}

After(async (world:PlaywrightWorld) => {
Expand Down
23 changes: 10 additions & 13 deletions packages/playwright/src/actions.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import url from 'node:url'
export const projectRoot = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..')

Given('I am on {string}', async function (world:PlaywrightWorld, path) {
let url = new URL(path, 'http://localhost:5173')
let url = new URL(path, world.baseUrl)
await world.page.goto(url.href)
await world.page.waitForTimeout(80)
})
When(`I visit {string}`, async function (world:PlaywrightWorld, path) {
let url = new URL(path, 'http://localhost:5173')
let url = new URL(path, world.baseUrl)
await world.page.goto(url.href)
await world.page.waitForTimeout(80)
})
When(`I navigate/go to {string}`, async function (world:PlaywrightWorld, path) {
let url = new URL(path, 'http://localhost:5173')
let url = new URL(path, world.baseUrl)
await world.page.goto(url.href)
await world.page.waitForTimeout(80)
})

When('I load the file {string}', async (world:PlaywrightWorld, path) => {
await world.page.goto(`file://${projectRoot}/${path}`)
})

When('I click/press/tap/touch (on ){string}', async function (world:PlaywrightWorld, identifier) {
let locator = await world.page.getByText(identifier).or(world.page.locator(identifier))
Expand Down Expand Up @@ -100,12 +100,9 @@ When(/^I go (back|forwards?)$/, async function (world:PlaywrightWorld, direction
else await world.page.goForward()
})

Then('(I )take (a )screenshot', async function (world:PlaywrightWorld, str:string) {
await world.page.waitForTimeout(50)
await world.page.screenshot({ path: `${projectRoot}/screenshots/${world.info.scenario}__${world.info.line}.png`.replace(/\/\//g,'/') })
Then('(I )take (a )screenshot', async function (world:PlaywrightWorld) {
await world.page.screenshot({ path: `${projectRoot}/${world.playwrightConfig.screenshotDir}/${world.info.rule ? world.info.rule + '__' + world.info.scenario : world.info.scenario}__${world.info.line}.png`.replace(/\/\//g,'/') })
})
Then('(I )take (a )screenshot #{int} in (the folder ){string}', async function (world:PlaywrightWorld, dir:string) {
await world.page.waitForTimeout(50)
await world.page.screenshot({ path: `${projectRoot}/${dir}/${world.info.scenario}__${world.info.line}.png`.replace(/\/\//g,'/') })
Then('(I )take (a )screenshot named {string}', async function (world:PlaywrightWorld, name:string) {
await world.page.screenshot({ path: `${projectRoot}/${world.playwrightConfig.screenshotDir}/${name}.png`.replace(/\/\//g,'/') })
})

29 changes: 14 additions & 15 deletions packages/playwright/src/outcomes.steps.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,45 @@
import { Then } from "quickpickle";
import type { PlaywrightWorld } from "./PlaywrightWorld";
import { expect } from '@playwright/test'
import './snapshotMatcher'

Then('I should see {string}', async function (world:PlaywrightWorld, text) {
await world.page.waitForTimeout(50)
await expect(world.page.getByText(text)).toBeVisible()
})
Then('I should not see {string}', async function (world:PlaywrightWorld, text) {
await world.page.waitForTimeout(50)
await expect(world.page.getByText(text)).not.toBeVisible()
})

Then('I should see a(n)/the {string} {word}', async function (world:PlaywrightWorld, identifier, role) {
await world.page.waitForTimeout(50)
if (role === 'element') await expect(world.page.locator(identifier)).toBeVisible()
else await expect(world.page.getByRole(role, { name: identifier })).toBeVisible()
})
Then('I should not see a(n)/the {string} {word}', async function (world:PlaywrightWorld, identifier, role) {
await world.page.waitForTimeout(50)
if (role === 'element') await expect(world.page.locator(identifier)).not.toBeVisible()
else await expect(world.page.getByRole(role, { name: identifier })).not.toBeVisible()
})

Then('I should see a(n)/the {string} with the text {string}', async function (world:PlaywrightWorld, identifier, text) {
await world.page.waitForTimeout(50)
Then('I should see a(n)/the {string} (element )with (the )(text ){string}', async function (world:PlaywrightWorld, identifier, text) {
await expect(world.page.locator(identifier).filter({ hasText: text })).toBeVisible()
})
Then('I should not see a(n)/the {string} with the text {string}', async function (world:PlaywrightWorld, identifier, text) {
await world.page.waitForTimeout(50)
Then('I should not see a(n)/the {string} (element )with (the )(text ){string}', async function (world:PlaywrightWorld, identifier, text) {
await expect(world.page.locator(identifier).filter({ hasText: text })).not.toBeVisible()
})



Then('the (value of ){string} (value )should contain/include/be/equal {string}', async function (world:PlaywrightWorld, identifier, val) {
let exact = world.info.step?.match(/should not (?:be|equal) ["']/) ? true : false
await world.page.waitForTimeout(50)
let value = await (await world.page.locator(identifier)).inputValue()
if (exact) await expect(value).toEqual(val)
else await expect(value).toContain(val)
})
Then('the (value of ){string} (value )should not contain/include/be/equal {string}', async function (world:PlaywrightWorld, identifier, val) {
let exact = world.info.step?.match(/should not (?:be|equal) ["']/) ? true : false
await world.page.waitForTimeout(50)
let value = await (await world.page.locator(identifier)).inputValue()
if (exact) await expect(value).not.toEqual(val)
else await expect(value).not.toContain(val)
})

Then(/^the metatag for "([^"]+)" should (be|equal|contain) "(.*)"$/, async function (world:PlaywrightWorld, name, eq, value) {
await world.page.waitForTimeout(50)
let val:string|null

if (name === 'title') val = await world.page.title()
Expand All @@ -61,13 +51,22 @@ Then(/^the metatag for "([^"]+)" should (be|equal|contain) "(.*)"$/, async funct
})

Then('the active element should be {string}', async function (world:PlaywrightWorld, identifier) {
await world.page.waitForTimeout(50)
let locator = await world.page.locator(identifier)
await expect(locator).toBeFocused()
})

Then('the active element should be the {string} {word}', async function (world:PlaywrightWorld, identifier, role) {
await world.page.waitForTimeout(50)
let locator = await world.page.getByRole(role, { name: identifier })
await expect(locator).toBeFocused()
})

Then('the user agent should contain/be {string}', async function (world:PlaywrightWorld, ua) {
await expect(world.browser.browserType().name()).toContain(ua)
})

Then('(the )screenshot should match', async function (world:PlaywrightWorld) {
await expect(world.page).toMatchScreenshot(`${world.playwrightConfig.screenshotDir}/${world.info.rule ? world.info.rule + '__' + world.info.scenario : world.info.scenario}__${world.info.line}.png`)
})
Then('(the )screenshot {string} should match', async function (world:PlaywrightWorld, name:string) {
await expect(world.page).toMatchScreenshot(`${world.playwrightConfig.screenshotDir}/${name}.png`)

Check failure on line 71 in packages/playwright/src/outcomes.steps.ts

View workflow job for this annotation

GitHub Actions / Release

tests/playwright.feature > Feature: Basic tests of Playwright browser and steps > Rule: Visual regression testing must be supported > Example: Passing visual regression test

Error: the screenshot "playwright-example" should match (#38) Failed to compare screenshots: Image sizes do not match. ❯ Proxy.<anonymous> ../../node_modules/.pnpm/[email protected]/node_modules/playwright/lib/matchers/expect.js:225:37 ❯ Object.f src/outcomes.steps.ts:71:28 ❯ Module.qp ../main/src/index.ts:73:46 ❯ tests/playwright.feature:120:13 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { matcherResult: { pass: false, message: 'Failed to compare screenshots: Image sizes do not match.' } }
})
Loading

0 comments on commit 120d2eb

Please sign in to comment.