import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { execFileSync } from 'node:child_process' import { fileURLToPath } from 'node:url' import path from 'node:path' const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..') const read = (name) => JSON.parse(readFileSync(path.join(EXT_DIR, name), 'utf8')) const readText = (...seg) => readFileSync(path.join(EXT_DIR, ...seg), 'utf8') // Only the git-free subcommands are exercised here: `version`/`patch` shell out // to git, and the extension lane runs on node:24-bookworm-slim which may not // ship it. Those two are covered where git is guaranteed — ci.yml and build.yml // run on ci-python. const packaging = (cmd) => execFileSync('sh', [path.join(EXT_DIR, 'scripts', 'packaging.sh'), cmd], { cwd: EXT_DIR, encoding: 'utf8' }) .trim() .split(/\s+/) .filter(Boolean) describe('packaging.sh — the single definition of what ships', () => { it('emits an ignore list and a pathspec that agree on the tracked files', () => { const ignore = packaging('ignore') const pathspec = packaging('pathspec').map((e) => e.replace(':(exclude)extension/', '')) // Every git-excluded path must also be hidden from web-ext. The reverse is // not required: node_modules and web-ext-artifacts are build output git // never tracks, so they appear only in the ignore list. for (const entry of pathspec) { expect(ignore, `pathspec has "${entry}" but --ignore-files does not`).toContain(entry) } expect(pathspec.length).toBeGreaterThan(0) expect(ignore).toContain('node_modules') }) it('emits glob patterns literally, never expanded against the working tree', () => { // The script iterates its lists with deliberate word-splitting, so it must // run with pathname expansion off. Without that, invoking it from a cwd // where test/ exists (exactly how ci.yml and vitest call it) expands // `test/**` into the individual spec files, and the pathspec silently stops // covering anything added later. const pathspec = packaging('pathspec') expect(pathspec).toContain(':(exclude)extension/test/**') expect(pathspec).toContain(':(exclude)extension/scripts/**') expect(pathspec.some((e) => e.includes('.spec.js'))).toBe(false) expect(pathspec.some((e) => e.includes('helpers'))).toBe(false) }) it('keeps its own scripts and specs out of the XPI', () => { // Both are repo infrastructure. web-ext packages everything not ignored, so // omitting either would ship dev tooling to users -- and `test/**` in // particular only survives because callers `set -f` before substituting it. const ignore = packaging('ignore') expect(ignore).toContain('vitest.config.js') // Both forms per directory. The glob covers the contents; the bare name // covers the directory ENTRY, which web-ext writes separately — with only // the glob, the XPI carries an empty `test/` and `scripts/`. for (const dir of ['test', 'scripts']) { expect(ignore, `${dir} contents`).toContain(`${dir}/**`) expect(ignore, `${dir} directory entry`).toContain(dir) } }) }) describe('consumers delegate rather than keeping their own copy', () => { // These assertions are the actual anti-regression value: it is easy for a // future edit to "simplify" by inlining a literal list again, which silently // reintroduces the drift that issue #2397 was about. it('package.json derives --ignore-files from the script', () => { for (const [name, script] of Object.entries(read('package.json').scripts)) { if (!script.includes('--ignore-files')) continue expect(script, `${name} should call packaging.sh`).toContain('scripts/packaging.sh ignore') expect(script, `${name} must set -f before the substitution`).toMatch(/set -f;/) } }) it('ci.yml derives its pathspec from the script and hardcodes none', () => { const ci = readText('..', '.forgejo', 'workflows', 'ci.yml') expect(ci).toContain('extension/scripts/packaging.sh pathspec') // A literal :(exclude)extension/... in the workflow means someone bypassed // the shared definition. expect(ci).not.toMatch(/:\(exclude\)extension\//) }) }) describe('extension version', () => { it('keeps manifest.json and package.json in lockstep', () => { expect(read('manifest.json').version).toBe(read('package.json').version) }) it('uses a plain dotted numeric version AMO will accept', () => { expect(read('package.json').version).toMatch(/^\d+(\.\d+)*$/) }) it('declares manifest v3', () => { expect(read('manifest.json').manifest_version).toBe(3) }) it('lists every background script that exists, in dependency order', () => { // url.js must load BEFORE api.js: api.js calls normalizeApiUrl at // init()-time, and these are classic scripts sharing one scope, so a // reordering here is a runtime ReferenceError with no build-time signal. const scripts = read('manifest.json').background.scripts for (const rel of scripts) { expect(() => readFileSync(path.join(EXT_DIR, rel)), `missing ${rel}`).not.toThrow() } expect(scripts.indexOf('lib/url.js')).toBeLessThan(scripts.indexOf('lib/api.js')) }) })