diff --git a/e2e-webdriver/.gitignore b/e2e-webdriver/.gitignore index a930e496..58ce0d72 100644 --- a/e2e-webdriver/.gitignore +++ b/e2e-webdriver/.gitignore @@ -1,2 +1,3 @@ node_modules/ .drivers/ +.drivers-*/ diff --git a/e2e-webdriver/scripts/install-edge-driver.mjs b/e2e-webdriver/scripts/install-edge-driver.mjs index fdfb9862..53a331f0 100644 --- a/e2e-webdriver/scripts/install-edge-driver.mjs +++ b/e2e-webdriver/scripts/install-edge-driver.mjs @@ -1,14 +1,9 @@ import process from 'node:process'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { download } from 'edgedriver'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); +import { resolveEdgeDriverPath } from './resolve-edge-driver.mjs'; if (process.platform !== 'win32') { process.exit(0); } -const driversDirectory = path.join(__dirname, '..', '.drivers'); -const driverPath = await download(undefined, driversDirectory); +const driverPath = await resolveEdgeDriverPath({ forceRefresh: true }); process.stdout.write(`${driverPath}\n`); diff --git a/e2e-webdriver/scripts/resolve-edge-driver.mjs b/e2e-webdriver/scripts/resolve-edge-driver.mjs new file mode 100644 index 00000000..27f5147c --- /dev/null +++ b/e2e-webdriver/scripts/resolve-edge-driver.mjs @@ -0,0 +1,134 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { download as downloadEdgeDriver } from 'edgedriver'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const driversDirectory = path.join(__dirname, '..', '.drivers'); +const edgeDriverPath = path.join(driversDirectory, 'msedgedriver.exe'); +const versionMarkerPath = path.join(driversDirectory, 'webview-version.txt'); + +function compareVersionStrings(left, right) { + const leftParts = left.split('.').map(Number); + const rightParts = right.split('.').map(Number); + + for (let index = 0; index < 4; index += 1) { + const leftValue = leftParts[index] ?? 0; + const rightValue = rightParts[index] ?? 0; + if (leftValue !== rightValue) { + return leftValue - rightValue; + } + } + + return 0; +} + +function readVersionMarker() { + if (!fs.existsSync(versionMarkerPath)) { + return null; + } + + return fs.readFileSync(versionMarkerPath, 'utf8').trim() || null; +} + +function writeVersionMarker(webViewVersion) { + fs.mkdirSync(driversDirectory, { recursive: true }); + fs.writeFileSync(versionMarkerPath, `${webViewVersion}\n`); +} + +function getInstalledDriverVersion(driverPath) { + if (!fs.existsSync(driverPath)) { + return null; + } + + const result = spawnSync(driverPath, ['--version'], { encoding: 'utf8' }); + const output = `${result.stdout}\n${result.stderr}`; + const match = output.match(/(\d+\.\d+\.\d+\.\d+)/); + return match?.[1] ?? null; +} + +function getWebView2VersionOnWindows() { + const programFilesX86 = process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)'; + const webViewRoot = path.join(programFilesX86, 'Microsoft', 'EdgeWebView', 'Application'); + + if (!fs.existsSync(webViewRoot)) { + return null; + } + + const versionDirectories = fs.readdirSync(webViewRoot, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => entry.name) + .filter(name => /^\d+\.\d+\.\d+\.\d+$/.test(name)) + .sort(compareVersionStrings); + + return versionDirectories.at(-1) ?? null; +} + +function removeInstalledDriver() { + if (fs.existsSync(edgeDriverPath)) { + fs.unlinkSync(edgeDriverPath); + } + + const driverNotesDirectory = path.join(driversDirectory, 'Driver_Notes'); + if (fs.existsSync(driverNotesDirectory)) { + fs.rmSync(driverNotesDirectory, { recursive: true, force: true }); + } + + if (fs.existsSync(versionMarkerPath)) { + fs.unlinkSync(versionMarkerPath); + } +} + +function driverMatchesWebViewVersion(driverVersion, webViewVersion) { + if (!driverVersion || !webViewVersion) { + return false; + } + + return compareVersionStrings(driverVersion, webViewVersion) === 0; +} + +export async function resolveEdgeDriverPath(options = {}) { + const { forceRefresh = false } = options; + + if (process.platform !== 'win32') { + return null; + } + + const explicitDriverPath = process.env.SFM_MS_EDGE_DRIVER_PATH; + if (explicitDriverPath) { + return explicitDriverPath; + } + + const webViewVersion = getWebView2VersionOnWindows(); + if (!webViewVersion) { + throw new Error( + 'Could not detect Microsoft Edge WebView2 runtime version. Install WebView2 or set SFM_MS_EDGE_DRIVER_PATH.', + ); + } + + const markerVersion = readVersionMarker(); + const installedDriverVersion = getInstalledDriverVersion(edgeDriverPath); + const hasMatchingDriver = fs.existsSync(edgeDriverPath) + && markerVersion === webViewVersion + && driverMatchesWebViewVersion(installedDriverVersion, webViewVersion); + + if (!forceRefresh && hasMatchingDriver) { + return edgeDriverPath; + } + + removeInstalledDriver(); + fs.mkdirSync(driversDirectory, { recursive: true }); + + const downloadedDriverPath = await downloadEdgeDriver(webViewVersion, driversDirectory); + const downloadedDriverVersion = getInstalledDriverVersion(downloadedDriverPath); + + if (!driverMatchesWebViewVersion(downloadedDriverVersion, webViewVersion)) { + throw new Error( + `Downloaded msedgedriver ${downloadedDriverVersion ?? 'unknown'} does not match WebView2 ${webViewVersion}.`, + ); + } + + writeVersionMarker(webViewVersion); + return downloadedDriverPath; +} diff --git a/e2e-webdriver/test/helpers/main-window.js b/e2e-webdriver/test/helpers/main-window.js index 81843034..c9b10087 100644 --- a/e2e-webdriver/test/helpers/main-window.js +++ b/e2e-webdriver/test/helpers/main-window.js @@ -6,6 +6,8 @@ export const ADDRESS_BAR_BREADCRUMBS_SELECTOR = '[data-e2e-root="address-bar-bre export const FILE_BROWSER_LOADING_SELECTOR = '[data-e2e-root="file-browser-loading"]'; export const FILE_BROWSER_ENTRIES_SELECTOR = '[data-e2e-root="file-browser-entries"]'; export const FILE_BROWSER_EMPTY_DIRECTORY_SELECTOR = '[data-e2e-root="file-browser-empty-directory"]'; +export const FILE_BROWSER_DIRECTORY_ENTRY_SELECTOR = '.file-browser-list-view__entry--dir[data-entry-path]'; +export const ADDRESS_BAR_PARENT_BREADCRUMB_SELECTOR = '.address-bar__part:not(.address-bar__part--last)'; export async function focusWindowThatContainsMainSidebar() { await browser.waitUntil(async () => { @@ -35,3 +37,55 @@ export async function waitForPathname(expectedPathname) { timeoutMsg: `Route did not become ${expectedPathname}`, }); } + +export async function openNavigatorFromSidebar() { + const navigatorButton = $('button.nav-sidebar-item[value="navigator"]'); + await navigatorButton.waitForClickable({ timeout: 30000 }); + await navigatorButton.click(); + await waitForPathname('/navigator'); +} + +export async function waitForNavigatorDirectoryListing() { + const breadcrumbs = $(ADDRESS_BAR_BREADCRUMBS_SELECTOR); + await breadcrumbs.waitForDisplayed({ timeout: 30000 }); + + await browser.waitUntil(async () => { + return !(await $(FILE_BROWSER_LOADING_SELECTOR).isExisting()); + }, { + timeout: 120000, + timeoutMsg: 'Timed out waiting for navigator directory content to finish loading', + }); + + const hasEntryList = await $(FILE_BROWSER_ENTRIES_SELECTOR).isExisting(); + const hasEmptyDirectoryState = await $(FILE_BROWSER_EMPTY_DIRECTORY_SELECTOR).isExisting(); + + if (!hasEntryList && !hasEmptyDirectoryState) { + throw new Error('Expected file list or empty directory state after navigator load'); + } +} + +export async function getNavigatorCurrentPath() { + return $(ADDRESS_BAR_BREADCRUMBS_SELECTOR).getAttribute('data-e2e-current-path'); +} + +export async function waitForNavigatorCurrentPath(expectedPath) { + await browser.waitUntil(async () => { + const currentPath = await getNavigatorCurrentPath(); + return currentPath === expectedPath; + }, { + timeout: 30000, + interval: 100, + timeoutMsg: `Timed out waiting for navigator path to become ${expectedPath}`, + }); +} + +export async function waitForNavigatorCurrentPathChange(previousPath) { + await browser.waitUntil(async () => { + const currentPath = await getNavigatorCurrentPath(); + return Boolean(currentPath) && currentPath !== previousPath; + }, { + timeout: 30000, + interval: 100, + timeoutMsg: `Timed out waiting for navigator path to change from ${previousPath}`, + }); +} diff --git a/e2e-webdriver/test/specs/navigator-folder-navigation.e2e.js b/e2e-webdriver/test/specs/navigator-folder-navigation.e2e.js new file mode 100644 index 00000000..8d6efacc --- /dev/null +++ b/e2e-webdriver/test/specs/navigator-folder-navigation.e2e.js @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict'; +import { $, browser } from '@wdio/globals'; +import { + ADDRESS_BAR_PARENT_BREADCRUMB_SELECTOR, + FILE_BROWSER_DIRECTORY_ENTRY_SELECTOR, + focusWindowThatContainsMainSidebar, + getNavigatorCurrentPath, + openNavigatorFromSidebar, + waitForNavigatorCurrentPath, + waitForNavigatorCurrentPathChange, + waitForNavigatorDirectoryListing, +} from '../helpers/main-window.js'; + +describe('Navigator folder navigation', () => { + it('opens a subfolder on double-click and returns via the parent breadcrumb', async function () { + await focusWindowThatContainsMainSidebar(); + await openNavigatorFromSidebar(); + await waitForNavigatorDirectoryListing(); + + const initialPath = await getNavigatorCurrentPath(); + assert.ok(initialPath, 'Expected navigator to resolve a current directory path'); + + const directoryEntries = await $$(FILE_BROWSER_DIRECTORY_ENTRY_SELECTOR); + if (directoryEntries.length === 0) { + this.skip(); + } + + const firstDirectoryEntry = directoryEntries[0]; + const folderPath = await firstDirectoryEntry.getAttribute('data-entry-path'); + assert.ok(folderPath, 'Expected directory entry to expose data-entry-path'); + + await firstDirectoryEntry.doubleClick(); + await waitForNavigatorCurrentPathChange(initialPath); + await waitForNavigatorDirectoryListing(); + + const openedPath = await getNavigatorCurrentPath(); + assert.equal(openedPath, folderPath); + + const parentBreadcrumbs = await $$(ADDRESS_BAR_PARENT_BREADCRUMB_SELECTOR); + assert.ok(parentBreadcrumbs.length > 0, 'Expected a parent breadcrumb for back navigation'); + + const immediateParentBreadcrumb = parentBreadcrumbs[parentBreadcrumbs.length - 1]; + await immediateParentBreadcrumb.waitForClickable({ timeout: 30000 }); + await immediateParentBreadcrumb.click(); + + await waitForNavigatorCurrentPath(initialPath); + await waitForNavigatorDirectoryListing(); + + const returnedPath = await getNavigatorCurrentPath(); + assert.equal(returnedPath, initialPath); + }); +}); diff --git a/e2e-webdriver/test/specs/navigator-page.e2e.js b/e2e-webdriver/test/specs/navigator-page.e2e.js index 36d8bb73..de680aae 100644 --- a/e2e-webdriver/test/specs/navigator-page.e2e.js +++ b/e2e-webdriver/test/specs/navigator-page.e2e.js @@ -1,12 +1,9 @@ -import assert from 'node:assert/strict'; -import { $, browser } from '@wdio/globals'; +import { browser } from '@wdio/globals'; import { - ADDRESS_BAR_BREADCRUMBS_SELECTOR, - FILE_BROWSER_EMPTY_DIRECTORY_SELECTOR, - FILE_BROWSER_ENTRIES_SELECTOR, - FILE_BROWSER_LOADING_SELECTOR, focusWindowThatContainsMainSidebar, - waitForPathname, + getNavigatorCurrentPath, + openNavigatorFromSidebar, + waitForNavigatorDirectoryListing, } from '../helpers/main-window.js'; function looksLikeFilesystemPath(path) { @@ -32,37 +29,16 @@ function looksLikeFilesystemPath(path) { describe('Navigator page', () => { it('opens on a real folder and shows a directory listing or empty state', async () => { await focusWindowThatContainsMainSidebar(); - - const navigatorButton = $('button.nav-sidebar-item[value="navigator"]'); - await navigatorButton.waitForClickable({ timeout: 30000 }); - await navigatorButton.click(); - await waitForPathname('/navigator'); - - const breadcrumbs = $(ADDRESS_BAR_BREADCRUMBS_SELECTOR); - await breadcrumbs.waitForDisplayed({ timeout: 30000 }); + await openNavigatorFromSidebar(); + await waitForNavigatorDirectoryListing(); await browser.waitUntil(async () => { - const pathValue = await breadcrumbs.getAttribute('data-e2e-current-path'); + const pathValue = await getNavigatorCurrentPath(); return looksLikeFilesystemPath(pathValue); }, { timeout: 30000, interval: 100, timeoutMsg: 'Timed out waiting for navigator address bar to show a resolved filesystem path', }); - - await browser.waitUntil(async () => { - return !(await $(FILE_BROWSER_LOADING_SELECTOR).isExisting()); - }, { - timeout: 120000, - timeoutMsg: 'Timed out waiting for navigator directory content to finish loading', - }); - - const hasEntryList = await $(FILE_BROWSER_ENTRIES_SELECTOR).isExisting(); - const hasEmptyDirectoryState = await $(FILE_BROWSER_EMPTY_DIRECTORY_SELECTOR).isExisting(); - - assert.ok( - hasEntryList || hasEmptyDirectoryState, - 'Expected file list or empty directory state after load', - ); }); }); diff --git a/e2e-webdriver/wdio.conf.js b/e2e-webdriver/wdio.conf.js index 6633e49c..ea7de113 100644 --- a/e2e-webdriver/wdio.conf.js +++ b/e2e-webdriver/wdio.conf.js @@ -3,10 +3,9 @@ import os from 'node:os'; import path from 'node:path'; import { spawn, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -import { download as downloadEdgeDriver } from 'edgedriver'; +import { resolveEdgeDriverPath } from './scripts/resolve-edge-driver.mjs'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const vendoredEdgeDriverPath = path.join(__dirname, '.drivers', 'msedgedriver.exe'); const repoRoot = path.resolve(__dirname, '..'); const tauriBinaryName = process.platform === 'win32' ? 'sigma-file-manager.exe' : 'sigma-file-manager'; @@ -39,6 +38,17 @@ export const config = { onPrepare: () => { if (process.env.SFM_SKIP_TAURI_BUILD_FOR_WEBDRIVER === '1') { + const distIndexPath = path.join(repoRoot, 'dist', 'index.html'); + if (!fs.existsSync(applicationPath)) { + throw new Error( + 'Missing debug SFM binary for WebDriver e2e. Run: npm run tauri -- build --debug --no-bundle', + ); + } + if (!fs.existsSync(distIndexPath)) { + throw new Error( + 'Missing frontend dist for WebDriver e2e. Run: npm run tauri -- build --debug --no-bundle', + ); + } return; } const buildResult = spawnSync( @@ -61,27 +71,8 @@ export const config = { beforeSession: async () => { const tauriDriverArguments = []; if (process.platform === 'win32') { - const explicitEdgeDriverPath = process.env.SFM_MS_EDGE_DRIVER_PATH; - if (explicitEdgeDriverPath) { - tauriDriverArguments.push('--native-driver', explicitEdgeDriverPath); - } - else if (fs.existsSync(vendoredEdgeDriverPath)) { - tauriDriverArguments.push('--native-driver', vendoredEdgeDriverPath); - } - else { - try { - const driversDirectory = path.join(__dirname, '.drivers'); - const downloadedEdgeDriverPath = await downloadEdgeDriver(undefined, driversDirectory); - tauriDriverArguments.push('--native-driver', downloadedEdgeDriverPath); - } - catch (downloadError) { - throw new Error( - 'Microsoft Edge WebDriver (msedgedriver.exe) is required. Add it to PATH, set SFM_MS_EDGE_DRIVER_PATH, ' - + 'or run: npm run install-edge-driver --prefix e2e-webdriver', - { cause: downloadError }, - ); - } - } + const edgeDriverPath = await resolveEdgeDriverPath(); + tauriDriverArguments.push('--native-driver', edgeDriverPath); } tauriDriverProcess = spawn( tauriDriverPath, diff --git a/package.json b/package.json index 535cd592..4f27d89a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "test": "npm run test:unit:run", - "test:e2e": "npm run test:e2e:webdriver:skip-build", + "test:e2e": "npm run test:e2e:webdriver", "test:e2e:webdriver": "npm run test --prefix e2e-webdriver", "test:e2e:webdriver:skip-build": "cross-env SFM_SKIP_TAURI_BUILD_FOR_WEBDRIVER=1 npm run test --prefix e2e-webdriver", "test:e2e:webdriver:install-edge-driver": "npm run install-edge-driver --prefix e2e-webdriver",