chore: prepare spec helpers and vitest setup for migration

This commit is contained in:
Samuel Attard
2026-04-11 22:56:35 -07:00
parent 7a446b0f84
commit d8fe4fd091
4 changed files with 119 additions and 7 deletions

View File

@@ -0,0 +1,38 @@
import * as chai from 'chai';
import { afterEach, beforeEach } from 'vitest';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { runCleanupFunctions } from '../lib/spec-helpers';
import chaiAsPromised = require('chai-as-promised');
import dirtyChai = require('dirty-chai');
chai.use(chaiAsPromised);
chai.use(dirtyChai as any);
// Show full object diff.
// https://github.com/chaijs/chai/issues/469
chai.config.truncateThreshold = 0;
// Skip any tests listed in disabled-tests.json.
const disabledTests = new Set(JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'disabled-tests.json'), 'utf8')));
beforeEach((ctx) => {
const parts: string[] = [ctx.task.name];
let suite = ctx.task.suite;
while (suite) {
if (suite.name) parts.unshift(suite.name);
suite = suite.suite;
}
if (disabledTests.has(parts.join(' '))) {
ctx.skip();
}
});
// Run defer()-ed cleanup functions after each test, before other afterEach hooks
// registered by the test file (vitest runs hooks in registration order, and
// setupFiles are loaded first).
afterEach(async () => {
await runCleanupFunctions();
});

View File

@@ -4,14 +4,21 @@ import * as path from 'node:path';
import electronPool from './electron-pool';
const electronShim = path.resolve(__dirname, 'electron-shim.cjs');
export default defineConfig({
resolve: {
alias: {
electron: path.resolve(__dirname, 'electron-shim.cjs')
electron: electronShim,
'electron/main': electronShim,
'electron/common': electronShim,
'electron/renderer': electronShim
}
},
test: {
include: ['spec/**/*.spec.ts'],
exclude: ['spec/fixtures/**', 'spec/node_modules/**'],
setupFiles: ['./spec/_vitest_runner/setup.ts'],
// Custom pool: each worker is a real Electron main process.
pool: electronPool as any,
// Run test *files* in parallel across workers...

View File

@@ -2,15 +2,22 @@
// It is launched via `spawn(electron, [<spec-v2 dir>])` with an IPC channel,
// so `process.send` is available and vitest's fork-worker protocol works.
const { app } = require('electron');
const { app, protocol } = require('electron');
const path = require('node:path');
const v8 = require('node:v8');
process.on('uncaughtException', (err) => {
console.error('Unhandled exception in vitest worker:', err);
process.exit(1);
});
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
if (process.env.ELECTRON_TEST_DISABLE_HARDWARE_ACCELERATION) {
app.disableHardwareAcceleration();
}
// The pool allocates (mkdtemp) and cleans up this directory; the worker just
// points Electron at it before app ready.
const userDataDir = process.env.ELECTRON_VITEST_USER_DATA_DIR;
@@ -19,8 +26,50 @@ if (!userDataDir) {
}
app.setPath('userData', userDataDir);
v8.setFlagsFromString('--expose_gc');
app.commandLine.appendSwitch('js-flags', '--expose_gc');
app.on('window-all-closed', () => null);
// Use fake device for Media Stream to replace actual camera and microphone.
app.commandLine.appendSwitch('use-fake-device-for-media-stream');
app.commandLine.appendSwitch(
'host-resolver-rules',
[
'MAP localhost2 127.0.0.1',
'MAP ipv4.localhost2 10.0.0.1',
'MAP ipv6.localhost2 [::1]',
'MAP notfound.localhost2 ~NOTFOUND'
].join(', ')
);
// Enable features required by tests.
app.commandLine.appendSwitch(
'enable-features',
[
// spec/api-web-frame-main-spec.ts
'DocumentPolicyIncludeJSCallStacksInCrashReports',
// spec/spellchecker-spec.ts
'UnrestrictSpellingAndGrammarForTesting'
].join(',')
);
global.standardScheme = 'app';
global.zoomScheme = 'zoom';
global.serviceWorkerScheme = 'sw';
protocol.registerSchemesAsPrivileged([
{ scheme: global.standardScheme, privileges: { standard: true, secure: true, stream: false } },
{ scheme: global.zoomScheme, privileges: { standard: true, secure: true } },
{ scheme: global.serviceWorkerScheme, privileges: { allowServiceWorkers: true, standard: true, secure: true } },
{ scheme: 'http-like', privileges: { standard: true, secure: true, corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors-blob', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'cors', privileges: { corsEnabled: true, supportFetchAPI: true } },
{ scheme: 'no-cors', privileges: { supportFetchAPI: true } },
{ scheme: 'no-fetch', privileges: { corsEnabled: true } },
{ scheme: 'stream', privileges: { standard: true, stream: true } },
{ scheme: 'foo', privileges: { standard: true } },
{ scheme: 'bar', privileges: { standard: true } }
]);
app
.whenReady()
.then(async () => {

View File

@@ -1,7 +1,7 @@
import { BrowserWindow } from 'electron/main';
import { AssertionError } from 'chai';
import { SuiteFunction, TestFunction } from 'mocha';
import { afterAll, beforeAll, describe, it } from 'vitest';
import * as childProcess from 'node:child_process';
import * as http from 'node:http';
@@ -22,8 +22,26 @@ const addOnly = <T>(fn: Function): T => {
return wrapped as any;
};
export const ifit = (condition: boolean) => (condition ? it : addOnly<TestFunction>(it.skip));
export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<SuiteFunction>(describe.skip));
export const ifit = (condition: boolean) => (condition ? it : addOnly<typeof it>(it.skip));
export const ifdescribe = (condition: boolean) => (condition ? describe : addOnly<typeof describe>(describe.skip));
type DoneCallback = (err?: unknown) => void;
/**
* Adapts a mocha-style callback test (receiving a `done` function) into a
* vitest-compatible test that returns a Promise. `done()` resolves,
* `done(err)` rejects.
*/
export function withDone(fn: (done: DoneCallback) => void): () => Promise<void> {
return () =>
new Promise<void>((resolve, reject) => {
const done: DoneCallback = (err) => {
if (err != null) reject(err instanceof Error ? err : new Error(String(err)));
else resolve();
};
fn(done);
});
}
export const isWayland =
process.platform === 'linux' &&
@@ -189,10 +207,10 @@ export async function getRemoteContext() {
}
export function useRemoteContext(opts?: any) {
before(async () => {
beforeAll(async () => {
remoteContext.unshift(await makeRemoteContext(opts));
});
after(() => {
afterAll(() => {
const w = remoteContext.shift();
w!.close();
});