browser.test.ts

6.5 KB · raw

1/**2 * browser.test.ts — locks down feature-detected browser identification.3 *4 * Why this matters for passkeys: detectBrowser() routes the entire passkey5 * code path. "chrome" picks chrome.webAuthenticationProxy. "firefox" picks6 * the content-script proxy. Both decisions must work for every Chromium7 * derivative (Brave, Edge, Vivaldi, Opera, Arc) and every Firefox derivative8 * (LibreWolf, Floorp, Waterfox) because their UAs differ but their feature9 * surfaces match the upstream browser.10 */1112import { afterEach, describe, expect, it, vi } from "vitest";1314import { detectBrowser, firefoxVersion } from "./browser";1516type ChromeShape = Partial<{17  webAuthenticationProxy: object | undefined;18  runtime: { getURL?: (path: string) => string };19}>;2021function installChrome(shape: ChromeShape | undefined) {22  if (shape === undefined) {23    (globalThis as { chrome?: unknown }).chrome = undefined;24    return;25  }26  (globalThis as { chrome?: unknown }).chrome = shape;27}2829function installUserAgent(ua: string) {30  vi.stubGlobal("navigator", { userAgent: ua });31}3233describe("detectBrowser", () => {34  afterEach(() => {35    vi.unstubAllGlobals();36    (globalThis as { chrome?: unknown }).chrome = undefined;37  });3839  it("returns 'chrome' when chrome.webAuthenticationProxy is present", () => {40    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });41    expect(detectBrowser()).toBe("chrome");42  });4344  it("treats Brave (Chromium derivative) as 'chrome' — UA-independent", () => {45    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });46    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");47    expect(detectBrowser()).toBe("chrome");48  });4950  it("treats Edge (Chromium derivative) as 'chrome'", () => {51    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });52    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");53    expect(detectBrowser()).toBe("chrome");54  });5556  it("treats Vivaldi (Chromium derivative) as 'chrome'", () => {57    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });58    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");59    expect(detectBrowser()).toBe("chrome");60  });6162  it("treats Opera (Chromium derivative) as 'chrome'", () => {63    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });64    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");65    expect(detectBrowser()).toBe("chrome");66  });6768  it("treats Arc (Chromium derivative) as 'chrome'", () => {69    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });70    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");71    expect(detectBrowser()).toBe("chrome");72  });7374  it("returns 'firefox' when the extension URL scheme is moz-extension://", () => {75    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });76    expect(detectBrowser()).toBe("firefox");77  });7879  it("treats LibreWolf (Firefox fork) as 'firefox' — UA-independent", () => {80    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });81    // LibreWolf preserves Firefox/N in its UA for site compatibility.82    installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0 LibreWolf/124.0");83    expect(detectBrowser()).toBe("firefox");84  });8586  it("treats Floorp (Firefox fork) as 'firefox'", () => {87    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });88    installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0 Floorp/11.18");89    expect(detectBrowser()).toBe("firefox");90  });9192  it("returns 'unknown' when neither webAuthenticationProxy nor moz-extension is present", () => {93    installChrome({ runtime: { getURL: () => "safari-web-extension://abc/" } });94    expect(detectBrowser()).toBe("unknown");95  });9697  it("returns 'unknown' when chrome global is missing entirely (Safari, in-page contexts)", () => {98    installChrome(undefined);99    expect(detectBrowser()).toBe("unknown");100  });101102  it("does not throw if runtime.getURL is missing or throws", () => {103    installChrome({ runtime: {} });104    expect(detectBrowser()).toBe("unknown");105106    installChrome({107      runtime: {108        getURL: () => {109          throw new Error("not available");110        },111      },112    });113    expect(detectBrowser()).toBe("unknown");114  });115});116117describe("firefoxVersion", () => {118  afterEach(() => {119    vi.unstubAllGlobals();120    (globalThis as { chrome?: unknown }).chrome = undefined;121  });122123  it("returns NaN when detectBrowser is not firefox", () => {124    installChrome({ webAuthenticationProxy: {}, runtime: { getURL: () => "chrome-extension://abc/" } });125    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");126    expect(firefoxVersion()).toBeNaN();127  });128129  it("parses Firefox/N from a stock Firefox UA", () => {130    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });131    installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:150.0) Gecko/20100101 Firefox/150.0");132    expect(firefoxVersion()).toBe(150);133  });134135  it("parses Firefox/N from a LibreWolf UA (preserves Firefox token)", () => {136    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });137    installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0 LibreWolf/124.0");138    expect(firefoxVersion()).toBe(124);139  });140141  it("parses Firefox/N from a Floorp UA", () => {142    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });143    installUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0 Floorp/11.18");144    expect(firefoxVersion()).toBe(124);145  });146147  it("returns NaN when Firefox/N cannot be parsed from a UA-spoofed Firefox", () => {148    installChrome({ runtime: { getURL: () => "moz-extension://abc/" } });149    installUserAgent("Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 CustomBrowser/1.0");150    expect(firefoxVersion()).toBeNaN();151  });152});