diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..3173c85f --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-18-bookworm", + "features": { + "ghcr.io/devcontainers-extra/features/apt-packages:1": { + "clean_ppas": true, + "preserve_apt_list": true, + "packages": "xvfb,xauth", + "ppas": "ppa:deadsnakes/ppa" + }, + "ghcr.io/devcontainers/features/git-lfs:1": { + "autoPull": true, + "version": "latest" + } + }, + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install && sudo npx playwright install-deps && npx playwright install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..fe152bf8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,45 @@ +# Copilot instructions (LinkHints) + +## Big picture +- This is a WebExtension built from TypeScript (+ some Preact UIs). Source lives in `src/`. +- Rollup builds everything into `compiled/` (Sucrase strips TS/JSX). `tsc` is used for type-checking only. +- Build/entrypoint mapping is centralized in `project.config.ts` and consumed by `rollup.config.js`. + +## Key modules +- `src/background/`: the “hub” (state machine + orchestration). See `src/background/Program.ts`. +- `src/worker/`: content script loaded in every frame; captures keypresses and finds/report elements. +- `src/renderer/`: top-frame content script; renders hints/underlines (shadow DOM container). +- `src/options/` and `src/popup/`: Preact UIs. +- `src/shared/`: shared types/helpers; message schema in `src/shared/messages.ts`. +- `docs/` → `compiled-docs/`: website sources. + +## Program + messaging model (core convention) +- Each subsystem has a `Program` class in `src/*/Program.ts(x)` and a tiny bootstrap in `src/*/main.ts(x)`. +- Subsystems communicate via `browser.runtime` messages; **background is the router**. +- Message types are discriminated unions in `src/shared/messages.ts` (`ToBackground`, `FromBackground`, plus nested `ToWorker`, `FromWorker`, etc.). +- Each `Program` typically defines a local `wrapMessage(...)` helper to wrap its inner message into `ToBackground`. + +## Shared utilities you should use +- Listener lifecycle + error logging: `addListener`, `addEventListener`, and `Resets` in `src/shared/main.ts`. +- Logging: `log(...)` in `src/shared/main.ts` (programs update `log.level` via StateSync). +- Build-time globals are injected by Rollup and typed in `@types/globals.d.ts` (`BROWSER`, `PROD`, `META_*`, `COLOR_*`, `DEFAULT_*`). Don’t try to “import config” for these. + +## Generated outputs (don’t edit) +- Do not edit `compiled/`, `compiled-docs/`, or `dist-*`. +- Template generators called by Rollup: + - `src/manifest.ts` → `compiled/manifest.json` + - `src/html.tsx` → minimal HTML shells in `compiled/` + - `src/icons.tsx` (+ `src/icons/`) → icons; update PNGs via `npm run png-icons` + - `src/css.ts` → injects colors from `project.config.ts` into CSS + +## Developer workflows (exact commands) +- Install: `npm ci` +- Type-check/lint/format check/build: `npm test` +- Build once (writes `compiled/`): `npm run compile` +- Watch build: `npm run watch` +- Run extension (auto-reloads on `compiled/` changes): `npm run firefox` / `npm run chrome` +- Shortcut to run watch + both browsers: `npm start` + +## Change guidance (repo-specific) +- If you add/change a cross-component action, update `src/shared/messages.ts` and both sender/receiver `Program.ts` switch handling. +- Keep message payloads JSON-serializable; prefer discriminated unions over ad-hoc objects. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f62b78e5..7d1bd1d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,6 @@ name: CI on: push: - branches: - - "main" pull_request: permissions: @@ -12,7 +10,8 @@ permissions: id-token: write jobs: - build: + deploy: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -42,16 +41,85 @@ jobs: run: npm test - name: Setup Pages - if: github.ref == 'refs/heads/main' uses: actions/configure-pages@v2 - name: Upload artifact - if: github.ref == 'refs/heads/main' - uses: actions/upload-pages-artifact@v1 + uses: actions/upload-pages-artifact@v3 with: path: compiled-docs - name: Deploy to GitHub Pages - if: github.ref == 'refs/heads/main' id: deployment uses: actions/deploy-pages@v1 + + check: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Cache node_modules + id: cache-node_modules + uses: actions/cache@v3 + with: + path: node_modules + key: node_modules-${{ hashFiles('package.json', 'package-lock.json') }} + + - name: npm ci + if: steps.cache-node_modules.outputs.cache-hit != 'true' + run: npm ci + + - name: npm test + run: npm test + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Use Node.js + uses: actions/setup-node@v3 + with: + node-version: 18.x + + - name: Cache node_modules + id: cache-node_modules + uses: actions/cache@v3 + with: + path: node_modules + key: node_modules-${{ hashFiles('package.json', 'package-lock.json') }} + + - name: Cache Playwright browsers + uses: actions/cache@v3 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + - name: npm ci + if: steps.cache-node_modules.outputs.cache-hit != 'true' + run: npm ci + + - name: Install xvfb + run: sudo apt-get update && sudo apt-get install -y xvfb + + - name: Install playwright dependencies + run: sudo npx playwright install-deps + + - name: Install Playwright browsers + run: npx playwright install + + - name: Run Playwright tests + run: xvfb-run -a npm run test:playwright + + - name: Upload test results + uses: actions/upload-artifact@v6 + with: + name: test-results + path: test-results/ diff --git a/.gitignore b/.gitignore index 3aa59c3b..1bad8320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .eslintcache -custom.config.cjs +/custom.config.cjs /node_modules/ /compiled/ +/compiled*/ /compiled-docs/ /dist*/ +/test-results +/.tool-versions diff --git a/.prettierignore b/.prettierignore index 7950fffb..4fecfdab 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,5 +3,7 @@ *.custom.js /compiled/ /compiled-docs/ +/test-results/ +/tests/*-snapshots/ *.json /rollup.config-*.js diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ef65a98..6cc82604 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -94,6 +94,16 @@ It is recommended to set up [TypeScript], [ESLint] and [Prettier] integrations i See `package.json` for details and additional scripts. +### Automated Testing + +To run the automated end-to-end tests using Playwright: + +``` +npm run test:playwright +``` + +This will compile the extension and run tests in Firefox, verifying functionality like the tutorial workflow. + ### Chrome and Firefox Open Chrome/Firefox, with a new profile where Link Hints is pre-installed: diff --git a/package-lock.json b/package-lock.json index ae0a8581..31673b98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "webextension-polyfill": "0.10.0" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@rollup/plugin-commonjs": "24.0.0", "@rollup/plugin-node-resolve": "15.0.1", "@rollup/plugin-replace": "5.0.2", @@ -31,6 +32,8 @@ "js-tokens": "8.0.0", "minifycss": "1.0.1", "optional-require": "1.1.8", + "playwright": "^1.57.0", + "playwright-webextext": "^0.0.4", "preact-render-to-string": "5.2.6", "prettier": "2.8.3", "readdirp": "3.6.0", @@ -496,6 +499,22 @@ "node": ">=14" } }, + "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/@pnpm/network.ca-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", @@ -5091,6 +5110,60 @@ "node": ">= 6" } }, + "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-webextext": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/playwright-webextext/-/playwright-webextext-0.0.4.tgz", + "integrity": "sha512-B5uIZSRtH6wi5HPhEwbEkZxEoAY5bUKCjqVW2qCsAYWQTQZ4ULOfsyglI+gfiohxrAzHT+mtkZMXZvHTtunsDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20" + }, + "peerDependencies": { + "@playwright/test": ">=1.0.0", + "playwright": ">=1.0.0" + }, + "peerDependenciesMeta": { + "@playwright/test": { + "optional": true + }, + "playwright": { + "optional": true + } + } + }, + "node_modules/playwright/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/preact": { "version": "10.11.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", @@ -7302,6 +7375,15 @@ "dev": true, "optional": true }, + "@playwright/test": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz", + "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==", + "dev": true, + "requires": { + "playwright": "1.57.0" + } + }, "@pnpm/network.ca-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", @@ -10609,6 +10691,31 @@ "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", "dev": true }, + "playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "dev": true, + "requires": { + "fsevents": "2.3.2", + "playwright-core": "1.57.0" + }, + "dependencies": { + "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 + } + } + }, + "playwright-webextext": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/playwright-webextext/-/playwright-webextext-0.0.4.tgz", + "integrity": "sha512-B5uIZSRtH6wi5HPhEwbEkZxEoAY5bUKCjqVW2qCsAYWQTQZ4ULOfsyglI+gfiohxrAzHT+mtkZMXZvHTtunsDg==", + "dev": true, + "requires": {} + }, "preact": { "version": "10.11.3", "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", diff --git a/package.json b/package.json index d12b42e9..3e070c0d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build:all": "npm run build:chrome && npm run build:firefox", "png-icons": "sucrase-node scripts/png-icons.ts", "web-ext": "sucrase-node node_modules/.bin/web-ext", + "test:playwright": "npm run compile && playwright test --project=firefox", "test": "run-pty --auto-exit % npm run build:all % eslint . --report-unused-disable-directives % prettier --check . % tsc" }, "dependencies": { @@ -23,6 +24,7 @@ "webextension-polyfill": "0.10.0" }, "devDependencies": { + "@playwright/test": "^1.57.0", "@rollup/plugin-commonjs": "24.0.0", "@rollup/plugin-node-resolve": "15.0.1", "@rollup/plugin-replace": "5.0.2", @@ -41,6 +43,8 @@ "js-tokens": "8.0.0", "minifycss": "1.0.1", "optional-require": "1.1.8", + "playwright": "^1.57.0", + "playwright-webextext": "^0.0.4", "preact-render-to-string": "5.2.6", "prettier": "2.8.3", "readdirp": "3.6.0", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..3afcae4e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,26 @@ +import type { PlaywrightTestConfig } from "@playwright/test"; + +const config: PlaywrightTestConfig = { + testDir: "tests", + timeout: 60_000, + retries: 2, + expect: { + timeout: 10_000, + toHaveScreenshot: { + maxDiffPixelRatio: 0.02, + }, + }, + reporter: "list", + projects: [ + { + name: "firefox", + use: { + browserName: "firefox", + // extensions need to be loaded in headed mode + headless: false, + }, + }, + ], +}; + +export default config; diff --git a/src/renderer/Program.ts b/src/renderer/Program.ts index dc7da176..994794d8 100644 --- a/src/renderer/Program.ts +++ b/src/renderer/Program.ts @@ -123,7 +123,10 @@ export default class RendererProgram { // into this shadow root, which is a small optimization. (The override of // `.attachShadow` in injected.ts does not apply to code running in the // extension context, only in the page context). - const shadowRoot = container.attachShadow({ mode: "closed" }); + // In development, use "open" for testing accessibility via playwright. + const shadowRoot = container.attachShadow({ + mode: PROD ? "closed" : "open", + }); const root = document.createElement("div"); root.className = ROOT_CLASS; diff --git a/tests/tutorial.spec.ts b/tests/tutorial.spec.ts new file mode 100644 index 00000000..0adfd45f --- /dev/null +++ b/tests/tutorial.spec.ts @@ -0,0 +1,318 @@ +import { expect as playwrightExpect } from "@playwright/test"; +import path from "path"; +import type { BrowserContext, Page } from "playwright"; +import { createFixture } from "playwright-webextext"; + +const TUTORIAL_WAIT_MS = 1_000; + +const tutorialUrl = "https://lydell.github.io/LinkHints/tutorial.html"; + +const extensionPath = path.resolve(__dirname, "..", "compiled"); + +const { test, expect } = createFixture(extensionPath); + +// Helper to activate hints +async function activateHints( + page: Page, + keystroke: string = "Alt+j" +): Promise { + await page.waitForFunction( + () => + document.querySelector("#__LinkHintsWebExt")?.shadowRoot?.innerHTML === + undefined + ); + // UGH I want to get rid of this so bad. + await page.waitForTimeout(200); + await page.keyboard.press(keystroke); + await page.waitForFunction( + () => + document.querySelector("#__LinkHintsWebExt")?.shadowRoot?.innerHTML !== + undefined + ); + // UGH I want to get rid of this so bad. + await page.waitForTimeout(200); +} + +// Helper to perform step 3 actions +async function performStep3( + page: Page, + context: BrowserContext, + keystroke: string +): Promise { + // Wait for #step-3 + await page.waitForURL(/#step-3/); + + // Activate hints + await activateHints(page, keystroke); + + // Snapshot + await snapshotHints( + page, + `shadow-step3-${keystroke.replace(/[^a-zA-Z0-9]/g, "")}.html` + ); + + // Press 'o' + await page.keyboard.press("e"); + + // Check new tab + let newPage: Page | undefined; + await expect + .poll(() => { + const pages = context.pages(); + newPage = pages.find((p) => p.url().includes("example.com")); + return newPage; + }) + .toBeTruthy(); + await newPage?.close(); +} + +// Helper to snapshot hints +async function snapshotHints(page: Page, snapshotName: string): Promise { + const shadowHTML = await page.locator("#__LinkHintsWebExt").evaluate((el) => { + if (el.shadowRoot === null) { + throw new Error("Missing shadow DOM"); + } + return el.shadowRoot.innerHTML; + }); + playwrightExpect(shadowHTML).toMatchSnapshot(snapshotName); + await playwrightExpect(page).toHaveScreenshot(); +} + +test("Run through tutorial", async ({ + context, +}: { + context: BrowserContext; +}) => { + console.log("Starting tutorial test"); + + // Wait for the tutorial page to load. + await new Promise((r) => { + setTimeout(r, TUTORIAL_WAIT_MS); + }); + + // Now manually open the tutorial page + const page = await context.newPage(); + await page.goto(tutorialUrl); + await page.waitForLoadState("load"); + + expect(page.url()).toBe(tutorialUrl); + console.log("Tutorial page loaded"); + + // Activate hints + console.log("Activating hints for initial step"); + await activateHints(page); + + // Snapshot + await snapshotHints(page, "shadow.html"); + console.log("Initial snapshot taken"); + + // Simulate user selecting the first hint by pressing 'j' + console.log("Pressing 'j' to go to step-1"); + await page.keyboard.press("j"); + + // Check that the URL now includes #step-1 + await page.waitForURL(/#step-1/); + console.log("Reached step-1"); + + // Activate hints again on step-1 + console.log("Activating hints on step-1"); + await activateHints(page); + + // Snapshot hints on step-1 + await snapshotHints(page, "shadow-step1.html"); + console.log("Step-1 snapshot taken"); + + // Press 'f' again to go to step-2 + console.log("Pressing 'f' to go to step-2"); + await page.keyboard.press("f"); + + await page.waitForURL(/#step-2/); + console.log("Reached step-2"); + + // Activate hints on step-2 + console.log("Activating hints on step-2"); + await activateHints(page); + + // Snapshot hints on step-2 + await snapshotHints(page, "shadow-step2.html"); + console.log("Step-2 snapshot taken"); + + // Press 'f' to go to step-3 + console.log("Pressing 'f' to go to step-3"); + await page.keyboard.press("f"); + + // Perform step 3 with Alt+k + console.log("Performing step-3 with Alt+k"); + await performStep3(page, context, "Alt+k"); + + // Perform step 3 with Alt+l + console.log("Performing step-3 with Alt+l"); + // XXX: I don't think we can tell if a tab has focus in playwright. Maybe by taking a screenshot of the browser? + await performStep3(page, context, "Alt+l"); + + // Press escape to exit hints mode + console.log("Pressing Escape to exit hints mode"); + await page.keyboard.press("Escape"); + + // Press 'f' to go to step-4 + console.log("Activating hints and pressing 'f' to go to step-4"); + await activateHints(page); + await page.keyboard.press("f"); + + await page.waitForURL(/#step-4/); + console.log("Reached step-4"); + + // Activate hints on step-4 + console.log("Activating hints on step-4"); + await activateHints(page); + + // Snapshot hints on step-4 + await snapshotHints(page, "shadow-step4.html"); + console.log("Step-4 snapshot taken"); + + // Type "1984" + console.log("Typing '1984' on step-4"); + await page.keyboard.type("1984"); + + // Look for the visible string "1984 is a novel by George Orwell." + await expect(page.locator("html")).toContainText( + "1984 is a novel by George Orwell." + ); + console.log("Verified text '1984 is a novel by George Orwell.' is visible"); + + // Then click one of the tiny pagination links + console.log("Activating hints and typing '11' for pagination"); + await activateHints(page); + await page.keyboard.type("11"); + + // Then look for an a tag with text 11 in step-4 and verify it is focused + await expect( + page.locator("#step-4 a").filter({ hasText: "11" }) + ).toBeFocused(); + console.log("Verified link '11' is focused"); + + await activateHints(page); + console.log("Activating hints and pressing 'j' to go to step-5"); + await page.keyboard.press("j"); + + await page.waitForURL(/#step-5/); + console.log("Reached step-5"); + + await activateHints(page); + console.log("Activating hints on step-5"); + + // Snapshot hints on step-5 + await snapshotHints(page, "shadow-step5.html"); + console.log("Step-5 snapshot taken"); + + // Type "IM" and ensure that iMac is selected + console.log("Typing 'IM' to select iMac"); + await page.keyboard.type("IM"); + await expect( + page.locator("#step-5 a").filter({ hasText: "iMac" }) + ).toBeFocused(); + console.log("Verified iMac is focused"); + + // Then try "IPHONE" + console.log("Activating hints and typing 'IPHONE' to select iPhone"); + await activateHints(page); + await page.keyboard.type("IPHONE"); + await expect( + page.locator("#step-5 a").filter({ hasText: "iPhone" }) + ).toBeFocused(); + console.log("Verified iPhone is focused"); + + await activateHints(page); + console.log("Activating hints and pressing 'f' to go to step-6"); + await page.keyboard.press("f"); + + await page.waitForURL(/#step-6/); + console.log("Reached step-6"); + + // Activate hints on step-6 + // This must be J, not j. + console.log("Activating hints with Alt+Shift+J on step-6"); + await activateHints(page, "Alt+Shift+J"); + // Check boxes + console.log("Typing 'gmv' to check checkboxes"); + await page.keyboard.type("gmv", { delay: 1000 }); + + // Snapshot hints on step-6 + await snapshotHints(page, "shadow-step6.html"); + console.log("Step-6 snapshot taken"); + + await page.keyboard.press("Escape"); + console.log("Pressed Escape to exit hints"); + + // Verify that the checkboxes are checked + await expect(page.locator('#step-6 input[id="lettuce"]')).toBeChecked(); + await expect(page.locator('#step-6 input[id="cucumber"]')).toBeChecked(); + await expect(page.locator('#step-6 input[id="tomato"]')).toBeChecked(); + console.log("Verified checkboxes are checked"); + + // Then open example.com, mozilla.org, and wikipedia.org + console.log("Activating hints with Alt+Shift+K to open links"); + await activateHints(page, "Alt+Shift+K"); + + // Open links + console.log("Typing 'eow' to open example.com, mozilla.org, wikipedia.org"); + await page.keyboard.type("eow", { delay: 1000 }); + await page.keyboard.press("Escape"); + console.log("Pressed Escape after opening links"); + + const urls = ["example.com", "mozilla.org", "wikipedia.org"]; + for (const urlPart of urls) { + console.log("Checking for new page with URL part:", urlPart); + const pages = context.pages(); + const newPage = pages.find((p) => p.url().includes(urlPart)); + expect(newPage).toBeTruthy(); + await newPage?.close(); + } + console.log("Verified new pages opened and closed"); + + // Go to step-7 + console.log("Activating hints and pressing 'f' to go to step-7"); + await activateHints(page); + await page.keyboard.press("f"); + + await page.waitForURL(/#step-7/); + console.log("Reached step-7"); + + await activateHints(page, "Alt+Shift+L"); + console.log("Activating hints with Alt+Shift+L on step-7"); + + // Snapshot hints on step-7 + await snapshotHints(page, "shadow-step7.html"); + console.log("Step-7 snapshot taken"); + + // Select "Link Hints adds two extra shortcuts:" + console.log("Pressing 'n' to select text"); + await page.keyboard.press("n"); + await page.waitForTimeout(1000); + + // Get the selected text and verify it is correct + await expect + .poll(async () => + page.evaluate(() => { + const selection = window.getSelection(); + return selection !== null ? selection.toString() : ""; + }) + ) + .toBe("Link Hints adds two extra shortcuts:"); + console.log("Verified selected text is correct"); + + await activateHints(page, "Alt+Shift+L"); + console.log("Activating hints again with Alt+Shift+L"); + + // Copy "Link Hints adds two extra shortcuts:" to clipboard + console.log("Pressing Alt+n to copy selected text"); + await page.keyboard.press("Alt+n"); + + // Verify clipboard contents + await expect + .poll(async () => page.evaluate(() => navigator.clipboard.readText())) + .toBe("Link Hints adds two extra shortcuts:"); + console.log("Verified clipboard contents"); + + console.log("Tutorial test completed"); +}); diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-1-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-1-firefox-linux.png new file mode 100644 index 00000000..e151b098 Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-1-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-2-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-2-firefox-linux.png new file mode 100644 index 00000000..f8085f12 Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-2-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-3-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-3-firefox-linux.png new file mode 100644 index 00000000..6bf94c46 Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-3-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-4-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-4-firefox-linux.png new file mode 100644 index 00000000..0d77c15f Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-4-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-5-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-5-firefox-linux.png new file mode 100644 index 00000000..693e6033 Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-5-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-6-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-6-firefox-linux.png new file mode 100644 index 00000000..35aa458f Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-6-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-7-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-7-firefox-linux.png new file mode 100644 index 00000000..3ad12f8b Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-7-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-8-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-8-firefox-linux.png new file mode 100644 index 00000000..b5a6080b Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-8-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-9-firefox-linux.png b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-9-firefox-linux.png new file mode 100644 index 00000000..0e025fc1 Binary files /dev/null and b/tests/tutorial.spec.ts-snapshots/Run-through-tutorial-9-firefox-linux.png differ diff --git a/tests/tutorial.spec.ts-snapshots/shadow-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-firefox-linux.html new file mode 100644 index 00000000..86275d4b --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-firefox-linux.html @@ -0,0 +1,55 @@ +
f
j
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step1-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step1-firefox-linux.html new file mode 100644 index 00000000..049dd5fa --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step1-firefox-linux.html @@ -0,0 +1,55 @@ +
f
j
d
k
s
l
a
u
r
i
e
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step2-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step2-firefox-linux.html new file mode 100644 index 00000000..f041813a --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step2-firefox-linux.html @@ -0,0 +1,55 @@ +
f
j
d
k
s
l
a
u
r
i
e
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step3-Altk-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step3-Altk-firefox-linux.html new file mode 100644 index 00000000..13ffba80 --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step3-Altk-firefox-linux.html @@ -0,0 +1,55 @@ +
f
j
d
k
s
f
l
a
u
r
i
e
o
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step3-Altl-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step3-Altl-firefox-linux.html new file mode 100644 index 00000000..13ffba80 --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step3-Altl-firefox-linux.html @@ -0,0 +1,55 @@ +
f
j
d
k
s
f
l
a
u
r
i
e
o
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step4-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step4-firefox-linux.html new file mode 100644 index 00000000..63d55efe --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step4-firefox-linux.html @@ -0,0 +1,55 @@ +
j
d
k
s
l
a
u
r
i
e
o
w
h
g
m
v
c
n
ff
fj
fd
fk
fs
fl
fa
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step5-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step5-firefox-linux.html new file mode 100644 index 00000000..7850d844 --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step5-firefox-linux.html @@ -0,0 +1,55 @@ +
f
j
d
k
s
l
a
u
r
i
e
o
w
h
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step6-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step6-firefox-linux.html new file mode 100644 index 00000000..6d6b3754 --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step6-firefox-linux.html @@ -0,0 +1,55 @@ +
​f
​j
​d
​k
​s
​l
​a
​u
​r
​i
​e
​o
​w
​h
​g
​m
​v
¯\_(ツ)_/¯
\ No newline at end of file diff --git a/tests/tutorial.spec.ts-snapshots/shadow-step7-firefox-linux.html b/tests/tutorial.spec.ts-snapshots/shadow-step7-firefox-linux.html new file mode 100644 index 00000000..d70e42e0 --- /dev/null +++ b/tests/tutorial.spec.ts-snapshots/shadow-step7-firefox-linux.html @@ -0,0 +1,55 @@ +
j
d
k
s
l
a
u
r
i
e
o
w
h
g
m
v
c
n
ff
fj
fd
fk
fs
fl
fa
fu
fr
fi
fe
fo
fw
fh
¯\_(ツ)_/¯
\ No newline at end of file