@fairfox/polly 0.38.2 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/cli/polly.js.map +1 -1
  2. package/dist/src/background/index.js +1 -2
  3. package/dist/src/background/index.js.map +6 -6
  4. package/dist/src/background/message-router.js +1 -2
  5. package/dist/src/background/message-router.js.map +6 -6
  6. package/dist/src/client/index.js +46 -27
  7. package/dist/src/client/index.js.map +5 -5
  8. package/dist/src/client/wrapper.d.ts +8 -0
  9. package/dist/src/elysia/index.js +10 -31
  10. package/dist/src/elysia/index.js.map +5 -5
  11. package/dist/src/elysia/route-match.d.ts +9 -0
  12. package/dist/src/elysia/types.d.ts +4 -5
  13. package/dist/src/index.js +1 -2
  14. package/dist/src/index.js.map +7 -7
  15. package/dist/src/mesh-node.js.map +1 -1
  16. package/dist/src/mesh.js.map +7 -7
  17. package/dist/src/peer.js.map +3 -3
  18. package/dist/src/polly-ui/index.js.map +3 -3
  19. package/dist/src/polly-ui/markdown.js +583 -517
  20. package/dist/src/polly-ui/markdown.js.map +6 -6
  21. package/dist/src/polly-ui/registry.d.ts +16 -0
  22. package/dist/src/polly-ui/registry.generated.d.ts +20 -0
  23. package/dist/src/shared/adapters/index.js.map +3 -3
  24. package/dist/src/shared/lib/context-helpers.js +1 -2
  25. package/dist/src/shared/lib/context-helpers.js.map +6 -6
  26. package/dist/src/shared/lib/mesh-signaling-client.d.ts +1 -1
  27. package/dist/src/shared/lib/message-bus.js +1 -2
  28. package/dist/src/shared/lib/message-bus.js.map +6 -6
  29. package/dist/src/shared/lib/peer-relay-adapter.d.ts +3 -2
  30. package/dist/src/shared/lib/resource.js.map +3 -3
  31. package/dist/src/shared/lib/state.js.map +3 -3
  32. package/dist/src/shared/state/app-state.js.map +4 -4
  33. package/dist/src/shared/types/messages.d.ts +0 -9
  34. package/dist/src/shared/types/messages.js.map +1 -1
  35. package/dist/tools/quality/src/attest.d.ts +55 -0
  36. package/dist/tools/quality/src/cache.d.ts +34 -0
  37. package/dist/tools/quality/src/cli.js +1738 -2
  38. package/dist/tools/quality/src/cli.js.map +13 -4
  39. package/dist/tools/quality/src/config.d.ts +18 -0
  40. package/dist/tools/quality/src/host.d.ts +46 -0
  41. package/dist/tools/quality/src/index.d.ts +7 -0
  42. package/dist/tools/quality/src/index.js +1637 -1
  43. package/dist/tools/quality/src/index.js.map +13 -4
  44. package/dist/tools/quality/src/plugins/core-checks.d.ts +20 -0
  45. package/dist/tools/quality/src/plugins/core.d.ts +18 -0
  46. package/dist/tools/quality/src/plugins/extra-checks.d.ts +14 -0
  47. package/dist/tools/quality/src/plugins/import-checks.d.ts +16 -0
  48. package/dist/tools/quality/src/plugins/polly-ui.d.ts +31 -0
  49. package/dist/tools/quality/src/types.d.ts +104 -0
  50. package/dist/tools/test/src/adapters/index.js.map +1 -1
  51. package/dist/tools/test/src/index.js.map +1 -1
  52. package/dist/tools/test/src/visual/index.js +64 -38
  53. package/dist/tools/test/src/visual/index.js.map +4 -4
  54. package/dist/tools/verify/Dockerfile +1 -1
  55. package/dist/tools/verify/specs/Dockerfile +1 -1
  56. package/dist/tools/verify/src/cli.js +324 -226
  57. package/dist/tools/verify/src/cli.js.map +8 -7
  58. package/dist/tools/visualize/src/cli.js.map +3 -3
  59. package/package.json +40 -29
  60. package/dist/src/utils/function-serialization.d.ts +0 -14
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Additional checks bundled into `pollyCorePlugin`.
3
+ *
4
+ * Each check here is a port of an existing `scripts/check-*.ts` script
5
+ * from polly's repo into the plugin contract. The original scripts
6
+ * remain on disk for back-compat with the existing pre-commit
7
+ * orchestrator (`scripts/check.ts`); the plugin path is the new way
8
+ * for downstream consumers (lingua, fairfox, warehouse-experiments)
9
+ * who do not want to copy the script verbatim.
10
+ *
11
+ * Out of scope for this release:
12
+ * - `polly:boundaries` issue text describes a workspace-dependency
13
+ * model (each package may only import from packages it lists in
14
+ * its `dependencies`). Polly itself is a single-package repo and
15
+ * uses directional zone bans (`src/` cannot import `tools/`,
16
+ * etc.); that model is what ships here. The workspace-dep model
17
+ * can be layered on as an additional configuration mode later.
18
+ */
19
+ import type { Check } from "../types";
20
+ export declare const additionalCoreChecks: Check<unknown>[];
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `pollyCorePlugin` — the polly-provided core plugin.
3
+ *
4
+ * Wraps the four checks polly already ships in `@fairfox/polly/quality`
5
+ * into the new `Check` contract. The wrapping is purely adaptive: each
6
+ * underlying function (`checkNoAsCasting`, `checkNoRequire`,
7
+ * `checkSecrets`, `checkGitignoreCoversAllowlist`) keeps its existing
8
+ * exports unchanged, so consumers wiring those up by hand continue to
9
+ * work. The plugin path is the new way; the function path stays the
10
+ * same.
11
+ *
12
+ * The CSS family and shared-components live in a separate `polly-ui`
13
+ * plugin (#92–#94) because they belong to the styled-component
14
+ * contract polly-ui owns. Issue #98's scope is the four core checks.
15
+ */
16
+ import type { QualityPlugin } from "../types";
17
+ export declare const POLLY_CORE_VERSION = "0.45.0";
18
+ export declare const pollyCorePlugin: QualityPlugin;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Three more checks bundled into `pollyCorePlugin`:
3
+ *
4
+ * - polly:forbidden-deps — import-graph ban list (#87)
5
+ * - polly:no-state-hooks — ban useState/useReducer/useSignal (#99)
6
+ * - polly:typographic-quotes — opt-in straight-vs-curly enforcement (#88)
7
+ *
8
+ * Each check is parameterised: defaults match polly's own pre-commit
9
+ * surface; consumers override via `polly.config.ts`. Test files are
10
+ * excluded by default for the import-walking checks since mocks and
11
+ * fixtures legitimately reference banned packages.
12
+ */
13
+ import type { Check } from "../types";
14
+ export declare const extraCoreChecks: Check<unknown>[];
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Import-graph checks for `pollyCorePlugin`:
3
+ *
4
+ * - polly:relative-imports — ban `../` imports beyond a depth threshold (#84)
5
+ * - polly:tsconfig-paths — ban `compilerOptions.paths` aliases (#84)
6
+ * - polly:no-raw-http — force HTTP through a canonical client (#86)
7
+ * - polly:types — multi-package tsc --noEmit orchestrator (#85)
8
+ *
9
+ * All four are parameterised. Defaults are silent or zero-impact unless a
10
+ * consumer opts in via `polly.config.ts`. Polly's own pre-commit pipeline
11
+ * does not currently run these — they exist primarily for downstream
12
+ * consumers (lingua, fairfox, warehouse-experiments) and ship under the
13
+ * core plugin namespace so adoption is one config-block change away.
14
+ */
15
+ import type { Check } from "../types";
16
+ export declare const importCoreChecks: Check<unknown>[];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `pollyUiPlugin` — the polly-ui-provided plugin (#89, #90, #91–#94).
3
+ *
4
+ * Re-homes the CSS conformance family and shared-components ban under a
5
+ * dedicated `polly-ui` namespace, and adds a new `no-inline-handlers`
6
+ * check for JSX event handlers. Every wrap reuses the underlying
7
+ * function from `tools/quality/src/css/*` and
8
+ * `tools/quality/src/check-shared-components.ts` so behaviour is
9
+ * identical to the pre-host invocation; only the integration surface
10
+ * changes.
11
+ *
12
+ * Out of scope for this release:
13
+ * - The data-action dispatcher *runtime* described in #90. The check
14
+ * ships here so a project can ban inline handlers today; the
15
+ * dispatcher needs a real Preact implementation that mounts near
16
+ * <OverlayRoot> and registers a delegated event listener, and that
17
+ * work belongs in `src/polly-ui/actions.tsx` rather than under the
18
+ * quality plugin host. The check on its own is useful — it forces
19
+ * the consumer to pick an alternative — and the runtime can land
20
+ * in a follow-up release without changing the check's id.
21
+ * - Registry-driven CSS validation. The four CSS checks currently
22
+ * parse polly-ui's CSS at scan time. The new
23
+ * `@fairfox/polly/ui/registry` (introduced alongside this plugin)
24
+ * exposes the canonical token + component lists as data, and a
25
+ * follow-up can swap the parse-time discovery for a registry
26
+ * lookup. The shape of the wraps does not change when that
27
+ * happens; only the inner implementation does.
28
+ */
29
+ import type { QualityPlugin } from "../types";
30
+ export declare const POLLY_UI_PLUGIN_VERSION = "0.46.0";
31
+ export declare const pollyUiPlugin: QualityPlugin;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Plugin-host contract for `@fairfox/polly/quality`.
3
+ *
4
+ * A `QualityPlugin` bundles a set of `Check`s under a namespace. The host
5
+ * loads one or more plugins from `polly.config.ts`, validates that each
6
+ * check's id is unique, and runs the requested set in parallel. A check
7
+ * declares the inputs it reads so the cache layer (see `cache.ts`) can
8
+ * compute a content-hash and skip re-execution when nothing has changed.
9
+ *
10
+ * Plugin and check ids are namespaced as `<plugin>:<name>`. Polly's own
11
+ * plugin uses the `polly` prefix; polly-ui will use `polly-ui`; consumer
12
+ * plugins use whatever prefix matches their package name.
13
+ */
14
+ /**
15
+ * Outcome of running a single check. The host aggregates these into a
16
+ * `RunReport` for the CLI. `cached` is true when the result came back from
17
+ * the on-disk cache without re-running the check body.
18
+ */
19
+ export type CheckRunResult = {
20
+ id: string;
21
+ ok: boolean;
22
+ durationMs: number;
23
+ cached: boolean;
24
+ /** Human-readable summary line (one violation per element, or status). */
25
+ messages: string[];
26
+ /** Raw error if the check threw; surfaces in CLI output. */
27
+ error?: string;
28
+ };
29
+ /**
30
+ * Context passed to a check at run time. The host fills in `rootDir`
31
+ * and `signal` (for cancellation) before invoking `run`.
32
+ */
33
+ export type CheckContext<TConfig = unknown> = {
34
+ /** Repository root the consumer is checking. */
35
+ rootDir: string;
36
+ /** Resolved configuration for this check (already validated). */
37
+ config: TConfig;
38
+ /** Aborted when the run is cancelled (CLI Ctrl-C, watch reload, etc.). */
39
+ signal?: AbortSignal;
40
+ };
41
+ /**
42
+ * A single check. The host calls `validate(config)` once at load time
43
+ * and `run(ctx)` once per execution. `filesRead(config)` is consulted by
44
+ * the cache before `run` — return the absolute paths whose content must
45
+ * invalidate a cached result.
46
+ */
47
+ export type Check<TConfig = unknown> = {
48
+ /** Namespaced id, e.g. `polly:no-as-casting`. */
49
+ id: string;
50
+ /** One-line description for `polly quality list`. */
51
+ description: string;
52
+ /**
53
+ * Validate user-supplied config. Return `null` for valid input or a
54
+ * non-empty array of error messages for invalid input. Errors are
55
+ * surfaced at load time, not at run time.
56
+ */
57
+ validate?: (config: unknown) => string[] | null;
58
+ /**
59
+ * Files whose content affects the result of this check. Returned as
60
+ * absolute paths (or paths relative to `rootDir`; the cache normalises).
61
+ * The host hashes these and skips `run` on cache hit.
62
+ */
63
+ filesRead?: (config: TConfig, rootDir: string) => Promise<string[]> | string[];
64
+ /**
65
+ * Environment variables and tool versions that contribute to the
66
+ * cache key alongside file content. Use this when the check's behaviour
67
+ * depends on something not captured by `filesRead` (e.g. a binary's
68
+ * version, an env var that flips a code path).
69
+ */
70
+ cacheKeyExtras?: (config: TConfig) => Record<string, string>;
71
+ /** Run the check. Throws are caught by the host and reported as failures. */
72
+ run: (ctx: CheckContext<TConfig>) => Promise<CheckOutcome>;
73
+ };
74
+ /**
75
+ * The body of a check returns either pass or fail with messages. The host
76
+ * adds id, duration, and cache status to produce a `CheckRunResult`.
77
+ */
78
+ export type CheckOutcome = {
79
+ ok: boolean;
80
+ messages: string[];
81
+ };
82
+ /**
83
+ * A plugin contributes a namespaced bundle of checks.
84
+ */
85
+ export type QualityPlugin = {
86
+ /** Namespace used as the prefix on every check id. */
87
+ name: string;
88
+ version: string;
89
+ checks: Check<unknown>[];
90
+ };
91
+ /**
92
+ * Per-check configuration map keyed by check id. The host pulls the
93
+ * matching entry out of `qualityConfig` and passes it as `ctx.config`.
94
+ */
95
+ export type QualityRunConfig = {
96
+ plugins: QualityPlugin[];
97
+ /** Per-check config keyed by check id (e.g. `"polly:no-as-casting"`). */
98
+ checks?: Record<string, unknown>;
99
+ };
100
+ export type RunReport = {
101
+ ok: boolean;
102
+ results: CheckRunResult[];
103
+ totalDurationMs: number;
104
+ };
@@ -7,7 +7,7 @@
7
7
  "// Mock logger adapter for testing\nimport type { LoggerAdapter } from \"@/shared/adapters/logger.adapter\";\nimport type { LogLevel } from \"@/shared/types/messages\";\n\nexport interface LogCall {\n level: LogLevel;\n message: string;\n context?: Record<string, unknown>;\n error?: Error;\n timestamp: number;\n}\n\nexport interface MockLogger extends LoggerAdapter {\n _calls: LogCall[];\n _clear(): void;\n}\n\nexport function createMockLogger(options?: { silent?: boolean }): MockLogger {\n const calls: LogCall[] = [];\n const silent = options?.silent ?? true;\n\n const logToConsole = (level: LogLevel, message: string, context?: Record<string, unknown>) => {\n if (!silent) {\n // biome-ignore lint/suspicious/noConsole: Mock logger intentionally uses console for testing\n const consoleMethod = level === \"debug\" ? console.log : console[level];\n consoleMethod(message, context);\n }\n };\n\n return {\n debug(message: string, context?: Record<string, unknown>): void {\n calls.push({\n level: \"debug\",\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"debug\", message, context);\n },\n\n info(message: string, context?: Record<string, unknown>): void {\n calls.push({\n level: \"info\",\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"info\", message, context);\n },\n\n warn(message: string, context?: Record<string, unknown>): void {\n calls.push({\n level: \"warn\",\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"warn\", message, context);\n },\n\n error(message: string, error?: Error, context?: Record<string, unknown>): void {\n calls.push({\n level: \"error\",\n message,\n ...(error && { error }),\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"error\", message, { ...context, error });\n },\n\n log(level: LogLevel, message: string, context?: Record<string, unknown>): void {\n calls.push({\n level,\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(level, message, context);\n },\n\n // Test-only internals\n _calls: calls,\n _clear() {\n calls.length = 0;\n },\n };\n}\n",
8
8
  "import type {\n CreateOffscreenDocumentParameters,\n OffscreenAdapter,\n} from \"@/shared/adapters/offscreen.adapter\";\n\nexport interface MockOffscreen extends OffscreenAdapter {\n _hasDocument: boolean;\n}\n\nexport function createMockOffscreen(): MockOffscreen {\n let hasDocument = false;\n\n return {\n createDocument: async (_parameters: CreateOffscreenDocumentParameters): Promise<void> => {\n hasDocument = true;\n },\n closeDocument: async (): Promise<void> => {\n hasDocument = false;\n },\n hasDocument: async (): Promise<boolean> => {\n return hasDocument;\n },\n _hasDocument: hasDocument,\n };\n}\n",
9
9
  "import type { MessageSender, PortAdapter, RuntimeAdapter } from \"@/shared/adapters/runtime.adapter\";\n\nexport interface MockPort extends PortAdapter {\n _listeners: Set<(message: unknown) => void>;\n _disconnectListeners: Set<() => void>;\n}\n\nexport function createMockPort(name: string): MockPort {\n const listeners = new Set<(message: unknown) => void>();\n const disconnectListeners = new Set<() => void>();\n\n return {\n name,\n onMessage: (callback) => listeners.add(callback),\n onDisconnect: (callback) => disconnectListeners.add(callback),\n postMessage: (message) => {\n for (const listener of listeners) {\n listener(message);\n }\n },\n disconnect: () => {\n for (const listener of disconnectListeners) {\n listener();\n }\n },\n _listeners: listeners,\n _disconnectListeners: disconnectListeners,\n };\n}\n\nexport interface MockRuntime extends RuntimeAdapter {\n id: string;\n _messageListeners: Set<\n (message: unknown, sender: MessageSender, sendResponse: (response: unknown) => void) => void\n >;\n _connectListeners: Set<(port: PortAdapter) => void>;\n}\n\nexport function createMockRuntime(id = \"test-extension-id\"): MockRuntime {\n const messageListeners = new Set<\n (message: unknown, sender: MessageSender, sendResponse: (response: unknown) => void) => void\n >();\n const connectListeners = new Set<(port: PortAdapter) => void>();\n\n return {\n id,\n sendMessage: async <T>(message: T): Promise<unknown> => {\n // Check if this is a response message\n if (typeof message === \"object\" && message !== null && \"success\" in message) {\n // This is a response, route it back to all listeners\n for (const listener of messageListeners) {\n listener(message, { url: \"\" }, () => {\n // Empty response handler for mock\n });\n }\n return undefined;\n }\n\n // This is a request, call ALL listeners (Chrome calls all, but only first response is used)\n if (messageListeners.size === 0) {\n return undefined;\n }\n\n return new Promise((resolve) => {\n let resolved = false;\n const sharedSendResponse = (res: unknown) => {\n if (!resolved) {\n resolved = true;\n resolve(res);\n }\n };\n\n // Call all listeners (Chrome behavior)\n for (const listener of messageListeners) {\n const result = listener(message, { url: \"\" }, sharedSendResponse);\n // If listener returns true, it will send response asynchronously\n // If it returns false/undefined/void and we haven't resolved yet, continue to next listener\n if (typeof result === \"boolean\" && result === true) {\n // Listener will send response asynchronously, wait for it\n }\n }\n\n // If no listener handled it, resolve with undefined\n if (!resolved) {\n resolve(undefined);\n }\n });\n },\n onMessage: (\n callback: (\n message: unknown,\n sender: MessageSender,\n sendResponse: (response: unknown) => void\n ) => undefined | boolean\n ) => {\n messageListeners.add(callback);\n },\n removeMessageListener: (\n callback: (\n message: unknown,\n sender: MessageSender,\n sendResponse: (response: unknown) => void\n ) => undefined | boolean\n ) => {\n messageListeners.delete(callback);\n },\n connect: (name: string): PortAdapter => {\n const port = createMockPort(name);\n for (const listener of connectListeners) {\n listener(port);\n }\n return port;\n },\n onConnect: (callback: (port: PortAdapter) => void) => {\n connectListeners.add(callback);\n },\n getURL: (path: string): string => {\n return `chrome-extension://${id}/${path}`;\n },\n getId: (): string => {\n return id;\n },\n openOptionsPage: (): void => {\n // Mock implementation - no-op for tests\n },\n _messageListeners: messageListeners,\n _connectListeners: connectListeners,\n };\n}\n",
10
- "import type { StorageAdapter, StorageChanges } from \"@/shared/adapters/storage.adapter\";\n\nexport interface MockStorageArea extends StorageAdapter {\n _data: Map<string, unknown>;\n}\n\nexport function createMockStorageArea(): MockStorageArea {\n const data = new Map<string, unknown>();\n\n return {\n get: async <T = Record<string, unknown>>(\n keys?: string | string[] | Record<string, unknown> | null\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Mock storage needs to handle multiple key types\n ): Promise<T> => {\n if (!keys) {\n return Object.fromEntries(data) as T;\n }\n if (typeof keys === \"string\") {\n return (data.has(keys) ? { [keys]: data.get(keys) } : {}) as T;\n }\n if (Array.isArray(keys)) {\n const result: Record<string, unknown> = {};\n for (const key of keys) {\n if (data.has(key)) {\n result[key] = data.get(key);\n }\n }\n return result as unknown as T;\n }\n // Object with defaults\n const result: Record<string, unknown> = {};\n for (const [key, defaultValue] of Object.entries(keys)) {\n result[key] = data.has(key) ? data.get(key) : defaultValue;\n }\n return result as unknown as T;\n },\n set: async (items) => {\n for (const [key, value] of Object.entries(items)) {\n data.set(key, value);\n }\n },\n remove: async (keys) => {\n const keyArray = Array.isArray(keys) ? keys : [keys];\n for (const key of keyArray) {\n data.delete(key);\n }\n },\n clear: async () => {\n data.clear();\n },\n onChanged: (_callback: (changes: StorageChanges, areaName: string) => void) => {\n // Mock implementation - not needed for current tests\n },\n _data: data,\n };\n}\n",
10
+ "import type { StorageAdapter, StorageChanges } from \"@/shared/adapters/storage.adapter\";\n\nexport interface MockStorageArea extends StorageAdapter {\n _data: Map<string, unknown>;\n}\n\nexport function createMockStorageArea(): MockStorageArea {\n const data = new Map<string, unknown>();\n\n return {\n get: async <T = Record<string, unknown>>(\n keys?: string | string[] | Record<string, unknown> | null\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Mock storage needs to handle multiple key types\n ): Promise<T> => {\n if (!keys) {\n return Object.fromEntries(data) as unknown as T;\n }\n if (typeof keys === \"string\") {\n return (data.has(keys) ? { [keys]: data.get(keys) } : {}) as unknown as T;\n }\n if (Array.isArray(keys)) {\n const result: Record<string, unknown> = {};\n for (const key of keys) {\n if (data.has(key)) {\n result[key] = data.get(key);\n }\n }\n return result as unknown as T;\n }\n // Object with defaults\n const result: Record<string, unknown> = {};\n for (const [key, defaultValue] of Object.entries(keys)) {\n result[key] = data.has(key) ? data.get(key) : defaultValue;\n }\n return result as unknown as T;\n },\n set: async (items) => {\n for (const [key, value] of Object.entries(items)) {\n data.set(key, value);\n }\n },\n remove: async (keys) => {\n const keyArray = Array.isArray(keys) ? keys : [keys];\n for (const key of keyArray) {\n data.delete(key);\n }\n },\n clear: async () => {\n data.clear();\n },\n onChanged: (_callback: (changes: StorageChanges, areaName: string) => void) => {\n // Mock implementation - not needed for current tests\n },\n _data: data,\n };\n}\n",
11
11
  "import type { TabsAdapter } from \"@/shared/adapters/tabs.adapter\";\n\nexport interface MockTabs extends TabsAdapter {\n _tabs: Map<number, chrome.tabs.Tab>;\n}\n\nexport function createMockTabs(): MockTabs {\n const tabs = new Map<number, chrome.tabs.Tab>();\n\n return {\n query: async (queryInfo: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> => {\n const results: chrome.tabs.Tab[] = [];\n for (const tab of tabs.values()) {\n let matches = true;\n if (queryInfo.active !== undefined && tab.active !== queryInfo.active) {\n matches = false;\n }\n if (queryInfo.currentWindow !== undefined) {\n matches = false;\n }\n if (matches) {\n results.push(tab);\n }\n }\n return results;\n },\n get: async (tabId: number): Promise<chrome.tabs.Tab> => {\n const tab = tabs.get(tabId);\n if (!tab) {\n throw new Error(`Tab ${tabId} not found`);\n }\n return tab;\n },\n sendMessage: async (_tabId: number, _message: unknown): Promise<unknown> => {\n return Promise.resolve({ success: true });\n },\n reload: async (\n _tabId: number,\n _reloadProperties?: { bypassCache?: boolean }\n ): Promise<void> => {\n // Mock implementation\n },\n onRemoved: (\n _callback: (tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) => void\n ): void => {\n // Mock implementation - register listener\n },\n onUpdated: (\n _callback: (\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab\n ) => void\n ): void => {\n // Mock implementation - register listener\n },\n onActivated: (_callback: (activeInfo: { tabId: number; windowId: number }) => void): void => {\n // Mock implementation - register listener\n },\n create: async (createProperties: chrome.tabs.CreateProperties): Promise<chrome.tabs.Tab> => {\n const newTab: chrome.tabs.Tab = {\n id: Math.floor(Math.random() * 10000),\n index: tabs.size,\n pinned: false,\n highlighted: false,\n windowId: 1,\n active: true,\n incognito: false,\n selected: true,\n discarded: false,\n autoDiscardable: true,\n groupId: -1,\n url: createProperties.url || \"about:blank\",\n title: createProperties.url || \"New Tab\",\n frozen: false,\n };\n if (newTab.id !== undefined) {\n tabs.set(newTab.id, newTab);\n }\n return newTab;\n },\n _tabs: tabs,\n };\n}\n",
12
12
  "import type { WindowAdapter } from \"@/shared/adapters/window.adapter\";\n\nexport interface MockWindow extends WindowAdapter {\n _messageListeners: Set<(event: MessageEvent) => void>;\n}\n\nexport function createMockWindow(): MockWindow {\n const messageListeners = new Set<(event: MessageEvent) => void>();\n\n return {\n postMessage: (message: unknown, targetOrigin: string) => {\n const event = new MessageEvent(\"message\", {\n data: message,\n origin: targetOrigin,\n source: null,\n });\n for (const listener of messageListeners) {\n listener(event);\n }\n },\n addEventListener: (type: string, listener: (event: MessageEvent) => void) => {\n if (type === \"message\") {\n messageListeners.add(listener);\n }\n },\n removeEventListener: (type: string, listener: (event: MessageEvent) => void) => {\n if (type === \"message\") {\n messageListeners.delete(listener);\n }\n },\n _messageListeners: messageListeners,\n };\n}\n",
13
13
  "import { createMockContextMenus, type MockContextMenus } from \"./context-menus.mock\";\nimport { createMockFetch, type MockFetch } from \"./fetch.mock\";\nimport { createMockLogger, type MockLogger } from \"./logger.mock\";\nimport { createMockOffscreen, type MockOffscreen } from \"./offscreen.mock\";\nimport { createMockPort, createMockRuntime, type MockPort, type MockRuntime } from \"./runtime.mock\";\nimport { createMockStorageArea, type MockStorageArea } from \"./storage.mock\";\nimport { createMockTabs, type MockTabs } from \"./tabs.mock\";\nimport { createMockWindow, type MockWindow } from \"./window.mock\";\n\n/**\n * Mock adapters with full type information including mock-specific properties\n */\nexport interface MockExtensionAdapters {\n runtime: MockRuntime;\n storage: MockStorageArea;\n tabs: MockTabs;\n window: MockWindow;\n offscreen: MockOffscreen;\n contextMenus: MockContextMenus;\n fetch: MockFetch;\n logger: MockLogger;\n}\n\n/**\n * Convenience interface grouping Chrome-like mock APIs\n * Useful when tests need direct access to internal mock state\n */\nexport interface MockChrome {\n runtime: MockRuntime;\n storage: {\n local: MockStorageArea;\n };\n tabs: MockTabs;\n}\n\n/**\n * Create a mock Chrome object with grouped APIs\n * Use this when you need access to internal mock state (e.g., mockChrome.tabs._tabs)\n */\nexport function createMockChrome(): MockChrome {\n return {\n runtime: createMockRuntime(),\n storage: {\n local: createMockStorageArea(),\n },\n tabs: createMockTabs(),\n };\n}\n\n/**\n * Create a complete set of mock adapters for testing\n * Returns mock adapters with full type information\n */\nexport function createMockAdapters(): MockExtensionAdapters {\n return {\n runtime: createMockRuntime(),\n storage: createMockStorageArea(),\n tabs: createMockTabs(),\n window: createMockWindow(),\n offscreen: createMockOffscreen(),\n contextMenus: createMockContextMenus(),\n fetch: createMockFetch(),\n logger: createMockLogger({ silent: true }),\n };\n}\n\nexport type {\n MockContextMenus,\n MockFetch,\n MockLogger,\n MockOffscreen,\n MockPort,\n MockRuntime,\n MockStorageArea,\n MockTabs,\n MockWindow,\n};\n// Re-export individual mock factories and types for convenience\nexport {\n createMockContextMenus,\n createMockFetch,\n createMockLogger,\n createMockOffscreen,\n createMockPort,\n createMockRuntime,\n createMockStorageArea,\n createMockTabs,\n createMockWindow,\n};\n"
@@ -7,7 +7,7 @@
7
7
  "// Mock logger adapter for testing\nimport type { LoggerAdapter } from \"@/shared/adapters/logger.adapter\";\nimport type { LogLevel } from \"@/shared/types/messages\";\n\nexport interface LogCall {\n level: LogLevel;\n message: string;\n context?: Record<string, unknown>;\n error?: Error;\n timestamp: number;\n}\n\nexport interface MockLogger extends LoggerAdapter {\n _calls: LogCall[];\n _clear(): void;\n}\n\nexport function createMockLogger(options?: { silent?: boolean }): MockLogger {\n const calls: LogCall[] = [];\n const silent = options?.silent ?? true;\n\n const logToConsole = (level: LogLevel, message: string, context?: Record<string, unknown>) => {\n if (!silent) {\n // biome-ignore lint/suspicious/noConsole: Mock logger intentionally uses console for testing\n const consoleMethod = level === \"debug\" ? console.log : console[level];\n consoleMethod(message, context);\n }\n };\n\n return {\n debug(message: string, context?: Record<string, unknown>): void {\n calls.push({\n level: \"debug\",\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"debug\", message, context);\n },\n\n info(message: string, context?: Record<string, unknown>): void {\n calls.push({\n level: \"info\",\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"info\", message, context);\n },\n\n warn(message: string, context?: Record<string, unknown>): void {\n calls.push({\n level: \"warn\",\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"warn\", message, context);\n },\n\n error(message: string, error?: Error, context?: Record<string, unknown>): void {\n calls.push({\n level: \"error\",\n message,\n ...(error && { error }),\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(\"error\", message, { ...context, error });\n },\n\n log(level: LogLevel, message: string, context?: Record<string, unknown>): void {\n calls.push({\n level,\n message,\n ...(context && { context }),\n timestamp: Date.now(),\n });\n logToConsole(level, message, context);\n },\n\n // Test-only internals\n _calls: calls,\n _clear() {\n calls.length = 0;\n },\n };\n}\n",
8
8
  "import type {\n CreateOffscreenDocumentParameters,\n OffscreenAdapter,\n} from \"@/shared/adapters/offscreen.adapter\";\n\nexport interface MockOffscreen extends OffscreenAdapter {\n _hasDocument: boolean;\n}\n\nexport function createMockOffscreen(): MockOffscreen {\n let hasDocument = false;\n\n return {\n createDocument: async (_parameters: CreateOffscreenDocumentParameters): Promise<void> => {\n hasDocument = true;\n },\n closeDocument: async (): Promise<void> => {\n hasDocument = false;\n },\n hasDocument: async (): Promise<boolean> => {\n return hasDocument;\n },\n _hasDocument: hasDocument,\n };\n}\n",
9
9
  "import type { MessageSender, PortAdapter, RuntimeAdapter } from \"@/shared/adapters/runtime.adapter\";\n\nexport interface MockPort extends PortAdapter {\n _listeners: Set<(message: unknown) => void>;\n _disconnectListeners: Set<() => void>;\n}\n\nexport function createMockPort(name: string): MockPort {\n const listeners = new Set<(message: unknown) => void>();\n const disconnectListeners = new Set<() => void>();\n\n return {\n name,\n onMessage: (callback) => listeners.add(callback),\n onDisconnect: (callback) => disconnectListeners.add(callback),\n postMessage: (message) => {\n for (const listener of listeners) {\n listener(message);\n }\n },\n disconnect: () => {\n for (const listener of disconnectListeners) {\n listener();\n }\n },\n _listeners: listeners,\n _disconnectListeners: disconnectListeners,\n };\n}\n\nexport interface MockRuntime extends RuntimeAdapter {\n id: string;\n _messageListeners: Set<\n (message: unknown, sender: MessageSender, sendResponse: (response: unknown) => void) => void\n >;\n _connectListeners: Set<(port: PortAdapter) => void>;\n}\n\nexport function createMockRuntime(id = \"test-extension-id\"): MockRuntime {\n const messageListeners = new Set<\n (message: unknown, sender: MessageSender, sendResponse: (response: unknown) => void) => void\n >();\n const connectListeners = new Set<(port: PortAdapter) => void>();\n\n return {\n id,\n sendMessage: async <T>(message: T): Promise<unknown> => {\n // Check if this is a response message\n if (typeof message === \"object\" && message !== null && \"success\" in message) {\n // This is a response, route it back to all listeners\n for (const listener of messageListeners) {\n listener(message, { url: \"\" }, () => {\n // Empty response handler for mock\n });\n }\n return undefined;\n }\n\n // This is a request, call ALL listeners (Chrome calls all, but only first response is used)\n if (messageListeners.size === 0) {\n return undefined;\n }\n\n return new Promise((resolve) => {\n let resolved = false;\n const sharedSendResponse = (res: unknown) => {\n if (!resolved) {\n resolved = true;\n resolve(res);\n }\n };\n\n // Call all listeners (Chrome behavior)\n for (const listener of messageListeners) {\n const result = listener(message, { url: \"\" }, sharedSendResponse);\n // If listener returns true, it will send response asynchronously\n // If it returns false/undefined/void and we haven't resolved yet, continue to next listener\n if (typeof result === \"boolean\" && result === true) {\n // Listener will send response asynchronously, wait for it\n }\n }\n\n // If no listener handled it, resolve with undefined\n if (!resolved) {\n resolve(undefined);\n }\n });\n },\n onMessage: (\n callback: (\n message: unknown,\n sender: MessageSender,\n sendResponse: (response: unknown) => void\n ) => undefined | boolean\n ) => {\n messageListeners.add(callback);\n },\n removeMessageListener: (\n callback: (\n message: unknown,\n sender: MessageSender,\n sendResponse: (response: unknown) => void\n ) => undefined | boolean\n ) => {\n messageListeners.delete(callback);\n },\n connect: (name: string): PortAdapter => {\n const port = createMockPort(name);\n for (const listener of connectListeners) {\n listener(port);\n }\n return port;\n },\n onConnect: (callback: (port: PortAdapter) => void) => {\n connectListeners.add(callback);\n },\n getURL: (path: string): string => {\n return `chrome-extension://${id}/${path}`;\n },\n getId: (): string => {\n return id;\n },\n openOptionsPage: (): void => {\n // Mock implementation - no-op for tests\n },\n _messageListeners: messageListeners,\n _connectListeners: connectListeners,\n };\n}\n",
10
- "import type { StorageAdapter, StorageChanges } from \"@/shared/adapters/storage.adapter\";\n\nexport interface MockStorageArea extends StorageAdapter {\n _data: Map<string, unknown>;\n}\n\nexport function createMockStorageArea(): MockStorageArea {\n const data = new Map<string, unknown>();\n\n return {\n get: async <T = Record<string, unknown>>(\n keys?: string | string[] | Record<string, unknown> | null\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Mock storage needs to handle multiple key types\n ): Promise<T> => {\n if (!keys) {\n return Object.fromEntries(data) as T;\n }\n if (typeof keys === \"string\") {\n return (data.has(keys) ? { [keys]: data.get(keys) } : {}) as T;\n }\n if (Array.isArray(keys)) {\n const result: Record<string, unknown> = {};\n for (const key of keys) {\n if (data.has(key)) {\n result[key] = data.get(key);\n }\n }\n return result as unknown as T;\n }\n // Object with defaults\n const result: Record<string, unknown> = {};\n for (const [key, defaultValue] of Object.entries(keys)) {\n result[key] = data.has(key) ? data.get(key) : defaultValue;\n }\n return result as unknown as T;\n },\n set: async (items) => {\n for (const [key, value] of Object.entries(items)) {\n data.set(key, value);\n }\n },\n remove: async (keys) => {\n const keyArray = Array.isArray(keys) ? keys : [keys];\n for (const key of keyArray) {\n data.delete(key);\n }\n },\n clear: async () => {\n data.clear();\n },\n onChanged: (_callback: (changes: StorageChanges, areaName: string) => void) => {\n // Mock implementation - not needed for current tests\n },\n _data: data,\n };\n}\n",
10
+ "import type { StorageAdapter, StorageChanges } from \"@/shared/adapters/storage.adapter\";\n\nexport interface MockStorageArea extends StorageAdapter {\n _data: Map<string, unknown>;\n}\n\nexport function createMockStorageArea(): MockStorageArea {\n const data = new Map<string, unknown>();\n\n return {\n get: async <T = Record<string, unknown>>(\n keys?: string | string[] | Record<string, unknown> | null\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Mock storage needs to handle multiple key types\n ): Promise<T> => {\n if (!keys) {\n return Object.fromEntries(data) as unknown as T;\n }\n if (typeof keys === \"string\") {\n return (data.has(keys) ? { [keys]: data.get(keys) } : {}) as unknown as T;\n }\n if (Array.isArray(keys)) {\n const result: Record<string, unknown> = {};\n for (const key of keys) {\n if (data.has(key)) {\n result[key] = data.get(key);\n }\n }\n return result as unknown as T;\n }\n // Object with defaults\n const result: Record<string, unknown> = {};\n for (const [key, defaultValue] of Object.entries(keys)) {\n result[key] = data.has(key) ? data.get(key) : defaultValue;\n }\n return result as unknown as T;\n },\n set: async (items) => {\n for (const [key, value] of Object.entries(items)) {\n data.set(key, value);\n }\n },\n remove: async (keys) => {\n const keyArray = Array.isArray(keys) ? keys : [keys];\n for (const key of keyArray) {\n data.delete(key);\n }\n },\n clear: async () => {\n data.clear();\n },\n onChanged: (_callback: (changes: StorageChanges, areaName: string) => void) => {\n // Mock implementation - not needed for current tests\n },\n _data: data,\n };\n}\n",
11
11
  "import type { TabsAdapter } from \"@/shared/adapters/tabs.adapter\";\n\nexport interface MockTabs extends TabsAdapter {\n _tabs: Map<number, chrome.tabs.Tab>;\n}\n\nexport function createMockTabs(): MockTabs {\n const tabs = new Map<number, chrome.tabs.Tab>();\n\n return {\n query: async (queryInfo: chrome.tabs.QueryInfo): Promise<chrome.tabs.Tab[]> => {\n const results: chrome.tabs.Tab[] = [];\n for (const tab of tabs.values()) {\n let matches = true;\n if (queryInfo.active !== undefined && tab.active !== queryInfo.active) {\n matches = false;\n }\n if (queryInfo.currentWindow !== undefined) {\n matches = false;\n }\n if (matches) {\n results.push(tab);\n }\n }\n return results;\n },\n get: async (tabId: number): Promise<chrome.tabs.Tab> => {\n const tab = tabs.get(tabId);\n if (!tab) {\n throw new Error(`Tab ${tabId} not found`);\n }\n return tab;\n },\n sendMessage: async (_tabId: number, _message: unknown): Promise<unknown> => {\n return Promise.resolve({ success: true });\n },\n reload: async (\n _tabId: number,\n _reloadProperties?: { bypassCache?: boolean }\n ): Promise<void> => {\n // Mock implementation\n },\n onRemoved: (\n _callback: (tabId: number, removeInfo: chrome.tabs.OnRemovedInfo) => void\n ): void => {\n // Mock implementation - register listener\n },\n onUpdated: (\n _callback: (\n tabId: number,\n changeInfo: chrome.tabs.OnUpdatedInfo,\n tab: chrome.tabs.Tab\n ) => void\n ): void => {\n // Mock implementation - register listener\n },\n onActivated: (_callback: (activeInfo: { tabId: number; windowId: number }) => void): void => {\n // Mock implementation - register listener\n },\n create: async (createProperties: chrome.tabs.CreateProperties): Promise<chrome.tabs.Tab> => {\n const newTab: chrome.tabs.Tab = {\n id: Math.floor(Math.random() * 10000),\n index: tabs.size,\n pinned: false,\n highlighted: false,\n windowId: 1,\n active: true,\n incognito: false,\n selected: true,\n discarded: false,\n autoDiscardable: true,\n groupId: -1,\n url: createProperties.url || \"about:blank\",\n title: createProperties.url || \"New Tab\",\n frozen: false,\n };\n if (newTab.id !== undefined) {\n tabs.set(newTab.id, newTab);\n }\n return newTab;\n },\n _tabs: tabs,\n };\n}\n",
12
12
  "import type { WindowAdapter } from \"@/shared/adapters/window.adapter\";\n\nexport interface MockWindow extends WindowAdapter {\n _messageListeners: Set<(event: MessageEvent) => void>;\n}\n\nexport function createMockWindow(): MockWindow {\n const messageListeners = new Set<(event: MessageEvent) => void>();\n\n return {\n postMessage: (message: unknown, targetOrigin: string) => {\n const event = new MessageEvent(\"message\", {\n data: message,\n origin: targetOrigin,\n source: null,\n });\n for (const listener of messageListeners) {\n listener(event);\n }\n },\n addEventListener: (type: string, listener: (event: MessageEvent) => void) => {\n if (type === \"message\") {\n messageListeners.add(listener);\n }\n },\n removeEventListener: (type: string, listener: (event: MessageEvent) => void) => {\n if (type === \"message\") {\n messageListeners.delete(listener);\n }\n },\n _messageListeners: messageListeners,\n };\n}\n",
13
13
  "import { createMockContextMenus, type MockContextMenus } from \"./context-menus.mock\";\nimport { createMockFetch, type MockFetch } from \"./fetch.mock\";\nimport { createMockLogger, type MockLogger } from \"./logger.mock\";\nimport { createMockOffscreen, type MockOffscreen } from \"./offscreen.mock\";\nimport { createMockPort, createMockRuntime, type MockPort, type MockRuntime } from \"./runtime.mock\";\nimport { createMockStorageArea, type MockStorageArea } from \"./storage.mock\";\nimport { createMockTabs, type MockTabs } from \"./tabs.mock\";\nimport { createMockWindow, type MockWindow } from \"./window.mock\";\n\n/**\n * Mock adapters with full type information including mock-specific properties\n */\nexport interface MockExtensionAdapters {\n runtime: MockRuntime;\n storage: MockStorageArea;\n tabs: MockTabs;\n window: MockWindow;\n offscreen: MockOffscreen;\n contextMenus: MockContextMenus;\n fetch: MockFetch;\n logger: MockLogger;\n}\n\n/**\n * Convenience interface grouping Chrome-like mock APIs\n * Useful when tests need direct access to internal mock state\n */\nexport interface MockChrome {\n runtime: MockRuntime;\n storage: {\n local: MockStorageArea;\n };\n tabs: MockTabs;\n}\n\n/**\n * Create a mock Chrome object with grouped APIs\n * Use this when you need access to internal mock state (e.g., mockChrome.tabs._tabs)\n */\nexport function createMockChrome(): MockChrome {\n return {\n runtime: createMockRuntime(),\n storage: {\n local: createMockStorageArea(),\n },\n tabs: createMockTabs(),\n };\n}\n\n/**\n * Create a complete set of mock adapters for testing\n * Returns mock adapters with full type information\n */\nexport function createMockAdapters(): MockExtensionAdapters {\n return {\n runtime: createMockRuntime(),\n storage: createMockStorageArea(),\n tabs: createMockTabs(),\n window: createMockWindow(),\n offscreen: createMockOffscreen(),\n contextMenus: createMockContextMenus(),\n fetch: createMockFetch(),\n logger: createMockLogger({ silent: true }),\n };\n}\n\nexport type {\n MockContextMenus,\n MockFetch,\n MockLogger,\n MockOffscreen,\n MockPort,\n MockRuntime,\n MockStorageArea,\n MockTabs,\n MockWindow,\n};\n// Re-export individual mock factories and types for convenience\nexport {\n createMockContextMenus,\n createMockFetch,\n createMockLogger,\n createMockOffscreen,\n createMockPort,\n createMockRuntime,\n createMockStorageArea,\n createMockTabs,\n createMockWindow,\n};\n",
@@ -13351,13 +13351,14 @@ var require_png = __commonJS((exports) => {
13351
13351
  // tools/test/src/visual/compare.ts
13352
13352
  var {readFileSync, writeFileSync} = (() => ({}));
13353
13353
 
13354
- // node_modules/.bun/pixelmatch@7.1.0/node_modules/pixelmatch/index.js
13354
+ // node_modules/.bun/pixelmatch@7.2.0/node_modules/pixelmatch/index.js
13355
13355
  function pixelmatch(img1, img2, output, width, height, options = {}) {
13356
13356
  const {
13357
13357
  threshold = 0.1,
13358
13358
  alpha = 0.1,
13359
13359
  aaColor = [255, 255, 0],
13360
13360
  diffColor = [255, 0, 0],
13361
+ checkerboard = true,
13361
13362
  includeAA,
13362
13363
  diffColorAlt,
13363
13364
  diffMask
@@ -13365,9 +13366,9 @@ function pixelmatch(img1, img2, output, width, height, options = {}) {
13365
13366
  if (!isPixelData(img1) || !isPixelData(img2) || output && !isPixelData(output))
13366
13367
  throw new Error("Image data: Uint8Array, Uint8ClampedArray or Buffer expected.");
13367
13368
  if (img1.length !== img2.length || output && output.length !== img1.length)
13368
- throw new Error("Image sizes do not match.");
13369
+ throw new Error(`Image sizes do not match. Image 1 size: ${img1.length}, image 2 size: ${img2.length}`);
13369
13370
  if (img1.length !== width * height * 4)
13370
- throw new Error("Image data size does not match width/height.");
13371
+ throw new Error(`Image data size does not match width/height. Expecting ${width * height * 4}. Got ${img1.length}`);
13371
13372
  const len = width * height;
13372
13373
  const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
13373
13374
  const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len);
@@ -13380,8 +13381,8 @@ function pixelmatch(img1, img2, output, width, height, options = {}) {
13380
13381
  }
13381
13382
  if (identical) {
13382
13383
  if (output && !diffMask) {
13383
- for (let i = 0;i < len; i++)
13384
- drawGrayPixel(img1, 4 * i, alpha, output);
13384
+ for (let i = 0, pos = 0;i < len; i++, pos += 4)
13385
+ drawGrayPixel(img1, pos, alpha, output);
13385
13386
  }
13386
13387
  return 0;
13387
13388
  }
@@ -13390,29 +13391,27 @@ function pixelmatch(img1, img2, output, width, height, options = {}) {
13390
13391
  const [diffR, diffG, diffB] = diffColor;
13391
13392
  const [altR, altG, altB] = diffColorAlt || diffColor;
13392
13393
  let diff = 0;
13393
- for (let y = 0;y < height; y++) {
13394
- for (let x = 0;x < width; x++) {
13395
- const i = y * width + x;
13396
- const pos = i * 4;
13397
- const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, false);
13398
- if (Math.abs(delta) > maxDelta) {
13399
- const isAA = antialiased(img1, x, y, width, height, a32, b32) || antialiased(img2, x, y, width, height, b32, a32);
13400
- if (!includeAA && isAA) {
13401
- if (output && !diffMask)
13402
- drawPixel(output, pos, aaR, aaG, aaB);
13403
- } else {
13404
- if (output) {
13405
- if (delta < 0) {
13406
- drawPixel(output, pos, altR, altG, altB);
13407
- } else {
13408
- drawPixel(output, pos, diffR, diffG, diffB);
13409
- }
13394
+ for (let i = 0, pos = 0;i < len; i++, pos += 4) {
13395
+ const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, checkerboard);
13396
+ if (Math.abs(delta) > maxDelta) {
13397
+ const x = i % width;
13398
+ const y = i / width | 0;
13399
+ const isExcludedAA = !includeAA && (antialiased(img1, x, y, width, height, a32, b32, checkerboard) || antialiased(img2, x, y, width, height, b32, a32, checkerboard));
13400
+ if (isExcludedAA) {
13401
+ if (output && !diffMask)
13402
+ drawPixel(output, pos, aaR, aaG, aaB);
13403
+ } else {
13404
+ if (output) {
13405
+ if (delta < 0) {
13406
+ drawPixel(output, pos, altR, altG, altB);
13407
+ } else {
13408
+ drawPixel(output, pos, diffR, diffG, diffB);
13410
13409
  }
13411
- diff++;
13412
13410
  }
13413
- } else if (output && !diffMask) {
13414
- drawGrayPixel(img1, pos, alpha, output);
13411
+ diff++;
13415
13412
  }
13413
+ } else if (output && !diffMask) {
13414
+ drawGrayPixel(img1, pos, alpha, output);
13416
13415
  }
13417
13416
  }
13418
13417
  return diff;
@@ -13420,12 +13419,16 @@ function pixelmatch(img1, img2, output, width, height, options = {}) {
13420
13419
  function isPixelData(arr) {
13421
13420
  return ArrayBuffer.isView(arr) && arr.BYTES_PER_ELEMENT === 1;
13422
13421
  }
13423
- function antialiased(img, x1, y1, width, height, a32, b32) {
13422
+ function antialiased(img, x1, y1, width, height, a32, b32, checkerboard) {
13424
13423
  const x0 = Math.max(x1 - 1, 0);
13425
13424
  const y0 = Math.max(y1 - 1, 0);
13426
13425
  const x2 = Math.min(x1 + 1, width - 1);
13427
13426
  const y2 = Math.min(y1 + 1, height - 1);
13428
- const pos = y1 * width + x1;
13427
+ const pos4 = (y1 * width + x1) * 4;
13428
+ const cr = img[pos4];
13429
+ const cg = img[pos4 + 1];
13430
+ const cb = img[pos4 + 2];
13431
+ const ca = img[pos4 + 3];
13429
13432
  let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
13430
13433
  let min = 0;
13431
13434
  let max = 0;
@@ -13437,7 +13440,7 @@ function antialiased(img, x1, y1, width, height, a32, b32) {
13437
13440
  for (let y = y0;y <= y2; y++) {
13438
13441
  if (x === x1 && y === y1)
13439
13442
  continue;
13440
- const delta = colorDelta(img, img, pos * 4, (y * width + x) * 4, true);
13443
+ const delta = brightnessDelta(img, pos4, (y * width + x) * 4, cr, cg, cb, ca, checkerboard);
13441
13444
  if (delta === 0) {
13442
13445
  zeroes++;
13443
13446
  if (zeroes > 2)
@@ -13475,7 +13478,7 @@ function hasManySiblings(img, x1, y1, width, height) {
13475
13478
  }
13476
13479
  return false;
13477
13480
  }
13478
- function colorDelta(img1, img2, k, m, yOnly) {
13481
+ function colorDelta(img1, img2, k, m, checkerboard) {
13479
13482
  const r1 = img1[k];
13480
13483
  const g1 = img1[k + 1];
13481
13484
  const b1 = img1[k + 2];
@@ -13488,26 +13491,49 @@ function colorDelta(img1, img2, k, m, yOnly) {
13488
13491
  let dg = g1 - g2;
13489
13492
  let db = b1 - b2;
13490
13493
  const da = a1 - a2;
13491
- if (!dr && !dg && !db && !da)
13492
- return 0;
13493
13494
  if (a1 < 255 || a2 < 255) {
13494
- const rb = 48 + 159 * (k % 2);
13495
- const gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
13496
- const bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
13495
+ let rb = 255, gb = 255, bb = 255;
13496
+ if (checkerboard) {
13497
+ rb = 48 + 159 * (k % 2);
13498
+ gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
13499
+ bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
13500
+ }
13497
13501
  dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
13498
13502
  dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
13499
13503
  db = (b1 * a1 - b2 * a2 - bb * da) / 255;
13500
13504
  }
13501
13505
  const y = dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
13502
- if (yOnly)
13503
- return y;
13504
13506
  const i = dr * 0.59597799 - dg * 0.2741761 - db * 0.32180189;
13505
13507
  const q = dr * 0.21147017 - dg * 0.52261711 + db * 0.31114694;
13506
13508
  const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
13507
13509
  return y > 0 ? -delta : delta;
13508
13510
  }
13511
+ function brightnessDelta(img, k, m, r1, g1, b1, a1, checkerboard) {
13512
+ const r2 = img[m];
13513
+ const g2 = img[m + 1];
13514
+ const b2 = img[m + 2];
13515
+ const a2 = img[m + 3];
13516
+ let dr = r1 - r2;
13517
+ let dg = g1 - g2;
13518
+ let db = b1 - b2;
13519
+ const da = a1 - a2;
13520
+ if (!dr && !dg && !db && !da)
13521
+ return 0;
13522
+ if (a1 < 255 || a2 < 255) {
13523
+ let rb = 255, gb = 255, bb = 255;
13524
+ if (checkerboard) {
13525
+ rb = 48 + 159 * (k % 2);
13526
+ gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
13527
+ bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
13528
+ }
13529
+ dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
13530
+ dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
13531
+ db = (b1 * a1 - b2 * a2 - bb * da) / 255;
13532
+ }
13533
+ return dr * 0.29889531 + dg * 0.58662247 + db * 0.11448223;
13534
+ }
13509
13535
  function drawPixel(output, pos, r, g, b) {
13510
- output[pos + 0] = r;
13536
+ output[pos] = r;
13511
13537
  output[pos + 1] = g;
13512
13538
  output[pos + 2] = b;
13513
13539
  output[pos + 3] = 255;
@@ -13965,4 +13991,4 @@ export {
13965
13991
  assertSafeUpdateMode
13966
13992
  };
13967
13993
 
13968
- //# debugId=5C6BB1754AE9201464756E2164756E21
13994
+ //# debugId=A8F0CA1BEDC12C8F64756E2164756E21