Files
electron/spec/api-shell.spec.ts
claude[bot]andClaude 74141f73b9 test: fix retry-reliant macOS UI specs (#55223)
app sandbox options: the mixed-sandbox fixture credited each window's
argv to whichever window it had created last, so a sandboxed window
that reported after its DevTools opened left argv.sandbox unset and the
spec hung until the timeout. Take each window's argv from its own
webContents.ipc, open both windows at once, read the report until the
app ends the connection, and fail as soon as the app exits early.

shell.openExternal: the 'removes focus' spec read isFocused() only after
openExternal() resolved, by which time the activation requests made to
take focus beforehand, or the browser finishing its launch, can have
given focus back. Read the state when 'blur' arrives instead.

Notification remove*: wait for the notifications to drop out of
getHistory() instead of sleeping 100 or 500 ms, since Notification
Center applies removals asynchronously.


Claude-Session: https://claude.ai/code/session_01LPMvJBQiyP6b7QjatACmnS

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-26 08:01:50 +00:00

238 lines
9.1 KiB
TypeScript

import { shell } from 'electron/common';
import { BrowserWindow, app } from 'electron/main';
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest';
import { execSync } from 'node:child_process';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as http from 'node:http';
import * as os from 'node:os';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe, ifit, listen, waitUntil } from './lib/spec-helpers.ts';
import { closeAllWindows } from './lib/window-helpers.ts';
describe('shell module', () => {
describe('shell.openExternal()', { tags: ['serial'] }, () => {
let envVars: Record<string, string | undefined> = {};
let server: http.Server;
afterAll(() => {
if (process.env.CI && process.platform === 'win32') {
// Edge may cause issues with visibility tests, so make sure it is closed after testing.
const killEdge = 'Get-Process | Where Name -Like "msedge" | Stop-Process';
execSync(killEdge, { shell: 'powershell.exe' });
}
}, 60000);
beforeEach(() => {
envVars = {
display: process.env.DISPLAY,
de: process.env.DE,
browser: process.env.BROWSER
};
});
afterEach(async () => {
// reset env vars to prevent side effects
if (process.platform === 'linux') {
process.env.DE = envVars.de;
process.env.BROWSER = envVars.browser;
process.env.DISPLAY = envVars.display;
}
await closeAllWindows();
if (server) {
server.close();
server = null as unknown as http.Server;
}
});
async function urlOpened() {
let url = 'http://127.0.0.1';
let requestReceived: Promise<any>;
if (process.platform === 'linux') {
process.env.BROWSER = '/bin/true';
process.env.DE = 'generic';
process.env.DISPLAY = '';
requestReceived = Promise.resolve();
} else if (process.platform === 'darwin') {
// On the Mac CI machines, Safari tries to ask for a password to the
// code signing keychain we set up to test code signing (see
// https://github.com/electron/electron/pull/19969#issuecomment-526278890),
// so use a blur event as a crude proxy.
const w = new BrowserWindow({ show: true });
requestReceived = once(w, 'blur');
} else {
server = http.createServer((req, res) => {
res.end();
});
url = (await listen(server)).url;
requestReceived = new Promise<void>((resolve) => server.on('connection', () => resolve()));
}
return { url, requestReceived };
}
it('opens an external link', async () => {
const { url, requestReceived } = await urlOpened();
await Promise.all<void>([shell.openExternal(url), requestReceived]);
});
ifit(process.platform === 'darwin')('throws when there is no application registered to open the URL', async () => {
const url = `unknownscheme-${Date.now()}://test`;
await expect(shell.openExternal(url)).rejects.toThrow(/No application found to open URL/);
});
it('opens an external link in the renderer', async () => {
const { url, requestReceived } = await urlOpened();
const w = new BrowserWindow({
show: false,
webPreferences: { sandbox: false, contextIsolation: false, nodeIntegration: true }
});
await w.loadURL('about:blank');
await Promise.all<void>([
w.webContents.executeJavaScript(`require("electron").shell.openExternal(${JSON.stringify(url)})`),
requestReceived
]);
});
ifit(process.platform === 'darwin')(
'removes focus from the electron window after opening an external link',
async () => {
const url = 'http://127.0.0.1';
const w = new BrowserWindow({ show: true });
await once(w, 'focus');
// The browser launched by the earlier openExternal specs can still be
// coming to the front and take focus back from the new window, so wait
// until the window holds focus before checking that openExternal
// removes it.
await waitUntil(async () => {
if (!w.isFocused()) {
app.focus({ steal: true });
w.focus();
return false;
}
await setTimeout(250);
return w.isFocused();
});
expect(w.isFocused()).toBe(true);
// Read the focus state when 'blur' arrives. By the time openExternal()
// also resolves, the activation requests made above or the browser
// finishing its launch can already have handed focus back to the
// window, which says nothing about whether opening the link removed it.
const focusedWhenBlurred = once(w, 'blur').then(() => w.isFocused());
await shell.openExternal(url);
expect(await focusedWhenBlurred).toBe(false);
}
);
});
describe('shell.trashItem()', () => {
afterEach(closeAllWindows);
it('moves an item to the trash', async () => {
const dir = await fs.promises.mkdtemp(path.resolve(app.getPath('temp'), 'electron-shell-spec-'));
const filename = path.join(dir, 'temp-to-be-deleted');
await fs.promises.writeFile(filename, 'dummy-contents');
await shell.trashItem(filename);
expect(fs.existsSync(filename)).toBe(false);
});
it('throws when called with a nonexistent path', async () => {
const filename = path.join(app.getPath('temp'), 'does-not-exist');
await expect(shell.trashItem(filename)).rejects.toThrow();
});
it('works in the renderer process', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, contextIsolation: false } });
w.loadURL('about:blank');
await expect(
w.webContents.executeJavaScript("require('electron').shell.trashItem('does-not-exist')")
).rejects.toThrow(/does-not-exist|Failed to move item|Failed to create FileOperation/);
});
});
const shortcutOptions = {
target: 'C:\\target',
description: 'description',
cwd: 'cwd',
args: 'args',
appUserModelId: 'appUserModelId',
icon: 'icon',
iconIndex: 1,
toastActivatorClsid: '{0E3CFA27-6FEA-410B-824F-A174B6E865E5}'
};
ifdescribe(process.platform === 'win32')('shell.readShortcutLink(shortcutPath)', () => {
it('throws when failed', () => {
expect(() => {
shell.readShortcutLink('not-exist');
}).toThrow('Failed to read shortcut link');
});
const fixtures = path.resolve(import.meta.dirname, 'fixtures');
it('reads all properties of a shortcut', () => {
const shortcut = shell.readShortcutLink(path.join(fixtures, 'assets', 'shortcut.lnk'));
expect(shortcut).toEqual(shortcutOptions);
});
});
ifdescribe(process.platform === 'win32')('shell.writeShortcutLink(shortcutPath[, operation], options)', () => {
const tmpShortcut = path.join(os.tmpdir(), `${Date.now()}.lnk`);
afterEach(() => {
fs.unlinkSync(tmpShortcut);
});
it('writes the shortcut', () => {
expect(shell.writeShortcutLink(tmpShortcut, { target: 'C:\\' })).toBe(true);
expect(fs.existsSync(tmpShortcut)).toBe(true);
});
it('writes the shortcut with omitted operation (defaults to create)', () => {
expect(shell.writeShortcutLink(tmpShortcut, shortcutOptions)).toBe(true);
expect(fs.existsSync(tmpShortcut)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual(shortcutOptions);
const newOptions = { ...shortcutOptions, description: 'new description' };
expect(shell.writeShortcutLink(tmpShortcut, newOptions)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual(newOptions);
});
it('correctly sets the fields', () => {
expect(shell.writeShortcutLink(tmpShortcut, shortcutOptions)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual(shortcutOptions);
});
it('updates the shortcut', () => {
expect(shell.writeShortcutLink(tmpShortcut, 'update', shortcutOptions)).toBe(false);
expect(shell.writeShortcutLink(tmpShortcut, 'create', shortcutOptions)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual(shortcutOptions);
const change = { target: 'D:\\' };
expect(shell.writeShortcutLink(tmpShortcut, 'update', change)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual({ ...shortcutOptions, ...change });
});
it('replaces the shortcut', () => {
expect(shell.writeShortcutLink(tmpShortcut, 'replace', shortcutOptions)).toBe(false);
expect(shell.writeShortcutLink(tmpShortcut, 'create', shortcutOptions)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual(shortcutOptions);
const change = {
target: 'D:\\',
description: 'description2',
cwd: 'cwd2',
args: 'args2',
appUserModelId: 'appUserModelId2',
icon: 'icon2',
iconIndex: 2,
toastActivatorClsid: '{C51A3996-CAD9-4934-848B-16285D4A1496}'
};
expect(shell.writeShortcutLink(tmpShortcut, 'replace', change)).toBe(true);
expect(shell.readShortcutLink(tmpShortcut)).toEqual(change);
});
});
});