diff --git a/components/TransLine.tsx b/components/TransLine.tsx index 491b000b..1b9896d2 100644 --- a/components/TransLine.tsx +++ b/components/TransLine.tsx @@ -1,8 +1,9 @@ import { Trans } from 'react-i18next' import React from 'react' +import { I18nString } from '../types/common' export interface TransLineProps { - i18nKey: string // Expects an i18n key + i18nKey: I18nString links?: string[] | null } diff --git a/public/locales/en/claim-status.json b/public/locales/en/claim-status.json index 9e59fd25..59425983 100644 --- a/public/locales/en/claim-status.json +++ b/public/locales/en/claim-status.json @@ -1,11 +1,25 @@ { - "pending-determination": { - "description": "We need to confirm your eligibility for benefits." + "scenarios": { + "scenario1": { + "description": "We need to confirm your eligibility for benefits." + }, + "scenario7": { + "description": "There are no pending issues at this time." + }, + "scenario8": { + "description": "Your claim is active and there are no pending issues at this time." + }, + "scenario9": { + "description": "Your payments are pending further review.", + "next-step": "Check your UI Online inbox for the latest updates." + }, + "scenario10": { + "description": "[this copy may or may not be unique] Your payments are pending further review." + } }, - "base-pending": { - "description": "Your payments are pending further review." - }, - "base-no-pending": { - "description": "There are no pending issues at this time." + "conditional-next-steps": { + "certify-no-pending": "<0>Keep certifying as long as you need benefits.", + "certify-pending": "<0>Keep certifying as long as you need benefits, so your payments are not delayed further.", + "contact-info": "Make sure we have your current phone number and email address." } } diff --git a/public/locales/es/claim-status.json b/public/locales/es/claim-status.json index 70a25f0d..583d0b39 100644 --- a/public/locales/es/claim-status.json +++ b/public/locales/es/claim-status.json @@ -1,11 +1,23 @@ { - "pending-determination": { - "description": "Necesitamos confirmar su elegibilidad para los beneficios." + "scenarios": { + "scenario1": { + "description": "Necesitamos confirmar su elegibilidad para los beneficios." + }, + "scenario7": { + "description": "No hay asuntos pendientes en este momento." + }, + "scenario8": { + "description": "Su reclamo está activo y no hay problemas pendientes en este momento." + }, + "scenario9": { + "description": "Sus pagos están pendientes de revisión adicional." + }, + "scenario10": { + "description": "Sus pagos están pendientes de revisión adicional." + } }, - "base-pending": { - "description": "Sus pagos están pendientes de revisión adicional." - }, - "base-no-pending": { - "description": "No hay asuntos pendientes en este momento." + "conditional-next-steps": { + "certify-no-pending": "<0>Siga certificando mientras necesite beneficios.", + "certify-pending": "<0>Siga certificando mientras necesite beneficios, para que sus pagos no se retrasen más." } } diff --git a/stories/Page.stories.tsx b/stories/Page.stories.tsx index a6ac59ef..f35eae50 100644 --- a/stories/Page.stories.tsx +++ b/stories/Page.stories.tsx @@ -2,8 +2,9 @@ import { Story, Meta } from '@storybook/react' import { withNextRouter } from 'storybook-addon-next-router' import Home, { HomeProps } from '../pages/index' -import getScenarioContent, { ScenarioType, ScenarioTypeKey } from '../utils/getScenarioContent' +import getScenarioContent, { ScenarioType, ScenarioTypeNames } from '../utils/getScenarioContent' import apiGatewayStub from '../utils/apiGatewayStub' +import { getNumericEnumKeys } from '../utils/numericEnum' // See https://storybook.js.org/docs/riot/essentials/controls#dealing-with-complex-values export default { @@ -12,12 +13,11 @@ export default { decorators: [withNextRouter], argTypes: { scenario: { - options: Object.keys(ScenarioType), - mapping: Object.keys(ScenarioType), // return the key instead of the value - defaultValue: 'BaseNoPending', + options: getNumericEnumKeys(ScenarioType), + defaultValue: 0, control: { type: 'select', - labels: ScenarioType, + labels: ScenarioTypeNames, }, }, }, @@ -25,11 +25,11 @@ export default { // Extend HomeProps to add a complex story value interface StoryHomeProps extends HomeProps { - scenario: ScenarioTypeKey + scenario: number } const Template: Story = ({ ...args }) => { - args.scenarioContent = getScenarioContent(apiGatewayStub(ScenarioType[args.scenario])) + args.scenarioContent = getScenarioContent(apiGatewayStub(args.scenario)) return } diff --git a/tests/pages/index.test.tsx b/tests/pages/index.test.tsx index abeea899..69a7c301 100644 --- a/tests/pages/index.test.tsx +++ b/tests/pages/index.test.tsx @@ -15,7 +15,7 @@ jest.mock('next/router', () => ({ let scenarioContent: ScenarioContent beforeAll(() => { - scenarioContent = getScenarioContent(apiGatewayStub(ScenarioType.PendingDetermination)) + scenarioContent = getScenarioContent(apiGatewayStub(ScenarioType.Scenario1)) }) describe('Exemplar react-test-renderer Snapshot test', () => { diff --git a/tests/utils/apiGatewayStub.test.tsx b/tests/utils/apiGatewayStub.test.tsx new file mode 100644 index 00000000..7aa97a6f --- /dev/null +++ b/tests/utils/apiGatewayStub.test.tsx @@ -0,0 +1,39 @@ +import apiGatewayStub from '../../utils/apiGatewayStub' +import { ScenarioType } from '../../utils/getScenarioContent' + +describe('The API gateway stub response for the Pending Determination scenario', () => { + it('is correct', () => { + const response = apiGatewayStub(ScenarioType.Scenario1) + expect(response.pendingDetermination.length).toBeGreaterThan(0) + }) +}) + +describe('The API gateway stub response for the Base States', () => { + it('is correct for Scenario 7', () => { + const response = apiGatewayStub(ScenarioType.Scenario7) + expect([null, [], false, undefined]).toContain(response.pendingDetermination) + expect(response.hasPendingWeeks).toBe(false) + expect(response.hasCertificationWeeksAvailable).toBe(false) + }) + + it('is correct for Scenario 8', () => { + const response = apiGatewayStub(ScenarioType.Scenario8) + expect([null, [], false, undefined]).toContain(response.pendingDetermination) + expect(response.hasPendingWeeks).toBe(false) + expect(response.hasCertificationWeeksAvailable).toBe(true) + }) + + it('is correct for Scenario 9', () => { + const response = apiGatewayStub(ScenarioType.Scenario9) + expect([null, [], false, undefined]).toContain(response.pendingDetermination) + expect(response.hasPendingWeeks).toBe(true) + expect(response.hasCertificationWeeksAvailable).toBe(false) + }) + + it('is correct for Scenario 10', () => { + const response = apiGatewayStub(ScenarioType.Scenario10) + expect([null, [], false, undefined]).toContain(response.pendingDetermination) + expect(response.hasPendingWeeks).toBe(true) + expect(response.hasCertificationWeeksAvailable).toBe(true) + }) +}) diff --git a/tests/utils/getScenarioContent.test.tsx b/tests/utils/getScenarioContent.test.tsx index 532d922e..81963449 100644 --- a/tests/utils/getScenarioContent.test.tsx +++ b/tests/utils/getScenarioContent.test.tsx @@ -1,34 +1,34 @@ -import getScenarioContent, { getScenario, ScenarioType } from '../../utils/getScenarioContent' -import { ScenarioContent } from '../../types/common' - -// Shared test constants for mock API gateway responses -const pendingDeterminationScenario = { pendingDetermination: ['temporary text'] } -const basePendingScenario = { hasPendingWeeks: true } -const baseNoPendingScenario = { hasPendingWeeks: false } +import { getClaimStatusDescription, getScenario, ScenarioType } from '../../utils/getScenarioContent' +import apiGatewayStub from '../../utils/apiGatewayStub' +import { getNumericEnumKeys } from '../../utils/numericEnum' /** * Begin tests */ -// Test getScenarioContent() -describe('Retrieving the scenario content', () => { - it('returns the correct status description for the scenario', () => { - const pendingDetermination: ScenarioContent = getScenarioContent(pendingDeterminationScenario) - expect(pendingDetermination.statusContent.statusDescription).toBe('claim-status:pending-determination.description') - - const basePending: ScenarioContent = getScenarioContent(basePendingScenario) - expect(basePending.statusContent.statusDescription).toBe('claim-status:base-pending.description') - - const baseNoPending: ScenarioContent = getScenarioContent(baseNoPendingScenario) - expect(baseNoPending.statusContent.statusDescription).toBe('claim-status:base-no-pending.description') +// Test getClaimStatusDescripton() +describe('Getting the Claim Status description', () => { + it('returns the correct description for the scenario', () => { + for (const key of getNumericEnumKeys(ScenarioType)) { + expect(getClaimStatusDescription(key)).toEqual( + expect.stringMatching(/claim-status:scenarios.scenario[0-9]+.description/), + ) + } }) }) -// Test getScenario(): pending determination scenario -describe('The pending determination scenario', () => { +/** + * Test getScenario() + * + * Refer to ScenarioType and ScenarioTypeNames for which scenario has which number. + */ + +// Scenario 1 +describe('Scenario 1', () => { it('is returned when there is a pendingDetermination object', () => { - const scenarioType: ScenarioType = getScenario(pendingDeterminationScenario) - expect(scenarioType).toBe(ScenarioType.PendingDetermination) + const pendingDeterminationScenario = apiGatewayStub(ScenarioType.Scenario1) + const scenarioType = getScenario(pendingDeterminationScenario) + expect(scenarioType).toBe(ScenarioType.Scenario1) }) it('is returned when there is a pendingDetermination object regardless of other criteria', () => { @@ -36,63 +36,41 @@ describe('The pending determination scenario', () => { pendingDetermination: ['temporary text'], hasPendingWeeks: true, } - const scenarioTypeWith: ScenarioType = getScenario(pendingDeterminationScenarioWith) - expect(scenarioTypeWith).toBe(ScenarioType.PendingDetermination) + const scenarioTypeWith = getScenario(pendingDeterminationScenarioWith) + expect(scenarioTypeWith).toBe(ScenarioType.Scenario1) const pendingDeterminationScenarioWithout = { pendingDetermination: ['temporary text'], hasPendingWeeks: false, } - const scenarioTypeWithout: ScenarioType = getScenario(pendingDeterminationScenarioWithout) - expect(scenarioTypeWithout).toBe(ScenarioType.PendingDetermination) - }) -}) - -// Test getScenario(): base state with pending weeks scenario -describe('The base state (with pending weeks) scenario', () => { - it('is returned when there are pending weeks', () => { - const scenarioType: ScenarioType = getScenario(basePendingScenario) - expect(scenarioType).toBe(ScenarioType.BasePending) + const scenarioTypeWithout = getScenario(pendingDeterminationScenarioWithout) + expect(scenarioTypeWithout).toBe(ScenarioType.Scenario1) }) - it('is returned when there are pending weeks and pendingDetermination is null', () => { - const basePendingScenarioNull = { hasPendingWeeks: true, pendingDetermination: null } - const scenarioType: ScenarioType = getScenario(basePendingScenarioNull) - expect(scenarioType).toBe(ScenarioType.BasePending) + it('is not returned if pendingDetermination is null', () => { + const pendingDeterminationScenarioNull = { pendingDetermination: null } + const scenarioTypeNull = getScenario(pendingDeterminationScenarioNull) + expect(scenarioTypeNull).not.toBe(ScenarioType.Scenario1) }) - it('is returned when there are pending weeks and pendingDetermination is an empty array', () => { - const basePendingScenarioEmpty = { hasPendingWeeks: true, pendingDetermination: [] } - const scenarioType: ScenarioType = getScenario(basePendingScenarioEmpty) - expect(scenarioType).toBe(ScenarioType.BasePending) + it('is not returned if pendingDetermination is an empty array', () => { + const pendingDeterminationScenarioEmpty = { pendingDetermination: [] } + const scenarioTypeEmpty = getScenario(pendingDeterminationScenarioEmpty) + expect(scenarioTypeEmpty).not.toBe(ScenarioType.Scenario1) }) }) -// Test getScenario(): base state with no pending weeks scenario -describe('The base state (with no pending weeks) scenario', () => { - it('is returned when there are no pending weeks', () => { - const scenarioType: ScenarioType = getScenario(baseNoPendingScenario) - expect(scenarioType).toBe(ScenarioType.BaseNoPending) - }) - - it('is returned when there are no pending weeks and pendingDetermination is null', () => { - const baseNoPendingScenarioNull = { hasPendingWeeks: false, pendingDetermination: null } - const scenarioType: ScenarioType = getScenario(baseNoPendingScenarioNull) - expect(scenarioType).toBe(ScenarioType.BaseNoPending) - }) - - it('is returned when there are no pending weeks and pendingDetermination is an empty array', () => { - const baseNoPendingScenarioEmpty = { hasPendingWeeks: false, pendingDetermination: [] } - const scenarioType: ScenarioType = getScenario(baseNoPendingScenarioEmpty) - expect(scenarioType).toBe(ScenarioType.BaseNoPending) - }) -}) - -// Test getScenario(): error -describe('Getting the scenario', () => { - it.skip('errors when given an unknown scenario', () => { - expect(() => { - getScenario({}) - }).toThrowError('Unknown Scenario') +// Scenarios 7-10 +describe('The Base State scenarios', () => { + it('are returned as expected', () => { + const baseScenarios = [ + ScenarioType.Scenario7, + ScenarioType.Scenario8, + ScenarioType.Scenario9, + ScenarioType.Scenario10, + ] + for (const scenarioType of baseScenarios) { + expect(getScenario(apiGatewayStub(scenarioType))).toBe(scenarioType) + } }) }) diff --git a/tests/utils/numericEnum.test.tsx b/tests/utils/numericEnum.test.tsx new file mode 100644 index 00000000..94e9466d --- /dev/null +++ b/tests/utils/numericEnum.test.tsx @@ -0,0 +1,19 @@ +import { getNumericEnumKeys, getNumericEnumLength } from '../../utils/numericEnum' + +// Shared mock enum +enum TestEnum { + FOO, + BAR, +} + +describe('Numeric enum length', () => { + it('equals the number of written elements', () => { + expect(getNumericEnumLength(TestEnum)).toEqual(2) + }) +}) + +describe('Numeric enum keys', () => { + it('equals an array of incrementing numbers', () => { + expect(getNumericEnumKeys(TestEnum)).toEqual([0, 1]) + }) +}) diff --git a/types/common.tsx b/types/common.tsx index 8a928321..562835d5 100644 --- a/types/common.tsx +++ b/types/common.tsx @@ -1,3 +1,6 @@ +// Type aliases +export type I18nString = string + // Type interfaces for API gateway result export interface PendingDetermination { determinationStatus?: null | undefined | string @@ -19,7 +22,7 @@ export interface Claim { uniqueNumber?: null | string claimDetails?: null | ClaimDetailsResult hasPendingWeeks?: null | undefined | boolean - hasCertificationWeeksAvailable?: null | boolean + hasCertificationWeeksAvailable?: null | undefined | boolean pendingDetermination?: null | [PendingDetermination] } diff --git a/utils/apiGatewayStub.tsx b/utils/apiGatewayStub.tsx index 8e585398..81015d8f 100644 --- a/utils/apiGatewayStub.tsx +++ b/utils/apiGatewayStub.tsx @@ -1,3 +1,9 @@ +/** + * Stub for the API gateway. + * + * Provides stub responses for API gateway queries for Storybook and Jest testing. + */ + import { ScenarioType } from '../utils/getScenarioContent' import { Claim } from '../types/common' @@ -5,7 +11,6 @@ import { Claim } from '../types/common' * Stub the API gateway response for a given scenario. */ export default function apiGatewayStub(scenarioType: ScenarioType): Claim { - console.log('apiGatewayStub') const claim: Claim = { uniqueNumber: null, claimDetails: null, @@ -15,16 +20,25 @@ export default function apiGatewayStub(scenarioType: ScenarioType): Claim { } switch (scenarioType) { - case ScenarioType.PendingDetermination: + case ScenarioType.Scenario1: claim.pendingDetermination = [{ determinationStatus: null }] break - case ScenarioType.BasePending: + // @TODO: This scenario should probably not be the default case. + // case ScenarioType.Scenario7: + + case ScenarioType.Scenario8: + claim.hasCertificationWeeksAvailable = true + break + + case ScenarioType.Scenario9: claim.hasPendingWeeks = true break - // @TODO: This scenario should probably not be the default case. - // case ScenarioType.BaseNoPending: + case ScenarioType.Scenario10: + claim.hasPendingWeeks = true + claim.hasCertificationWeeksAvailable = true + break // @TODO: No match should throw an error // default: diff --git a/utils/getScenarioContent.tsx b/utils/getScenarioContent.tsx index 09fa340b..25e3ba2e 100644 --- a/utils/getScenarioContent.tsx +++ b/utils/getScenarioContent.tsx @@ -1,75 +1,96 @@ -import { Claim, ClaimDetailsContent, ClaimStatusContent, ScenarioContent } from '../types/common' +/** + * Utility file for returning the correct content for each scenario. + * + * Scenarios are referred to by number and the numbers match the content spreadsheet from + * UIB. The ScenarioType enum is a numeric enum so that we can take advantage of the + * built-in Typescript reverse mapping for numeric enums. However, we set the long-form + * description in ScenarioTypeNames for easy(ish) reference. + */ -export type ScenarioTypeKey = keyof typeof ScenarioType +import { Claim, ClaimDetailsContent, ClaimStatusContent, I18nString, ScenarioContent } from '../types/common' export enum ScenarioType { - PendingDetermination = 'Pending determination scenario', - BasePending = 'Base state with pending weeks', - BaseNoPending = 'Base state with no pending weeks', + Scenario1, + Scenario7, + Scenario8, + Scenario9, + Scenario10, +} + +export const ScenarioTypeNames = { + [ScenarioType.Scenario1]: 'Pending determination scenario', + [ScenarioType.Scenario7]: 'Base state; No pending weeks; No weeks to certify', + [ScenarioType.Scenario8]: 'Base state; No pending weeks; Has weeks to certify', + [ScenarioType.Scenario9]: 'Base state; Has pending weeks; No weeks to certify', + [ScenarioType.Scenario10]: 'Base state; Has pending weeks; Has weeks to certify', } /** * Identify the correct scenario to display. * - * @param {Qbject} claim - * @returns {Object} + * @TODO: Validating the API gateway response #150 */ export function getScenario(claimData: Claim): ScenarioType { // The pending determination scenario: if claimData contains any pendingDetermination // objects // @TODO: refactor with more detailed pending determination scenarios #252 if (claimData.pendingDetermination && claimData.pendingDetermination.length > 0) { - return ScenarioType.PendingDetermination - } - // The generic pending scenario: if there are no pendingDetermination objects - // AND hasPendingWeeks is true - else if (claimData.hasPendingWeeks === true) { - return ScenarioType.BasePending - } - // The generic "all clear"/base state scenario: if there are no pendingDetermination objects - // and hasPendingWeeks is false - else if (claimData.hasPendingWeeks === false) { - return ScenarioType.BaseNoPending + return ScenarioType.Scenario1 } - // This is unexpected - // @TODO: Log the scenario and display 500 + + // No pendingDetermination objects: display a Base State scenario. else { - // throw new Error('Unexpected scenario') - return ScenarioType.BaseNoPending + // @TODO: Validate that hasPendingWeeks is a boolean + if (claimData.hasPendingWeeks === false) { + // @TODO: Validate that hasCertificationWeeks is a boolean + if (claimData.hasCertificationWeeksAvailable === false) { + return ScenarioType.Scenario7 + } else { + return ScenarioType.Scenario8 + } + } + // hasPendingWeeks === true + else { + if (claimData.hasCertificationWeeksAvailable === false) { + return ScenarioType.Scenario9 + } else { + return ScenarioType.Scenario10 + } + } } } +/** + * Get Claim Status description content. + */ +export function getClaimStatusDescription(scenarioType: ScenarioType): I18nString { + return `claim-status:scenarios.${ScenarioType[scenarioType].toLowerCase()}.description` +} + +/** + * Get Claim Status content. + */ +export function buildClaimStatusContent(scenarioType: ScenarioType): ClaimStatusContent { + const statusContent: ClaimStatusContent = { + statusDescription: getClaimStatusDescription(scenarioType), + nextSteps: [ + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', + ], + } + + return statusContent +} + /** * Return scenario content. - * - * @param {Object} claim - * @param {enum} scenarioType - * @returns {Object} */ export default function getScenarioContent(claimData: Claim): ScenarioContent { // Get the scenario type. const scenarioType = getScenario(claimData) // Construct claim status content. - // This sets an i18n string. - let statusDescription = '' - if (scenarioType === ScenarioType.PendingDetermination) { - statusDescription = 'claim-status:pending-determination.description' - } else if (scenarioType === ScenarioType.BasePending) { - statusDescription = 'claim-status:base-pending.description' - } else { - statusDescription = 'claim-status:base-no-pending.description' - } - - const nextSteps = [ - 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', - 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.', - ] - - const statusContent: ClaimStatusContent = { - statusDescription: statusDescription, - nextSteps: nextSteps, - } + const statusContent = buildClaimStatusContent(scenarioType) // Construct claim details content. // @TODO: Remove placeholder default content diff --git a/utils/numericEnum.tsx b/utils/numericEnum.tsx new file mode 100644 index 00000000..68d12d2f --- /dev/null +++ b/utils/numericEnum.tsx @@ -0,0 +1,34 @@ +/** + * Utility file with helper functions for working with Typescript Enums with numeric members. + * + * References: + * - https://www.typescriptlang.org/docs/handbook/enums.html#numeric-enums + * - https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings + * + * Typescript Enums with numeric members look like this: + * enum TestEnum { + * FOO, + * BAR + * } + * They have integer values starting with 0 and incrementing by 1: TestEnum.FOO == 0 + * They have built-in reverse mappings: TestEnum[0] == "FOO" + */ + +type NumericEnum = { [key: number]: string } + +/** + * Get the number of elements in a numeric enum. + * + * Numeric enums have twice the number of elements because they provide + * reverse lookups. + */ +export function getNumericEnumLength(enumme: NumericEnum): number { + return Object.keys(enumme).length / 2 +} + +/** + * Generate an array of incrementing numbers. + */ +export function getNumericEnumKeys(enumme: NumericEnum): number[] { + return Array.from(Array(getNumericEnumLength(enumme)).keys()) +} diff --git a/utils/queryApiGateway.tsx b/utils/queryApiGateway.tsx index 180769ad..374d5ae1 100644 --- a/utils/queryApiGateway.tsx +++ b/utils/queryApiGateway.tsx @@ -1,3 +1,16 @@ +/** + * Utility file for connecting to and querying the API gateway. + * + * Prerequisites for a successful connection: + * - unique number in the received request header + * - the following environment variables: + * - ID_HEADER_NAME: string for the unique number header key + * - API_URL: url for the API gateway + * - API_USER_KEY: key for authenticating with the API gateway + * - CERTIFICATE_DIR: path to the PKCS#12 certificate + * - P12_FILE: filename of the PKCS#12 certificate for authenticating with the API gateway + */ + import path from 'path' import fs from 'fs' import https from 'https'