diff --git a/.gitignore b/.gitignore index f40fbd8..a47d5a4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,8 @@ _site .jekyll-cache .jekyll-metadata vendor +node_modules +test-results +playwright-report +visual-regression-current.png +visual-regression-result.png diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa9e91c --- /dev/null +++ b/README.md @@ -0,0 +1,54 @@ +# Managerbot Website + +## Development + +### Prerequisites + +- Ruby (with Bundler) +- Node.js (for visual regression tests) + +### Running locally + +```bash +bundle install +bundle exec jekyll serve +``` + +The site will be available at `http://localhost:4000`. + +## Visual Regression Testing + +Visual regression tests use Playwright to capture screenshots of the home page and compare them against a baseline to detect unintended visual changes. + +### Setup + +```bash +npm install +npx playwright install chromium +``` + +### Running tests + +1. Start the Jekyll server: + ```bash + bundle exec jekyll serve + ``` + +2. In a separate terminal, run the visual regression test: + ```bash + npm run visual-regression:check + ``` + +### Updating the baseline + +If you've made intentional visual changes, update the baseline snapshot: + +```bash +npm run visual-regression +``` + +### Output + +Each test run saves images in the root directory: +- `visual-regression-current.png` - the current screenshot (always saved) +- `visual-regression-diff.png` - diff image highlighting visual changes (only when differences detected) diff --git a/_config.yml b/_config.yml index 9634894..59bdbe0 100644 --- a/_config.yml +++ b/_config.yml @@ -33,22 +33,11 @@ sass: plugins: - jekyll-feed -# Exclude from processing. -# The following items will not be processed, by default. -# Any item listed under the `exclude:` key here will be automatically added to -# the internal "default list". -# -# Excluded items can be processed by explicitly listing the directories or -# their entries' file path in the `include:` list. -# -# exclude: -# - .sass-cache/ -# - .jekyll-cache/ -# - gemfiles/ -# - Gemfile -# - Gemfile.lock -# - node_modules/ -# - vendor/bundle/ -# - vendor/cache/ -# - vendor/gems/ -# - vendor/ruby/ +exclude: + - node_modules/ + - tests/ + - package.json + - package-lock.json + - playwright.config.ts + - test-results/ + - playwright-report/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0c30a15 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,96 @@ +{ + "name": "hyper-unearthing-visual-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hyper-unearthing-visual-tests", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^20.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..b7a3f5e --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "hyper-unearthing-visual-tests", + "version": "1.0.0", + "scripts": { + "visual-regression": "npx playwright test visual-regression.spec.ts --update-snapshots", + "visual-regression:check": "npx playwright test visual-regression.spec.ts" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "@types/node": "^20.0.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..c59b9d8 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 0, + workers: 1, + reporter: 'list', + use: { + baseURL: 'http://localhost:4000', + trace: 'off', + }, + snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}', + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.01, + }, + }, +}); diff --git a/tests/__snapshots__/visual-regression.spec.ts/home-page.png b/tests/__snapshots__/visual-regression.spec.ts/home-page.png new file mode 100644 index 0000000..4ff1dc0 Binary files /dev/null and b/tests/__snapshots__/visual-regression.spec.ts/home-page.png differ diff --git a/tests/visual-regression.spec.ts b/tests/visual-regression.spec.ts new file mode 100644 index 0000000..ff87b76 --- /dev/null +++ b/tests/visual-regression.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; +import * as path from 'path'; + +const OUTPUT_DIR = path.join(__dirname, '..'); +const DIFF_IMAGE = path.join(OUTPUT_DIR, 'visual-regression-diff.png'); +const CURRENT_IMAGE = path.join(OUTPUT_DIR, 'visual-regression-current.png'); + +test('home page visual regression', async ({ page }, testInfo) => { + await page.goto('/'); + + // Wait for the page to be fully loaded + await page.waitForLoadState('networkidle'); + + // Always save the current screenshot + const screenshot = await page.screenshot({ fullPage: true }); + fs.writeFileSync(CURRENT_IMAGE, screenshot); + + try { + await expect(page).toHaveScreenshot('home-page.png', { + fullPage: true, + maxDiffPixelRatio: 0.01, + }); + + // Test passed - remove diff image if it exists + if (fs.existsSync(DIFF_IMAGE)) { + fs.unlinkSync(DIFF_IMAGE); + } + console.log(`✓ No visual differences detected.`); + console.log(` Current screenshot: ${CURRENT_IMAGE}`); + } catch (error) { + // Test failed - find and copy the diff image + const attachments = testInfo.attachments; + const diffAttachment = attachments.find(a => a.name === 'home-page-diff.png'); + + if (diffAttachment && diffAttachment.path) { + fs.copyFileSync(diffAttachment.path, DIFF_IMAGE); + console.log(`✗ Visual differences detected!`); + console.log(` Diff image: ${DIFF_IMAGE}`); + console.log(` Current screenshot: ${CURRENT_IMAGE}`); + } else { + console.log(`✗ Visual differences detected.`); + console.log(` Current screenshot: ${CURRENT_IMAGE}`); + } + + throw error; + } +});