340 lines
9.8 KiB
TypeScript
340 lines
9.8 KiB
TypeScript
/**
|
||
* WebdriverIO Configuration for Native Tauri Testing
|
||
*
|
||
* This config runs tests against the actual Tauri desktop app using tauri-driver.
|
||
* Requires: cargo install tauri-driver
|
||
*
|
||
* Usage:
|
||
* 1. Build the app: npm run tauri:build
|
||
* 2. Run tests: npm run test:native
|
||
*/
|
||
|
||
import type { Options } from '@wdio/types';
|
||
import * as path from 'node:path';
|
||
import * as fs from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
// Track tauri-driver process
|
||
let tauriDriverProcess: ChildProcess | null = null;
|
||
const tauriDriverPath = getTauriDriverPath();
|
||
const tauriDriverStatus = getTauriDriverStatus(tauriDriverPath);
|
||
const shouldRunNative = tauriDriverStatus === 'supported';
|
||
|
||
const isWslEnv = isWsl();
|
||
if (isWslEnv && process.env.NOTEFLOW_ENCRYPT_AUDIO === undefined) {
|
||
process.env.NOTEFLOW_ENCRYPT_AUDIO = 'false';
|
||
console.log('WSL detected; disabling audio encryption for native tests');
|
||
}
|
||
if (process.env.NOTEFLOW_DISABLE_AUDIO_MONITOR === undefined) {
|
||
process.env.NOTEFLOW_DISABLE_AUDIO_MONITOR = '1';
|
||
}
|
||
if (process.env.NOTEFLOW_DISABLE_AUDIO_CAPTURE === undefined) {
|
||
process.env.NOTEFLOW_DISABLE_AUDIO_CAPTURE = '1';
|
||
}
|
||
if (process.env.NOTEFLOW_DISABLE_AUDIO_DEVICES === undefined) {
|
||
process.env.NOTEFLOW_DISABLE_AUDIO_DEVICES = '1';
|
||
}
|
||
if (process.env.NOTEFLOW_DISABLE_AUDIO_TESTS === undefined) {
|
||
process.env.NOTEFLOW_DISABLE_AUDIO_TESTS = '1';
|
||
}
|
||
if (process.env.NOTEFLOW_REQUEST_TIMEOUT_SECS === undefined) {
|
||
process.env.NOTEFLOW_REQUEST_TIMEOUT_SECS = '300';
|
||
}
|
||
if (process.env.NOTEFLOW_E2E_NATIVE === undefined) {
|
||
process.env.NOTEFLOW_E2E_NATIVE = '1';
|
||
}
|
||
|
||
if (tauriDriverStatus === 'not_supported') {
|
||
console.warn('tauri-driver not supported on this platform; skipping native e2e tests.');
|
||
}
|
||
|
||
function isWsl(): boolean {
|
||
if (process.platform !== 'linux') {
|
||
return false;
|
||
}
|
||
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP || process.env.WSLENV) {
|
||
return true;
|
||
}
|
||
try {
|
||
const release = fs.readFileSync('/proc/sys/kernel/osrelease', 'utf8').toLowerCase();
|
||
return release.includes('microsoft') || release.includes('wsl');
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Detect the built Tauri binary path based on platform
|
||
function getTauriBinaryPath(): string {
|
||
const projectRoot = path.resolve(__dirname, 'src-tauri');
|
||
|
||
if (process.platform === 'win32') {
|
||
// Windows: look for .exe in release or debug
|
||
// Binary name comes from Cargo.toml package name
|
||
const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri.exe');
|
||
const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri.exe');
|
||
|
||
if (fs.existsSync(releasePath)) {
|
||
return releasePath;
|
||
}
|
||
if (fs.existsSync(debugPath)) {
|
||
return debugPath;
|
||
}
|
||
|
||
// Fallback to release path (will error if not built)
|
||
return releasePath;
|
||
} else if (process.platform === 'darwin') {
|
||
// macOS: .app bundle
|
||
const releasePath = path.join(
|
||
projectRoot,
|
||
'target',
|
||
'release',
|
||
'bundle',
|
||
'macos',
|
||
'NoteFlow.app',
|
||
'Contents',
|
||
'MacOS',
|
||
'noteflow-tauri'
|
||
);
|
||
const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');
|
||
|
||
if (fs.existsSync(releasePath)) {
|
||
return releasePath;
|
||
}
|
||
if (fs.existsSync(debugPath)) {
|
||
return debugPath;
|
||
}
|
||
return releasePath;
|
||
} else {
|
||
// Linux: AppImage or direct binary
|
||
const releasePath = path.join(projectRoot, 'target', 'release', 'noteflow-tauri');
|
||
const debugPath = path.join(projectRoot, 'target', 'debug', 'noteflow-tauri');
|
||
|
||
if (fs.existsSync(releasePath)) {
|
||
return releasePath;
|
||
}
|
||
if (fs.existsSync(debugPath)) {
|
||
return debugPath;
|
||
}
|
||
return releasePath;
|
||
}
|
||
}
|
||
|
||
// Get tauri-driver path
|
||
function getTauriDriverPath(): string {
|
||
if (process.platform === 'win32') {
|
||
// On Windows, tauri-driver is in cargo bin
|
||
const cargoHome = process.env.CARGO_HOME || path.join(process.env.USERPROFILE || '', '.cargo');
|
||
return path.join(cargoHome, 'bin', 'tauri-driver.exe');
|
||
}
|
||
return 'tauri-driver';
|
||
}
|
||
|
||
type TauriDriverStatus = 'supported' | 'not_supported' | 'missing' | 'error';
|
||
|
||
function getTauriDriverStatus(driverPath: string): TauriDriverStatus {
|
||
const result = spawnSync(driverPath, ['--help'], { encoding: 'utf8' });
|
||
const error = result.error as NodeJS.ErrnoException | undefined;
|
||
if (error?.code === 'ENOENT') {
|
||
return 'missing';
|
||
}
|
||
if (error) {
|
||
return 'error';
|
||
}
|
||
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.toLowerCase();
|
||
if (output.includes('not supported')) {
|
||
return 'not_supported';
|
||
}
|
||
return result.status === 0 ? 'supported' : 'error';
|
||
}
|
||
|
||
// Get msedgedriver path (Windows only)
|
||
async function getMsEdgeDriverPath(): Promise<string | null> {
|
||
if (process.platform !== 'win32') {
|
||
return null;
|
||
}
|
||
|
||
// Try edgedriver npm package first
|
||
try {
|
||
const edgedriver = await import('edgedriver');
|
||
const downloadedPath = await edgedriver.download();
|
||
if (fs.existsSync(downloadedPath)) {
|
||
return downloadedPath;
|
||
}
|
||
} catch {
|
||
// Package not available or failed
|
||
}
|
||
|
||
// Check common locations
|
||
const possiblePaths = [
|
||
// Custom env var
|
||
process.env.MSEDGEDRIVER_PATH,
|
||
// Common install locations
|
||
'C:\\Program Files\\Microsoft\\Edge\\msedgedriver.exe',
|
||
'C:\\Program Files (x86)\\Microsoft\\Edge\\msedgedriver.exe',
|
||
path.join(process.env.USERPROFILE || '', 'msedgedriver.exe'),
|
||
path.join(process.env.USERPROFILE || '', '.webdrivers', 'msedgedriver.exe'),
|
||
];
|
||
|
||
for (const p of possiblePaths) {
|
||
if (p && fs.existsSync(p)) {
|
||
return p;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
export const config: Options.Testrunner = {
|
||
// Test specs
|
||
specs: ['./e2e-native/**/*.spec.ts'],
|
||
exclude: [],
|
||
|
||
// Capabilities
|
||
maxInstances: 1, // Tauri apps should run one at a time
|
||
capabilities: [
|
||
{
|
||
// Use tauri-driver as the WebDriver server
|
||
'tauri:options': {
|
||
application: getTauriBinaryPath(),
|
||
},
|
||
},
|
||
],
|
||
|
||
// Test framework
|
||
framework: 'mocha',
|
||
mochaOpts: {
|
||
ui: 'bdd',
|
||
timeout: 180000,
|
||
},
|
||
|
||
// Reporters
|
||
reporters: ['spec'],
|
||
|
||
// Log level
|
||
logLevel: 'info',
|
||
|
||
// Connection settings for tauri-driver
|
||
hostname: '127.0.0.1',
|
||
port: 4444,
|
||
|
||
// No built-in service - tauri-driver started via onPrepare hook
|
||
services: [],
|
||
|
||
// Timeouts
|
||
connectionRetryTimeout: 120000,
|
||
connectionRetryCount: 3,
|
||
|
||
// Hooks
|
||
onPrepare: async () => {
|
||
if (!shouldRunNative) {
|
||
if (tauriDriverStatus === 'missing') {
|
||
throw new Error(
|
||
`tauri-driver not found at: ${tauriDriverPath}\nInstall it with: cargo install tauri-driver`
|
||
);
|
||
}
|
||
if (tauriDriverStatus === 'not_supported') {
|
||
process.exit(0);
|
||
}
|
||
throw new Error('tauri-driver failed to start');
|
||
}
|
||
|
||
console.log(`Starting tauri-driver: ${tauriDriverPath}`);
|
||
|
||
// On Windows, check for msedgedriver
|
||
const edgeDriverPath = await getMsEdgeDriverPath();
|
||
if (process.platform === 'win32' && !edgeDriverPath) {
|
||
console.warn(
|
||
'\n⚠️ msedgedriver.exe not found in common locations.\n' +
|
||
' Download from: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/\n' +
|
||
' Then either:\n' +
|
||
' - Add to PATH\n' +
|
||
' - Set MSEDGEDRIVER_PATH environment variable\n' +
|
||
' - Place in your home directory\n'
|
||
);
|
||
}
|
||
|
||
// Build args
|
||
const args = ['--port', '4444'];
|
||
if (edgeDriverPath) {
|
||
args.push('--native-driver', edgeDriverPath);
|
||
console.log(`Using msedgedriver: ${edgeDriverPath}`);
|
||
}
|
||
|
||
// Start tauri-driver
|
||
tauriDriverProcess = spawn(tauriDriverPath, args, {
|
||
stdio: ['ignore', 'pipe', 'pipe'],
|
||
});
|
||
|
||
tauriDriverProcess.stdout?.on('data', (data: Buffer | string) => {
|
||
const text = typeof data === 'string' ? data : data.toString();
|
||
console.log(`[tauri-driver] ${text.trim()}`);
|
||
});
|
||
|
||
tauriDriverProcess.stderr?.on('data', (data: Buffer | string) => {
|
||
const text = typeof data === 'string' ? data : data.toString();
|
||
console.error(`[tauri-driver] ${text.trim()}`);
|
||
});
|
||
|
||
// Wait for tauri-driver to be ready
|
||
await new Promise<void>((resolve, reject) => {
|
||
const timeout = setTimeout(() => {
|
||
reject(new Error('tauri-driver failed to start within 10s'));
|
||
}, 10000);
|
||
|
||
const checkReady = async () => {
|
||
try {
|
||
const response = await fetch('http://127.0.0.1:4444/status');
|
||
if (response.ok) {
|
||
clearTimeout(timeout);
|
||
console.log('tauri-driver is ready');
|
||
resolve();
|
||
}
|
||
} catch {
|
||
// Not ready yet, retry
|
||
setTimeout(checkReady, 200);
|
||
}
|
||
};
|
||
|
||
// Start checking after a brief delay
|
||
setTimeout(checkReady, 500);
|
||
});
|
||
},
|
||
|
||
onComplete: async () => {
|
||
// Stop tauri-driver
|
||
if (tauriDriverProcess) {
|
||
console.log('Stopping tauri-driver');
|
||
tauriDriverProcess.kill();
|
||
tauriDriverProcess = null;
|
||
}
|
||
},
|
||
|
||
beforeSession: async () => {
|
||
if (!shouldRunNative) {
|
||
return;
|
||
}
|
||
const binaryPath = getTauriBinaryPath();
|
||
if (!fs.existsSync(binaryPath)) {
|
||
throw new Error(
|
||
`Tauri binary not found at: ${binaryPath}\n` +
|
||
'Please build the app first with: npm run tauri:build'
|
||
);
|
||
}
|
||
console.log(`Using Tauri binary: ${binaryPath}`);
|
||
},
|
||
|
||
afterTest: async (test, _context, { error }) => {
|
||
if (error) {
|
||
// Take screenshot on failure
|
||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||
const screenshotPath = `./e2e-native/screenshots/${test.title}-${timestamp}.png`;
|
||
await browser.saveScreenshot(screenshotPath);
|
||
console.log(`Screenshot saved: ${screenshotPath}`);
|
||
}
|
||
},
|
||
};
|