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..5268bd12 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,63 @@ +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: 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", + launchOptions: { + slowMo: 1000, + }, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run dev", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}) 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..6581ffe4 --- /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("/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..cb6395e6 --- /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("/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..ac665080 --- /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("/dashboard") + + // Create New Scape + await page.getByRole("button", { name: "New Scape" }).first().click() + 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() + + // 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: {