Skip to content

Commit

Permalink
Fix/v6 fixing sr panel regression for open invoices (#2611)
Browse files Browse the repository at this point in the history
* Fixing srPanel regression for OpenInvoices (and adding e2e tests)

* Improved comments on SRPanel error utils

* Riverty e2e test passes

* Also cherry-picked allowing forward slashes in address fields

* Ensure that post code related SRPanel errors include the field label

* Decoupling delivery address and billing address specifications, for SRPanel, in OpenInvoice.tsx

* Added e2e test for incorrectly filled postal code

* Added unit tests for OpenInvoice field mapping util

* Removed unused import

* When assessing error look for expected props, unique to an SFError c.f. instanceOf ValidationRuleResult. It's semantically more correct and plays better with unit tests

* Added unit test for sortErrorsByLayout function in Errors/utils
  • Loading branch information
sponglord authored Mar 22, 2024
1 parent 5659d06 commit bf2ce35
Show file tree
Hide file tree
Showing 18 changed files with 596 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html class="no-js" lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Adyen Web | Open Invoices</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
</head>

<body>
<main>
<div class="merchant-checkout__form">
<div id="openInvoicesContainer">
<div id="rivertyContainer"></div>
</div>
</div>
</main>

<script type="text/javascript">
window.htmlPages = <%= JSON.stringify(htmlWebpackPlugin.htmlPages) || '' %>;
</script>
</body>
</html>
28 changes: 28 additions & 0 deletions packages/e2e-playwright/app/src/pages/OpenInvoices/OpenInvoices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { AdyenCheckout, Riverty } from '@adyen/adyen-web';
import '@adyen/adyen-web/styles/adyen.css';
import { getPaymentMethods } from '../../services';
import { amount, shopperLocale, countryCode } from '../../services/commonConfig';
import { handleSubmit, handleAdditionalDetails, handleError } from '../../handlers';
import '../../style.scss';

const initCheckout = async () => {
const paymentMethodsResponse = await getPaymentMethods({ amount, shopperLocale });

window.checkout = await AdyenCheckout({
amount,
countryCode,
clientKey: process.env.__CLIENT_KEY__,
paymentMethodsResponse,
locale: shopperLocale,
environment: 'test',
onSubmit: handleSubmit,
onAdditionalDetails: handleAdditionalDetails,
onError: handleError,
showPayButton: true,
...window.mainConfiguration
});

window.riverty = new Riverty(checkout, window.rivertyConfig).mount('#rivertyContainer');
};

initCheckout();
30 changes: 30 additions & 0 deletions packages/e2e-playwright/models/openInvoices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Locator, Page } from '@playwright/test';

class OpenInvoices {
readonly page: Page;

readonly rootElement: Locator;
readonly rootElementSelector: string;

readonly riverty: Locator;
readonly rivertyDeliveryAddressCheckbox: Locator;

constructor(page: Page, rootElementSelector = '#openInvoicesContainer') {
this.page = page;

this.rootElement = page.locator(rootElementSelector);
this.rootElementSelector = rootElementSelector;

this.riverty = this.rootElement.locator('#rivertyContainer');

this.rivertyDeliveryAddressCheckbox = this.riverty
.locator('.adyen-checkout__checkbox')
.filter({ hasText: 'Specify a separate delivery address' });
}

async isComponentVisible() {
await this.rootElement.waitFor({ state: 'visible' });
}
}

export { OpenInvoices };
40 changes: 40 additions & 0 deletions packages/e2e-playwright/pages/openInvoices/openInvoices.fixture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { test as base, expect, Page } from '@playwright/test';
import { OpenInvoicesPage } from './openInvoices.page';
type Fixture = {
openInvoicesPage: OpenInvoicesPage;
openInvoicesPage_riverty: OpenInvoicesPage;
};

const test = base.extend<Fixture>({
openInvoicesPage: async ({ page }, use) => {
await useOpenInvoicesPage(page, use);
},

openInvoicesPage_riverty: async ({ page }, use) => {
const mainConfig = JSON.stringify({
srConfig: {
showPanel: true
}
});
await page.addInitScript({
content: `window.mainConfiguration = ${mainConfig}`
});

const rivertyConfig = JSON.stringify({
countryCode: 'DE'
});
await page.addInitScript({
content: `window.rivertyConfig = ${rivertyConfig}`
});

await useOpenInvoicesPage(page, use);
}
});

