From e851c0bbe110bfcaa2034cbcc2509de2d886afa9 Mon Sep 17 00:00:00 2001 From: "trop[bot]" <37223003+trop[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:31:38 -0500 Subject: [PATCH] test: add Linux-specific test for `app.getApplicationNameForProtocol()` (#51214) * test: add Linux-specific test for getApplicationNameForProtocol() On Linux, use XDG env vars to inject a mock that we can use to test app.getApplicationNameForProtocol(). Co-authored-by: Charles Kerr * fixup! test: add Linux-specific test for getApplicationNameForProtocol() better system mocks Co-authored-by: Charles Kerr * chore: make lint happy --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Charles Kerr Co-authored-by: David Sanders --- spec/api-app-spec.ts | 126 +++++++++++++++++++ spec/fixtures/api/protocol-name/main.js | 8 ++ spec/fixtures/api/protocol-name/package.json | 4 + 3 files changed, 138 insertions(+) create mode 100644 spec/fixtures/api/protocol-name/main.js create mode 100644 spec/fixtures/api/protocol-name/package.json diff --git a/spec/api-app-spec.ts b/spec/api-app-spec.ts index 1d217a61f7..1617fee238 100644 --- a/spec/api-app-spec.ts +++ b/spec/api-app-spec.ts @@ -1453,6 +1453,132 @@ describe('app module', () => { it('returns an empty string for a bogus protocol', () => { expect(app.getApplicationNameForProtocol('bogus-protocol://')).to.equal(''); }); + + ifdescribe(process.platform === 'linux')('on Linux with mocked XDG dirs', () => { + 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, + xdgDataHome: string, + xdgConfigHome: string, + xdgBinDir: string + ): Promise { + return new Promise((resolve, reject) => { + const child = cp.spawn(process.execPath, [fixtureApp, url], { + env: { + ...process.env, + PATH: [xdgBinDir, process.env.PATH].filter(Boolean).join(':'), + XDG_DATA_HOME: xdgDataHome, + XDG_DATA_DIRS: [xdgDataHome, process.env.XDG_DATA_DIRS].filter(Boolean).join(':'), + XDG_CONFIG_HOME: xdgConfigHome + }, + stdio: ['ignore', 'pipe', 'pipe'] + }); + let stdout = ''; + let stderr = ''; + child.stdout.on('data', (d: Buffer) => { + stdout += d; + }); + child.stderr.on('data', (d: Buffer) => { + stderr += d; + }); + child.on('close', (code) => { + if (code !== 0) { + reject(new Error(`Fixture exited with code ${code}: ${stderr}`)); + return; + } + + try { + const parsed = JSON.parse(stdout); + resolve(parsed.name.trimEnd()); + } catch { + reject(new Error(`Failed to parse output: ${stdout}\nstderr: ${stderr}`)); + } + }); + child.on('error', reject); + }); + } + + let xdgDir: string; + let xdgDataHome: string; + 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); + }); + + after(() => { + fs.rmSync(xdgDir, { recursive: true, force: true }); + }); + + it('returns the desktop file ID for a registered protocol', async () => { + const name = await spawnWithXdgMock(`${mockScheme}://`, xdgDataHome, xdgConfigHome, xdgBinDir); + expect(name).to.equal(desktopFileId); + }); + + it('returns an empty string for an unregistered protocol', async () => { + const name = await spawnWithXdgMock('unregistered-proto://', xdgDataHome, xdgConfigHome, xdgBinDir); + expect(name).to.equal(''); + }); + }); }); ifdescribe(process.platform !== 'linux')('getApplicationInfoForProtocol()', () => { diff --git a/spec/fixtures/api/protocol-name/main.js b/spec/fixtures/api/protocol-name/main.js new file mode 100644 index 0000000000..381aad82c9 --- /dev/null +++ b/spec/fixtures/api/protocol-name/main.js @@ -0,0 +1,8 @@ +const { app } = require('electron'); + +app.whenReady().then(() => { + const url = process.argv[2]; + const name = app.getApplicationNameForProtocol(url); + process.stdout.write(JSON.stringify({ name })); + app.quit(); +}); diff --git a/spec/fixtures/api/protocol-name/package.json b/spec/fixtures/api/protocol-name/package.json new file mode 100644 index 0000000000..c09844bbf4 --- /dev/null +++ b/spec/fixtures/api/protocol-name/package.json @@ -0,0 +1,4 @@ +{ + "name": "electron-test-protocol-name", + "main": "main.js" +}