Files
noteflow/client/wdio.mac.conf.ts
Travis Vasceannie dab973d8aa
Some checks failed
CI / test-python (push) Failing after 22m26s
CI / test-typescript (push) Successful in 11m4s
CI / test-rust (push) Failing after 7m11s
x
2026-01-24 17:02:07 -05:00

227 lines
7.8 KiB
TypeScript

/**
* WebdriverIO Configuration for macOS Native Tauri Testing (Appium mac2).
*
* This config targets the built .app bundle on macOS using Appium's mac2 driver.
* Requires Appium 2 + mac2 driver installed and Appium server running.
*/
import type { Options } from '@wdio/types';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { tmpdir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const APP_BUNDLE_ID = 'com.noteflow.desktop';
const APPIUM_HOST = '127.0.0.1';
const APPIUM_PORT = 4723;
function writeStdout(message: string): void {
process.stdout.write(`${message}\n`);
}
function writeStderr(message: string): void {
process.stderr.write(`${message}\n`);
}
function getTauriAppBundlePath(): string {
const projectRoot = path.resolve(__dirname, 'src-tauri');
const releasePath = path.join(
projectRoot,
'target',
'release',
'bundle',
'macos',
'NoteFlow.app'
);
const debugPath = path.join(projectRoot, 'target', 'debug', 'bundle', 'macos', 'NoteFlow.app');
if (fs.existsSync(releasePath)) {
return releasePath;
}
if (fs.existsSync(debugPath)) {
return debugPath;
}
return releasePath;
}
const APP_BUNDLE_PATH = getTauriAppBundlePath();
const SCREENSHOT_DIR = path.join(__dirname, 'e2e-native-mac', 'screenshots');
const E2E_NATIVE_ENABLED = process.env.NOTEFLOW_E2E_NATIVE === '1';
const E2E_RUN_ID = process.env.NOTEFLOW_E2E_RUN_ID ?? `run-${Date.now()}`;
const E2E_OUTPUT_PATH =
process.env.NOTEFLOW_E2E_OUTPUT_PATH ?? path.join(tmpdir(), 'noteflow-e2e-native.json');
const E2E_WAV_PATH =
process.env.NOTEFLOW_E2E_WAV_PATH ??
path.join(__dirname, 'e2e-native-mac', 'fixtures', 'test-tones-2s.wav');
const E2E_SERVER_ADDRESS = process.env.NOTEFLOW_E2E_SERVER ?? '127.0.0.1:50052';
const PROCESS_ARGUMENTS = E2E_NATIVE_ENABLED
? {
env: {
NOTEFLOW_E2E_NATIVE: '1',
NOTEFLOW_E2E_RUN_ID: E2E_RUN_ID,
NOTEFLOW_E2E_OUTPUT_PATH: E2E_OUTPUT_PATH,
NOTEFLOW_E2E_WAV_PATH: E2E_WAV_PATH,
NOTEFLOW_E2E_TIMEOUT_SECS: process.env.NOTEFLOW_E2E_TIMEOUT_SECS ?? '60',
NOTEFLOW_E2E_CHUNK_MS: process.env.NOTEFLOW_E2E_CHUNK_MS ?? '100',
NOTEFLOW_E2E_SPEED: process.env.NOTEFLOW_E2E_SPEED ?? '2',
NOTEFLOW_SERVER_ADDRESS: E2E_SERVER_ADDRESS,
NOTEFLOW_DISABLE_AUDIO_CAPTURE: '1',
},
}
: undefined;
async function ensureAppiumServer(): Promise<void> {
const statusUrl = `http://${APPIUM_HOST}:${APPIUM_PORT}/status`;
try {
const response = await fetch(statusUrl);
if (!response.ok) {
throw new Error(`Appium status check failed: ${response.status}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
const cause = error instanceof Error && error.cause ? String(error.cause) : '';
const details = [message, cause].filter(Boolean).join(' ');
if (details.includes('EPERM') || details.includes('Operation not permitted')) {
throw new Error(
'Local network access appears blocked for this process. Allow your terminal (or Codex) under System Settings → Privacy & Security → Local Network, and ensure no firewall blocks 127.0.0.1:4723.'
);
}
throw new Error(
`Appium server not reachable at ${statusUrl}. Start it with: appium --base-path / --log-level error\nDetails: ${details}`
);
}
}
function ensureXcodeAvailable(): void {
const result = spawnSync('xcodebuild', ['-version'], { encoding: 'utf8' });
if (result.error || result.status !== 0) {
const message = result.stderr?.trim() || result.stdout?.trim() || 'xcodebuild not available';
throw new Error(
`Xcode is required for the mac2 driver (WebDriverAgentMac). Install Xcode and select it with:\n sudo xcode-select -s /Applications/Xcode.app/Contents/Developer\nDetails: ${message}`
);
}
}
function ensureDeveloperModeEnabled(): void {
const result = spawnSync('/usr/sbin/DevToolsSecurity', ['-status'], { encoding: 'utf8' });
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.toLowerCase();
if (output.includes('enabled')) {
return;
}
if (output.includes('disabled')) {
throw new Error(
`Developer mode is disabled. Enable it in System Settings → Privacy & Security → Developer Mode.
You can also enable dev tools access via CLI:
sudo /usr/sbin/DevToolsSecurity -enable
sudo dseditgroup -o edit -a "$(whoami)" -t user _developer
Then log out and back in.`
);
}
if (result.error || result.status !== 0) {
const message = result.stderr?.trim() || result.stdout?.trim() || 'DevToolsSecurity failed';
writeStderr(
`Warning: Unable to read developer mode status. Verify it is enabled in System Settings → Privacy & Security → Developer Mode. Details: ${message}`
);
}
}
function ensureAutomationModeConfigured(): void {
// Run automationmodetool without arguments to get configuration status
// Automation mode itself gets enabled when WebDriverAgentMac runs;
// we just need to verify the machine is configured to allow it without prompts.
const result = spawnSync('automationmodetool', [], { encoding: 'utf8' });
const output = `${result.stdout ?? ''}${result.stderr ?? ''}`.toLowerCase();
const requiresAuth =
output.includes('requires user authentication') || output.includes('requires authentication');
const doesNotRequireAuth =
output.includes('does not require user authentication') ||
output.includes('does not require authentication');
// Check if the machine requires user authentication for automation mode
// If it does, the user needs to run the enable command
if (requiresAuth && !doesNotRequireAuth) {
throw new Error(
'Automation Mode requires user authentication. Configure it with:\n sudo automationmodetool enable-automationmode-without-authentication\nThis allows WebDriverAgentMac to enable automation mode without prompts.'
);
}
// If automationmodetool isn't found or fails completely, warn but don't block
if (result.error) {
writeStderr(`Warning: Could not check automation mode configuration: ${result.error.message}`);
}
}
export const config: Options.Testrunner = {
// Test specs
specs: ['./e2e-native-mac/**/*.spec.ts'],
exclude: [],
// Capabilities
maxInstances: 1,
capabilities: [
{
platformName: 'mac',
'appium:automationName': 'mac2',
'appium:app': APP_BUNDLE_PATH,
'appium:bundleId': APP_BUNDLE_ID,
'appium:newCommandTimeout': 120,
'appium:serverStartupTimeout': 120000,
'appium:showServerLogs': true,
...(PROCESS_ARGUMENTS ? { 'appium:processArguments': PROCESS_ARGUMENTS } : {}),
},
],
// Test framework
framework: 'mocha',
mochaOpts: {
ui: 'bdd',
timeout: 180000,
},
// Reporters
reporters: ['spec'],
// Log level
logLevel: 'info',
// Appium connection settings
hostname: APPIUM_HOST,
port: APPIUM_PORT,
path: '/',
// No built-in service - Appium started separately
services: [],
// Timeouts
connectionRetryTimeout: 120000,
connectionRetryCount: 3,
// Hooks
onPrepare: async () => {
if (!fs.existsSync(APP_BUNDLE_PATH)) {
throw new Error(
`Tauri app bundle not found at: ${APP_BUNDLE_PATH}\nBuild it with: npm run tauri:build`
);
}
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
ensureXcodeAvailable();
ensureDeveloperModeEnabled();
ensureAutomationModeConfigured();
await ensureAppiumServer();
},
afterTest: async (test, _context, { error }) => {
if (error) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const screenshotPath = path.join(SCREENSHOT_DIR, `${test.title}-${timestamp}.png`);
await browser.saveScreenshot(screenshotPath);
writeStdout(`Screenshot saved: ${screenshotPath}`);
}
},
};