Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ services:

frontend:
container_name: frontend
environment:
- VITE_SHOW_CFPB_HEADER
- VITE_ADMIN_EMAIL
- VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT
- VITE_PII_WARNING_TEXT
build:
context: ./front-end
target: dev
Expand Down
11 changes: 5 additions & 6 deletions front-end/src/config.ts → front-end/.env
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
export const ADMIN_EMAIL = ''
VITE_ADMIN_EMAIL = ''

export const DOWNLOAD_ACKNOWLEDGMENT_TEXT =
VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT =
'I understand that by downloading data from this system, I will be accessing Personally Identifiable Information (PII) and Confidential Information (CI).'

export const DOWNLOAD_ACKNOWLEDGMENT_LABEL =
'I confirm that I am knowingly downloading PII or CI and understand that I am responsible for safeguarding this data.'

export const PII_WARNING_TEXT =
VITE_PII_WARNING_TEXT =
'I understand that by using this system, I will be accessing Personally Identifiable Information (PII) and Confidential Information (CI) and I understand my responsibilities to safeguard all this information.'

VITE_SHOW_CFPB_HEADER = false
Binary file not shown.
34 changes: 32 additions & 2 deletions front-end/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { defineConfig } from 'cypress'
import dotenv from 'dotenv'
import path from 'path'
import { fileURLToPath } from 'url'
import { loadEnv } from 'vite'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// Get any env variable overrides from parent of parent directory
dotenv.config({ path: path.resolve(__dirname, '../../.env') })

export default defineConfig({
// Set to false to disable deprecated Cypress.env()
Expand All @@ -8,9 +18,29 @@ export default defineConfig({

e2e: {
baseUrl: 'http://localhost:3000/',
specPattern: 'cypress/e2e/**/*.ts'
},
specPattern: 'cypress/e2e/**/*.ts',
setupNodeEvents(on, config) {
// Capture the current mode (defaults to 'development' if not set)
const mode = (config.env.mode as string) || 'development'

// Load env variables from the project root based on the mode
const viteEnv = loadEnv(mode, process.cwd())

// Assign the variables to config.env so Cypress can access them
// Check for override values first
config.env.VITE_ADMIN_EMAIL =
process.env.VITE_ADMIN_EMAIL ?? viteEnv.VITE_ADMIN_EMAIL
config.env.VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT =
process.env.VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT ??
viteEnv.VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT
config.env.VITE_PII_WARNING_TEXT =
process.env.VITE_PII_WARNING_TEXT ?? viteEnv.VITE_PII_WARNING_TEXT
config.env.VITE_SHOW_CFPB_HEADER =
process.env.VITE_SHOW_CFPB_HEADER ?? viteEnv.VITE_SHOW_CFPB_HEADER

return config
}
},
component: {
devServer: {
framework: 'react',
Expand Down
95 changes: 95 additions & 0 deletions front-end/cypress/e2e/general.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Metro2Modal } from '@cypress/helpers/modalHelpers'
import { stripHtmlTags } from '@cypress/helpers/utils'
import { PII_COOKIE_NAME } from '@src/constants/settings'
const modal = new Metro2Modal()

describe('General page content', () => {
describe('Warning modal', () => {
it('Should show a warning modal when PII cookie is not set', () => {
cy.intercept('GET', '/api/users/', { fixture: 'user' }).as('getUser')
cy.visit('/')
cy.wait('@getUser')
// On page load, no PII warning cookie is set
cy.getCookie(PII_COOKIE_NAME).should('not.exist')

// Without the cookie, the warning modal is displayed
modal.getModal('warning-modal').should('exist')

// The warning modal shows the value of the warning text env variable
cy.env(['VITE_PII_WARNING_TEXT']).then(({ VITE_PII_WARNING_TEXT }) => {
modal
.getModal('warning-modal')
.should('be.visible')
.within(() => {
cy.findByTestId('warning-text')
.should('exist')
.and('include.text', stripHtmlTags(VITE_PII_WARNING_TEXT as string))
})
})
})

it('Should set a cookie when PII warning is accepted', () => {
cy.intercept('GET', '/api/users/', { fixture: 'user' }).as('getUser')
cy.visit('/')
cy.wait('@getUser')

// On page load, no PII warning cookie is set
cy.getCookie(PII_COOKIE_NAME).should('not.exist')

// The warning modal is displayed
modal
.getModal('warning-modal')
.should('exist')
.within(() => {
// Clicking the accept button adds a cookie
cy.findByTestId('accept-warning-button').click()
cy.getCookie(PII_COOKIE_NAME).should('have.property', 'value', 'true')
})
})

it('Should not show a warning modal when PII cookie is set', () => {
cy.setCookie(PII_COOKIE_NAME, 'true')
cy.intercept('GET', '/api/users/', { fixture: 'user' }).as('getUser')
cy.visit('/')
cy.wait('@getUser')

// with the cookie, the warning modal is not displayed
modal.getModal('warning-modal').should('not.exist')
})
})

describe('Page header', () => {
it('Should show a page header based on env variable', () => {
cy.setCookie(PII_COOKIE_NAME, 'true')
cy.intercept('GET', '/api/users/', { fixture: 'user' }).as('getUser')
cy.visit('/')
cy.wait('@getUser')

cy.env(['VITE_SHOW_CFPB_HEADER']).then(({ VITE_SHOW_CFPB_HEADER }) => {
if (VITE_SHOW_CFPB_HEADER === 'true') {
// CFPB header should show
cy.get('.o-header')
.should('be.visible')
.within(() => {
cy.get('.o-header__logo').should('exist')
cy.get('.nav-items')
.should('have.length', 1)
.first()
.should('have.text', 'User guide')
})
} else {
// Default header should show
cy.get('header[data-testid="metro2-header"]')
.should('be.visible')
.within(() => {
cy.get('h1').should('have.text', 'Metro2 Evaluator Tool')
cy.get('.links')
.should('have.length', 1)
.first()
.should('have.text', 'Need help? See the user guide')
})
}
})
})
})
})
21 changes: 21 additions & 0 deletions front-end/cypress/e2e/general_errors.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,26 @@ describe('Errors', () => {
`The account doesn’t exist.`
)
})

it('Should show a mailto link if admin email env variable is set', () => {
cy.intercept('GET', '/api/events/1/', { fixture: 'event_1' }).as('getEvent')
cy.intercept('GET', '/api/events/1/account/not-an-account/', {
statusCode: 404
}).as('nonexistentAccount')

cy.visit('/events/1/accounts/not-an-account')

cy.wait(['@getEvent', '@nonexistentAccount'])
cy.env(['VITE_ADMIN_EMAIL']).then(({ VITE_ADMIN_EMAIL }) => {
if (VITE_ADMIN_EMAIL) {
cy.findByTestId('contact-link')
.should('exist')
.and('have.attr', 'href')
.and('include', `mailto:${VITE_ADMIN_EMAIL}`)
} else {
cy.findByTestId('contact-link').should('not.exist')
}
})
})
})
})
19 changes: 19 additions & 0 deletions front-end/cypress/e2e/page_Account.cy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getInputByLabel, Metro2Modal } from '@cypress/helpers/modalHelpers'
import { stripHtmlTags } from '@cypress/helpers/utils'
import { PII_COOKIE_NAME } from '@src/constants/settings'
import { accountTableFields } from '@src/pages/Account/utils/accountTableFields'

