Make #98 easier
Added `FirefoxProfile` to simplify passing preference settings and a Firefox profile directory to Firefox.
diff --git a/lib/src/firefox_profile.dart b/lib/src/firefox_profile.dart
new file mode 100644
index 0000000..a6a8e02
--- /dev/null
+++ b/lib/src/firefox_profile.dart
@@ -0,0 +1,353 @@
+// Copyright 2015 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+library webdriver.src.firefox_profile;
+
+import 'dart:io' as io;
+import 'package:path/path.dart' as path;
+import 'dart:collection';
+import 'dart:convert' show LineSplitter;
+import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder;
+import 'package:crypto/crypto.dart' show CryptoUtils;
+
+/// Unmodifiable defaults for 'prefs.js' and 'user.js'.
+final lockedPrefs = [
+ new BooleanOption("app.update.auto", false),
+ new BooleanOption("app.update.enabled", false),
+ new IntegerOption("browser.displayedE10SNotice", 4),
+ new BooleanOption("browser.download.manager.showWhenStarting", false),
+ new BooleanOption("browser.EULA.override", true),
+ new BooleanOption("browser.EULA.3.accepted", true),
+ new IntegerOption("browser.link.open_external", 2),
+ new IntegerOption("browser.link.open_newwindow", 2),
+ new BooleanOption("browser.offline", false),
+ new BooleanOption("browser.reader.detectedFirstArticle", true),
+ new BooleanOption("browser.safebrowsing.enabled", false),
+ new BooleanOption("browser.safebrowsing.malware.enabled", false),
+ new BooleanOption("browser.search.update", false),
+ new StringOption("browser.selfsupport.url", ""),
+ new BooleanOption("browser.sessionstore.resume_from_crash", false),
+ new BooleanOption("browser.shell.checkDefaultBrowser", false),
+ new BooleanOption("browser.tabs.warnOnClose", false),
+ new BooleanOption("browser.tabs.warnOnOpen", false),
+ new BooleanOption("datareporting.healthreport.service.enabled", false),
+ new BooleanOption("datareporting.healthreport.uploadEnabled", false),
+ new BooleanOption("datareporting.healthreport.service.firstRun", false),
+ new BooleanOption("datareporting.healthreport.logging.consoleEnabled", false),
+ new BooleanOption("datareporting.policy.dataSubmissionEnabled", false),
+ new BooleanOption("datareporting.policy.dataSubmissionPolicyAccepted", false),
+ new BooleanOption("devtools.errorconsole.enabled", true),
+ new BooleanOption("dom.disable_open_during_load", false),
+ new IntegerOption("extensions.autoDisableScopes", 10),
+ new BooleanOption("extensions.blocklist.enabled", false),
+ new BooleanOption("extensions.logging.enabled", true),
+ new BooleanOption("extensions.update.enabled", false),
+ new BooleanOption("extensions.update.notifyUser", false),
+ new BooleanOption("javascript.enabled", true),
+ new BooleanOption("network.manage-offline-status", false),
+ new IntegerOption("network.http.phishy-userpass-length", 255),
+ new BooleanOption("offline-apps.allow_by_default", true),
+ new BooleanOption("prompts.tab_modal.enabled", false),
+ new BooleanOption("security.csp.enable", false),
+ new IntegerOption("security.fileuri.origin_policy", 3),
+ new BooleanOption("security.fileuri.strict_origin_policy", false),
+ new BooleanOption("security.warn_entering_secure", false),
+ new BooleanOption("security.warn_entering_secure.show_once", false),
+ new BooleanOption("security.warn_entering_weak", false),
+ new BooleanOption("security.warn_entering_weak.show_once", false),
+ new BooleanOption("security.warn_leaving_secure", false),
+ new BooleanOption("security.warn_leaving_secure.show_once", false),
+ new BooleanOption("security.warn_submit_insecure", false),
+ new BooleanOption("security.warn_viewing_mixed", false),
+ new BooleanOption("security.warn_viewing_mixed.show_once", false),
+ new BooleanOption("signon.rememberSignons", false),
+ new BooleanOption("toolkit.networkmanager.disable", true),
+ new IntegerOption("toolkit.telemetry.prompted", 2),
+ new BooleanOption("toolkit.telemetry.enabled", false),
+ new BooleanOption("toolkit.telemetry.rejected", true),
+ new BooleanOption("xpinstall.signatures.required", false),
+];
+
+/// Default values for 'user.js'.
+final defaultUserPrefs = [
+ new BooleanOption("browser.dom.window.dump.enabled", true),
+ new StringOption("browser.newtab.url", "about:blank"),
+ new BooleanOption("browser.newtabpage.enabled", false),
+ new IntegerOption("browser.startup.page", 0),
+ new StringOption("browser.startup.homepage", "about:blank"),
+ new IntegerOption("dom.max_chrome_script_run_time", 30),
+ new IntegerOption("dom.max_script_run_time", 30),
+ new BooleanOption("dom.report_all_js_exceptions", true),
+ new BooleanOption("javascript.options.showInConsole", true),
+ new IntegerOption("network.http.max-connections-per-server", 10),
+ new StringOption("startup.homepage_welcome_url", "about:blank"),
+ new BooleanOption("webdriver_accept_untrusted_certs", true),
+ new BooleanOption("webdriver_assume_untrusted_issuer", true),
+];
+
+/// Creates a Firefox profile in a format so it can be passed using the
+/// `desired` capabilities map.
+class FirefoxProfile {
+ final io.Directory profileDirectory;
+
+ Set<PrefsOption> _prefs = new Set<PrefsOption>();
+
+ /// The read-only settings of the `prefs.js` file of the profile directory.
+ List<PrefsOption> get prefs => new UnmodifiableListView<PrefsOption>(_prefs);
+
+ /// The settings of the `user.js` file of the profile directory overridden by
+ /// the settings in [lockedPrefs].
+ /// [setOption] and [removeOption] allow to update, add, and remove settings
+ /// except these included in [lockedPrefs].
+ Set<PrefsOption> _userPrefs = new Set<PrefsOption>();
+ List<PrefsOption> get userPrefs =>
+ new UnmodifiableListView<PrefsOption>(_userPrefs);
+
+ FirefoxProfile({this.profileDirectory}) {
+ _userPrefs.addAll(defaultUserPrefs);
+ if (profileDirectory != null) {
+ final prefsFile =
+ new io.File(path.join(profileDirectory.absolute.path, 'prefs.js'));
+ if (prefsFile.existsSync()) {
+ _prefs = loadPrefsFile(prefsFile);
+ }
+
+ final userPrefsFile =
+ new io.File(path.join(profileDirectory.absolute.path, 'user.js'));
+ if (userPrefsFile.existsSync()) {
+ _userPrefs = loadPrefsFile(userPrefsFile);
+ }
+ }
+ _prefs.addAll(lockedPrefs);
+ }
+
+ /// Add an option or replace an option if one already exists with the same
+ /// name.
+ /// If option exists in [lockedPrefs] it will not be added.
+ /// Returns `true` if [userPrefs] was updated, `false` otherwise.
+ bool setOption(PrefsOption option) {
+ if (lockedPrefs.contains(option)) {
+ print('Option "${option.name}" (${option.value}) is locked and therefore '
+ 'ignored');
+ return false;
+ }
+ _userPrefs.add(option);
+ return true;
+ }
+
+ /// Remove the option named [name] from [userPrefs].
+ /// If [lockedPrefs] contains this option it will not be removed.
+ /// Returns `true` if [userPrefs] was removed, `false` if it was not removed
+ /// because [userPrefs] doesn't contain it because [lockedPrefs]
+ /// contains it.
+ bool removeOption(String name) {
+ final option = _userPrefs.firstWhere((o) => o.name == name,
+ orElse: () => new InvalidOption(name));
+ if (option is InvalidOption) {
+ return false;
+ }
+ return _userPrefs.remove(option);
+ }
+
+ /// Helper for [loadPrefsFile]
+ static bool _ignoreLine(String line) {
+ line ??= '';
+ line = line.trim();
+ if (line.isEmpty ||
+ line.startsWith('//') ||
+ line.startsWith('#') ||
+ line.startsWith('/*') ||
+ line.startsWith('*') ||
+ line.startsWith('*/')) {
+ return true;
+ }
+ return false;
+ }
+
+ /// Load a prefs file and parse the content into a set of [PrefOption].
+ /// For lines which can't be properly parsed a message is printed and the line
+ /// is otherwise ignored.
+ /// Comments, lines starting with `//` are silently ignored.
+ static Set<PrefsOption> loadPrefsFile(io.File file) {
+ final prefs = new Set<PrefsOption>();
+ final lines = LineSplitter
+ .split(file.readAsStringSync())
+ .where((line) => !_ignoreLine(line));
+ bool canNotParseCaption = true;
+
+ for (final line in lines) {
+ final option = new PrefsOption.parse(line);
+ if (option is InvalidOption) {
+ if (canNotParseCaption) {
+ print('Can\'t parse lines from file "${file.path}":');
+ canNotParseCaption = false;
+ }
+ print(' ${line}');
+ continue;
+ }
+ prefs.add(option);
+ }
+ return prefs;
+ }
+
+ /// Creates a map like `{'browserName: 'firefox', 'firefox_profile': 'xxxxx'}`
+ /// where `xxxxx` is the zipped and base64 encoded content of the files in
+ /// [profileDirectory] if one was pased.
+ /// The files `prefs.js` and `user.js` are generated from the content of
+ /// `prefs` and `userPrefs`.
+ /// It can be uses like
+ /// `var desired = Capabilities.firefox..addAll(firefoxProfile.toJson()}`
+ Map toJson() {
+ Archive archive = new Archive();
+ if (profileDirectory != null) {
+ profileDirectory.listSync(recursive: true).forEach((f) {
+ ArchiveFile archiveFile;
+ final name = path.relative(f.path, from: profileDirectory.path);
+ if (f is io.Directory) {
+ archiveFile = new ArchiveFile('${name}/', 0, []);
+ } else if (f is io.File) {
+ if (name == 'prefs.js' || name == 'user.js') {
+ return;
+ }
+ archiveFile = new ArchiveFile(
+ name, f.statSync().size, (f as io.File).readAsBytesSync());
+ } else {
+ throw 'Invalid file type for file "${f.path}" (${io.FileSystemEntity.typeSync(f.path)}).';
+ }
+ archive.addFile(archiveFile);
+ });
+ }
+ final prefsJsContent =
+ prefs.map((option) => option.asPrefString).join('\n').codeUnits;
+ archive.addFile(
+ new ArchiveFile('prefs.js', prefsJsContent.length, prefsJsContent));
+
+ final userJsContent =
+ userPrefs.map((option) => option.asPrefString).join('\n').codeUnits;
+ archive.addFile(
+ new ArchiveFile('user.js', userJsContent.length, userJsContent));
+
+ final zipData = new ZipEncoder().encode(archive);
+ return {'firefox_profile': CryptoUtils.bytesToBase64(zipData)};
+ }
+}
+
+abstract class PrefsOption<T> {
+ /// This pattern is used to parse preferences in user.js. It is intended to
+ /// match all preference lines in the format generated by Firefox; it won't
+ /// necessarily match all possible lines that Firefox will parse.
+ ///
+ /// e.g. if you have a line with extra spaces after the end-of-line semicolon,
+ /// this pattern will not match that line because Firefox never generates
+ /// lines like that.
+ static final RegExp _preferencePattern =
+ new RegExp(r'user_pref\("([^"]+)", ("?.+?"?)\);');
+
+ final String name;
+ T _value;
+ dynamic get value => _value;
+
+ factory PrefsOption(String name, value) {
+ assert(value is bool || value is int || value is String);
+ if (value is bool) {
+ return new BooleanOption(name, value) as PrefsOption<T>;
+ } else if (value is int) {
+ return new IntegerOption(name, value) as PrefsOption<T>;
+ } else if (value is String) {
+ return new StringOption(name, value) as PrefsOption<T>;
+ }
+ }
+
+ factory PrefsOption.parse(String prefs) {
+ final match = _preferencePattern.firstMatch(prefs);
+ if (match == null) {
+ return new InvalidOption('Not a valid prefs option: "${prefs}".')
+ as PrefsOption<T>;
+ }
+ final name = match.group(1);
+ final valueString = match.group(2);
+ if (valueString.startsWith('"') && valueString.endsWith('"')) {
+ final value = valueString
+ .substring(1, valueString.length - 1)
+ .replaceAll(r'\"', r'"')
+ .replaceAll(r'\\', r'\');
+ return new StringOption(name, value) as PrefsOption<T>;
+ }
+ if (valueString.toLowerCase() == 'true') {
+ return new BooleanOption(name, true) as PrefsOption<T>;
+ } else if (valueString.toLowerCase() == 'false') {
+ return new BooleanOption(name, false) as PrefsOption<T>;
+ }
+ try {
+ int value = int.parse(valueString);
+ return new IntegerOption(name, value) as PrefsOption<T>;
+ } catch (_) {}
+ return new InvalidOption('Not a valid prefs option: "${prefs}".')
+ as PrefsOption<T>;
+ }
+
+ PrefsOption._(this.name, [this._value]) {
+ assert(name.isNotEmpty);
+ }
+
+ bool operator ==(other) {
+ if (other is! PrefsOption) return false;
+ return other.name == name;
+ }
+
+ @override
+ int get hashCode => name.hashCode;
+
+ String get asPrefString => 'user_pref("${name}", ${_valueAsPrefString});';
+
+ String get _valueAsPrefString;
+}
+
+/// Used as a placeholder for unparsable lines.
+class InvalidOption extends PrefsOption<String> {
+ InvalidOption(String value) : super._('invalid', value);
+
+ @override
+ String get _valueAsPrefString =>
+ throw 'An invalid option can\'t be serialized.';
+}
+
+/// A boolean preferences option with name and value.
+class BooleanOption extends PrefsOption<bool> {
+ BooleanOption(String name, bool value) : super._(name, value);
+
+ @override
+ String get _valueAsPrefString => value.toString();
+}
+
+/// An integer preferences option with name and value.
+class IntegerOption extends PrefsOption<int> {
+ IntegerOption(String name, int value) : super._(name, value);
+
+ @override
+ String get _valueAsPrefString => value.toString();
+}
+
+/// A String preferences option with name and value.
+/// [_valueAsPrefString] escapes `"` as `\"` and `\` as `\\`.
+class StringOption extends PrefsOption<String> {
+ StringOption(String name, String value) : super._(name, value);
+
+ String _escape(String value) =>
+ value.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
+ @override
+ String get _valueAsPrefString {
+ return '"${_escape(value)}"';
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 1778d14..eba34ac 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -8,6 +8,7 @@
environment:
sdk: '>=1.10.0 <2.0.0'
dependencies:
+ archive: '^1.0.20'
matcher: '^0.12.0+1'
path: '^1.3.6'
stack_trace: '^1.3.4'
diff --git a/test/io_test.dart b/test/io_test.dart
index 2bd630f..3f24ad4 100644
--- a/test/io_test.dart
+++ b/test/io_test.dart
@@ -19,6 +19,7 @@
import 'src/alert.dart' as alert;
import 'src/command_event.dart' as command_event;
+import 'src/firefox_profile.dart' as firefox_profile;
import 'src/keyboard.dart' as keyboard;
import 'src/logs.dart' as logs;
import 'src/mouse.dart' as mouse;
@@ -36,6 +37,7 @@
alert.runTests();
command_event.runTests();
+ firefox_profile.runTests();
keyboard.runTests();
logs.runTests();
mouse.runTests();
diff --git a/test/src/firefox_profile.dart b/test/src/firefox_profile.dart
new file mode 100644
index 0000000..c509f79
--- /dev/null
+++ b/test/src/firefox_profile.dart
@@ -0,0 +1,188 @@
+// Copyright 2015 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+@TestOn("vm")
+library webdriver.firefox_profile_test;
+
+import 'dart:io' as io;
+import 'package:test/test.dart';
+import 'package:webdriver/core.dart';
+
+import 'package:webdriver/src/firefox_profile.dart';
+import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipDecoder;
+import 'package:crypto/crypto.dart' show CryptoUtils;
+
+import 'dart:convert' show Encoding, UTF8;
+
+void runTests() {
+ group('Firefox profile', () {
+ test('parse and serialize string value with quotes', () {
+ const value =
+ r'user_pref("extensions.xpiState", "{\"app-global\":{\"{972ce4c6-7e08-4474-a285-3208198ce6fd}\":{\"d\":\"/opt/firefox/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}\",\"e\":true,\"v\":\"40.0\",\"st\":1439535413000,\"mt\":1438968709000}}}");';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<StringOption>());
+ expect(option.asPrefString, value);
+ });
+
+ test('parse and serialize string value with backslash', () {
+ const value =
+ r'user_pref("browser.cache.disk.parent_directory", "\\\\volume\\web\\cache\\mz");';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<StringOption>());
+ expect(option.asPrefString, value);
+ });
+
+ test('parse and serialize integer value', () {
+ const value = r'user_pref("browser.cache.frecency_experiment", 3);';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<IntegerOption>());
+ expect(option.asPrefString, value);
+ });
+
+ test('parse and serialize negative integer value', () {
+ const value = r'user_pref("browser.cache.frecency_experiment", -3);';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<IntegerOption>());
+ expect(option.asPrefString, value);
+ });
+
+ test('parse and serialize boolean true', () {
+ const value =
+ r'user_pref("browser.cache.disk.smart_size.first_run", true);';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<BooleanOption>());
+ expect(option.asPrefString, value);
+ });
+
+ test('parse and serialize boolean false', () {
+ const value =
+ r'user_pref("browser.cache.disk.smart_size.first_run", false);';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<BooleanOption>());
+ expect(option.asPrefString, value);
+ });
+
+ test('parse boolean uppercase True', () {
+ const value =
+ r'user_pref("browser.cache.disk.smart_size.first_run", True);';
+ var option = new PrefsOption.parse(value);
+ expect(option, new isInstanceOf<BooleanOption>());
+ expect(option.value, true);
+ });
+
+ test('added value should be in prefs', () {
+ var profile = new FirefoxProfile();
+ var option =
+ new PrefsOption('browser.bookmarks.restore_default_bookmarks', false);
+
+ expect(profile.setOption(option), true);
+
+ expect(profile.userPrefs,
+ anyElement((e) => e.name == option.name && e.value == option.value));
+ });
+
+ test('overriding locked value should be ignored', () {
+ var profile = new FirefoxProfile();
+ var lockedOption = new PrefsOption('javascript.enabled', false);
+ var lockedOptionOrig =
+ profile.prefs.firstWhere((e) => e.name == lockedOption.name);
+ expect(lockedOption.value, isNot(lockedOptionOrig.value));
+
+ expect(profile.setOption(lockedOption), false);
+
+ expect(profile.userPrefs, isNot(anyElement(lockedOption)));
+ expect(profile.prefs.firstWhere((e) => e.name == lockedOption.name).value,
+ lockedOptionOrig.value);
+ });
+
+ test('removing locked value should be ignored', () {
+ var profile = new FirefoxProfile();
+ var lockedOption = new PrefsOption('javascript.enabled', false);
+ var lockedOptionOrig =
+ profile.prefs.firstWhere((e) => e.name == lockedOption.name);
+ expect(lockedOption.value, isNot(lockedOptionOrig.value));
+
+ expect(profile.removeOption(lockedOption.name), false);
+
+ expect(profile.userPrefs, isNot(anyElement(lockedOption)));
+ expect(profile.prefs.firstWhere((e) => e.name == lockedOption.name).value,
+ lockedOptionOrig.value);
+ });
+
+ test('encode/decode "user.js" in-memory', () {
+ var profile = new FirefoxProfile();
+ profile.setOption(new PrefsOption(Capabilities.hasNativeEvents, true));
+
+ var archive = unpackArchiveData(profile.toJson());
+
+ var expectedFiles = ['prefs.js', 'user.js'];
+ expect(archive.files.length, greaterThanOrEqualTo(expectedFiles.length));
+ expectedFiles.forEach((f) => expect(
+ archive.files, anyElement((ArchiveFile f) => f.name == 'prefs.js')));
+
+ var prefs = FirefoxProfile.loadPrefsFile(new MockFile(
+ new String.fromCharCodes(
+ archive.files.firstWhere((f) => f.name == 'user.js').content)));
+ expect(
+ prefs,
+ anyElement((PrefsOption o) =>
+ o.name == Capabilities.hasNativeEvents && o.value == true));
+ });
+
+ test('encode/decode profile directory from disk', () {
+ var profile = new FirefoxProfile(
+ profileDirectory: new io.Directory('test/src/firefox_profile'));
+ profile.setOption(new PrefsOption(Capabilities.hasNativeEvents, true));
+
+ var archive = unpackArchiveData(profile.toJson());
+
+ var expectedFiles = [
+ 'prefs.js',
+ 'user.js',
+ 'addons.js',
+ 'webapps/',
+ 'webapps/webapps.json'
+ ];
+ expect(archive.files.length, greaterThanOrEqualTo(expectedFiles.length));
+ expectedFiles.forEach((f) => expect(
+ archive.files, anyElement((ArchiveFile f) => f.name == 'prefs.js')));
+
+ var prefs = FirefoxProfile.loadPrefsFile(new MockFile(
+ new String.fromCharCodes(
+ archive.files.firstWhere((f) => f.name == 'user.js').content)));
+ expect(
+ prefs,
+ anyElement((PrefsOption o) =>
+ o.name == Capabilities.hasNativeEvents && o.value == true));
+ });
+ });
+}
+
+Archive unpackArchiveData(Map profileData) {
+ var zipArchive =
+ CryptoUtils.base64StringToBytes(profileData['firefox_profile']);
+ return new ZipDecoder().decodeBytes(zipArchive, verify: true);
+}
+
+/// Simulate file for `FirefoxProfile.loadPrefsFile()`
+class MockFile implements io.File {
+ String content;
+
+ MockFile(this.content);
+
+ @override
+ String readAsStringSync({Encoding encoding: UTF8}) => content;
+
+ noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
+}
diff --git a/test/src/firefox_profile/addons.json b/test/src/firefox_profile/addons.json
new file mode 100644
index 0000000..a06d029
--- /dev/null
+++ b/test/src/firefox_profile/addons.json
@@ -0,0 +1 @@
+{"schema":5,"addons":[]}
\ No newline at end of file
diff --git a/test/src/firefox_profile/prefs.js b/test/src/firefox_profile/prefs.js
new file mode 100644
index 0000000..45728f0
--- /dev/null
+++ b/test/src/firefox_profile/prefs.js
@@ -0,0 +1,90 @@
+# Mozilla User Preferences
+
+/* Do not edit this file.
+ *
+ * If you make changes to this file while the application is running,
+ * the changes will be overwritten when the application exits.
+ *
+ * To make a manual change to preferences, you can visit the URL about:config
+ */
+
+user_pref("app.update.lastUpdateTime.addon-background-update-timer", 1441289625);
+user_pref("app.update.lastUpdateTime.background-update-timer", 1441345564);
+user_pref("app.update.lastUpdateTime.blocklist-background-update-timer", 1441289745);
+user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 1441363323);
+user_pref("app.update.lastUpdateTime.experiments-update-timer", 1441289505);
+user_pref("app.update.lastUpdateTime.search-engine-update-timer", 1441345444);
+user_pref("app.update.lastUpdateTime.xpi-signature-verification", 1441289865);
+user_pref("browser.bookmarks.restore_default_bookmarks", false);
+user_pref("browser.cache.disk.capacity", 358400);
+user_pref("browser.cache.disk.filesystem_reported", 1);
+user_pref("browser.cache.disk.smart_size.first_run", false);
+user_pref("browser.cache.frecency_experiment", 3);
+user_pref("browser.download.importedFromSqlite", true);
+user_pref("browser.migration.version", 30);
+user_pref("browser.newtabpage.enhanced", true);
+user_pref("browser.newtabpage.introShown", true);
+user_pref("browser.newtabpage.storageVersion", 1);
+user_pref("browser.pagethumbnails.storage_version", 3);
+user_pref("browser.places.smartBookmarksVersion", 7);
+user_pref("browser.search.countryCode", "AT");
+user_pref("browser.search.region", "AT");
+user_pref("browser.sessionstore.upgradeBackup.latestBuildID", "20150807085045");
+user_pref("browser.shell.checkDefaultBrowser", false);
+user_pref("browser.slowStartup.averageTime", 10338);
+user_pref("browser.slowStartup.samples", 3);
+user_pref("browser.toolbarbuttons.introduced.pocket-button", true);
+user_pref("datareporting.healthreport.lastDataSubmissionRequestedTime", "1441289087693");
+user_pref("datareporting.healthreport.lastDataSubmissionSuccessfulTime", "1441289089977");
+user_pref("datareporting.healthreport.nextDataSubmissionTime", "1441375489977");
+user_pref("datareporting.healthreport.service.firstRun", true);
+user_pref("datareporting.policy.dataSubmissionPolicyAcceptedVersion", 2);
+user_pref("datareporting.policy.dataSubmissionPolicyNotifiedTime", "1441188290924");
+user_pref("datareporting.policy.firstRunTime", "1441187972796");
+user_pref("datareporting.sessions.current.activeTicks", 29);
+user_pref("datareporting.sessions.current.clean", true);
+user_pref("datareporting.sessions.current.firstPaint", 5114);
+user_pref("datareporting.sessions.current.main", 226);
+user_pref("datareporting.sessions.current.sessionRestored", 4446);
+user_pref("datareporting.sessions.current.startTime", "1441289023049");
+user_pref("datareporting.sessions.current.totalTime", 75887);
+user_pref("datareporting.sessions.currentIndex", 2);
+user_pref("datareporting.sessions.prunedIndex", 1);
+user_pref("dom.apps.reset-permissions", true);
+user_pref("dom.ipc.plugins.asyncInit", false);
+user_pref("dom.mozApps.used", true);
+user_pref("experiments.activeExperiment", false);
+user_pref("extensions.blocklist.pingCountTotal", 3);
+user_pref("extensions.blocklist.pingCountVersion", 3);
+user_pref("extensions.bootstrappedAddons", "{}");
+user_pref("extensions.databaseSchema", 17);
+user_pref("extensions.enabledAddons", "%7B972ce4c6-7e08-4474-a285-3208198ce6fd%7D:40.0");
+user_pref("extensions.getAddons.cache.lastUpdate", 1441289626);
+user_pref("extensions.getAddons.databaseSchema", 5);
+user_pref("extensions.hotfix.lastVersion", "20150818.01");
+user_pref("extensions.lastAppVersion", "40.0");
+user_pref("extensions.lastPlatformVersion", "40.0");
+user_pref("extensions.pendingOperations", false);
+user_pref("extensions.shownSelectionUI", true);
+user_pref("extensions.xpiState", "{\"app-global\":{\"{972ce4c6-7e08-4474-a285-3208198ce6fd}\":{\"d\":\"/opt/firefox/browser/extensions/{972ce4c6-7e08-4474-a285-3208198ce6fd}\",\"e\":true,\"v\":\"40.0\",\"st\":1439535413000,\"mt\":1438968709000}}}");
+user_pref("gecko.buildID", "20150807085045");
+user_pref("gecko.mstone", "40.0");
+user_pref("idle.lastDailyNotification", 1441294099);
+user_pref("media.gmp-gmpopenh264.lastUpdate", 1441188292);
+user_pref("media.gmp-gmpopenh264.version", "1.4");
+user_pref("media.gmp-manager.buildID", "20150807085045");
+user_pref("media.gmp-manager.lastCheck", 1441289088);
+user_pref("network.cookie.prefsMigrated", true);
+user_pref("network.predictor.cleaned-up", true);
+user_pref("pdfjs.migrationVersion", 2);
+user_pref("pdfjs.previousHandler.alwaysAskBeforeHandling", true);
+user_pref("pdfjs.previousHandler.preferredAction", 4);
+user_pref("places.database.lastMaintenance", 1441294099);
+user_pref("places.history.expiration.transient_current_max_pages", 104858);
+user_pref("plugin.disable_full_page_plugin_for_types", "application/pdf");
+user_pref("plugin.importedState", true);
+user_pref("privacy.sanitize.migrateFx3Prefs", true);
+user_pref("signon.importedFromSqlite", true);
+user_pref("storage.vacuum.last.index", 0);
+user_pref("storage.vacuum.last.places.sqlite", 1441294099);
+user_pref("toolkit.startup.last_success", 1441289023);
diff --git a/test/src/firefox_profile/webapps/webapps.json b/test/src/firefox_profile/webapps/webapps.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/test/src/firefox_profile/webapps/webapps.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file