diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 09f91e6..fade885 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,10 +16,24 @@ jobs: - run: yarn install - run: yarn lint-test - build: + test: runs-on: ubuntu-latest env: CI_JOB_NUMBER: 2 + steps: + - uses: actions/checkout@v1 + - name: Use Node.js from .nvmrc + uses: actions/setup-node@v6 + with: + node-version-file: '.nvmrc' + - run: corepack enable + - run: yarn install + - run: yarn workspace @hawk.so/javascript test + + build: + runs-on: ubuntu-latest + env: + CI_JOB_NUMBER: 3 steps: - uses: actions/checkout@v1 with: diff --git a/packages/javascript/README.md b/packages/javascript/README.md index c541694..8cd86fc 100644 --- a/packages/javascript/README.md +++ b/packages/javascript/README.md @@ -89,7 +89,7 @@ Initialization settings: | `disableVueErrorHandler` | boolean | optional | Do not initialize Vue errors handling | | `consoleTracking` | boolean | optional | Initialize console logs tracking | | `breadcrumbs` | false or BreadcrumbsOptions object | optional | Configure breadcrumbs tracking (see below) | -| `beforeSend` | function(event) => event | optional | This Method allows you to filter any data you don't want sending to Hawk | +| `beforeSend` | function(event) => event \| false \| void | optional | Filter data before sending. Return modified event, `false` to drop the event. | Other available [initial settings](types/hawk-initial-settings.d.ts) are described at the type definition. @@ -187,7 +187,7 @@ const hawk = new HawkCatcher({ beforeBreadcrumb: (breadcrumb, hint) => { // Filter or modify breadcrumbs before storing if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) { - return null; // Discard this breadcrumb + return false; // Discard this breadcrumb } return breadcrumb; } @@ -203,7 +203,7 @@ const hawk = new HawkCatcher({ | `trackFetch` | `boolean` | `true` | Automatically track `fetch()` and `XMLHttpRequest` calls as breadcrumbs. Captures request URL, method, status code, and response time. | | `trackNavigation` | `boolean` | `true` | Automatically track navigation events (History API: `pushState`, `replaceState`, `popstate`). Captures route changes. | | `trackClicks` | `boolean` | `true` | Automatically track UI click events. Captures element selector, coordinates, and other click metadata. | -| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)` and can return modified breadcrumb, `null` to discard it, or the original breadcrumb. Useful for filtering sensitive data or PII. | +| `beforeBreadcrumb` | `function` | `undefined` | Hook called before each breadcrumb is stored. Receives `(breadcrumb, hint)`. Return modified breadcrumb to keep it, `false` to discard. | ### Manual Breadcrumbs diff --git a/packages/javascript/example/index.html b/packages/javascript/example/index.html index 91f6185..c8a050b 100644 --- a/packages/javascript/example/index.html +++ b/packages/javascript/example/index.html @@ -312,7 +312,7 @@

Test Vue integration: <test-component>