const useOpenInvoicesPage = async (page: Page, use: any, PageType = OpenInvoicesPage) => {
const openInvoicesPage = new PageType(page);
await openInvoicesPage.goto();
await use(openInvoicesPage);
};

export { test, expect };
25 changes: 25 additions & 0 deletions packages/e2e-playwright/pages/openInvoices/openInvoices.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Locator, Page } from '@playwright/test';
import { OpenInvoices } from '../../models/openInvoices';

class OpenInvoicesPage {
readonly page: Page;

readonly openInvoices: OpenInvoices;
readonly payButton: Locator;

constructor(page: Page) {
this.page = page;
this.openInvoices = new OpenInvoices(page);
this.payButton = page.getByRole('button', { name: /Confirm/i });
}

async goto(url?: string) {
await this.page.goto('http://localhost:3024/openinvoices');
}

async pay() {
await this.payButton.click();
}
}

export { OpenInvoicesPage };
113 changes: 113 additions & 0 deletions packages/e2e-playwright/tests/openInvoices/riverty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { test, expect } from '../../pages/openInvoices/openInvoices.fixture';

import LANG from '../../../lib/src/language/locales/en-US.json';
import { USER_TYPE_DELAY } from '../utils/constants';

const REQUIRED_FIELD = LANG['field.error.required'].replace('%{label}', '');

const BILLING_ADDRESS = LANG['billingAddress'];
const DELIVERY_ADDRESS = LANG['deliveryAddress'];

const EMPTY_POST_CODE = `${REQUIRED_FIELD}${BILLING_ADDRESS} ${LANG['postalCode']}`;
const INVALID_FORMAT_EXPECTS = LANG['invalid.format.expects'].replace(/%{label}|%{format}/g, '');
const INVALID_POST_CODE = `${BILLING_ADDRESS} ${LANG['postalCode']}${INVALID_FORMAT_EXPECTS}99999`;

const expectedSRPanelTexts = [
`${LANG['firstName.invalid']}`,
`${LANG['lastName.invalid']}`,
`${REQUIRED_FIELD}${LANG['dateOfBirth']}`,
`${REQUIRED_FIELD}${LANG['shopperEmail']}`,
`${REQUIRED_FIELD}${LANG['telephoneNumber']}`,
`${REQUIRED_FIELD}${BILLING_ADDRESS} ${LANG['street']}`,
`${REQUIRED_FIELD}${BILLING_ADDRESS} ${LANG['houseNumberOrName']}`,
EMPTY_POST_CODE,
`${REQUIRED_FIELD}${BILLING_ADDRESS} ${LANG['city']}`,
`${REQUIRED_FIELD}${DELIVERY_ADDRESS} ${LANG['deliveryAddress.firstName']}`,
`${REQUIRED_FIELD}${DELIVERY_ADDRESS} ${LANG['deliveryAddress.lastName']}`,
`${REQUIRED_FIELD}${DELIVERY_ADDRESS} ${LANG['street']}`,
`${REQUIRED_FIELD}${DELIVERY_ADDRESS} ${LANG['houseNumberOrName']}`,
`${REQUIRED_FIELD}${DELIVERY_ADDRESS} ${LANG['postalCode']}`,
`${REQUIRED_FIELD}${DELIVERY_ADDRESS} ${LANG['city']}`,
`${LANG['consent.checkbox.invalid']}`
];

