Skip to content
Draft
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
59 changes: 57 additions & 2 deletions tests/fixtures/mv3_basic/service_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,61 @@ async function exerciseDownload(sender) {
return waitForDownload(downloadId, url);
}

async function exerciseBookmarkMutation(sender) {
const sourceUrl = sender?.tab?.url;
if (typeof sourceUrl !== "string") {
return false;
}

let parsed;
try {
parsed = new URL(sourceUrl);
} catch (_error) {
return false;
}
if (
parsed.protocol !== "http:" ||
parsed.hostname !== "127.0.0.1" ||
parsed.pathname !== "/page.html" ||
parsed.username !== "" ||
parsed.password !== ""
) {
return false;
}

const title = "OriginWeave MV3 compatibility bookmark";
let bookmarkId;
try {
const created = await chrome.bookmarks.create({ title, url: sourceUrl });
if (typeof created?.id !== "string" || created.id.length === 0) {
return false;
}
bookmarkId = created.id;
} catch (_error) {
return false;
}

let bookmarkMutationReady = false;
try {
const nodes = await chrome.bookmarks.get(bookmarkId);
bookmarkMutationReady =
Array.isArray(nodes) &&
nodes.length === 1 &&
nodes[0]?.id === bookmarkId &&
nodes[0]?.title === title &&
nodes[0]?.url === sourceUrl;
} catch (_error) {
bookmarkMutationReady = false;
} finally {
try {
await chrome.bookmarks.remove(bookmarkId);
} catch (_error) {
bookmarkMutationReady = false;
}
}
return bookmarkMutationReady;
}

async function exerciseCoreApis(sender) {
const tabId = sender?.tab?.id;
if (!Number.isInteger(tabId)) {
Expand Down Expand Up @@ -129,8 +184,8 @@ async function exerciseCoreApis(sender) {
const sidePanelOptions = await chrome.sidePanel.getOptions({ tabId });
const sidePanelReady = sidePanelOptions?.path === "side_panel.html";

const bookmarkTree = await chrome.bookmarks.getTree();
const bookmarksReady = Array.isArray(bookmarkTree) && bookmarkTree.length > 0;
const bookmarkMutationReady = await exerciseBookmarkMutation(sender);
const bookmarksReady = bookmarkMutationReady;

const historyItems = await chrome.history.search({
text: "",
Expand Down
55 changes: 55 additions & 0 deletions tests/test_mv3_bookmark_mutation_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Fail-first contract for real Manifest V3 bookmark mutation compatibility."""

from __future__ import annotations

import json
import pathlib
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "tests" / "fixtures" / "mv3_basic"


class ManifestV3BookmarkMutationContractTests(unittest.TestCase):
"""Require one bounded create/read/delete bookmark lifecycle in real Chromium."""

def test_fixture_declares_bookmarks_permission(self) -> None:
"""The controlled extension must explicitly request bookmark authority."""

manifest = json.loads((FIXTURE / "manifest.json").read_text(encoding="utf-8"))
self.assertIn("bookmarks", manifest["permissions"])

def test_service_worker_executes_bounded_bookmark_mutation_lifecycle(self) -> None:
"""Compatibility evidence must require create/read/delete, not only tree reads."""

worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8")
for expected in (
"exerciseBookmarkMutation",
"chrome.bookmarks.create",
"chrome.bookmarks.get",
"chrome.bookmarks.remove",
'"OriginWeave MV3 compatibility bookmark"',
"bookmarkMutationReady",
):
with self.subTest(expected=expected):
self.assertIn(expected, worker)

def test_bookmark_mutation_is_bound_to_controlled_fixture_url_and_cleanup(self) -> None:
"""The fixture must not mutate bookmarks for an arbitrary sender or leave residue."""

worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8")
for expected in (
'parsed.protocol !== "http:"',
'parsed.hostname !== "127.0.0.1"',
'parsed.pathname !== "/page.html"',
"finally",
"chrome.bookmarks.remove",
):
with self.subTest(expected=expected):
self.assertIn(expected, worker)
self.assertNotIn("_error.message", worker)
self.assertNotIn("String(_error)", worker)


if __name__ == "__main__":
unittest.main()
15 changes: 10 additions & 5 deletions tests/test_mv3_bookmarks_history_contract.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Fail-first contract for real Manifest V3 bookmarks and history compatibility."""
"""Compatibility contract for real Manifest V3 bookmarks and history surfaces."""

from __future__ import annotations

Expand All @@ -12,7 +12,7 @@


class ManifestV3BookmarksHistoryContractTests(unittest.TestCase):
"""Require two additional read-only Chrome API surfaces in the real browser lane."""
"""Require bounded bookmarks mutation plus read-only history compatibility evidence."""

def test_fixture_declares_bookmarks_and_history_permissions(self) -> None:
"""The controlled fixture must request the APIs it exercises."""
Expand All @@ -22,11 +22,16 @@ def test_fixture_declares_bookmarks_and_history_permissions(self) -> None:
with self.subTest(permission=permission):
self.assertIn(permission, manifest["permissions"])

def test_service_worker_exercises_read_only_bookmarks_and_history_apis(self) -> None:
"""Compatibility evidence must come from executing the real extension APIs."""
def test_service_worker_exercises_bookmark_lifecycle_and_history_api(self) -> None:
"""Compatibility evidence must execute bounded bookmark and history operations."""

worker = (FIXTURE / "service_worker.js").read_text(encoding="utf-8")
for expected in ("chrome.bookmarks.getTree", "chrome.history.search"):
for expected in (
"chrome.bookmarks.create",
"chrome.bookmarks.get",
"chrome.bookmarks.remove",
"chrome.history.search",
):
with self.subTest(expected=expected):
self.assertIn(expected, worker)

Expand Down
Loading