test: add linux coverage for default protocol client APIs (#51285)

Add Linux-only app tests to check the default protocol handler.
This includes adding reusable XDG mock fixtures.

Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com>
Co-authored-by: Charles Kerr <charles@charleskerr.com>
This commit is contained in:
trop[bot]
2026-04-23 16:28:45 -05:00
committed by GitHub
parent 75923388e6
commit 26ff715d36
6 changed files with 168 additions and 58 deletions

View File

@@ -9,16 +9,32 @@ import * as fs from 'node:fs';
import * as http from 'node:http';
import * as https from 'node:https';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import * as readline from 'node:readline';
import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';
import { collectStreamBody, getResponse } from './lib/net-helpers';
import { ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import { defer, ifdescribe, ifit, isWayland, listen, waitUntil } from './lib/spec-helpers';
import { closeWindow, closeAllWindows } from './lib/window-helpers';
const fixturesPath = path.resolve(__dirname, 'fixtures');
const xdgMockFixturePath = path.join(fixturesPath, 'api', 'xdg-mock');
function makeXdgMockDirectories (prefix: string) {
const xdgDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
fs.cpSync(xdgMockFixturePath, xdgDir, { recursive: true });
const xdgDataHome = path.join(xdgDir, 'data');
const xdgConfigHome = path.join(xdgDir, 'config');
const xdgBinDir = path.join(xdgDir, 'bin');
fs.chmodSync(path.join(xdgBinDir, 'xdg-mime'), 0o755);
fs.chmodSync(path.join(xdgBinDir, 'xdg-settings'), 0o755);
return { xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir };
}
const isMacOSx64 = process.platform === 'darwin' && process.arch === 'x64';
@@ -1458,7 +1474,6 @@ describe('app module', () => {
const fixtureApp = path.join(fixturesPath, 'api', 'protocol-name');
const desktopFileId = 'mock-browser.desktop';
const mockScheme = 'mockproto';
const mockMimeType = `x-scheme-handler/${mockScheme}`;
function spawnWithXdgMock (
url: string,
@@ -1507,62 +1522,7 @@ describe('app module', () => {
let xdgConfigHome: string;
let xdgBinDir: string;
before(() => {
xdgDir = fs.mkdtempSync(path.join(require('node:os').tmpdir(), 'electron-xdg-'));
xdgDataHome = path.join(xdgDir, 'data');
xdgConfigHome = path.join(xdgDir, 'config');
xdgBinDir = path.join(xdgDir, 'bin');
const appsDir = path.join(xdgDataHome, 'applications');
fs.mkdirSync(appsDir, { recursive: true });
fs.mkdirSync(xdgConfigHome, { recursive: true });
fs.mkdirSync(xdgBinDir, { recursive: true });
fs.writeFileSync(
path.join(appsDir, desktopFileId),
[
'[Desktop Entry]',
'Name=Mock Browser',
'Exec=/usr/bin/true %u',
'Type=Application',
`MimeType=${mockMimeType};`
].join('\n')
);
const mimeAppsContents = [
'[Default Applications]',
`${mockMimeType}=${desktopFileId}`,
'',
'[Added Associations]',
`${mockMimeType}=${desktopFileId};`
].join('\n');
fs.writeFileSync(path.join(xdgConfigHome, 'mimeapps.list'), mimeAppsContents);
fs.writeFileSync(path.join(appsDir, 'mimeapps.list'), mimeAppsContents);
fs.writeFileSync(path.join(appsDir, 'defaults.list'), mimeAppsContents);
// Different xdg-utils versions resolve custom XDG dirs differently, so
// prepend a deterministic xdg-mime shim for this test.
const xdgMimePath = path.join(xdgBinDir, 'xdg-mime');
fs.writeFileSync(
xdgMimePath,
[
'#!/bin/sh',
'if [ "$1" != "query" ] || [ "$2" != "default" ]; then',
' exit 1',
'fi',
'mime="$3"',
'for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do',
' if [ -f "$list" ]; then',
' result=$(grep "^$mime=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)',
' if [ -n "$result" ]; then',
' printf "%s\\n" "$result"',
' exit 0',
' fi',
' fi',
'done',
'exit 0'
].join('\n')
);
fs.chmodSync(xdgMimePath, 0o755);
({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-'));
});
after(() => {
@@ -1604,6 +1564,95 @@ describe('app module', () => {
});
});
ifdescribe(process.platform === 'linux')('default protocol client APIs with mocked XDG settings', () => {
const protocol = 'electron-test-linux';
const desktopFileId = 'electron-test.desktop';
const protocolMimeType = `x-scheme-handler/${protocol}`;
let xdgDir: string;
let xdgDataHome: string;
let xdgConfigHome: string;
let xdgBinDir: string;
let oldEnv: Record<string, string | undefined>;
const getRegisteredHandler = () => {
for (const list of [
path.join(xdgConfigHome, 'mimeapps.list'),
path.join(xdgDataHome, 'applications', 'mimeapps.list'),
path.join(xdgDataHome, 'applications', 'defaults.list')
]) {
if (!fs.existsSync(list)) continue;
const match = fs
.readFileSync(list, 'utf8')
.split('\n')
.find((line) => line.startsWith(`${protocolMimeType}=`));
// foo=bar.desktop; --> bar.desktop
if (match) return match.split('=', 2)[1].split(';', 1)[0];
}
return '';
};
beforeEach(() => {
({ xdgDir, xdgDataHome, xdgConfigHome, xdgBinDir } = makeXdgMockDirectories('electron-xdg-default-client-'));
oldEnv = {
PATH: process.env.PATH,
CHROME_DESKTOP: process.env.CHROME_DESKTOP,
XDG_DATA_HOME: process.env.XDG_DATA_HOME,
XDG_DATA_DIRS: process.env.XDG_DATA_DIRS,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME
};
defer(() => {
for (const [key, value] of Object.entries(oldEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
fs.rmSync(xdgDir, { recursive: true, force: true });
});
process.env.PATH = [xdgBinDir, oldEnv.PATH].filter(Boolean).join(':');
process.env.XDG_DATA_HOME = xdgDataHome;
process.env.XDG_DATA_DIRS = [xdgDataHome, oldEnv.XDG_DATA_DIRS].filter(Boolean).join(':');
process.env.XDG_CONFIG_HOME = xdgConfigHome;
app.setDesktopName(desktopFileId);
});
it('writes the default handler to the XDG association files', async () => {
expect(getRegisteredHandler()).to.equal('');
expect(app.setAsDefaultProtocolClient(protocol)).to.equal(true);
await waitUntil(() => getRegisteredHandler() === desktopFileId);
expect(getRegisteredHandler()).to.equal(desktopFileId);
});
it('detects whether the app is the default protocol client', async () => {
expect(app.isDefaultProtocolClient(protocol)).to.equal(false);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${protocolMimeType}=other.desktop`].join('\n')
);
expect(app.isDefaultProtocolClient(protocol)).to.equal(false);
fs.writeFileSync(
path.join(xdgConfigHome, 'mimeapps.list'),
['[Default Applications]', `${protocolMimeType}=${desktopFileId}`].join('\n')
);
await waitUntil(() => app.isDefaultProtocolClient(protocol));
expect(app.isDefaultProtocolClient(protocol)).to.equal(true);
});
});
describe('protocol scheme validation', () => {
it('rejects empty protocol names', () => {
expect(app.setAsDefaultProtocolClient('')).to.equal(false);

15
spec/fixtures/api/xdg-mock/bin/xdg-mime vendored Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
if [ "$1" != "query" ] || [ "$2" != "default" ]; then
exit 1
fi
mime="$3"
for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do
if [ -f "$list" ]; then
result=$(grep "^$mime=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)
if [ -n "$result" ]; then
printf "%s\n" "$result"
exit 0
fi
fi
done
exit 0

View File

@@ -0,0 +1,31 @@
#!/bin/sh
set -eu
get_handler() {
for list in "$XDG_CONFIG_HOME/mimeapps.list" "$XDG_DATA_HOME/applications/mimeapps.list" "$XDG_DATA_HOME/applications/defaults.list"; do
if [ -f "$list" ]; then
result=$(grep "^x-scheme-handler/$1=" "$list" | head -n 1 | cut -d= -f2 | cut -d";" -f1)
if [ -n "$result" ]; then
printf "%s\n" "$result"
return 0
fi
fi
done
return 1
}
if [ "$1" = "set" ] && [ "$2" = "default-url-scheme-handler" ]; then
mkdir -p "$XDG_CONFIG_HOME"
{
printf "[Default Applications]\n"
printf "x-scheme-handler/%s=%s\n" "$3" "$4"
} > "$XDG_CONFIG_HOME/mimeapps.list"
exit 0
fi
if [ "$1" = "check" ] && [ "$2" = "default-url-scheme-handler" ]; then
if [ "$(get_handler "$3" 2>/dev/null || true)" = "$4" ]; then
printf "yes\n"
else
printf "no\n"
fi
exit 0
fi
exit 1

View File

@@ -0,0 +1,5 @@
[Default Applications]
x-scheme-handler/mockproto=mock-browser.desktop
[Added Associations]
x-scheme-handler/mockproto=mock-browser.desktop;

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Electron Test
Exec=/usr/bin/true %u
Type=Application
MimeType=x-scheme-handler/electron-test-linux;

View File

@@ -0,0 +1,5 @@
[Desktop Entry]
Name=Mock Browser
Exec=/usr/bin/true %u
Type=Application
MimeType=x-scheme-handler/mockproto;