diff --git a/src/client/modules/ExportWins/Form/AddExportWinForm.jsx b/src/client/modules/ExportWins/Form/AddExportWinForm.jsx index ffcc43bdf5e..218b24febc6 100644 --- a/src/client/modules/ExportWins/Form/AddExportWinForm.jsx +++ b/src/client/modules/ExportWins/Form/AddExportWinForm.jsx @@ -20,7 +20,7 @@ import OfficerDetailsStep from './OfficerDetailsStep' import CreditForThisWinStep from './CreditForThisWinStep' import CustomerDetailsStep from './CustomerDetailsStep' import WinDetailsStep from './WinDetailsStep' -import SupportGivenStep from './SupportGivenStep' +import SupportProvidedStep from './SupportProvidedStep' import CheckBeforeSendingStep from './CheckBeforeSending' const StyledLoadingBox = styled(LoadingBox)({ @@ -87,7 +87,7 @@ const AddExportWinForm = ({ isEditing, csrfToken, currentAdviserId }) => { - +
{JSON.stringify(values, null, 2)}
diff --git a/src/client/modules/ExportWins/Form/SupportGivenStep.jsx b/src/client/modules/ExportWins/Form/SupportProvidedStep.jsx similarity index 64% rename from src/client/modules/ExportWins/Form/SupportGivenStep.jsx rename to src/client/modules/ExportWins/Form/SupportProvidedStep.jsx index 07aedc78e18..9f600ffbb09 100644 --- a/src/client/modules/ExportWins/Form/SupportGivenStep.jsx +++ b/src/client/modules/ExportWins/Form/SupportProvidedStep.jsx @@ -1,15 +1,21 @@ import React from 'react' +import styled from 'styled-components' import { useFormContext } from '../../../components/Form/hooks' -import { Step } from '../../../components' +import { Step, FieldInput } from '../../../components' import { steps } from './constants' +const StyledFieldInput = styled(FieldInput)({ + display: 'none', +}) + const SupportProvidedStep = () => { // eslint-disable-next-line no-unused-vars const { values } = useFormContext() return (

Support provided

+
) } diff --git a/src/client/modules/ExportWins/Form/WinDetailsStep.jsx b/src/client/modules/ExportWins/Form/WinDetailsStep.jsx index 243da32d5b2..4a085ffc0e3 100644 --- a/src/client/modules/ExportWins/Form/WinDetailsStep.jsx +++ b/src/client/modules/ExportWins/Form/WinDetailsStep.jsx @@ -1,21 +1,225 @@ import React from 'react' +import { Details, ListItem, UnorderedList } from 'govuk-react' +import { H3 } from '@govuk-react/heading' import styled from 'styled-components' +import ResourceOptionsField from '../../../components/Form/elements/ResourceOptionsField' import { useFormContext } from '../../../../client/components/Form/hooks' -import { Step, FieldInput } from '../../../components' -import { steps } from './constants' +import CountriesResource from '../../../components/Resource/Countries' +import { BLACK, WHITE } from '../../../../client/utils/colours' +import { SectorResource } from '../../../components/Resource' +import { OPTION_YES } from '../../../../common/constants' +import { WinTypeValues } from './WinTypeValues' +import { StyledHintParagraph } from './styled' +import { + Step, + FieldDate, + FieldInput, + FieldTextarea, + FieldTypeahead, + FieldCheckboxes, +} from '../../../components' +import { + steps, + winTypes, + winTypeOptions, + goodsServicesOptions, +} from './constants' +import { + formatValue, + getTwelveMonthsAgo, + sumAllWinTypeYearlyValues, +} from './utils' -const StyledFieldInput = styled(FieldInput)({ - display: 'none', +const MAX_WORDS = 100 + +const StyledDetails = styled(Details)({ + margin: 0, +}) + +const StyledExportTotal = styled('p')({ + padding: 10, + color: WHITE, + backgroundColor: BLACK, }) const WinDetailsStep = () => { - // eslint-disable-next-line no-unused-vars const { values } = useFormContext() + const twelveMonthsAgo = getTwelveMonthsAgo() + const month = twelveMonthsAgo.getMonth() + 1 + const year = twelveMonthsAgo.getFullYear() + return ( -

Win details

- +

Win details

+ + The customer will be asked to confirm this infomation. + + + + + + + + + + + + ({ + ...option, + ...(option.value === winTypes.EXPORT && { + children: ( + + ), + }), + ...(option.value === winTypes.BUSINESS_SUCCESS && { + link: ( + +

Business success is defined as:

+ + + the exchange of ownership of goods/services from a + subsidiary of an eligible UK company to a non_UK resident. + + + in financial services, the value of assets under management + or the value of a listing. + + + the collection of cash from an overdue invoice. + + + reduced tax burden on a customer achieved by lobbying. + + repatriation of profits to the UK. + +
+ ), + children: ( + + ), + }), + ...(option.value === winTypes.ODI && { + link: ( + +

+ An ODI is a cross-border investment from the UK into another + country, where the source of the money is the UK. +

+

+ The aim of the investment is to set up a lasting interest in a + company, where the investor has a genuine influence in the + management. This may involve providing capital for vehicles, + machinery, buildings, and running costs to: +

+ + set up an overseas subsidiary. + enter into a joint venture. + expand current overseas operations. + +
+ ), + children: ( + + ), + }), + }))} + /> + + {values?.win_type?.length > 1 && ( + + Total export value:{' '} + {formatValue(sumAllWinTypeYearlyValues(values?.win_type, values))} + + )} + + + + + +
) } diff --git a/src/client/modules/ExportWins/Form/WinTypeValues.jsx b/src/client/modules/ExportWins/Form/WinTypeValues.jsx new file mode 100644 index 00000000000..33c6678df41 --- /dev/null +++ b/src/client/modules/ExportWins/Form/WinTypeValues.jsx @@ -0,0 +1,59 @@ +import React from 'react' +import Label from '@govuk-react/label' +import pluralize from 'pluralize' +import styled from 'styled-components' + +import { LIGHT_GREY } from '../../../../client/utils/colours' +import { FieldCurrency } from '../../../components' +import { StyledHintParagraph } from './styled' +import { + formatValue, + getYearFromWinType, + sumWinTypeYearlyValues, +} from './utils' + +const WinTypeContainer = styled('div')({ + marginLeft: 56, +}) + +const FieldCurrencyContainer = styled('div')({ + display: 'flex', + gap: 5, +}) + +const StyledLabel = styled(Label)({ + fontWeight: 'bold', +}) + +const StyledParagraph = styled('p')({ + padding: 10, + fontWeight: 'bold', + backgroundColor: LIGHT_GREY, +}) + +export const WinTypeValues = ({ label, name, years = 5, values }) => { + const year = getYearFromWinType(name, values) + return ( + + {label} + + (round to nearest £) + + + {[...Array(years).keys()].map((index) => ( + + ))} + + + Totalling over {year} {pluralize('year', year)}:{' '} + {formatValue(sumWinTypeYearlyValues(name, values))} + + + ) +} diff --git a/test/component/cypress/specs/ExportWins/WinTypeValues.cy.jsx b/test/component/cypress/specs/ExportWins/WinTypeValues.cy.jsx new file mode 100644 index 00000000000..605bed1ebb8 --- /dev/null +++ b/test/component/cypress/specs/ExportWins/WinTypeValues.cy.jsx @@ -0,0 +1,241 @@ +import React from 'react' + +import { WinTypeValues } from '../../../../../src/client/modules/ExportWins/Form/WinTypeValues' +import { Form } from '../../../../../src/client/components' +import DataHubProvider from '../provider' + +describe('WinTypeValues', () => { + const Component = (props) => ( + +
+ + +
+ ) + + context('when rendering an export win - sequential years', () => { + it('should render all elements of the component', () => { + cy.mount( + + ) + cy.get('[data-test="label"]').should( + 'have.text', + 'Export value over the next 5 years' + ) + cy.get('[data-test="hint"]').should('have.text', '(round to nearest £)') + cy.get('input[type="text"]').should('have.length', 5) + const expected = ['Year 1', 'Year 2', 'Year 3', 'Year 4', 'Year 5'] + expected.forEach((year, index) => { + cy.get(`[data-test="field-export_win_${index}"]`) + .find('label') + .should('have.text', year) + }) + }) + + it('should render an export win value over 5 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 5 years: £5,000,000' + ) + }) + + it('should render an export win value over 4 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 4 years: £4,000,000' + ) + }) + + it('should render an export win value over 3 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 3 years: £3,000,000' + ) + }) + + it('should render an export win value over 2 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 2 years: £2,000,000' + ) + }) + + it('should render an export win value over 1 year', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 1 year: £1,000,000' + ) + }) + + it('should render an export win value over 0 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 0 years: £0' + ) + }) + }) + + context('when rendering an export win - sporadic years', () => { + it('should render an export win value over 5 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 5 years: £3,000,000' + ) + }) + + it('should render an export win value over 4 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 4 years: £2,000,000' + ) + }) + + it('should render an export win value over 3 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 3 years: £2,000,000' + ) + }) + + it('should render an export win value over 2 years', () => { + cy.mount( + + ) + cy.get('[data-test="total"]').should( + 'have.text', + 'Totalling over 2 years: £1,000,000' + ) + }) + }) +}) diff --git a/test/functional/cypress/specs/export-win/add-export-win-spec.js b/test/functional/cypress/specs/export-win/add-export-win-spec.js index 4bdfdeb714f..a8e3f78d2ff 100644 --- a/test/functional/cypress/specs/export-win/add-export-win-spec.js +++ b/test/functional/cypress/specs/export-win/add-export-win-spec.js @@ -1,17 +1,22 @@ +import { getTwelveMonthsAgo } from '../../../../../src/client/modules/ExportWins/Form/utils' +import { clickContinueButton } from '../../support/actions' +import { companyFaker } from '../../fakers/companies' +import { advisersListFaker } from '../../fakers/advisers' +import { teamTypeListFaker } from '../../fakers/team-type' +import { hqTeamListFaker } from '../../fakers/hq-team' +import urls from '../../../../../src/lib/urls' import { assertUrl, + assertFieldInput, assertFieldError, assertLocalHeader, assertErrorSummary, + assertFieldTextarea, assertFieldTypeahead, + assertFieldDateShort, + assertFieldCheckboxes, assertFieldRadiosWithLegend, } from '../../support/assertions' -import { clickContinueButton } from '../../support/actions' -import { companyFaker } from '../../fakers/companies' -import { advisersListFaker } from '../../fakers/advisers' -import { teamTypeListFaker } from '../../fakers/team-type' -import { hqTeamListFaker } from '../../fakers/hq-team' -import urls from '../../../../../src/lib/urls' const clickContinueAndAssertUrl = (url) => { clickContinueButton() @@ -365,8 +370,312 @@ describe('Adding an export win', () => { }) context('Win details', () => { - it('should complete this step and continue to "Support provided"', () => { + // Helpers + const twelveMonthsAgo = getTwelveMonthsAgo() + const month = twelveMonthsAgo.getMonth() + 1 + const year = twelveMonthsAgo.getFullYear() + + // Fields + const country = '[data-test="field-country"]' + const date = '[data-test="field-date"]' + const description = '[data-test="field-description"]' + const nameOfCustomer = '[data-test="field-name_of_customer"]' + const confidential = '[data-test="field-name_of_customer_confidential"]' + const businessType = '[data-test="field-business_type"]' + const winType = '[data-test="field-win_type"]' + const goodsVsServices = '[data-test="field-goods_vs_services"]' + const nameOfExport = '[data-test="field-name_of_export"]' + const sector = '[data-test="field-sector"]' + + beforeEach(() => cy.visit(`${urls.companies.exportWins.create()}${winDetails}`) + ) + + it('should render a step heading', () => { + cy.get('[data-test="step-heading"]').should('have.text', 'Win details') + }) + + it('should render a hint', () => { + cy.get('[data-test="hint"]').should( + 'have.text', + 'The customer will be asked to confirm this infomation.' + ) + }) + + it('should render Destination country label and a Typeahead', () => { + cy.get(country).then((element) => { + assertFieldTypeahead({ + element, + label: 'Destination country', + }) + }) + }) + + it('should render the Win date', () => { + cy.get(date).then((element) => { + // Both Month and Year labels are tested within the assertion + assertFieldDateShort({ + element, + label: 'Date won', + hint: `For example ${month} ${year}, date of win must be in the last 12 months.`, + }) + }) + }) + + it('should render Summary of the support given', () => { + cy.get(description).then((element) => { + assertFieldTextarea({ + element, + label: 'Summary of the support given', + hint: 'Outline what had the most impact or would be memorable to the customer in less than 100 words.', + wordCount: 'You have 100 words remaining.', + }) + }) + }) + + it('should renderer Overseas customer', () => { + cy.get(nameOfCustomer).then((element) => { + assertFieldInput({ + element, + label: 'Overseas customer', + placeholder: 'Add name', + }) + }) + }) + + it('should render a Confidential checkbox', () => { + assertFieldCheckboxes({ + element: confidential, + hint: 'Check this box if your customer has asked for this not to be public (optional).', + options: [ + { + label: 'Confidential', + checked: false, + }, + ], + }) + }) + + it('should renderer a type of business deal', () => { + cy.get(businessType).then((element) => { + assertFieldInput({ + element, + label: 'Type of business deal', + hint: 'For example: export sales, contract, order, distributor, tender / competition win, joint venture, outward investment.', + placeholder: 'Enter a type of business deal', + }) + }) + }) + + it('should render Type of win ', () => { + assertFieldCheckboxes({ + element: winType, + legend: 'Type of win', + options: [ + { + label: 'Export', + checked: false, + }, + { + label: 'Business success', + checked: false, + }, + { + label: 'Outward Direct Investment (ODI)', + checked: false, + }, + ], + }) + }) + + it('should render the WinTypeValues component for each win type', () => { + const exportWinCheckbox = '[data-test="checkbox-export_win"]' + const exportWinTypeValues = '[data-test="win-type-values-export_win"]' + const businessSuccessCheckbox = + '[data-test="checkbox-business_success_win"]' + const businessSuccessTypeValues = + '[data-test="win-type-values-business_success_win"]' + const odiCheckbox = '[data-test="checkbox-odi_win"]' + const odiWinTypeValues = '[data-test="win-type-values-odi_win"]' + + cy.get(winType).as('winType') + + // Export win + cy.get('@winType') + .find(exportWinTypeValues) + .should('not.exist') + .get('@winType') + .find(exportWinCheckbox) + .check() + .next() + .get(exportWinTypeValues) + .should('exist') + + // Business type + cy.get('@winType') + .find(businessSuccessTypeValues) + .should('not.exist') + .get('@winType') + .find(businessSuccessCheckbox) + .check() + .next() + .get(businessSuccessTypeValues) + .should('exist') + + // ODI + cy.get('@winType') + .find(odiWinTypeValues) + .should('not.exist') + .get('@winType') + .find(odiCheckbox) + .check() + .next() + .get(odiWinTypeValues) + .should('exist') + }) + + it('should render the total export value across all 3 win types', () => { + cy.get(winType).as('winType') + + // Check the Export checkbox to render the input fields + cy.get('@winType').find('[data-test="checkbox-export_win"]').check() + + const exportWinFields = [ + '[data-test="export-win-0-input"]', + '[data-test="export-win-1-input"]', + '[data-test="export-win-2-input"]', + '[data-test="export-win-3-input"]', + '[data-test="export-win-4-input"]', + ] + exportWinFields.forEach((dataTest) => + cy.get('@winType').find(dataTest).type('1000000') + ) + + // Check the Business success checkbox to render the input fields + cy.get('@winType') + .find('[data-test="checkbox-business_success_win"]') + .check() + + const businessSuccessFields = [ + '[data-test="business-success-win-0-input"]', + '[data-test="business-success-win-1-input"]', + '[data-test="business-success-win-2-input"]', + '[data-test="business-success-win-3-input"]', + '[data-test="business-success-win-4-input"]', + ] + businessSuccessFields.forEach((dataTest) => + cy.get('@winType').find(dataTest).type('1000000') + ) + + // Check the ODI checkbox to render the input fields + cy.get('@winType').find('[data-test="checkbox-odi_win"]').check() + + const odiFields = [ + '[data-test="odi-win-0-input"]', + '[data-test="odi-win-1-input"]', + '[data-test="odi-win-2-input"]', + '[data-test="odi-win-3-input"]', + '[data-test="odi-win-4-input"]', + ] + odiFields.forEach((dataTest) => + cy.get('@winType').find(dataTest).type('1000000') + ) + + // Assert the total export value + cy.get('[data-test="total-export-value"]').should( + 'have.text', + 'Total export value: £15,000,000' + ) + }) + + it('should render Goods and Services', () => { + assertFieldCheckboxes({ + element: goodsVsServices, + legend: 'What does the value relate to?', + hint: 'Select goods or services', + options: [ + { + label: 'Goods', + checked: false, + }, + { + label: 'Services', + checked: false, + }, + ], + }) + }) + + it('should renderer name of goods or services', () => { + cy.get(nameOfExport).then((element) => { + assertFieldInput({ + element, + label: 'Name of goods or services', + hint: "For instance 'shortbread biscuits'.", + placeholder: 'Enter a name for goods or services', + }) + }) + }) + + it('should render a sector label and typeahead', () => { + cy.get(sector).then((element) => { + assertFieldTypeahead({ + element, + label: 'Sector', + }) + }) + }) + + it('should display validation error messages on mandatory fields', () => { + clickContinueButton() + assertErrorSummary([ + 'Choose a destination country', + 'Enter the win date', + 'Enter a summary', + 'Enter the name of the overseas customer', + 'Enter the type of business deal', + 'Choose at least one type of win', + 'Select at least one option', + 'Enter the name of goods or services', + 'Enter a sector', + ]) + assertFieldError(cy.get(country), 'Choose a destination country', false) + assertFieldError(cy.get(date), 'Enter the win date', true) + assertFieldError(cy.get(description), 'Enter a summary', true) + assertFieldError( + cy.get(nameOfCustomer), + 'Enter the name of the overseas customer', + false + ) + assertFieldError( + cy.get(businessType), + 'Enter the type of business deal', + true + ) + assertFieldError(cy.get(winType), 'Choose at least one type of win', true) + // We can't use assertFieldError here as it picks up the wrong span + cy.get(goodsVsServices).should('contain', 'Select at least one option') + assertFieldError( + cy.get(nameOfExport), + 'Enter the name of goods or services', + true + ) + assertFieldError(cy.get(sector), 'Enter a sector', false) + }) + + it('should complete this step and continue to "Support provided"', () => { + cy.get(country).selectTypeaheadOption('United states') + cy.get(date).as('winDate') + cy.get('@winDate').find('[data-test="date-month"]').type('03') + cy.get('@winDate').find('[data-test="date-year"]').type('2023') + cy.get(description).find('textarea').type('Foo bar baz') + cy.get(nameOfCustomer).find('input').type('David French') + cy.get(confidential).find('input[type="checkbox"]').check() + cy.get(businessType).find('input').type('Contract') + cy.get(winType).find('[data-test="checkbox-export_win"]').check() + cy.get(goodsVsServices).find('input[type="checkbox"]').eq(0).check() + cy.get(nameOfExport).find('input').type('Biscuits') + cy.get(sector).selectTypeaheadOption('Advanced Engineering') clickContinueAndAssertUrl(supportProvided) }) }) diff --git a/test/functional/cypress/support/assertions.js b/test/functional/cypress/support/assertions.js index 969facd11f9..07bf6df0e18 100644 --- a/test/functional/cypress/support/assertions.js +++ b/test/functional/cypress/support/assertions.js @@ -283,16 +283,29 @@ const assertFieldRadiosWithoutLabel = ({ element, value, optionsCount }) => .should('have.text', value) ) -const assertFieldCheckbox = ({ element, label, value, checked }) => { - cy.wrap(element) - .as('fieldCheckbox') - .find('label') - .should('contain.text', label) - +const assertFieldCheckboxes = ({ element, legend, hint, options = [] }) => { + cy.get(element).as('fieldCheckbox') + // Optional fields + legend && cy.get('@fieldCheckbox').find('legend').should('have.text', legend) + hint && + cy + .get('@fieldCheckbox') + .find('[data-test="hint-text"]') + .should('have.text', hint) + // Mandatory fields cy.get('@fieldCheckbox') - .find('input') - .should('have.attr', 'value', value) - .should(checked ? 'be.checked' : 'not.be.checked') + .find('input[type="checkbox"]') + .should('have.length', options.length) + .get('@fieldCheckbox') + .find('label') + .each((label, index) => { + // Each label wraps an input + cy.get(label) + .should('have.text', options[index].label) + .find('input[type="checkbox"]') + .should(options[index].checked ? 'be.checked' : 'not.be.checked') + .should('have.attr', 'aria-label', options[index].label) + }) } const assertFieldTypeahead = ({ @@ -376,10 +389,9 @@ const assertFieldInput = ({ ($el) => (ignoreHint && value ? cy.wrap($el).next() : undefined) ) .find('input') - .then( - ($el) => - value && cy.wrap($el).should('have.attr', 'value', String(value) || '') - ) + .then(($el) => { + value && cy.wrap($el).should('have.attr', 'value', String(value) || '') + }) const assertFieldInputNoLabel = ({ element, value = undefined }) => cy @@ -393,9 +405,8 @@ const assertFieldInputNoLabel = ({ element, value = undefined }) => const assertFieldHidden = ({ element, name, value }) => cy.wrap(element).should('have.attr', 'name', name).should('have.value', value) -const assertFieldTextarea = ({ element, label, hint, value }) => - cy - .wrap(element) +const assertFieldTextarea = ({ element, label, hint, value, wordCount }) => { + cy.wrap(element) .find('label') .should('contain', label) .parent() @@ -412,6 +423,9 @@ const assertFieldTextarea = ({ element, label, hint, value }) => .find('textarea') .then(($el) => value ?? cy.wrap($el).should('have.text', value || '')) + wordCount && cy.wrap(element).find('p').should('have.text', wordCount) +} + const assertFieldTextareaNoLabel = ({ element, value = undefined }) => cy .wrap(element) @@ -886,7 +900,7 @@ module.exports = { assertFieldRadios, assertFieldRadiosWithLegend, assertFieldRadiosWithoutLabel, - assertFieldCheckbox, + assertFieldCheckboxes, assertFieldAddress, assertFieldUneditable, assertFormActions,