mirror of
https://github.com/electron/electron.git
synced 2026-10-28 11:44:46 +08:00
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>
238 lines
9.1 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|