feat: add Firefox extension support
- Add Firefox as an install target with native messaging manifest support. - Generate Firefox-specific extension packages with Gecko metadata and AMO-compatible manifest transforms. - Keep tab group commands available in Firefox through dynamic tab group API helpers. - Avoid Firefox linter warnings for static tab group API references and direct eval tokens. - Add Firefox packaging and installer regression coverage. - Bump the package and extension version to 0.15.1.
This commit is contained in:
+26
-1
@@ -87,7 +87,7 @@ def test_install_help_lists_supported_browsers():
|
||||
result = CliRunner().invoke(main, ["install", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "[chrome|chromium|brave|edge|vivaldi]" in result.output
|
||||
assert "[chrome|chromium|brave|edge|vivaldi|firefox]" in result.output
|
||||
|
||||
def test_install_writes_testing_and_webstore_allowed_origins(tmp_path):
|
||||
manifests = []
|
||||
@@ -117,6 +117,31 @@ def test_install_writes_testing_and_webstore_allowed_origins(tmp_path):
|
||||
assert "Testing extension ID" in result.output
|
||||
assert "Chrome Web Store extension ID" in result.output
|
||||
|
||||
def test_install_writes_firefox_allowed_extensions(tmp_path):
|
||||
manifests = []
|
||||
|
||||
def fake_install_manifest(_browser, _host_exe, manifest):
|
||||
manifests.append(manifest)
|
||||
return [tmp_path / "com.browsercli.host.json"]
|
||||
|
||||
with patch("browser_cli.commands.install.native_host_exe", return_value=tmp_path / "browser-cli-native-host"), patch(
|
||||
"browser_cli.commands.install.write_native_host_exe"
|
||||
), patch("browser_cli.commands.install._install_manifest", side_effect=fake_install_manifest):
|
||||
result = CliRunner().invoke(main, ["install", "firefox"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert manifests == [
|
||||
{
|
||||
"name": "com.browsercli.host",
|
||||
"description": "browser-cli native messaging host",
|
||||
"path": str(tmp_path / "browser-cli-native-host"),
|
||||
"type": "stdio",
|
||||
"allowed_extensions": ["browser-cli@yiprawr.dev"],
|
||||
}
|
||||
]
|
||||
assert "about:debugging#/runtime/this-firefox" in result.output
|
||||
assert "Firefox extension ID" in result.output
|
||||
|
||||
def test_install_windows_registers_native_host(tmp_path):
|
||||
writes = []
|
||||
|
||||
|
||||
@@ -122,6 +122,22 @@ def test_tab_activation_open_and_merge_do_not_steal_audible_video_window():
|
||||
assert "skippedAudibleWindows" in tabs
|
||||
assert "const target = movableWindows.find(w => w.focused) || movableWindows[0];" in tabs
|
||||
|
||||
def test_built_extension_avoids_static_firefox_unsupported_tab_group_api_refs():
|
||||
background = (ROOT / "extension" / "background.js").read_text()
|
||||
|
||||
assert "chrome.tabGroups" not in background
|
||||
assert "chrome.tabs.group" not in background
|
||||
assert "chrome.tabs.ungroup" not in background
|
||||
assert 'chrome["tabGroups"' in background
|
||||
assert 'chrome.tabs["group"' in background
|
||||
|
||||
def test_built_extension_avoids_direct_eval_token_for_firefox_linter():
|
||||
background = (ROOT / "extension" / "background.js").read_text()
|
||||
|
||||
assert "(0, eval)(" not in background
|
||||
assert "eval(" not in background
|
||||
assert 'globalThis["eval"]' in background
|
||||
|
||||
def test_session_autosave_is_debounced_and_non_overlapping():
|
||||
# The autosave lifecycle moved out of session.ts into a dedicated
|
||||
# AutoSaveManager (autosave.ts) during the structure refactor; the shared
|
||||
|
||||
@@ -19,6 +19,9 @@ def _fake_extension(tmp_path: Path) -> Path:
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "1.2.3",
|
||||
"permissions": ["tabs", "tabGroups", "windows", "nativeMessaging"],
|
||||
"background": {"service_worker": "background.js"},
|
||||
"browser_specific_settings": {"gecko": {"id": "browser-cli@yiprawr.dev"}},
|
||||
"key": "test-key",
|
||||
}), encoding="utf-8")
|
||||
for name in ("background.js", "content-dispatch.js", "content.js"):
|
||||
@@ -47,6 +50,24 @@ def test_webstore_package_strips_manifest_key(tmp_path):
|
||||
assert "content.js" in names
|
||||
assert "icons/icon-128.png" in names
|
||||
|
||||
def test_firefox_package_strips_chromium_key_and_firefox_incompatible_permission(tmp_path):
|
||||
packager = _packager_with_fake_extension(tmp_path)
|
||||
out = packager.package_extension(firefox=True, out=tmp_path / "firefox.zip")
|
||||
|
||||
with zipfile.ZipFile(out) as zf:
|
||||
manifest = json.loads(zf.read("manifest.json"))
|
||||
|
||||
assert "key" not in manifest
|
||||
assert manifest["browser_specific_settings"]["gecko"]["id"] == "browser-cli@yiprawr.dev"
|
||||
assert "tabGroups" in manifest["permissions"]
|
||||
assert "windows" not in manifest["permissions"]
|
||||
assert "nativeMessaging" in manifest["permissions"]
|
||||
assert "service_worker" not in manifest["background"]
|
||||
assert manifest["background"]["scripts"] == ["background.js"]
|
||||
assert manifest["browser_specific_settings"]["gecko"]["strict_min_version"] == "140.0"
|
||||
assert manifest["browser_specific_settings"]["gecko_android"]["strict_min_version"] == "142.0"
|
||||
assert manifest["browser_specific_settings"]["gecko"]["data_collection_permissions"] == {"required": ["none"]}
|
||||
|
||||
def test_local_package_keeps_manifest_key(tmp_path):
|
||||
packager = _packager_with_fake_extension(tmp_path)
|
||||
out = packager.package_extension(webstore=False, out=tmp_path / "local.zip")
|
||||
|
||||
Reference in New Issue
Block a user