diff --git a/src/__tests__/extensions/rageclick.js b/src/__tests__/extensions/rageclick.js deleted file mode 100644 index 8e6aa2701..000000000 --- a/src/__tests__/extensions/rageclick.js +++ /dev/null @@ -1,83 +0,0 @@ -import RageClick from '../../extensions/rageclick' - -describe('RageClick()', () => { - given('instance', () => new RageClick(given.enabled)) - given('enabled', () => true) - - const isRageClick = (x, y, t) => given.instance.isRageClick(x, y, t) - - it('identifies some rage clicking', () => { - const detection = [ - isRageClick(0, 0, 10), - isRageClick(10, 10, 20), - isRageClick(5, 5, 40), // triggers rage click - isRageClick(5, 5, 50), // does not re-trigger - ] - - expect(detection).toEqual([false, false, true, false]) - }) - - it('identifies some rage clicking when time delta has passed', () => { - const detection = [ - isRageClick(0, 0, 10), - isRageClick(10, 10, 20), - isRageClick(5, 5, 40), // triggers rage click - // these next three don't trigger - // because you need to move past threshold before triggering again - isRageClick(5, 5, 80), - isRageClick(5, 5, 100), - isRageClick(5, 5, 110), - // moving past the time threshold resets the counter - isRageClick(5, 5, 1120), - isRageClick(5, 5, 1121), - isRageClick(5, 5, 1122), // triggers rage click - ] - - expect(detection).toEqual([false, false, true, false, false, false, false, false, true]) - }) - - it('identifies some rage clicking when pixel delta has passed', () => { - const detection = [ - isRageClick(0, 0, 10), - isRageClick(10, 10, 20), - isRageClick(5, 5, 40), // triggers rage click - // these next three don't trigger - // because you need to move past threshold before triggering again - isRageClick(5, 5, 80), - isRageClick(5, 5, 100), - isRageClick(5, 5, 110), - // moving past the pixel threshold resets the counter - isRageClick(36, 5, 120), - isRageClick(36, 5, 130), - isRageClick(36, 5, 140), // triggers rage click - ] - - expect(detection).toEqual([false, false, true, false, false, false, false, false, true]) - }) - - it('does not capture rage clicks if not enabled', () => { - given('enabled', () => false) - - isRageClick(5, 5, 10) - isRageClick(5, 5, 20) - const rageClickDetected = isRageClick(5, 5, 40) - - expect(rageClickDetected).toBeFalsy() - }) - - it('does not capture clicks too far apart (time)', () => { - isRageClick(5, 5, 10) - isRageClick(5, 5, 20) - const rageClickDetected = isRageClick(5, 5, 4000) - - expect(rageClickDetected).toBeFalsy() - }) - - it('does not capture clicks too far apart (space)', () => { - isRageClick(0, 0, 10) - isRageClick(10, 10, 20) - const rageClickDetected = isRageClick(50, 10, 40) - - expect(rageClickDetected).toBeFalsy() - }) -}) diff --git a/src/__tests__/extensions/rageclick.test.ts b/src/__tests__/extensions/rageclick.test.ts new file mode 100644 index 000000000..e1c71c819 --- /dev/null +++ b/src/__tests__/extensions/rageclick.test.ts @@ -0,0 +1,86 @@ +import RageClick from '../../extensions/rageclick' + +describe('RageClick()', () => { + let instance: RageClick + + describe('when enabled', () => { + beforeEach(() => { + instance = new RageClick(true) + }) + + it('identifies some rage clicking', () => { + const detection = [ + instance.isRageClick(0, 0, 10), + instance.isRageClick(10, 10, 20), + instance.isRageClick(5, 5, 40), // triggers rage click + instance.isRageClick(5, 5, 50), // does not re-trigger + ] + + expect(detection).toEqual([false, false, true, false]) + }) + + it('identifies some rage clicking when time delta has passed', () => { + const detection = [ + instance.isRageClick(0, 0, 10), + instance.isRageClick(10, 10, 20), + instance.isRageClick(5, 5, 40), // triggers rage click + // these next three don't trigger + // because you need to move past threshold before triggering again + instance.isRageClick(5, 5, 80), + instance.isRageClick(5, 5, 100), + instance.isRageClick(5, 5, 110), + // moving past the time threshold resets the counter + instance.isRageClick(5, 5, 1120), + instance.isRageClick(5, 5, 1121), + instance.isRageClick(5, 5, 1122), // triggers rage click + ] + + expect(detection).toEqual([false, false, true, false, false, false, false, false, true]) + }) + + it('identifies some rage clicking when pixel delta has passed', () => { + const detection = [ + instance.isRageClick(0, 0, 10), + instance.isRageClick(10, 10, 20), + instance.isRageClick(5, 5, 40), // triggers rage click + // these next three don't trigger + // because you need to move past threshold before triggering again + instance.isRageClick(5, 5, 80), + instance.isRageClick(5, 5, 100), + instance.isRageClick(5, 5, 110), + // moving past the pixel threshold resets the counter + instance.isRageClick(36, 5, 120), + instance.isRageClick(36, 5, 130), + instance.isRageClick(36, 5, 140), // triggers rage click + ] + + expect(detection).toEqual([false, false, true, false, false, false, false, false, true]) + }) + + it('does not capture clicks too far apart (time)', () => { + instance.isRageClick(5, 5, 10) + instance.isRageClick(5, 5, 20) + const rageClickDetected = instance.isRageClick(5, 5, 4000) + + expect(rageClickDetected).toBeFalsy() + }) + + it('does not capture clicks too far apart (space)', () => { + instance.isRageClick(0, 0, 10) + instance.isRageClick(10, 10, 20) + const rageClickDetected = instance.isRageClick(50, 10, 40) + + expect(rageClickDetected).toBeFalsy() + }) + }) + + test('does not capture rage clicks when disabled', () => { + instance = new RageClick(false) + + instance.isRageClick(5, 5, 10) + instance.isRageClick(5, 5, 20) + const rageClickDetected = instance.isRageClick(5, 5, 40) + + expect(rageClickDetected).toBeFalsy() + }) +}) diff --git a/src/__tests__/extensions/toolbar.js b/src/__tests__/extensions/toolbar.js deleted file mode 100644 index 91445dcf3..000000000 --- a/src/__tests__/extensions/toolbar.js +++ /dev/null @@ -1,206 +0,0 @@ -import { Toolbar } from '../../extensions/toolbar' -import { loadScript } from '../../utils' - -jest.mock('../../utils', () => ({ - ...jest.requireActual('../../utils'), - loadScript: jest.fn((path, callback) => callback()), -})) - -describe('Toolbar', () => { - given('toolbar', () => new Toolbar(given.lib)) - - given('lib', () => ({ - config: given.config, - set_config: jest.fn(), - })) - - given('config', () => ({ - api_host: 'http://api.example.com', - token: 'test_token', - })) - - beforeEach(() => { - loadScript.mockImplementation((path, callback) => callback()) - window.ph_load_toolbar = jest.fn() - delete window['_postHogToolbarLoaded'] - }) - - describe('maybeLoadToolbar', () => { - given('subject', () => () => given.toolbar.maybeLoadToolbar(given.location, given.localStorage, given.history)) - - given('location', () => ({ - hash: `#${given.hash}`, - pathname: 'pathname', - search: '?search', - })) - - given('localStorage', () => ({ - getItem: jest.fn().mockImplementation(() => given.storedEditorParams), - setItem: jest.fn(), - })) - - given('history', () => ({ replaceState: jest.fn() })) - - given('hash', () => - Object.keys(given.hashParams) - .map((k) => `${k}=${given.hashParams[k]}`) - .join('&') - ) - - given('hashState', () => ({ - action: 'ph_authorize', - desiredHash: '#myhash', - projectId: 3, - projectOwnerId: 722725, - readOnly: false, - token: 'test_token', - userFlags: { - flag_1: 0, - flag_2: 1, - }, - userId: 12345, - })) - given('hashParams', () => ({ - access_token: given.accessToken, - state: encodeURIComponent(JSON.stringify(given.hashState)), - expires_in: 3600, - })) - - given('toolbarParams', () => ({ - action: 'ph_authorize', - desiredHash: '#myhash', - projectId: 3, - projectOwnerId: 722725, - readOnly: false, - token: 'test_token', - userFlags: { - flag_1: 0, - flag_2: 1, - }, - userId: 12345, - ...given.toolbarParamsOverrides, - })) - - beforeEach(() => { - jest.spyOn(given.toolbar, 'loadToolbar').mockImplementation(() => {}) - }) - - it('should initialize the toolbar when the hash state contains action "ph_authorize"', () => { - given('toolbarParamsOverrides', () => ({ - action: 'ph_authorize', - })) - - given.subject() - expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ - ...given.toolbarParams, - source: 'url', - }) - }) - - it('should initialize the toolbar when there are editor params in the session', () => { - given.subject() - expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ - ...given.toolbarParams, - source: 'url', - }) - }) - - it('should NOT initialize the toolbar when the activation query param does not exist', () => { - given('hash', () => '') - - expect(given.subject()).toEqual(false) - expect(given.toolbar.loadToolbar).not.toHaveBeenCalled() - }) - - it('should return false when parsing invalid JSON from fragment state', () => { - given('hashParams', () => ({ - access_token: 'test_access_token', - state: 'literally', - expires_in: 3600, - })) - - expect(given.subject()).toEqual(false) - expect(given.toolbar.loadToolbar).not.toHaveBeenCalled() - }) - - it('should work if calling toolbar params `__posthog`', () => { - given('hashParams', () => ({ - access_token: given.accessToken, - __posthog: encodeURIComponent(JSON.stringify(given.toolbarParams)), - expires_in: 3600, - })) - - given.subject() - expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ ...given.toolbarParams, source: 'url' }) - }) - - it('should use the apiURL in the hash if available', () => { - given.hashState.apiURL = 'blabla' - - given.toolbar.maybeLoadToolbar(given.location, given.localStorage, given.history) - - expect(given.toolbar.loadToolbar).toHaveBeenCalledWith({ - ...given.toolbarParams, - apiURL: 'blabla', - source: 'url', - }) - }) - }) - - describe('load and close toolbar', () => { - given('subject', () => () => given.toolbar.loadToolbar(given.toolbarParams)) - - given('toolbarParams', () => ({ - accessToken: 'accessToken', - token: 'public_token', - expiresAt: 'expiresAt', - apiKey: 'apiKey', - })) - - it('should persist for next time', () => { - expect(given.subject()).toBe(true) - expect(JSON.parse(window.localStorage.getItem('_postHogToolbarParams'))).toEqual({ - ...given.toolbarParams, - apiURL: 'http://api.example.com', - }) - }) - - it('should load if not previously loaded', () => { - expect(given.subject()).toBe(true) - expect(window.ph_load_toolbar).toHaveBeenCalledWith( - { ...given.toolbarParams, apiURL: 'http://api.example.com' }, - given.lib - ) - }) - - it('should NOT load if previously loaded', () => { - expect(given.subject()).toBe(true) - expect(given.subject()).toBe(false) - }) - }) - - describe('load and close toolbar with minimal params', () => { - given('subject', () => () => given.toolbar.loadToolbar(given.toolbarParams)) - - given('toolbarParams', () => ({ - accessToken: 'accessToken', - })) - - it('should load if not previously loaded', () => { - expect(given.subject()).toBe(true) - expect(window.ph_load_toolbar).toHaveBeenCalledWith( - { - ...given.toolbarParams, - apiURL: 'http://api.example.com', - token: 'test_token', - }, - given.lib - ) - }) - - it('should NOT load if previously loaded', () => { - expect(given.subject()).toBe(true) - expect(given.subject()).toBe(false) - }) - }) -}) diff --git a/src/__tests__/extensions/toolbar.test.ts b/src/__tests__/extensions/toolbar.test.ts new file mode 100644 index 000000000..27f63b418 --- /dev/null +++ b/src/__tests__/extensions/toolbar.test.ts @@ -0,0 +1,199 @@ +import { Toolbar } from '../../extensions/toolbar' +import { _isString, _isUndefined } from '../../utils/type-utils' +import { PostHog } from '../../posthog-core' +import { PostHogConfig, ToolbarParams } from '../../types' +import { window } from '../../utils/globals' + +jest.mock('../../utils', () => ({ + ...jest.requireActual('../../utils'), + loadScript: jest.fn((_path: any, callback: any) => callback()), +})) + +const makeToolbarParams = (overrides: Partial): ToolbarParams => ({ + token: 'test_token', + ...overrides, +}) + +describe('Toolbar', () => { + let toolbar: Toolbar + let instance: PostHog + const toolbarParams = makeToolbarParams({}) + + beforeEach(() => { + instance = { + config: { + api_host: 'http://api.example.com', + token: 'test_token', + } as unknown as PostHogConfig, + set_config: jest.fn(), + } as unknown as PostHog + toolbar = new Toolbar(instance) + }) + + beforeEach(() => { + ;(window as any).ph_load_toolbar = jest.fn() + delete (window as any)['_postHogToolbarLoaded'] + }) + + describe('maybeLoadToolbar', () => { + const localStorage = { + getItem: jest.fn(), + setItem: jest.fn(), + } + const history = { replaceState: jest.fn() } + + const defaultHashState = { + action: 'ph_authorize', + desiredHash: '#myhash', + projectId: 3, + projectOwnerId: 722725, + readOnly: false, + token: 'test_token', + userFlags: { + flag_1: 0, + flag_2: 1, + }, + userId: 12345, + } + + const withHashParamsFrom = ( + hashState: Record | string = defaultHashState, + key: string = 'state' + ) => ({ + access_token: 'access token', + [key]: encodeURIComponent(_isString(hashState) ? hashState : JSON.stringify(hashState)), + expires_in: 3600, + }) + + const withHash = (hashParams: Record) => { + return Object.keys(hashParams) + .map((k) => `${k}=${hashParams[k]}`) + .join('&') + } + + const aLocation = (hash?: string) => { + if (_isUndefined(hash)) { + hash = withHash(withHashParamsFrom()) + } + + return { + hash: `#${hash}`, + pathname: 'pathname', + search: '?search', + } + } + + beforeEach(() => { + localStorage.getItem.mockImplementation(() => {}) + + jest.spyOn(toolbar, 'loadToolbar') + }) + + it('should initialize the toolbar when the hash state contains action "ph_authorize"', () => { + // the default hash state in the test setup contains the action "ph_authorize" + toolbar.maybeLoadToolbar(aLocation(), localStorage, history) + + expect(toolbar.loadToolbar).toHaveBeenCalledWith({ + ...toolbarParams, + ...defaultHashState, + source: 'url', + }) + }) + + it('should initialize the toolbar when there are editor params in the session', () => { + // if the hash state does not contain ph_authorize then look in storage + localStorage.getItem.mockImplementation(() => JSON.stringify(toolbarParams)) + + const hashState = { ...defaultHashState, action: undefined } + toolbar.maybeLoadToolbar(aLocation(withHash(withHashParamsFrom(hashState))), localStorage, history) + + expect(toolbar.loadToolbar).toHaveBeenCalledWith({ + ...toolbarParams, + source: 'localstorage', + }) + }) + + it('should NOT initialize the toolbar when the activation query param does not exist', () => { + expect(toolbar.maybeLoadToolbar(aLocation(''), localStorage, history)).toEqual(false) + + expect(toolbar.loadToolbar).not.toHaveBeenCalled() + }) + + it('should return false when parsing invalid JSON from fragment state', () => { + expect( + toolbar.maybeLoadToolbar(aLocation(withHash(withHashParamsFrom('literally'))), localStorage, history) + ).toEqual(false) + expect(toolbar.loadToolbar).not.toHaveBeenCalled() + }) + + it('should work if calling toolbar params `__posthog`', () => { + toolbar.maybeLoadToolbar( + aLocation(withHash(withHashParamsFrom(defaultHashState, '__posthog'))), + localStorage, + history + ) + expect(toolbar.loadToolbar).toHaveBeenCalledWith({ ...toolbarParams, ...defaultHashState, source: 'url' }) + }) + + it('should use the apiURL in the hash if available', () => { + toolbar.maybeLoadToolbar( + aLocation(withHash(withHashParamsFrom({ ...defaultHashState, apiURL: 'blabla' }))), + localStorage, + history + ) + + expect(toolbar.loadToolbar).toHaveBeenCalledWith({ + ...toolbarParams, + ...defaultHashState, + apiURL: 'blabla', + source: 'url', + }) + }) + }) + + describe('load and close toolbar', () => { + it('should persist for next time', () => { + expect(toolbar.loadToolbar(toolbarParams)).toBe(true) + expect(JSON.parse((window as any).localStorage.getItem('_postHogToolbarParams'))).toEqual({ + ...toolbarParams, + apiURL: 'http://api.example.com', + }) + }) + + it('should load if not previously loaded', () => { + expect(toolbar.loadToolbar(toolbarParams)).toBe(true) + expect((window as any).ph_load_toolbar).toHaveBeenCalledWith( + { ...toolbarParams, apiURL: 'http://api.example.com' }, + instance + ) + }) + + it('should NOT load if previously loaded', () => { + expect(toolbar.loadToolbar(toolbarParams)).toBe(true) + expect(toolbar.loadToolbar(toolbarParams)).toBe(false) + }) + }) + + describe('load and close toolbar with minimal params', () => { + const minimalToolbarParams: ToolbarParams = { + token: 'accessToken', + } + + it('should load if not previously loaded', () => { + expect(toolbar.loadToolbar(minimalToolbarParams)).toBe(true) + expect((window as any).ph_load_toolbar).toHaveBeenCalledWith( + { + ...minimalToolbarParams, + apiURL: 'http://api.example.com', + token: 'accessToken', + }, + instance + ) + }) + + it('should NOT load if previously loaded', () => { + expect(toolbar.loadToolbar(minimalToolbarParams)).toBe(true) + expect(toolbar.loadToolbar(minimalToolbarParams)).toBe(false) + }) + }) +})