Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/cmd/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export default async function run(
firefoxApkComponent,
// Chromium CLI options.
chromiumBinary,
chromiumPref = {},
chromiumProfile,
},
{
Expand Down Expand Up @@ -84,6 +85,11 @@ export default async function run(
// Create an alias for --pref since it has been transformed into an
// object containing one or more preferences.
const customPrefs = pref;

// Create an alias for --chromium-pref since it has been transformed into an
// object containing one or more preferences.
const customChromiumPrefs = chromiumPref;

const manifestData = await getValidatedManifest(sourceDir);

const profileDir = firefoxProfile || chromiumProfile;
Expand Down Expand Up @@ -191,6 +197,7 @@ export default async function run(
...commonRunnerParams,
verbose,
chromiumBinary,
customChromiumPrefs,
chromiumProfile,
};

Expand Down
15 changes: 15 additions & 0 deletions src/extension-runners/chromium.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { createLogger } from '../util/logger.js';
import { TempDir } from '../util/temp-dir.js';
import isDirectory from '../util/is-directory.js';
import fileExists from '../util/file-exists.js';
import expandPrefs from '../util/expand-prefs.js';

const log = createLogger(import.meta.url);

Expand All @@ -28,6 +29,8 @@ export const DEFAULT_CHROME_FLAGS = ChromeLauncher.defaultFlags().filter(
(flag) => !EXCLUDED_CHROME_FLAGS.includes(flag),
);

const DEFAULT_PREFS = { 'extensions.ui.developer_mode': true };

// This is a client for the Chrome Devtools protocol. The methods and results
// are documented at https://chromedevtools.github.io/devtools-protocol/tot/
class ChromeDevtoolsProtocolClient {
Expand Down Expand Up @@ -331,6 +334,7 @@ export class ChromiumExtensionRunner {
logLevel: this.params.verbose ? 'verbose' : 'silent',
// Ignore default flags to keep the extension enabled.
ignoreDefaultFlags: true,
prefs: this.getPrefs(),
});
this.cdp = new ChromeDevtoolsProtocolClient(this.chromiumInstance);

Expand Down Expand Up @@ -598,4 +602,15 @@ export class ChromiumExtensionRunner {
}
}
}

/**
* Returns a deep preferences object based on a set of flat preferences, like
* "extensions.ui.developer_mode".
*/
getPrefs() {
return expandPrefs({
...DEFAULT_PREFS,
...this.params.customChromiumPrefs,
});
}
}
12 changes: 12 additions & 0 deletions src/program.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,18 @@ Example: $0 --help run.
demandOption: false,
type: 'string',
},
'chromium-pref': {
describe:
'Launch chromium with a custom preference ' +
'(example: --chromium-pref=browser.theme.follows_system_colors=false). ' +
'You can repeat this option to set more than one ' +
'preference.',
demandOption: false,
requiresArg: true,
type: 'array',
coerce: (arg) =>
arg != null ? coerceCLICustomPreference(arg) : undefined,
},
'chromium-profile': {
describe: 'Path to a custom Chromium profile',
demandOption: false,
Expand Down
36 changes: 36 additions & 0 deletions src/util/expand-prefs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Given an object where the keys are the flattened path to a
* preference, and the value is the value to set at that path, return
* an object where the paths are fully expanded.
*/
export default function expandPrefs(prefs) {
const prefsMap = new Map();

for (const [key, value] of Object.entries(prefs)) {
let submap = prefsMap;
const props = key.split('.');
const lastProp = props.pop();

for (const prop of props) {
if (!submap.has(prop)) {
submap.set(prop, new Map());
}

submap = submap.get(prop);

if (!(submap instanceof Map)) {
throw new Error(
`Cannot set ${key} because a value already exists at ${prop}`,
);
}
}
submap.set(lastProp, value);
}
return mapToObject(prefsMap);
}

function mapToObject(map) {
return Object.fromEntries(
Array.from(map, ([k, v]) => [k, v instanceof Map ? mapToObject(v) : v]),
);
}
16 changes: 16 additions & 0 deletions tests/unit/test-cmd/test.run.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,22 @@ describe('run', () => {
);
});

it('provides chromium custom preferences to the Chromium runner params', async () => {
const fakeChromiumPref = { 'extensions.ui.developer_mode': false };
const cmd = await prepareRun();
await cmd.run({
target: ['chromium'],
chromiumPref: fakeChromiumPref,
});

sinon.assert.calledWithMatch(chromiumRunnerStub, {
customChromiumPrefs: sinon.match.object,
});

const { customChromiumPrefs } = chromiumRunnerStub.firstCall.args[0];
assert.strictEqual(customChromiumPrefs, fakeChromiumPref);
});

