diff --git a/src/server/plugins/engine/views/summary.html b/src/server/plugins/engine/views/summary.html index e18ffd654..582346f84 100644 --- a/src/server/plugins/engine/views/summary.html +++ b/src/server/plugins/engine/views/summary.html @@ -88,6 +88,7 @@

{{ t('pages.summary.declaration') } When a better fix is implemented, this class can be removed and preventDoubleClick should be set back to true. #} {{ govukButton({ text: submitButtonText, + classes: "prevent-multiple-clicks", name: "action", value: "send", preventDoubleClick: false diff --git a/test/form/summary-submit-button.test.js b/test/form/summary-submit-button.test.js new file mode 100644 index 000000000..3bfcd3e1f --- /dev/null +++ b/test/form/summary-submit-button.test.js @@ -0,0 +1,92 @@ +import { join } from 'node:path' + +import { initDebounceClick } from '~/src/client/javascripts/debounce-click.js' +import { FORM_PREFIX } from '~/src/server/constants.js' +import { createServer } from '~/src/server/index.js' +import { getFormMetadata } from '~/src/server/plugins/engine/services/formsService.js' +import * as fixtures from '~/test/fixtures/index.js' +import { renderResponse } from '~/test/helpers/component-helpers.js' +import { getCookie, getCookieHeader } from '~/test/utils/get-cookie.js' + +const basePath = `${FORM_PREFIX}/minimal` + +jest.mock('~/src/server/plugins/engine/services/formsService.js') +jest.mock('~/src/server/plugins/engine/services/formSubmissionService.js') + +describe('Page: /summary submit button', () => { + /** @type {Server} */ + let server + + /** @type {ReturnType} */ + let headers + + beforeAll(async () => { + server = await createServer({ + formFileName: 'minimal.js', + formFilePath: join(import.meta.dirname, 'definitions'), + enforceCsrf: true + }) + + await server.initialize() + + const response = await server.inject({ + url: `${basePath}/start` + }) + + const csrfToken = getCookie(response, 'crumb') + headers = getCookieHeader(response, ['session', 'crumb']) + + await server.inject({ + url: `${basePath}/start`, + method: 'POST', + headers, + payload: { + crumb: csrfToken, + field: 'value' + } + }) + }) + + beforeEach(() => { + jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) + }) + + afterAll(async () => { + await server.stop() + }) + + afterEach(() => { + document.body.innerHTML = '' + }) + + it('only submits the form once when the submit button is clicked twice in quick succession', async () => { + const { container } = await renderResponse(server, { + url: `${basePath}/summary`, + headers + }) + + // Wire up the client-side JS against the real rendered page, as + // application.js does in the browser via initAll() + initDebounceClick() + + const $form = /** @type {HTMLFormElement} */ ( + container.getByRole('button', { name: 'Submit' }).closest('form') + ) + const $submit = container.getByRole('button', { name: 'Submit' }) + + const onSubmit = jest.fn((/** @type {Event} */ event) => + // Stop jsdom attempting a real navigation + event.preventDefault() + ) + $form.addEventListener('submit', onSubmit) + + $submit.click() + $submit.click() + + expect(onSubmit).toHaveBeenCalledTimes(1) + }) +}) + +/** + * @import { Server } from '@hapi/hapi' + */