// beforeBreadcrumb: (breadcrumb, hint) => { // // Filter or modify breadcrumbs before storing // if (breadcrumb.category === 'fetch' && breadcrumb.data?.url?.includes('/sensitive')) { - // return null; // Discard this breadcrumb + // return false; // Discard this breadcrumb // } // return breadcrumb; // } diff --git a/packages/javascript/package.json b/packages/javascript/package.json index a9d4b44..dfb7b94 100644 --- a/packages/javascript/package.json +++ b/packages/javascript/package.json @@ -1,6 +1,6 @@ { "name": "@hawk.so/javascript", - "version": "3.2.15", + "version": "3.2.16", "description": "JavaScript errors tracking for Hawk.so", "files": [ "dist" @@ -20,6 +20,8 @@ "dev": "vite", "build": "vite build", "stats": "size-limit > stats.txt", + "test": "vitest run", + "test:watch": "vitest", "lint": "eslint --fix \"src/**/*.{js,ts}\"" }, "repository": { @@ -40,9 +42,11 @@ "error-stack-parser": "^2.1.4" }, "devDependencies": { - "@hawk.so/types": "0.5.2", + "@hawk.so/types": "0.5.8", + "jsdom": "^28.0.0", "vite": "^7.3.1", "vite-plugin-dts": "^4.2.4", + "vitest": "^4.0.18", "vue": "^2" } } diff --git a/packages/javascript/src/addons/breadcrumbs.ts b/packages/javascript/src/addons/breadcrumbs.ts index 8c5361a..10a7a96 100644 --- a/packages/javascript/src/addons/breadcrumbs.ts +++ b/packages/javascript/src/addons/breadcrumbs.ts @@ -5,6 +5,7 @@ import type { Breadcrumb, BreadcrumbLevel, BreadcrumbType, Json, JsonNode } from import Sanitizer from '../modules/sanitizer'; import { buildElementSelector } from '../utils/selector'; import log from '../utils/log'; +import { isValidBreadcrumb } from '../utils/validation'; /** * Default maximum number of breadcrumbs to store @@ -48,11 +49,13 @@ export interface BreadcrumbsOptions { maxBreadcrumbs?: number; /** - * Hook called before each breadcrumb is stored - * Return null to discard the breadcrumb - * Return modified breadcrumb to store it + * Hook called before each breadcrumb is stored. + * - Return modified breadcrumb — it will be stored instead of the original. + * - Return `false` — the breadcrumb will be discarded. + * - Return nothing (`void` / `undefined` / `null`) — the original breadcrumb is stored as-is (a warning is logged). + * - If the hook returns an invalid value, a warning is logged and the original breadcrumb is stored. */ - beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null; + beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void; /** * Enable automatic fetch/XHR breadcrumbs @@ -91,7 +94,7 @@ interface InternalBreadcrumbsOptions { trackFetch: boolean; trackNavigation: boolean; trackClicks: boolean; - beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | null; + beforeBreadcrumb?: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => Breadcrumb | false | void; } /** @@ -233,16 +236,30 @@ export class BreadcrumbManager { * Apply beforeBreadcrumb hook */ if (this.options.beforeBreadcrumb) { - const result = this.options.beforeBreadcrumb(bc, hint); + const breadcrumbClone = structuredClone(bc); + const result = this.options.beforeBreadcrumb(breadcrumbClone, hint); - if (result === null) { - /** - * Discard breadcrumb - */ + /** + * false means discard + */ + if (result === false) { return; } - Object.assign(bc, result); + /** + * Valid breadcrumb → apply changes from hook + */ + if (isValidBreadcrumb(result)) { + Object.assign(bc, result); + } else { + /** + * Anything else is invalid — warn, bc stays untouched (hook only received a clone) + */ + log( + 'Invalid beforeBreadcrumb value. It should return breadcrumb or false. Breadcrumb is stored without changes.', + 'warn' + ); + } } /** diff --git a/packages/javascript/src/addons/consoleCatcher.ts b/packages/javascript/src/addons/consoleCatcher.ts index 16815ab..29519ea 100644 --- a/packages/javascript/src/addons/consoleCatcher.ts +++ b/packages/javascript/src/addons/consoleCatcher.ts @@ -150,7 +150,7 @@ export class ConsoleCatcher { * This ensures DevTools will navigate to the user's code, not the interceptor's code. * * @param errorStack - Full stack trace string from Error.stack - * @returns Object with userStack (full stack from user code) and fileLine (first frame for DevTools link) + * @returns {object} Object with userStack (full stack from user code) and fileLine (first frame for DevTools link) */ private extractUserStack(errorStack: string | undefined): { userStack: string; @@ -250,7 +250,7 @@ export class ConsoleCatcher { * 4. Store it in the buffer * 5. Forward the call to the native console (so output still appears in DevTools) * - * @param {...any} args + * @param args - console method arguments */ window.console[method] = (...args: unknown[]): void => { // Capture full stack trace and extract user code stack diff --git a/packages/javascript/src/catcher.ts b/packages/javascript/src/catcher.ts index 313ba93..0646507 100644 --- a/packages/javascript/src/catcher.ts +++ b/packages/javascript/src/catcher.ts @@ -2,7 +2,7 @@ import Socket from './modules/socket'; import Sanitizer from './modules/sanitizer'; import log from './utils/log'; import StackParser from './modules/stackParser'; -import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI } from './types'; +import type { CatcherMessage, HawkInitialSettings, BreadcrumbsAPI, Transport } from './types'; import { VueIntegration } from './integrations/vue'; import { id } from './utils/id'; import type { @@ -10,7 +10,7 @@ import type { EventContext, JavaScriptAddons, VueIntegrationAddons, - Json, EncodedIntegrationToken, DecodedIntegrationToken, + Json, EncodedIntegrationToken, DecodedIntegrationToken } from '@hawk.so/types'; import type { JavaScriptCatcherIntegrations } from './types/integrations'; import { EventRejectedError } from './errors'; @@ -18,7 +18,7 @@ import type { HawkJavaScriptEvent } from './types'; import { isErrorProcessed, markErrorAsProcessed } from './utils/event'; import { ConsoleCatcher } from './addons/consoleCatcher'; import { BreadcrumbManager } from './addons/breadcrumbs'; -import { validateUser, validateContext } from './utils/validation'; +import { validateUser, validateContext, isValidEventPayload } from './utils/validation'; /** * Allow to use global VERSION, that will be overwritten by Webpack @@ -73,16 +73,18 @@ export default class Catcher { private context: EventContext | undefined; /** - * This Method allows developer to filter any data you don't want sending to Hawk - * If method returns false, event will not be sent + * This Method allows developer to filter any data you don't want sending to Hawk. + * - Return modified event — it will be sent instead of the original. + * - Return `false` — the event will be dropped entirely. + * - Any other value is invalid — the original event is sent as-is (a warning is logged). */ - private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false); + private readonly beforeSend: undefined | ((event: HawkJavaScriptEvent) => HawkJavaScriptEvent | false | void); /** * Transport for dialog between Catcher and Collector - * (WebSocket decorator) + * (WebSocket decorator by default, or custom via settings.transport) */ - private readonly transport: Socket; + private readonly transport: Transport; /** * Module for parsing backtrace @@ -148,7 +150,7 @@ export default class Catcher { /** * Init transport */ - this.transport = new Socket({ + this.transport = settings.transport ?? new Socket({ collectorEndpoint: settings.collectorEndpoint || `wss://${this.getIntegrationId()}.k1.hawk.so:443/ws`, reconnectionAttempts: settings.reconnectionAttempts, reconnectionTimeout: settings.reconnectionTimeout, @@ -436,12 +438,29 @@ export default class Catcher { * Filter sensitive data */ if (typeof this.beforeSend === 'function') { - const beforeSendResult = this.beforeSend(payload); + const eventPayloadClone = structuredClone(payload); + const result = this.beforeSend(eventPayloadClone); - if (beforeSendResult === false) { + /** + * false → drop event + */ + if (result === false) { throw new EventRejectedError('Event rejected by beforeSend method.'); + } + + /** + * Valid event payload → use it instead of original + */ + if (isValidEventPayload(result)) { + payload = result as HawkJavaScriptEvent; } else { - payload = beforeSendResult; + /** + * Anything else is invalid — warn, payload stays untouched (hook only received a clone) + */ + log( + 'Invalid beforeSend value. It should return event or false. Event is sent without changes.', + 'warn' + ); } } diff --git a/packages/javascript/src/modules/socket.ts b/packages/javascript/src/modules/socket.ts index c07af1d..eb64b59 100644 --- a/packages/javascript/src/modules/socket.ts +++ b/packages/javascript/src/modules/socket.ts @@ -1,12 +1,13 @@ import log from '../utils/log'; import type { CatcherMessage } from '@/types'; +import type { Transport } from '../types/transport'; /** * Custom WebSocket wrapper class * * @copyright CodeX */ -export default class Socket { +export default class Socket implements Transport { /** * Socket connection endpoint */ diff --git a/packages/javascript/src/types/hawk-initial-settings.ts b/packages/javascript/src/types/hawk-initial-settings.ts index 51e80f0..987cdf4 100644 --- a/packages/javascript/src/types/hawk-initial-settings.ts +++ b/packages/javascript/src/types/hawk-initial-settings.ts @@ -1,5 +1,6 @@ import type { EventContext, AffectedUser } from '@hawk.so/types'; import type { HawkJavaScriptEvent } from './event'; +import type { Transport } from './transport'; import type { BreadcrumbsOptions } from '../addons/breadcrumbs'; /** @@ -65,10 +66,11 @@ export interface HawkInitialSettings { /** * This Method allows you to filter any data you don't want sending to Hawk. - * - * Return `false` to prevent the event from being sent to Hawk. + * - Return modified event — it will be sent instead of the original. + * - Return `false` — the event will be dropped entirely. + * - Any other value is invalid — the original event is sent as-is (a warning is logged). */ - beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false; + beforeSend?(event: HawkJavaScriptEvent): HawkJavaScriptEvent | false | void; /** * Disable Vue.js error handler @@ -90,4 +92,10 @@ export interface HawkInitialSettings { * @default enabled with default options */ breadcrumbs?: false | BreadcrumbsOptions; + + /** + * Custom transport for sending events. + * If not provided, default WebSocket transport is used. + */ + transport?: Transport; } diff --git a/packages/javascript/src/types/index.ts b/packages/javascript/src/types/index.ts index ef0556e..f3354c3 100644 --- a/packages/javascript/src/types/index.ts +++ b/packages/javascript/src/types/index.ts @@ -1,5 +1,6 @@ import type { CatcherMessage } from './catcher-message'; import type { HawkInitialSettings } from './hawk-initial-settings'; +import type { Transport } from './transport'; import type { HawkJavaScriptEvent } from './event'; import type { VueIntegrationData, NuxtIntegrationData, NuxtIntegrationAddons, JavaScriptCatcherIntegrations } from './integrations'; import type { BreadcrumbsAPI } from './breadcrumbs-api'; @@ -7,6 +8,7 @@ import type { BreadcrumbsAPI } from './breadcrumbs-api'; export type { CatcherMessage, HawkInitialSettings, + Transport, HawkJavaScriptEvent, VueIntegrationData, NuxtIntegrationData, diff --git a/packages/javascript/src/types/transport.ts b/packages/javascript/src/types/transport.ts new file mode 100644 index 0000000..f2237dc --- /dev/null +++ b/packages/javascript/src/types/transport.ts @@ -0,0 +1,8 @@ +import type { CatcherMessage } from './catcher-message'; + +/** + * Transport interface — anything that can send a CatcherMessage + */ +export interface Transport { + send(message: CatcherMessage): Promise; +} diff --git a/packages/javascript/src/utils/selector.ts b/packages/javascript/src/utils/selector.ts index d886884..f14aedb 100644 --- a/packages/javascript/src/utils/selector.ts +++ b/packages/javascript/src/utils/selector.ts @@ -4,13 +4,14 @@ * * @param element - HTML element to build selector from * @param maxDepth - Maximum recursion depth (default: 3) - * @returns CSS selector string (e.g., "div#myId.class1.class2" or ".some-parent button") + * @returns {string} CSS selector string (e.g., "div#myId.class1.class2" or ".some-parent button") */ export function buildElementSelector(element: HTMLElement, maxDepth: number = 3): string { let selector = element.tagName.toLowerCase(); if (element.id) { selector += `#${element.id}`; + return selector; } @@ -23,7 +24,9 @@ export function buildElementSelector(element: HTMLElement, maxDepth: number = 3) const classNameStr = String(element.className); if (classNameStr) { - selector += `.${classNameStr.split(' ').filter(Boolean).join('.')}`; + selector += `.${classNameStr.split(' ').filter(Boolean) + .join('.')}`; + return selector; } } diff --git a/packages/javascript/src/utils/validation.ts b/packages/javascript/src/utils/validation.ts index 8db0e0b..c0f9f66 100644 --- a/packages/javascript/src/utils/validation.ts +++ b/packages/javascript/src/utils/validation.ts @@ -1,11 +1,11 @@ import log from './log'; -import type { AffectedUser, EventContext } from '@hawk.so/types'; +import type { AffectedUser, Breadcrumb, EventContext, EventData, JavaScriptAddons } from '@hawk.so/types'; import Sanitizer from '../modules/sanitizer'; /** * Validates user data - basic security checks * - * @param user + * @param user - user data to validate */ export function validateUser(user: AffectedUser): boolean { if (!user || !Sanitizer.isObject(user)) { @@ -27,7 +27,7 @@ export function validateUser(user: AffectedUser): boolean { /** * Validates context data - basic security checks * - * @param context + * @param context - context data to validate */ export function validateContext(context: EventContext | undefined): boolean { if (context && !Sanitizer.isObject(context)) { @@ -38,3 +38,57 @@ export function validateContext(context: EventContext | undefined): boolean { return true; } + +/** + * Checks if value is a plain object (not array, Date, etc.) + * + * @param value - value to check + */ +function isPlainObject(value: unknown): value is Record { + return Object.prototype.toString.call(value) === '[object Object]'; +} + +/** + * Runtime check for required EventData fields. + * Per @hawk.so/types EventData, `title` is the only non-optional field. + * Additionally validates `backtrace` shape if present (must be an array). + * + * @param payload - value to validate + */ +export function isValidEventPayload(payload: unknown): payload is EventData { + if (!isPlainObject(payload)) { + return false; + } + + if (typeof payload.title !== 'string' || payload.title.trim() === '') { + return false; + } + + if (payload.backtrace !== undefined && !Array.isArray(payload.backtrace)) { + return false; + } + + return true; +} + +/** + * Runtime check that value is a valid Breadcrumb-like object. + * Must be a plain object with a string message and numeric timestamp. + * + * @param breadcrumb - value to validate + */ +export function isValidBreadcrumb(breadcrumb: unknown): breadcrumb is Breadcrumb { + if (!isPlainObject(breadcrumb)) { + return false; + } + + if (typeof breadcrumb.message !== 'string' || breadcrumb.message.trim() === '') { + return false; + } + + if (breadcrumb.timestamp !== undefined && typeof breadcrumb.timestamp !== 'number') { + return false; + } + + return true; +} diff --git a/packages/javascript/tests/before-send.test.ts b/packages/javascript/tests/before-send.test.ts new file mode 100644 index 0000000..e9652a1 --- /dev/null +++ b/packages/javascript/tests/before-send.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import type { CatcherMessage } from '../src/types/catcher-message'; +import type { Transport } from '../src/types/transport'; +import type { HawkJavaScriptEvent } from '../src/types/event'; +import Catcher from '../src/catcher'; + +const TEST_TOKEN = 'eyJpbnRlZ3JhdGlvbklkIjoiOTU3MmQyOWQtNWJhZS00YmYyLTkwN2MtZDk5ZDg5MGIwOTVmIiwic2VjcmV0IjoiZTExODFiZWItMjdlMS00ZDViLWEwZmEtZmUwYTM1Mzg5OWMyIn0='; +/** + * Wait for fire-and-forget async calls inside hawk.send() to complete + */ +const wait = (): Promise => new Promise((r) => setTimeout(r, 0)); + +function createTransport() { + const sendSpy = vi.fn<(msg: CatcherMessage) => Promise>().mockResolvedValue(undefined); + const transport: Transport = { send: sendSpy }; + + return { sendSpy, transport }; +} + +function getSentPayload(spy: ReturnType): HawkJavaScriptEvent | null { + const calls = spy.mock.calls; + + return calls.length ? calls[calls.length - 1][0].payload : null; +} + +/** + * Shared Catcher config — no breadcrumbs, no global handlers, fake transport + */ +function createCatcher(transport: Transport, beforeSend: NonNullable[0] extends object ? ConstructorParameters[0]['beforeSend'] : never>) { + return new Catcher({ + token: TEST_TOKEN, + disableGlobalErrorsHandling: true, + breadcrumbs: false, + transport, + beforeSend, + }); +} + +describe('beforeSend', () => { + let warnSpy: ReturnType; + + beforeEach(() => { + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('should send event as-is when beforeSend returns it unchanged', async () => { + // Arrange + const { sendSpy, transport } = createTransport(); + const hawk = createCatcher(transport, (event) => event); + + // Act + hawk.send(new Error('hello')); + await wait(); + + // Assert + expect(sendSpy).toHaveBeenCalledOnce(); + expect(getSentPayload(sendSpy)!.title).toBe('hello'); + }); + + it('should send modified event when beforeSend mutates and returns it', async () => { + // Arrange + const { sendSpy, transport } = createTransport(); + const hawk = createCatcher(transport, (event) => { + event.context = { sanitized: true }; + + return event; + }); + + // Act + hawk.send(new Error('modify')); + await wait(); + + // Assert + expect(sendSpy).toHaveBeenCalledOnce(); + expect(getSentPayload(sendSpy)!.context).toEqual({ sanitized: true }); + }); + + it('should not send event when beforeSend returns false', async () => { + // Arrange + const { sendSpy, transport } = createTransport(); + const hawk = createCatcher(transport, () => false); + + // Act + hawk.send(new Error('drop')); + await wait(); + + // Assert + expect(sendSpy).not.toHaveBeenCalled(); + }); + + it.each([ + { label: 'undefined', value: undefined }, + { label: 'null', value: null }, + { label: 'number (42)', value: 42 }, + { label: 'string ("oops")', value: 'oops' }, + { label: 'true', value: true }, + ])('should send original event and warn when beforeSend returns $label', async ({ value }) => { + // Arrange + const { sendSpy, transport } = createTransport(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hawk = createCatcher(transport, () => value as any); + + // Act + hawk.send(new Error('invalid')); + await wait(); + + // Assert + expect(sendSpy).toHaveBeenCalledOnce(); + expect(getSentPayload(sendSpy)!.title).toBe('invalid'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid beforeSend value'), + expect.anything(), + expect.anything() + ); + }); + + it('should send original event and warn when beforeSend deletes required field (title)', async () => { + // Arrange + const { sendSpy, transport } = createTransport(); + const hawk = createCatcher(transport, (event) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (event as any).title; + + return event; + }); + + // Act + hawk.send(new Error('required-field')); + await wait(); + + // Assert — fallback to original payload, title preserved + expect(sendSpy).toHaveBeenCalledOnce(); + expect(getSentPayload(sendSpy)!.title).toBe('required-field'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid beforeSend value'), + expect.anything(), + expect.anything() + ); + }); + + it('should send event without deleted optional fields', async () => { + // Arrange + const { sendSpy, transport } = createTransport(); + const hawk = createCatcher(transport, (event) => { + delete event.release; + + return event; + }); + + // Act + hawk.send(new Error('optional')); + await wait(); + + // Assert + expect(sendSpy).toHaveBeenCalledOnce(); + expect(getSentPayload(sendSpy)!.release).toBeUndefined(); + }); +}); diff --git a/packages/javascript/tests/breadcrumbs.test.ts b/packages/javascript/tests/breadcrumbs.test.ts new file mode 100644 index 0000000..f02d7dc --- /dev/null +++ b/packages/javascript/tests/breadcrumbs.test.ts @@ -0,0 +1,233 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { BreadcrumbManager } from '../src/addons/breadcrumbs'; +import type { Breadcrumb } from '@hawk.so/types'; + +function resetManager(): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (BreadcrumbManager as any).instance = null; +} + +describe('BreadcrumbManager', () => { + let warnSpy: ReturnType; + + beforeEach(() => { + resetManager(); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('should return empty array when no breadcrumbs added', () => { + const m = BreadcrumbManager.getInstance(); + + m.init(); + expect(m.getBreadcrumbs()).toEqual([]); + }); + + it('should store breadcrumb with auto-generated timestamp', () => { + const m = BreadcrumbManager.getInstance(); + + m.init(); + m.addBreadcrumb({ type: 'default', message: 'test', level: 'info' }); + + const crumbs = m.getBreadcrumbs(); + + expect(crumbs).toHaveLength(1); + expect(crumbs[0].message).toBe('test'); + expect(crumbs[0].timestamp).toBeTypeOf('number'); + }); + + it('should keep explicit timestamp as-is', () => { + const m = BreadcrumbManager.getInstance(); + + m.init(); + m.addBreadcrumb({ type: 'default', message: 'test', level: 'info', timestamp: 12345 }); + + expect(m.getBreadcrumbs()[0].timestamp).toBe(12345); + }); + + it('should drop oldest breadcrumbs when buffer overflows (FIFO)', () => { + const m = BreadcrumbManager.getInstance(); + + m.init({ maxBreadcrumbs: 3 }); + + for (let i = 0; i < 5; i++) { + m.addBreadcrumb({ type: 'default', message: `msg-${i}`, level: 'info' }); + } + + const crumbs = m.getBreadcrumbs(); + + expect(crumbs).toHaveLength(3); + expect(crumbs[0].message).toBe('msg-2'); + expect(crumbs[2].message).toBe('msg-4'); + }); + + it('should store max 15 breadcrumbs by default', () => { + const m = BreadcrumbManager.getInstance(); + + m.init(); + + for (let i = 0; i < 20; i++) { + m.addBreadcrumb({ type: 'default', message: `msg-${i}`, level: 'info' }); + } + + expect(m.getBreadcrumbs()).toHaveLength(15); + }); + + it('should empty buffer on clear', () => { + const m = BreadcrumbManager.getInstance(); + + m.init(); + m.addBreadcrumb({ type: 'default', message: 'test', level: 'info' }); + m.clearBreadcrumbs(); + + expect(m.getBreadcrumbs()).toEqual([]); + }); + + it('should return a copy, not the internal array', () => { + const m = BreadcrumbManager.getInstance(); + + m.init(); + m.addBreadcrumb({ type: 'default', message: 'test', level: 'info' }); + + const first = m.getBreadcrumbs(); + const second = m.getBreadcrumbs(); + + expect(first).not.toBe(second); + expect(first).toEqual(second); + + first.push({ type: 'default', message: 'injected', level: 'info', timestamp: 0 } as Breadcrumb); + + expect(m.getBreadcrumbs()).toHaveLength(1); + }); + + it('should ignore second init call', () => { + const m = BreadcrumbManager.getInstance(); + + m.init({ maxBreadcrumbs: 5 }); + m.init({ maxBreadcrumbs: 100 }); + + for (let i = 0; i < 10; i++) { + m.addBreadcrumb({ type: 'default', message: `msg-${i}`, level: 'info' }); + } + + expect(m.getBreadcrumbs()).toHaveLength(5); + }); +}); + +describe('beforeBreadcrumb', () => { + let warnSpy: ReturnType; + + beforeEach(() => { + resetManager(); + warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('should store modified breadcrumb when hook returns changed object', () => { + // Arrange + const m = BreadcrumbManager.getInstance(); + + m.init({ + beforeBreadcrumb(bc) { + bc.message = 'MODIFIED'; + + return bc; + }, + }); + + // Act + m.addBreadcrumb({ type: 'default', message: 'original', level: 'info' }); + + // Assert + expect(m.getBreadcrumbs()[0].message).toBe('MODIFIED'); + }); + + it('should not store breadcrumb when hook returns false', () => { + // Arrange + const m = BreadcrumbManager.getInstance(); + + m.init({ + beforeBreadcrumb: () => false, + }); + + // Act + m.addBreadcrumb({ type: 'default', message: 'drop', level: 'info' }); + + // Assert + expect(m.getBreadcrumbs()).toHaveLength(0); + }); + + it.each([ + { label: 'undefined', value: undefined }, + { label: 'null', value: null }, + { label: 'number (42)', value: 42 }, + { label: 'string ("oops")', value: 'oops' }, + { label: 'true', value: true }, + ])('should store original breadcrumb and warn when hook returns $label', ({ value }) => { + // Arrange + const m = BreadcrumbManager.getInstance(); + + m.init({ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + beforeBreadcrumb: () => value as any, + }); + + // Act + m.addBreadcrumb({ type: 'default', message: 'original', level: 'info' }); + + // Assert + expect(m.getBreadcrumbs()[0].message).toBe('original'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining('Invalid beforeBreadcrumb value'), + expect.anything(), + expect.anything() + ); + }); + + it('should store original breadcrumb and warn when hook deletes required field (message)', () => { + // Arrange + const m = BreadcrumbManager.getInstance(); + + m.init({ + beforeBreadcrumb(bc) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (bc as any).message; + + return bc; + }, + }); + + // Act + m.addBreadcrumb({ type: 'default', message: 'keep-me', level: 'info' }); + + // Assert — fallback to original, message preserved + expect(m.getBreadcrumbs()[0].message).toBe('keep-me'); + }); + + it('should filter breadcrumbs by category using hook', () => { + // Arrange + const m = BreadcrumbManager.getInstance(); + + m.init({ + beforeBreadcrumb(bc) { + return bc.category === 'secret' ? false : bc; + }, + }); + + // Act + m.addBreadcrumb({ type: 'default', message: 'public', level: 'info', category: 'public' }); + m.addBreadcrumb({ type: 'default', message: 'secret', level: 'info', category: 'secret' }); + + // Assert + const crumbs = m.getBreadcrumbs(); + + expect(crumbs).toHaveLength(1); + expect(crumbs[0].message).toBe('public'); + }); +}); diff --git a/packages/javascript/tsconfig.test.json b/packages/javascript/tsconfig.test.json new file mode 100644 index 0000000..166c447 --- /dev/null +++ b/packages/javascript/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": null, + "declaration": false + }, + "include": [ + "src/**/*", + "tests/**/*", + "vitest.config.ts" + ] +} diff --git a/packages/javascript/vitest.config.ts b/packages/javascript/vitest.config.ts new file mode 100644 index 0000000..47ad6a2 --- /dev/null +++ b/packages/javascript/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'jsdom', + include: ['tests/**/*.test.ts'], + }, + define: { + VERSION: JSON.stringify('0.0.0-test'), + }, + resolve: { + alias: { + '@/types': path.resolve(__dirname, './src/types'), + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 88bec46..317217e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5,6 +5,46 @@ __metadata: version: 8 cacheKey: 10c0 +"@acemir/cssom@npm:^0.9.31": + version: 0.9.31 + resolution: "@acemir/cssom@npm:0.9.31" + checksum: 10c0/cbfff98812642104ec3b37de1ad3a53f216ddc437e7b9276a23f46f2453844ea3c3f46c200bc4656a2f747fb26567560b3cc5183d549d119a758926551b5f566 + languageName: node + linkType: hard + +"@asamuzakjp/css-color@npm:^4.1.1": + version: 4.1.2 + resolution: "@asamuzakjp/css-color@npm:4.1.2" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-color-parser": "npm:^4.0.1" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + lru-cache: "npm:^11.2.5" + checksum: 10c0/e432fdef978b37654a2ca31169a149b9173e708f70c82612acb123a36dbc7dd99913c48cbf2edd6fe3652cc627d4bc94bf87571463da0b788f15b973d4ce5b0f + languageName: node + linkType: hard + +"@asamuzakjp/dom-selector@npm:^6.7.6": + version: 6.7.8 + resolution: "@asamuzakjp/dom-selector@npm:6.7.8" + dependencies: + "@asamuzakjp/nwsapi": "npm:^2.3.9" + bidi-js: "npm:^1.0.3" + css-tree: "npm:^3.1.0" + is-potential-custom-element-name: "npm:^1.0.1" + lru-cache: "npm:^11.2.5" + checksum: 10c0/4274e5025e6e399654cb066f33a165f4dc65596a33612b0a345dce80666ad1f234b68b8b6db6f005fc76be365ea36e09bd7b08990442461f390c77b19cfea885 + languageName: node + linkType: hard + +"@asamuzakjp/nwsapi@npm:^2.3.9": + version: 2.3.9 + resolution: "@asamuzakjp/nwsapi@npm:2.3.9" + checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -61,6 +101,59 @@ __metadata: languageName: node linkType: hard +"@csstools/color-helpers@npm:^6.0.1": + version: 6.0.1 + resolution: "@csstools/color-helpers@npm:6.0.1" + checksum: 10c0/866844267d5aa5a02fe9d54f6db6fc18f6306595edb03664cc8ef15c99d3e6f3b42eb1a413c98bafa5b2dc0d8e0193da9b3bcc9d6a04f5de74cbd44935e74b3c + languageName: node + linkType: hard + +"@csstools/css-calc@npm:^3.0.0": + version: 3.0.1 + resolution: "@csstools/css-calc@npm:3.0.1" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/ccda2b5d78ce6fecce58c59cc5747f5a8db6b5cae9b6d8a38444169d2d6591bf1f5e8bf147e5248e55e28a736e09be26a52621009089987786d69c209df39e0c + languageName: node + linkType: hard + +"@csstools/css-color-parser@npm:^4.0.1": + version: 4.0.1 + resolution: "@csstools/css-color-parser@npm:4.0.1" + dependencies: + "@csstools/color-helpers": "npm:^6.0.1" + "@csstools/css-calc": "npm:^3.0.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/c46be5b9f5c0ef3cd25b47a71bd2a4d1c4856b123ecba4abe8eaa0688d3fc47f58fa67ea281d6b9efca4b9fdfa88fb045c51d0f9b8c612a56bd546d38260b138 + languageName: node + linkType: hard + +"@csstools/css-parser-algorithms@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-parser-algorithms@npm:4.0.0" + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d + languageName: node + linkType: hard + +"@csstools/css-syntax-patches-for-csstree@npm:^1.0.21": + version: 1.0.27 + resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.27" + checksum: 10c0/ef3f2a639109758c0f3c04520465800ca4c830174bd6f7979795083877c82ace51ab8353857b06a818cb6c0de6d4dc88f84a86fc3b021be47f11a0f1c4b74e7e + languageName: node + linkType: hard + +"@csstools/css-tokenizer@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-tokenizer@npm:4.0.0" + checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f + languageName: node + linkType: hard + "@es-joy/jsdoccomment@npm:~0.41.0": version: 0.41.0 resolution: "@es-joy/jsdoccomment@npm:0.41.0" @@ -471,14 +564,28 @@ __metadata: languageName: node linkType: hard +"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.6.0": + version: 1.13.0 + resolution: "@exodus/bytes@npm:1.13.0" + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + checksum: 10c0/f854f2685544c9695f0f92583c65e35ed9f85dff1921ad17de6bf4d24d53075f31ce15a30402a3205958e63ec70211dbf525e97cdd874b157948814f9f2b208d + languageName: node + linkType: hard + "@hawk.so/javascript@npm:^3.0.0, @hawk.so/javascript@workspace:packages/javascript": version: 0.0.0-use.local resolution: "@hawk.so/javascript@workspace:packages/javascript" dependencies: - "@hawk.so/types": "npm:0.5.2" + "@hawk.so/types": "npm:0.5.8" error-stack-parser: "npm:^2.1.4" + jsdom: "npm:^28.0.0" vite: "npm:^7.3.1" vite-plugin-dts: "npm:^4.2.4" + vitest: "npm:^4.0.18" vue: "npm:^2" languageName: unknown linkType: soft @@ -506,12 +613,12 @@ __metadata: languageName: unknown linkType: soft -"@hawk.so/types@npm:0.5.2": - version: 0.5.2 - resolution: "@hawk.so/types@npm:0.5.2" +"@hawk.so/types@npm:0.5.8": + version: 0.5.8 + resolution: "@hawk.so/types@npm:0.5.8" dependencies: bson: "npm:^7.0.0" - checksum: 10c0/4a76aebc3bc4c87649d4a9991f51c81fac2ad457896ba19261afa66d8979739bed6108db322122400e99b3ca81cb4fbc3be7dee9b7c334d3cbfef51c2ec57719 + checksum: 10c0/95d45be58594b30aad097d25beb800e2d236cf869b3f57916a67bea841263cab96eecf4d678dd86f94ec46318759b9d566c42228a450c1d05689b3f326654d53 languageName: node linkType: hard @@ -1094,6 +1201,16 @@ __metadata: languageName: node linkType: hard +"@types/chai@npm:^5.2.2": + version: 5.2.3 + resolution: "@types/chai@npm:5.2.3" + dependencies: + "@types/deep-eql": "npm:*" + assertion-error: "npm:^2.0.1" + checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f + languageName: node + linkType: hard + "@types/cookie@npm:^0.6.0": version: 0.6.0 resolution: "@types/cookie@npm:0.6.0" @@ -1101,6 +1218,13 @@ __metadata: languageName: node linkType: hard +"@types/deep-eql@npm:*": + version: 4.0.2 + resolution: "@types/deep-eql@npm:4.0.2" + checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 + languageName: node + linkType: hard + "@types/estree@npm:1.0.8, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5, @types/estree@npm:^1.0.6": version: 1.0.8 resolution: "@types/estree@npm:1.0.8" @@ -1252,6 +1376,86 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/expect@npm:4.0.18" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + "@types/chai": "npm:^5.2.2" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" + chai: "npm:^6.2.1" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/123b0aa111682e82ec5289186df18037b1a1768700e468ee0f9879709aaa320cf790463c15c0d8ee10df92b402f4394baf5d27797e604d78e674766d87bcaadc + languageName: node + linkType: hard + +"@vitest/mocker@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/mocker@npm:4.0.18" + dependencies: + "@vitest/spy": "npm:4.0.18" + estree-walker: "npm:^3.0.3" + magic-string: "npm:^0.30.21" + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + checksum: 10c0/fb0a257e7e167759d4ad228d53fa7bad2267586459c4a62188f2043dd7163b4b02e1e496dc3c227837f776e7d73d6c4343613e89e7da379d9d30de8260f1ee4b + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/pretty-format@npm:4.0.18" + dependencies: + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/0086b8c88eeca896d8e4b98fcdef452c8041a1b63eb9e85d3e0bcc96c8aa76d8e9e0b6990ebb0bb0a697c4ebab347e7735888b24f507dbff2742ddce7723fd94 + languageName: node + linkType: hard + +"@vitest/runner@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/runner@npm:4.0.18" + dependencies: + "@vitest/utils": "npm:4.0.18" + pathe: "npm:^2.0.3" + checksum: 10c0/fdb4afa411475133c05ba266c8092eaf1e56cbd5fb601f92ec6ccb9bab7ca52e06733ee8626599355cba4ee71cb3a8f28c84d3b69dc972e41047edc50229bc01 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/snapshot@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + magic-string: "npm:^0.30.21" + pathe: "npm:^2.0.3" + checksum: 10c0/d3bfefa558db9a69a66886ace6575eb96903a5ba59f4d9a5d0fecb4acc2bb8dbb443ef409f5ac1475f2e1add30bd1d71280f98912da35e89c75829df9e84ea43 + languageName: node + linkType: hard + +"@vitest/spy@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/spy@npm:4.0.18" + checksum: 10c0/6de537890b3994fcadb8e8d8ac05942320ae184f071ec395d978a5fba7fa928cbb0c5de85af86a1c165706c466e840de8779eaff8c93450c511c7abaeb9b8a4e + languageName: node + linkType: hard + +"@vitest/utils@npm:4.0.18": + version: 4.0.18 + resolution: "@vitest/utils@npm:4.0.18" + dependencies: + "@vitest/pretty-format": "npm:4.0.18" + tinyrainbow: "npm:^3.0.3" + checksum: 10c0/4a3c43c1421eb90f38576926496f6c80056167ba111e63f77cf118983902673737a1a38880b890d7c06ec0a12475024587344ee502b3c43093781533022f2aeb + languageName: node + linkType: hard + "@volar/language-core@npm:2.4.28, @volar/language-core@npm:~2.4.11": version: 2.4.28 resolution: "@volar/language-core@npm:2.4.28" @@ -1626,6 +1830,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 + languageName: node + linkType: hard + "astral-regex@npm:^2.0.0": version: 2.0.0 resolution: "astral-regex@npm:2.0.0" @@ -1670,6 +1881,15 @@ __metadata: languageName: node linkType: hard +"bidi-js@npm:^1.0.3": + version: 1.0.3 + resolution: "bidi-js@npm:1.0.3" + dependencies: + require-from-string: "npm:^2.0.2" + checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1 + languageName: node + linkType: hard + "brace-expansion@npm:^1.1.7": version: 1.1.12 resolution: "brace-expansion@npm:1.1.12" @@ -1786,6 +2006,13 @@ __metadata: languageName: node linkType: hard +"chai@npm:^6.2.1": + version: 6.2.2 + resolution: "chai@npm:6.2.2" + checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 + languageName: node + linkType: hard + "chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -1922,6 +2149,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.1.0": + version: 3.1.0 + resolution: "css-tree@npm:3.1.0" + dependencies: + mdn-data: "npm:2.12.2" + source-map-js: "npm:^1.0.1" + checksum: 10c0/b5715852c2f397c715ca00d56ec53fc83ea596295ae112eb1ba6a1bda3b31086380e596b1d8c4b980fe6da09e7d0fc99c64d5bb7313030dd0fba9c1415f30979 + languageName: node + linkType: hard + "cssesc@npm:^3.0.0": version: 3.0.0 resolution: "cssesc@npm:3.0.0" @@ -1931,6 +2168,18 @@ __metadata: languageName: node linkType: hard +"cssstyle@npm:^5.3.7": + version: 5.3.7 + resolution: "cssstyle@npm:5.3.7" + dependencies: + "@asamuzakjp/css-color": "npm:^4.1.1" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.0.21" + css-tree: "npm:^3.1.0" + lru-cache: "npm:^11.2.4" + checksum: 10c0/9330f014f4209df06305264b92b8e963dfef636fdc2ae7d13f24ea7da6468aba1dc5eb13082621258bdd22cbd7fb7cb291894e188a3cdf660e8b79cd2c5e5e0e + languageName: node + linkType: hard + "csstype@npm:^3.1.0": version: 3.2.3 resolution: "csstype@npm:3.2.3" @@ -1938,6 +2187,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^7.0.0": + version: 7.0.0 + resolution: "data-urls@npm:7.0.0" + dependencies: + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f + languageName: node + linkType: hard + "data-view-buffer@npm:^1.0.2": version: 1.0.2 resolution: "data-view-buffer@npm:1.0.2" @@ -1999,6 +2258,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.6.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -2113,6 +2379,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^6.0.0": + version: 6.0.1 + resolution: "entities@npm:6.0.1" + checksum: 10c0/ed836ddac5acb34341094eb495185d527bd70e8632b6c0d59548cbfa23defdbae70b96f9a405c82904efa421230b5b3fd2283752447d737beffd3f3e6ee74414 + languageName: node + linkType: hard + "entities@npm:^7.0.0": version: 7.0.1 resolution: "entities@npm:7.0.1" @@ -2219,6 +2492,13 @@ __metadata: languageName: node linkType: hard +"es-module-lexer@npm:^1.7.0": + version: 1.7.0 + resolution: "es-module-lexer@npm:1.7.0" + checksum: 10c0/4c935affcbfeba7fb4533e1da10fa8568043df1e3574b869385980de9e2d475ddc36769891936dbb07036edb3c3786a8b78ccf44964cd130dedc1f2c984b6c7b + languageName: node + linkType: hard + "es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": version: 1.1.1 resolution: "es-object-atoms@npm:1.1.1" @@ -2833,6 +3113,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "esutils@npm:^2.0.2, esutils@npm:^2.0.3": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -2840,6 +3129,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^1.2.2": + version: 1.3.0 + resolution: "expect-type@npm:1.3.0" + checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.3 resolution: "exponential-backoff@npm:3.1.3" @@ -3288,6 +3584,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^6.0.0": + version: 6.0.0 + resolution: "html-encoding-sniffer@npm:6.0.0" + dependencies: + "@exodus/bytes": "npm:^1.6.0" + checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025 + languageName: node + linkType: hard + "http-cache-semantics@npm:^4.1.1": version: 4.2.0 resolution: "http-cache-semantics@npm:4.2.0" @@ -3295,7 +3600,7 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": +"http-proxy-agent@npm:^7.0.0, http-proxy-agent@npm:^7.0.2": version: 7.0.2 resolution: "http-proxy-agent@npm:7.0.2" dependencies: @@ -3305,7 +3610,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -3562,6 +3867,13 @@ __metadata: languageName: node linkType: hard +"is-potential-custom-element-name@npm:^1.0.1": + version: 1.0.1 + resolution: "is-potential-custom-element-name@npm:1.0.1" + checksum: 10c0/b73e2f22bc863b0939941d369486d308b43d7aef1f9439705e3582bfccaa4516406865e32c968a35f97a99396dac84e2624e67b0a16b0a15086a785e16ce7db9 + languageName: node + linkType: hard + "is-reference@npm:^3.0.3": version: 3.0.3 resolution: "is-reference@npm:3.0.3" @@ -3718,6 +4030,39 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^28.0.0": + version: 28.0.0 + resolution: "jsdom@npm:28.0.0" + dependencies: + "@acemir/cssom": "npm:^0.9.31" + "@asamuzakjp/dom-selector": "npm:^6.7.6" + "@exodus/bytes": "npm:^1.11.0" + cssstyle: "npm:^5.3.7" + data-urls: "npm:^7.0.0" + decimal.js: "npm:^10.6.0" + html-encoding-sniffer: "npm:^6.0.0" + http-proxy-agent: "npm:^7.0.2" + https-proxy-agent: "npm:^7.0.6" + is-potential-custom-element-name: "npm:^1.0.1" + parse5: "npm:^8.0.0" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^6.0.0" + undici: "npm:^7.20.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^8.0.1" + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/6aa2419506f912f40c5f1c6ca52c6dfdfde5970cfbaf105ebfc55ab975dda2d2492b6f8dc4c62b94e46501c4f77dfd2a60ea229ee67f924d59fe6c51bf653043 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -3870,6 +4215,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.2.4, lru-cache@npm:^11.2.5": + version: 11.2.6 + resolution: "lru-cache@npm:11.2.6" + checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd + languageName: node + linkType: hard + "lru-cache@npm:^6.0.0": version: 6.0.0 resolution: "lru-cache@npm:6.0.0" @@ -3914,6 +4266,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.12.2": + version: 2.12.2 + resolution: "mdn-data@npm:2.12.2" + checksum: 10c0/b22443b71d70f72ccc3c6ba1608035431a8fc18c3c8fc53523f06d20e05c2ac10f9b53092759a2ca85cf02f0d37036f310b581ce03e7b99ac74d388ef8152ade + languageName: node + linkType: hard + "merge2@npm:^1.3.0, merge2@npm:^1.4.1": version: 1.4.1 resolution: "merge2@npm:1.4.1" @@ -4241,7 +4600,7 @@ __metadata: languageName: node linkType: hard -"obug@npm:^2.1.0": +"obug@npm:^2.1.0, obug@npm:^2.1.1": version: 2.1.1 resolution: "obug@npm:2.1.1" checksum: 10c0/59dccd7de72a047e08f8649e94c1015ec72f94eefb6ddb57fb4812c4b425a813bc7e7cd30c9aca20db3c59abc3c85cc7a62bb656a968741d770f4e8e02bc2e78 @@ -4305,6 +4664,15 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^8.0.0": + version: 8.0.0 + resolution: "parse5@npm:8.0.0" + dependencies: + entities: "npm:^6.0.0" + checksum: 10c0/8279892dcd77b2f2229707f60eb039e303adf0288812b2a8fd5acf506a4d432da833c6c5d07a6554bef722c2367a81ef4a1f7e9336564379a7dba3e798bf16b3 + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -4504,7 +4872,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0": +"punycode@npm:^2.1.0, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 @@ -4808,6 +5176,15 @@ __metadata: languageName: node linkType: hard +"saxes@npm:^6.0.0": + version: 6.0.0 + resolution: "saxes@npm:6.0.0" + dependencies: + xmlchars: "npm:^2.2.0" + checksum: 10c0/3847b839f060ef3476eb8623d099aa502ad658f5c40fd60c105ebce86d244389b0d76fcae30f4d0c728d7705ceb2f7e9b34bb54717b6a7dbedaf5dad2d9a4b74 + languageName: node + linkType: hard + "semver@npm:^6.3.1": version: 6.3.1 resolution: "semver@npm:6.3.1" @@ -4945,6 +5322,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "sirv@npm:^3.0.0": version: 3.0.2 resolution: "sirv@npm:3.0.2" @@ -5019,7 +5403,7 @@ __metadata: languageName: node linkType: hard -"source-map-js@npm:^1.2.1": +"source-map-js@npm:^1.0.1, source-map-js@npm:^1.2.1": version: 1.2.1 resolution: "source-map-js@npm:1.2.1" checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf @@ -5121,6 +5505,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + "stackframe@npm:^1.3.4": version: 1.3.4 resolution: "stackframe@npm:1.3.4" @@ -5128,6 +5519,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.10.0": + version: 3.10.0 + resolution: "std-env@npm:3.10.0" + checksum: 10c0/1814927a45004d36dde6707eaf17552a546769bc79a6421be2c16ce77d238158dfe5de30910b78ec30d95135cc1c59ea73ee22d2ca170f8b9753f84da34c427f + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -5293,6 +5691,13 @@ __metadata: languageName: node linkType: hard +"symbol-tree@npm:^3.2.4": + version: 3.2.4 + resolution: "symbol-tree@npm:3.2.4" + checksum: 10c0/dfbe201ae09ac6053d163578778c53aa860a784147ecf95705de0cd23f42c851e1be7889241495e95c37cabb058edb1052f141387bef68f705afc8f9dd358509 + languageName: node + linkType: hard + "table@npm:^6.0.9": version: 6.9.0 resolution: "table@npm:6.9.0" @@ -5326,6 +5731,20 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.9.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c + languageName: node + linkType: hard + +"tinyexec@npm:^1.0.2": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10c0/1261a8e34c9b539a9aae3b7f0bb5372045ff28ee1eba035a2a059e532198fe1a182ec61ac60fa0b4a4129f0c4c4b1d2d57355b5cb9aa2d17ac9454ecace502ee + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -5336,6 +5755,31 @@ __metadata: languageName: node linkType: hard +"tinyrainbow@npm:^3.0.3": + version: 3.0.3 + resolution: "tinyrainbow@npm:3.0.3" + checksum: 10c0/1e799d35cd23cabe02e22550985a3051dc88814a979be02dc632a159c393a998628eacfc558e4c746b3006606d54b00bcdea0c39301133956d10a27aa27e988c + languageName: node + linkType: hard + +"tldts-core@npm:^7.0.23": + version: 7.0.23 + resolution: "tldts-core@npm:7.0.23" + checksum: 10c0/b3d936a75b5f65614c356a58ef37563681c6224187dcce9f57aac76d92aae83b1a6fe6ab910f77472b35456bc145a8441cb3e572b4850be43cb4f3465e0610ec + languageName: node + linkType: hard + +"tldts@npm:^7.0.5": + version: 7.0.23 + resolution: "tldts@npm:7.0.23" + dependencies: + tldts-core: "npm:^7.0.23" + bin: + tldts: bin/cli.js + checksum: 10c0/492874770afaade724a10f8a97cce511d74bed07735c7f1100b7957254d7a5bbbc18becaf5cd049f9d7b0feeb945a64af64d5a300dfb851a4ac57cf3a5998afc + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -5352,6 +5796,24 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^6.0.0": + version: 6.0.0 + resolution: "tough-cookie@npm:6.0.0" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10c0/7b17a461e9c2ac0d0bea13ab57b93b4346d0b8c00db174c963af1e46e4ea8d04148d2a55f2358fc857db0c0c65208a98e319d0c60693e32e0c559a9d9cf20cb5 + languageName: node + linkType: hard + +"tr46@npm:^6.0.0": + version: 6.0.0 + resolution: "tr46@npm:6.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e + languageName: node + linkType: hard + "ts-api-utils@npm:^1.0.1": version: 1.4.3 resolution: "ts-api-utils@npm:1.4.3" @@ -5501,6 +5963,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^7.20.0": + version: 7.21.0 + resolution: "undici@npm:7.21.0" + checksum: 10c0/ec5d7524125ac9c392a8a67b84fe4f9301936ca6b2afd7be12e73ab98726e55761dc9624ac10361d2f346e6fdaf66043381a62f7d0b565facd61bbfda975a586 + languageName: node + linkType: hard + "unique-filename@npm:^5.0.0": version: 5.0.0 resolution: "unique-filename@npm:5.0.0" @@ -5572,7 +6041,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:^7.3.1": +"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.3.1": version: 7.3.1 resolution: "vite@npm:7.3.1" dependencies: @@ -5639,6 +6108,65 @@ __metadata: languageName: node linkType: hard +"vitest@npm:^4.0.18": + version: 4.0.18 + resolution: "vitest@npm:4.0.18" + dependencies: + "@vitest/expect": "npm:4.0.18" + "@vitest/mocker": "npm:4.0.18" + "@vitest/pretty-format": "npm:4.0.18" + "@vitest/runner": "npm:4.0.18" + "@vitest/snapshot": "npm:4.0.18" + "@vitest/spy": "npm:4.0.18" + "@vitest/utils": "npm:4.0.18" + es-module-lexer: "npm:^1.7.0" + expect-type: "npm:^1.2.2" + magic-string: "npm:^0.30.21" + obug: "npm:^2.1.1" + pathe: "npm:^2.0.3" + picomatch: "npm:^4.0.3" + std-env: "npm:^3.10.0" + tinybench: "npm:^2.9.0" + tinyexec: "npm:^1.0.2" + tinyglobby: "npm:^0.2.15" + tinyrainbow: "npm:^3.0.3" + vite: "npm:^6.0.0 || ^7.0.0" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@opentelemetry/api": ^1.9.0 + "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 + "@vitest/browser-playwright": 4.0.18 + "@vitest/browser-preview": 4.0.18 + "@vitest/browser-webdriverio": 4.0.18 + "@vitest/ui": 4.0.18 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@opentelemetry/api": + optional: true + "@types/node": + optional: true + "@vitest/browser-playwright": + optional: true + "@vitest/browser-preview": + optional: true + "@vitest/browser-webdriverio": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: 10c0/b913cd32032c95f29ff08c931f4b4c6fd6d2da498908d6770952c561a1b8d75c62499a1f04cadf82fb89cc0f9a33f29fb5dfdb899f6dbb27686a9d91571be5fa + languageName: node + linkType: hard + "vscode-uri@npm:^3.0.8": version: 3.1.0 resolution: "vscode-uri@npm:3.1.0" @@ -5656,6 +6184,40 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + +"webidl-conversions@npm:^8.0.1": + version: 8.0.1 + resolution: "webidl-conversions@npm:8.0.1" + checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46 + languageName: node + linkType: hard + +"whatwg-mimetype@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-mimetype@npm:5.0.0" + checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4 + languageName: node + linkType: hard + +"whatwg-url@npm:^16.0.0": + version: 16.0.0 + resolution: "whatwg-url@npm:16.0.0" + dependencies: + "@exodus/bytes": "npm:^1.11.0" + tr46: "npm:^6.0.0" + webidl-conversions: "npm:^8.0.1" + checksum: 10c0/9b8cb392be244d0e9687ffe543f9ea63b7aa051a98547ea362a38d182d89bfbd96e13e7ed3f40df1f7566bb7c3581f6c081ddea950cf5382532716ce33000ff4 + languageName: node + linkType: hard + "which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1": version: 1.1.1 resolution: "which-boxed-primitive@npm:1.1.1" @@ -5739,6 +6301,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 + languageName: node + linkType: hard + "word-wrap@npm:^1.2.5": version: 1.2.5 resolution: "word-wrap@npm:1.2.5" @@ -5753,6 +6327,20 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + +"xmlchars@npm:^2.2.0": + version: 2.2.0 + resolution: "xmlchars@npm:2.2.0" + checksum: 10c0/b64b535861a6f310c5d9bfa10834cf49127c71922c297da9d4d1b45eeaae40bf9b4363275876088fbe2667e5db028d2cd4f8ee72eed9bede840a67d57dab7593 + languageName: node + linkType: hard + "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0"