From bab8f735bc60b0ec37d2bed53cfc0f6d18cba5fd Mon Sep 17 00:00:00 2001 From: Steve Clay Date: Tue, 9 Jul 2024 10:01:33 -0400 Subject: [PATCH] Wizard: Add code for Law Enforcement journey and feedback submission (#1961) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [FOIA22-162] Add law enforcement questions and plumbing for tooltips * [FOIA22-162] Update law enforcement questions and allow HTML labels * [FOIA22-162] Link to q22 that was accidentally removed while debugging * [FOIA22-163] Wire up tooltip, needs styling * [FOIA22-162] Latest law enforcement changes * [FOIA22-162] Remove "law enforcement" from Federal answer * [FOIA22-162] Add unique results page m67 * [FOIA22-162] Remove duplicate question misplaced as answer * Adds info button * [FOIA22-142] Add feature tests for agency search and wizard * [FOIA22-163] More accessible modal and better radio button wrapping * [FOIA22-142] Add documentation for running e2e tests locally * Fix pill groups in middle widths * [FOIA22-166] Place flex wrapper around sets of square links * [FOIA22-162] Add topic updating when answers are selected * [FOIA22-166] Support eyebrow text in WYSIWYG messages * Add space before external icon * [FOIA22-165] Add Wizard feedback page, and re-arrange last steps component into separate page - WIP: need to correctly send data to webform * [FOIA22-165] Edit JSDoc syntax * [FOIA22-165] Change `Content-Type` for feedback request, fix issue with `shouldShowFeedbackOption` being set correctly, and minor cleanup * Update document typings * [FOIA22-165] Adjust e2e tests to account for changes to the summary/Last Steps * [FOIA22-165] Only log data in the browser console if the feedback submission is not successful - Proceed with user flow regardless * Don't introduce comment * Adds definition label and icon within modal * [FOIA22-165] Feedback webform data validation, display error messages, make the radio buttons responsive and more accessible, and fix width of modal window * [FOIA22-165] Modal conditionally displays the "Definition" eyebrow & icon - Last Steps modal should not display it * [FOIA22-165] Modal uses the tooltip elements when `title` is empty string (instead of the `isDefinition` prop), and add `contentLabel` for definition tooltips to improve a11y (Elements with `role="dialog"` or `role="alertdialog"` should have accessible names), and don't need to create a new history entry just to populate some error messages in the store * [FOIA22-165] Edit max length message for feedback, and spacing * [FOIA22-167] Configure for API updates Allows us to handle the new polydelta APIs with better control of which URLs are used. There are no effective changes in this commit. * [FOIA22-167] Configure for API updates (2) * Uses title prop for definition eyebrow * [FOIA22-170] Content Revisions for Law Enforcement * [FOIA22-169] Exclude selected words from agency abbreviation matching * [FOIA22-169] Display logging info when excluding an agency match abbreviation, and improve comments detailing the rationale * Explicitly log stop word removals * [FOIA22-167] v2 API ready for production * Adds design adjustments * Fixes lint errors * [FOIA22-162] Margin below agencies wrapper * Addresses feedback * [FOIA22-162] Fixes noticed in QA --------- Co-authored-by: Sirod Johnson Co-authored-by: Donna Hogan Co-authored-by: Matt West --- features/AgencySearch.feature | 19 + features/README.md | 18 + features/Wizard.feature | 60 +++ features/step_definitions/mink-gherkin.js | 10 +- features/support/mink.js | 42 +-- js/components/wizard_component_body_text.jsx | 12 +- .../wizard_component_description.jsx | 19 + .../wizard_component_feedback_radio.jsx | 52 +++ js/components/wizard_component_form_item.jsx | 56 ++- .../wizard_component_info_button.jsx | 23 ++ js/components/wizard_component_label.jsx | 20 + .../wizard_component_last_steps_block.jsx | 71 ---- js/components/wizard_component_modal.jsx | 11 +- js/components/wizard_component_pill_group.jsx | 4 +- js/components/wizard_component_question.jsx | 25 ++ js/components/wizard_component_quote.jsx | 19 + js/components/wizard_layout_flex.jsx | 21 ++ js/components/wizard_layout_inline.jsx | 22 ++ js/components/wizard_layout_question_head.jsx | 21 ++ js/components/wizard_page_continue.jsx | 21 +- js/components/wizard_page_feedback.jsx | 156 ++++++++ js/components/wizard_page_intro.jsx | 2 + js/components/wizard_page_last_steps.jsx | 100 +++++ js/components/wizard_page_query.jsx | 89 +++-- js/components/wizard_page_question.jsx | 29 +- js/components/wizard_page_summary.jsx | 172 +++++---- js/components/wizard_pages.jsx | 7 +- js/models/wizard_extra_messages.js | 10 + js/models/wizard_law_enforcement_records.js | 175 +++++++++ js/models/wizard_topics.js | 4 + js/settings/cloud-gov.js | 4 +- js/settings/ddev.js | 4 +- js/settings/development.js | 4 +- js/settings/forum-one.js | 4 +- js/settings/local.js | 4 +- js/settings/production.js | 4 +- js/settings/staging.js | 4 +- js/settings/uat.js | 4 +- js/stores/wizard_store.js | 77 +++- js/util/wizard_agency_search.js | 23 +- js/util/wizard_api.js | 73 +++- js/util/wizard_helpers.js | 55 ++- js/wizard-types.d.ts | 28 ++ .../_sass/modules/_foia_component_card.scss | 9 +- www.foia.gov/_sass/theme/_wizard_page.scss | 357 ++++++++++++++---- www.foia.gov/img/icon-info-outline.svg | 3 + www.foia.gov/img/icon-radio-checked.svg | 2 +- www.foia.gov/img/icon-radio-unchecked.svg | 2 +- www.foia.gov/img/icon-radio.svg | 3 + 49 files changed, 1613 insertions(+), 341 deletions(-) create mode 100644 features/AgencySearch.feature create mode 100644 features/README.md create mode 100644 features/Wizard.feature create mode 100644 js/components/wizard_component_description.jsx create mode 100644 js/components/wizard_component_feedback_radio.jsx create mode 100644 js/components/wizard_component_info_button.jsx create mode 100644 js/components/wizard_component_label.jsx delete mode 100644 js/components/wizard_component_last_steps_block.jsx create mode 100644 js/components/wizard_component_question.jsx create mode 100644 js/components/wizard_component_quote.jsx create mode 100644 js/components/wizard_layout_flex.jsx create mode 100644 js/components/wizard_layout_inline.jsx create mode 100644 js/components/wizard_layout_question_head.jsx create mode 100644 js/components/wizard_page_feedback.jsx create mode 100644 js/components/wizard_page_last_steps.jsx create mode 100644 js/models/wizard_law_enforcement_records.js create mode 100644 www.foia.gov/img/icon-info-outline.svg create mode 100644 www.foia.gov/img/icon-radio.svg diff --git a/features/AgencySearch.feature b/features/AgencySearch.feature new file mode 100644 index 00000000..2fbdf8ba --- /dev/null +++ b/features/AgencySearch.feature @@ -0,0 +1,19 @@ +@agencysearch +Feature: Agency Search + + As site visitor + I need to be able to search for an agency + So that I can find a particular agency + + Background: + Given I am on "/agency-search.html" + And I wait 60 seconds + + Scenario: The agency type-ahead works + Then I should see "Search an agency name or keyword" + And I enter "ENV" into the agency search box + And I wait 1 second + Then I should see "Council on Environmental Quality" + And I hard click on "the first agency suggestion" + And I wait 5 seconds + Then I should see "The Council on Environmental Quality oversees NEPA implementation" diff --git a/features/README.md b/features/README.md new file mode 100644 index 00000000..db6454f5 --- /dev/null +++ b/features/README.md @@ -0,0 +1,18 @@ +# End-to-end Tests + +## About the testing framework +- The framework used for e2e tests in this project is Cucumber-mink: https://cucumber-mink.js.org/. +- Additional reference: https://cucumber.io/docs/cucumber/api/?lang=javascript + +## Running tests locally +When running the `.feature` tests locally, the API must be made available to the front end: +1. In one terminal window/tab, start the foia-api (`ddev start` or equivalent) +2. In another tab, start the foia.gov front-end in development mode: (`make serve.dev` or `npm run serve:dev`) +3. In another tab, launch the tests: + - To run all `.feature` tests, run `npx cucumber-js` + - To run specific `.feature` tests, run `npx cucumber-js features/AgencySearch.feature features/Wizard.feature` etc. + - The `--fail-fast` flag can be appended to make the test suite exit at any failure rather than attempting to complete the entire batch. + +### Additional Notes +- When debugging tests, the browser automation can be switched to non-headless mode by adding `headless: false`, to the Mink config in `features/support/mink.js`. +- The entire test suite may take ~10 min to complete. diff --git a/features/Wizard.feature b/features/Wizard.feature new file mode 100644 index 00000000..ab1cad29 --- /dev/null +++ b/features/Wizard.feature @@ -0,0 +1,60 @@ +@wizard +Feature: wizard + + As a site visitor + I need to use the wizard + So that I can be guided to the appropriate agency or information + + Background: + Given I am on "/wizard.html" + And I wait 60 seconds + + Scenario: The Wizard loads successfully + Then I should see "Hello," + And I should see "The government hosts a vast amount of information," + And I hard click on "the wizard primary button" + And I wait 1 second + Then I should see "Let's dive in..." + + Scenario: The tax records journey can be navigated + Then I hard click on "the wizard primary button" + Then I wait 5 second + Then the "the wizard primary button" element should not exist + Then I hard click on "the Tax records topic button" + Then the "the wizard primary button" element should exist + Then I hard click on "the wizard primary button" + And I wait 5 seconds + Then I should see "Are you seeking your own records?" + And I select the radio option for the answer "Yes" + And I hard click on "the wizard primary button" + And I wait 1 second + Then I should see "Okay, you’re looking for:" + And I should see "Your own tax records" + And I hard click on "the wizard primary button" + And I wait 1 second + Then I select the radio option for the answer "Copy or transcript of tax return" + And I hard click on "the wizard primary button" + And I wait 1 second + Then I should see "Visit the Internal Revenue Service (IRS) website" + And I should see 1 "external link card" element + And I should see "Were these results helpful?" + Then I select the radio option for the answer "Yes" + And I hard click on "the wizard primary button" + Then I select the radio option for the answer "Yes, I would like to do another search." + And I hard click on "the wizard primary button" + And I wait 1 second + + Scenario: A user query can be submitted and results are returned + Then I hard click on "the wizard primary button" + Then I wait 1 second + Then the "wizard primary button" element should not exist + Then I enter "John Lewis Voting Rights Act" into the wizard query box + Then the "the wizard primary button" element should exist + And I hard click on "the wizard primary button" + And I wait 5 seconds + Then I should see "Okay, you’re looking for:" + And I should see "John Lewis Voting Rights Act" + And I should see "We found the following public information:" + And the "external link card" element should exist + And I should see "If the information above is not what you're looking for, the following agencies may have it." + And I should see "Were these results helpful?" diff --git a/features/step_definitions/mink-gherkin.js b/features/step_definitions/mink-gherkin.js index edeb1265..68ba5d3b 100644 --- a/features/step_definitions/mink-gherkin.js +++ b/features/step_definitions/mink-gherkin.js @@ -10,7 +10,7 @@ const customSteps = [ async callback(value) { const inputSelector = this.mink.getSelector(value); const inputHandle = await this.mink.page.$(inputSelector); - await inputHandle.evaluate(b => b.click()); + await inputHandle.evaluate((b) => b.click()); return inputHandle.dispose(); }, }, @@ -68,7 +68,7 @@ const customSteps = [ const inputSelector = `input[name="${value}"]`; const inputHandle = await this.mink.page.$(inputSelector); await Promise.delay(1 * 1000); - await inputHandle.evaluate(b => b.click()); + await inputHandle.evaluate((b) => b.click()); return inputHandle.dispose(); }, }, @@ -78,7 +78,7 @@ const customSteps = [ const inputSelector = this.mink.getSelector('the first radio option'); const inputHandle = await this.mink.page.$(inputSelector); await Promise.delay(1 * 1000); - await inputHandle.evaluate(b => b.click()); + await inputHandle.evaluate((b) => b.click()); return inputHandle.dispose(); }, }, @@ -88,7 +88,7 @@ const customSteps = [ const inputSelector = this.mink.getSelector('the first radio option'); const inputHandle = await this.mink.page.$(inputSelector); await Promise.delay(1 * 1000); - await inputHandle.evaluate(b => b.click()); + await inputHandle.evaluate((b) => b.click()); return inputHandle.dispose(); }, }, @@ -98,7 +98,7 @@ const customSteps = [ const inputSelector = this.mink.getSelector('the first radio option'); const inputHandle = await this.mink.page.$(inputSelector); await Promise.delay(1 * 1000); - await inputHandle.evaluate(b => b.click()); + await inputHandle.evaluate((b) => b.click()); return inputHandle.dispose(); }, }, diff --git a/features/support/mink.js b/features/support/mink.js index 2c92c5ae..294de2dd 100644 --- a/features/support/mink.js +++ b/features/support/mink.js @@ -8,27 +8,27 @@ const driver = new mink.Mink({ height: 768, }, selectors: { - "homepage search button": ".usa-search-submit-text", - "the homepage search box": "#search-field-big", - "the agency search box": "#agency-search", - "the annual report search box": "#agency-component-search-1", - "the A-to-Z button": "button[aria-controls='a1']", - "the A button": "button[aria-controls='A']", - "the last item in the A section": "#A li:last-child span", - "the start request button": ".start-request", - "the first agency suggestion": ".foia-component-card:first-of-type", - "the first radio option": "input[type='radio']:first-of-type", - "the View Report button": "button[value='view']", - "the data type dropdown": "select[name='data_type']", - "the Select All Agencies button": ".select-all-agencies > a", - "the Hero image credit": "a[href='https://commons.wikimedia.org/wiki/File:Usdepartmentofjustice.jpg']", - "the justice.gov link": "a[href='http://www.justice.gov']", - "the external link script": "script[src='/assets/js/extlink.min.js']", - "the wizard primary button": "button.w-component-button.usa-button.usa-button-primary-alt.usa-button-big", - "the Tax records topic button": "div.w-component-pill-group > ul > li:nth-child(2) > button", - "the wizard query box": "textarea.w-component-form-item__element", - "external link card": "a.foia-component-card.foia-component-card--alt.foia-component-card--ext" - } + 'homepage search button': '.usa-search-submit-text', + 'the homepage search box': '#search-field-big', + 'the agency search box': '#agency-search', + 'the annual report search box': '#agency-component-search-1', + 'the A-to-Z button': "button[aria-controls='a1']", + 'the A button': "button[aria-controls='A']", + 'the last item in the A section': '#A li:last-child span', + 'the start request button': '.start-request', + 'the first agency suggestion': '.foia-component-card:first-of-type', + 'the first radio option': "input[type='radio']:first-of-type", + 'the View Report button': "button[value='view']", + 'the data type dropdown': "select[name='data_type']", + 'the Select All Agencies button': '.select-all-agencies > a', + 'the Hero image credit': "a[href='https://commons.wikimedia.org/wiki/File:Usdepartmentofjustice.jpg']", + 'the justice.gov link': "a[href='http://www.justice.gov']", + 'the external link script': "script[src='/assets/js/extlink.min.js']", + 'the wizard primary button': 'button.w-component-button.usa-button.usa-button-primary-alt.usa-button-big', + 'the Tax records topic button': 'div.w-component-pill-group > ul > li:nth-child(2) > button', + 'the wizard query box': 'textarea.w-component-form-item__element', + 'external link card': 'a.foia-component-card.foia-component-card--alt.foia-component-card--ext', + }, }); // Avoid timeout issues. diff --git a/js/components/wizard_component_body_text.jsx b/js/components/wizard_component_body_text.jsx index 99d7f512..89442fe6 100644 --- a/js/components/wizard_component_body_text.jsx +++ b/js/components/wizard_component_body_text.jsx @@ -4,7 +4,16 @@ import PropTypes from 'prop-types'; /** * @param {import('prop-types').InferProps} props */ -function BodyText({ children }) { +function BodyText({ children, html = '' }) { + if (html) { + return ( +

+ ); + } return (

{children}

); @@ -12,6 +21,7 @@ function BodyText({ children }) { BodyText.propTypes = { children: PropTypes.string, + html: PropTypes.string, }; export default BodyText; diff --git a/js/components/wizard_component_description.jsx b/js/components/wizard_component_description.jsx new file mode 100644 index 00000000..74542a6f --- /dev/null +++ b/js/components/wizard_component_description.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +/** + * @param {import('prop-types').InferProps} props + */ +function Description({ children }) { + return ( +
+ {children} +
+ ); +} + +Description.propTypes = { + children: PropTypes.node, +}; + +export default Description; diff --git a/js/components/wizard_component_feedback_radio.jsx b/js/components/wizard_component_feedback_radio.jsx new file mode 100644 index 00000000..7c2d15b1 --- /dev/null +++ b/js/components/wizard_component_feedback_radio.jsx @@ -0,0 +1,52 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import FormItem from './wizard_component_form_item'; + +function FeedbackRadioSet({ + name, prefix, suffix, options, onChange, +}) { + return ( +
+ {prefix} +
+ {options.map(({ label, value }, index) => { + let ariaLabel; + if (index === 0) { + ariaLabel = `${prefix} ${value}`; + } else if (index === options.length - 1) { + ariaLabel = `${suffix} ${value}`; + } else { + ariaLabel = value; + } + + return ( + + ); + })} +
+ {suffix} +
+ ); +} + +FeedbackRadioSet.propTypes = { + name: PropTypes.string.isRequired, + prefix: PropTypes.string, + suffix: PropTypes.string, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.number, + }), + ).isRequired, + onChange: PropTypes.func.isRequired, +}; +export default FeedbackRadioSet; diff --git a/js/components/wizard_component_form_item.jsx b/js/components/wizard_component_form_item.jsx index 252dd1d3..aff31029 100644 --- a/js/components/wizard_component_form_item.jsx +++ b/js/components/wizard_component_form_item.jsx @@ -1,5 +1,9 @@ -import React from 'react'; import PropTypes from 'prop-types'; +import React, { useState } from 'react'; +import { useWizard } from '../stores/wizard_store'; +import BodyText from './wizard_component_body_text'; +import InfoButton from './wizard_component_info_button'; +import Modal from './wizard_component_modal'; let idCounter = 0; @@ -9,31 +13,35 @@ let idCounter = 0; function FormItem({ type, isLabelHidden, - label, + labelHtml, onChange, name, value, checked, mid, + tooltipMid, placeholder, disabled, maxLength, + ariaLabel, }) { + const { getMessage } = useWizard(); + const [modalIsOpen, setIsOpen] = useState(/** @type boolean */ false); const id = `FormItem${idCounter++}`; let element; switch (type) { case 'text': - element = ; + element = ; break; case 'textarea': - element =