123456789101112import { 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 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});