227 lines
7.8 KiB
TypeScript
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}`);
|
|
}
|
|
},
|
|
};
|