Skip to content

Commit 04998bc

Browse files
committed
feat: Support --start-url for target firefox-android
1 parent e582e8f commit 04998bc

3 files changed

Lines changed: 198 additions & 7 deletions

File tree

src/extension-runners/firefox-android.js

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
* in a Firefox for Android instance.
44
*/
55

6+
import fs from 'fs/promises';
67
import path from 'path';
78
import readline from 'readline';
89

10+
import JSZip from 'jszip';
11+
912
import { withTempDir } from '../util/temp-dir.js';
1013
import DefaultADBUtils from '../util/adb.js';
1114
import {
@@ -24,7 +27,6 @@ const ignoredParams = {
2427
keepProfileChanges: '--keep-profile-changes',
2528
browserConsole: '--browser-console',
2629
preInstall: '--pre-install',
27-
startUrl: '--start-url',
2830
args: '--args',
2931
};
3032

@@ -116,6 +118,8 @@ export class FirefoxAndroidExtensionRunner {
116118
// Connect to RDP socket on the local tcp server, install all the pushed extension
117119
// and keep track of the built and installed extension by extension sourceDir.
118120
await this.rdpInstallExtensions();
121+
122+
await this.launchStartUrlsIfNeeded();
119123
}
120124

121125
// Method exported from the IExtensionRunner interface.
@@ -569,4 +573,83 @@ export class FirefoxAndroidExtensionRunner {
569573
this.reloadableExtensions.set(extension.sourceDir, addonId);
570574
}
571575
}
576+
577+
/**
578+
* Creates a helper extension to open --start-url as needed.
579+
* Must be called after rdpInstallExtensions().
580+
*
581+
* The opened tabs could be in the background, if Fenix's homescreen is shown
582+
* when the app launches. This is a limitation of Firefox for Android.
583+
*/
584+
async launchStartUrlsIfNeeded() {
585+
const {
586+
adbUtils,
587+
selectedAdbDevice,
588+
selectedArtifactsDir,
589+
params: { startUrl },
590+
} = this;
591+
if (!startUrl?.length) {
592+
return;
593+
}
594+
const urls = Array.isArray(startUrl) ? startUrl : [startUrl];
595+
596+
log.debug(`Trying to open URLs: ${urls.join(' ')}`);
597+
598+
async function backgroundScript(urlsToOpen) {
599+
// eslint-disable-next-line no-undef
600+
const browser = globalThis.browser;
601+
for (const url of urlsToOpen) {
602+
try {
603+
await browser.tabs.create({ url });
604+
} catch (e) {
605+
// Ignore invalid URLs.
606+
Promise.reject(e);
607+
}
608+
}
609+
await browser.management.uninstallSelf();
610+
}
611+
// The extension ID and file name here are chosen to be unique. In theory
612+
// if users choose the same extension ID, this would overwrite their
613+
// extension. They should simply not choose these identifiers!
614+
const xpiFileName = 'web-ext-internal-helper-to-open-start-urls.xpi';
615+
const adbExtensionPath = `${selectedArtifactsDir}/${xpiFileName}`;
616+
const manifestJson = JSON.stringify({
617+
name: 'web-ext helper to open URLs',
618+
description: 'web-ext helper to open URLs in new tabs and self-destruct',
619+
version: '1',
620+
manifest_version: 3,
621+
background: { scripts: ['background.js'] },
622+
permissions: ['notifications'],
623+
browser_specific_settings: {
624+
gecko: { id: '@web-ext-internal-helper-to-open-start-urls' },
625+
gecko_android: {},
626+
},
627+
});
628+
const backgroundJs = `(${backgroundScript})(${JSON.stringify(urls)})`;
629+
await withTempDir(async (tmpDir) => {
630+
const zip = new JSZip();
631+
zip.file('manifest.json', manifestJson);
632+
zip.file('background.js', backgroundJs);
633+
const rawZipBytes = await zip.generateAsync({
634+
compression: 'DEFLATE',
635+
type: 'nodebuffer',
636+
});
637+
const extensionPath = path.join(tmpDir.path(), xpiFileName);
638+
await fs.writeFile(extensionPath, rawZipBytes);
639+
640+
await adbUtils.pushFile(
641+
selectedAdbDevice,
642+
extensionPath,
643+
adbExtensionPath,
644+
);
645+
});
646+
try {
647+
// this.remoteFirefox is initialized by rdpInstallExtensions.
648+
await this.remoteFirefox.installTemporaryAddon(adbExtensionPath);
649+
log.debug('Successfully installed helper extension to open URLs');
650+
} catch (e) {
651+
// Unexpected, but not a fatal error from web-ext's perspective.
652+
log.error(`Failed to open URLs via internal helper extension: ${e}`);
653+
}
654+
}
572655
}

