mirror of
https://github.com/Alvin-Zilverstand/return-youtube-dislike.git
synced 2026-09-12 09:58:20 +02:00
e500088e65
Add Firefox data consent, repair contributor sign-in, and introduce optional Hide clutter buttons. Harden video navigation, voting, and action layouts with shared browser regression coverage and reproducible release packaging.
398 lines
14 KiB
JavaScript
398 lines
14 KiB
JavaScript
const path = require("node:path");
|
|
const {
|
|
assertWorkerIdentity,
|
|
extensionServiceWorkerUrl,
|
|
normalizeExtensionPath,
|
|
readUnpackedExtensions,
|
|
reloadLiveExtension,
|
|
reloadLiveExtensionInBrowser,
|
|
selectEnabledUnpackedExtension,
|
|
waitForReloadedExtensionEnabled,
|
|
waitForExtensionWorker,
|
|
} = require("../e2e/live/live-extension-reloader");
|
|
|
|
const EXTENSION_ID = "abcdefghijklmnopabcdefghijklmnop";
|
|
const BUILD_ID = "0123456789abcdef0123456789abcdef";
|
|
const EXTENSION_PATH = "C:\\repo\\Extensions\\combined\\dist\\chrome";
|
|
|
|
function identity(overrides = {}) {
|
|
return {
|
|
backgroundScript: "ryd.background.js",
|
|
compiledBuildId: BUILD_ID,
|
|
extensionId: EXTENSION_ID,
|
|
resourceBuildId: BUILD_ID,
|
|
version: "4.0.5",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function flushPromiseJobs() {
|
|
for (let index = 0; index < 10; index += 1) await Promise.resolve();
|
|
}
|
|
|
|
describe("live unpacked-extension reload helper", () => {
|
|
test("matches the exact unpacked path with Windows separator and case normalization", () => {
|
|
const extension = selectEnabledUnpackedExtension(
|
|
[
|
|
{ enabled: true, id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", path: "C:\\other", version: "4.0.5" },
|
|
{ enabled: true, id: EXTENSION_ID, path: "c:/REPO/Extensions/combined/dist/chrome/", version: "4.0.4" },
|
|
],
|
|
EXTENSION_PATH,
|
|
{ pathImpl: path.win32, platform: "win32" },
|
|
);
|
|
|
|
expect(extension.id).toBe(EXTENSION_ID);
|
|
expect(normalizeExtensionPath(extension.path, { pathImpl: path.win32, platform: "win32" })).toBe(
|
|
normalizeExtensionPath(EXTENSION_PATH, { pathImpl: path.win32, platform: "win32" }),
|
|
);
|
|
});
|
|
|
|
test.each([
|
|
[[], /is not installed/],
|
|
[[{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH }], /installed but disabled/],
|
|
[[{ enabled: true, id: "not-an-id", path: EXTENSION_PATH }], /invalid extension ID/],
|
|
[
|
|
[
|
|
{ enabled: true, id: EXTENSION_ID, path: EXTENSION_PATH },
|
|
{ enabled: true, id: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", path: EXTENSION_PATH },
|
|
],
|
|
/more than one/,
|
|
],
|
|
])("refuses an unsafe unpacked-extension inventory", (extensions, expectedError) => {
|
|
expect(() =>
|
|
selectEnabledUnpackedExtension(extensions, EXTENSION_PATH, {
|
|
pathImpl: path.win32,
|
|
platform: "win32",
|
|
}),
|
|
).toThrow(expectedError);
|
|
});
|
|
|
|
test("can use the default-profile inventory only for exact path-to-ID identification", () => {
|
|
expect(
|
|
selectEnabledUnpackedExtension(
|
|
[{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
|
|
EXTENSION_PATH,
|
|
{ pathImpl: path.win32, platform: "win32", requireEnabled: false },
|
|
),
|
|
).toMatchObject({ enabled: false, id: EXTENSION_ID });
|
|
});
|
|
|
|
test("uses an exact extension worker URL", () => {
|
|
expect(extensionServiceWorkerUrl(EXTENSION_ID)).toBe(`chrome-extension://${EXTENSION_ID}/ryd.background.js`);
|
|
expect(() => extensionServiceWorkerUrl("not-an-id")).toThrow(/valid Chrome extension ID/);
|
|
});
|
|
|
|
test("starts a dormant worker through the ServiceWorker domain", async () => {
|
|
const worker = { url: () => extensionServiceWorkerUrl(EXTENSION_ID) };
|
|
let workers = [];
|
|
const context = { serviceWorkers: jest.fn(() => workers) };
|
|
const serviceWorkerSession = {
|
|
send: jest.fn(async (method) => {
|
|
if (method === "ServiceWorker.startWorker") workers = [worker];
|
|
}),
|
|
};
|
|
|
|
await expect(
|
|
waitForExtensionWorker(context, serviceWorkerSession, EXTENSION_ID, {
|
|
delayImpl: async () => {},
|
|
now: (() => {
|
|
let value = 0;
|
|
return () => value++;
|
|
})(),
|
|
timeoutMilliseconds: 20,
|
|
}),
|
|
).resolves.toBe(worker);
|
|
expect(serviceWorkerSession.send).toHaveBeenNthCalledWith(1, "ServiceWorker.enable");
|
|
expect(serviceWorkerSession.send).toHaveBeenNthCalledWith(2, "ServiceWorker.startWorker", {
|
|
scopeURL: `chrome-extension://${EXTENSION_ID}/`,
|
|
});
|
|
});
|
|
|
|
test("fails closed when Chrome cannot inventory unpacked extensions", async () => {
|
|
const browserSession = { send: jest.fn().mockRejectedValue(new Error("Method not available")) };
|
|
|
|
await expect(readUnpackedExtensions(browserSession)).rejects.toThrow(
|
|
/cannot be identified safely.*no extension was changed/i,
|
|
);
|
|
});
|
|
|
|
test("polls through Chrome's transient post-reload disabled inventory state", async () => {
|
|
jest.useFakeTimers({ doNotFake: ["performance"] });
|
|
try {
|
|
const browserSession = {
|
|
send: jest
|
|
.fn()
|
|
.mockResolvedValueOnce({
|
|
extensions: [{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
|
|
})
|
|
.mockResolvedValueOnce({
|
|
extensions: [{ enabled: true, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
|
|
}),
|
|
};
|
|
|
|
const extensionPromise = waitForReloadedExtensionEnabled(browserSession, EXTENSION_PATH, EXTENSION_ID, {
|
|
pathOptions: { pathImpl: path.win32, platform: "win32" },
|
|
timeoutMilliseconds: 100,
|
|
});
|
|
await flushPromiseJobs();
|
|
expect(jest.getTimerCount()).toBe(1);
|
|
jest.advanceTimersByTime(50);
|
|
await flushPromiseJobs();
|
|
|
|
await expect(extensionPromise).resolves.toMatchObject({ enabled: true, id: EXTENSION_ID });
|
|
expect(browserSession.send).toHaveBeenCalledTimes(2);
|
|
} finally {
|
|
jest.useRealTimers();
|
|
}
|
|
});
|
|
|
|
test("times out when Chrome's post-reload inventory remains disabled", async () => {
|
|
jest.useFakeTimers({ doNotFake: ["performance"] });
|
|
try {
|
|
const browserSession = {
|
|
send: jest.fn().mockResolvedValue({
|
|
extensions: [{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
|
|
}),
|
|
};
|
|
|
|
const extensionPromise = waitForReloadedExtensionEnabled(browserSession, EXTENSION_PATH, EXTENSION_ID, {
|
|
pathOptions: { pathImpl: path.win32, platform: "win32" },
|
|
timeoutMilliseconds: 100,
|
|
});
|
|
const rejection = expect(extensionPromise).rejects.toThrow(/remained disabled for 100ms after reload/);
|
|
await flushPromiseJobs();
|
|
expect(jest.getTimerCount()).toBe(1);
|
|
jest.advanceTimersByTime(50);
|
|
await flushPromiseJobs();
|
|
expect(jest.getTimerCount()).toBe(1);
|
|
jest.advanceTimersByTime(50);
|
|
await flushPromiseJobs();
|
|
|
|
await rejection;
|
|
expect(browserSession.send).toHaveBeenCalledTimes(3);
|
|
} finally {
|
|
jest.useRealTimers();
|
|
}
|
|
});
|
|
|
|
test("rejects a different extension ID immediately while polling the exact unpacked path", async () => {
|
|
const differentExtensionId = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
|
const browserSession = {
|
|
send: jest.fn().mockResolvedValue({
|
|
extensions: [{ enabled: false, id: differentExtensionId, path: EXTENSION_PATH, version: "4.0.5" }],
|
|
}),
|
|
};
|
|
|
|
await expect(
|
|
waitForReloadedExtensionEnabled(browserSession, EXTENSION_PATH, EXTENSION_ID, {
|
|
pathOptions: { pathImpl: path.win32, platform: "win32" },
|
|
timeoutMilliseconds: 100,
|
|
}),
|
|
).rejects.toThrow(`changed from ${EXTENSION_ID} to ${differentExtensionId}`);
|
|
expect(browserSession.send).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("requires both the compiled and resource build IDs after reload", () => {
|
|
expect(() =>
|
|
assertWorkerIdentity(identity({ compiledBuildId: "f".repeat(32) }), EXTENSION_ID, {
|
|
expectedBuildId: BUILD_ID,
|
|
expectedVersion: "4.0.5",
|
|
}),
|
|
).toThrow(/is running build/);
|
|
expect(() =>
|
|
assertWorkerIdentity(identity({ resourceBuildId: "f".repeat(32) }), EXTENSION_ID, {
|
|
expectedBuildId: BUILD_ID,
|
|
expectedVersion: "4.0.5",
|
|
}),
|
|
).toThrow(/exposes resource build/);
|
|
});
|
|
|
|
test("reloads only the exact enabled unpacked extension and proves the replacement worker build", async () => {
|
|
let workers;
|
|
let inventoryVersion = "4.0.4";
|
|
let closeOldWorker;
|
|
const oldWorkerClosed = new Promise((resolve) => {
|
|
closeOldWorker = resolve;
|
|
});
|
|
const oldWorker = {
|
|
evaluate: jest
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
identity({ compiledBuildId: "a".repeat(32), resourceBuildId: BUILD_ID, version: "4.0.4" }),
|
|
)
|
|
.mockImplementationOnce(async () => {
|
|
workers = [];
|
|
inventoryVersion = "4.0.5";
|
|
closeOldWorker();
|
|
return true;
|
|
}),
|
|
url: () => extensionServiceWorkerUrl(EXTENSION_ID),
|
|
waitForEvent: jest.fn(() => oldWorkerClosed),
|
|
};
|
|
const newWorker = {
|
|
evaluate: jest.fn().mockResolvedValue(identity()),
|
|
url: () => extensionServiceWorkerUrl(EXTENSION_ID),
|
|
};
|
|
workers = [oldWorker];
|
|
|
|
const browserSession = {
|
|
send: jest.fn(async (method) => {
|
|
expect(method).toBe("Extensions.getExtensions");
|
|
return {
|
|
extensions: [{ enabled: true, id: EXTENSION_ID, path: EXTENSION_PATH, version: inventoryVersion }],
|
|
};
|
|
}),
|
|
};
|
|
const context = { serviceWorkers: jest.fn(() => workers) };
|
|
const serviceWorkerSession = {
|
|
send: jest.fn(async (method) => {
|
|
if (method === "ServiceWorker.startWorker") workers = [newWorker];
|
|
}),
|
|
};
|
|
|
|
await expect(
|
|
reloadLiveExtension({
|
|
browserSession,
|
|
context,
|
|
expectedBuildId: BUILD_ID,
|
|
expectedExtensionPath: EXTENSION_PATH,
|
|
expectedVersion: "4.0.5",
|
|
pathOptions: { pathImpl: path.win32, platform: "win32" },
|
|
serviceWorkerSession,
|
|
timeoutMilliseconds: 100,
|
|
waitOptions: { delayImpl: async () => {}, now: Date.now },
|
|
}),
|
|
).resolves.toEqual({
|
|
buildId: BUILD_ID,
|
|
extensionId: EXTENSION_ID,
|
|
previousVersion: "4.0.4",
|
|
version: "4.0.5",
|
|
});
|
|
|
|
expect(browserSession.send).toHaveBeenCalledTimes(2);
|
|
expect(oldWorker.waitForEvent).toHaveBeenCalledWith("close", { timeout: 100 });
|
|
expect(oldWorker.evaluate).toHaveBeenCalledTimes(2);
|
|
expect(newWorker.evaluate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("refuses to start or reload the unpacked extension when Chrome reports it disabled", async () => {
|
|
const browserSession = {
|
|
send: jest.fn().mockResolvedValue({
|
|
extensions: [{ enabled: false, id: EXTENSION_ID, path: EXTENSION_PATH, version: "4.0.5" }],
|
|
}),
|
|
};
|
|
const context = { serviceWorkers: jest.fn() };
|
|
const serviceWorkerSession = { send: jest.fn() };
|
|
|
|
await expect(
|
|
reloadLiveExtension({
|
|
browserSession,
|
|
context,
|
|
expectedBuildId: BUILD_ID,
|
|
expectedExtensionPath: EXTENSION_PATH,
|
|
expectedVersion: "4.0.5",
|
|
pathOptions: { pathImpl: path.win32, platform: "win32" },
|
|
serviceWorkerSession,
|
|
}),
|
|
).rejects.toThrow(/installed but disabled/);
|
|
expect(context.serviceWorkers).not.toHaveBeenCalled();
|
|
expect(serviceWorkerSession.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("does not request a worker reload when the expected unpacked path is absent", async () => {
|
|
const browserSession = {
|
|
send: jest.fn().mockResolvedValue({
|
|
extensions: [{ enabled: true, id: EXTENSION_ID, path: "C:\\somewhere-else", version: "4.0.5" }],
|
|
}),
|
|
};
|
|
const context = { serviceWorkers: jest.fn() };
|
|
const serviceWorkerSession = { send: jest.fn() };
|
|
|
|
await expect(
|
|
reloadLiveExtension({
|
|
browserSession,
|
|
context,
|
|
expectedBuildId: BUILD_ID,
|
|
expectedExtensionPath: EXTENSION_PATH,
|
|
expectedVersion: "4.0.5",
|
|
pathOptions: { pathImpl: path.win32, platform: "win32" },
|
|
serviceWorkerSession,
|
|
}),
|
|
).rejects.toThrow(/is not installed/);
|
|
expect(context.serviceWorkers).not.toHaveBeenCalled();
|
|
expect(serviceWorkerSession.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("uses only the caller-selected YouTube page for the target-profile ServiceWorker session", async () => {
|
|
const existingInternalPage = { url: () => "chrome://extensions/" };
|
|
const selectedPage = {
|
|
context: jest.fn(),
|
|
isClosed: jest.fn().mockReturnValue(false),
|
|
url: jest.fn().mockReturnValue("https://www.youtube.com/watch?v=AAAAAAAAAAA"),
|
|
};
|
|
const browserSession = { detach: jest.fn().mockResolvedValue(undefined) };
|
|
const serviceWorkerSession = { detach: jest.fn().mockResolvedValue(undefined) };
|
|
const browser = { newBrowserCDPSession: jest.fn().mockResolvedValue(browserSession) };
|
|
const context = {
|
|
newCDPSession: jest.fn().mockResolvedValue(serviceWorkerSession),
|
|
newPage: jest.fn(),
|
|
pages: jest.fn().mockReturnValue([existingInternalPage]),
|
|
};
|
|
selectedPage.context.mockReturnValue(context);
|
|
const reloadResult = { extensionId: EXTENSION_ID };
|
|
const reloadImpl = jest.fn().mockResolvedValue(reloadResult);
|
|
|
|
await expect(
|
|
reloadLiveExtensionInBrowser({
|
|
browser,
|
|
context,
|
|
expectedBuildId: BUILD_ID,
|
|
expectedExtensionPath: EXTENSION_PATH,
|
|
expectedVersion: "4.0.5",
|
|
reloadImpl,
|
|
sessionPage: selectedPage,
|
|
timeoutMilliseconds: 12345,
|
|
}),
|
|
).resolves.toBe(reloadResult);
|
|
|
|
expect(context.pages).not.toHaveBeenCalled();
|
|
expect(context.newPage).not.toHaveBeenCalled();
|
|
expect(context.newCDPSession).toHaveBeenCalledWith(selectedPage);
|
|
expect(reloadImpl).toHaveBeenCalledWith({
|
|
browserSession,
|
|
context,
|
|
expectedBuildId: BUILD_ID,
|
|
expectedExtensionPath: EXTENSION_PATH,
|
|
expectedVersion: "4.0.5",
|
|
serviceWorkerSession,
|
|
timeoutMilliseconds: 12345,
|
|
});
|
|
expect(serviceWorkerSession.detach).toHaveBeenCalledTimes(1);
|
|
expect(browserSession.detach).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test("rejects a session page from a different profile before opening a browser CDP session", async () => {
|
|
const otherContext = {};
|
|
const sessionPage = {
|
|
context: () => otherContext,
|
|
isClosed: () => false,
|
|
url: () => "https://www.youtube.com/",
|
|
};
|
|
const browser = { newBrowserCDPSession: jest.fn() };
|
|
const context = { newCDPSession: jest.fn(), newPage: jest.fn() };
|
|
|
|
await expect(
|
|
reloadLiveExtensionInBrowser({
|
|
browser,
|
|
context,
|
|
expectedBuildId: BUILD_ID,
|
|
expectedExtensionPath: EXTENSION_PATH,
|
|
expectedVersion: "4.0.5",
|
|
sessionPage,
|
|
}),
|
|
).rejects.toThrow(/does not belong to the attached Chrome context/);
|
|
expect(browser.newBrowserCDPSession).not.toHaveBeenCalled();
|
|
expect(context.newCDPSession).not.toHaveBeenCalled();
|
|
});
|
|
});
|