test.describe('Test Riverty Component', () => {
test('#1 Test how Riverty component handles SRPanel messages', async ({ openInvoicesPage_riverty }) => {
const { openInvoices, page } = openInvoicesPage_riverty;

await openInvoices.isComponentVisible();

await openInvoices.rivertyDeliveryAddressCheckbox.click();

await openInvoicesPage_riverty.pay();

// Need to wait so that the expected elements can be found by locator.allInnerTexts
await page.waitForTimeout(100);

// Inspect SRPanel errors: all expected errors, in the expected order
await page
.locator('.adyen-checkout-sr-panel__msg')
.allInnerTexts()
.then(retrievedSRPanelTexts => {
// check we have srPanel errors
expect(retrievedSRPanelTexts.length).toBeGreaterThan(0);

// check individual messages
retrievedSRPanelTexts.forEach((retrievedText, index) => {
// KEEP - handy for debugging test
// console.log('\n### riverty.spec:::: retrievedText', retrievedText);
// console.log('### riverty.spec:::: expectedTexts', expectedSRPanelTexts[index]);

expect(retrievedText).toContain(expectedSRPanelTexts[index]);
});
});

// KEEP - handy for debugging test
// await expect(page.getByText('Enter your first name-sr', { exact: true })).toBeVisible();
});

test('#2 Test how Riverty component handles SRPanel messages, specifically a postal code with an invalid format', async ({
openInvoicesPage_riverty
}) => {
const { openInvoices, page } = openInvoicesPage_riverty;

await openInvoices.isComponentVisible();

await openInvoices.rivertyDeliveryAddressCheckbox.click();

expectedSRPanelTexts.splice(7, 1, INVALID_POST_CODE);

await openInvoices.riverty
.getByRole('group', { name: 'Billing address' })
.getByLabel('Postal code')
// .locator('.adyen-checkout__field--postalCode .adyen-checkout__input--postalCode')
.type('3', { delay: USER_TYPE_DELAY });

await openInvoicesPage_riverty.pay(); // first click need to trigger blur event on postCode field
await openInvoicesPage_riverty.pay(); // second click to trigger validation

// Need to wait so that the expected elements can be found by locator.allInnerTexts
await page.waitForTimeout(100);

// Inspect SRPanel errors: all expected errors, in the expected order
await page
.locator('.adyen-checkout-sr-panel__msg')
.allInnerTexts()
.then(retrievedSRPanelTexts => {
// check we have srPanel errors
expect(retrievedSRPanelTexts.length).toBeGreaterThan(0);

// check individual messages
retrievedSRPanelTexts.forEach((retrievedText, index) => {
// KEEP - handy for debugging test
// console.log('\n### riverty.spec:::: retrievedText', retrievedText);
// console.log('### riverty.spec:::: expectedTexts', expectedSRPanelTexts[index]);

expect(retrievedText).toContain(expectedSRPanelTexts[index]);
});
});

// Restore array to start state
expectedSRPanelTexts.splice(7, 1, EMPTY_POST_CODE);
});
});
2 changes: 1 addition & 1 deletion packages/lib/config/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ module.exports = {
'!src/**/types.ts',
'!src/language/locales/**'
],
coveragePathIgnorePatterns: ['node_modules/', 'config/', 'scripts/', 'storybook/', '.storybook/', 'auto/']
coveragePathIgnorePatterns: ['node_modules/', 'config/', 'scripts/', 'storybook/', '.storybook/', 'auto/', '_']
};
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ class Specifications {
* Returns an array with the address schema of the selected country or the default address schema
* Flat version of getAddressSchemaForCountry
* @param country - The selected country
* @param mode - Address schema mode, can be 'full', 'partial' or 'none'
* @returns Array
*/
getAddressSchemaForCountryFlat(country: string): AddressField[] {
Expand Down
32 changes: 21 additions & 11 deletions packages/lib/src/components/internal/OpenInvoice/OpenInvoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export default function OpenInvoice(props: OpenInvoiceProps) {
fieldTypeMappingFn: mapFieldKey
});

const specifications = useMemo(() => new Specifications(), []);
const billingAddressSpecifications = useMemo(() => new Specifications(), []);
const deliveryAddressSpecifications = useMemo(() => new Specifications(props.deliveryAddressSpecification), []);
/** end SR stuff */

const initialActiveFieldsets: OpenInvoiceActiveFieldsets = getInitialActiveFieldsets(visibility, props.data);
Expand Down Expand Up @@ -114,6 +115,7 @@ export default function OpenInvoice(props: OpenInvoiceProps) {
const newData: OpenInvoiceStateData = getActiveFieldsData(activeFieldsets, data);

const DELIVERY_ADDRESS_PREFIX = 'deliveryAddress:';
const BILLING_ADDRESS_PREFIX = 'billingAddress:';

/** Create messages for SRPanel */
// Extract nested errors from the various child components...
Expand All @@ -126,15 +128,17 @@ export default function OpenInvoice(props: OpenInvoiceProps) {
...remainingErrors
} = errors;

// (Differentiate between billingAddress and deliveryAddress errors by adding a prefix to the latter)
// Differentiate between billingAddress and deliveryAddress errors by adding a prefix.
// This also allows overlapping errors e.g. now that addresses can contain first & last name fields
const enhancedBillingAddressErrors = enhanceErrorObjectKeys(extractedBillingAddressErrors, BILLING_ADDRESS_PREFIX);
const enhancedDeliveryAddressErrors = enhanceErrorObjectKeys(extractedDeliveryAddressErrors, DELIVERY_ADDRESS_PREFIX);

// ...and then collate the errors into a new object so that they all sit at top level
const errorsForPanel = {
...(typeof extractedCompanyDetailsErrors === 'object' && extractedCompanyDetailsErrors),
...(typeof extractedPersonalDetailsErrors === 'object' && extractedPersonalDetailsErrors),
...(typeof extractedBankAccountErrors === 'object' && extractedBankAccountErrors),
...(typeof extractedBillingAddressErrors === 'object' && extractedBillingAddressErrors),
...(typeof enhancedBillingAddressErrors === 'object' && enhancedBillingAddressErrors),
...(typeof enhancedDeliveryAddressErrors === 'object' && enhancedDeliveryAddressErrors),
...remainingErrors
};
Expand All @@ -147,25 +151,31 @@ export default function OpenInvoice(props: OpenInvoiceProps) {

const bankAccountLayout = ['holder', 'iban'];

const billingAddressLayout = specifications.getAddressSchemaForCountryFlat(data.billingAddress?.country);
const billingAddressLayout = billingAddressSpecifications.getAddressSchemaForCountryFlat(data.billingAddress?.country);
// In order to sort the address errors the layout entries need to have the same (prefixed) identifier as the errors themselves
const billingAddressLayoutEnhanced = billingAddressLayout.map(item => `${BILLING_ADDRESS_PREFIX}${item}`);

const deliveryAddressLayout = specifications.getAddressSchemaForCountryFlat(data.deliveryAddress?.country);
// In order to sort the deliveryAddress errors the layout entries need to have the same (prefixed) identifier as the errors themselves
const deliveryAddressLayout = deliveryAddressSpecifications.getAddressSchemaForCountryFlat(data.deliveryAddress?.country);
const deliveryAddressLayoutEnhanced = deliveryAddressLayout.map(item => `${DELIVERY_ADDRESS_PREFIX}${item}`);

const fullLayout = companyDetailsLayout.concat(personalDetailLayout, bankAccountLayout, billingAddressLayout, deliveryAddressLayoutEnhanced, [
'consentCheckbox'
]);
const fullLayout = companyDetailsLayout.concat(
personalDetailLayout,
bankAccountLayout,
billingAddressLayoutEnhanced,
deliveryAddressLayoutEnhanced,
['consentCheckbox']
);

// Country specific address labels
const countrySpecificLabels = specifications.getAddressLabelsForCountry(data.billingAddress?.country ?? data.deliveryAddress?.country);
const countrySpecificLabels_billing = billingAddressSpecifications.getAddressLabelsForCountry(data.billingAddress?.country);
const countrySpecificLabels_delivery = deliveryAddressSpecifications.getAddressLabelsForCountry(data.deliveryAddress?.country);

// Set messages: Pass dynamic props (errors, layout etc) to SRPanel via partial
const srPanelResp: SetSRMessagesReturnObject = setSRMessages?.({
errors: errorsForPanel,
isValidating: isValidating.current,
layout: fullLayout,
countrySpecificLabels
countrySpecificLabels: { ...countrySpecificLabels_billing, ...countrySpecificLabels_delivery }
});

// Relates to onBlur errors
Expand Down
Loading

0 comments on commit bf2ce35

Please sign in to comment.