Keep Firefox permission prompts outside the toolbar popup

This commit is contained in:
Anarios
2026-10-18 20:28:02 +02:00
parent c31f60cc24
commit e904b06ae0
8 changed files with 364 additions and 48 deletions
+6
View File
@@ -92,6 +92,12 @@ describe("popup color settings", () => {
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
test("uses the default number format before first-run storage is initialized", () => {
expect(() => openPopup({ numberDisplayFormat: undefined })).not.toThrow();
expect(document.getElementById("number_format").value).toBe("compactShort");
expect(chrome.storage.sync.set).not.toHaveBeenCalled();
});
test("uses a keyboard-compatible change event for preset selection and previews both themes", () => {
openPopup();
chooseTheme("hibiscus");
+66 -15
View File
@@ -60,7 +60,28 @@ function localizeHtmlPage() {
localizeHtmlPage();
const privateBrowsing = isPrivateBrowsingContext();
const coreConsentReady = privateBrowsing ? null : waitForCoreDataConsent({ alwaysExplain: true });
function isToolbarPopup() {
try {
return browser.extension?.getViews?.({ type: "popup" }).includes(window) === true;
} catch (_) {
return false;
}
}
function waitForPopupCoreConsent() {
const toolbarPopup = isToolbarPopup();
return waitForCoreDataConsent({
alwaysExplain: !toolbarPopup,
openConsentPage: toolbarPopup
? async () => {
await browser.tabs.create({ url: browser.runtime.getURL("consent.html"), active: true });
window.close();
}
: undefined,
});
}
const coreConsentReady = privateBrowsing ? null : waitForPopupCoreConsent();
if (privateBrowsing) {
const home = document.createElement("center");
const message = document.createElement("p");
@@ -76,7 +97,7 @@ if (privateBrowsing) {
} else {
coreConsentReady.then(() => initDiscussionPopup());
onCoreDataPermissionChanged((granted) => {
if (!granted) void waitForCoreDataConsent({ alwaysExplain: true });
if (!granted) void waitForPopupCoreConsent();
});
/* Links */
@@ -174,6 +195,7 @@ function initPatreonAuth() {
const userName = document.getElementById("patreon-user-name");
const userTier = document.getElementById("patreon-tier");
let authenticationGeneration = 0;
let openingPermissionPage = false;
const initialGeneration = authenticationGeneration;
chrome.storage.sync.get(["patreonUser", "patreonSessionToken"], async (data) => {
@@ -234,6 +256,8 @@ function initPatreonAuth() {
userAvatar.style.display = "none";
userName.textContent = "";
userTier.textContent = "";
if (window.location.hash === "#patreon-login-btn") loginBtn.focus();
if (window.location.hash === "#github-login-btn") githubLoginBtn?.focus();
}
function showLoggedInView(user) {
@@ -402,7 +426,43 @@ function initPatreonAuth() {
}
}
function requestLoginConsent(buttonId, generation, continueLogin) {
if (!usesFirefoxDataCollectionConsent()) {
continueLogin(true);
return;
}
if (!isToolbarPopup()) {
requestAuthenticationDataPermission().then(continueLogin);
return;
}
// Keep the native permission prompt and its continuation outside the toolbar popup.
openingPermissionPage = true;
void hasAuthenticationDataPermission()
.then(async (granted) => {
if (generation !== authenticationGeneration) return;
if (granted) {
continueLogin(true);
return;
}
await browser.tabs.create({ url: browser.runtime.getURL("popup.html") + "#" + buttonId, active: true });
if (generation === authenticationGeneration) window.close();
})
.catch(() => {
if (generation !== authenticationGeneration) return;
alert(
chrome.i18n.getMessage(
buttonId === "github-login-btn" ? "githubLoginStartFailed" : "patreonLoginStartFailed",
),
);
})
.finally(() => {
openingPermissionPage = false;
});
}
loginBtn.addEventListener("click", () => {
if (openingPermissionPage) return;
const generation = ++authenticationGeneration;
const continueLogin = (consentGranted) => {
if (generation !== authenticationGeneration) return;
@@ -439,16 +499,12 @@ function initPatreonAuth() {
});
};
if (!usesFirefoxDataCollectionConsent()) {
continueLogin(true);
return;
}
requestAuthenticationDataPermission().then(continueLogin);
requestLoginConsent("patreon-login-btn", generation, continueLogin);
});
if (githubLoginBtn) {
githubLoginBtn.addEventListener("click", () => {
if (openingPermissionPage) return;
const generation = ++authenticationGeneration;
const continueLogin = (consentGranted) => {
if (generation !== authenticationGeneration) return;
@@ -485,12 +541,7 @@ function initPatreonAuth() {
});
};
if (!usesFirefoxDataCollectionConsent()) {
continueLogin(true);
return;
}
requestAuthenticationDataPermission().then(continueLogin);
requestLoginConsent("github-login-btn", generation, continueLogin);
});
}
@@ -731,7 +782,7 @@ function updateColorThemePreviewContent() {
}
}
function handleNumberDisplayFormatChangeEvent(value) {
function handleNumberDisplayFormatChangeEvent(value = "compactShort") {
config.numberDisplayFormat = value;
document.getElementById("number_format").querySelector('option[value="' + value + '"]').selected = true;
}
+123 -2
View File
@@ -1,7 +1,7 @@
/** @jest-environment jsdom */
const fs = require("fs");
const path = require("path");
jest.mock("./src/consent-prompt", () => ({ waitForCoreDataConsent: () => Promise.resolve() }));
jest.mock("./src/consent-prompt", () => ({ waitForCoreDataConsent: jest.fn(() => Promise.resolve()) }));
jest.mock("./src/core-data-consent", () => ({
fetchWithCoreConsent: (...args) => fetch(...args),
onCoreDataPermissionChanged: jest.fn(),
@@ -29,7 +29,13 @@ describe("popup account consent lifecycle", () => {
document.documentElement.innerHTML = fs.readFileSync(path.join(__dirname, "popup.html"), "utf8");
global.chrome = {
i18n: { getMessage: () => "" },
runtime: { getManifest: () => require("./manifest-firefox.json"), sendMessage: jest.fn() },
runtime: {
getManifest: () => require("./manifest-firefox.json"),
getURL: (path) => `moz-extension://fixture/${path}`,
sendMessage: jest.fn(),
},
extension: { getViews: jest.fn(() => []) },
tabs: { create: jest.fn().mockResolvedValue({ id: 9 }) },
permissions: {
getAll: jest.fn(() => Promise.resolve({ data_collection: consent ? ["authenticationInfo"] : [] })),
contains: jest.fn().mockResolvedValue(true),
@@ -65,12 +71,53 @@ describe("popup account consent lifecycle", () => {
});
afterEach(() => {
window.location.hash = "";
jest.restoreAllMocks();
delete global.chrome;
delete global.browser;
delete global.fetch;
});
test.each(["patreon-login-btn", "github-login-btn"])(
"focuses the selected %s after the asynchronous account view becomes visible",
async (buttonId) => {
window.location.hash = `#${buttonId}`;
const button = document.getElementById(buttonId);
const focus = jest.spyOn(button, "focus").mockImplementation(() => {
expect(document.getElementById("patreon-logged-out").style.display).toBe("block");
});
expect(document.getElementById("patreon-logged-out").style.display).toBe("none");
await readCachedSession({});
expect(focus).toHaveBeenCalledTimes(1);
},
);
test.each([true, false])("moves toolbar core consent into a tab and closes only on success=%s", async (success) => {
chrome.extension.getViews.mockReturnValue([window]);
jest.resetModules();
const waitForConsent = require("./src/consent-prompt").waitForCoreDataConsent;
require("./popup");
const options = waitForConsent.mock.calls[0][0];
expect(options.alwaysExplain).toBe(false);
expect(options.openConsentPage).toEqual(expect.any(Function));
const close = jest.spyOn(window, "close").mockImplementation(() => {});
let complete;
chrome.tabs.create.mockImplementation(
() =>
new Promise((resolve, reject) => {
complete = () => (success ? resolve({ id: 9 }) : reject(new Error("Could not open tab")));
}),
);
const opening = options.openConsentPage();
expect(chrome.tabs.create).toHaveBeenCalledWith({ url: "moz-extension://fixture/consent.html", active: true });
expect(close).not.toHaveBeenCalled();
complete();
if (success) await opening;
else await expect(opening).rejects.toThrow("Could not open tab");
expect(close).toHaveBeenCalledTimes(success ? 1 : 0);
expect(chrome.permissions.request).not.toHaveBeenCalled();
});
test.each([
["github_not_configured", "githubLoginUnavailable"],
["github_unavailable", "githubLoginUnavailable"],
@@ -140,6 +187,80 @@ describe("popup account consent lifecycle", () => {
},
);
test.each(["patreon-login-btn", "github-login-btn"])(
"moves missing %s consent out of the toolbar before requesting permission",
async (button) => {
consent = false;
chrome.extension.getViews.mockReturnValue([window]);
const close = jest.spyOn(window, "close").mockImplementation(() => {});
let opened;
chrome.tabs.create.mockImplementation(() => new Promise((resolve) => (opened = resolve)));
document.getElementById(button).click();
document.getElementById(button).click();
await flushPromises();
expect(chrome.tabs.create).toHaveBeenCalledTimes(1);
expect(chrome.tabs.create).toHaveBeenCalledWith({
url: `moz-extension://fixture/popup.html#${button}`,
active: true,
});
expect(chrome.permissions.request).not.toHaveBeenCalled();
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
expect(close).not.toHaveBeenCalled();
opened({ id: 9 });
await flushPromises();
expect(close).toHaveBeenCalledTimes(1);
},
);
test.each(["patreon", "github"])("keeps already-granted %s toolbar login in the existing flow", async (provider) => {
chrome.extension.getViews.mockReturnValue([window]);
document.getElementById(`${provider}-login-btn`).click();
await flushPromises();
expect(chrome.tabs.create).not.toHaveBeenCalled();
expect(chrome.permissions.request).not.toHaveBeenCalled();
expect(chrome.runtime.sendMessage).toHaveBeenCalledWith(
{ message: `${provider}_oauth_login` },
expect.any(Function),
);
});
test("failed consent-tab opening leaves the toolbar open and can be retried", async () => {
consent = false;
chrome.extension.getViews.mockReturnValue([window]);
chrome.tabs.create.mockRejectedValueOnce(new Error("Tab could not open"));
const close = jest.spyOn(window, "close").mockImplementation(() => {});
const alert = jest.spyOn(window, "alert").mockImplementation(() => {});
document.getElementById("github-login-btn").click();
await flushPromises();
expect(alert).toHaveBeenCalledTimes(1);
expect(close).not.toHaveBeenCalled();
document.getElementById("github-login-btn").click();
await flushPromises();
expect(chrome.tabs.create).toHaveBeenCalledTimes(2);
expect(close).toHaveBeenCalledTimes(1);
expect(chrome.permissions.request).not.toHaveBeenCalled();
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
});
test.each(["revocation", "logout"])(
"does not open a consent tab after %s during its permission query",
async (event) => {
chrome.extension.getViews.mockReturnValue([window]);
let resolvePermission;
chrome.permissions.getAll.mockImplementationOnce(() => new Promise((resolve) => (resolvePermission = resolve)));
document.getElementById("patreon-login-btn").click();
if (event === "revocation") removeCoreConsent();
else document.getElementById("patreon-logout-btn").click();
chrome.runtime.sendMessage.mockClear();
resolvePermission({ data_collection: [] });
await flushPromises();
expect(chrome.tabs.create).not.toHaveBeenCalled();
expect(chrome.permissions.request).not.toHaveBeenCalled();
expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
},
);
test("does not verify or show a cached session whose storage read completes after revocation", async () => {
revoke();
readCachedSession({ patreonUser: cachedUser, patreonSessionToken: "test-token" });
+11 -1
View File
@@ -262,7 +262,16 @@ api.runtime.onMessage.addListener((request, sender, sendResponse) => {
function handleMessage(request, sender, sendResponse) {
if (request?.message === "ryd_open_consent") {
if (sender.id !== api.runtime.id || sender.url !== api.runtime.getURL("consent.html")) {
let isYouTubeLauncher = false;
if (sender.tab?.incognito === false && sender.frameId === 0) {
try {
const { protocol, hostname } = new URL(sender.url);
isYouTubeLauncher =
(protocol === "https:" || protocol === "http:") &&
(hostname === "youtube.com" || hostname.endsWith(".youtube.com"));
} catch (_) {}
}
if (sender.id !== api.runtime.id || (sender.url !== api.runtime.getURL("consent.html") && !isYouTubeLauncher)) {
sendResponse({ success: false });
return;
}
@@ -273,6 +282,7 @@ function handleMessage(request, sender, sendResponse) {
try {
const existing = await tabs.get(consentPageTabId);
if (existing.url === api.runtime.getURL("consent.html") && existing.incognito === false) {
await tabs.reload(consentPageTabId);
await tabs.update(consentPageTabId, { active: true });
return;
}
+49 -10
View File
@@ -93,6 +93,7 @@ describe("background account consent lifecycle", () => {
tabs: {
get: jest.fn(async () => ({ id: 9, url: "moz-extension://fixture/consent.html", incognito: false })),
create: jest.fn(async () => ({ id: 9 })),
reload: jest.fn(async () => {}),
update: jest.fn(async () => ({ id: 9 })),
query: jest.fn((query, callback) =>
callback([{ id: 1, url: "https://www.youtube.com/watch?v=video123456", incognito: false }]),
@@ -331,18 +332,55 @@ describe("background account consent lifecycle", () => {
).not.toHaveBeenCalled();
});
test("only the explainer can open the consent page and repeated clicks reuse the tab", async () => {
test.each([
{ url: "moz-extension://fixture/consent.html" },
{ url: "https://www.youtube.com/watch?v=video123456", frameId: 0 },
{ url: "https://m.youtube.com/", frameId: 0 },
{ url: "http://youtube.com/", frameId: 0 },
])(
"the explainer or top-level YouTube launcher opens and refreshes the reused consent page (%j)",
async (context) => {
const response = jest.fn();
const sender = { ...normalSender, ...context };
messageListener({ message: "ryd_open_consent" }, sender, response);
messageListener({ message: "ryd_open_consent" }, sender, response);
await flushPromises();
expect(response).toHaveBeenCalledWith({ success: true });
expect(chrome.tabs.reload).not.toHaveBeenCalled();
messageListener({ message: "ryd_open_consent" }, sender, response);
await flushPromises();
expect(chrome.tabs.create).toHaveBeenCalledTimes(1);
expect(chrome.tabs.reload).toHaveBeenCalledTimes(1);
expect(chrome.tabs.reload).toHaveBeenCalledWith(9);
expect(chrome.tabs.update).toHaveBeenCalledWith(9, { active: true });
expect(fetch).not.toHaveBeenCalled();
},
);
test.each([
{ id: "other-extension" },
{ frameId: 1 },
{ frameId: undefined },
{ tab: undefined },
{ tab: { id: 2, incognito: true } },
{ tab: { id: 2 } },
{ url: "https://example.com/" },
{ url: "https://youtube.com.example.com/" },
{ url: "https://youtube.com@example.com/" },
{ url: "blob:https://www.youtube.com/test" },
{ url: "not a URL" },
])("rejects unrelated consent-page opener contexts (%j)", async (override) => {
const response = jest.fn();
const sender = { ...normalSender, url: "moz-extension://fixture/consent.html" };
messageListener({ message: "ryd_open_consent" }, { ...sender, url: "https://www.youtube.com/" }, response);
expect(response).toHaveBeenCalledWith({ success: false });
messageListener(
{ message: "ryd_open_consent" },
{ ...normalSender, url: "https://www.youtube.com/watch?v=video123456", frameId: 0, ...override },
response,
);
await flushPromises();
expect(response).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
expect(chrome.tabs.create).not.toHaveBeenCalled();
messageListener({ message: "ryd_open_consent" }, sender, response);
await flushPromises();
messageListener({ message: "ryd_open_consent" }, sender, response);
await flushPromises();
expect(chrome.tabs.create).toHaveBeenCalledTimes(1);
expect(chrome.tabs.update).toHaveBeenCalledWith(9, { active: true });
expect(chrome.tabs.update).not.toHaveBeenCalled();
expect(chrome.tabs.reload).not.toHaveBeenCalled();
expect(fetch).not.toHaveBeenCalled();
});
@@ -355,6 +393,7 @@ describe("background account consent lifecycle", () => {
await flushPromises();
expect(chrome.tabs.create).toHaveBeenCalledTimes(2);
expect(chrome.tabs.update).not.toHaveBeenCalled();
expect(chrome.tabs.reload).not.toHaveBeenCalled();
});
test.each(["patreon_oauth_login", "github_oauth_login"])(
+30 -10
View File
@@ -4,14 +4,16 @@ import { isPrivateBrowsingContext } from "./private-browsing";
let pendingConsent = null;
function waitForCoreDataConsent({ alwaysExplain = false } = {}) {
function waitForCoreDataConsent({ alwaysExplain = false, openConsentPage } = {}) {
if (!usesFirefoxDataCollectionConsent()) return Promise.resolve();
if (pendingConsent) return pendingConsent;
pendingConsent = (async () => {
if (await hasCoreDataPermission()) return;
const api = consentApi();
const privateBrowsing = isPrivateBrowsingContext();
const useLauncher = !alwaysExplain && !privateBrowsing;
let dismissed = false;
if (!isPrivateBrowsingContext()) {
if (!privateBrowsing) {
try {
dismissed = (await api.storage.local.get("rydConsentDismissed"))?.rydConsentDismissed === true;
} catch (_) {}
@@ -31,10 +33,12 @@ function waitForCoreDataConsent({ alwaysExplain = false } = {}) {
font: 600 14px system-ui; background: light-dark(#fff, #202020); color: light-dark(#151515, #fff); }
[hidden] { display: none; }
`;
const frame = document.createElement("iframe");
const frame = useLauncher ? null : document.createElement("iframe");
const url = api.runtime.getURL("consent.html");
frame.src = url;
frame.title = "Enable Return YouTube Dislike";
if (frame) {
frame.src = url;
frame.title = "Enable Return YouTube Dislike";
}
const reopen = document.createElement("button");
reopen.type = "button";
reopen.textContent = "Enable Return YouTube Dislike";
@@ -46,13 +50,29 @@ function waitForCoreDataConsent({ alwaysExplain = false } = {}) {
}
function collapse() {
host.setAttribute("collapsed", "");
frame.hidden = true;
if (frame) frame.hidden = true;
reopen.hidden = false;
}
reopen.addEventListener("click", expand);
if (dismissed && !alwaysExplain) collapse();
reopen.addEventListener(
"click",
useLauncher
? async () => {
reopen.disabled = true;
try {
if (openConsentPage) await openConsentPage();
else await api.runtime.sendMessage({ message: "ryd_open_consent" });
} catch (_) {
} finally {
reopen.disabled = false;
}
}
: expand,
);
if (useLauncher || (dismissed && !alwaysExplain)) collapse();
else expand();
shadow.append(style, frame, reopen);
shadow.append(style);
if (frame) shadow.append(frame);
shadow.append(reopen);
(document.body || document.documentElement).append(host);
let requestPending = false;
const unsubscribe = onCoreDataPermissionChanged((granted) => {
@@ -65,7 +85,7 @@ function waitForCoreDataConsent({ alwaysExplain = false } = {}) {
resolve();
}
function onMessage(event) {
if (event.source !== frame.contentWindow || event.origin !== new URL(url).origin) return;
if (!frame || event.source !== frame.contentWindow || event.origin !== new URL(url).origin) return;
if (event.data?.type === "ryd-consent-requested") requestPending = true;
if (event.data?.type === "ryd-consent-dismissed") {
requestPending = false;
+76 -10
View File
@@ -12,6 +12,7 @@ describe("consent overlay lifecycle", () => {
let core;
let changed;
let local;
let sendMessage;
let waitForConsent;
beforeEach(() => {
jest.resetModules();
@@ -19,9 +20,10 @@ describe("consent overlay lifecycle", () => {
require("./data-collection-permissions").usesFirefoxDataCollectionConsent.mockReturnValue(true);
require("./private-browsing").isPrivateBrowsingContext.mockReturnValue(false);
local = { get: jest.fn(async () => ({})), set: jest.fn(), remove: jest.fn() };
sendMessage = jest.fn().mockResolvedValue({ success: true });
core.consentApi.mockReturnValue({
storage: { local },
runtime: { getURL: () => "moz-extension://fixture/consent.html" },
runtime: { getURL: () => "moz-extension://fixture/consent.html", sendMessage },
});
core.hasCoreDataPermission.mockResolvedValue(false);
core.onCoreDataPermissionChanged.mockImplementation((listener) => {
@@ -38,7 +40,7 @@ describe("consent overlay lifecycle", () => {
await flush();
const host = document.querySelector("#ryd-consent-prompt");
const frame = host.shadowRoot.querySelector("iframe");
Object.defineProperty(frame, "contentWindow", { value: {} });
if (frame) Object.defineProperty(frame, "contentWindow", { value: {} });
return { host, frame };
}
function message(frame, type, source = frame.contentWindow) {
@@ -54,24 +56,88 @@ describe("consent overlay lifecycle", () => {
test("deduplicates prompts and does not resolve until consent is granted", async () => {
const pending = waitForConsent();
expect(waitForConsent()).toBe(pending);
const { host } = await mounted();
expect(host.hasAttribute("collapsed")).toBe(false);
const { host, frame } = await mounted();
expect(host.hasAttribute("collapsed")).toBe(true);
expect(frame).toBeNull();
expect(sendMessage).not.toHaveBeenCalled();
changed(true);
await pending;
expect(host.isConnected).toBe(false);
});
test("a dismissed prompt stays collapsed and can be reopened", async () => {
local.get.mockResolvedValue({ rydConsentDismissed: true });
test.each([false, true])(
"the launcher opens the explainer once without granting consent (dismissed=%s)",
async (dismissed) => {
local.get.mockResolvedValue({ rydConsentDismissed: dismissed });
let opened;
sendMessage.mockImplementation(() => new Promise((resolve) => (opened = resolve)));
const pending = waitForConsent();
const resolved = jest.fn();
void pending.then(resolved);
const { host } = await mounted();
expect(host.hasAttribute("collapsed")).toBe(true);
const button = host.shadowRoot.querySelector("button");
button.click();
button.click();
expect(sendMessage).toHaveBeenCalledTimes(1);
expect(sendMessage).toHaveBeenCalledWith({ message: "ryd_open_consent" });
expect(button.disabled).toBe(true);
opened({ success: true });
await flush();
expect(button.disabled).toBe(false);
expect(host.hasAttribute("collapsed")).toBe(true);
expect(host.shadowRoot.querySelector("iframe")).toBeNull();
expect(resolved).not.toHaveBeenCalled();
changed(false);
expect(host.isConnected).toBe(true);
changed(true);
await pending;
expect(host.isConnected).toBe(false);
},
);
test.each(["rejected", "failed"])("opening the explainer can be retried after a %s request", async (failure) => {
if (failure === "rejected") sendMessage.mockRejectedValueOnce(new Error("Unavailable"));
else sendMessage.mockResolvedValueOnce({ success: false });
const pending = waitForConsent();
const { host } = await mounted();
expect(host.hasAttribute("collapsed")).toBe(true);
host.shadowRoot.querySelector("button").click();
expect(host.hasAttribute("collapsed")).toBe(false);
const button = host.shadowRoot.querySelector("button");
button.click();
await flush();
expect(button.disabled).toBe(false);
expect(host.isConnected).toBe(true);
button.click();
await flush();
expect(sendMessage).toHaveBeenCalledTimes(2);
changed(true);
await pending;
});
test("uses the popup's tab opener and permits retry when opening fails", async () => {
const openConsentPage = jest.fn().mockRejectedValueOnce(new Error("Unavailable")).mockResolvedValueOnce();
const pending = waitForConsent({ openConsentPage });
const { host, frame } = await mounted();
expect(frame).toBeNull();
const button = host.shadowRoot.querySelector("button");
button.click();
await flush();
expect(button.disabled).toBe(false);
button.click();
await flush();
expect(openConsentPage).toHaveBeenCalledTimes(2);
expect(sendMessage).not.toHaveBeenCalled();
expect(host.isConnected).toBe(true);
changed(true);
await pending;
});
test("a grant during mounting removes the launcher even before a change notification", async () => {
core.hasCoreDataPermission.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
await waitForConsent();
expect(document.querySelector("#ryd-consent-prompt")).toBeNull();
expect(sendMessage).not.toHaveBeenCalled();
});
test("waits for the active iframe request to finish before unloading it", async () => {
const pending = waitForConsent({ alwaysExplain: true });
const { frame, host } = await mounted();
@@ -86,7 +152,7 @@ describe("consent overlay lifecycle", () => {
});
test("ignores another window's messages and rechecks claimed grants", async () => {
const pending = waitForConsent();
const pending = waitForConsent({ alwaysExplain: true });
const { frame, host } = await mounted();
message(frame, "ryd-consent-dismissed", {});
expect(host.hasAttribute("collapsed")).toBe(false);
@@ -81,6 +81,9 @@ async function expectTeaserPalette(page, theme, legacyTokens) {
await expect(control).toHaveCSS("color", hoverColor);
await page.mouse.move(0, 0);
await control.focus();
await page.keyboard.press("Shift+Tab");
await page.keyboard.press("Tab");
await expect(control).toBeFocused();
await expect(control).toHaveCSS("color", hoverColor);
await control.blur();
}