/** * @jest-environment jsdom */ import { fireEvent } from '@testing-library/dom' import { camelCase } from 'es-toolkit' import { collectFormSubmissions, getNearestLabel, InputElement } from '../forms' const ogSetTimeout = global.setTimeout const sleep = (ms: number) => new Promise((resolve) => ogSetTimeout(resolve, ms)) describe('forms', () => { let form: HTMLFormElement let submitButton: HTMLButtonElement let originalFormHandler: jest.Mock let originalSubmit: jest.Mock beforeAll(() => { // Stub the missing HTMLFormElement.prototype.submit method. HTMLFormElement.prototype.submit = originalSubmit = jest.fn() }) beforeEach(() => { // Create a form and add it to the DOM. form = document.createElement('form') form.innerHTML = ` ` document.body.appendChild(form) // Stash a reference to the submit button. submitButton = form.querySelector('button')! // Create a mock handler that prevents submission (necessary to prevent jsdom from barfing). originalFormHandler = jest.fn().mockImplementation((event) => { event.preventDefault() }) form.addEventListener('submit', originalFormHandler) }) test('leaves existing form submission in tact', async () => { const callback = jest.fn().mockResolvedValue(undefined) collectFormSubmissions(callback) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) await sleep(200) expect(originalFormHandler).toHaveBeenCalled() expect(callback).toHaveBeenCalled() }) test('leaves form.submit calls in tact', async () => { const callback = jest.fn().mockResolvedValue(undefined) collectFormSubmissions(callback) await form.submit.call(form) expect(originalSubmit).toHaveBeenCalled() expect(callback).toHaveBeenCalled() }) test('ignores sensitive fields like passwords', () => { const callback = jest.fn() collectFormSubmissions(callback) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).not.toHaveBeenCalledWith( expect.objectContaining({ formData: { password: 'sekret' } }) ) }) test('ignores forms with data-koala-collect=off', () => { const callback = jest.fn() collectFormSubmissions(callback) form.setAttribute('data-koala-collect', 'off') fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).not.toHaveBeenCalled() }) test('ignores forms on ignored paths', () => { Object.defineProperty(window, 'location', { value: { pathname: '/contact' } }) const callback = jest.fn() collectFormSubmissions(callback, { ignoredForms: [ { identifier_type: 'path', identifier: '/contact' } ] }) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).not.toHaveBeenCalled() }) test('captures other fields we care about', () => { const callback = jest.fn() collectFormSubmissions(callback) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).toHaveBeenCalledWith( expect.objectContaining({ formData: { name: 'kate', company: 'bigco', 'random-field-we-dont-want': 'who-cares' } }) ) }) test('doesnt blow up form submissions if the callback throws', async () => { const callback = jest.fn().mockRejectedValueOnce(new Error('newp')) collectFormSubmissions(callback) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) await sleep(100) expect(originalFormHandler).toHaveBeenCalled() }) describe('traits', () => { test('collects all form data separate from traits', () => { const callback = jest.fn() collectFormSubmissions(callback) form.querySelector('[name="company"]')?.setAttribute('name', 'random-field') fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).toHaveBeenCalledWith( expect.objectContaining({ formData: { name: 'kate', 'random-field': 'bigco', 'random-field-we-dont-want': 'who-cares' }, traits: {} }) ) }) }) describe('form configurations', () => { test('extracts traits if an email or first/last name is provided', () => { form.innerHTML = ` ` submitButton = form.querySelector('button')! const callback = jest.fn() collectFormSubmissions(callback) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).toHaveBeenCalledWith( expect.objectContaining({ formData: { FIRST_NAME: 'NETTO', First_Name: 'Netto', Last_Name: 'Farah', NAME: 'NETTO FARAH', Name: 'Netto Farah', favorite_color: 'black', first_name: 'Netto', lastName: 'Farah', name: 'netto farah', job_title: 'President of Shipping' }, traits: { firstName: 'NETTO', lastName: 'Farah', name: 'Netto Farah', title: 'President of Shipping' } }) ) }) test('ignores name related fields if no email and no first/last name is provided', () => { form.innerHTML = ` ` submitButton = form.querySelector('button')! const callback = jest.fn() collectFormSubmissions(callback) fireEvent( submitButton, new MouseEvent('click', { bubbles: true, cancelable: true }) ) expect(callback).toHaveBeenCalledWith( expect.objectContaining({ formData: { Last_Name: 'Farah', NAME: 'NETTO FARAH', Name: 'Netto Farah', favorite_color: 'black', lastName: 'Farah', name: 'netto farah' }, traits: {} }) ) }) test('uses associated label for unnamed fields', () => { form.innerHTML = `