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 app/javascript/packages/form-steps/form-steps.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/dom';
import sinon from 'sinon';
import { PageHeading } from '@18f/identity-components';
import * as analytics from '@18f/identity-analytics';
import FormSteps, { FormStepComponentProps, getStepIndexByName } from './form-steps';
import FormError from './form-error';
import FormStepsContext from './form-steps-context';
Expand All @@ -20,6 +21,10 @@ interface StepValues {
describe('FormSteps', () => {
const sandbox = sinon.createSandbox();

beforeEach(() => {
sandbox.spy(analytics, 'trackEvent');
});

afterEach(() => {
sandbox.restore();
});
Expand Down
23 changes: 15 additions & 8 deletions app/javascript/packages/form-steps/form-steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ interface FormStepsProps {
/**
* Callback triggered on step change.
*/
onStepChange?: () => void;
onStepChange?: (stepName: string) => void;

/**
* Callback triggered on step submit.
*/
onStepSubmit?: (stepName: string) => void;

/**
* Whether to prompt the user about unsaved changes when navigating away from an in-progress form.
Expand All @@ -161,9 +166,9 @@ interface FormStepsProps {
* @param step Current step.
* @param titleFormat Format string for page title.
*/
function useStepTitle(step: FormStep, titleFormat?: string) {
function useStepTitle(step?: FormStep, titleFormat?: string) {
useEffect(() => {
if (titleFormat && step.title) {
if (titleFormat && step?.title) {
document.title = replaceVariables(titleFormat, { step: step.title });
}
}, [step]);
Expand Down Expand Up @@ -203,6 +208,7 @@ function FormSteps({
steps = [],
onComplete = () => {},
onStepChange = () => {},
onStepSubmit = () => {},
initialValues = {},
initialActiveErrors = [],
autoFocus,
Expand Down Expand Up @@ -232,7 +238,7 @@ function FormSteps({
}, [activeErrors]);

const stepIndex = Math.max(getStepIndexByName(steps, stepName), 0);
const step = steps[stepIndex];
const step = steps[stepIndex] as FormStep | undefined;

/**
* After a change in content, maintain focus by resetting to the beginning of the new content.
Expand All @@ -248,6 +254,10 @@ function FormSteps({
setStepName(stepName);
}

useStepTitle(step, titleFormat);
useDidUpdateEffect(() => onStepChange(stepName!), [step]);
useDidUpdateEffect(onPageTransition, [step]);

useEffect(() => {
// Treat explicit initial step the same as step transition, placing focus to header.
if (autoFocus) {
Expand All @@ -261,10 +271,6 @@ function FormSteps({
}
}, [stepErrors]);

useStepTitle(step, titleFormat);
useDidUpdateEffect(onStepChange, [step]);
useDidUpdateEffect(onPageTransition, [step]);

/**
* Returns array of form errors for the current set of values.
*/
Expand Down Expand Up @@ -322,6 +328,7 @@ function FormSteps({
return;
}

onStepSubmit(step?.name);
const nextStepIndex = stepIndex + 1;
const isComplete = nextStepIndex === steps.length;
if (isComplete) {
Expand Down
34 changes: 27 additions & 7 deletions app/javascript/packages/verify-flow/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import sinon from 'sinon';
import { render } from '@testing-library/react';
import * as analytics from '@18f/identity-analytics';
import userEvent from '@testing-library/user-event';
import { VerifyFlow } from './index';

describe('VerifyFlow', () => {
const sandbox = sinon.createSandbox();
const personalKey = '0000-0000-0000-0000';

beforeEach(() => {
sandbox.spy(analytics, 'trackEvent');
});

afterEach(() => {
sandbox.restore();
});

it('advances through flow to completion', async () => {
const personalKey = '0000-0000-0000-0000';
const onComplete = sinon.spy();

const { getByText, getByLabelText } = render(
<VerifyFlow
appName="Example App"
initialValues={{ personalKey }}
basePath="/"
onComplete={onComplete}
/>,
<VerifyFlow appName="Example App" initialValues={{ personalKey }} onComplete={onComplete} />,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason removing the basePath was the only thing I could figure out to do to finally get this test to pass (it would get stuck on the personal_key step and fail to get the continue button element, so it was unable proceed to the next step) . Of course once I removed basePath I got a typecheck error so I had to update the type definition as well.

);

await userEvent.click(getByText('forms.buttons.continue'));
Expand All @@ -23,4 +29,18 @@ describe('VerifyFlow', () => {

expect(onComplete).to.have.been.called();
});

it('calls trackEvents for personal key steps', async () => {
const { getByLabelText, getByText, getAllByText } = render(
<VerifyFlow appName="Example App" initialValues={{ personalKey }} onComplete={() => {}} />,
);
expect(analytics.trackEvent).to.have.been.calledWith('IdV: personal key visited');

await userEvent.click(getByText('forms.buttons.continue'));
expect(analytics.trackEvent).to.have.been.calledWith('IdV: show personal key modal');
await userEvent.type(getByLabelText('forms.personal_key.confirmation_label'), personalKey);
await userEvent.click(getAllByText('forms.buttons.submit.default')[1]);

expect(analytics.trackEvent).to.have.been.calledWith('IdV: personal key submitted');
});
});
25 changes: 24 additions & 1 deletion app/javascript/packages/verify-flow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { useEffect } from 'react';
import { FormSteps } from '@18f/identity-form-steps';
import { StepIndicator, StepIndicatorStep, StepStatus } from '@18f/identity-step-indicator';
import { t } from '@18f/identity-i18n';
import { Alert } from '@18f/identity-components';
import { trackEvent } from '@18f/identity-analytics';
import { STEPS } from './steps';

export interface VerifyFlowValues {
Expand All @@ -19,7 +21,7 @@ interface VerifyFlowProps {
/**
* The path to which the current step is appended to create the current step URL.
*/
basePath: string;
basePath?: string;

/**
* Application name, used in generating page titles for current step.
Expand All @@ -33,6 +35,19 @@ interface VerifyFlowProps {
}

export function VerifyFlow({ initialValues = {}, basePath, appName, onComplete }: VerifyFlowProps) {
function trackVisitedStepEvent(stepName) {
if (stepName === 'personal_key') {
trackEvent('IdV: personal key visited');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do it here or in a separate pull request, but I think for LG-6201 to be complete, we need to figure out how to avoid prefixing for these events, since otherwise we don't have strict parity with how it was working previously.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets do it in another pull request

}
if (stepName === 'personal_key_confirm') {
trackEvent('IdV: show personal key modal');
}
}

useEffect(() => {
trackVisitedStepEvent(STEPS[0].name);
}, []);

return (
<>
<StepIndicator className="margin-x-neg-2 margin-top-neg-4 tablet:margin-x-neg-6 tablet:margin-top-neg-4">
Expand All @@ -51,6 +66,14 @@ export function VerifyFlow({ initialValues = {}, basePath, appName, onComplete }
promptOnNavigate={false}
basePath={basePath}
titleFormat={`%{step} - ${appName}`}
onStepSubmit={(submittedStepName) => {
if (submittedStepName === 'personal_key_confirm') {
trackEvent('IdV: personal key submitted');
}
}}
onStepChange={(stepName) => {
trackVisitedStepEvent(stepName);
}}
onComplete={onComplete}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import sinon from 'sinon';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FormSteps } from '@18f/identity-form-steps';
import * as analytics from '@18f/identity-analytics';
import PersonalKeyConfirmStep from './personal-key-confirm-step';

describe('PersonalKeyConfirmStep', () => {
Expand All @@ -14,6 +15,16 @@ describe('PersonalKeyConfirmStep', () => {
registerField: () => () => {},
};

const sandbox = sinon.createSandbox();

beforeEach(() => {
sandbox.spy(analytics, 'trackEvent');
});

afterEach(() => {
sandbox.restore();
});

it('allows the user to return to the previous step by clicking "Back" button', async () => {
const toPreviousStep = sinon.spy();
const { getByText } = render(
Expand All @@ -36,6 +47,17 @@ describe('PersonalKeyConfirmStep', () => {
expect(toPreviousStep).to.have.been.called();
});

it('calls trackEvent when user dismisses modal by pressing "Back" button', async () => {
const toPreviousStep = sinon.spy();

const { getByText } = render(
<PersonalKeyConfirmStep {...DEFAULT_PROPS} toPreviousStep={toPreviousStep} />,
);

await userEvent.click(getByText('forms.buttons.back'));
expect(analytics.trackEvent).to.have.been.calledWith('IdV: hide personal key modal');
});

it('allows the user to continue only with a correct value', async () => {
const onComplete = sinon.spy();
const { getByLabelText, getAllByText, container } = render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { t } from '@18f/identity-i18n';
import type { FormStepComponentProps } from '@18f/identity-form-steps';
import { Modal } from '@18f/identity-modal';
import { getAssetPath } from '@18f/identity-assets';
import { trackEvent } from '@18f/identity-analytics';
import PersonalKeyStep from '../personal-key/personal-key-step';
import PersonalKeyInput from './personal-key-input';
import type { VerifyFlowValues } from '../..';
Expand All @@ -14,12 +15,17 @@ function PersonalKeyConfirmStep(stepProps: PersonalKeyConfirmStepProps) {
const { registerField, value, onChange, toPreviousStep } = stepProps;
const personalKey = value.personalKey!;

const closeModalActions = () => {
trackEvent('IdV: hide personal key modal');
toPreviousStep();
};

return (
<>
<FormStepsContext.Provider value={{ isLastStep: false, onPageTransition() {} }}>
<PersonalKeyStep {...stepProps} />
</FormStepsContext.Provider>
<Modal onRequestClose={toPreviousStep}>
<Modal onRequestClose={closeModalActions}>
<div className="pin-top pin-x display-flex flex-column flex-align-center top-neg-3">
<img alt="" height="60" width="60" src={getAssetPath('p-key.svg')} />
</div>
Expand All @@ -40,7 +46,7 @@ function PersonalKeyConfirmStep(stepProps: PersonalKeyConfirmStepProps) {
<FormStepsContinueButton className="margin-y-0" />
</div>
<div className="grid-col-12 tablet:grid-col-6">
<Button isBig isWide isOutline onClick={toPreviousStep}>
<Button isBig isWide isOutline onClick={closeModalActions}>
{t('forms.buttons.back')}
</Button>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import sinon from 'sinon';
import * as analytics from '@18f/identity-analytics';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PersonalKeyStep from './personal-key-step';

describe('PersonalKeyStep', () => {
const sandbox = sinon.createSandbox();
const DEFAULT_PROPS = {
onChange() {},
onError() {},
errors: [],
toPreviousStep() {},
registerField: () => () => {},
unknownFieldErrors: [],
value: { personalKey: '' },
};

beforeEach(() => {
sandbox.spy(analytics, 'trackEvent');
});

afterEach(() => {
sandbox.restore();
});

it('calls trackEvent when user clicks on "Download" button', async () => {
const { getByText } = render(<PersonalKeyStep {...DEFAULT_PROPS} />);

const button = getByText('forms.backup_code.download');
button.addEventListener('click', (event) => event.preventDefault());
await userEvent.click(button);
expect(analytics.trackEvent).to.have.been.calledWith('IdV: download personal key');
});

it('calls trackEvent when user clicks on "Clipboard" button', async () => {
const { getByText } = render(<PersonalKeyStep {...DEFAULT_PROPS} />);

await userEvent.click(getByText('components.clipboard_button.label'));
expect(analytics.trackEvent).to.have.been.calledWith('IdV: copy personal key');
});

it('calls trackEvent when user clicks on "Print" button', async () => {
window.print = () => {};

const { getByText } = render(<PersonalKeyStep {...DEFAULT_PROPS} />);

await userEvent.click(getByText('components.print_button.label'));
expect(analytics.trackEvent).to.have.been.calledWith('IdV: print personal key');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { formatHTML } from '@18f/identity-react-i18n';
import { FormStepsContinueButton } from '@18f/identity-form-steps';
import type { FormStepComponentProps } from '@18f/identity-form-steps';
import { getAssetPath } from '@18f/identity-assets';
import { trackEvent } from '@18f/identity-analytics';
import type { VerifyFlowValues } from '../..';
import DownloadButton from './download-button';

Expand Down Expand Up @@ -45,15 +46,21 @@ function PersonalKeyStep({ value }: PersonalKeyStepProps) {
<DownloadButton
content={personalKey}
fileName="personal_key.txt"
onClick={() => trackEvent('IdV: download personal key')}
isOutline
className="margin-right-2 margin-bottom-2 tablet:margin-bottom-0"
>
{t('forms.backup_code.download')}
</DownloadButton>
<PrintButton isOutline className="margin-right-2 margin-bottom-2 tablet:margin-bottom-0" />
<PrintButton
isOutline
onClick={() => trackEvent('IdV: print personal key')}
className="margin-right-2 margin-bottom-2 tablet:margin-bottom-0"
/>
<ClipboardButton
clipboardText={personalKey}
isOutline
onClick={() => trackEvent('IdV: copy personal key')}
className="margin-bottom-2 tablet:margin-bottom-0"
/>
<div className="margin-y-5 clearfix">
Expand Down