From 54e2904be4308ba19c3d6caf096f451a428a5a98 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Jan 2026 11:13:29 +0000 Subject: [PATCH 1/3] feat: add WebdriverIO E2E testing framework for Tauri - Add e2e/ directory with WebdriverIO + tauri-driver configuration - Create first E2E test for project creation flow - Add data-testid attributes to ProjectList component for test selectors - Add npm scripts for running E2E tests --- e2e/package.json | 19 +++++++ e2e/specs/project.e2e.ts | 82 ++++++++++++++++++++++++++ e2e/tsconfig.json | 13 +++++ e2e/wdio.conf.ts | 101 +++++++++++++++++++++++++++++++++ package.json | 2 + src/components/ProjectList.tsx | 12 +++- 6 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 e2e/package.json create mode 100644 e2e/specs/project.e2e.ts create mode 100644 e2e/tsconfig.json create mode 100644 e2e/wdio.conf.ts diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000..48b78de --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,19 @@ +{ + "name": "devora-e2e", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "test": "wdio run wdio.conf.ts", + "test:headless": "wdio run wdio.conf.ts --headless" + }, + "devDependencies": { + "@wdio/cli": "^8.40.0", + "@wdio/local-runner": "^8.40.0", + "@wdio/mocha-framework": "^8.40.0", + "@wdio/spec-reporter": "^8.40.0", + "@types/mocha": "^10.0.7", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/e2e/specs/project.e2e.ts b/e2e/specs/project.e2e.ts new file mode 100644 index 0000000..3c136c6 --- /dev/null +++ b/e2e/specs/project.e2e.ts @@ -0,0 +1,82 @@ +import { expect } from '@wdio/globals' + +describe('Project Management', () => { + describe('Create Project', () => { + it('should create a new project successfully', async () => { + // Wait for the app to load + await browser.pause(2000) + + // Click the "New Project" button + const newProjectBtn = await $('[data-testid="new-project-btn"]') + await newProjectBtn.waitForDisplayed({ timeout: 5000 }) + await newProjectBtn.click() + + // Wait for the form to appear + const form = await $('[data-testid="new-project-form"]') + await form.waitForDisplayed({ timeout: 5000 }) + + // Fill in the project name + const nameInput = await $('[data-testid="project-name-input"]') + await nameInput.setValue('E2E Test Project') + + // Fill in the description (optional) + const descInput = await $('[data-testid="project-desc-input"]') + await descInput.setValue('This project was created by E2E test') + + // Click create button + const createBtn = await $('[data-testid="create-project-btn"]') + await createBtn.click() + + // Wait for navigation to the new project page + await browser.pause(1000) + + // Verify we're on the project detail page (URL should contain /project/) + const url = await browser.getUrl() + expect(url).toContain('/project/') + }) + + it('should cancel project creation', async () => { + // Navigate back to home + await browser.url('/') + await browser.pause(1000) + + // Click the "New Project" button + const newProjectBtn = await $('[data-testid="new-project-btn"]') + await newProjectBtn.waitForDisplayed({ timeout: 5000 }) + await newProjectBtn.click() + + // Wait for the form to appear + const form = await $('[data-testid="new-project-form"]') + await form.waitForDisplayed({ timeout: 5000 }) + + // Click cancel button + const cancelBtn = await $('[data-testid="cancel-create-btn"]') + await cancelBtn.click() + + // Verify the form is hidden + await form.waitForDisplayed({ timeout: 5000, reverse: true }) + }) + + it('should show created project in the list', async () => { + // Navigate back to home + await browser.url('/') + await browser.pause(1000) + + // Look for project cards + const projectCards = await $$('[data-testid="project-card"]') + + // There should be at least one project (the one we created) + expect(projectCards.length).toBeGreaterThan(0) + + // Find the project we created + const projectNames = await Promise.all( + projectCards.map(async (card) => { + const nameElement = await card.$('h3') + return nameElement.getText() + }) + ) + + expect(projectNames).toContain('E2E Test Project') + }) + }) +}) diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 0000000..26ccdb9 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "types": ["node", "@wdio/globals/types", "@wdio/mocha-framework"] + }, + "include": ["**/*.ts"] +} diff --git a/e2e/wdio.conf.ts b/e2e/wdio.conf.ts new file mode 100644 index 0000000..04c3ca8 --- /dev/null +++ b/e2e/wdio.conf.ts @@ -0,0 +1,101 @@ +import type { Options } from '@wdio/types' +import { spawn, ChildProcess } from 'child_process' +import path from 'path' +import { fileURLToPath } from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +// Path to the Tauri application binary +const tauriAppPath = path.resolve( + __dirname, + '../src-tauri/target/release/devora' +) + +let tauriDriver: ChildProcess | null = null + +export const config: Options.Testrunner = { + runner: 'local', + autoCompileOpts: { + autoCompile: true, + tsNodeOpts: { + project: './tsconfig.json', + transpileOnly: true, + }, + }, + + specs: ['./specs/**/*.ts'], + exclude: [], + + maxInstances: 1, + + capabilities: [ + { + // Use tauri as the browserName for tauri-driver + browserName: 'wry', + 'tauri:options': { + application: tauriAppPath, + }, + } as WebdriverIO.Capabilities, + ], + + logLevel: 'info', + + bail: 0, + + waitforTimeout: 10000, + connectionRetryTimeout: 120000, + connectionRetryCount: 3, + + framework: 'mocha', + + reporters: ['spec'], + + mochaOpts: { + ui: 'bdd', + timeout: 60000, + }, + + // Start tauri-driver before tests + onPrepare: async function () { + console.log('Building Tauri application...') + const { execSync } = await import('child_process') + + // Build the Tauri app in release mode + execSync('cargo build --release', { + cwd: path.resolve(__dirname, '../src-tauri'), + stdio: 'inherit', + }) + + console.log('Tauri application built successfully') + }, + + beforeSession: async function () { + console.log('Starting tauri-driver...') + + // Spawn tauri-driver on port 4444 + tauriDriver = spawn('tauri-driver', [], { + stdio: ['ignore', 'pipe', 'pipe'], + }) + + tauriDriver.stdout?.on('data', (data: Buffer) => { + console.log(`[tauri-driver] ${data.toString()}`) + }) + + tauriDriver.stderr?.on('data', (data: Buffer) => { + console.error(`[tauri-driver] ${data.toString()}`) + }) + + // Wait for tauri-driver to start + await new Promise((resolve) => setTimeout(resolve, 2000)) + console.log('tauri-driver started') + }, + + afterSession: async function () { + console.log('Stopping tauri-driver...') + if (tauriDriver) { + tauriDriver.kill() + tauriDriver = null + } + }, +} diff --git a/package.json b/package.json index 1e233d6..fe7ac08 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "test": "bun test", "test:watch": "bun test --watch", "test:coverage": "bun test --coverage", + "test:e2e": "cd e2e && npm test", + "test:e2e:install": "cd e2e && npm install", "lint": "eslint src", "lint:fix": "eslint src --fix", "format": "prettier --write \"src/**/*.{ts,tsx}\"", diff --git a/src/components/ProjectList.tsx b/src/components/ProjectList.tsx index 7821c2d..4adbbb4 100644 --- a/src/components/ProjectList.tsx +++ b/src/components/ProjectList.tsx @@ -62,7 +62,7 @@ export default function ProjectList() { {projects.length} {projects.length === 1 ? 'project' : 'projects'} in workspace