Merge pull request #426 from edx/bseverino/fix-reroute

[MST-717] Reroute user to summary panel in A/B test
This commit is contained in:
Bianca Severino
2021-04-07 11:12:03 -04:00
committed by GitHub
10 changed files with 168 additions and 38 deletions

View File

@@ -79,6 +79,10 @@ export default function IdVerificationContextProvider({ children }) {
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
const [shouldUseCamera, setShouldUseCamera] = useState(false);
// If the user reaches the end of the flow and goes back to retake their photos,
// this flag ensures that they are directed straight back to the summary panel
const [reachedSummary, setReachedSummary] = useState(false);
const contextValue = {
existingIdVerification,
facePhotoFile,
@@ -91,12 +95,14 @@ export default function IdVerificationContextProvider({ children }) {
profileDataManager,
optimizelyExperimentName,
shouldUseCamera,
reachedSummary,
setExistingIdVerification,
setFacePhotoFile,
setIdPhotoFile,
setIdPhotoName,
setOptimizelyExperimentName,
setShouldUseCamera,
setReachedSummary,
tryGetUserMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });

View File

@@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import { getConfig, history } from '@edx/frontend-platform';
import {
Alert, Hyperlink, Input, Button, Spinner,
@@ -27,11 +27,14 @@ function SummaryPanel(props) {
idPhotoName,
stopUserMedia,
optimizelyExperimentName,
setReachedSummary,
} = useContext(IdVerificationContext);
const nameToBeUsed = idPhotoName || nameOnAccount || '';
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionError, setSubmissionError] = useState(null);
useEffect(() => setReachedSummary(true), []);
function renderManagedProfileMessage() {
if (!profileDataManager) {
return null;

View File

@@ -2,56 +2,79 @@ import { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import IdVerificationContext, { MEDIA_ACCESS } from './IdVerificationContext';
const SLUGS = {
REVIEW_REQUIREMENTS: 'review-requirements',
CHOOSE_MODE: 'choose-mode',
REQUEST_CAMERA_ACCESS: 'request-camera-access',
PORTRAIT_PHOTO_CONTEXT: 'portrait-photo-context',
TAKE_PORTRAIT_PHOTO: 'take-portrait-photo',
ID_CONTEXT: 'id-context',
TAKE_ID_PHOTO: 'take-id-photo',
GET_NAME_ID: 'get-name-id',
SUMMARY: 'summary',
SUBMITTED: 'submitted',
};
const panelSteps = [
'review-requirements',
'choose-mode',
'request-camera-access',
'portrait-photo-context',
'take-portrait-photo',
'id-context',
'take-id-photo',
'get-name-id',
'summary',
'submitted',
SLUGS.REVIEW_REQUIREMENTS,
SLUGS.CHOOSE_MODE,
SLUGS.REQUEST_CAMERA_ACCESS,
SLUGS.PORTRAIT_PHOTO_CONTEXT,
SLUGS.TAKE_PORTRAIT_PHOTO,
SLUGS.ID_CONTEXT,
SLUGS.TAKE_ID_PHOTO,
SLUGS.GET_NAME_ID,
SLUGS.SUMMARY,
SLUGS.SUBMITTED,
];
// eslint-disable-next-line import/prefer-default-export
export const useNextPanelSlug = (originSlug) => {
// Go back to the summary view if that's where they came from
const location = useLocation();
const isFromSummary = location.state && location.state.fromSummary;
const isFromPortrait = location.state && location.state.fromPortraitCapture;
const isFromId = location.state && location.state.fromIdCapture;
const { shouldUseCamera, mediaAccess, optimizelyExperimentName } = useContext(IdVerificationContext);
const {
mediaAccess,
optimizelyExperimentName,
reachedSummary,
shouldUseCamera,
} = useContext(IdVerificationContext);
if (isFromSummary) {
return 'summary';
const canRerouteToSummary = [
SLUGS.TAKE_PORTRAIT_PHOTO,
SLUGS.TAKE_ID_PHOTO,
SLUGS.GET_NAME_ID,
];
if (reachedSummary && canRerouteToSummary.includes(originSlug)) {
return SLUGS.SUMMARY;
}
// the following are used as part of an A/B experiment
if (isFromPortrait) {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return 'portrait-photo-context';
return SLUGS.PORTRAIT_PHOTO_CONTEXT;
}
return 'take-portrait-photo';
return SLUGS.TAKE_PORTRAIT_PHOTO;
}
if (isFromId) {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return 'id-context';
return SLUGS.ID_CONTEXT;
}
return 'take-id-photo';
return SLUGS.TAKE_ID_PHOTO;
}
if (originSlug === 'review-requirements' && !optimizelyExperimentName) {
return 'request-camera-access';
if (originSlug === SLUGS.REVIEW_REQUIREMENTS && !optimizelyExperimentName) {
return SLUGS.REQUEST_CAMERA_ACCESS;
}
if (originSlug === 'choose-mode' && !shouldUseCamera) {
return 'take-portrait-photo';
if (originSlug === SLUGS.CHOOSE_MODE && !shouldUseCamera) {
return SLUGS.TAKE_PORTRAIT_PHOTO;
}
if (originSlug === 'take-portrait-photo' && !shouldUseCamera) {
return 'take-id-photo';
if (originSlug === SLUGS.TAKE_PORTRAIT_PHOTO && !shouldUseCamera) {
return SLUGS.TAKE_ID_PHOTO;
}
if (originSlug === 'request-camera-access' && mediaAccess !== MEDIA_ACCESS.GRANTED) {
return 'take-portrait-photo';
if (originSlug === SLUGS.REQUEST_CAMERA_ACCESS && mediaAccess !== MEDIA_ACCESS.GRANTED) {
return SLUGS.TAKE_PORTRAIT_PHOTO;
}
const nextIndex = panelSteps.indexOf(originSlug) + 1;
@@ -63,16 +86,16 @@ export const useNextPanelSlug = (originSlug) => {
export const useVerificationRedirectSlug = (slug) => {
const { facePhotoFile, idPhotoFile, optimizelyExperimentName } = useContext(IdVerificationContext);
const indexOfCurrentPanel = panelSteps.indexOf(slug);
if (!optimizelyExperimentName && slug === 'choose-mode') {
return 'review-requirements';
if (!optimizelyExperimentName && slug === SLUGS.CHOOSE_MODE) {
return SLUGS.REVIEW_REQUIREMENTS;
}
if (!facePhotoFile) {
if (indexOfCurrentPanel > panelSteps.indexOf('take-portrait-photo')) {
return 'portrait-photo-context';
if (indexOfCurrentPanel > panelSteps.indexOf(SLUGS.TAKE_PORTRAIT_PHOTO)) {
return SLUGS.PORTRAIT_PHOTO_CONTEXT;
}
} else if (!idPhotoFile) {
if (indexOfCurrentPanel > panelSteps.indexOf('take-id-photo')) {
return 'id-context';
if (indexOfCurrentPanel > panelSteps.indexOf(SLUGS.TAKE_ID_PHOTO)) {
return SLUGS.ID_CONTEXT;
}
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import {
render, cleanup, act, screen,
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
@@ -25,6 +25,7 @@ describe('ChooseModePanel', () => {
const contextValue = {
optimizelyExperimentName: 'test',
shouldUseCamera: false,
reachedSummary: false,
};
afterEach(() => {
@@ -73,6 +74,23 @@ describe('ChooseModePanel', () => {
expect(nextButton.getAttribute('href')).toEqual('/request-camera-access');
});
it('reroutes correctly if reachedSummary is true', async () => {
contextValue.shouldUseCamera = true;
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlChooseModePanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const nextButton = await screen.findByTestId('next-button');
fireEvent.click(nextButton);
expect(history.location.pathname).toEqual('/request-camera-access');
});
it('redirects if user is not part of experiment', async () => {
contextValue.optimizelyExperimentName = '';

View File

@@ -26,13 +26,30 @@ describe('IdContextPanel', () => {
const contextValue = {
optimizelyExperimentName: '',
facePhotoFile: 'test.jpg',
reachedSummary: false,
};
afterEach(() => {
cleanup();
});
it('routes to TakeIdPhotoPanel', async () => {
it('routes to TakeIdPhotoPanel normally', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
});
it('routes to TakeIdPhotoPanel if reachedSummary is true', async () => {
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">

View File

@@ -25,13 +25,30 @@ describe('PortraitPhotoContextPanel', () => {
const contextValue = {
optimizelyExperimentName: '',
reachedSummary: false,
};
afterEach(() => {
cleanup();
});
it('routes to TakePortraitPhotoPanel', async () => {
it('routes to TakePortraitPhotoPanel normally', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlPortraitPhotoContextPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-portrait-photo');
});
it('routes to TakePortraitPhotoPanel if reachedSummary is true', async () => {
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">

View File

@@ -26,6 +26,7 @@ describe('RequestCameraAccessPanel', () => {
};
const contextValue = {
reachedSummary: false,
tryGetUserMedia: jest.fn(),
};
@@ -241,6 +242,46 @@ describe('RequestCameraAccessPanel', () => {
expect(history.location.pathname).toEqual('/id-context');
});
it('reroutes to portrait context when reachedSummary is true', async () => {
contextValue.mediaAccess = 'granted';
contextValue.reachedSummary = true;
history.location.state = { fromPortraitCapture: true };
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/portrait-photo-context');
});
it('reroutes to ID context when reachedSummary is true', async () => {
contextValue.mediaAccess = 'granted';
contextValue.reachedSummary = true;
history.location.state = { fromIdCapture: true };
Bowser.parse = jest.fn().mockReturnValue({ browser: { name: '' } });
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlRequestCameraAccessPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/id-context');
});
it('reroutes correctly to portrait context with no media access', async () => {
contextValue.mediaAccess = 'denied';
contextValue.optimizelyExperimentName = 'test';

View File

@@ -34,6 +34,7 @@ describe('SummaryPanel', () => {
idPhotoName: 'test name',
optimizelyExperimentName: 'test-experiment',
stopUserMedia: jest.fn(),
setReachedSummary: jest.fn(),
};
const getPanel = async () => {

View File

@@ -27,6 +27,7 @@ describe('TakeIdPhotoPanel', () => {
const contextValue = {
facePhotoFile: 'test.jpg',
idPhotoFile: null,
reachedSummary: false,
setIdPhotoFile: jest.fn(),
};
@@ -67,7 +68,7 @@ describe('TakeIdPhotoPanel', () => {
it('routes back to SummaryPanel if that was the source', async () => {
contextValue.idPhotoFile = 'test.jpg';
history.location.state = { fromSummary: true };
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">

View File

@@ -26,6 +26,8 @@ describe('TakePortraitPhotoPanel', () => {
const contextValue = {
facePhotoFile: null,
idPhotoFile: null,
reachedSummary: false,
setFacePhotoFile: jest.fn(),
setShouldUseCamera: jest.fn(),
};
@@ -68,7 +70,8 @@ describe('TakePortraitPhotoPanel', () => {
it('routes back to SummaryPanel if that was the source', async () => {
contextValue.facePhotoFile = 'test.jpg';
history.location.state = { fromSummary: true };
contextValue.idPhotoFile = 'test.jpg';
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">