it('creates multiple extension runners', async () => {
const cmd = await prepareRun();
await cmd.run({ target: ['firefox-android', 'firefox-desktop'] });
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/test-extension-runners/test.chromium.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@ describe('util/extension-runners/chromium', async () => {
assert.notInclude(DEFAULT_CHROME_FLAGS, '--disable-extensions');
});

it('check if default extensions.ui.developer_mode is true', async () => {
const { params } = prepareExtensionRunnerParams();
const runnerInstance = new ChromiumExtensionRunner(params);
await runnerInstance.run();
sinon.assert.calledWithMatch(params.chromiumLaunch, {
prefs: {
extensions: {
ui: {
developer_mode: true,
},
},
},
});
await runnerInstance.exit();
});

it('installs and runs the extension', async () => {
const { params, fakeChromeInstance } = prepareExtensionRunnerParams();
const runnerInstance = new ChromiumExtensionRunner(params);
Expand All @@ -120,6 +136,13 @@ describe('util/extension-runners/chromium', async () => {
'--disable-blink-features=AutomationControlled',
],
startingUrl: undefined,
prefs: {
extensions: {
ui: {
developer_mode: true,
},
},
},
});

await runnerInstance.exit();
Expand Down Expand Up @@ -602,6 +625,41 @@ describe('util/extension-runners/chromium', async () => {
sinon.assert.calledOnce(fakeChromeInstance.kill);
});

describe('getPrefs', () => {
it('merges default and custom preferences from an object and passes them to the launcher', async () => {
const { params } = prepareExtensionRunnerParams({
params: {
customChromiumPrefs: {
'extensions.ui.developer_mode': false,
'browser.theme.color': 'dark',
},
},
});

const runnerInstance = new ChromiumExtensionRunner(params);

const expectedPrefs = {
extensions: {
ui: {
developer_mode: false,
},
},
browser: {
theme: {
color: 'dark',
},
},
};

assert.deepEqual(runnerInstance.getPrefs(), expectedPrefs);

await runnerInstance.run();
sinon.assert.calledWithMatch(params.chromiumLaunch, {
prefs: expectedPrefs,
});
});
});

describe('reloadAllExtensions', () => {
let runnerInstance;

Expand Down
68 changes: 68 additions & 0 deletions tests/unit/test-util/test.expand-prefs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { assert, expect } from 'chai';
import { describe, it } from 'mocha';

import expandPrefs from '../../../src/util/expand-prefs.js';

describe('utils/expand-prefs', () => {
it('expands dot-deliminated preferences into a deep object', () => {
const input = {
a: 'a',
'b.c': 'c',
'd.e.f': 'f',
};
const expected = {
a: 'a',
b: {
c: 'c',
},
d: {
e: {
f: 'f',
},
},
};
const actual = expandPrefs(input);

assert.deepEqual(actual, expected);
});

it("doesn't pollute the object prototype", () => {
const call = 'overriden';
const input = {
'hasOwnProperty.call': call,
};
const expected = {
hasOwnProperty: {
call,
},
};
const actual = expandPrefs(input);

assert.notEqual(Object.prototype.hasOwnProperty.call, call);
assert.deepEqual(actual, expected);
});

it('throws an error when setting the child property of an already set parent', () => {
const input = {
a: 'a',
'a.b': 'b',
};

expect(() => expandPrefs(input)).to.throw(
'Cannot set a.b because a value already exists at a',
);
});

it('allows overriding a parent even if a child has already been set', () => {
const input = {
'a.b': 'b',
a: 'a',
};
const expected = {
a: 'a',
};
const actual = expandPrefs(input);

assert.deepEqual(actual, expected);
});
});
21 changes: 21 additions & 0 deletions tests/unit/test.program.js
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,27 @@ describe('program.main', () => {
});
});

it('converts chromium custom preferences into an object', () => {
const fakeCommands = fake(commands, {
run: () => Promise.resolve(),
});
return execProgram(
[
'run',
'--chromium-pref',
'extensions.ui.developer_mode=false',
'--chromium-pref',
'browser.theme.color=dark',
],
{ commands: fakeCommands },
).then(() => {
const { chromiumPref } = fakeCommands.run.firstCall.args[0];
assert.isObject(chromiumPref);
assert.strictEqual(chromiumPref['extensions.ui.developer_mode'], false);
assert.strictEqual(chromiumPref['browser.theme.color'], 'dark');
});
});

it('passes shouldExitProgram option to commands', () => {
const fakeCommands = fake(commands, {
lint: () => Promise.resolve(),
Expand Down