tests/unit/helpers.js

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import path from 'path';
22
import EventEmitter from 'events';
33
import stream from 'stream';
4+
import streamConsumers from 'stream/consumers';
45
import { promisify } from 'util';
56
import { fileURLToPath, pathToFileURL } from 'url';
67

@@ -26,14 +27,15 @@ export class ZipFile {
2627
constructor() {
2728
this._zip = null;
2829
this._close = null;
30+
this._entries = null;
2931
}
3032

3133
/*
3234
* Open a zip file and return a promise that resolves to a yauzl
3335
* zipfile object.
3436
*/
35-
open(...args) {
36-
return promisify(yauzl.open)(...args).then((zip) => {
37+
open(zippath) {
38+
return promisify(yauzl.open)(zippath, { autoClose: false }).then((zip) => {
3739
this._zip = zip;
3840
this._close = new Promise((resolve) => {
3941
zip.once('close', resolve);
@@ -62,8 +64,13 @@ export class ZipFile {
6264
'Cannot operate on a falsey zip file. Call open() first.',
6365
);
6466
}
67+
if (this._entries) {
68+
throw new Error('readEach can be called only once');
69+
}
70+
this._entries = new Map();
6571

6672
this._zip.on('entry', (entry) => {
73+
this._entries.set(entry.fileName, entry);
6774
onRead(entry);
6875
});
6976

@@ -94,6 +101,32 @@ export class ZipFile {
94101
});
95102
});
96103
}
104+
105+
async getEntryByFileName(fileName) {
106+
if (!this._entries) {
107+
await this.readEach(() => {});
108+
}
109+
return this._entries.get(fileName);
110+
}
111+
112+
/**
113+
* Resolve a promise with the content of the entry as a string.
114+
*/
115+
async getAsText(fileName) {
116+
const entry = await this.getEntryByFileName(fileName);
117+
if (!entry) {
118+
throw new Error(`Entry not found in zip file: ${fileName}`);
119+
}
120+
return new Promise((resolve, reject) => {
121+
this._zip.openReadStream(entry, (err, readStream) => {
122+
if (err) {
123+
reject(err);
124+
} else {
125+
resolve(streamConsumers.text(readStream));
126+
}
127+
});
128+
});
129+
}
97130
}
98131

99132
/*

tests/unit/test-extension-runners/test.firefox-android.js

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
createFakeStdin,
1414
getFakeFirefox,
1515
getFakeRemoteFirefox,
16+
ZipFile,
1617
} from '../helpers.js';
1718

1819
// Fake result for client.installTemporaryAddon().then(installResult => ...)
@@ -833,6 +834,84 @@ describe('util/extension-runners/firefox-android', () => {
833834
sinon.assert.calledOnce(anotherCallback);
834835
});
835836

837+
it('opens a single URL when specified via --start-url', async () => {
838+
const { params, fakeADBUtils } = prepareSelectedDeviceAndAPKParams();
839+
params.startUrl = 'https://example.com/';
840+
841+
let pushedBackgroundJs;
842+
fakeADBUtils.pushFile = sinon.spy(async (_, localZipPath) => {
843+
if (localZipPath.includes('web-ext-internal-helper-to-open-start-urls.xpi')) {
844+
const zipFile = new ZipFile();
845+
await zipFile.open(localZipPath);
846+
pushedBackgroundJs = await zipFile.getAsText('background.js');
847+
await zipFile.close();
848+
}
849+
});
850+
851+
const runnerInstance = new FirefoxAndroidExtensionRunner(params);
852+
await runnerInstance.run();
853+
854+
const { installTemporaryAddon } = runnerInstance.remoteFirefox;
855+
856+
sinon.assert.calledTwice(installTemporaryAddon);
857+
858+
sinon.assert.calledWithMatch(
859+
installTemporaryAddon,
860+
`${runnerInstance.selectedArtifactsDir}/${builtFileName}.xpi`,
861+
);
862+
863+
sinon.assert.calledWithMatch(
864+
installTemporaryAddon,
865+
`${runnerInstance.selectedArtifactsDir}/web-ext-internal-helper-to-open-start-urls.xpi`,
866+
);
867+
sinon.assert.calledWithMatch(
868+
fakeADBUtils.pushFile,
869+
'emulator-1',
870+
sinon.match(/\bweb-ext-internal-helper-to-open-start-urls\.xpi$/),
871+
`${runnerInstance.selectedArtifactsDir}/web-ext-internal-helper-to-open-start-urls.xpi`,
872+
);
873+
assert.include(pushedBackgroundJs, '["https://example.com/"]');
874+
});
875+
876+
it('opens a multiple URL when specified via --start-url', async () => {
877+
const { params, fakeADBUtils } = prepareSelectedDeviceAndAPKParams();
878+
params.startUrl = ['about:blank', 'http://localhost'];
879+
880+
let pushedBackgroundJs;
881+
fakeADBUtils.pushFile = sinon.spy(async (_, localZipPath) => {
882+
if (localZipPath.includes('web-ext-internal-helper-to-open-start-urls.xpi')) {
883+
const zipFile = new ZipFile();
884+
await zipFile.open(localZipPath);
885+
pushedBackgroundJs = await zipFile.getAsText('background.js');
886+
await zipFile.close();
887+
}
888+
});
889+
890+
const runnerInstance = new FirefoxAndroidExtensionRunner(params);
891+
await runnerInstance.run();
892+
893+
sinon.assert.calledWithMatch(
894+
fakeADBUtils.pushFile,
895+
'emulator-1',
896+
sinon.match(/\bweb-ext-internal-helper-to-open-start-urls\.xpi$/),
897+
`${runnerInstance.selectedArtifactsDir}/web-ext-internal-helper-to-open-start-urls.xpi`,
898+
);
899+
assert.include(pushedBackgroundJs, '["about:blank","http://localhost"]');
900+
});
901+
902+
it('does not install helper extension without --start-url', async () => {
903+
const { params } = prepareSelectedDeviceAndAPKParams();
904+
905+
const runnerInstance = new FirefoxAndroidExtensionRunner(params);
906+
await runnerInstance.run();
907+
908+
const { installTemporaryAddon } = runnerInstance.remoteFirefox;
909+
sinon.assert.calledOnce(installTemporaryAddon);
910+
const seenPath = installTemporaryAddon.firstCall.args[0];
911+
assert.include(seenPath, '.xpi');
912+
assert.notInclude(seenPath, 'web-ext-internal-helper-to-open-start-urls.xpi');
913+
});
914+
836915
it('logs warnings on the unsupported CLI options', async () => {
837916
const params = prepareSelectedDeviceAndAPKParams();
838917

@@ -856,10 +935,6 @@ describe('util/extension-runners/firefox-android', () => {
856935
params: { preInstall: true },
857936
expectedMessage: /Android target does not support --pre-install/,
858937
},
859-
{
860-
params: { startUrl: 'http://fake-start-url.org' },
861-
expectedMessage: /Android target does not support --start-url/,
862-
},
863938
{
864939
params: { args: ['-headless=false'] },
865940
expectedMessage: /Android target does not support --args/,

0 commit comments

Comments
 (0)