Files
extension/packages/browser/tests/webextension-polyfill.test.ts
zeekayandHanzo Dev 00bbed932b The browser package builds from a clean clone, and its README says how
pnpm install failed on main. packages/browser names @hanzo/logo from a
catalog with no entry for it, and pnpm 11 ignores onlyBuiltDependencies
and stops on every install script nobody approved; allowBuilds says the
same thing in the form pnpm 11 reads, so only better-sqlite3 runs its
script. The lockfile had not been regenerated since a71abe4 moved the
manifests to catalog versions, so --frozen-lockfile could not pass; it
is regenerated here.

Then the build. esbuild 0.28 cannot lower webextension-polyfill to
safari14, so the Safari target is safari15, and @hanzo/ui 8.27 imports
react-hook-form, an optional peer the package now declares. The
polyfill pin the test asks for had become a caret in the catalog; it is
exact again, and the test resolves a catalog: spec.

After the change pnpm install --frozen-lockfile passes, node src/build.js
writes dist/chrome, dist/firefox and dist/safari, vitest passes 291 of
291, the Chrome build loads in Chromium as
biingenefmanpecedoafkfajbnlgdmbl, and web-ext lint reports no errors on
dist/firefox.

packages/browser/README.md is new. LLM.md named a .hanzo/workflows
directory that is gone and called .github/workflows empty, and two
comments and a test script pointed at a cdp-bridge-server.ts that no
longer exists.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-11 12:09:00 -07:00

110 lines
4.7 KiB
TypeScript

/**
* webextension-polyfill integration.
*
* The 1.9.0 unification: every entrypoint imports `webextension-polyfill`
* as its first import. On Chrome/Edge that defines `globalThis.browser`
* as the Promise-returning WebExtension API; on Firefox/Safari it's a
* no-op because `browser.*` is already native.
*
* The unified `webExtensionAdapter()` factory in `shared/auth.ts` then
* picks up that global so we have ONE adapter codepath instead of two
* (chromeAdapter / firefoxAdapter) with subtly different behaviours.
*
* This suite verifies the contract structurally (the polyfill is in
* deps, the entrypoints import it, the adapter exists) — runtime
* verification is in the e2e specs.
*/
import { describe, it, expect } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
const pkg = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'),
);
const workspace = fs.readFileSync(
path.join(__dirname, '..', '..', '..', 'pnpm-workspace.yaml'),
'utf8',
);
describe('webextension-polyfill is a runtime dependency', () => {
it('listed in dependencies (not devDependencies)', () => {
expect(pkg.dependencies['webextension-polyfill']).toBeDefined();
});
it('pinned to a known version (no caret) so MV3 behavior is reproducible', () => {
// Caret ranges across major versions of the polyfill have historically
// changed return-value shapes. Pin to a known version. A `catalog:` spec
// takes its version from pnpm-workspace.yaml.
const spec = pkg.dependencies['webextension-polyfill'];
const v =
spec === 'catalog:'
? workspace.match(/^\s+'?webextension-polyfill'?:\s*'([^']+)'/m)?.[1]
: spec;
expect(v).not.toMatch(/^\^/);
expect(v).toMatch(/^\d+\.\d+\.\d+$/);
});
});
describe('shared/auth.ts unified adapter', () => {
const sharedAuth = fs.readFileSync(
path.join(__dirname, '..', 'src/shared/auth.ts'),
'utf8',
);
it('exports webExtensionAdapter with storage.get returning a Promise', () => {
expect(sharedAuth).toMatch(/export\s+function\s+webExtensionAdapter\s*\(\s*\)\s*:\s*BrowserAdapter/);
// Verify the adapter wraps with Promise.resolve so the surface is
// identical regardless of underlying API style.
expect(sharedAuth).toMatch(/Promise\.resolve\(b\.storage\.local\.get/);
});
it('falls back to globalThis.chrome if globalThis.browser is missing', () => {
// The legacy fallback handles tests / non-extension contexts where
// neither has been set up. It uses ?? so an empty browser object
// doesn't shadow chrome.
expect(sharedAuth).toMatch(/globalThis as any\)\.browser\s*\?\?\s*\(globalThis as any\)\.chrome/);
});
it('chromeAdapter and firefoxAdapter are aliases for back-compat', () => {
// Existing callers from before 1.9.0 must continue to work.
expect(sharedAuth).toMatch(/export\s+function\s+chromeAdapter[\s\S]*?return\s+webExtensionAdapter\(\)/);
expect(sharedAuth).toMatch(/export\s+function\s+firefoxAdapter[\s\S]*?return\s+webExtensionAdapter\(\)/);
});
});
describe('build pipeline ships the polyfill', () => {
// src/build.js writes to the extension repo root's dist/<engine>/. These two
// used to read the old output root under packages/browser and `return` when
// it was absent — so once the root moved they asserted nothing, quietly, on
// every run. An assertion about the bundle has to have a bundle, so the
// build is a fixture the whole suite shares (tests/built-dist.ts) and these
// simply require it. Building it HERE, as this file briefly did, raced the
// identical build in output-root.test.ts.
const BROWSER = path.join(__dirname, '..');
const DIST = path.resolve(BROWSER, '..', '..', 'dist');
const bundle = (engine: string) => {
const file = path.join(DIST, engine, 'background.js');
expect(fs.existsSync(file), `${engine}/background.js`).toBe(true);
return fs.readFileSync(file, 'utf8');
};
it('chrome bundle includes webextension-polyfill code', () => {
// The polyfill module sets `globalThis.browser`. We grep for its
// distinctive marker so we know it actually ended up in the bundle.
expect(bundle('chrome')).toMatch(
/webextension-polyfill|browserPolyfill|chromeApiAvailable|globalThis\.browser/,
);
});
it('firefox bundle does not break if the polyfill is a no-op', () => {
const bg = bundle('firefox');
// The Firefox bundle should still be valid JS regardless of whether
// the polyfill no-ops or wraps. Probe for any obvious syntax failure.
expect(bg.length).toBeGreaterThan(1000);
// Must NOT contain leftover NodeJS-only syntax (require()) since the
// bundle target is browser.
expect(bg).not.toMatch(/^const\s+\w+\s*=\s*require\(/m);
});
});