/** * browser.test.ts — locks down feature-detected browser identification. * * Why this matters for passkeys: detectBrowser() routes the entire passkey * code path. "chrome" picks chrome.webAuthenticationProxy. "firefox" picks * the content-script proxy. Both decisions must work for every Chromium * derivative (Brave, Edge, Vivaldi, Opera, Arc) and every Firefox derivative * (LibreWolf, Floorp, Waterfox) because their UAs differ but their feature * surfaces match the upstream browser. */ import { afterEach, describe, expect, it, vi } from "vitest"; import { detectBrowser, firefoxVersion } from "./browser"; type ChromeShape = Partial<{ webAuthenticationProxy: object | undefined; runtime: { getURL?: (path: string) => string }; }>; function installChrome(shape: ChromeShape | undefined) { if (shape === undefined) { (globalThis as { chrome?: unknown }).chrome = undefined; return; } (globalThis as { chrome?: unknown }).chrome = shape; } function installUserAgent(ua: string) { vi.stubGlobal("navigator", { userAgent: ua }); } describe("detectBrowser", () => { afterEach(() => { vi.unstubAllGlobals(); (globalThis as { chrome?: unknown }).chrome = undefined; }); it("returns 'chrome' when chrome.webAuthenticationProxy is present", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); expect(detectBrowser()).toBe("chrome"); }); it("treats Brave (Chromium derivative) as 'chrome' — UA-independent", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Brave/1.62 Chrome/122.0.0.0 Safari/537.36"); expect(detectBrowser()).toBe("chrome"); }); it("treats Edge (Chromium derivative) as 'chrome'", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"); expect(detectBrowser()).toBe("chrome"); }); it("treats Vivaldi (Chromium derivative) as 'chrome'", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Vivaldi/6.5.3206.55"); expect(detectBrowser()).toBe("chrome"); }); it("treats Opera (Chromium derivative) as 'chrome'", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 OPR/108.0.0.0"); expect(detectBrowser()).toBe("chrome"); }); it("treats Arc (Chromium derivative) as 'chrome'", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Arc/1.30"); expect(detectBrowser()).toBe("chrome"); }); it("returns 'firefox' when the extension URL scheme is moz-extension://", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); expect(detectBrowser()).toBe("firefox"); }); it("treats LibreWolf (Firefox fork) as 'firefox' — UA-independent", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); // LibreWolf preserves Firefox/N in its UA for site compatibility. installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0 LibreWolf/124.0"); expect(detectBrowser()).toBe("firefox"); }); it("treats Floorp (Firefox fork) as 'firefox'", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0 Floorp/11.18"); expect(detectBrowser()).toBe("firefox"); }); it("returns 'unknown' when neither webAuthenticationProxy nor moz-extension is present", () => { installChrome({ runtime: { getURL: () => "safari-web-extension://abc/" } }); expect(detectBrowser()).toBe("unknown"); }); it("returns 'unknown' when chrome global is missing entirely (Safari, in-page contexts)", () => { installChrome(undefined); expect(detectBrowser()).toBe("unknown"); }); it("does not throw if runtime.getURL is missing or throws", () => { installChrome({ runtime: {} }); expect(detectBrowser()).toBe("unknown"); installChrome({ runtime: { getURL: () => { throw new Error("not available"); }, }, }); expect(detectBrowser()).toBe("unknown"); }); }); describe("firefoxVersion", () => { afterEach(() => { vi.unstubAllGlobals(); (globalThis as { chrome?: unknown }).chrome = undefined; }); it("returns NaN when detectBrowser is not firefox", () => { installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"); expect(firefoxVersion()).toBeNaN(); }); it("parses Firefox/N from a stock Firefox UA", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0"); expect(firefoxVersion()).toBe(150); }); it("parses Firefox/N from a LibreWolf UA (preserves Firefox token)", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0 LibreWolf/124.0"); expect(firefoxVersion()).toBe(124); }); it("parses Firefox/N from a Floorp UA", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0 Floorp/11.18"); expect(firefoxVersion()).toBe(124); }); it("returns NaN when Firefox/N cannot be parsed from a UA-spoofed Firefox", () => { installChrome({ runtime: { getURL: () => "moz-extension://abc/" } }); installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 CustomBrowser/1.0"); expect(firefoxVersion()).toBeNaN(); }); });