From ad4cdaeb0d50dc3d742398ecd6a2ec64496669ae Mon Sep 17 00:00:00 2001 From: carl2027 Date: Sat, 3 Jan 2026 19:34:38 +0800 Subject: [PATCH 1/3] wip --- frontend/app/element/search.tsx | 11 +++++++++++ frontend/app/store/keymodel.ts | 31 +++++++++++++++++++++++++++++++ frontend/types/custom.d.ts | 1 + 3 files changed, 43 insertions(+) diff --git a/frontend/app/element/search.tsx b/frontend/app/element/search.tsx index 031bcd97d8..528ed74041 100644 --- a/frontend/app/element/search.tsx +++ b/frontend/app/element/search.tsx @@ -26,6 +26,7 @@ const SearchComponent = ({ caseSensitive: caseSensitiveAtom, wholeWord: wholeWordAtom, isOpen: isOpenAtom, + focusTrigger: focusTriggerAtom, anchorRef, offsetX = 10, offsetY = 10, @@ -33,10 +34,12 @@ const SearchComponent = ({ onNext, onPrev, }: SearchProps) => { + const inputRef = useRef(null); const [isOpen, setIsOpen] = useAtom(isOpenAtom); const [search, setSearch] = useAtom(searchAtom); const [index, setIndex] = useAtom(indexAtom); const [numResults, setNumResults] = useAtom(numResultsAtom); + const [focusTrigger] = useAtom(focusTriggerAtom); const handleOpenChange = useCallback((open: boolean) => { setIsOpen(open); @@ -56,6 +59,12 @@ const SearchComponent = ({ onSearch?.(search); }, [search]); + useEffect(() => { + if (focusTrigger > 0 && isOpen) { + inputRef.current?.focus(); + } + }, [focusTrigger, isOpen]); + const middleware: Middleware[] = []; const offsetCallback = useCallback( ({ rects }) => { @@ -146,6 +155,7 @@ const SearchComponent = ({
boolean; @@ -626,6 +627,30 @@ function registerGlobalKeys() { return true; }); } + function getSelectedText(): string { + // Check for terminal selection first + const bcm = getBlockComponentModel(getFocusedBlockInStaticTab()); + if (bcm?.viewModel?.viewType === "term") { + const termViewModel = bcm.viewModel as TermViewModel; + if (termViewModel.termRef?.current?.terminal) { + const terminalSelection = termViewModel.termRef.current.terminal.getSelection(); + if (terminalSelection && terminalSelection.length > 0) { + return terminalSelection.trim(); + } + } + } + + // Check for regular text selection + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0 && !selection.isCollapsed) { + const selectedText = selection.toString().trim(); + if (selectedText.length > 0) { + return selectedText; + } + } + return ""; + } + function activateSearch(event: WaveKeyboardEvent): boolean { const bcm = getBlockComponentModel(getFocusedBlockInStaticTab()); // Ctrl+f is reserved in most shells @@ -633,7 +658,13 @@ function registerGlobalKeys() { return false; } if (bcm.viewModel.searchAtoms) { + let selectedText = getSelectedText(); + globalStore.set(bcm.viewModel.searchAtoms.isOpen, true); + globalStore.set(bcm.viewModel.searchAtoms.searchValue, selectedText); + + const currentValue = globalStore.get(bcm.viewModel.searchAtoms.focusTrigger) as number; + globalStore.set(bcm.viewModel.searchAtoms.focusTrigger, currentValue + 1); return true; } return false; diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 3476fe5392..adc6524e71 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -273,6 +273,7 @@ declare global { resultsIndex: PrimitiveAtom; resultsCount: PrimitiveAtom; isOpen: PrimitiveAtom; + focusTrigger: PrimitiveAtom; regex?: PrimitiveAtom; caseSensitive?: PrimitiveAtom; wholeWord?: PrimitiveAtom; From fca41cb7b6aaa1cf392d8a42c140de42598eb158 Mon Sep 17 00:00:00 2001 From: carl2027 Date: Sat, 3 Jan 2026 22:36:59 +0800 Subject: [PATCH 2/3] add search input ref --- frontend/app/element/search.tsx | 21 ++++++++++----------- frontend/app/store/keymodel.ts | 10 ++++++++-- frontend/app/view/term/term.tsx | 2 ++ frontend/types/custom.d.ts | 1 - 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/frontend/app/element/search.tsx b/frontend/app/element/search.tsx index 528ed74041..3eb20a25c3 100644 --- a/frontend/app/element/search.tsx +++ b/frontend/app/element/search.tsx @@ -11,6 +11,7 @@ import "./search.scss"; type SearchProps = SearchAtoms & { anchorRef?: React.RefObject; + searchInputRef?: React.RefObject; offsetX?: number; offsetY?: number; onSearch?: (search: string) => void; @@ -26,7 +27,7 @@ const SearchComponent = ({ caseSensitive: caseSensitiveAtom, wholeWord: wholeWordAtom, isOpen: isOpenAtom, - focusTrigger: focusTriggerAtom, + searchInputRef: providedInputRef, anchorRef, offsetX = 10, offsetY = 10, @@ -34,12 +35,12 @@ const SearchComponent = ({ onNext, onPrev, }: SearchProps) => { - const inputRef = useRef(null); + const localInputRef = useRef(null); + const inputRef = providedInputRef || localInputRef; const [isOpen, setIsOpen] = useAtom(isOpenAtom); const [search, setSearch] = useAtom(searchAtom); const [index, setIndex] = useAtom(indexAtom); const [numResults, setNumResults] = useAtom(numResultsAtom); - const [focusTrigger] = useAtom(focusTriggerAtom); const handleOpenChange = useCallback((open: boolean) => { setIsOpen(open); @@ -59,12 +60,6 @@ const SearchComponent = ({ onSearch?.(search); }, [search]); - useEffect(() => { - if (focusTrigger > 0 && isOpen) { - inputRef.current?.focus(); - } - }, [focusTrigger, isOpen]); - const middleware: Middleware[] = []; const offsetCallback = useCallback( ({ rects }) => { @@ -194,6 +189,7 @@ export const Search = memo(SearchComponent) as typeof SearchComponent; type SearchOptions = { anchorRef?: React.RefObject; + searchInputRef?: React.RefObject; viewModel?: ViewModel; regex?: boolean; caseSensitive?: boolean; @@ -207,7 +203,6 @@ export function useSearch(options?: SearchOptions): SearchProps { resultsIndex: atom(0), resultsCount: atom(0), isOpen: atom(false), - focusTrigger: atom(0), regex: options?.regex !== undefined ? atom(options.regex) : undefined, caseSensitive: options?.caseSensitive !== undefined ? atom(options.caseSensitive) : undefined, wholeWord: options?.wholeWord !== undefined ? atom(options.wholeWord) : undefined, @@ -215,15 +210,19 @@ export function useSearch(options?: SearchOptions): SearchProps { [] ); const anchorRef = options?.anchorRef ?? useRef(null); + const searchInputRef = options?.searchInputRef ?? useRef(null); useEffect(() => { if (options?.viewModel) { options.viewModel.searchAtoms = searchAtoms; + // Store searchInputRef on viewModel for external access (e.g., keymodel.ts) + (options.viewModel as any).searchInputRef = searchInputRef; return () => { options.viewModel.searchAtoms = undefined; + (options.viewModel as any).searchInputRef = undefined; }; } }, [options?.viewModel]); - return { ...searchAtoms, anchorRef }; + return { ...searchAtoms, anchorRef, searchInputRef }; } const createToggleButtonDecl = ( diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index 1965fccfd0..643db6470a 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -663,8 +663,14 @@ function registerGlobalKeys() { globalStore.set(bcm.viewModel.searchAtoms.isOpen, true); globalStore.set(bcm.viewModel.searchAtoms.searchValue, selectedText); - const currentValue = globalStore.get(bcm.viewModel.searchAtoms.focusTrigger) as number; - globalStore.set(bcm.viewModel.searchAtoms.focusTrigger, currentValue + 1); + // Focus the search input using the exposed searchInputRef + const searchInputRef = bcm.viewModel.searchInputRef; + if (searchInputRef?.current) { + setTimeout(() => { + searchInputRef.current?.focus(); + }, 10); + } + return true; } return false; diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 387155752d..f61a1d2f38 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -172,8 +172,10 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => const isBasicTerm = termMode != "vdom" && blockData?.meta?.controller != "cmd"; // needs to match isBasicTerm // search + const searchInputRef = React.useRef(null); const searchProps = useSearch({ anchorRef: viewRef, + searchInputRef: searchInputRef, viewModel: model, caseSensitive: false, wholeWord: false, diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index adc6524e71..3476fe5392 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -273,7 +273,6 @@ declare global { resultsIndex: PrimitiveAtom; resultsCount: PrimitiveAtom; isOpen: PrimitiveAtom; - focusTrigger: PrimitiveAtom; regex?: PrimitiveAtom; caseSensitive?: PrimitiveAtom; wholeWord?: PrimitiveAtom; From 923c01a215206522cd9eae61a8b7fb57a152f6b7 Mon Sep 17 00:00:00 2001 From: lyx-tec Date: Sat, 30 May 2026 09:56:05 +0800 Subject: [PATCH 3/3] feat: add Playwright E2E testing (#3) --- .github/workflows/playwright.yml | 34 ++++++++++ e2e/helpers/launch.ts | 23 +++++++ e2e/search-clipboard.spec.ts | 47 ++++++++++++++ .../changes/add-playwright-e2e/.openspec.yaml | 2 + openspec/changes/add-playwright-e2e/design.md | 41 ++++++++++++ .../changes/add-playwright-e2e/proposal.md | 30 +++++++++ .../specs/e2e-search-clipboard/spec.md | 17 +++++ .../specs/playwright-electron-setup/spec.md | 23 +++++++ openspec/changes/add-playwright-e2e/tasks.md | 15 +++++ package-lock.json | 64 +++++++++++++++++++ package.json | 5 +- playwright.config.ts | 10 +++ test-results/playwright/.last-run.json | 4 ++ 13 files changed, 314 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 e2e/helpers/launch.ts create mode 100644 e2e/search-clipboard.spec.ts create mode 100644 openspec/changes/add-playwright-e2e/.openspec.yaml create mode 100644 openspec/changes/add-playwright-e2e/design.md create mode 100644 openspec/changes/add-playwright-e2e/proposal.md create mode 100644 openspec/changes/add-playwright-e2e/specs/e2e-search-clipboard/spec.md create mode 100644 openspec/changes/add-playwright-e2e/specs/playwright-electron-setup/spec.md create mode 100644 openspec/changes/add-playwright-e2e/tasks.md create mode 100644 playwright.config.ts create mode 100644 test-results/playwright/.last-run.json diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000..9f6fe1241a --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,34 @@ +name: Playwright E2E Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Install Task + uses: arduino/setup-task@v2 + + - name: Install Zig + uses: mlugg/setup-zig@v1 + + - name: Build app + run: task package -- --win --x64 --config.win.target=zip + + - name: Run E2E tests + run: npx playwright test diff --git a/e2e/helpers/launch.ts b/e2e/helpers/launch.ts new file mode 100644 index 0000000000..95d95f3383 --- /dev/null +++ b/e2e/helpers/launch.ts @@ -0,0 +1,23 @@ +import { _electron as electron, ElectronApplication, Page } from "@playwright/test"; +import * as path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +export const APP_PATH = path.resolve(__dirname, "../../make/win-unpacked/Wave.exe"); + +export async function launchApp(): Promise { + const app = await electron.launch({ + executablePath: APP_PATH, + args: [], + env: { + ...process.env, + WAVETERM_NOCONFIRMQUIT: "1", + }, + }); + return app; +} + +export async function getMainWindow(app: ElectronApplication): Promise { + return await app.firstWindow(); +} diff --git a/e2e/search-clipboard.spec.ts b/e2e/search-clipboard.spec.ts new file mode 100644 index 0000000000..b343c93e70 --- /dev/null +++ b/e2e/search-clipboard.spec.ts @@ -0,0 +1,47 @@ +import { ElectronApplication, Page } from "@playwright/test"; +import { test, expect } from "@playwright/test"; +import { launchApp } from "./helpers/launch"; + +async function getMainWindow(app: ElectronApplication): Promise { + return await app.firstWindow(); +} + +async function setClipboard(app: ElectronApplication, text: string): Promise { + await app.evaluate(({ clipboard }, txt) => clipboard.writeText(txt), text); +} + +async function readClipboard(app: ElectronApplication): Promise { + return await app.evaluate(({ clipboard }) => clipboard.readText()); +} + +test.describe("search + copy-on-select clipboard behavior", () => { + let app: ElectronApplication; + let window: Page; + + test.beforeEach(async () => { + app = await launchApp(); + window = await getMainWindow(app); + }); + + test.afterEach(async () => { + await app.close(); + }); + + test("navigating search results should not overwrite clipboard", async () => { + await window.waitForTimeout(8000); + + await setClipboard(app, "known-clipboard-content"); + expect(await readClipboard(app)).toBe("known-clipboard-content"); + + await window.keyboard.press("Control+f"); + await window.waitForTimeout(1000); + + await window.keyboard.type("test"); + await window.waitForTimeout(1000); + + await window.keyboard.press("Enter"); + await window.waitForTimeout(1000); + + expect(await readClipboard(app)).toBe("known-clipboard-content"); + }); +}); diff --git a/openspec/changes/add-playwright-e2e/.openspec.yaml b/openspec/changes/add-playwright-e2e/.openspec.yaml new file mode 100644 index 0000000000..b44fb0ec50 --- /dev/null +++ b/openspec/changes/add-playwright-e2e/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-30 diff --git a/openspec/changes/add-playwright-e2e/design.md b/openspec/changes/add-playwright-e2e/design.md new file mode 100644 index 0000000000..103378983f --- /dev/null +++ b/openspec/changes/add-playwright-e2e/design.md @@ -0,0 +1,41 @@ +## Context + +WaveTerm's UI testing is limited to a single TestDriver.ai onboarding scenario that runs only in CI. For the copy-on-select fix and future UI changes, we need fast, local, reliable E2E tests. Playwright with Electron support is the standard choice. + +## Goals / Non-Goals + +**Goals:** +- Add Playwright with Electron support as dev dependency +- Create a config that works with both dev mode (`electron-vite dev`) and packaged app +- Write a test verifying the search + copy-on-select clipboard fix +- Ensure tests can run locally without CI dependencies + +**Non-Goals:** +- Full UI coverage of all features (future work) +- Replacing existing TestDriver.ai tests +- Testing backend/Go code + +## Decisions + +**Decision: Launch Electron app via direct binary for testing** + +Use Playwright's `_electron.launch()` with the packaged app executable. This avoids the complexity of hooking into `electron-vite dev` hot-reload. + +For local development, users can run `task package -- --win --x64 --config.win.target=zip` to produce a portable build, then run tests against it. + +- Alternative considered: Launching via `electron-vite dev` — harder to detect ready state, slower +- Alternative considered: Using `electron-vite preview` — still requires a build step + +**Decision: No global test fixtures for now** + +Keep tests self-contained with a shared launch helper. Avoid complex fixture setup until more tests exist. + +**Decision: Tests in `e2e/` at project root** + +Follow Playwright conventions. Place in `e2e/` not `frontend/e2e/` since tests cover the full Electron app. + +## Risks / Trade-offs + +- **Build needed**: Tests require a packaged build first (can be automated in CI) +- **Windows focus**: Initial setup targets Windows (current platform); other platforms can be added later +- **No hot reload**: Unlike TestDriver.ai, Playwright tests launch the full app binary diff --git a/openspec/changes/add-playwright-e2e/proposal.md b/openspec/changes/add-playwright-e2e/proposal.md new file mode 100644 index 0000000000..6ceaa5df44 --- /dev/null +++ b/openspec/changes/add-playwright-e2e/proposal.md @@ -0,0 +1,30 @@ +## Why + +WaveTerm currently has no UI automated testing. The only E2E test uses TestDriver.ai (CI-only, single onboarding scenario). Manual testing is required for every UI bug fix. Introducing Playwright enables fast, reliable, locally-runnable UI tests for the terminal and core UI components. + +## What Changes + +- Add `@playwright/test` as dev dependency +- Create Playwright configuration for Electron app testing +- Write E2E test for the recently fixed search + copy-on-select clipboard bug +- Set up a test helper for launching the Electron app (both dev mode and packaged) +- Add npm scripts for running Playwright tests +- Add CI workflow for running Playwright tests on PRs + +## Capabilities + +### New Capabilities + +- `playwright-electron-setup`: Playwright config targeting WaveTerm's Electron app, with launch helpers for dev/packaged modes +- `e2e-search-clipboard`: E2E test coverage for the copy-on-select clipboard behavior when search is open + +### Modified Capabilities + +- ``: no spec-level requirement changes + +## Impact + +- `package.json` — new devDependency `@playwright/test`, new scripts +- `playwright.config.ts` — new Playwright configuration file (project root) +- `e2e/` — new directory with test specs and helpers +- `.github/workflows/playwright.yml` — new CI workflow (optional) diff --git a/openspec/changes/add-playwright-e2e/specs/e2e-search-clipboard/spec.md b/openspec/changes/add-playwright-e2e/specs/e2e-search-clipboard/spec.md new file mode 100644 index 0000000000..35e60e3d79 --- /dev/null +++ b/openspec/changes/add-playwright-e2e/specs/e2e-search-clipboard/spec.md @@ -0,0 +1,17 @@ +## ADDED Requirements + +### Requirement: Search + copy-on-select clipboard test + +The system SHALL have an E2E test that verifies the fix for copy-on-select not overwriting clipboard when search is open. + +#### Scenario: Clipboard not overwritten during search navigation +- **GIVEN** `term:copyonselect` is enabled +- **WHEN** user opens search (Ctrl+F) and types text +- **WHEN** user navigates through search results +- **THEN** the clipboard SHALL NOT contain the terminal selection text + +#### Scenario: Copy-on-select works when search is closed +- **GIVEN** `term:copyonselect` is enabled +- **WHEN** search is closed +- **WHEN** user selects text in the terminal +- **THEN** the clipboard SHALL contain the selected text diff --git a/openspec/changes/add-playwright-e2e/specs/playwright-electron-setup/spec.md b/openspec/changes/add-playwright-e2e/specs/playwright-electron-setup/spec.md new file mode 100644 index 0000000000..d2b7e9dfd1 --- /dev/null +++ b/openspec/changes/add-playwright-e2e/specs/playwright-electron-setup/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Playwright configuration for Electron + +The system SHALL provide a Playwright configuration that can launch the WaveTerm Electron app for testing. + +#### Scenario: Dev mode launch +- **WHEN** running `npx playwright test` in dev mode +- **THEN** the test framework SHALL launch WaveTerm via `electron-vite dev` +- **AND** wait for the app window to be ready before running tests + +#### Scenario: Packaged app launch +- **WHEN** running Playwright against a packaged build +- **THEN** the test framework SHALL launch the Wave.exe binary directly +- **AND** wait for the window to be ready + +### Requirement: Test organization + +E2E tests SHALL be organized in an `e2e/` directory at the project root, with shared helpers in `e2e/helpers/`. + +#### Scenario: Directory structure +- **WHEN** viewing the `e2e/` directory +- **THEN** it SHALL contain spec files and a `helpers/` subdirectory with launch utilities diff --git a/openspec/changes/add-playwright-e2e/tasks.md b/openspec/changes/add-playwright-e2e/tasks.md new file mode 100644 index 0000000000..96725937a3 --- /dev/null +++ b/openspec/changes/add-playwright-e2e/tasks.md @@ -0,0 +1,15 @@ +## 1. Setup + +- [x] 1.1 Install Playwright and Electron support: `npm install -D @playwright/test` and install Electron support +- [x] 1.2 Create `playwright.config.ts` with Electron launch configuration +- [x] 1.3 Create `e2e/helpers/launch.ts` — shared helper for launching WaveTerm app + +## 2. Test Implementation + +- [x] 2.1 Write `e2e/search-clipboard.spec.ts` — search + copy-on-select clipboard test +- [x] 2.2 Verify test passes against a packaged build + +## 3. Integration + +- [x] 3.1 Add npm scripts: `npm run test:e2e` and `npm run test:e2e:build` +- [x] 3.2 Create `.github/workflows/playwright.yml` CI workflow diff --git a/package-lock.json b/package-lock.json index fdf8623be7..05925b3641 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,6 +83,7 @@ }, "devDependencies": { "@eslint/js": "^9.39", + "@playwright/test": "1.60.0", "@rollup/plugin-node-resolve": "^16.0.3", "@tailwindcss/vite": "^4.2.1", "@types/css-tree": "^2", @@ -6691,6 +6692,22 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -23614,6 +23631,53 @@ "pathe": "^2.0.3" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/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/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 367cf0d7e7..f80607177e 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,13 @@ "build:prod": "electron-vite build --mode production", "coverage": "vitest run --coverage", "test": "vitest", - "postinstall": "node ./postinstall.cjs" + "postinstall": "node ./postinstall.cjs", + "test:e2e": "playwright test", + "test:e2e:build": "task package -- --win --x64 --config.win.target=zip && playwright test" }, "devDependencies": { "@eslint/js": "^9.39", + "@playwright/test": "1.60.0", "@rollup/plugin-node-resolve": "^16.0.3", "@tailwindcss/vite": "^4.2.1", "@types/css-tree": "^2", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000000..61ad1c5795 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "@playwright/test"; + +export default defineConfig({ + testDir: "./e2e", + timeout: 90000, + expect: { timeout: 15000 }, + retries: 0, + reporter: [["list"], ["html", { outputFolder: "playwright-report" }]], + outputDir: "test-results/playwright", +}); diff --git a/test-results/playwright/.last-run.json b/test-results/playwright/.last-run.json new file mode 100644 index 0000000000..cbcc1fbac1 --- /dev/null +++ b/test-results/playwright/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file