From 74ef749d20fccc4cfd23fc99d8c4c465e47e490b Mon Sep 17 00:00:00 2001 From: Peter Hudec Date: Tue, 27 Feb 2024 16:44:28 +0000 Subject: [PATCH] Added component tests for ExportWins/Details --- src/client/modules/ExportWins/Details.jsx | 239 ++++++------ .../cypress/specs/ExportWins/Details.cy.jsx | 340 ++++++++++++++++++ test/functional/cypress/support/assertions.js | 3 + .../routes/v4/export-win/export-win.js | 1 + test/sandbox/server.js | 2 +- 5 files changed, 465 insertions(+), 120 deletions(-) create mode 100644 test/component/cypress/specs/ExportWins/Details.cy.jsx diff --git a/src/client/modules/ExportWins/Details.jsx b/src/client/modules/ExportWins/Details.jsx index efccec9af22..db77fa8f6a1 100644 --- a/src/client/modules/ExportWins/Details.jsx +++ b/src/client/modules/ExportWins/Details.jsx @@ -1,10 +1,9 @@ -/* eslint-disable prettier/prettier */ import React from 'react' -import { Route } from 'react-router-dom' import { Link } from 'govuk-react' import { Link as ReactRouterLink } from 'react-router-dom/cjs/react-router-dom' import styled from 'styled-components' import { SPACING } from '@govuk-react/constants' +import pluralize from 'pluralize' import { DefaultLayout, SummaryTable } from '../../components' import urls from '../../../lib/urls' @@ -29,131 +28,133 @@ const ExportWinTitle = (props) => ( {(exportWin) => ( <> - {exportWin.name_of_export} to {exportWin.country?.name} + {exportWin.nameOfExport} to {exportWin.country?.name} )} ) -const Detail = () => ( - - {({ - match: { - params: { winId }, - }, - }) => ( - } - pageTitle={} - breadcrumbs={[ - { - link: urls.dashboard.index(), - text: 'Home', +const groupBreakdowns = (breakdowns) => { + const result = + breakdowns.reduce( + ({ groups: groups, totalAmount, yearRange }, breakdown) => { + const group = groups[breakdown.type.name] + + return { + totalAmount: totalAmount + breakdown.value, + yearRange: { + min: Math.min(yearRange.min, breakdown.year), + max: Math.max(yearRange.max, breakdown.year), }, - { - link: urls.companies.exportWins.index(), - text: 'Export wins', + groups: { + ...groups, + [breakdown.type.name]: { + yearRange: { + min: Math.min(group?.yearRange.min || Infinity, breakdown.year), + max: Math.max(group?.yearRange.min || 0, breakdown.year), + }, + total: (group?.total || 0) + breakdown.value, + }, }, - { text: }, - ]} - > - - {(exportWin) => { - // TODO: Same / similar computation is used in the add export win form so it should be reused - const { - groupedBreakdowns = [], - totalAmount, - yearRange, - totalYears = yearRange?.max - yearRange?.min + 1, - } = exportWin?.breakdowns.reduce( - ({ groupedBreakdowns, totalAmount, yearRange }, x) => ({ - totalAmount: totalAmount + x.value, - yearRange: { - min: Math.min(yearRange.min, x.year), - max: Math.max(yearRange.max, x.year), - }, - groupedBreakdowns: { - ...groupedBreakdowns, - [x.type.name]: { - yearCount: - (groupedBreakdowns[x.type.name]?.yearCount || 0) + 1, - total: - (groupedBreakdowns[x.type.name]?.total || 0) + x.value, - }, - }, - }), - { - groupedBreakdowns: {}, - totalAmount: 0, - yearRange: { min: Infinity, max: 0 }, - } - ) || {} + } + }, + { + groups: {}, + totalAmount: 0, + yearRange: { min: Infinity, max: 0 }, + } + ) || {} - return ( - <> - - - {exportWin?.goodsVsServices.name} - - - {exportWin?.country.name} - - {exportWin && - Object.keys(groupedBreakdowns).length > 1 && - Object.entries(groupedBreakdowns).map( - ([k, { total, yearCount }]) => ( - - {`${currencyGBP(total)} over ${yearCount} years`} - - ) - )} - - {exportWin && - `${currencyGBP(totalAmount)} over ${totalYears} years`} - - - {exportWin && formatMediumDate(exportWin.date)} - - - {exportWin?.leadOfficer.name} - - - {exportWin?.customerResponse?.comments} - - - {exportWin && - (exportWin.isPersonallyConfirmed ? 'Yes' : 'No')} - - {exportWin?.isPersonallyConfirmed && - exportWin?.breakdowns.length === 0 && ( - - ??? - - )} - - - {exportWin?.isLineManagerConfirmed && ( - - View customer feedback - - )} - - Export wins - - - - ) - }} - - - )} - + return { + ...result, + totalYears: result.yearRange.max - result.yearRange.min + 1, + } +} + +const Detail = ({ + match: { + params: { winId }, + }, +}) => ( + } + pageTitle={} + breadcrumbs={[ + { + link: urls.dashboard.index(), + text: 'Home', + }, + { + link: urls.companies.exportWins.index(), + text: 'Export wins', + }, + { text: }, + ]} + > + + {(exportWin) => { + const { groups, totalAmount, totalYears } = exportWin + ? groupBreakdowns(exportWin.breakdowns) + : {} + + return ( + <> + + + {exportWin?.goodsVsServices.name} + + + {exportWin?.country.name} + + {exportWin && + Object.keys(groups).length > 1 && + Object.entries(groups).map(([k, { total, yearRange }]) => { + const years = yearRange.max - yearRange.min + 1 + return ( + + {`${currencyGBP(total)} over ${years} ${pluralize('year', years)}`} + + ) + })} + + {exportWin && + `${currencyGBP(totalAmount)} over ${totalYears} ${pluralize('year', totalYears)}`} + + + {exportWin && formatMediumDate(exportWin.date)} + + + {exportWin?.leadOfficer.name} + + + {exportWin?.comments} + + + {exportWin && (exportWin.isPersonallyConfirmed ? 'Yes' : 'No')} + + + + {exportWin?.isPersonallyConfirmed && ( + + View customer feedback + + )} + + Export wins + + + + ) + }} + + ) export default Detail diff --git a/test/component/cypress/specs/ExportWins/Details.cy.jsx b/test/component/cypress/specs/ExportWins/Details.cy.jsx new file mode 100644 index 00000000000..6d8d9c0a4d9 --- /dev/null +++ b/test/component/cypress/specs/ExportWins/Details.cy.jsx @@ -0,0 +1,340 @@ +import React from 'react' + +import { + assertBreadcrumbs, + assertLocalHeader, + assertLink, +} from '../../../../functional/cypress/support/assertions' +import Details from '../../../../../src/client/modules/ExportWins/Details' +import urls from '../../../../../src/lib/urls' +import DataHubProvider from '../provider' + +const insertAfter = (o, n, oo) => { + const entries = Object.entries(o) + return Object.fromEntries( + entries.toSpliced( + entries.findIndex(([k]) => k === n) + 1, + null, + ...Object.entries(oo) + ) + ) +} + +const EXPORT_WIN = { + id: 'export-win-id', + name_of_export: 'Rubber chicken', + lead_officer: { + name: 'Leo Jacobs', + }, + country: { + name: 'Australia', + }, + comments: 'Lorem ipsum dolor sit amet', + total_expected_export_value: 2528571, + date: '2024-03-26T14:29:01.521Z', + goods_vs_services: { + name: 'Services', + }, + is_personally_confirmed: false, + breakdowns: [ + { + type: { + name: 'Export', + }, + value: 123456, + year: 3, + }, + ], +} + +const EXPECTED_ROWS = { + 'Goods or services': EXPORT_WIN.goods_vs_services.name, + 'Destination country': EXPORT_WIN.country.name, + 'Total value': '£123,456 over 1 year', + 'Date won': '26 Mar 2024', + 'Lead officer name': 'Leo Jacobs', + Comments: EXPORT_WIN.comments, + 'Export win confirmed': 'No', +} + +describe('ExportWins/Details', () => { + const title = `${EXPORT_WIN.name_of_export} to ${EXPORT_WIN.country.name}` + + const Component = ({ exportWinAPIResponse }) => ( + exportWinAPIResponse }} + > +
+ + ) + + ;[ + { + testTitle: 'Confirmed', + exportWinAPIResponse: { + ...EXPORT_WIN, + is_personally_confirmed: true, + }, + tableRows: { + ...EXPECTED_ROWS, + 'Export win confirmed': 'Yes', + }, + shouldHaveCustomerFeedbackLink: true, + }, + { + testTitle: 'Unconfirmed', + exportWinAPIResponse: { + ...EXPORT_WIN, + is_personally_confirmed: false, + }, + tableRows: { + ...EXPECTED_ROWS, + 'Export win confirmed': 'No', + }, + }, + { + testTitle: 'Single Foo breakdown', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Foo' }, + value: 111111, + year: 1, + }, + ], + }, + tableRows: { + ...EXPECTED_ROWS, + 'Total value': '£111,111 over 1 year', + }, + }, + { + testTitle: 'Single Bar breakdown', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Bar' }, + value: 222222, + year: 1, + }, + ], + }, + tableRows: { + ...EXPECTED_ROWS, + 'Total value': '£222,222 over 1 year', + }, + }, + { + testTitle: 'Two Bar breakdowns', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Bar' }, + value: 222222, + year: 2, + }, + { + type: { name: 'Bar' }, + value: 111111, + year: 2, + }, + ], + }, + tableRows: { + ...EXPECTED_ROWS, + 'Total value': '£333,333 over 1 year', + }, + }, + { + testTitle: 'Three Foo breakdowns', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Foo' }, + value: 222222, + year: 3, + }, + { + type: { name: 'Foo' }, + value: 111111, + year: 3, + }, + { + type: { name: 'Foo' }, + value: 333333, + year: 3, + }, + ], + }, + tableRows: { + ...EXPECTED_ROWS, + 'Total value': '£666,666 over 1 year', + }, + }, + { + testTitle: 'Three different 1 year breakdowns', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Foo' }, + value: 222222, + year: 4, + }, + { + type: { name: 'Bar' }, + value: 111111, + year: 4, + }, + { + type: { name: 'Baz' }, + value: 333333, + year: 4, + }, + ], + }, + tableRows: insertAfter( + { + ...EXPECTED_ROWS, + 'Total value': '£666,666 over 1 year', + }, + 'Destination country', + { + Foo: '£222,222 over 1 year', + Bar: '£111,111 over 1 year', + Baz: '£333,333 over 1 year', + } + ), + }, + { + testTitle: 'Two different breakdowns over 5 years', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Foo' }, + value: 3, + year: 1, + }, + { + type: { name: 'Bar' }, + value: 20, + year: 5, + }, + ], + }, + tableRows: insertAfter( + { + ...EXPECTED_ROWS, + 'Total value': '£23 over 5 years', + }, + 'Destination country', + { + Foo: '£3 over 1 year', + Bar: '£20 over 1 year', + } + ), + }, + { + testTitle: 'Three different breakdowns over 5 years', + exportWinAPIResponse: { + ...EXPORT_WIN, + breakdowns: [ + { + type: { name: 'Foo' }, + value: 1, + year: 1, + }, + { + type: { name: 'Baz' }, + value: 100, + year: 2, + }, + { + type: { name: 'Bar' }, + value: 10, + year: 3, + }, + { + type: { name: 'Baz' }, + value: 100, + year: 4, + }, + { + type: { name: 'Foo' }, + value: 1, + year: 3, + }, + { + type: { name: 'Baz' }, + value: 100, + year: 5, + }, + ], + }, + tableRows: insertAfter( + { + ...EXPECTED_ROWS, + 'Total value': '£312 over 5 years', + }, + 'Destination country', + { + Foo: '£2 over 3 years', + Baz: '£300 over 4 years', + Bar: '£10 over 1 year', + } + ), + }, + ].forEach( + ({ + testTitle, + exportWinAPIResponse, + tableRows, + shouldHaveCustomerFeedbackLink, + }) => { + it(testTitle, () => { + const rowEntries = Object.entries(tableRows) + cy.mount() + + assertBreadcrumbs({ + Home: urls.dashboard.index(), + 'Export wins': urls.companies.exportWins.index(), + [title]: null, + }) + + assertLocalHeader(title) + + // TODO: Abstract it away into assertSummaryTableStrict + cy.get('main table tr') + .as('rows') + .should('have.length', rowEntries.length) + + rowEntries.forEach(([key, val], i) => { + cy.get('@rows') + .eq(i) + .within(() => { + cy.get('th').should('have.length', 1).should('have.text', key) + cy.get('td').should('have.length', 1).should('have.text', val) + }) + }) + + assertLink('export-wins-link', '/exportwins') + + if (shouldHaveCustomerFeedbackLink) { + cy.contains('a', 'View customer feedback').should( + 'have.attr', + 'href', + `/exportwins/${EXPORT_WIN.id}/customer-feedback` + ) + } else { + cy.contains('View customer feedback').should('not.exist') + } + }) + } + ) +}) diff --git a/test/functional/cypress/support/assertions.js b/test/functional/cypress/support/assertions.js index 07bf6df0e18..8de79d1b35b 100644 --- a/test/functional/cypress/support/assertions.js +++ b/test/functional/cypress/support/assertions.js @@ -50,6 +50,8 @@ const assertValueTable = (dataTest, expected) => { }) } +// FIXME: The assertion only asserts subset of rows, +// but it should assert exact list including order const assertSummaryTable = ({ dataTest, heading, @@ -868,6 +870,7 @@ const assertTypeaheadValues = (selector, values) => { * Assert that a link exists and that the href url is correct */ const assertLink = (dataTest, expected) => { + // FIXME: Should also assert the link text cy.get(`[data-test=${dataTest}]`) .should('exist') .should('have.attr', 'href', expected) diff --git a/test/sandbox/routes/v4/export-win/export-win.js b/test/sandbox/routes/v4/export-win/export-win.js index 708baae160a..62e0b82a257 100644 --- a/test/sandbox/routes/v4/export-win/export-win.js +++ b/test/sandbox/routes/v4/export-win/export-win.js @@ -45,6 +45,7 @@ const fakeExportWin = () => ({ email: faker.internet.email(), }, ], + name_of_export: faker.commerce.productName(), customer_name: faker.person.fullName(), customer_job_title: faker.person.jobTitle(), customer_email_address: faker.internet.email(), diff --git a/test/sandbox/server.js b/test/sandbox/server.js index 496b722d528..9a274de1680 100644 --- a/test/sandbox/server.js +++ b/test/sandbox/server.js @@ -530,7 +530,7 @@ app.post('/v4/event', createEvent) // V4 Export Win app.get('/v4/export_win', getExportWinCollection) -app.get('/v4/export_win/:exportWinId', getExportWin) +app.get('/v4/export-win/:exportWinId', getExportWin) // V3 Feature Flag app.get('/v3/feature-flag', featureFlag)