From 51095aece38b48d90922694f873fff5b2ca9177c Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 16:00:15 +0000 Subject: [PATCH 1/9] chore: convert more tests to TS --- .../{page-view.ts => page-view.test.ts} | 0 src/__tests__/request-queue.js | 119 -------- src/__tests__/request-queue.test.ts | 128 ++++++++ .../{retry-queue.js => retry-queue.test.ts} | 131 ++++---- src/__tests__/{surveys.js => surveys.test.ts} | 287 ++++++++++-------- .../{test-uuid.js => test-uuid.test.ts} | 0 src/__tests__/utils.js | 261 ---------------- src/__tests__/utils.test.ts | 279 +++++++++++++++++ 8 files changed, 631 insertions(+), 574 deletions(-) rename src/__tests__/{page-view.ts => page-view.test.ts} (100%) delete mode 100644 src/__tests__/request-queue.js create mode 100644 src/__tests__/request-queue.test.ts rename src/__tests__/{retry-queue.js => retry-queue.test.ts} (71%) rename src/__tests__/{surveys.js => surveys.test.ts} (59%) rename src/__tests__/{test-uuid.js => test-uuid.test.ts} (100%) delete mode 100644 src/__tests__/utils.js create mode 100644 src/__tests__/utils.test.ts diff --git a/src/__tests__/page-view.ts b/src/__tests__/page-view.test.ts similarity index 100% rename from src/__tests__/page-view.ts rename to src/__tests__/page-view.test.ts diff --git a/src/__tests__/request-queue.js b/src/__tests__/request-queue.js deleted file mode 100644 index 8586b7446..000000000 --- a/src/__tests__/request-queue.js +++ /dev/null @@ -1,119 +0,0 @@ -import { RequestQueue } from '../request-queue' - -const EPOCH = 1_600_000_000 - -describe('RequestQueue', () => { - given('queue', () => new RequestQueue(given.handlePollRequest)) - given('handlePollRequest', () => jest.fn()) - - beforeEach(() => { - jest.useFakeTimers() - - jest.spyOn(given.queue, 'getTime').mockReturnValue(EPOCH) - jest.spyOn(console, 'warn').mockImplementation(() => {}) - }) - - it('handles poll after enqueueing requests', () => { - given.queue.enqueue('/e', { event: 'foo', timestamp: EPOCH - 3000 }, { transport: 'XHR' }) - given.queue.enqueue('/identify', { event: '$identify', timestamp: EPOCH - 2000 }) - given.queue.enqueue('/e', { event: 'bar', timestamp: EPOCH - 1000 }) - given.queue.enqueue('/e', { event: 'zeta', timestamp: EPOCH }, { _batchKey: 'sessionRecording' }) - - given.queue.poll() - - expect(given.handlePollRequest).toHaveBeenCalledTimes(0) - - jest.runOnlyPendingTimers() - - expect(given.handlePollRequest).toHaveBeenCalledTimes(3) - expect(given.handlePollRequest).toHaveBeenCalledWith( - '/e', - [ - { event: 'foo', offset: 3000 }, - { event: 'bar', offset: 1000 }, - ], - { transport: 'XHR' } - ) - expect(given.handlePollRequest).toHaveBeenCalledWith( - '/identify', - [{ event: '$identify', offset: 2000 }], - undefined - ) - expect(given.handlePollRequest).toHaveBeenCalledWith('/e', [{ event: 'zeta', offset: 0 }], { - _batchKey: 'sessionRecording', - }) - }) - - it('clears polling flag after 4 empty iterations', () => { - given.queue.enqueue('/e', { event: 'foo', timestamp: EPOCH - 3000 }) - - for (let i = 0; i < 5; i++) { - given.queue.poll() - jest.runOnlyPendingTimers() - - expect(given.queue.isPolling).toEqual(true) - } - - given.queue.poll() - jest.runOnlyPendingTimers() - - expect(given.queue.isPolling).toEqual(false) - }) - - it('handles unload', () => { - given.queue.enqueue('/s', { recording_payload: 'example' }) - given.queue.enqueue('/e', { event: 'foo', timestamp: 1_610_000_000 }) - given.queue.enqueue('/identify', { event: '$identify', timestamp: 1_620_000_000 }) - given.queue.enqueue('/e', { event: 'bar', timestamp: 1_630_000_000 }) - - given.queue.unload() - - expect(given.handlePollRequest).toHaveBeenCalledTimes(3) - expect(given.handlePollRequest).toHaveBeenNthCalledWith( - 1, - '/e', - [ - { event: 'foo', timestamp: 1_610_000_000 }, - { event: 'bar', timestamp: 1_630_000_000 }, - ], - { transport: 'sendBeacon' } - ) - expect(given.handlePollRequest).toHaveBeenNthCalledWith(2, '/s', [{ recording_payload: 'example' }], { - transport: 'sendBeacon', - }) - expect(given.handlePollRequest).toHaveBeenNthCalledWith( - 3, - '/identify', - [{ event: '$identify', timestamp: 1_620_000_000 }], - { transport: 'sendBeacon' } - ) - }) - - it('handles unload with batchKeys', () => { - given.queue.enqueue('/e', { event: 'foo', timestamp: 1_610_000_000 }, { transport: 'XHR' }) - given.queue.enqueue('/identify', { event: '$identify', timestamp: 1_620_000_000 }) - given.queue.enqueue('/e', { event: 'bar', timestamp: 1_630_000_000 }) - given.queue.enqueue('/e', { event: 'zeta', timestamp: 1_640_000_000 }, { _batchKey: 'sessionRecording' }) - - given.queue.unload() - - expect(given.handlePollRequest).toHaveBeenCalledTimes(3) - expect(given.handlePollRequest).toHaveBeenCalledWith( - '/e', - [ - { event: 'foo', timestamp: 1_610_000_000 }, - { event: 'bar', timestamp: 1_630_000_000 }, - ], - { transport: 'sendBeacon' } - ) - expect(given.handlePollRequest).toHaveBeenCalledWith( - '/identify', - [{ event: '$identify', timestamp: 1_620_000_000 }], - { transport: 'sendBeacon' } - ) - expect(given.handlePollRequest).toHaveBeenCalledWith('/e', [{ event: 'zeta', timestamp: 1_640_000_000 }], { - _batchKey: 'sessionRecording', - transport: 'sendBeacon', - }) - }) -}) diff --git a/src/__tests__/request-queue.test.ts b/src/__tests__/request-queue.test.ts new file mode 100644 index 000000000..44a0b69e4 --- /dev/null +++ b/src/__tests__/request-queue.test.ts @@ -0,0 +1,128 @@ +import { RequestQueue } from '../request-queue' +import { CaptureOptions, Properties, XHROptions } from '../types' + +const EPOCH = 1_600_000_000 + +describe('RequestQueue', () => { + let handlePollRequest: (url: string, data: Properties, options?: XHROptions) => void + let queue: RequestQueue + + beforeEach(() => { + handlePollRequest = jest.fn() + queue = new RequestQueue(handlePollRequest) + jest.useFakeTimers() + + jest.spyOn(queue, 'getTime').mockReturnValue(EPOCH) + jest.spyOn(console, 'warn').mockImplementation(() => {}) + }) + + it('handles poll after enqueueing requests', () => { + queue.enqueue('/e', { event: 'foo', timestamp: EPOCH - 3000 }, { transport: 'XHR' }) + queue.enqueue('/identify', { event: '$identify', timestamp: EPOCH - 2000 }, {}) + queue.enqueue('/e', { event: 'bar', timestamp: EPOCH - 1000 }, {}) + queue.enqueue('/e', { event: 'zeta', timestamp: EPOCH }, { + _batchKey: 'sessionRecording', + } as CaptureOptions as XHROptions) + + queue.poll() + + expect(handlePollRequest).toHaveBeenCalledTimes(0) + + jest.runOnlyPendingTimers() + + expect(handlePollRequest).toHaveBeenCalledTimes(3) + expect(jest.mocked(handlePollRequest).mock.calls).toEqual([ + [ + '/e', + [ + { event: 'foo', offset: 3000 }, + { event: 'bar', offset: 1000 }, + ], + { transport: 'XHR' }, + ], + ['/identify', [{ event: '$identify', offset: 2000 }], {}], + [ + '/e', + [{ event: 'zeta', offset: 0 }], + { + _batchKey: 'sessionRecording', + }, + ], + ]) + }) + + it('clears polling flag after 4 empty iterations', () => { + queue.enqueue('/e', { event: 'foo', timestamp: EPOCH - 3000 }, {}) + + for (let i = 0; i < 5; i++) { + queue.poll() + jest.runOnlyPendingTimers() + + expect(queue.isPolling).toEqual(true) + } + + queue.poll() + jest.runOnlyPendingTimers() + + expect(queue.isPolling).toEqual(false) + }) + + it('handles unload', () => { + queue.enqueue('/s', { recording_payload: 'example' }, {}) + queue.enqueue('/e', { event: 'foo', timestamp: 1_610_000_000 }, {}) + queue.enqueue('/identify', { event: '$identify', timestamp: 1_620_000_000 }, {}) + queue.enqueue('/e', { event: 'bar', timestamp: 1_630_000_000 }, {}) + + queue.unload() + + expect(handlePollRequest).toHaveBeenCalledTimes(3) + expect(handlePollRequest).toHaveBeenNthCalledWith( + 1, + '/e', + [ + { event: 'foo', timestamp: 1_610_000_000 }, + { event: 'bar', timestamp: 1_630_000_000 }, + ], + { transport: 'sendBeacon' } + ) + expect(handlePollRequest).toHaveBeenNthCalledWith(2, '/s', [{ recording_payload: 'example' }], { + transport: 'sendBeacon', + }) + expect(handlePollRequest).toHaveBeenNthCalledWith( + 3, + '/identify', + [{ event: '$identify', timestamp: 1_620_000_000 }], + { transport: 'sendBeacon' } + ) + }) + + it('handles unload with batchKeys', () => { + queue.enqueue('/e', { event: 'foo', timestamp: 1_610_000_000 }, { transport: 'XHR' }) + queue.enqueue('/identify', { event: '$identify', timestamp: 1_620_000_000 }, {}) + queue.enqueue('/e', { event: 'bar', timestamp: 1_630_000_000 }, {}) + queue.enqueue('/e', { event: 'zeta', timestamp: 1_640_000_000 }, { + _batchKey: 'sessionRecording', + } as CaptureOptions as XHROptions) + + queue.unload() + + expect(handlePollRequest).toHaveBeenCalledTimes(3) + expect(handlePollRequest).toHaveBeenCalledWith( + '/e', + [ + { event: 'foo', timestamp: 1_610_000_000 }, + { event: 'bar', timestamp: 1_630_000_000 }, + ], + { transport: 'sendBeacon' } + ) + expect(handlePollRequest).toHaveBeenCalledWith( + '/identify', + [{ event: '$identify', timestamp: 1_620_000_000 }], + { transport: 'sendBeacon' } + ) + expect(handlePollRequest).toHaveBeenCalledWith('/e', [{ event: 'zeta', timestamp: 1_640_000_000 }], { + _batchKey: 'sessionRecording', + transport: 'sendBeacon', + }) + }) +}) diff --git a/src/__tests__/retry-queue.js b/src/__tests__/retry-queue.test.ts similarity index 71% rename from src/__tests__/retry-queue.js rename to src/__tests__/retry-queue.test.ts index 32a8a22d6..83356ffb3 100644 --- a/src/__tests__/retry-queue.js +++ b/src/__tests__/retry-queue.test.ts @@ -4,35 +4,36 @@ import { pickNextRetryDelay, RetryQueue } from '../retry-queue' import * as SendRequest from '../send-request' import { RateLimiter } from '../rate-limiter' import { SESSION_RECORDING_BATCH_KEY } from '../extensions/replay/sessionrecording' +import { assignableWindow } from '../utils/globals' +import { CaptureOptions } from '../types' const EPOCH = 1_600_000_000 -const defaultRequestOptions = { +const defaultRequestOptions: CaptureOptions = { method: 'POST', transport: 'XHR', } describe('RetryQueue', () => { - given('rateLimiter', () => new RateLimiter()) - given('retryQueue', () => new RetryQueue(given.onXHRError, given.rateLimiter)) - given('onXHRError', () => jest.fn().mockImplementation(console.error)) - - given('xhrStatus', () => 418) + const onXHRError = jest.fn().mockImplementation(console.error) + const rateLimiter = new RateLimiter() + let retryQueue: RetryQueue const xhrMockClass = () => ({ open: jest.fn(), send: jest.fn(), setRequestHeader: jest.fn(), - status: given.xhrStatus, + status: 418, }) beforeEach(() => { - window.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass) - window.navigator.sendBeacon = jest.fn() + retryQueue = new RetryQueue(onXHRError, rateLimiter) + assignableWindow.XMLHttpRequest = jest.fn().mockImplementation(xhrMockClass) + assignableWindow.navigator.sendBeacon = jest.fn() jest.useFakeTimers() - jest.spyOn(given.retryQueue, 'getTime').mockReturnValue(EPOCH) - jest.spyOn(window.console, 'warn').mockImplementation() - given.rateLimiter.limits = {} + jest.spyOn(retryQueue, 'getTime').mockReturnValue(EPOCH) + jest.spyOn(assignableWindow.console, 'warn').mockImplementation() + rateLimiter.limits = {} }) const fastForwardTimeAndRunTimer = () => { @@ -41,22 +42,22 @@ describe('RetryQueue', () => { } const enqueueRequests = () => { - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'foo', timestamp: EPOCH - 3000 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'bar', timestamp: EPOCH - 2000 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'baz', timestamp: EPOCH - 1000 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'fizz', timestamp: EPOCH }, options: defaultRequestOptions, @@ -66,9 +67,9 @@ describe('RetryQueue', () => { it('processes retry requests', () => { enqueueRequests() - expect(given.retryQueue.queue.length).toEqual(4) + expect(retryQueue.queue.length).toEqual(4) - expect(given.retryQueue.queue).toEqual([ + expect(retryQueue.queue).toEqual([ { requestData: { url: '/e', @@ -103,161 +104,161 @@ describe('RetryQueue', () => { }, ]) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(0) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(0) fastForwardTimeAndRunTimer() // clears queue - expect(given.retryQueue.queue.length).toEqual(0) + expect(retryQueue.queue.length).toEqual(0) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(4) - expect(given.onXHRError).toHaveBeenCalledTimes(0) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(4) + expect(onXHRError).toHaveBeenCalledTimes(0) }) it('does not process event retry requests when events are rate limited', () => { - given.rateLimiter.limits = { + rateLimiter.limits = { events: new Date().getTime() + 10_000, } - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'baz', timestamp: EPOCH - 1000 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'baz', timestamp: EPOCH - 500 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/s', data: { event: 'fizz', timestamp: EPOCH }, options: { ...defaultRequestOptions, _batchKey: SESSION_RECORDING_BATCH_KEY }, }) - expect(given.retryQueue.queue.length).toEqual(3) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(0) + expect(retryQueue.queue.length).toEqual(3) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(0) fastForwardTimeAndRunTimer() // clears queue - expect(given.retryQueue.queue.length).toEqual(0) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(1) - expect(given.onXHRError).toHaveBeenCalledTimes(0) + expect(retryQueue.queue.length).toEqual(0) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(1) + expect(onXHRError).toHaveBeenCalledTimes(0) }) it('does not process recording retry requests when they are rate limited', () => { - given.rateLimiter.limits = { + rateLimiter.limits = { [SESSION_RECORDING_BATCH_KEY]: new Date().getTime() + 10_000, } - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'baz', timestamp: EPOCH - 1000 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'baz', timestamp: EPOCH - 500 }, options: defaultRequestOptions, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/s', data: { event: 'fizz', timestamp: EPOCH }, options: { ...defaultRequestOptions, _batchKey: SESSION_RECORDING_BATCH_KEY }, }) - expect(given.retryQueue.queue.length).toEqual(3) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(0) + expect(retryQueue.queue.length).toEqual(3) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(0) fastForwardTimeAndRunTimer() // clears queue - expect(given.retryQueue.queue.length).toEqual(0) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(2) - expect(given.onXHRError).toHaveBeenCalledTimes(0) + expect(retryQueue.queue.length).toEqual(0) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(2) + expect(onXHRError).toHaveBeenCalledTimes(0) }) it('tries to send requests via beacon on unload', () => { enqueueRequests() - given.retryQueue.poll() - given.retryQueue.unload() + retryQueue.poll() + retryQueue.unload() - expect(given.retryQueue.queue.length).toEqual(0) - expect(window.navigator.sendBeacon).toHaveBeenCalledTimes(4) + expect(retryQueue.queue.length).toEqual(0) + expect(assignableWindow.navigator.sendBeacon).toHaveBeenCalledTimes(4) }) it('does not try to send requests via beacon on unload when rate limited', () => { - given.rateLimiter.limits = { + rateLimiter.limits = { events: new Date().getTime() + 10_000, } enqueueRequests() - given.retryQueue.unload() + retryQueue.unload() - expect(given.retryQueue.queue.length).toEqual(0) - expect(window.navigator.sendBeacon).toHaveBeenCalledTimes(0) + expect(retryQueue.queue.length).toEqual(0) + expect(assignableWindow.navigator.sendBeacon).toHaveBeenCalledTimes(0) }) it('when you flush the queue onXHRError is passed to xhr', () => { const xhrSpy = jest.spyOn(SendRequest, 'xhr') enqueueRequests() - given.retryQueue.flush() + retryQueue.flush() fastForwardTimeAndRunTimer() - expect(xhrSpy).toHaveBeenCalledWith(expect.objectContaining({ onXHRError: given.onXHRError })) + expect(xhrSpy).toHaveBeenCalledWith(expect.objectContaining({ onXHRError: onXHRError })) }) it('enqueues requests when offline and flushes immediately when online again', () => { - given.retryQueue.areWeOnline = false - expect(given.retryQueue.areWeOnline).toEqual(false) + retryQueue.areWeOnline = false + expect(retryQueue.areWeOnline).toEqual(false) enqueueRequests() fastForwardTimeAndRunTimer() // requests aren't attempted when we're offline - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(0) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(0) // doesn't log that it is offline from the retry queue - expect(given.onXHRError).toHaveBeenCalledTimes(0) + expect(onXHRError).toHaveBeenCalledTimes(0) // queue stays the same - expect(given.retryQueue.queue.length).toEqual(4) + expect(retryQueue.queue.length).toEqual(4) - given.retryQueue._handleWeAreNowOnline() + retryQueue._handleWeAreNowOnline() - expect(given.retryQueue.areWeOnline).toEqual(true) - expect(given.retryQueue.queue.length).toEqual(0) + expect(retryQueue.areWeOnline).toEqual(true) + expect(retryQueue.queue.length).toEqual(0) - expect(window.XMLHttpRequest).toHaveBeenCalledTimes(4) + expect(assignableWindow.XMLHttpRequest).toHaveBeenCalledTimes(4) }) it('retries using an exponential backoff mechanism', () => { const fixedDate = new Date('2021-05-31T00:00:00') jest.spyOn(global.Date, 'now').mockImplementation(() => fixedDate.getTime()) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: '1retry', timestamp: EPOCH }, options: defaultRequestOptions, retriesPerformedSoFar: 1, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: '5retries', timestamp: EPOCH }, options: defaultRequestOptions, retriesPerformedSoFar: 5, }) - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: '9retries', timestamp: EPOCH }, options: defaultRequestOptions, retriesPerformedSoFar: 9, }) - expect(given.retryQueue.queue).toEqual([ + expect(retryQueue.queue).toEqual([ { requestData: { url: '/e', @@ -289,14 +290,14 @@ describe('RetryQueue', () => { }) it('does not enqueue a request after 10 retries', () => { - given.retryQueue.enqueue({ + retryQueue.enqueue({ url: '/e', data: { event: 'maxretries', timestamp: EPOCH }, options: defaultRequestOptions, retriesPerformedSoFar: 10, }) - expect(given.retryQueue.queue.length).toEqual(0) + expect(retryQueue.queue.length).toEqual(0) }) describe('backoff calculation', () => { diff --git a/src/__tests__/surveys.js b/src/__tests__/surveys.test.ts similarity index 59% rename from src/__tests__/surveys.js rename to src/__tests__/surveys.test.ts index 7fee65890..301c934a1 100644 --- a/src/__tests__/surveys.js +++ b/src/__tests__/surveys.test.ts @@ -1,138 +1,175 @@ +/// + import { PostHogSurveys } from '../posthog-surveys' -import { SurveyType, SurveyQuestionType } from '../posthog-surveys-types' +import { SurveyType, SurveyQuestionType, Survey } from '../posthog-surveys-types' import { PostHogPersistence } from '../posthog-persistence' +import { PostHog } from '../posthog-core' +import { DecideResponse, PostHogConfig, Properties } from '../types' +import { window } from '../utils/globals' describe('surveys', () => { - given('config', () => ({ - token: 'testtoken', - api_host: 'https://app.posthog.com', - persistence: 'memory', - })) - given('instance', () => ({ - config: given.config, - _prepare_callback: (callback) => callback, - persistence: new PostHogPersistence(given.config), - register: (props) => given.instance.persistence.register(props), - unregister: (key) => given.instance.persistence.unregister(key), - get_property: (key) => given.instance.persistence.props[key], - _send_request: jest.fn().mockImplementation((url, data, headers, callback) => callback(given.surveysResponse)), + let config: PostHogConfig + let instance: PostHog + let surveys: PostHogSurveys + let surveysResponse: { status?: number; surveys?: Survey[] } + const originalWindowLocation = window!.location + + const decideResponse = { featureFlags: { - _send_request: jest - .fn() - .mockImplementation((url, data, headers, callback) => callback(given.decideResponse)), - isFeatureEnabled: jest - .fn() - .mockImplementation((featureFlag) => given.decideResponse.featureFlags[featureFlag]), + 'linked-flag-key': true, + 'survey-targeting-flag-key': true, + 'linked-flag-key2': true, + 'survey-targeting-flag-key2': false, }, - })) + } as unknown as DecideResponse - given('surveys', () => new PostHogSurveys(given.instance)) - - afterEach(() => { - given.instance.persistence.clear() - }) - - const firstSurveys = [ + const firstSurveys: Survey[] = [ { name: 'first survey', description: 'first survey description', type: SurveyType.Popover, questions: [{ type: SurveyQuestionType.Open, question: 'what is a bokoblin?' }], - }, + } as unknown as Survey, ] - const secondSurveys = [ + const secondSurveys: Survey[] = [ { name: 'first survey', description: 'first survey description', type: SurveyType.Popover, questions: [{ type: SurveyQuestionType.Open, question: 'what is a bokoblin?' }], - }, + } as unknown as Survey, { name: 'second survey', description: 'second survey description', type: SurveyType.Popover, questions: [{ type: SurveyQuestionType.Open, question: 'what is a moblin?' }], - }, + } as unknown as Survey, ] - given('surveysResponse', () => ({ surveys: firstSurveys })) + beforeEach(() => { + surveysResponse = { surveys: firstSurveys } + + config = { + token: 'testtoken', + api_host: 'https://app.posthog.com', + persistence: 'memory', + } as unknown as PostHogConfig + + instance = { + config: config, + _prepare_callback: (callback: any) => callback, + persistence: new PostHogPersistence(config), + register: (props: Properties) => instance.persistence?.register(props), + unregister: (key: string) => instance.persistence?.unregister(key), + get_property: (key: string) => instance.persistence?.props[key], + _send_request: jest.fn().mockImplementation((_url, _data, _headers, callback) => callback(surveysResponse)), + featureFlags: { + _send_request: jest + .fn() + .mockImplementation((_url, _data, _headers, callback) => callback(decideResponse)), + isFeatureEnabled: jest + .fn() + .mockImplementation((featureFlag) => decideResponse.featureFlags[featureFlag]), + }, + } as unknown as PostHog + + surveys = new PostHogSurveys(instance) + + Object.defineProperty(window, 'location', { + configurable: true, + enumerable: true, + writable: true, + // eslint-disable-next-line compat/compat + value: new URL('https://example.com'), + }) + }) + + afterEach(() => { + instance.persistence?.clear() + + Object.defineProperty(window, 'location', { + configurable: true, + enumerable: true, + value: originalWindowLocation, + }) + }) it('getSurveys gets a list of surveys if not present already', () => { - given.surveys.getSurveys((data) => { + surveys.getSurveys((data) => { expect(data).toEqual(firstSurveys) }) - expect(given.instance._send_request).toHaveBeenCalledWith( + expect(instance._send_request).toHaveBeenCalledWith( 'https://app.posthog.com/api/surveys/?token=testtoken', {}, { method: 'GET' }, expect.any(Function) ) - expect(given.instance._send_request).toHaveBeenCalledTimes(1) - expect(given.instance.persistence.props.$surveys).toEqual(firstSurveys) + expect(instance._send_request).toHaveBeenCalledTimes(1) + expect(instance.persistence?.props.$surveys).toEqual(firstSurveys) - given('surveysResponse', () => ({ surveys: secondSurveys })) - given.surveys.getSurveys((data) => { + surveysResponse = { surveys: secondSurveys } + surveys.getSurveys((data) => { expect(data).toEqual(firstSurveys) }) // request again, shouldn't call _send_request again, so 1 total call instead of 2 - expect(given.instance._send_request).toHaveBeenCalledTimes(1) + expect(instance._send_request).toHaveBeenCalledTimes(1) }) it('getSurveys force reloads when called with true', () => { - given.surveys.getSurveys((data) => { + surveys.getSurveys((data) => { expect(data).toEqual(firstSurveys) }) - expect(given.instance._send_request).toHaveBeenCalledWith( + expect(instance._send_request).toHaveBeenCalledWith( 'https://app.posthog.com/api/surveys/?token=testtoken', {}, { method: 'GET' }, expect.any(Function) ) - expect(given.instance._send_request).toHaveBeenCalledTimes(1) - expect(given.instance.persistence.props.$surveys).toEqual(firstSurveys) + expect(instance._send_request).toHaveBeenCalledTimes(1) + expect(instance.persistence?.props.$surveys).toEqual(firstSurveys) - given('surveysResponse', () => ({ surveys: secondSurveys })) + surveysResponse = { surveys: secondSurveys } - given.surveys.getSurveys((data) => { + surveys.getSurveys((data) => { expect(data).toEqual(secondSurveys) }, true) - expect(given.instance.persistence.props.$surveys).toEqual(secondSurveys) - expect(given.instance._send_request).toHaveBeenCalledTimes(2) + expect(instance.persistence?.props.$surveys).toEqual(secondSurveys) + expect(instance._send_request).toHaveBeenCalledTimes(2) }) it('getSurveys returns empty array if surveys are undefined', () => { - given('surveysResponse', () => ({ status: 0 })) - given.surveys.getSurveys((data) => { + surveysResponse = { status: 0 } + surveys.getSurveys((data) => { expect(data).toEqual([]) }) }) describe('getActiveMatchingSurveys', () => { - const draftSurvey = { + const draftSurvey: Survey = { name: 'draft survey', description: 'draft survey description', type: SurveyType.Popover, questions: [{ type: SurveyQuestionType.Open, question: 'what is a draft survey?' }], start_date: null, - } - const activeSurvey = { + } as unknown as Survey + const activeSurvey: Survey = { name: 'active survey', description: 'active survey description', type: SurveyType.Popover, questions: [{ type: SurveyQuestionType.Open, question: 'what is a active survey?' }], start_date: new Date().toISOString(), end_date: null, - } - const completedSurvey = { + } as unknown as Survey + const completedSurvey: Survey = { name: 'completed survey', description: 'completed survey description', type: SurveyType.Popover, questions: [{ type: SurveyQuestionType.Open, question: 'what is a completed survey?' }], start_date: new Date('09/10/2022').toISOString(), end_date: new Date('10/10/2022').toISOString(), - } - const surveyWithUrl = { + } as unknown as Survey + const surveyWithUrl: Survey = { name: 'survey with url', description: 'survey with url description', type: SurveyType.Popover, @@ -140,8 +177,8 @@ describe('surveys', () => { conditions: { url: 'posthog.com' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithRegexUrl = { + } as unknown as Survey + const surveyWithRegexUrl: Survey = { name: 'survey with regex url', description: 'survey with regex url description', type: SurveyType.Popover, @@ -149,8 +186,8 @@ describe('surveys', () => { conditions: { url: 'regex-url', urlMatchType: 'regex' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithParamRegexUrl = { + } as unknown as Survey + const surveyWithParamRegexUrl: Survey = { name: 'survey with param regex url', description: 'survey with param regex url description', type: SurveyType.Popover, @@ -158,8 +195,8 @@ describe('surveys', () => { conditions: { url: '(\\?|\\&)(name.*)\\=([^&]+)', urlMatchType: 'regex' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithWildcardSubdomainUrl = { + } as unknown as Survey + const surveyWithWildcardSubdomainUrl: Survey = { name: 'survey with wildcard subdomain url', description: 'survey with wildcard subdomain url description', type: SurveyType.Popover, @@ -167,8 +204,8 @@ describe('surveys', () => { conditions: { url: '(.*.)?subdomain.com', urlMatchType: 'regex' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithWildcardRouteUrl = { + } as unknown as Survey + const surveyWithWildcardRouteUrl: Survey = { name: 'survey with wildcard route url', description: 'survey with wildcard route url description', type: SurveyType.Popover, @@ -176,8 +213,8 @@ describe('surveys', () => { conditions: { url: 'wildcard.com/(.*.)', urlMatchType: 'regex' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithExactUrlMatch = { + } as unknown as Survey + const surveyWithExactUrlMatch: Survey = { name: 'survey with wildcard route url', description: 'survey with wildcard route url description', type: SurveyType.Popover, @@ -185,8 +222,8 @@ describe('surveys', () => { conditions: { url: 'https://example.com/exact', urlMatchType: 'exact' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithSelector = { + } as unknown as Survey + const surveyWithSelector: Survey = { name: 'survey with selector', description: 'survey with selector description', type: SurveyType.Popover, @@ -194,8 +231,8 @@ describe('surveys', () => { conditions: { selector: '.test-selector' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithUrlAndSelector = { + } as unknown as Survey + const surveyWithUrlAndSelector: Survey = { name: 'survey with url and selector', description: 'survey with url and selector description', type: SurveyType.Popover, @@ -203,8 +240,8 @@ describe('surveys', () => { conditions: { url: 'posthogapp.com', selector: '#foo' }, start_date: new Date().toISOString(), end_date: null, - } - const surveyWithFlags = { + } as unknown as Survey + const surveyWithFlags: Survey = { name: 'survey with flags', description: 'survey with flags description', type: SurveyType.Popover, @@ -213,8 +250,8 @@ describe('surveys', () => { targeting_flag_key: 'survey-targeting-flag-key', start_date: new Date().toISOString(), end_date: null, - } - const surveyWithUnmatchedFlags = { + } as unknown as Survey + const surveyWithUnmatchedFlags: Survey = { name: 'survey with flags2', description: 'survey with flags description', type: SurveyType.Popover, @@ -223,8 +260,8 @@ describe('surveys', () => { targeting_flag_key: 'survey-targeting-flag-key2', start_date: new Date().toISOString(), end_date: null, - } - const surveyWithEverything = { + } as unknown as Survey + const surveyWithEverything: Survey = { name: 'survey with everything', description: 'survey with everything description', type: SurveyType.Popover, @@ -234,48 +271,51 @@ describe('surveys', () => { conditions: { url: 'posthogapp.com', selector: '.test-selector' }, linked_flag_key: 'linked-flag-key', targeting_flag_key: 'survey-targeting-flag-key', - } + } as unknown as Survey it('returns surveys that are active', () => { - given('surveysResponse', () => ({ surveys: [draftSurvey, activeSurvey, completedSurvey] })) + surveysResponse = { surveys: [draftSurvey, activeSurvey, completedSurvey] } - given.surveys.getActiveMatchingSurveys((data) => { + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([activeSurvey]) }) }) it('returns surveys based on url and selector matching', () => { - given('surveysResponse', () => ({ + surveysResponse = { surveys: [surveyWithUrl, surveyWithSelector, surveyWithUrlAndSelector], - })) - const originalWindowLocation = window.location - delete window.location + } // eslint-disable-next-line compat/compat - window.location = new URL('https://posthog.com') - given.surveys.getActiveMatchingSurveys((data) => { + window!.location = new URL('https://posthog.com') as unknown as Location + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithUrl]) }) - window.location = originalWindowLocation + window!.location = originalWindowLocation document.body.appendChild(document.createElement('div')).className = 'test-selector' - given.surveys.getActiveMatchingSurveys((data) => { + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithSelector]) }) - document.body.removeChild(document.querySelector('.test-selector')) + const testSelectorEl = document!.querySelector('.test-selector') + if (testSelectorEl) { + document.body.removeChild(testSelectorEl) + } // eslint-disable-next-line compat/compat - window.location = new URL('https://posthogapp.com') + window!.location = new URL('https://posthogapp.com') as unknown as Location document.body.appendChild(document.createElement('div')).id = 'foo' - given.surveys.getActiveMatchingSurveys((data) => { + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithUrlAndSelector]) }) - window.location = originalWindowLocation - document.body.removeChild(document.querySelector('#foo')) + const child = document.querySelector('#foo') + if (child) { + document.body.removeChild(child) + } }) it('returns surveys based on url with urlMatchType settings', () => { - given('surveysResponse', () => ({ + surveysResponse = { surveys: [ surveyWithRegexUrl, surveyWithParamRegexUrl, @@ -283,78 +323,67 @@ describe('surveys', () => { surveyWithWildcardSubdomainUrl, surveyWithExactUrlMatch, ], - })) + } - const originalWindowLocation = window.location - delete window.location + const originalWindowLocation = window!.location // eslint-disable-next-line compat/compat - window.location = new URL('https://regex-url.com/test') - given.surveys.getActiveMatchingSurveys((data) => { + window!.location = new URL('https://regex-url.com/test') as unknown as Location + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithRegexUrl]) }) - window.location = originalWindowLocation + window!.location = originalWindowLocation // eslint-disable-next-line compat/compat - window.location = new URL('https://example.com?name=something') - given.surveys.getActiveMatchingSurveys((data) => { + window!.location = new URL('https://example.com?name=something') as unknown as Location + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithParamRegexUrl]) }) - window.location = originalWindowLocation + window!.location = originalWindowLocation // eslint-disable-next-line compat/compat - window.location = new URL('https://app.subdomain.com') - given.surveys.getActiveMatchingSurveys((data) => { + window!.location = new URL('https://app.subdomain.com') as unknown as Location + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithWildcardSubdomainUrl]) }) - window.location = originalWindowLocation + window!.location = originalWindowLocation // eslint-disable-next-line compat/compat - window.location = new URL('https://wildcard.com/something/other') - given.surveys.getActiveMatchingSurveys((data) => { + window!.location = new URL('https://wildcard.com/something/other') as unknown as Location + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithWildcardRouteUrl]) }) - window.location = originalWindowLocation + window!.location = originalWindowLocation // eslint-disable-next-line compat/compat - window.location = new URL('https://example.com/exact') - given.surveys.getActiveMatchingSurveys((data) => { + window!.location = new URL('https://example.com/exact') as unknown as Location + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithExactUrlMatch]) }) - window.location = originalWindowLocation + window!.location = originalWindowLocation }) - given('decideResponse', () => ({ - featureFlags: { - 'linked-flag-key': true, - 'survey-targeting-flag-key': true, - 'linked-flag-key2': true, - 'survey-targeting-flag-key2': false, - }, - })) - it('returns surveys that match linked and targeting feature flags', () => { - given('surveysResponse', () => ({ surveys: [activeSurvey, surveyWithFlags, surveyWithEverything] })) - given.surveys.getActiveMatchingSurveys((data) => { + surveysResponse = { surveys: [activeSurvey, surveyWithFlags, surveyWithEverything] } + surveys.getActiveMatchingSurveys((data) => { // active survey is returned because it has no flags aka there are no restrictions on flag enabled for it expect(data).toEqual([activeSurvey, surveyWithFlags]) }) }) it('does not return surveys that have flag keys but no matching flags', () => { - given('surveysResponse', () => ({ surveys: [surveyWithFlags, surveyWithUnmatchedFlags] })) - given.surveys.getActiveMatchingSurveys((data) => { + surveysResponse = { surveys: [surveyWithFlags, surveyWithUnmatchedFlags] } + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([surveyWithFlags]) }) }) it('returns surveys that inclusively matches any of the above', () => { - window.location.delete // eslint-disable-next-line compat/compat - window.location = new URL('https://posthogapp.com') + window!.location = new URL('https://posthogapp.com') as unknown as Location document.body.appendChild(document.createElement('div')).className = 'test-selector' - given('surveysResponse', () => ({ surveys: [activeSurvey, surveyWithSelector, surveyWithEverything] })) + surveysResponse = { surveys: [activeSurvey, surveyWithSelector, surveyWithEverything] } // activeSurvey returns because there are no restrictions on conditions or flags on it - given.surveys.getActiveMatchingSurveys((data) => { + surveys.getActiveMatchingSurveys((data) => { expect(data).toEqual([activeSurvey, surveyWithSelector, surveyWithEverything]) }) }) diff --git a/src/__tests__/test-uuid.js b/src/__tests__/test-uuid.test.ts similarity index 100% rename from src/__tests__/test-uuid.js rename to src/__tests__/test-uuid.test.ts diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js deleted file mode 100644 index 94c8c4224..000000000 --- a/src/__tests__/utils.js +++ /dev/null @@ -1,261 +0,0 @@ -/* - * Test that basic SDK usage (init, capture, etc) does not - * blow up in non-browser (node.js) envs. These are not - * tests of server-side capturing functionality (which is - * currently not supported in the browser lib). - */ - -import { - _copyAndTruncateStrings, - _isBlockedUA, - DEFAULT_BLOCKED_UA_STRS, - loadScript, - isCrossDomainCookie, -} from '../utils' -import { _info } from '../utils/event-utils' - -function userAgentFor(botString) { - const randOne = (Math.random() + 1).toString(36).substring(7) - const randTwo = (Math.random() + 1).toString(36).substring(7) - return `Mozilla/5.0 (compatible; ${botString}/${randOne}; +http://a.com/bot/${randTwo})` -} - -describe('_.copyAndTruncateStrings', () => { - given('subject', () => _copyAndTruncateStrings(given.target, given.maxStringLength)) - - given('target', () => ({ - key: 'value', - [5]: 'looongvalue', - nested: { - keeeey: ['vaaaaaalue', 1, 99999999999.4], - }, - })) - given('maxStringLength', () => 5) - - it('truncates objects', () => { - expect(given.subject).toEqual({ - key: 'value', - [5]: 'looon', - nested: { - keeeey: ['vaaaa', 1, 99999999999.4], - }, - }) - }) - - it('makes a copy', () => { - const copy = given.subject - - given.target.foo = 'bar' - - expect(copy).not.toEqual(given.target) - }) - - it('does not truncate when passed null', () => { - given('maxStringLength', () => null) - - expect(given.subject).toEqual(given.subject) - }) - - it('handles recursive objects', () => { - given('target', () => { - const object = { key: 'vaaaaalue', values: ['fooobar'] } - object.values.push(object) - object.ref = object - return object - }) - - expect(given.subject).toEqual({ key: 'vaaaa', values: ['fooob', undefined] }) - }) - - it('does not truncate the apm raw performance property', () => { - const original = { - $performance_raw: 'longer_than_the_maximum', - } - given('target', () => original) - - expect(given.subject).toEqual(original) - }) - - it('handles frozen objects', () => { - const original = Object.freeze({ key: 'vaaaaalue' }) - given('target', () => original) - - expect(given.subject).toEqual({ key: 'vaaaa' }) - }) -}) - -describe('_.info', () => { - given('subject', () => _info) - - it('deviceType', () => { - const deviceTypes = { - // iPad - 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25': - 'Tablet', - // Samsung tablet - 'Mozilla/5.0 (Linux; Android 7.1.1; SM-T555 Build/NMF26X; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.96 Safari/537.36': - 'Tablet', - // Windows Chrome - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36': - 'Desktop', - // Mac Safari - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A': - 'Desktop', - // iPhone - 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Mobile/15E148 Safari/604.1': - 'Mobile', - // LG Android - 'Mozilla/5.0 (Linux; Android 6.0; LG-H631 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/38.0.2125.102 Mobile Safari/537.36': - 'Mobile', - } - - for (const [userAgent, deviceType] of Object.entries(deviceTypes)) { - expect(given.subject.deviceType(userAgent)).toEqual(deviceType) - } - }) - - it('osVersion', () => { - const osVersions = { - // Windows Phone - 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635; BOOST) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537': - { os_name: 'Windows Phone', os_version: '' }, - 'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36': { - os_name: 'Windows', - os_version: '6.3', - }, - 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/44.0.2403.67 Mobile/12D508 Safari/600.1.4': - { - os_name: 'iOS', - os_version: '8.2.0', - }, - 'Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H143 Safari/600.1.4': - { - os_name: 'iOS', - os_version: '8.4.0', - }, - 'Mozilla/5.0 (Linux; Android 4.4.2; Lenovo A7600-F Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Safari/537.36': - { - os_name: 'Android', - os_version: '4.4.2', - }, - 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9300; es) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.480 Mobile Safari/534.8+': - { - os_name: 'BlackBerry', - os_version: '', - }, - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36': - { - os_name: 'Mac OS X', - os_version: '10.9.5', - }, - 'Opera/9.80 (Linux armv7l; InettvBrowser/2.2 (00014A;SonyDTV140;0001;0001) KDL40W600B; CC/MEX) Presto/2.12.407 Version/12.50': - { - os_name: 'Linux', - os_version: '', - }, - 'Mozilla/5.0 (X11; CrOS armv7l 6680.81.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36': - { - os_name: 'Chrome OS', - os_version: '', - }, - } - - for (const [userAgent, osInfo] of Object.entries(osVersions)) { - expect(given.subject.os(userAgent)).toEqual(osInfo) - } - }) - - it('properties', () => { - const properties = given.subject.properties() - - expect(properties['$lib']).toEqual('web') - expect(properties['$device_type']).toEqual('Desktop') - }) -}) - -describe('loadScript', () => { - beforeEach(() => { - document.getElementsByTagName('html')[0].innerHTML = '' - }) - - it('should insert the given script before the one already on the page', () => { - document.body.appendChild(document.createElement('script')) - const callback = jest.fn() - loadScript('https://fake_url', callback) - const scripts = document.getElementsByTagName('script') - const new_script = scripts[0] - - expect(scripts.length).toBe(2) - expect(new_script.type).toBe('text/javascript') - expect(new_script.src).toBe('https://fake_url/') - new_script.onload('test') - expect(callback).toHaveBeenCalledWith(undefined, 'test') - }) - - it("should add the script to the page when there aren't any preexisting scripts on the page", () => { - const callback = jest.fn() - loadScript('https://fake_url', callback) - const scripts = document.getElementsByTagName('script') - const new_script = scripts[0] - - expect(scripts.length).toBe(1) - expect(new_script.type).toBe('text/javascript') - expect(new_script.src).toBe('https://fake_url/') - }) - - it('should respond with an error if one happens', () => { - const callback = jest.fn() - loadScript('https://fake_url', callback) - const scripts = document.getElementsByTagName('script') - const new_script = scripts[0] - - new_script.onerror('uh-oh') - expect(callback).toHaveBeenCalledWith('uh-oh') - }) - - describe('user agent blocking', () => { - it.each(DEFAULT_BLOCKED_UA_STRS.concat('testington'))( - 'blocks a bot based on the user agent %s', - (botString) => { - const randomisedUserAgent = userAgentFor(botString) - - expect(_isBlockedUA(randomisedUserAgent, ['testington'])).toBe(true) - } - ) - - it('should block googlebot desktop', () => { - expect( - _isBlockedUA( - 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36', - [] - ) - ).toBe(true) - }) - - it('should block openai bot', () => { - expect( - _isBlockedUA( - 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.0; +https://openai.com/gptbot)', - [] - ) - ).toBe(true) - }) - }) - - describe('check for cross domain cookies', () => { - it.each([ - [false, 'https://test.herokuapp.com'], - [false, 'test.herokuapp.com'], - [false, 'herokuapp.com'], - [false, undefined], - // ensure it isn't matching herokuapp anywhere in the domain - [true, 'https://test.herokuapp.com.impersonator.io'], - [true, 'mysite-herokuapp.com'], - [true, 'https://bbc.co.uk'], - [true, 'bbc.co.uk'], - [true, 'www.bbc.co.uk'], - ])('should return %s when hostname is %s', (expectedResult, hostname) => { - expect(isCrossDomainCookie({ hostname })).toEqual(expectedResult) - }) - }) -}) diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts new file mode 100644 index 000000000..a3646b11d --- /dev/null +++ b/src/__tests__/utils.test.ts @@ -0,0 +1,279 @@ +/// + +/* + * Test that basic SDK usage (init, capture, etc) does not + * blow up in non-browser (node.js) envs. These are not + * tests of server-side capturing functionality (which is + * currently not supported in the browser lib). + */ + +import { + _copyAndTruncateStrings, + _isBlockedUA, + DEFAULT_BLOCKED_UA_STRS, + loadScript, + isCrossDomainCookie, + _base64Encode, +} from '../utils' +import { _info } from '../utils/event-utils' +import { document } from '../utils/globals' + +function userAgentFor(botString: string) { + const randOne = (Math.random() + 1).toString(36).substring(7) + const randTwo = (Math.random() + 1).toString(36).substring(7) + return `Mozilla/5.0 (compatible; ${botString}/${randOne}; +http://a.com/bot/${randTwo})` +} + +describe('utils', () => { + describe('_.copyAndTruncateStrings', () => { + let target: Record + + beforeEach(() => { + target = { + key: 'value', + [5]: 'looongvalue', + nested: { + keeeey: ['vaaaaaalue', 1, 99999999999.4], + }, + } + }) + + it('truncates objects', () => { + expect(_copyAndTruncateStrings(target, 5)).toEqual({ + key: 'value', + [5]: 'looon', + nested: { + keeeey: ['vaaaa', 1, 99999999999.4], + }, + }) + }) + + it('makes a copy', () => { + const copy = _copyAndTruncateStrings(target, 5) + + target.foo = 'bar' + + expect(copy).not.toEqual(target) + }) + + it('does not truncate when passed null', () => { + expect(_copyAndTruncateStrings(target, null)).toEqual(target) + }) + + it('handles recursive objects', () => { + const recursiveObject: Record = { key: 'vaaaaalue', values: ['fooobar'] } + recursiveObject.values.push(recursiveObject) + recursiveObject.ref = recursiveObject + + expect(_copyAndTruncateStrings(recursiveObject, 5)).toEqual({ key: 'vaaaa', values: ['fooob', undefined] }) + }) + + it('handles frozen objects', () => { + const original = Object.freeze({ key: 'vaaaaalue' }) + expect(_copyAndTruncateStrings(original, 5)).toEqual({ key: 'vaaaa' }) + }) + }) + + describe('_.info', () => { + it('deviceType', () => { + const deviceTypes = { + // iPad + 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25': + 'Tablet', + // Samsung tablet + 'Mozilla/5.0 (Linux; Android 7.1.1; SM-T555 Build/NMF26X; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.96 Safari/537.36': + 'Tablet', + // Windows Chrome + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36': + 'Desktop', + // Mac Safari + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A': + 'Desktop', + // iPhone + 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Mobile/15E148 Safari/604.1': + 'Mobile', + // LG Android + 'Mozilla/5.0 (Linux; Android 6.0; LG-H631 Build/MRA58K) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/38.0.2125.102 Mobile Safari/537.36': + 'Mobile', + } + + for (const [userAgent, deviceType] of Object.entries(deviceTypes)) { + expect(_info.deviceType(userAgent)).toEqual(deviceType) + } + }) + + it('osVersion', () => { + const osVersions = { + // Windows Phone + 'Mozilla/5.0 (Mobile; Windows Phone 8.1; Android 4.0; ARM; Trident/7.0; Touch; rv:11.0; IEMobile/11.0; NOKIA; Lumia 635; BOOST) like iPhone OS 7_0_3 Mac OS X AppleWebKit/537 (KHTML, like Gecko) Mobile Safari/537': + { os_name: 'Windows Phone', os_version: '' }, + 'Mozilla/5.0 (Windows NT 6.3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36': + { + os_name: 'Windows', + os_version: '6.3', + }, + 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_2 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) CriOS/44.0.2403.67 Mobile/12D508 Safari/600.1.4': + { + os_name: 'iOS', + os_version: '8.2.0', + }, + 'Mozilla/5.0 (iPad; CPU OS 8_4 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H143 Safari/600.1.4': + { + os_name: 'iOS', + os_version: '8.4.0', + }, + 'Mozilla/5.0 (Linux; Android 4.4.2; Lenovo A7600-F Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.133 Safari/537.36': + { + os_name: 'Android', + os_version: '4.4.2', + }, + 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9300; es) AppleWebKit/534.8+ (KHTML, like Gecko) Version/6.0.0.480 Mobile Safari/534.8+': + { + os_name: 'BlackBerry', + os_version: '', + }, + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36': + { + os_name: 'Mac OS X', + os_version: '10.9.5', + }, + 'Opera/9.80 (Linux armv7l; InettvBrowser/2.2 (00014A;SonyDTV140;0001;0001) KDL40W600B; CC/MEX) Presto/2.12.407 Version/12.50': + { + os_name: 'Linux', + os_version: '', + }, + 'Mozilla/5.0 (X11; CrOS armv7l 6680.81.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.118 Safari/537.36': + { + os_name: 'Chrome OS', + os_version: '', + }, + } + + for (const [userAgent, osInfo] of Object.entries(osVersions)) { + expect(_info.os(userAgent)).toEqual(osInfo) + } + }) + + it('properties', () => { + const properties = _info.properties() + + expect(properties['$lib']).toEqual('web') + expect(properties['$device_type']).toEqual('Desktop') + }) + }) + + describe('loadScript', () => { + beforeEach(() => { + document!.getElementsByTagName('html')![0].innerHTML = '' + }) + + it('should insert the given script before the one already on the page', () => { + document!.body.appendChild(document!.createElement('script')) + const callback = jest.fn() + loadScript('https://fake_url', callback) + const scripts = document!.getElementsByTagName('script') + const new_script = scripts[0] + + expect(scripts.length).toBe(2) + expect(new_script.type).toBe('text/javascript') + expect(new_script.src).toBe('https://fake_url/') + const event = new Event('test') + new_script.onload!(event) + expect(callback).toHaveBeenCalledWith(undefined, event) + }) + + it("should add the script to the page when there aren't any preexisting scripts on the page", () => { + const callback = jest.fn() + loadScript('https://fake_url', callback) + const scripts = document!.getElementsByTagName('script') + + expect(scripts?.length).toBe(1) + expect(scripts![0].type).toBe('text/javascript') + expect(scripts![0].src).toBe('https://fake_url/') + }) + + it('should respond with an error if one happens', () => { + const callback = jest.fn() + loadScript('https://fake_url', callback) + const scripts = document!.getElementsByTagName('script') + const new_script = scripts[0] + + new_script.onerror!('uh-oh') + expect(callback).toHaveBeenCalledWith('uh-oh') + }) + + describe('user agent blocking', () => { + it.each(DEFAULT_BLOCKED_UA_STRS.concat('testington'))( + 'blocks a bot based on the user agent %s', + (botString) => { + const randomisedUserAgent = userAgentFor(botString) + + expect(_isBlockedUA(randomisedUserAgent, ['testington'])).toBe(true) + } + ) + + it('should block googlebot desktop', () => { + expect( + _isBlockedUA( + 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Chrome/W.X.Y.Z Safari/537.36', + [] + ) + ).toBe(true) + }) + + it('should block openai bot', () => { + expect( + _isBlockedUA( + 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; GPTBot/1.0; +https://openai.com/gptbot)', + [] + ) + ).toBe(true) + }) + }) + + describe('check for cross domain cookies', () => { + it.each([ + [false, 'https://test.herokuapp.com'], + [false, 'test.herokuapp.com'], + [false, 'herokuapp.com'], + [false, undefined], + // ensure it isn't matching herokuapp anywhere in the domain + [true, 'https://test.herokuapp.com.impersonator.io'], + [true, 'mysite-herokuapp.com'], + [true, 'https://bbc.co.uk'], + [true, 'bbc.co.uk'], + [true, 'www.bbc.co.uk'], + ])('should return %s when hostname is %s', (expectedResult, hostname) => { + expect(isCrossDomainCookie({ hostname } as unknown as Location)).toEqual(expectedResult) + }) + }) + }) + + describe('base64Encode', () => { + it('should return null when input is null', () => { + expect(_base64Encode(null)).toBe(null) + }) + + it('should return undefined when input is undefined', () => { + expect(_base64Encode(undefined)).toBe(undefined) + }) + + it('should return base64 encoded string when input is a string', () => { + const input = 'Hello, World!' + const expectedOutput = 'SGVsbG8sIFdvcmxkIQ==' // Base64 encoded string of 'Hello, World!' + expect(_base64Encode(input)).toBe(expectedOutput) + }) + + it('should handle special characters correctly', () => { + const input = '✓ à la mode' + const expectedOutput = '4pyTIMOgIGxhIG1vZGU=' // Base64 encoded string of '✓ à la mode' + expect(_base64Encode(input)).toBe(expectedOutput) + }) + + it('should handle empty string correctly', () => { + const input = '' + const expectedOutput = '' // Base64 encoded string of an empty string is an empty string + expect(_base64Encode(input)).toBe(expectedOutput) + }) + }) +}) From d78450373da3173d7c14327f68d1acddf75449b8 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 16:33:12 +0000 Subject: [PATCH 2/9] more tests to TS --- package.json | 1 + .../{gdpr-utils.js => gdpr-utils.test.ts} | 52 +++++++++++-------- src/__tests__/{loader.js => loader.test.ts} | 12 ++--- yarn.lock | 12 +++++ 4 files changed, 48 insertions(+), 29 deletions(-) rename src/__tests__/{gdpr-utils.js => gdpr-utils.test.ts} (91%) rename src/__tests__/{loader.js => loader.test.ts} (77%) diff --git a/package.json b/package.json index a6ca85e04..6694c8c71 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@types/eslint": "^8.44.6", "@types/jest": "^29.5.1", "@types/react-dom": "^18.0.10", + "@types/sinon": "^17.0.1", "@types/uuid": "^9.0.1", "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", diff --git a/src/__tests__/gdpr-utils.js b/src/__tests__/gdpr-utils.test.ts similarity index 91% rename from src/__tests__/gdpr-utils.js rename to src/__tests__/gdpr-utils.test.ts index dc06de8c7..16bec3ec9 100644 --- a/src/__tests__/gdpr-utils.js +++ b/src/__tests__/gdpr-utils.test.ts @@ -3,6 +3,7 @@ import sinon from 'sinon' import * as gdpr from '../gdpr-utils' import { _isNull } from '../utils/type-utils' +import { document, assignableWindow } from '../utils/globals' const TOKENS = [ `test-token`, @@ -13,23 +14,28 @@ const DEFAULT_PERSISTENCE_PREFIX = `__ph_opt_in_out_` const CUSTOM_PERSISTENCE_PREFIX = `𝓶𝓶𝓶𝓬𝓸𝓸𝓴𝓲𝓮𝓼` function deleteAllCookies() { - var cookies = document.cookie.split(';') + const cookies = document.cookie.split(';') - for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i] - var eqPos = cookie.indexOf('=') - var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i] + const eqPos = cookie.indexOf('=') + const name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT' } } -function forPersistenceTypes(runTests) { +function forPersistenceTypes(runTests: any) { ;[`cookie`, `localStorage`, `localStorage+cookie`].forEach(function (persistenceType) { describe(persistenceType, runTests.bind(null, persistenceType)) }) } -function assertPersistenceValue(persistenceType, token, value, persistencePrefix = DEFAULT_PERSISTENCE_PREFIX) { +function assertPersistenceValue( + persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined, + token: string, + value: string | number | null, + persistencePrefix = DEFAULT_PERSISTENCE_PREFIX +) { if (persistenceType === `cookie`) { if (_isNull(value)) { expect(document.cookie).not.toContain(token) @@ -38,9 +44,9 @@ function assertPersistenceValue(persistenceType, token, value, persistencePrefix } } else { if (_isNull(value)) { - expect(window.localStorage.getItem(persistencePrefix + token)).toBeNull() + expect(assignableWindow.localStorage.getItem(persistencePrefix + token)).toBeNull() } else { - expect(window.localStorage.getItem(persistencePrefix + token)).toBe(`${value}`) + expect(assignableWindow.localStorage.getItem(persistencePrefix + token)).toBe(`${value}`) } } } @@ -51,12 +57,12 @@ describe(`GDPR utils`, () => { afterEach(() => { document.getElementsByTagName('html')[0].innerHTML = '' - window.localStorage.clear() + assignableWindow.localStorage.clear() deleteAllCookies() }) describe(`optIn`, () => { - forPersistenceTypes(function (persistenceType) { + forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { it(`should set a cookie marking the user as opted-in for a given token`, () => { TOKENS.forEach((token) => { gdpr.optIn(token, { persistenceType }) @@ -92,7 +98,7 @@ describe(`GDPR utils`, () => { it(`shouldn't capture an event if the user has opted out`, () => { TOKENS.forEach((token) => { - let capture = sinon.spy() + const capture = sinon.spy() gdpr.optOut(token, { persistenceType }) gdpr.optOut(token, { capture, persistenceType }) expect(capture.notCalled).toBe(true) @@ -101,7 +107,7 @@ describe(`GDPR utils`, () => { it(`should capture an event if the user has opted in`, () => { TOKENS.forEach((token) => { - let capture = sinon.spy() + const capture = sinon.spy() gdpr.optOut(token, { persistenceType }) gdpr.optIn(token, { persistenceType }) gdpr.optIn(token, { capture, persistenceType }) @@ -111,7 +117,7 @@ describe(`GDPR utils`, () => { it(`should capture an event if the user is switching opt from out to in`, () => { TOKENS.forEach((token) => { - let capture = sinon.spy() + const capture = sinon.spy() gdpr.optOut(token, { persistenceType }) gdpr.optIn(token, { capture, persistenceType }) expect(capture.calledOnce).toBe(true) @@ -141,7 +147,7 @@ describe(`GDPR utils`, () => { }) describe(`optOut`, () => { - forPersistenceTypes(function (persistenceType) { + forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { it(`should set a cookie marking the user as opted-out for a given token`, () => { TOKENS.forEach((token) => { gdpr.optOut(token, { persistenceType }) @@ -168,8 +174,8 @@ describe(`GDPR utils`, () => { it(`shouldn't capture an event if the user is switching opt from in to out`, () => { TOKENS.forEach((token) => { - let capture = sinon.spy() - gdpr.optIn(token) + const capture = sinon.spy() + gdpr.optIn(token, {}) gdpr.optOut(token, { capture, persistenceType }) expect(capture.calledOnce).toBe(false) }) @@ -198,7 +204,7 @@ describe(`GDPR utils`, () => { }) describe(`hasOptedIn`, () => { - forPersistenceTypes(function (persistenceType) { + forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { it(`should return 'false' if the user hasn't opted in for a given token`, () => { TOKENS.forEach((token) => { expect(gdpr.hasOptedIn(token, { persistenceType })).toBe(false) @@ -214,7 +220,7 @@ describe(`GDPR utils`, () => { it(`should return 'false' if the user opts in for any other token`, () => { const token = TOKENS[0] - gdpr.optIn(token) + gdpr.optIn(token, {}) TOKENS.filter((otherToken) => otherToken !== token).forEach((otherToken) => { expect(gdpr.hasOptedIn(otherToken, { persistenceType })).toBe(false) @@ -267,7 +273,7 @@ describe(`GDPR utils`, () => { expect( gdpr.hasOptedIn(token, { persistencePrefix: CUSTOM_PERSISTENCE_PREFIX, persistenceType }) ).toBe(true) - gdpr.optOut(token) + gdpr.optOut(token, {}) expect( gdpr.hasOptedIn(token, { persistencePrefix: CUSTOM_PERSISTENCE_PREFIX, persistenceType }) ).toBe(true) @@ -281,7 +287,7 @@ describe(`GDPR utils`, () => { }) describe(`hasOptedOut`, () => { - forPersistenceTypes(function (persistenceType) { + forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { it(`should return 'false' if the user hasn't opted out for a given token`, () => { TOKENS.forEach((token) => { expect(gdpr.hasOptedOut(token, { persistenceType })).toBe(false) @@ -364,7 +370,7 @@ describe(`GDPR utils`, () => { }) describe(`clearOptInOut`, () => { - forPersistenceTypes(function (persistenceType) { + forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { it(`should delete any opt cookies for a given token`, () => { ;[gdpr.optIn, gdpr.optOut].forEach((optFunc) => { TOKENS.forEach((token) => { @@ -450,7 +456,7 @@ describe(`GDPR utils`, () => { persistencePrefix: CUSTOM_PERSISTENCE_PREFIX, }) ).toBe(true) - gdpr.clearOptInOut(token) + gdpr.clearOptInOut(token, {}) expect( gdpr.hasOptedOut(token, { persistenceType, diff --git a/src/__tests__/loader.js b/src/__tests__/loader.test.ts similarity index 77% rename from src/__tests__/loader.js rename to src/__tests__/loader.test.ts index 34e0e56c0..ae244f3e1 100644 --- a/src/__tests__/loader.js +++ b/src/__tests__/loader.test.ts @@ -7,17 +7,18 @@ import posthog from '../loader-module' import sinon from 'sinon' +import { window } from '../utils/globals' describe(`Module-based loader in Node env`, () => { beforeEach(() => { jest.spyOn(posthog, '_send_request').mockReturnValue() - jest.spyOn(window.console, 'log').mockImplementation() + jest.spyOn(window!.console, 'log').mockImplementation() }) it('should load and capture the pageview event', () => { const sandbox = sinon.createSandbox() let loaded = false - posthog._originalCapture = posthog.capture + const _originalCapture = posthog.capture posthog.capture = sandbox.spy() posthog.init(`test-token`, { debug: true, @@ -28,14 +29,13 @@ describe(`Module-based loader in Node env`, () => { }, }) - expect(posthog.capture.calledOnce).toBe(true) - const captureArgs = posthog.capture.args[0] + sinon.assert.calledOnce(posthog.capture as sinon.SinonSpy) + const captureArgs = (posthog.capture as sinon.SinonSpy).args[0] const event = captureArgs[0] expect(event).toBe('$pageview') expect(loaded).toBe(true) - posthog.capture = posthog._originalCapture - delete posthog._originalCapture + posthog.capture = _originalCapture }) it(`supports identify()`, () => { diff --git a/yarn.lock b/yarn.lock index e09365bfa..b198ed119 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3698,6 +3698,18 @@ dependencies: "@types/node" "*" +"@types/sinon@^17.0.1": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.1.tgz#f17816577ee61d462cb7bfcea6ff64fb05063256" + integrity sha512-Q2Go6TJetYn5Za1+RJA1Aik61Oa2FS8SuJ0juIqUuJ5dZR4wvhKfmSdIqWtQ3P6gljKWjW0/R7FZkA4oXVL6OA== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2" + integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== + "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" From 1899d0e10f1afb045506746d44da4afcf43d5155 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 16:44:51 +0000 Subject: [PATCH 3/9] more tests to TS --- src/__tests__/compression.js | 44 ------------------------------- src/__tests__/compression.test.ts | 38 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 44 deletions(-) delete mode 100644 src/__tests__/compression.js create mode 100644 src/__tests__/compression.test.ts diff --git a/src/__tests__/compression.js b/src/__tests__/compression.js deleted file mode 100644 index 333070435..000000000 --- a/src/__tests__/compression.js +++ /dev/null @@ -1,44 +0,0 @@ -import { decideCompression, compressData } from '../compression' - -describe('decideCompression()', () => { - given('subject', () => decideCompression(given.compressionSupport)) - given('compressionSupport', () => ({})) - - it('returns base64 by default', () => { - expect(given.subject).toEqual('base64') - }) - - it('returns gzip-js if all compressions supported', () => { - given('compressionSupport', () => ({ - 'gzip-js': true, - 'a different thing that is either deprecated or new': true, - })) - - expect(given.subject).toEqual('gzip-js') - }) - - it('returns base64 if only unexpected compression is received', () => { - given('compressionSupport', () => ({ 'the new compression that is not supported yet': true })) - - expect(given.subject).toEqual('base64') - }) -}) - -describe('compressData()', () => { - given('subject', () => compressData(given.compression, given.jsonData, given.options)) - - given('jsonData', () => JSON.stringify({ large_key: new Array(500).join('abc') })) - given('options', () => ({ method: 'POST' })) - - it('handles base64', () => { - given('compression', () => 'base64') - - expect(given.subject).toMatchSnapshot() - }) - - it('handles gzip-js', () => { - given('compression', () => 'gzip-js') - - expect(given.subject).toMatchSnapshot() - }) -}) diff --git a/src/__tests__/compression.test.ts b/src/__tests__/compression.test.ts new file mode 100644 index 000000000..d7d6c0469 --- /dev/null +++ b/src/__tests__/compression.test.ts @@ -0,0 +1,38 @@ +import { compressData, decideCompression } from '../compression' +import { Compression, XHROptions } from '../types' + +describe('decideCompression()', () => { + it('returns base64 by default', () => { + expect(decideCompression({})).toEqual('base64') + }) + + it('returns gzip-js if all compressions supported', () => { + expect( + decideCompression({ + 'gzip-js': true, + 'a different thing that is either deprecated or new': true, + } as unknown as Partial>) + ).toEqual('gzip-js') + }) + + it('returns base64 if only unexpected compression is received', () => { + expect( + decideCompression({ 'the new compression that is not supported yet': true } as unknown as Partial< + Record + >) + ).toEqual('base64') + }) +}) + +describe('compressData()', () => { + const jsonData = JSON.stringify({ large_key: new Array(500).join('abc') }) + const options: XHROptions = { method: 'POST' } + + it('handles base64', () => { + expect(compressData(Compression.Base64, jsonData, options)).toMatchSnapshot() + }) + + it('handles gzip-js', () => { + expect(compressData(Compression.GZipJS, jsonData, options)).toMatchSnapshot() + }) +}) From 96441d17c836f35c37088de18d9582a9a6191fea Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 16:54:39 +0000 Subject: [PATCH 4/9] more tests to TS --- ...ure-utils.js => autocapture-utils.test.ts} | 158 ++++++++++-------- 1 file changed, 89 insertions(+), 69 deletions(-) rename src/__tests__/{autocapture-utils.js => autocapture-utils.test.ts} (72%) diff --git a/src/__tests__/autocapture-utils.js b/src/__tests__/autocapture-utils.test.ts similarity index 72% rename from src/__tests__/autocapture-utils.js rename to src/__tests__/autocapture-utils.test.ts index 142a9ef20..35aa20a3d 100644 --- a/src/__tests__/autocapture-utils.js +++ b/src/__tests__/autocapture-utils.test.ts @@ -1,3 +1,5 @@ +/// + import sinon from 'sinon' import { @@ -10,15 +12,16 @@ import { getNestedSpanText, getDirectAndNestedSpanText, } from '../autocapture-utils' +import { document } from '../utils/globals' describe(`Autocapture utility functions`, () => { afterEach(() => { - document.getElementsByTagName('html')[0].innerHTML = '' + document!.getElementsByTagName('html')[0].innerHTML = '' }) describe(`getSafeText`, () => { it(`should collect and normalize text from elements`, () => { - const el = document.createElement(`div`) + const el = document!.createElement(`div`) el.innerHTML = ` Why hello there ` expect(getSafeText(el)).toBe(`Why hello there`) @@ -41,7 +44,7 @@ describe(`Autocapture utility functions`, () => { }) it(`shouldn't collect text from element children`, () => { - const el = document.createElement(`div`) + const el = document!.createElement(`div`) let safeText el.innerHTML = `
sensitive
` @@ -64,26 +67,26 @@ describe(`Autocapture utility functions`, () => { it(`shouldn't collect text from potentially sensitive elements`, () => { let el - el = document.createElement(`input`) + el = document!.createElement(`input`) el.innerHTML = `Why hello there` expect(getSafeText(el)).toBe(``) - el = document.createElement(`textarea`) + el = document!.createElement(`textarea`) el.innerHTML = `Why hello there` expect(getSafeText(el)).toBe(``) - el = document.createElement(`select`) + el = document!.createElement(`select`) el.innerHTML = `Why hello there` expect(getSafeText(el)).toBe(``) - el = document.createElement(`div`) + el = document!.createElement(`div`) el.setAttribute(`contenteditable`, `true`) el.innerHTML = `Why hello there` expect(getSafeText(el)).toBe(``) }) it(`shouldn't collect sensitive values`, () => { - const el = document.createElement(`div`) + const el = document!.createElement(`div`) el.innerHTML = `Why 123-58-1321 hello there` expect(getSafeText(el)).toBe(`Why hello there`) @@ -105,17 +108,17 @@ describe(`Autocapture utility functions`, () => { describe(`shouldCaptureDomEvent`, () => { it(`should capture "submit" events on
elements`, () => { expect( - shouldCaptureDomEvent(document.createElement(`form`), { + shouldCaptureDomEvent(document!.createElement(`form`), { type: `submit`, - }) + } as unknown as Event) ).toBe(true) }) ;[`input`, `SELECT`, `textarea`].forEach((tagName) => { it(`should capture "change" events on <` + tagName.toLowerCase() + `> elements`, () => { expect( - shouldCaptureDomEvent(document.createElement(tagName), { + shouldCaptureDomEvent(document!.createElement(tagName), { type: `change`, - }) + } as unknown as Event) ).toBe(true) }) }) @@ -124,59 +127,68 @@ describe(`Autocapture utility functions`, () => { ;['a'].forEach((tagName) => { it(`should capture "click" events on <` + tagName.toLowerCase() + `> elements`, () => { expect( - shouldCaptureDomEvent(document.createElement(tagName), { + shouldCaptureDomEvent(document!.createElement(tagName), { type: `click`, - }) + } as unknown as Event) ).toBe(true) }) }) it(`should capture "click" events on
- +
@@ -850,15 +877,13 @@ describe('Autocapture system', () => { ` - document.body.innerHTML = dom const span1 = document.getElementById('span1') const span2 = document.getElementById('span2') const img2 = document.getElementById('img2') - const e1 = { + const e1 = makeMouseEvent({ target: span2, - type: 'click', - } + }) autocapture._captureEvent(e1, lib) const props1 = getCapturedProps(lib.capture) @@ -866,22 +891,20 @@ describe('Autocapture system', () => { "Some super duper really long Text with new lines that we'll strip out and also we will want to make this text shorter since it's not likely people really care about text content that's super long and it also takes up more space and bandwidth. Some super d" expect(props1['$elements'][0]).toHaveProperty('$el_text', text1) expect(props1['$el_text']).toEqual(text1) - lib.capture.resetHistory() + ;(lib.capture as sinon.SinonSpy).resetHistory() - const e2 = { + const e2 = makeMouseEvent({ target: span1, - type: 'click', - } + }) autocapture._captureEvent(e2, lib) const props2 = getCapturedProps(lib.capture) expect(props2['$elements'][0]).toHaveProperty('$el_text', 'Some text') expect(props2['$el_text']).toEqual('Some text') - lib.capture.resetHistory() + ;(lib.capture as sinon.SinonSpy).resetHistory() - const e3 = { + const e3 = makeMouseEvent({ target: img2, - type: 'click', - } + }) autocapture._captureEvent(e3, lib) const props3 = getCapturedProps(lib.capture) expect(props3['$elements'][0]).toHaveProperty('$el_text', '') @@ -889,7 +912,8 @@ describe('Autocapture system', () => { }) it('does not capture sensitive text content', () => { - const dom = ` + // ^ valid credit card and social security numbers + document.body.innerHTML = `
@@ -901,37 +925,32 @@ describe('Autocapture system', () => { Why hello there 5105-1051-0510-5100 - ` // ^ valid credit card and social security numbers - - document.body.innerHTML = dom + ` const button1 = document.getElementById('button1') const button2 = document.getElementById('button2') const button3 = document.getElementById('button3') - const e1 = { + const e1 = makeMouseEvent({ target: button1, - type: 'click', - } + }) autocapture._captureEvent(e1, lib) const props1 = getCapturedProps(lib.capture) expect(props1['$elements'][0]).toHaveProperty('$el_text') expect(props1['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) - lib.capture.resetHistory() + ;(lib.capture as sinon.SinonSpy).resetHistory() - const e2 = { + const e2 = makeMouseEvent({ target: button2, - type: 'click', - } + }) autocapture._captureEvent(e2, lib) const props2 = getCapturedProps(lib.capture) expect(props2['$elements'][0]).toHaveProperty('$el_text') expect(props2['$elements'][0]['$el_text']).toMatch(/Why\s+hello\s+there/) - lib.capture.resetHistory() + ;(lib.capture as sinon.SinonSpy).resetHistory() - const e3 = { + const e3 = makeMouseEvent({ target: button3, - type: 'click', - } + }) autocapture._captureEvent(e3, lib) const props3 = getCapturedProps(lib.capture) expect(props3['$elements'][0]).toHaveProperty('$el_text') @@ -942,44 +961,42 @@ describe('Autocapture system', () => { const e = { target: document.createElement('form'), type: 'submit', - } + } as unknown as FormDataEvent autocapture._captureEvent(e, lib) - expect(lib.capture.calledOnce).toBe(true) + expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) const props = getCapturedProps(lib.capture) expect(props['$event_type']).toBe('submit') }) it('should capture a click event inside a form with form field props', () => { - var form = document.createElement('form') - var link = document.createElement('a') - var input = document.createElement('input') + const form = document.createElement('form') + const link = document.createElement('a') + const input = document.createElement('input') input.name = 'test input' input.value = 'test val' form.appendChild(link) form.appendChild(input) - const e = { + const e = makeMouseEvent({ target: link, - type: 'click', - } + }) autocapture._captureEvent(e, lib) - expect(lib.capture.calledOnce).toBe(true) - const props = getCapturedProps(lib.capture) + expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) + const props = getCapturedProps(lib.capture as sinon.SinonSpy) expect(props['$event_type']).toBe('click') }) it('should capture a click event inside a shadowroot', () => { - var main_el = document.createElement('some-element') - var shadowRoot = main_el.attachShadow({ mode: 'open' }) - var button = document.createElement('a') + const main_el = document.createElement('some-element') + const shadowRoot = main_el.attachShadow({ mode: 'open' }) + const button = document.createElement('a') button.innerHTML = 'bla' shadowRoot.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } + }) autocapture._captureEvent(e, lib) - expect(lib.capture.calledOnce).toBe(true) + expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) const props = getCapturedProps(lib.capture) expect(props['$event_type']).toBe('click') }) @@ -988,20 +1005,20 @@ describe('Autocapture system', () => { const a = document.createElement('a') const span = document.createElement('span') a.appendChild(span) - autocapture._captureEvent({ target: a, type: 'click' }, lib) - expect(lib.capture.calledOnce).toBe(true) - lib.capture.resetHistory() + autocapture._captureEvent(makeMouseEvent({ target: a, type: 'click' }), lib) + expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) + ;(lib.capture as sinon.SinonSpy).resetHistory() - autocapture._captureEvent({ target: span, type: 'click' }, lib) - expect(lib.capture.calledOnce).toBe(true) - lib.capture.resetHistory() + autocapture._captureEvent(makeMouseEvent({ target: span, type: 'click' }), lib) + expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) + ;(lib.capture as sinon.SinonSpy).resetHistory() a.className = 'test1 ph-no-capture test2' - autocapture._captureEvent({ target: a, type: 'click' }, lib) - expect(lib.capture.callCount).toBe(0) + autocapture._captureEvent(makeMouseEvent({ target: a, type: 'click' }), lib) + expect((lib.capture as sinon.SinonSpy).callCount).toBe(0) - autocapture._captureEvent({ target: span, type: 'click' }, lib) - expect(lib.capture.callCount).toBe(0) + autocapture._captureEvent(makeMouseEvent({ target: span, type: 'click' }), lib) + expect((lib.capture as sinon.SinonSpy).callCount).toBe(0) }) it('does not capture any element attributes if mask_all_element_attributes is set', () => { @@ -1011,21 +1028,20 @@ describe('Autocapture system', () => { ` - const newLib = { + const newLib = makePostHog({ ...lib, config: { ...lib.config, mask_all_element_attributes: true, }, - } + }) document.body.innerHTML = dom const button1 = document.getElementById('button1') - const e1 = { + const e1 = makeMouseEvent({ target: button1, - type: 'click', - } + }) autocapture._captureEvent(e1, newLib) const props1 = getCapturedProps(newLib.capture) @@ -1039,21 +1055,20 @@ describe('Autocapture system', () => { ` - const newLib = { + const newLib = makePostHog({ ...lib, config: { ...lib.config, mask_all_text: true, }, - } + }) document.body.innerHTML = dom const a = document.getElementById('a1') - const e1 = { + const e1 = makeMouseEvent({ target: a, - type: 'click', - } + }) autocapture._captureEvent(e1, newLib) const props1 = getCapturedProps(newLib.capture) @@ -1063,23 +1078,20 @@ describe('Autocapture system', () => { }) describe('_addDomEventHandlers', () => { - const lib = { + const lib = makePostHog({ capture: sinon.spy(), - get_distinct_id() { - return 'distinctid' - }, config: { mask_all_element_attributes: false, - }, - } + } as PostHogConfig, + }) - let navigateSpy + let navigateSpy: sinon.SinonSpy beforeEach(() => { document.title = 'test page' autocapture._addDomEventHandlers(lib) navigateSpy = sinon.spy(autocapture, '_navigate') - lib.capture.resetHistory() + ;(lib.capture as sinon.SinonSpy).resetHistory() }) afterAll(() => { @@ -1091,41 +1103,39 @@ describe('Autocapture system', () => { document.body.appendChild(button) simulateClick(button) simulateClick(button) - expect(true).toBe(lib.capture.calledTwice) - const captureArgs1 = lib.capture.args[0] - const captureArgs2 = lib.capture.args[1] + expect(true).toBe((lib.capture as sinon.SinonSpy).calledTwice) + const captureArgs1 = (lib.capture as sinon.SinonSpy).args[0] + const captureArgs2 = (lib.capture as sinon.SinonSpy).args[1] const eventType1 = captureArgs1[1]['$event_type'] const eventType2 = captureArgs2[1]['$event_type'] expect(eventType1).toBe('click') expect(eventType2).toBe('click') - lib.capture.resetHistory() + ;(lib.capture as sinon.SinonSpy).resetHistory() }) }) describe('afterDecideResponse()', () => { - let posthog - let persistence + let posthog: PostHog + let persistence: PostHogPersistence beforeEach(() => { document.title = 'test page' autocapture._initializedTokens = [] - persistence = { props: {}, register: jest.fn() } - decideResponse = { config: { enable_collect_everything: true } } + persistence = { props: {}, register: jest.fn() } as unknown as PostHogPersistence + decideResponse = { config: { enable_collect_everything: true } } as DecideResponse - posthog = { + posthog = makePostHog({ config: { api_host: 'https://test.com', token: 'testtoken', autocapture: true, - }, - token: 'testtoken', + } as PostHogConfig, capture: jest.fn(), - get_distinct_id: () => 'distinctid', - get_property: (property_key) => + get_property: (property_key: string) => property_key === AUTOCAPTURE_DISABLED_SERVER_SIDE ? $autocapture_disabled_server_side : undefined, persistence: persistence, - } + }) jest.spyOn(autocapture, '_addDomEventHandlers') }) @@ -1166,7 +1176,7 @@ describe('Autocapture system', () => { decideResponse = { config: { enable_collect_everything: true }, autocapture_opt_out: serverSideOptOut, - } + } as DecideResponse autocapture.afterDecideResponse(decideResponse, posthog) expect(autocapture._isAutocaptureEnabled).toBe(expected) } @@ -1185,7 +1195,7 @@ describe('Autocapture system', () => { api_host: 'https://test.com', token: 'testtoken', autocapture: false, - } + } as PostHogConfig $autocapture_disabled_server_side = true autocapture.afterDecideResponse(decideResponse, posthog) @@ -1194,7 +1204,7 @@ describe('Autocapture system', () => { }) it('should NOT call _addDomEventHandlders if the decide request fails', () => { - decideResponse = { status: 0, error: 'Bad HTTP status: 400 Bad Request' } + decideResponse = { status: 0, error: 'Bad HTTP status: 400 Bad Request' } as unknown as DecideResponse autocapture.afterDecideResponse(decideResponse, posthog) @@ -1202,7 +1212,7 @@ describe('Autocapture system', () => { }) it('should NOT call _addDomEventHandlders when enable_collect_everything is "false"', () => { - decideResponse = { config: { enable_collect_everything: false } } + decideResponse = { config: { enable_collect_everything: false } } as DecideResponse autocapture.afterDecideResponse(decideResponse, posthog) @@ -1217,7 +1227,11 @@ describe('Autocapture system', () => { autocapture.afterDecideResponse(decideResponse, posthog) expect(autocapture._addDomEventHandlers).toHaveBeenCalledTimes(1) - posthog.config = { api_host: 'https://test.com', token: 'anotherproject', autocapture: true } + posthog.config = { + api_host: 'https://test.com', + token: 'anotherproject', + autocapture: true, + } as PostHogConfig autocapture.afterDecideResponse(decideResponse, posthog) expect(autocapture._addDomEventHandlers).toHaveBeenCalledTimes(2) }) @@ -1225,80 +1239,73 @@ describe('Autocapture system', () => { describe('shouldCaptureDomEvent autocapture config', () => { it('only capture urls which match the url regex allowlist', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('a') + const main_el = document.createElement('some-element') + const button = document.createElement('a') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } + }) const autocapture_config = { url_allowlist: ['https://posthog.com/test/*'], } - delete window.location - window.location = new URL('https://posthog.com/test/captured') + window!.location = new URL('https://posthog.com/test/captured') as unknown as Location expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(true) - delete window.location - window.location = new URL('https://posthog.com/docs/not-captured') + window!.location = new URL('https://posthog.com/docs/not-captured') as unknown as Location expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(false) }) it('an empty url regex allowlist does not match any url', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('a') + const main_el = document.createElement('some-element') + const button = document.createElement('a') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } - const autocapture_config = { + }) + const autocapture_config: AutocaptureConfig = { url_allowlist: [], } - delete window.location - window.location = new URL('https://posthog.com/test/captured') + window!.location = new URL('https://posthog.com/test/captured') as unknown as Location expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(false) }) it('only capture event types which match the allowlist', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('button') + const main_el = document.createElement('some-element') + const button = document.createElement('button') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } - const autocapture_config = { + }) + const autocapture_config: AutocaptureConfig = { dom_event_allowlist: ['click'], } expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(true) - const autocapture_config_change = { + const autocapture_config_change: AutocaptureConfig = { dom_event_allowlist: ['change'], } expect(shouldCaptureDomEvent(button, e, autocapture_config_change)).toBe(false) }) it('an empty event type allowlist matches no events', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('button') + const main_el = document.createElement('some-element') + const button = document.createElement('button') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } + }) const autocapture_config = { dom_event_allowlist: [], } @@ -1306,54 +1313,51 @@ describe('Autocapture system', () => { }) it('only capture elements which match the allowlist', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('button') + const main_el = document.createElement('some-element') + const button = document.createElement('button') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } - const autocapture_config = { + }) + const autocapture_config: AutocaptureConfig = { element_allowlist: ['button'], } expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(true) - const autocapture_config_change = { + const autocapture_config_change: AutocaptureConfig = { element_allowlist: ['a'], } expect(shouldCaptureDomEvent(button, e, autocapture_config_change)).toBe(false) }) it('an empty event allowlist means we capture no elements', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('button') + const main_el = document.createElement('some-element') + const button = document.createElement('button') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } - const autocapture_config = { + }) + const autocapture_config: AutocaptureConfig = { element_allowlist: [], } expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(false) }) it('only capture elements which match the css allowlist', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('button') + const main_el = document.createElement('some-element') + const button = document.createElement('button') button.setAttribute('data-track', 'yes') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } - const autocapture_config = { + }) + const autocapture_config: AutocaptureConfig = { css_selector_allowlist: ['[data-track="yes"]'], } expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(true) @@ -1365,17 +1369,16 @@ describe('Autocapture system', () => { }) it('an empty css selector list captures no elements', () => { - var main_el = document.createElement('some-element') - var button = document.createElement('button') + const main_el = document.createElement('some-element') + const button = document.createElement('button') button.setAttribute('data-track', 'yes') button.innerHTML = 'bla' main_el.appendChild(button) - const e = { + const e = makeMouseEvent({ target: main_el, composedPath: () => [button, main_el], - type: 'click', - } - const autocapture_config = { + }) + const autocapture_config: AutocaptureConfig = { css_selector_allowlist: [], } expect(shouldCaptureDomEvent(button, e, autocapture_config)).toBe(false) From ae53c6ebdd4097222273b08fdedc871ea3a2abef Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 20:44:09 +0000 Subject: [PATCH 7/9] updated snapshot --- .../__snapshots__/compression.test.ts.snap | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/__tests__/__snapshots__/compression.test.ts.snap diff --git a/src/__tests__/__snapshots__/compression.test.ts.snap b/src/__tests__/__snapshots__/compression.test.ts.snap new file mode 100644 index 000000000..3a1364187 --- /dev/null +++ b/src/__tests__/__snapshots__/compression.test.ts.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`compressData() handles base64 1`] = ` +Array [ + Object { + "data": "eyJsYXJnZV9rZXkiOiJhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmNhYmMifQ==", + }, + Object { + "method": "POST", + }, +] +`; + +exports[`compressData() handles gzip-js 1`] = ` +Array [ + Uint8Array [ + 31, + 139, + 8, + 0, + 0, + 0, + 0, + 0, + 0, + 3, + 171, + 86, + 202, + 73, + 44, + 74, + 79, + 141, + 207, + 78, + 173, + 84, + 178, + 82, + 74, + 76, + 74, + 30, + 69, + 163, + 104, + 20, + 141, + 162, + 81, + 52, + 156, + 144, + 82, + 45, + 0, + 150, + 15, + 165, + 86, + 233, + 5, + 0, + 0, + ], + Object { + "blob": true, + "method": "POST", + "urlQueryArgs": Object { + "compression": "gzip-js", + }, + }, +] +`; From 38d7e469ada95d86824ea2f91cd6482dd571bc4b Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 20:53:07 +0000 Subject: [PATCH 8/9] fix --- src/__tests__/gdpr-utils.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/__tests__/gdpr-utils.test.ts b/src/__tests__/gdpr-utils.test.ts index 16bec3ec9..7e1784f5f 100644 --- a/src/__tests__/gdpr-utils.test.ts +++ b/src/__tests__/gdpr-utils.test.ts @@ -4,6 +4,7 @@ import * as gdpr from '../gdpr-utils' import { _isNull } from '../utils/type-utils' import { document, assignableWindow } from '../utils/globals' +import { GDPROptions } from '../types' const TOKENS = [ `test-token`, @@ -31,7 +32,7 @@ function forPersistenceTypes(runTests: any) { } function assertPersistenceValue( - persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined, + persistenceType: GDPROptions['persistenceType'], token: string, value: string | number | null, persistencePrefix = DEFAULT_PERSISTENCE_PREFIX @@ -62,7 +63,7 @@ describe(`GDPR utils`, () => { }) describe(`optIn`, () => { - forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { + forPersistenceTypes(function (persistenceType: GDPROptions['persistenceType']) { it(`should set a cookie marking the user as opted-in for a given token`, () => { TOKENS.forEach((token) => { gdpr.optIn(token, { persistenceType }) @@ -147,7 +148,7 @@ describe(`GDPR utils`, () => { }) describe(`optOut`, () => { - forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { + forPersistenceTypes(function (persistenceType: GDPROptions['persistenceType']) { it(`should set a cookie marking the user as opted-out for a given token`, () => { TOKENS.forEach((token) => { gdpr.optOut(token, { persistenceType }) @@ -204,7 +205,7 @@ describe(`GDPR utils`, () => { }) describe(`hasOptedIn`, () => { - forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { + forPersistenceTypes(function (persistenceType: GDPROptions['persistenceType']) { it(`should return 'false' if the user hasn't opted in for a given token`, () => { TOKENS.forEach((token) => { expect(gdpr.hasOptedIn(token, { persistenceType })).toBe(false) @@ -287,7 +288,7 @@ describe(`GDPR utils`, () => { }) describe(`hasOptedOut`, () => { - forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { + forPersistenceTypes(function (persistenceType: GDPROptions['persistenceType']) { it(`should return 'false' if the user hasn't opted out for a given token`, () => { TOKENS.forEach((token) => { expect(gdpr.hasOptedOut(token, { persistenceType })).toBe(false) @@ -370,7 +371,7 @@ describe(`GDPR utils`, () => { }) describe(`clearOptInOut`, () => { - forPersistenceTypes(function (persistenceType: 'cookie' | 'localStorage' | 'localStorage+cookie' | undefined) { + forPersistenceTypes(function (persistenceType: GDPROptions['persistenceType']) { it(`should delete any opt cookies for a given token`, () => { ;[gdpr.optIn, gdpr.optOut].forEach((optFunc) => { TOKENS.forEach((token) => { From 2ea406b2c8a795f563fcbd826e12d1d3db396f0d Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sun, 19 Nov 2023 21:00:06 +0000 Subject: [PATCH 9/9] fix --- src/__tests__/autocapture-utils.test.ts | 52 ++++--------------------- src/__tests__/autocapture.test.ts | 12 +++--- 2 files changed, 14 insertions(+), 50 deletions(-) diff --git a/src/__tests__/autocapture-utils.test.ts b/src/__tests__/autocapture-utils.test.ts index 35aa20a3d..162c7dbf2 100644 --- a/src/__tests__/autocapture-utils.test.ts +++ b/src/__tests__/autocapture-utils.test.ts @@ -13,6 +13,7 @@ import { getDirectAndNestedSpanText, } from '../autocapture-utils' import { document } from '../utils/globals' +import { makeMouseEvent } from './autocapture.test' describe(`Autocapture utility functions`, () => { afterEach(() => { @@ -126,11 +127,7 @@ describe(`Autocapture utility functions`, () => { // [`div`, `sPan`, `A`, `strong`, `table`] ;['a'].forEach((tagName) => { it(`should capture "click" events on <` + tagName.toLowerCase() + `> elements`, () => { - expect( - shouldCaptureDomEvent(document!.createElement(tagName), { - type: `click`, - } as unknown as Event) - ).toBe(true) + expect(shouldCaptureDomEvent(document!.createElement(tagName), makeMouseEvent({}))).toBe(true) }) }) @@ -141,55 +138,22 @@ describe(`Autocapture utility functions`, () => { const button3 = document!.createElement(`input`) button3.setAttribute(`type`, `submit`) ;[button1, button2, button3].forEach((button) => { - expect( - shouldCaptureDomEvent(button, { - type: `click`, - } as unknown as Event) - ).toBe(true) + expect(shouldCaptureDomEvent(button, makeMouseEvent({}))).toBe(true) }) }) it(`should protect against bad inputs`, () => { - expect( - shouldCaptureDomEvent( - null as unknown as Element, - { - type: `click`, - } as unknown as Event - ) - ).toBe(false) - expect( - shouldCaptureDomEvent( - undefined as unknown as Element, - { - type: `click`, - } as unknown as Event - ) - ).toBe(false) - expect( - shouldCaptureDomEvent( - `div` as unknown as Element, - { - type: `click`, - } as unknown as Event - ) - ).toBe(false) + expect(shouldCaptureDomEvent(null as unknown as Element, makeMouseEvent({}))).toBe(false) + expect(shouldCaptureDomEvent(undefined as unknown as Element, makeMouseEvent({}))).toBe(false) + expect(shouldCaptureDomEvent(`div` as unknown as Element, makeMouseEvent({}))).toBe(false) }) it(`should NOT capture "click" events on elements`, () => { - expect( - shouldCaptureDomEvent(document!.createElement(`form`), { - type: `click`, - } as unknown as Event) - ).toBe(false) + expect(shouldCaptureDomEvent(document!.createElement(`form`), makeMouseEvent({}))).toBe(false) }) ;[`html`].forEach((tagName) => { it(`should NOT capture "click" events on <` + tagName.toLowerCase() + `> elements`, () => { - expect( - shouldCaptureDomEvent(document!.createElement(tagName), { - type: `click`, - } as unknown as Event) - ).toBe(false) + expect(shouldCaptureDomEvent(document!.createElement(tagName), makeMouseEvent({}))).toBe(false) }) }) }) diff --git a/src/__tests__/autocapture.test.ts b/src/__tests__/autocapture.test.ts index cc45553ce..07b21e818 100644 --- a/src/__tests__/autocapture.test.ts +++ b/src/__tests__/autocapture.test.ts @@ -32,8 +32,8 @@ function makePostHog(ph: Partial): PostHog { } as unknown as PostHog } -function makeMouseEvent(me: Partial) { - return { type: 'click', ...me } as unknown as MouseEvent +export function makeMouseEvent(partialEvent: Partial) { + return { type: 'click', ...partialEvent } as unknown as MouseEvent } describe('Autocapture system', () => { @@ -1005,19 +1005,19 @@ describe('Autocapture system', () => { const a = document.createElement('a') const span = document.createElement('span') a.appendChild(span) - autocapture._captureEvent(makeMouseEvent({ target: a, type: 'click' }), lib) + autocapture._captureEvent(makeMouseEvent({ target: a }), lib) expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) ;(lib.capture as sinon.SinonSpy).resetHistory() - autocapture._captureEvent(makeMouseEvent({ target: span, type: 'click' }), lib) + autocapture._captureEvent(makeMouseEvent({ target: span }), lib) expect((lib.capture as sinon.SinonSpy).calledOnce).toBe(true) ;(lib.capture as sinon.SinonSpy).resetHistory() a.className = 'test1 ph-no-capture test2' - autocapture._captureEvent(makeMouseEvent({ target: a, type: 'click' }), lib) + autocapture._captureEvent(makeMouseEvent({ target: a }), lib) expect((lib.capture as sinon.SinonSpy).callCount).toBe(0) - autocapture._captureEvent(makeMouseEvent({ target: span, type: 'click' }), lib) + autocapture._captureEvent(makeMouseEvent({ target: span }), lib) expect((lib.capture as sinon.SinonSpy).callCount).toBe(0) })