Expand Down Expand Up @@ -127,6 +128,24 @@ describe('Account data download', () => {
modal.closeModal()
modal.getModal().should('not.be.visible')
})

it('Should show a download acknowledgment message from env variable', () => {
modal.openModal('Save account data')
cy.env(['VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT']).then(
({ VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT }) => {
modal
.getModal()
.should('be.visible')
.within(() => {
cy.findByTestId('download-acknowledgment-text').should(
'include.text',
stripHtmlTags(VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT as string)
)
})
}
)
})

it('Should not allow downloading unless privacy notice is accepted', () => {
modal.openModal('Save account data')
modal.verifyPrivacyCheckboxRequired()
Expand Down
18 changes: 18 additions & 0 deletions front-end/cypress/e2e/page_Evaluator_Downloads.cy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { EvaluatorPage } from '@cypress/helpers/evaluatorPageHelpers'
import { Metro2Modal } from '@cypress/helpers/modalHelpers'
import { stripHtmlTags } from '@cypress/helpers/utils'

// Instantiate helpers
const modal = new Metro2Modal()
Expand Down Expand Up @@ -34,6 +35,23 @@ describe('Evaluator page', () => {
modal.getModal().should('not.be.visible')
})

it('Should show a download acknowledgment message from env variable', () => {
modal.openModal('Save results')
cy.env(['VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT']).then(
({ VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT }) => {
modal
.getModal()
.should('be.visible')
.within(() => {
cy.findByTestId('download-acknowledgment-text').should(
'include.text',
stripHtmlTags(VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT as string)
)
})
}
)
})

it('Should not allow downloading unless privacy notice is accepted', () => {
modal.openModal('Save results')
modal.verifyPrivacyCheckboxRequired()
Expand Down
13 changes: 7 additions & 6 deletions front-end/cypress/helpers/modalHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {
DOWNLOAD_ACKNOWLEDGMENT_LABEL,
DOWNLOAD_ACKNOWLEDGMENT_TEXT
} from '@src/config'
const DOWNLOAD_ACKNOWLEDGMENT_TEXT =
'I understand that by downloading data from this system, I will be accessing Personally Identifiable Information (PII) and Confidential Information (CI).'

const DOWNLOAD_ACKNOWLEDGMENT_LABEL =
'I confirm that I am knowingly downloading PII or CI and understand that I am responsible for safeguarding this data.'

export const getInputByLabel = (label: string) => {
return cy
Expand All @@ -13,8 +14,8 @@ export const getInputByLabel = (label: string) => {
}

export class Metro2Modal {
getModal() {
return cy.get('.modal')
getModal(testid: string | null = null) {
return testid ? cy.get(`[data-testid="${testid}"].modal`) : cy.get('.modal')
}

openModal(buttonText: string): Cypress.Chainable<JQuery> {
Expand Down
5 changes: 5 additions & 0 deletions front-end/cypress/helpers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const stripHtmlTags = (htmlString: string) => {
const div = document.createElement('div')
div.innerHTML = htmlString
return div.textContent
}
1 change: 1 addition & 0 deletions front-end/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"csstype": "^3.2.3",
"cypress": "^15.11.0",
"cypress-real-events": "^1.15.0",
"dotenv": "^17.4.2",
"eslint": "^10.0.2",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
Expand Down
7 changes: 3 additions & 4 deletions front-end/src/components/Error/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ButtonGroup, Link as CFPBLink } from '@cfpb/design-system-react'
import { ADMIN_EMAIL } from '@src/config'
import { Link } from '@src/utils/DSRLink'
import { useRouterState } from '@tanstack/react-router'
import type { ReactElement } from 'react'
Expand All @@ -18,6 +17,7 @@ export default function ErrorMessage({
}: ErrorMessageProperties): ReactElement {
const router = useRouterState()
const currentPath = router.location.pathname
const adminEmail = import.meta.env.VITE_ADMIN_EMAIL

return (
<div className='error-container content-row' data-testid='error-container'>
Expand All @@ -30,12 +30,11 @@ export default function ErrorMessage({
<Link asButton to='/' data-testid='back-button'>
Back to Metro 2 home page
</Link>
{ADMIN_EMAIL ? (
{adminEmail ? (
<CFPBLink
asButton
iconRight='email'
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
href={`mailto:${ADMIN_EMAIL ?? ''}?subject=${type}%20Error%20at%20%22${currentPath}%22`}
href={`mailto:${adminEmail ?? ''}?subject=${type}%20Error%20at%20%22${currentPath}%22`}
data-testid='contact-link'>
Contact an administrator
</CFPBLink>
Expand Down
16 changes: 8 additions & 8 deletions front-end/src/components/Modal/DownloadModal.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { Button, ButtonGroup, Checkbox } from '@cfpb/design-system-react'
import {
DOWNLOAD_ACKNOWLEDGMENT_LABEL,
DOWNLOAD_ACKNOWLEDGMENT_TEXT
} from '@src/config'
import DOMPurify from 'dompurify'
import type { ReactElement } from 'react'
import { useState } from 'react'
Expand All @@ -21,7 +17,9 @@ interface DownloadModalProperties {
buttonText?: string
}

const downloadText = DOMPurify.sanitize(DOWNLOAD_ACKNOWLEDGMENT_TEXT)
const downloadText = DOMPurify.sanitize(
import.meta.env.VITE_DOWNLOAD_ACKNOWLEDGMENT_TEXT
)

/**
* DownloadModal()
Expand Down Expand Up @@ -84,16 +82,18 @@ export default function DownloadModal({
{requirePrivacyAcknowledgment && (
<fieldset
className='o-form__fieldset block block--sub'
data-test-id='download-acknowledgment'>
data-testid='download-acknowledgment'>
<legend className='h4'>{privacyHeader}</legend>
<p dangerouslySetInnerHTML={{ __html: downloadText }}></p>
<p
dangerouslySetInnerHTML={{ __html: downloadText }}
data-testid='download-acknowledgment-text'></p>
<div className='u-mt15'>
<Checkbox
id='confirmPII'
isLarge
checked={privacyMessageAcknowledged}
data-testid='pii-checkbox'
label={DOWNLOAD_ACKNOWLEDGMENT_LABEL}
label='I confirm that I am knowingly downloading PII or CI and understand that I am responsible for safeguarding this data.'
onChange={onChange}
/>
</div>
Expand Down
7 changes: 4 additions & 3 deletions front-end/src/components/Modal/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ export function Modal({
children,
interactionRequired = false,
open,
onClose
}: ModalProperties & React.HTMLAttributes<HTMLDivElement>): ReactElement | null {
onClose,
...rest
}: ModalProperties & React.HTMLAttributes<HTMLDialogElement>): ReactElement | null {
const dialogRef = useRef<HTMLDialogElement>(null)
const openModal = (): void => {
dialogRef.current?.showModal()
Expand Down Expand Up @@ -51,7 +52,7 @@ export function Modal({
})

return (
<dialog className='modal' ref={dialogRef}>
<dialog className='modal' ref={dialogRef} {...rest}>
<div className='modal-wrapper'>
{interactionRequired ? (
''
Expand Down
Loading
Loading