Dev: auto-match Edge WebDriver to WebView2 and add navigator folder navigation test

This commit is contained in:
aleksey-hoffman committed 2026-07-03 01:15:51 +03:00
1 parent 97b5f41b27
commit fa654d3cf5
8 files changed
+265 -62

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
node_modules/
.drivers/
.drivers-*/
@@ -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`);
@@ -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;
}
+54
View File
@@ -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}`,
});
}
@@ -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);
});
});
+7 -31
View File
@@ -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',
);
});
});
+14 -23
View File
@@ -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,
+1 -1
View File
@@ -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",