diff --git a/package.json b/package.json index 65bed96..06cc94a 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "lint:fix": "oxlint --fix", "test:ui": "vitest --ui", "test:run": "vitest run", - "wdio": "wdio run ./wdio.conf.ts", - "test:e2e": "wdio run ./wdio.conf.ts" + "wdio": "wdio run wdio.conf.ts", + "test:e2e": "wdio run wdio.conf.ts" }, "dependencies": { "@base-ui/react": "^1.0.0", @@ -36,6 +36,7 @@ "@tauri-apps/plugin-opener": "^2.5.3", "@tauri-apps/plugin-store": "^2.4.2", "@tauri-apps/plugin-stronghold": "^2.3.1", + "@wdio/cli": "^9.23.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.562.0", @@ -60,12 +61,12 @@ "@testing-library/react": "^16.3.1", "@testing-library/user-event": "^14.6.1", "@types/jsdom": "^27.0.0", + "@types/mocha": "^10.0.10", "@types/node": "^25.0.6", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.7.0", "@vitest/ui": "^4.0.16", - "@wdio/cli": "^9.23.0", "@wdio/local-runner": "^9.23.0", "@wdio/mocha-framework": "^9.23.0", "@wdio/spec-reporter": "^9.20.0", @@ -78,10 +79,11 @@ "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0", + "types": "link:@wdio/globals/types", "typescript": "~5.8.3", "typescript-eslint": "^8.52.0", "vite": "^7.3.1", "vitest": "^4.0.16", "webdriverio": "^9.23.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7820569..2145b60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@tauri-apps/plugin-stronghold': specifier: ^2.3.1 version: 2.3.1 + '@wdio/cli': + specifier: ^9.23.0 + version: 9.23.0(@types/node@25.0.6)(expect-webdriverio@5.6.1) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -126,6 +129,9 @@ importers: '@types/jsdom': specifier: ^27.0.0 version: 27.0.0 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 '@types/node': specifier: ^25.0.6 version: 25.0.6 @@ -141,9 +147,6 @@ importers: '@vitest/ui': specifier: ^4.0.16 version: 4.0.16(vitest@4.0.16) - '@wdio/cli': - specifier: ^9.23.0 - version: 9.23.0(@types/node@25.0.6)(expect-webdriverio@5.6.1) '@wdio/local-runner': specifier: ^9.23.0 version: 9.23.0(@wdio/globals@9.23.0)(webdriverio@9.23.0) @@ -180,6 +183,9 @@ importers: tw-animate-css: specifier: ^1.4.0 version: 1.4.0 + types: + specifier: link:@wdio/globals/types + version: link:@wdio/globals/types typescript: specifier: ~5.8.3 version: 5.8.3 diff --git a/src-tauri/src/actions/download.rs b/src-tauri/src/actions/download.rs index 76b518c..fa15649 100644 --- a/src-tauri/src/actions/download.rs +++ b/src-tauri/src/actions/download.rs @@ -18,7 +18,6 @@ use crate::s3::{ }; use crate::AppState; -/// Global state for tracking active downloads and their cancellation tokens lazy_static::lazy_static! { static ref ACTIVE_DOWNLOADS: Arc>>> = Arc::new(Mutex::new(HashMap::new())); diff --git a/src-tauri/src/actions/upload.rs b/src-tauri/src/actions/upload.rs index 9593b6d..a1543b7 100644 --- a/src-tauri/src/actions/upload.rs +++ b/src-tauri/src/actions/upload.rs @@ -16,7 +16,6 @@ use crate::s3::{ }; use crate::AppState; -/// Global state for tracking active uploads and their cancellation tokens lazy_static::lazy_static! { static ref ACTIVE_UPLOADS: Arc>>> = Arc::new(Mutex::new(HashMap::new())); diff --git a/src/routes/(credential-manager)/connections/$connectionId/edit.tsx b/src/routes/(credential-manager)/connections/$connectionId/edit.tsx index 9d33806..a170499 100644 --- a/src/routes/(credential-manager)/connections/$connectionId/edit.tsx +++ b/src/routes/(credential-manager)/connections/$connectionId/edit.tsx @@ -173,6 +173,7 @@ function EditConnectionPage() {
{ e.preventDefault(); e.stopPropagation(); diff --git a/src/routes/(credential-manager)/connections/new.tsx b/src/routes/(credential-manager)/connections/new.tsx index a6dc885..4099830 100644 --- a/src/routes/(credential-manager)/connections/new.tsx +++ b/src/routes/(credential-manager)/connections/new.tsx @@ -112,6 +112,7 @@ function NewConnectionPage() {
{ e.preventDefault(); e.stopPropagation(); diff --git a/tests/e2e/specs/example.e2e.ts b/tests/e2e/specs/example.e2e.ts new file mode 100644 index 0000000..f7bc7ce --- /dev/null +++ b/tests/e2e/specs/example.e2e.ts @@ -0,0 +1,35 @@ +/// +/// + +// calculates the luma from a hex color `#abcdef` +function luma(hex:string) { + if (hex.startsWith('#')) { + hex = hex.substring(1); + } + + const rgb = parseInt(hex, 16); + const r = (rgb >> 16) & 0xff; + const g = (rgb >> 8) & 0xff; + const b = (rgb >> 0) & 0xff; + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +describe('Hello Tauri', () => { + it('should be cordial', async () => { + const header = await $('body > h1'); + const text = await header.getText(); + expect(text).toMatch(/^[hH]ello/); + }); + + it('should be excited', async () => { + const header = await $('body > h1'); + const text = await header.getText(); + expect(text).toMatch(/!$/); + }); + + it('should be easy on the eyes', async () => { + const body = await $('body'); + const backgroundColor = await body.getCSSProperty('background-color'); + expect(luma(backgroundColor.parsed.hex)).toBeLessThan(100); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 2a86eb6..7f2bd41 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,10 +2,13 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -13,19 +16,31 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - /* Path aliases */ "baseUrl": ".", "paths": { - "@/*": ["./src/*"] - } + "@/*": [ + "./src/*" + ] + }, + "types": [ + "node", + "@wdio/globals/types", + "@types/mocha", + "@wdio/mocha-framework" + ] }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} + "include": [ + "src" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} \ No newline at end of file diff --git a/wdio.conf.ts b/wdio.conf.ts index c962354..363a5b2 100644 --- a/wdio.conf.ts +++ b/wdio.conf.ts @@ -1,309 +1,86 @@ -import { ChildProcess, spawn } from 'child_process'; -import * as path from 'path'; +import os from 'os'; +import path from 'path'; +import { ChildProcess, spawn, spawnSync } from 'child_process'; import { fileURLToPath } from 'url'; -/** - * WebdriverIO configuration for Tauri E2E testing with tauri-driver. - * - * This configuration is designed to work with tauri-driver, which acts as a - * WebDriver server for Tauri applications. It supports both: - * - Docker-based execution (Linux with WebKitWebDriver) - * - Local execution on Linux systems with tauri-driver installed - * - * @see https://v2.tauri.app/develop/tests/webdriver/example/webdriverio/ - * @see https://webdriver.io/docs/configurationfile/ - */ +const __dirname = fileURLToPath(new URL('.', import.meta.url)); -// Get __dirname equivalent in ESM -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); +// keep track of the `tauri-driver` child process +let tauriDriver: ChildProcess | undefined; +let exit = false; -// Environment configuration -const TAURI_DRIVER_HOST = process.env.TAURI_DRIVER_HOST || '127.0.0.1'; -const TAURI_DRIVER_PORT = parseInt(process.env.TAURI_DRIVER_PORT || '4444', 10); - -// Build type: 'release' for production builds, 'debug' for development -const BUILD_TYPE = process.env.TAURI_BUILD_TYPE || 'release'; - -// Path to the built Tauri application binary -// On Windows, the executable has a .exe extension -const BINARY_NAME = process.platform === 'win32' ? 'obj.exe' : 'obj'; -const TAURI_APP_PATH = process.env.TAURI_APP_PATH || path.resolve(__dirname, `src-tauri/target/${BUILD_TYPE}/${BINARY_NAME}`); - -// Whether to spawn tauri-driver automatically (set to false in Docker where it's managed separately) -const SPAWN_TAURI_DRIVER = process.env.SPAWN_TAURI_DRIVER !== 'false'; - -// CI environment detection -const IS_CI = process.env.CI === 'true'; - -// Test execution configuration -const MAX_INSTANCES = parseInt(process.env.WDIO_MAX_INSTANCES || '1', 10); -const LOG_LEVEL = (process.env.WDIO_LOG_LEVEL || 'info') as WebdriverIO.Config['logLevel']; -const BAIL = parseInt(process.env.WDIO_BAIL || '0', 10); -const BASE_URL = process.env.WDIO_BASE_URL || ''; -const WAIT_FOR_TIMEOUT = parseInt(process.env.WDIO_WAIT_TIMEOUT || '10000', 10); -const CONNECTION_RETRY_TIMEOUT = parseInt(process.env.WDIO_CONNECTION_RETRY_TIMEOUT || '120000', 10); -const CONNECTION_RETRY_COUNT = parseInt(process.env.WDIO_CONNECTION_RETRY_COUNT || '3', 10); - -// Keep track of the tauri-driver process -let tauriDriver: ChildProcess | null = null; - -export const config: WebdriverIO.Config = { - // - // ==================== - // Runner Configuration - // ==================== - // WebdriverIO supports running e2e tests with local runner - runner: 'local', - - // - // ================== - // Specify Test Files - // ================== - // Define which test specs should run +export const config = { + host: '127.0.0.1', + port: 4444, specs: ['./tests/e2e/specs/**/*.e2e.ts'], - - // Patterns to exclude - exclude: [], - - // - // ============ - // Capabilities - // ============ - // Define capabilities for tauri-driver with WebKitWebDriver - // - // tauri-driver uses WebKitWebDriver under the hood on Linux, - // which requires specific capability handling - // - maxInstances: MAX_INSTANCES, - + maxInstances: 1, capabilities: [ { - // Maximum instances for this capability maxInstances: 1, - - // Browser name for tauri-driver - browserName: 'wry', - - // Tauri-specific options 'tauri:options': { - // Path to the built Tauri application binary - application: TAURI_APP_PATH, + application: './src-tauri/target/debug/obj', }, }, ], - - // - // =================== - // Test Configurations - // =================== - // Level of logging verbosity: trace | debug | info | warn | error | silent - logLevel: LOG_LEVEL, - - // If you only want to run your tests until a specific amount of tests have failed use - // bail (default is 0 - don't bail, run all tests). - bail: BAIL, - - // Set a base URL in order to shorten url command calls - baseUrl: BASE_URL, - - // Default timeout for all waitFor* commands - waitforTimeout: WAIT_FOR_TIMEOUT, - - // Default timeout in milliseconds for request - connectionRetryTimeout: CONNECTION_RETRY_TIMEOUT, - - // Default request retries count - connectionRetryCount: CONNECTION_RETRY_COUNT, - - // - // =================== - // WebDriver Connection - // =================== - // tauri-driver acts as a WebDriver server, typically on port 4444 - hostname: TAURI_DRIVER_HOST, - port: TAURI_DRIVER_PORT, - - // - // Test runner services - // Services take over a specific job you don't want to take care of. - // For tauri-driver, we don't need additional services as tauri-driver - // is started separately (either via hooks or docker-compose) - services: [], - - // - // Framework you want to run your specs with. - // Supported: Mocha, Jasmine, and Cucumber - framework: 'mocha', - - // The number of times to retry the entire spec file when it fails as a whole - specFileRetries: 1, - - // Delay in seconds between the spec file retry attempts - specFileRetriesDelay: 0, - - // Whether or not retried spec files should be retried immediately or deferred to the end of the queue - specFileRetriesDeferred: false, - - // - // Test reporters reporters: ['spec'], - - // - // Options to be passed to Mocha. - // See the full list at http://mochajs.org/ + framework: 'mocha', mochaOpts: { ui: 'bdd', - timeout: 60000, // 60 seconds - Tauri apps may need time to start - retries: 1, - }, - - // - // ===== - // Hooks - // ===== - // WebdriverIO provides several hooks you can use to interfere with the test process - // in order to enhance it and to build services around it. - - /** - * Gets executed once before all workers get launched. - */ - onPrepare: function () { - console.log('\n๐Ÿš€ Starting Tauri E2E tests with WebdriverIO'); - console.log(` Environment: ${IS_CI ? 'CI' : 'Local'}`); - console.log(` Platform: ${process.platform}`); - console.log(` tauri-driver: ${TAURI_DRIVER_HOST}:${TAURI_DRIVER_PORT}`); - console.log(` Application: ${TAURI_APP_PATH}`); - console.log(` Auto-spawn tauri-driver: ${SPAWN_TAURI_DRIVER}\n`); + timeout: 60000, }, - /** - * Gets executed before initializing the webdriver session and test framework. - * This is where we spawn tauri-driver if configured to do so. - */ - beforeSession: function () { - if (!SPAWN_TAURI_DRIVER) { - console.log('๐Ÿ“ก tauri-driver managed externally (Docker/manual)'); - return; - } - - console.log('๐Ÿ”„ Starting tauri-driver...'); - - // Spawn tauri-driver process - // tauri-driver is installed via cargo and should be in PATH after `cargo install tauri-driver` - tauriDriver = spawn('tauri-driver', ['--port', String(TAURI_DRIVER_PORT)], { - stdio: ['ignore', 'pipe', 'pipe'], + // ensure the rust project is built since we expect this binary to exist for the webdriver sessions + onPrepare: () => { + spawnSync('pnpm', ['tauri', 'build', '--debug', '--no-bundle'], { + cwd: path.resolve(__dirname, '../..'), + stdio: 'inherit', + shell: true, }); + }, - tauriDriver.stdout?.on('data', (data) => { - console.log(`[tauri-driver] ${data.toString().trim()}`); - }); + // ensure we are running `tauri-driver` before the session starts so that we can proxy the webdriver requests + beforeSession: () => { + tauriDriver = spawn(path.resolve(os.homedir(), '.cargo', 'bin', 'tauri-driver'), [], { stdio: [null, process.stdout, process.stderr] }); - tauriDriver.stderr?.on('data', (data) => { - console.error(`[tauri-driver error] ${data.toString().trim()}`); + tauriDriver.on('error', (error: Error) => { + console.error('tauri-driver error:', error); + process.exit(1); }); - - tauriDriver.on('error', (err) => { - console.error('โŒ Failed to start tauri-driver:', err.message); - console.error(' Make sure tauri-driver is installed: cargo install tauri-driver'); - }); - - tauriDriver.on('exit', (code, signal) => { - if (code !== 0 && code !== null) { - console.error(`โŒ tauri-driver exited with code ${code}`); - } - if (signal) { - console.log(` tauri-driver terminated by signal: ${signal}`); + tauriDriver.on('exit', (code: number) => { + if (!exit) { + console.error('tauri-driver exited with code:', code); + process.exit(1); } }); - - // Give tauri-driver time to start and bind to the port - return new Promise((resolve) => { - setTimeout(() => { - console.log('โœ… tauri-driver should be ready'); - resolve(); - }, 2000); - }); - }, - - /** - * Gets executed before test execution begins. - */ - before: async function (_capabilities, _specs, browser) { - // Wait for the Tauri app to be fully loaded - console.log('โณ Waiting for Tauri application to initialize...'); - - // Give the application some time to fully start - await browser.pause(2000); - - console.log('โœ… Tauri application ready for testing'); - }, - - /** - * Function to be executed before a test (in Mocha/Jasmine). - */ - beforeTest: function (test) { - console.log(`\n๐Ÿงช Running: ${test.title}`); - }, - - /** - * Function to be executed after a test (in Mocha/Jasmine). - */ - afterTest: function (test, _context, { error, passed }) { - if (passed) { - console.log(` โœ… Passed: ${test.title}`); - } else { - console.log(` โŒ Failed: ${test.title}`); - if (error) { - console.log(` Error: ${error.message}`); - } - } }, - /** - * Gets executed after all tests are done. - */ - after: async function () { - console.log('\n๐Ÿ Test execution completed'); + // clean up the `tauri-driver` process we spawned at the start of the session + afterSession: () => { + closeTauriDriver(); }, +}; - /** - * Gets executed right after terminating the webdriver session. - * This is where we clean up tauri-driver if we spawned it. - */ - afterSession: function () { - if (tauriDriver && !tauriDriver.killed) { - console.log('๐Ÿ›‘ Stopping tauri-driver...'); - tauriDriver.kill('SIGTERM'); - tauriDriver = null; +function closeTauriDriver() { + exit = true; + tauriDriver?.kill(); +} + +function onShutdown(fn: CallableFunction) { + const cleanup = () => { + try { + fn(); + } finally { + process.exit(); } - }, + }; - /** - * Gets executed after all workers got shut down and the process is about to exit. - */ - onComplete: function () { - // Ensure tauri-driver is stopped even if afterSession wasn't called - if (tauriDriver && !tauriDriver.killed) { - console.log('๐Ÿ›‘ Cleaning up tauri-driver...'); - tauriDriver.kill('SIGTERM'); - tauriDriver = null; - } - console.log('\nโœจ All Tauri E2E tests completed\n'); - }, -}; - -// Handle process termination to ensure tauri-driver is cleaned up -process.on('SIGINT', () => { - if (tauriDriver && !tauriDriver.killed) { - tauriDriver.kill('SIGTERM'); - } - process.exit(0); -}); + process.on('exit', cleanup); + process.on('SIGINT', cleanup); + process.on('SIGTERM', cleanup); + process.on('SIGHUP', cleanup); + process.on('SIGBREAK', cleanup); +} -process.on('SIGTERM', () => { - if (tauriDriver && !tauriDriver.killed) { - tauriDriver.kill('SIGTERM'); - } - process.exit(0); +onShutdown(() => { + closeTauriDriver(); });