mirror of
https://github.com/Anarios/return-youtube-dislike.git
synced 2026-10-27 23:31:06 +01:00
Keep Firefox permission prompts outside the toolbar popup
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"])(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user