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