diff --git a/packages/web/src/generic.test.ts b/packages/web/src/generic.test.ts index 910b22e..08ce8b1 100644 --- a/packages/web/src/generic.test.ts +++ b/packages/web/src/generic.test.ts @@ -1,10 +1,17 @@ import { beforeEach, describe, it, expect, jest } from '@jest/globals'; import { inject, track } from './generic'; - -describe('inject', () => { - describe('in development mode', () => { - it('should add the script tag correctly', () => { - inject({ mode: 'development' }); +import type { AllowedPropertyValues, Mode } from './types'; + +describe.each([ + { + mode: 'development', + file: 'https://va.vercel-scripts.com/v1/script.debug.js', + }, + { mode: 'production', file: 'http://localhost/_vercel/insights/script.js' }, +] as { mode: Mode; file: string }[])('in $mode mode', ({ mode, file }) => { + describe('inject', () => { + it('adds the script tag correctly', () => { + inject({ mode }); const scripts = document.getElementsByTagName('script'); expect(scripts).toHaveLength(1); @@ -15,172 +22,49 @@ describe('inject', () => { throw new Error('Could not find script tag'); } - expect(script.src).toEqual( - 'https://va.vercel-scripts.com/v1/script.debug.js' - ); + expect(script.src).toEqual(file); expect(script).toHaveAttribute('defer'); }); }); - describe('in production mode', () => { - it('should add the script tag correctly', () => { - inject({ mode: 'production' }); - - const scripts = document.getElementsByTagName('script'); - expect(scripts).toHaveLength(1); - - const script = document.head.querySelector('script'); - - if (!script) { - throw new Error('Could not find script tag'); - } - - expect(script.src).toEqual('http://localhost/_vercel/insights/script.js'); - expect(script).toHaveAttribute('defer'); - }); - }); -}); - -describe('track custom events', () => { - beforeEach(() => { - // reset the internal queue before every test - window.vaq = []; - }); - - describe('in production mode', () => { + describe('track custom events', () => { beforeEach(() => { - inject({ - mode: 'production', - }); + // reset the internal queue before every test + window.vaq = []; + inject({ mode }); }); describe('queue custom events', () => { - it('should track event with name only', () => { - track('my event'); - - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'my event', - }, - ]); + it('tracks event with name only', () => { + const name = 'my event'; + track(name); + expect(window.vaq?.[0]).toEqual(['event', { name }]); }); - it('should allow custom data to be tracked', () => { - track('custom event', { - string: 'string', - number: 1, - }); - - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'custom event', - data: { - string: 'string', - number: 1, - }, - }, - ]); + it('allows custom data to be tracked', () => { + const name = 'custom event'; + const data = { string: 'string', number: 1 }; + track(name, data); + expect(window.vaq?.[0]).toEqual(['event', { name, data }]); }); it('should strip data for nested objects', () => { - track('custom event', { - string: 'string', - number: 1, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- This is intentional - nested: { - object: '', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is intentional - } as any, - }); - - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'custom event', - data: { - string: 'string', - number: 1, - }, - }, - ]); - }); - }); - }); - - describe('in development mode', () => { - beforeEach(() => { - inject({ - mode: 'development', - }); - // eslint-disable-next-line @typescript-eslint/no-empty-function -- This is intentional - jest.spyOn(global.console, 'error').mockImplementation(() => {}); - }); - - describe('queue custom events', () => { - it('should track event with name only', () => { - track('my event'); - - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'my event', - }, - ]); - }); - - it('should allow custom data to be tracked', () => { - track('custom event', { - string: 'string', - number: 1, - }); - - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'custom event', - data: { - string: 'string', - number: 1, - }, - }, - ]); - }); + jest.spyOn(global.console, 'error').mockImplementation(() => void 0); - it('should log an error when nested properties are sent', () => { - track('custom event', { - string: 'string', - number: 1, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- This is intentional - nested: { - object: '', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is intentional - } as any, + const name = 'custom event'; + const data = { string: 'string', number: 1 }; + track(name, { + ...data, + nested: { object: '' } as unknown as AllowedPropertyValues, }); - // eslint-disable-next-line no-console -- Logging to console is intentional - expect(console.error).toHaveBeenCalledTimes(1); + if (mode === 'development') { + // eslint-disable-next-line jest/no-conditional-expect, no-console -- only in development + expect(console.error).toHaveBeenCalledTimes(1); + } else { + // eslint-disable-next-line jest/no-conditional-expect -- only in production + expect(window.vaq?.[0]).toEqual(['event', { name, data }]); + } }); }); }); diff --git a/packages/web/src/react.test.tsx b/packages/web/src/react.test.tsx index a75b0b3..68480c8 100644 --- a/packages/web/src/react.test.tsx +++ b/packages/web/src/react.test.tsx @@ -2,67 +2,80 @@ import * as React from 'react'; import { afterEach, beforeEach, describe, it, expect } from '@jest/globals'; import { cleanup, render } from '@testing-library/react'; import { Analytics, track } from './react'; +import type { AllowedPropertyValues, AnalyticsProps, Mode } from './types'; describe('', () => { afterEach(() => { cleanup(); }); - describe('in development mode', () => { - it('should add the script tag correctly', () => { - render(); + beforeEach(() => { + window.va = undefined; + // reset the internal queue before every test + window.vaq = []; + }); + + describe.each([ + { + mode: 'development', + file: 'https://va.vercel-scripts.com/v1/script.debug.js', + }, + { mode: 'production', file: 'http://localhost/_vercel/insights/script.js' }, + ] as { mode: Mode; file: string }[])('in $mode mode', ({ mode, file }) => { + it('adds the script tag correctly', () => { + render(); const scripts = document.getElementsByTagName('script'); expect(scripts).toHaveLength(1); const script = document.head.querySelector('script'); - - if (!script) { - throw new Error('Could not find script tag'); - } - - expect(script.src).toEqual( - 'https://va.vercel-scripts.com/v1/script.debug.js' - ); + expect(script).toBeDefined(); + expect(script?.src).toEqual(file); expect(script).toHaveAttribute('defer'); }); - }); - describe('in production mode', () => { - it('should add the script tag correctly', () => { - render(); + it('sets and changes beforeSend', () => { + const beforeSend: Required['beforeSend'] = (event) => + event; + const beforeSend2: Required['beforeSend'] = (event) => + event; + const { rerender } = render( + + ); - const scripts = document.getElementsByTagName('script'); - expect(scripts).toHaveLength(1); + expect(window.vaq?.[0]).toEqual(['beforeSend', beforeSend]); + expect(window.vaq).toHaveLength(1); + window.vaq?.splice(0, 1); - const script = document.head.querySelector('script'); + rerender(); + expect(window.vaq).toHaveLength(0); + + rerender(); + expect(window.vaq?.[0]).toEqual(['beforeSend', beforeSend2]); + expect(window.vaq).toHaveLength(1); + }); - if (!script) { - throw new Error('Could not find script tag'); - } + it('does not change beforeSend when undefined', () => { + const beforeSend: Required['beforeSend'] = (event) => + event; + const { rerender } = render(); - expect(script.src).toEqual('http://localhost/_vercel/insights/script.js'); - expect(script).toHaveAttribute('defer'); + expect(window.vaq?.[0]).toEqual(['beforeSend', beforeSend]); + expect(window.vaq).toHaveLength(1); + window.vaq?.splice(0, 1); + + rerender(); + expect(window.vaq).toHaveLength(0); }); }); describe('track custom events', () => { - beforeEach(() => { - // reset the internal queue before every test - window.vaq = []; - }); - describe('queue custom events', () => { - it('should track event with name only', () => { + it('tracks event with name only', () => { render(); - track('my event'); - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ + expect(window.vaq?.[0]).toEqual([ 'event', { name: 'my event', @@ -70,57 +83,25 @@ describe('', () => { ]); }); - it('should allow custom data to be tracked', () => { + it('allows custom data to be tracked', () => { render(); + const name = 'custom event'; + const data = { string: 'string', number: 1 }; + track(name, data); - track('custom event', { - string: 'string', - number: 1, - }); - - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'custom event', - data: { - string: 'string', - number: 1, - }, - }, - ]); + expect(window.vaq?.[0]).toEqual(['event', { name, data }]); }); - it('should strip data for nested objects', () => { + it('strips data for nested objects', () => { render(); - - track('custom event', { - string: 'string', - number: 1, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Intentional to trigger error - nested: { - object: '', - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allow - } as any, + const name = 'custom event'; + const data = { string: 'string', number: 1 }; + track(name, { + ...data, + nested: { object: '' } as unknown as AllowedPropertyValues, }); - expect(window.vaq).toBeDefined(); - - if (!window.vaq) throw new Error('window.vaq is not defined'); - - expect(window.vaq[0]).toEqual([ - 'event', - { - name: 'custom event', - data: { - string: 'string', - number: 1, - }, - }, - ]); + expect(window.vaq?.[0]).toEqual(['event', { name, data }]); }); }); }); diff --git a/packages/web/src/react.tsx b/packages/web/src/react.tsx index d86b9e2..54d1377 100644 --- a/packages/web/src/react.tsx +++ b/packages/web/src/react.tsx @@ -33,6 +33,12 @@ function Analytics( path?: string | null; } ): null { + useEffect(() => { + if (props.beforeSend) { + window.va?.('beforeSend', props.beforeSend); + } + }, [props.beforeSend]); + // biome-ignore lint/correctness/useExhaustiveDependencies: only run once useEffect(() => { inject({