diff --git a/Gemfile b/Gemfile index f999b614c..b2fe72d0f 100644 --- a/Gemfile +++ b/Gemfile @@ -9,7 +9,7 @@ ruby RUBY_VERSION # # This will help ensure the proper Jekyll version is running. # Happy Jekylling! -gem "jekyll", "3.9.5" +gem "jekyll", "3.10.0" gem "html-proofer", "3.19.4" gem "kramdown-parser-gfm", "1.1.0" diff --git a/Gemfile.lock b/Gemfile.lock index 993e6436f..90483b264 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,17 +1,19 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) colorator (1.1.0) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.3) + csv (3.3.0) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) ethon (0.15.0) ffi (>= 1.15.0) eventmachine (1.2.7) - ffi (1.16.3) + ffi (1.17.0) + ffi (1.17.0-x86_64-linux-gnu) forwardable-extended (2.6.0) html-proofer (3.19.4) addressable (~> 2.3) @@ -22,11 +24,12 @@ GEM typhoeus (~> 1.3) yell (~> 2.0) http_parser.rb (0.8.0) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) - jekyll (3.9.5) + jekyll (3.10.0) addressable (~> 2.4) colorator (~> 1.0) + csv (~> 3.0) em-websocket (~> 0.5) i18n (>= 0.7, < 2) jekyll-sass-converter (~> 1.0) @@ -37,6 +40,7 @@ GEM pathutil (~> 0.9) rouge (>= 1.7, < 4) safe_yaml (~> 1.0) + webrick (>= 1.0) jekyll-redirect-from (0.16.0) jekyll (>= 3.3, < 5.0) jekyll-sass-converter (1.5.2) @@ -48,26 +52,27 @@ GEM kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.4) - listen (3.8.0) + listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.8.1) - nokogiri (1.14.3) - mini_portile2 (~> 2.8.0) + mini_portile2 (2.8.6) + nokogiri (1.16.5) + mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.14.3-x86_64-linux) + nokogiri (1.16.5-x86_64-linux) racc (~> 1.4) parallel (1.22.1) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (5.0.4) - racc (1.6.2) + public_suffix (6.0.0) + racc (1.7.3) rainbow (3.1.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.2.6) + rexml (3.3.1) + strscan rouge (3.30.0) safe_yaml (1.0.5) sass (3.7.4) @@ -75,6 +80,7 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + strscan (3.1.0) typhoeus (1.4.0) ethon (>= 0.9.0) webrick (1.7.0) @@ -86,7 +92,7 @@ PLATFORMS DEPENDENCIES html-proofer (= 3.19.4) - jekyll (= 3.9.5) + jekyll (= 3.10.0) jekyll-redirect-from kramdown-parser-gfm (= 1.1.0) webrick (~> 1.7) diff --git a/bin/xml-datasets/xml-dataset-download.sh b/bin/xml-datasets/xml-dataset-download.sh index 7353c4bb4..6b1fa4e91 100644 --- a/bin/xml-datasets/xml-dataset-download.sh +++ b/bin/xml-datasets/xml-dataset-download.sh @@ -2,10 +2,11 @@ start=$SECONDS set -B -# Generate xml dataset by year +# Generates xml dataset by year for Dataset Download Page +# https://www.foia.gov/foia-dataset-download.html # USAGE: bash xml-dataset-download.sh -# EXAMPLE: bash xml-dataset-download.sh 2000 N4aCuDuJO8Ucf1FTR2EzVPZqo8NsSl1c7YLYOk8N -# CURL: curl -H "X-API-Key: N4aCuDuJO8Ucf1FTR2EzVPZqo8NsSl1c7YLYOk8N" https://api.foia.gov/api/annual-report-xml/ibwc/2022 -o test.xml +# EXAMPLE: bash xml-dataset-download.sh 2023 N4aCuDuJO8Ucf1FTR2EzVPZqo8NsSl1c7YLYOk8N +# CURL: curl -H "X-API-Key: N4aCuDuJO8Ucf1FTR2EzVPZqo8NsSl1c7YLYOk8N" https://api.foia.gov/api/annual-report-xml/ibwc/2023 -o test.xml YEAR=$1 API_KEY=$2 @@ -44,6 +45,9 @@ case $YEAR in 2022) declare -a agencies=(abmc doj achp acus afrh asc ceq cfa cfpb cftc cia cigie cncs co cppbsd cpsc csb csosa dc dfc dhs dnfsb doc dod doe doi dol dos dot eac ed eeoc epa fca fcc fcsic fdic fec ferc ffiec fhfa flra fmc fmcs fmshrc fomc frb frtib ftc gcerc gsa ha hhs hstsf hud iaf imls ipec fpisc jmmff lsc mcc mkuf mmc mspb nara nasa ncd ncpc ncua nea neh nigc nlrb nmb nrc nrpc nsf ntsb nw nwtrb odni oge omb ondcp onhir opm osc oshrc ostp pbgc pc pclob prc pt usrrb sba sec sigar ssa ssab sss stb treasury tva usab usadf usagm usaid usccr usda usibwc usich usip usitc usps ustda ustr va) ;; + 2023) + declare -a agencies=(oncd abmc doj achp acus afrh asc ceq cfa cfpb cftc cia cigie cncs co cppbsd cpsc csb csosa dc dfc dhs dnfsb doc dod doe doi dol dos dot eac ed eeoc epa fca fcc fcsic fdic fec ferc ffiec fhfa flra fmc fmcs fmshrc fomc frb frtib ftc gcerc gsa ha hhs hstsf hud iaf imls ipec fpisc jmmff lsc mcc mkuf mmc mspb nara nasa ncd ncpc ncua nea neh nigc nlrb nmb nrc nrpc nsf ntsb nw nwtrb odni oge omb ondcp onhir opm osc oshrc ostp pbgc pc pclob prc pt usrrb sba sec sigar ssa ssab sss stb treasury tva usab usadf usagm usaid usccr usda usibwc usich usip usitc usps ustda ustr va) + ;; *) echo -n "Year unavailable: ${YEAR}" exit; @@ -93,7 +97,7 @@ done echo -e "Zipping all XML files for the year ${YEAR}... \n" zip -r -j zips/$YEAR-FOIASetFull.zip files/$YEAR/*.xml -echo -e "Removing all XML files from this directory... \n" +#echo -e "Removing all XML files from this directory... \n" #rm -r files/$YEAR/*.xml # leave files but use gitignore for testing duration=$(( SECONDS - start )) echo -e "You can now manually place the zip files into the www.foia.gov directory..." diff --git a/features/AgencySearch.feature b/features/AgencySearch.feature new file mode 100644 index 000000000..2fbdf8ba0 --- /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 000000000..db6454f52 --- /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 000000000..ab1cad291 --- /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 edeb1265f..68ba5d3bb 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 2c92c5ae8..294de2dd2 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 99d7f512e..89442fe61 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 000000000..74542a6f5 --- /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 000000000..7c2d15b17 --- /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 252dd1d33..aff310291 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 =