From b9323fff5480fb1ac0d90531b20aa4412c00476e Mon Sep 17 00:00:00 2001 From: Aaqid Masoodi Date: Wed, 17 Dec 2025 01:49:58 +0000 Subject: [PATCH 1/3] test(e2e): setup playwright suite with 3 robust specs --- .github/workflows/playwright.yml | 27 +++++++++++ .gitignore | 7 ++- package-lock.json | 64 ++++++++++++++++++++++++++ package.json | 4 +- playwright.config.ts | 79 ++++++++++++++++++++++++++++++++ tests/example.spec.ts | 18 ++++++++ tests/file-creation.spec.ts | 20 ++++++++ tests/theme.spec.ts | 14 ++++++ tests/three-preview.spec.ts | 34 ++++++++++++++ vite.config.ts | 1 + 10 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 playwright.config.ts create mode 100644 tests/example.spec.ts create mode 100644 tests/file-creation.spec.ts create mode 100644 tests/theme.spec.ts create mode 100644 tests/three-preview.spec.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..3eb13143 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index f155d61d..f44d025c 100644 --- a/.gitignore +++ b/.gitignore @@ -129,4 +129,9 @@ walkthrough.md dist-electron release - +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/package-lock.json b/package-lock.json index 52e8bb50..0610d8f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", @@ -2258,6 +2259,22 @@ "url": "https://opencollective.com/pkgr" } }, + "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/@radix-ui/primitive": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", @@ -9698,6 +9715,53 @@ "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-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/playwright/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/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 fd5e24e6..d6041574 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "lint": "eslint .", "typecheck": "tsc --noEmit", "test": "vitest", + "test:e2e": "playwright test", "preview": "vite preview", "prepare": "husky", "electron:build-ts": "tsc -p tsconfig.electron.json", @@ -82,6 +83,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.1", + "@playwright/test": "^1.57.0", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", @@ -124,4 +126,4 @@ "eslint --fix" ] } -} +} \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..e37992f8 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from "@playwright/test" + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* 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: 'http://localhost:3000', + + /* 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, + // }, +}) diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 00000000..2711200b --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test" + +test("has title", async ({ page }) => { + await page.goto("https://playwright.dev/") + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/) +}) + +test("get started link", async ({ page }) => { + await page.goto("https://playwright.dev/") + + // Click the get started link. + await page.getByRole("link", { name: "Get started" }).click() + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole("heading", { name: "Installation" })).toBeVisible() +}) diff --git a/tests/file-creation.spec.ts b/tests/file-creation.spec.ts new file mode 100644 index 00000000..370cedde --- /dev/null +++ b/tests/file-creation.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from "@playwright/test" + +test("File Creation works", async ({ page }) => { + await page.goto("http://localhost:5173/dashboard") + + // Create Scape + await page.getByRole("button", { name: "New Scape" }).first().click() + await page.getByRole("textbox", { name: "Scape Name" }).fill("file creation test") + await page.getByText("Blank ProjectA simple HTML/").click() + await page.getByRole("button", { name: "Create Scape" }).click() + + // Create File + await page.getByRole("button", { name: "New File" }).click() + await page.getByRole("textbox", { name: "filename.ext" }).fill("testfile.txt") + await page.getByRole("textbox", { name: "filename.ext" }).press("Enter") + + // VERIFY: The file exists in the file tree + // We look for the text "testfile.txt" in the sidebar + await expect(page.getByText("testfile.txt").first()).toBeVisible() +}) diff --git a/tests/theme.spec.ts b/tests/theme.spec.ts new file mode 100644 index 00000000..c02e3e3c --- /dev/null +++ b/tests/theme.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from "@playwright/test" + +test("Theme Toggle correctly sets Dark Mode", async ({ page }) => { + // 1. Your Navigation (Recorded) + await page.goto("http://localhost:5173/dashboard") + await page.getByRole("button", { name: "Toggle theme" }).click() + await page.getByRole("menuitem", { name: "Dark" }).click() + + // 2. The Verification (Upgraded) + // Instead of checking if a random div is visible, we check the SYSTEM state. + + // Method A: Check the HTML Class (The Source of Truth) + await expect(page.locator("html")).toHaveClass(/dark/) +}) diff --git a/tests/three-preview.spec.ts b/tests/three-preview.spec.ts new file mode 100644 index 00000000..c501497a --- /dev/null +++ b/tests/three-preview.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test" + +test("Three.js Preview Renders Canvas", async ({ page }) => { + // 1. Navigation & Creation (From your recording) + await page.goto("http://localhost:5173/dashboard") + + // Create New Scape + await page.getByRole("button", { name: "New Scape" }).first().click() + await page.getByRole("textbox", { name: "Scape Name" }).fill("E2E Test 3D") + await page.getByText("3D graphics with Three.js").click() + await page.getByRole("button", { name: "Create Scape" }).click() + + // 2. The Verification (The "Magic Line") + // We use .frameLocator() to go INSIDE the iframe + const previewIframe = page.frameLocator('iframe[title="preview"]') + + // We check for the element (which Three.js creates) + const threeCanvas = previewIframe.locator("canvas") + + // Assert 1: It exists and is visible + // We give it a slightly longer timeout (10s) because assets need to load + await expect(threeCanvas).toBeVisible({ timeout: 15000 }) + + // Assert 2: It has dimensions (proves it's not a 0x0 hidden element) + const box = await threeCanvas.boundingBox() + expect(box?.width).toBeGreaterThan(100) + expect(box?.height).toBeGreaterThan(100) + + // 3. Test the "Refresh Bug" (Optional but recommended) + // Click the Refresh button in the UI + // Note: We need to find the correct selector for the refresh button. + // Based on code, it's likely a button with a restart icon, typically title="Restart Preview" or similar. + // For now, we trust the canvas loads initially. +}) diff --git a/vite.config.ts b/vite.config.ts index a9c255bb..eaeaca2f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -25,6 +25,7 @@ export default defineConfig({ globals: true, environment: "jsdom", setupFiles: "./src/test/setup.ts", + exclude: ["tests/**", "node_modules/**"], }, server: { headers: { From 3f250a9ae56d6c48cdce2c9c6a65d8c5669b7622 Mon Sep 17 00:00:00 2001 From: Aaqid Masoodi Date: Wed, 17 Dec 2025 02:05:27 +0000 Subject: [PATCH 2/3] test(e2e): update config to slowMo 1000 and refine specs --- playwright.config.ts | 3 +++ tests/three-preview.spec.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/playwright.config.ts b/playwright.config.ts index e37992f8..704225d1 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -30,6 +30,9 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", + launchOptions: { + slowMo: 1000, + }, }, /* Configure projects for major browsers */ diff --git a/tests/three-preview.spec.ts b/tests/three-preview.spec.ts index c501497a..3c10af87 100644 --- a/tests/three-preview.spec.ts +++ b/tests/three-preview.spec.ts @@ -6,7 +6,7 @@ test("Three.js Preview Renders Canvas", async ({ page }) => { // Create New Scape await page.getByRole("button", { name: "New Scape" }).first().click() - await page.getByRole("textbox", { name: "Scape Name" }).fill("E2E Test 3D") + await page.getByRole("textbox", { name: "Scape Name" }).fill("Test 3D") await page.getByText("3D graphics with Three.js").click() await page.getByRole("button", { name: "Create Scape" }).click() From d7ba86c7d308e311a0bd742eb00896db2200542c Mon Sep 17 00:00:00 2001 From: Aaqid Masoodi Date: Wed, 17 Dec 2025 02:15:25 +0000 Subject: [PATCH 3/3] ci: configure playwright webserver and baseurl --- playwright.config.ts | 33 +++++++-------------------------- tests/file-creation.spec.ts | 2 +- tests/theme.spec.ts | 2 +- tests/three-preview.spec.ts | 2 +- 4 files changed, 10 insertions(+), 29 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index 704225d1..5268bd12 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ /* 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: 'http://localhost:3000', + baseURL: process.env.BASE_URL || "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -51,32 +51,13 @@ export default defineConfig({ 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, - // }, + webServer: { + command: "npm run dev", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, }) diff --git a/tests/file-creation.spec.ts b/tests/file-creation.spec.ts index 370cedde..6581ffe4 100644 --- a/tests/file-creation.spec.ts +++ b/tests/file-creation.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "@playwright/test" test("File Creation works", async ({ page }) => { - await page.goto("http://localhost:5173/dashboard") + await page.goto("/dashboard") // Create Scape await page.getByRole("button", { name: "New Scape" }).first().click() diff --git a/tests/theme.spec.ts b/tests/theme.spec.ts index c02e3e3c..cb6395e6 100644 --- a/tests/theme.spec.ts +++ b/tests/theme.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from "@playwright/test" test("Theme Toggle correctly sets Dark Mode", async ({ page }) => { // 1. Your Navigation (Recorded) - await page.goto("http://localhost:5173/dashboard") + await page.goto("/dashboard") await page.getByRole("button", { name: "Toggle theme" }).click() await page.getByRole("menuitem", { name: "Dark" }).click() diff --git a/tests/three-preview.spec.ts b/tests/three-preview.spec.ts index 3c10af87..ac665080 100644 --- a/tests/three-preview.spec.ts +++ b/tests/three-preview.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from "@playwright/test" test("Three.js Preview Renders Canvas", async ({ page }) => { // 1. Navigation & Creation (From your recording) - await page.goto("http://localhost:5173/dashboard") + await page.goto("/dashboard") // Create New Scape await page.getByRole("button", { name: "New Scape" }).first().click()