diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 19d4b5770..4b4ff8587 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -10,15 +10,27 @@ # $ git commit --no-verify # $ git commit -n +# Check if there are any staged changes +if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 +fi + echo "Running linter..." -bundle exec rake standard +(cd app && bundle exec rubocop --autocorrect-all) linter_status=$? if [ $linter_status -ne 0 ]; then - echo "Fix above before committing. Run 'git commit -n' to bypass linter." + echo "Fix remaining linting issues before committing. Run 'git commit -n' to bypass linter." exit 1 fi +# Stage the files that were auto-corrected by RuboCop +git add app/**/*.rb + +echo "Running JavaScript/TypeScript formatter..." +(cd app && npm run format) + echo "Running Terraform formatter" files=$(git diff --cached --name-only terraform) for f in $files @@ -37,18 +49,15 @@ COMMIT_MSG_FILE=$1 # Get the current branch name BRANCH_NAME=$(git symbolic-ref --short HEAD) -# Extract the ticket number from the branch name -# This sed command looks for 'ffs' followed by an optional single character delimiter, -# then captures all following digits -TICKET=$(echo $BRANCH_NAME | sed -E 's/.*ffs[-_]?([0-9]+).*/\1/' | tr '[:lower:]' '[:upper:]') - -if [ -n "$TICKET" ]; then +# Only proceed if branch starts with ffs- or FFS- followed by numbers +if [[ $BRANCH_NAME =~ ^[Ff][Ff][Ss]-([0-9]+) ]]; then + TICKET="${BASH_REMATCH[1]}" + # Read the current commit message COMMIT_MSG=$(cat $COMMIT_MSG_FILE) # Check if the commit message already starts with the ticket number if [[ $COMMIT_MSG != FFS-$TICKET:* ]]; then - # Prepend the ticket number to the commit message - sed -i.bak "1s/^/FFS-$TICKET: /" $COMMIT_MSG_FILE + sed -i.bak '1s|^|FFS-'"$TICKET"': |' "$COMMIT_MSG_FILE" fi fi \ No newline at end of file diff --git a/app/.prettierrc b/app/.prettierrc new file mode 100644 index 000000000..147f381f8 --- /dev/null +++ b/app/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "tabWidth": 2 +} \ No newline at end of file diff --git a/app/app/javascript/application.js b/app/app/javascript/application.js index b2d64b4dd..3315c781f 100644 --- a/app/app/javascript/application.js +++ b/app/app/javascript/application.js @@ -1,14 +1,14 @@ // Entry point for the build script in your package.json -import "@hotwired/turbo-rails" -import "./controllers" +import '@hotwired/turbo-rails' +import './controllers' -import "@uswds/uswds" +import '@uswds/uswds' // make sure USWDS components are wired to their behavior after a Turbo navigation -import components from "@uswds/uswds/src/js/components" -let initialLoad = true; +import components from '@uswds/uswds/src/js/components' +let initialLoad = true -document.addEventListener("turbo:load", () => { +document.addEventListener('turbo:load', () => { if (initialLoad) { // initial domready is handled by `import "uswds"` code initialLoad = false @@ -20,8 +20,8 @@ document.addEventListener("turbo:load", () => { const behavior = components[key] behavior.on(target) }) -}); +}) -document.addEventListener("turbo:frame-render", () => { - initialLoad = true; -}); +document.addEventListener('turbo:frame-render', () => { + initialLoad = true +}) diff --git a/app/app/javascript/controllers/application.js b/app/app/javascript/controllers/application.js index 1213e85c7..c030eb8c7 100644 --- a/app/app/javascript/controllers/application.js +++ b/app/app/javascript/controllers/application.js @@ -1,9 +1,9 @@ -import { Application } from "@hotwired/stimulus" +import { Application } from '@hotwired/stimulus' const application = Application.start() // Configure Stimulus development experience application.debug = false -window.Stimulus = application +window.Stimulus = application export { application } diff --git a/app/app/javascript/controllers/cbv/employer_search.js b/app/app/javascript/controllers/cbv/employer_search.js index 842ed6223..c1e667b57 100644 --- a/app/app/javascript/controllers/cbv/employer_search.js +++ b/app/app/javascript/controllers/cbv/employer_search.js @@ -1,31 +1,27 @@ -import { Controller } from "@hotwired/stimulus" -import { loadPinwheel, initializePinwheel } from "../../utilities/pinwheel" -import { fetchToken } from '../../utilities/api'; -import { trackUserAction } from '../../utilities/api'; +import { Controller } from '@hotwired/stimulus' +import { loadPinwheel, initializePinwheel } from '../../utilities/pinwheel' +import { fetchToken } from '../../utilities/api' +import { trackUserAction } from '../../utilities/api' export default class extends Controller { - static targets = [ - "form", - "userAccountId", - "employerButton" - ]; + static targets = ['form', 'userAccountId', 'employerButton'] static values = { - cbvFlowId: Number + cbvFlowId: Number, } - pinwheel = loadPinwheel(); + pinwheel = loadPinwheel() connect() { - this.errorHandler = document.addEventListener("turbo:frame-missing", this.onTurboError) + this.errorHandler = document.addEventListener('turbo:frame-missing', this.onTurboError) } disconnect() { - document.removeEventListener("turbo:frame-missing", this.errorHandler) + document.removeEventListener('turbo:frame-missing', this.errorHandler) } onTurboError(event) { - console.warn("Got turbo error, redirecting:", event) + console.warn('Got turbo error, redirecting:', event) const location = event.detail.response.url event.detail.visit(location) @@ -36,87 +32,87 @@ export default class extends Controller { if (eventName === 'success') { const { accountId } = eventPayload this.userAccountIdTarget.value = accountId - trackUserAction("PinwheelSuccess", { + trackUserAction('PinwheelSuccess', { account_id: eventPayload.accountId, - platform_id: eventPayload.platformId + platform_id: eventPayload.platformId, }) - this.formTarget.submit(); + this.formTarget.submit() } else if (eventName === 'screen_transition') { const { screenName } = eventPayload switch (screenName) { - case "LOGIN": - trackUserAction("PinwheelShowLoginPage", { + case 'LOGIN': + trackUserAction('PinwheelShowLoginPage', { screen_name: screenName, employer_name: eventPayload.selectedEmployerName, - platform_name: eventPayload.selectedPlatformName + platform_name: eventPayload.selectedPlatformName, }) break - case "PROVIDER_CONFIRMATION": - trackUserAction("PinwheelShowProviderConfirmationPage", {}) + case 'PROVIDER_CONFIRMATION': + trackUserAction('PinwheelShowProviderConfirmationPage', {}) break - case "SEARCH_DEFAULT": - trackUserAction("PinwheelShowDefaultProviderSearch", {}) + case 'SEARCH_DEFAULT': + trackUserAction('PinwheelShowDefaultProviderSearch', {}) break - case "EXIT_CONFIRMATION": - trackUserAction("PinwheelAttemptClose", {}) + case 'EXIT_CONFIRMATION': + trackUserAction('PinwheelAttemptClose', {}) break } } else if (eventName === 'login_attempt') { - trackUserAction("PinwheelAttemptLogin", {}) + trackUserAction('PinwheelAttemptLogin', {}) } else if (eventName === 'error') { const { type, code, message } = eventPayload - trackUserAction("PinwheelError", { type, code, message }) + trackUserAction('PinwheelError', { type, code, message }) } else if (eventName === 'exit') { - trackUserAction("PinwheelCloseModal", {}) + trackUserAction('PinwheelCloseModal', {}) this.showHelpBanner() } } getDocumentLocale() { - const docLocale = document.documentElement.lang; - if (docLocale) return docLocale; + const docLocale = document.documentElement.lang + if (docLocale) return docLocale // Extract locale from URL path (e.g., /en/cbv/employer_search) - const pathMatch = window.location.pathname.match(/^\/([a-z]{2})\//i); - return pathMatch ? pathMatch[1] : 'en'; + const pathMatch = window.location.pathname.match(/^\/([a-z]{2})\//i) + return pathMatch ? pathMatch[1] : 'en' } async select(event) { - const locale = this.getDocumentLocale(); - const { responseType, id, name, isDefaultOption } = event.target.dataset; - await trackUserAction("ApplicantSelectedEmployerOrPlatformItem", { + const locale = this.getDocumentLocale() + const { responseType, id, name, isDefaultOption } = event.target.dataset + await trackUserAction('ApplicantSelectedEmployerOrPlatformItem', { item_type: responseType, item_id: id, item_name: name, is_default_option: isDefaultOption, - locale + locale, }) this.disableButtons() - const { token } = await fetchToken(responseType, id, locale); - this.submit(token); + const { token } = await fetchToken(responseType, id, locale) + this.submit(token) } submit(token) { - this.pinwheel.then(Pinwheel => initializePinwheel(Pinwheel, token, { - onEvent: this.onPinwheelEvent.bind(this), - onExit: this.reenableButtons.bind(this), - })); + this.pinwheel.then((Pinwheel) => + initializePinwheel(Pinwheel, token, { + onEvent: this.onPinwheelEvent.bind(this), + onExit: this.reenableButtons.bind(this), + }) + ) } disableButtons() { - this.employerButtonTargets - .forEach(el => el.setAttribute("disabled", "disabled")) + this.employerButtonTargets.forEach((el) => el.setAttribute('disabled', 'disabled')) } reenableButtons() { - this.employerButtonTargets - .forEach(el => el.removeAttribute("disabled")) + this.employerButtonTargets.forEach((el) => el.removeAttribute('disabled')) } showHelpBanner() { - const url = new URL(window.location.href); - url.searchParams.set('help', 'true'); - window.location.href = url.toString(); + const url = new URL(window.location.href) + url.searchParams.set('help', 'true') + window.location.href = url.toString() } } diff --git a/app/app/javascript/controllers/cbv/synchronizations_controller.js b/app/app/javascript/controllers/cbv/synchronizations_controller.js index 0da0c0171..30f867a99 100644 --- a/app/app/javascript/controllers/cbv/synchronizations_controller.js +++ b/app/app/javascript/controllers/cbv/synchronizations_controller.js @@ -1,28 +1,31 @@ -import { Controller } from "@hotwired/stimulus" +import { Controller } from '@hotwired/stimulus' import * as ActionCable from '@rails/actioncable' export default class extends Controller { - static targets = ["form", "userAccountId"]; + static targets = ['form', 'userAccountId'] - cable = ActionCable.createConsumer(); + cable = ActionCable.createConsumer() connect() { - this.cable.subscriptions.create({ channel: 'PaystubsChannel', account_id: this.userAccountIdTarget.value }, { - connected: () => { - console.log("Connected to the channel:", this); - }, - disconnected: () => { - console.log("Disconnected"); - }, - received: (data) => { - if (data.event === 'cbv.status_update') { - if (data.has_fully_synced) { - const accountId = data.account_id; - this.userAccountIdTarget.value = accountId; - this.formTarget.submit(); + this.cable.subscriptions.create( + { channel: 'PaystubsChannel', account_id: this.userAccountIdTarget.value }, + { + connected: () => { + console.log('Connected to the channel:', this) + }, + disconnected: () => { + console.log('Disconnected') + }, + received: (data) => { + if (data.event === 'cbv.status_update') { + if (data.has_fully_synced) { + const accountId = data.account_id + this.userAccountIdTarget.value = accountId + this.formTarget.submit() + } } - } + }, } - }); + ) } } diff --git a/app/app/javascript/controllers/help.js b/app/app/javascript/controllers/help.js index 291e3ea9d..d7950c1fa 100644 --- a/app/app/javascript/controllers/help.js +++ b/app/app/javascript/controllers/help.js @@ -1,21 +1,21 @@ -import { Controller } from "@hotwired/stimulus" -import { trackUserAction } from "../utilities/help" +import { Controller } from '@hotwired/stimulus' +import { trackUserAction } from '../utilities/help' export default class extends Controller { - static targets = ["iframe"] + static targets = ['iframe'] connect() { this.handleClick = (event) => { - if (event.target.href?.includes("#help-modal")) { - trackUserAction("ApplicantOpenedHelpModal", event.target.dataset.source) + if (event.target.href?.includes('#help-modal')) { + trackUserAction('ApplicantOpenedHelpModal', event.target.dataset.source) } } - - document.addEventListener("click", this.handleClick) + + document.addEventListener('click', this.handleClick) } disconnect() { - document.removeEventListener("click", this.handleClick) + document.removeEventListener('click', this.handleClick) } /** @@ -27,14 +27,14 @@ export default class extends Controller { prepareNextUrl() { const iframe = this.iframeTarget const currentSrc = new URL(iframe.src) - + // Generate a new random parameter const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(2))) - .map(b => b.toString(16).padStart(2, "0")) - .join("") - + .map((b) => b.toString(16).padStart(2, '0')) + .join('') + // Update the iframe src with new random parameter - currentSrc.searchParams.set("r", randomHex) + currentSrc.searchParams.set('r', randomHex) iframe.src = currentSrc.toString() } -} \ No newline at end of file +} diff --git a/app/app/javascript/controllers/index.js b/app/app/javascript/controllers/index.js index 94200f235..f25b49fcd 100644 --- a/app/app/javascript/controllers/index.js +++ b/app/app/javascript/controllers/index.js @@ -2,12 +2,12 @@ // Run that command whenever you add a new controller or create them with // ./bin/rails generate stimulus controllerName -import { application } from "./application" +import { application } from './application' -import CbvEmployerSearch from "./cbv/employer_search" -import CbvSynchronizationsController from "./cbv/synchronizations_controller" -import HelpController from "./help" +import CbvEmployerSearch from './cbv/employer_search' +import CbvSynchronizationsController from './cbv/synchronizations_controller' +import HelpController from './help' -application.register("cbv-employer-search", CbvEmployerSearch) -application.register("cbv-synchronizations", CbvSynchronizationsController) -application.register("help", HelpController) +application.register('cbv-employer-search', CbvEmployerSearch) +application.register('cbv-synchronizations', CbvSynchronizationsController) +application.register('help', HelpController) diff --git a/app/app/javascript/utilities/api.js b/app/app/javascript/utilities/api.js index 63c226c02..5fb4ca793 100644 --- a/app/app/javascript/utilities/api.js +++ b/app/app/javascript/utilities/api.js @@ -1,19 +1,18 @@ -import { fetchInternal } from './fetchInternal'; +import { fetchInternal } from './fetchInternal' -export const PINWHEEL_USER_ACTION = '/api/pinwheel/user_action'; -export const PINWHEEL_TOKENS_GENERATE = '/api/pinwheel/tokens'; +export const PINWHEEL_USER_ACTION = '/api/pinwheel/user_action' +export const PINWHEEL_TOKENS_GENERATE = '/api/pinwheel/tokens' -export const trackUserAction = (eventName, attributes, scope="pinwheel") => { +export const trackUserAction = (eventName, attributes, scope = 'pinwheel') => { return fetchInternal(PINWHEEL_USER_ACTION, { method: 'post', body: JSON.stringify({ [scope]: { event_name: eventName, attributes } }), }) -}; +} export const fetchToken = (response_type, id, locale) => { return fetchInternal(PINWHEEL_TOKENS_GENERATE, { method: 'post', body: JSON.stringify({ response_type, id, locale }), }) -}; - +} diff --git a/app/app/javascript/utilities/csrf.ts b/app/app/javascript/utilities/csrf.ts index 54a53355f..6f58fa7cc 100644 --- a/app/app/javascript/utilities/csrf.ts +++ b/app/app/javascript/utilities/csrf.ts @@ -1,36 +1,36 @@ // Borrowed from https://github.com/18F/identity-idp/blob/59bc8bb6c47402f386d9248bfad3c0803f68187e/app/javascript/packages/request/index.ts#L25-L59 export default class CSRF { static get token(): string | null { - return this.#tokenMetaElement?.content || null; + return this.#tokenMetaElement?.content || null } static set token(value: string | null) { if (!value) { - return; + return } if (this.#tokenMetaElement) { - this.#tokenMetaElement.content = value; + this.#tokenMetaElement.content = value } this.#paramInputElements.forEach((input) => { - input.value = value; - }); + input.value = value + }) } static get param(): string | undefined { - return this.#paramMetaElement?.content; + return this.#paramMetaElement?.content } static get #tokenMetaElement(): HTMLMetaElement | null { - return document.querySelector('meta[name="csrf-token"]'); + return document.querySelector('meta[name="csrf-token"]') } static get #paramMetaElement(): HTMLMetaElement | null { - return document.querySelector('meta[name="csrf-param"]'); + return document.querySelector('meta[name="csrf-param"]') } static get #paramInputElements(): NodeListOf { - return document.querySelectorAll(`input[name="${this.param}"]`); + return document.querySelectorAll(`input[name="${this.param}"]`) } } diff --git a/app/app/javascript/utilities/fetchInternal.js b/app/app/javascript/utilities/fetchInternal.js index 0767a8d5d..bbeafbae0 100644 --- a/app/app/javascript/utilities/fetchInternal.js +++ b/app/app/javascript/utilities/fetchInternal.js @@ -1,20 +1,18 @@ -import CSRF from './csrf'; - +import CSRF from './csrf' export const fetchInternal = (uri, params) => { const defaultHeaders = { 'X-CSRF-Token': CSRF.token, - 'Content-Type': 'application/json' - }; + 'Content-Type': 'application/json', + } - return fetch(uri, addHeadersToParams(defaultHeaders, params)) - .then(response => response.json()); -}; + return fetch(uri, addHeadersToParams(defaultHeaders, params)).then((response) => response.json()) +} const addHeadersToParams = (defaultHeaders, params) => { - const requestedHeaders = params.headers; - params.headers = requestedHeaders ? - Object.assign({}, defaultHeaders, requestedHeaders) : - defaultHeaders; + const requestedHeaders = params.headers + params.headers = requestedHeaders + ? Object.assign({}, defaultHeaders, requestedHeaders) + : defaultHeaders - return params; -}; + return params +} diff --git a/app/app/javascript/utilities/fetchInternalAPIService.js b/app/app/javascript/utilities/fetchInternalAPIService.js index 2624d6198..f9311bdfa 100644 --- a/app/app/javascript/utilities/fetchInternalAPIService.js +++ b/app/app/javascript/utilities/fetchInternalAPIService.js @@ -1,20 +1,18 @@ -import CSRF from './csrf'; - +import CSRF from './csrf' export const fetchInternalAPIService = (uri, params) => { const defaultHeaders = { 'X-CSRF-Token': CSRF.token, - 'Content-Type': 'application/json' - }; + 'Content-Type': 'application/json', + } - return fetch(uri, _addHeadersToParams(defaultHeaders, params)) - .then(response => response.json()); -}; + return fetch(uri, _addHeadersToParams(defaultHeaders, params)).then((response) => response.json()) +} const _addHeadersToParams = (defaultHeaders, params) => { - const requestedHeaders = params.headers; - params.headers = requestedHeaders ? - Object.assign({}, defaultHeaders, requestedHeaders) : - defaultHeaders; + const requestedHeaders = params.headers + params.headers = requestedHeaders + ? Object.assign({}, defaultHeaders, requestedHeaders) + : defaultHeaders - return params; -}; + return params +} diff --git a/app/app/javascript/utilities/help.js b/app/app/javascript/utilities/help.js index cca59c8cb..2894773ce 100644 --- a/app/app/javascript/utilities/help.js +++ b/app/app/javascript/utilities/help.js @@ -1,14 +1,14 @@ -import CSRF from './csrf'; +import CSRF from './csrf' -const HELP_USER_ACTION = '/api/help/user_action'; +const HELP_USER_ACTION = '/api/help/user_action' -export const trackUserAction = async (event_name, source) => { +export const trackUserAction = async (event_name, source) => { return fetch(HELP_USER_ACTION, { method: 'post', headers: { 'X-CSRF-Token': CSRF.token, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, body: JSON.stringify({ event_name, source }), - }).then(response => response.json()); -} \ No newline at end of file + }).then((response) => response.json()) +} diff --git a/app/app/javascript/utilities/meta.js b/app/app/javascript/utilities/meta.js index bf2e359cf..cb6829e2e 100644 --- a/app/app/javascript/utilities/meta.js +++ b/app/app/javascript/utilities/meta.js @@ -1,6 +1,6 @@ -function metaContent (name) { +function metaContent(name) { const element = document.head.querySelector(`meta[name="${name}"]`) return element && element.content } -export default metaContent; +export default metaContent diff --git a/app/app/javascript/utilities/pinwheel.js b/app/app/javascript/utilities/pinwheel.js index 3a9fcba5f..ab3f34bd1 100644 --- a/app/app/javascript/utilities/pinwheel.js +++ b/app/app/javascript/utilities/pinwheel.js @@ -1,25 +1,22 @@ -import loadScript from 'load-script'; - +import loadScript from 'load-script' export function loadPinwheel() { return new Promise((resolve, reject) => { loadScript('https://cdn.getpinwheel.com/pinwheel-v3.0.js', (err, script) => { if (err) { - reject(err); + reject(err) } else { - resolve(Pinwheel); + resolve(Pinwheel) } - }); - }); + }) + }) } export function initializePinwheel(Pinwheel, linkToken, callbacks) { Pinwheel.open({ linkToken, - ...callbacks - }); + ...callbacks, + }) - return Pinwheel; + return Pinwheel } - - diff --git a/app/package.json b/app/package.json index b23b04390..a9a537ec5 100644 --- a/app/package.json +++ b/app/package.json @@ -25,11 +25,13 @@ "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets", "build:css": "postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css", "pa11y-ci": "pa11y-ci --config .pa11yci", - "test": "vitest" + "test": "vitest", + "format": "prettier --write \"app/javascript/**/*.{js,jsx,ts,tsx}\" \"spec/javascript/**/*.{js,jsx,ts,tsx}\"" }, "devDependencies": { "jsdom": "^24.0.0", "pa11y-ci": "^3.1.0", + "prettier": "^3.2.5", "vitest": "^3.0.5" }, "resolutions": { diff --git a/app/spec/javascript/application.spec.js b/app/spec/javascript/application.spec.js index 7e5551f15..b1389a1e9 100644 --- a/app/spec/javascript/application.spec.js +++ b/app/spec/javascript/application.spec.js @@ -1,18 +1,18 @@ -import { expect, test, beforeEach } from "vitest"; -import { application } from "@js/controllers/application"; +import { expect, test, beforeEach } from 'vitest' +import { application } from '@js/controllers/application' beforeEach(() => { // Reset application state before each test - window.Stimulus = window.Stimulus || {}; -}); + window.Stimulus = window.Stimulus || {} +}) -test("Expect Application to be setup correctly", () => { - expect(window).toHaveProperty("Stimulus"); - expect(window.Stimulus.debug).toBeFalsy(); -}); +test('Expect Application to be setup correctly', () => { + expect(window).toHaveProperty('Stimulus') + expect(window.Stimulus.debug).toBeFalsy() +}) -test("Expect application to be registered", () => { - expect(application).toBeTruthy(); - expect(typeof application.register).toBe("function"); - expect(application.debug).toBe(false); -}); +test('Expect application to be registered', () => { + expect(application).toBeTruthy() + expect(typeof application.register).toBe('function') + expect(application.debug).toBe(false) +}) diff --git a/app/spec/javascript/test_setup.js b/app/spec/javascript/test_setup.js index 15625e86e..eaae98546 100644 --- a/app/spec/javascript/test_setup.js +++ b/app/spec/javascript/test_setup.js @@ -1,19 +1,19 @@ -import { vi } from 'vitest'; -import { Application } from '@hotwired/stimulus'; +import { vi } from 'vitest' +import { Application } from '@hotwired/stimulus' // Set up Stimulus -window.Stimulus = Application.start(); +window.Stimulus = Application.start() // Mock CSRF token document.head.innerHTML = ` -`; +` // Mock fetch API -global.fetch = vi.fn(); +global.fetch = vi.fn() // Mock window.matchMedia -window.matchMedia = vi.fn().mockImplementation(query => ({ +window.matchMedia = vi.fn().mockImplementation((query) => ({ matches: false, media: query, onchange: null, @@ -22,17 +22,17 @@ window.matchMedia = vi.fn().mockImplementation(query => ({ addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), -})); +})) // Reset all mocks before each test beforeEach(() => { - vi.clearAllMocks(); - fetch.mockReset(); -}); + vi.clearAllMocks() + fetch.mockReset() +}) // Clean up after each test afterEach(() => { document.head.innerHTML = ` - `; -}); \ No newline at end of file + ` +}) diff --git a/app/spec/javascript/utilities/api.spec.js b/app/spec/javascript/utilities/api.spec.js index ac2f94e25..53bcf5d55 100644 --- a/app/spec/javascript/utilities/api.spec.js +++ b/app/spec/javascript/utilities/api.spec.js @@ -1,146 +1,144 @@ -import { vi, describe, beforeEach, afterEach, it, expect } from "vitest"; -import * as api from "@js/utilities/api"; -import * as fetchAPIService from "@js/utilities/fetchInternalAPIService"; +import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest' +import * as api from '@js/utilities/api' +import * as fetchAPIService from '@js/utilities/fetchInternalAPIService' global.fetch = vi.fn() function createFetchResponse(data) { return { json: () => new Promise((resolve) => resolve(data)) } } - // Here we tell Vitest to mock fetch on the `window` object. +// Here we tell Vitest to mock fetch on the `window` object. describe('trackUserAction', () => { - beforeEach(async() => { - // Mock the fetch function. - const mockResponse = { - "pinwheel": { - "event_name": "Event", - "attributes": {} - } - }; - fetch.mockResolvedValue(createFetchResponse(mockResponse)) - }) - - it('sends a post request to the user_action endpoint', async () => { - const data = await api.trackUserAction("MockEventType", {}) - - // Check that fetch was called exactly once - expect(data.pinwheel.event_name).toBe("Event") - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][0]).toBe('/api/pinwheel/user_action') - expect(fetch.mock.calls[0][1]['method']).toBe('post') - }) - - it('includes CSRV and Content-Type headers', async () => { - const data = await api.trackUserAction("Event", {}) - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][1]).toHaveProperty('headers') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') - expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/json') - }) - - it('has expected request body', async() => { - const data = await api.trackUserAction("MockEventType", {}) - expect(fetch.mock.calls[0][1]['body']).toMatchSnapshot() - }) - it('has expected response payload', async() => { - const data = await api.trackUserAction("MockEventType", {}) - expect(data).toMatchSnapshot() - }) - - - afterEach(() => { - fetch.mockReset() - }) + beforeEach(async () => { + // Mock the fetch function. + const mockResponse = { + pinwheel: { + event_name: 'Event', + attributes: {}, + }, + } + fetch.mockResolvedValue(createFetchResponse(mockResponse)) + }) + + it('sends a post request to the user_action endpoint', async () => { + const data = await api.trackUserAction('MockEventType', {}) + + // Check that fetch was called exactly once + expect(data.pinwheel.event_name).toBe('Event') + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][0]).toBe('/api/pinwheel/user_action') + expect(fetch.mock.calls[0][1]['method']).toBe('post') + }) + + it('includes CSRV and Content-Type headers', async () => { + const data = await api.trackUserAction('Event', {}) + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][1]).toHaveProperty('headers') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') + expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/json') + }) + + it('has expected request body', async () => { + const data = await api.trackUserAction('MockEventType', {}) + expect(fetch.mock.calls[0][1]['body']).toMatchSnapshot() + }) + it('has expected response payload', async () => { + const data = await api.trackUserAction('MockEventType', {}) + expect(data).toMatchSnapshot() + }) + + afterEach(() => { + fetch.mockReset() + }) }) describe('fetchToken', () => { - beforeEach(async() => { - // Mock the fetch function. - const mockResponse = "token-response" - fetch.mockResolvedValue(createFetchResponse(mockResponse)) - }) - - it('sends a post request to the pinwheel tokens endpoint', async () => { - const data = await api.fetchToken("response_type", "id", "en") - - // Check that fetch was called exactly once - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][0]).toBe('/api/pinwheel/tokens') - expect(fetch.mock.calls[0][1]['method']).toBe('post') - }) - - it('includes CSRV and Content-Type headers', async () => { - const data = await api.fetchToken("response_type", "id", "en") - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][1]).toHaveProperty('headers') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') - expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/json') - }) - - it('has expected request body', async() => { - const data = await api.fetchToken("response_type", "id", "en") - expect(fetch.mock.calls[0][1]['body']).toMatchSnapshot() - }) - it('has expected response payload', async() => { - const data = await api.fetchToken("response_type", "id", "en") - expect(data).toMatchSnapshot() - }) - - afterEach(() => { - fetch.mockReset() - }) + beforeEach(async () => { + // Mock the fetch function. + const mockResponse = 'token-response' + fetch.mockResolvedValue(createFetchResponse(mockResponse)) + }) + + it('sends a post request to the pinwheel tokens endpoint', async () => { + const data = await api.fetchToken('response_type', 'id', 'en') + + // Check that fetch was called exactly once + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][0]).toBe('/api/pinwheel/tokens') + expect(fetch.mock.calls[0][1]['method']).toBe('post') + }) + + it('includes CSRV and Content-Type headers', async () => { + const data = await api.fetchToken('response_type', 'id', 'en') + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][1]).toHaveProperty('headers') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') + expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/json') + }) + + it('has expected request body', async () => { + const data = await api.fetchToken('response_type', 'id', 'en') + expect(fetch.mock.calls[0][1]['body']).toMatchSnapshot() + }) + it('has expected response payload', async () => { + const data = await api.fetchToken('response_type', 'id', 'en') + expect(data).toMatchSnapshot() + }) + + afterEach(() => { + fetch.mockReset() + }) }) describe('fetchInternalAPIService', () => { - beforeEach(async() => { - // Mock the fetch function. - const mockResponse = "good" - fetch.mockResolvedValue(createFetchResponse(mockResponse)) - }) - - it('sends a get request to the arbitrary endpoint', async () => { - const response = await fetchAPIService.fetchInternalAPIService('/api/arbitrary', { - "method": "get", - }) - // Check that fetch was called exactly once - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][0]).toBe('/api/arbitrary') - expect(fetch.mock.calls[0][1]['method']).toBe('get') - expect(response).toBe('good') - }) - - it('includes CSRV and Content-Type headers', async () => { - const response = await fetchAPIService.fetchInternalAPIService('/api/arbitrary', { - "method": "get", - }) - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][1]).toHaveProperty('headers') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') - expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/json') - }) - - it('overrides headers', async() => { - const response = await fetchAPIService.fetchInternalAPIService('/api/arbitrary', { - "method": "get", - "headers": { - "Content-Type": "application/csv", - "other": "header" - } - }) - - expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch.mock.calls[0][1]).toHaveProperty('headers') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') - expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/csv') - expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('other') - - }) - - afterEach(() => { - fetch.mockReset() - }) -}) \ No newline at end of file + beforeEach(async () => { + // Mock the fetch function. + const mockResponse = 'good' + fetch.mockResolvedValue(createFetchResponse(mockResponse)) + }) + + it('sends a get request to the arbitrary endpoint', async () => { + const response = await fetchAPIService.fetchInternalAPIService('/api/arbitrary', { + method: 'get', + }) + // Check that fetch was called exactly once + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][0]).toBe('/api/arbitrary') + expect(fetch.mock.calls[0][1]['method']).toBe('get') + expect(response).toBe('good') + }) + + it('includes CSRV and Content-Type headers', async () => { + const response = await fetchAPIService.fetchInternalAPIService('/api/arbitrary', { + method: 'get', + }) + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][1]).toHaveProperty('headers') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') + expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/json') + }) + + it('overrides headers', async () => { + const response = await fetchAPIService.fetchInternalAPIService('/api/arbitrary', { + method: 'get', + headers: { + 'Content-Type': 'application/csv', + other: 'header', + }, + }) + + expect(fetch).toHaveBeenCalledTimes(1) + expect(fetch.mock.calls[0][1]).toHaveProperty('headers') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('X-CSRF-Token') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('Content-Type') + expect(fetch.mock.calls[0][1]['headers']['Content-Type']).toEqual('application/csv') + expect(fetch.mock.calls[0][1]['headers']).toHaveProperty('other') + }) + + afterEach(() => { + fetch.mockReset() + }) +}) diff --git a/app/spec/javascript/utilities/pinwheel.spec.js b/app/spec/javascript/utilities/pinwheel.spec.js index 6bfcefa57..b09389e2b 100644 --- a/app/spec/javascript/utilities/pinwheel.spec.js +++ b/app/spec/javascript/utilities/pinwheel.spec.js @@ -1,64 +1,64 @@ -import { vi, describe, beforeEach, afterEach, it, expect } from "vitest"; -import * as pinwheel from "@js/utilities/pinwheel"; -import loadScript from "load-script"; +import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest' +import * as pinwheel from '@js/utilities/pinwheel' +import loadScript from 'load-script' -const MOCK_PINWHEEL_MODULE = { - open: vi.fn() -} -const MOCK_PINWHEEL_ERROR = "Failed to load SCRIPT" +const MOCK_PINWHEEL_MODULE = { + open: vi.fn(), +} +const MOCK_PINWHEEL_ERROR = 'Failed to load SCRIPT' const MOCK_LOAD_PINWHEEL_SUCCESS_IMPLEMENTATION = (url, callback) => { - vi.stubGlobal('Pinwheel', MOCK_PINWHEEL_MODULE) - callback(null, window.Pinwheel) + vi.stubGlobal('Pinwheel', MOCK_PINWHEEL_MODULE) + callback(null, window.Pinwheel) } const MOCK_PINWHEEL_INIT_CALLBACKS = { - onError: vi.fn(), - onSuccess: vi.fn() + onError: vi.fn(), + onSuccess: vi.fn(), } vi.mock('load-script', () => { - return { - default: vi.fn(), - } + return { + default: vi.fn(), + } }) describe('loadPinwheel', () => { - it('calls API endpoint', () => { - pinwheel.loadPinwheel() - expect(loadScript).toBeCalledTimes(1) - }); - it('uses the correct pinwheel api endpoint', () => { - pinwheel.loadPinwheel() - expect(loadScript.mock.calls[0][0]).toMatch('cdn.getpinwheel.com') - expect(loadScript.mock.calls[0][0]).toMatch('pinwheel-v3') - }) - it('should resolve with Pinwheel object when script loads successfully', async () => { - loadScript.mockImplementation(MOCK_LOAD_PINWHEEL_SUCCESS_IMPLEMENTATION) - const Pinwheel = await pinwheel.loadPinwheel() - expect(Pinwheel).toBeDefined() - expect(Pinwheel).toBe(MOCK_PINWHEEL_MODULE) - }) - it('rejects loading on error', async () => { - loadScript.mockImplementation((url, callback) => { - callback(new Error(MOCK_PINWHEEL_ERROR), null) - }) - await expect(pinwheel.loadPinwheel).rejects.toThrow(MOCK_PINWHEEL_ERROR) - }) - afterEach(() => { - loadScript.mockReset() + it('calls API endpoint', () => { + pinwheel.loadPinwheel() + expect(loadScript).toBeCalledTimes(1) + }) + it('uses the correct pinwheel api endpoint', () => { + pinwheel.loadPinwheel() + expect(loadScript.mock.calls[0][0]).toMatch('cdn.getpinwheel.com') + expect(loadScript.mock.calls[0][0]).toMatch('pinwheel-v3') + }) + it('should resolve with Pinwheel object when script loads successfully', async () => { + loadScript.mockImplementation(MOCK_LOAD_PINWHEEL_SUCCESS_IMPLEMENTATION) + const Pinwheel = await pinwheel.loadPinwheel() + expect(Pinwheel).toBeDefined() + expect(Pinwheel).toBe(MOCK_PINWHEEL_MODULE) + }) + it('rejects loading on error', async () => { + loadScript.mockImplementation((url, callback) => { + callback(new Error(MOCK_PINWHEEL_ERROR), null) }) + await expect(pinwheel.loadPinwheel).rejects.toThrow(MOCK_PINWHEEL_ERROR) + }) + afterEach(() => { + loadScript.mockReset() + }) }) describe('initializePinwheel', () => { - it('opens Pinwheel modal', async () => { - loadScript.mockImplementation(MOCK_LOAD_PINWHEEL_SUCCESS_IMPLEMENTATION) - const Pinwheel = await pinwheel.loadPinwheel() - pinwheel.initializePinwheel(Pinwheel, "link-token", MOCK_PINWHEEL_INIT_CALLBACKS) - expect(Pinwheel.open).toBeCalledTimes(1) - expect(Pinwheel.open.mock.calls[0]).toMatchSnapshot() - }) - afterEach(() => { - loadScript.mockReset() - }) -}) \ No newline at end of file + it('opens Pinwheel modal', async () => { + loadScript.mockImplementation(MOCK_LOAD_PINWHEEL_SUCCESS_IMPLEMENTATION) + const Pinwheel = await pinwheel.loadPinwheel() + pinwheel.initializePinwheel(Pinwheel, 'link-token', MOCK_PINWHEEL_INIT_CALLBACKS) + expect(Pinwheel.open).toBeCalledTimes(1) + expect(Pinwheel.open.mock.calls[0]).toMatchSnapshot() + }) + afterEach(() => { + loadScript.mockReset() + }) +})