diff --git a/packages/e2e-playwright/app/config/webpack.config.js b/packages/e2e-playwright/app/config/webpack.config.js index 68a565175c..292371fee0 100644 --- a/packages/e2e-playwright/app/config/webpack.config.js +++ b/packages/e2e-playwright/app/config/webpack.config.js @@ -19,8 +19,8 @@ const htmlPages = fs.readdirSync(basePageDir).map(fileName => ({ const htmlPageGenerator = ({ id }, index) => { console.log('htmlPageGenerator', id, index); return new HTMLWebpackPlugin({ - // make card index.html the rest of the pages will have page .html - filename: `${id !== 'Cards' ? `${id.toLowerCase()}/` : ''}index.html`, + // make Dropin index.html the rest of the pages will have page .html + filename: `${id !== 'Dropin' ? `${id.toLowerCase()}/` : ''}index.html`, template: path.join(__dirname, `../src/pages/${id}/${id}.html`), templateParameters: () => ({ htmlWebpackPlugin: { htmlPages } }), inject: 'body', diff --git a/packages/e2e-playwright/models/card.ts b/packages/e2e-playwright/models/card.ts index e1b66caafa..da95d6d54b 100644 --- a/packages/e2e-playwright/models/card.ts +++ b/packages/e2e-playwright/models/card.ts @@ -10,23 +10,39 @@ const CARD_IFRAME_LABEL = LANG['creditCard.encryptedCardNumber.aria.label']; const EXPIRY_DATE_IFRAME_LABEL = LANG['creditCard.encryptedExpiryDate.aria.label']; const CVC_IFRAME_LABEL = LANG['creditCard.encryptedSecurityCode.aria.label']; +const INSTALLMENTS_PAYMENTS = LANG['installments.installments']; +const REVOLVING_PAYMENT = LANG['installments.revolving']; + class Card { + readonly page: Page; + readonly rootElement: Locator; readonly rootElementSelector: string; readonly cardNumberField: Locator; + readonly cardNumberLabelElement: Locator; readonly cardNumberErrorElement: Locator; readonly cardNumberInput: Locator; + readonly brandingIcon: Locator; readonly expiryDateField: Locator; - readonly expiryDateErrorElement: Locator; + readonly expiryDateLabelText: Locator; readonly expiryDateInput: Locator; + readonly expiryDateErrorElement: Locator; readonly cvcField: Locator; + readonly cvcLabelText: Locator; readonly cvcErrorElement: Locator; readonly cvcInput: Locator; + readonly installmentsPaymentLabel: Locator; + readonly revolvingPaymentLabel: Locator; + readonly installmentsDropdown: Locator; + readonly selectorList: Locator; + constructor(page: Page, rootElementSelector = '.adyen-checkout__card-input') { + this.page = page; + this.rootElement = page.locator(rootElementSelector); this.rootElementSelector = rootElementSelector; @@ -34,8 +50,11 @@ class Card { * Card Number elements, in Checkout */ this.cardNumberField = this.rootElement.locator('.adyen-checkout__field--cardNumber'); // Holder + this.cardNumberLabelElement = this.cardNumberField.locator('.adyen-checkout__label'); this.cardNumberErrorElement = this.cardNumberField.locator('.adyen-checkout__error-text'); + this.brandingIcon = this.rootElement.locator('.adyen-checkout__card__cardNumber__brandIcon'); + /** * Card Number elements, in iframe */ @@ -46,7 +65,9 @@ class Card { * Expiry Date elements, in Checkout */ this.expiryDateField = this.rootElement.locator('.adyen-checkout__field--expiryDate'); // Holder + this.expiryDateLabelText = this.expiryDateField.locator('.adyen-checkout__label__text'); this.expiryDateErrorElement = this.expiryDateField.locator('.adyen-checkout__error-text'); // Related error element + // Related error element /** * Expiry Date elements, in iframe @@ -58,6 +79,7 @@ class Card { * Security code elements, in Checkout */ this.cvcField = this.rootElement.locator('.adyen-checkout__field--securityCode'); // Holder + this.cvcLabelText = this.cvcField.locator('.adyen-checkout__label__text'); this.cvcErrorElement = this.cvcField.locator('.adyen-checkout__error-text'); // Related error element /** @@ -65,6 +87,14 @@ class Card { */ const cvcIframe = this.rootElement.frameLocator(`[title="${CVC_IFRAME_TITLE}"]`); this.cvcInput = cvcIframe.locator(`input[aria-label="${CVC_IFRAME_LABEL}"]`); + + /** + * Installments related elements + */ + this.installmentsPaymentLabel = this.rootElement.getByText(INSTALLMENTS_PAYMENTS); + this.revolvingPaymentLabel = this.rootElement.getByText(REVOLVING_PAYMENT); + this.installmentsDropdown = this.rootElement.locator('.adyen-checkout__dropdown__button'); + this.selectorList = this.rootElement.getByRole('listbox'); } async isComponentVisible() { @@ -81,6 +111,14 @@ class Card { await this.cardNumberInput.clear(); } + async deleteExpiryDate() { + await this.expiryDateInput.clear(); + } + + async deleteCvc() { + await this.cvcInput.clear(); + } + async typeExpiryDate(expiryDate: string) { await this.expiryDateInput.type(expiryDate, { delay: USER_TYPE_DELAY }); } @@ -88,6 +126,11 @@ class Card { async typeCvc(cvc: string) { await this.cvcInput.type(cvc, { delay: USER_TYPE_DELAY }); } + + async selectListItem(who: string) { + const listItem = this.selectorList.locator(`#listItem-${who}`); + return listItem; + } } export { Card }; diff --git a/packages/e2e-playwright/models/issuer-list.ts b/packages/e2e-playwright/models/issuer-list.ts index 90f2a6f8ec..8574bf3e73 100644 --- a/packages/e2e-playwright/models/issuer-list.ts +++ b/packages/e2e-playwright/models/issuer-list.ts @@ -2,7 +2,6 @@ import { Locator, Page } from '@playwright/test'; import { USER_TYPE_DELAY } from '../tests/utils/constants'; const SELECTOR_DELAY = 300; -const KEYBOARD_DELAY = 300; class IssuerList { readonly rootElement: Locator; @@ -43,18 +42,6 @@ class IssuerList { async typeOnSelectorField(filter: string) { await this.selectorCombobox.type(filter, { delay: USER_TYPE_DELAY }); } - - async pressKeyboardToNextItem() { - await this.page.keyboard.press('ArrowDown', { delay: KEYBOARD_DELAY }); - } - - async pressKeyboardToPreviousItem() { - await this.page.keyboard.press('ArrowDown', { delay: KEYBOARD_DELAY }); - } - - async pressKeyboardToSelectItem() { - await this.page.keyboard.press('Enter', { delay: KEYBOARD_DELAY }); - } } export { IssuerList }; diff --git a/packages/e2e-playwright/pages/cards/card.avs.page.ts b/packages/e2e-playwright/pages/cards/card.avs.page.ts index ff2d31dd40..86c07e81ea 100644 --- a/packages/e2e-playwright/pages/cards/card.avs.page.ts +++ b/packages/e2e-playwright/pages/cards/card.avs.page.ts @@ -14,7 +14,7 @@ class CardAvsPage { } async goto(url?: string) { - await this.page.goto('http://localhost:3024/'); + await this.page.goto('http://localhost:3024/cards'); } } diff --git a/packages/e2e-playwright/pages/cards/card.fixture.ts b/packages/e2e-playwright/pages/cards/card.fixture.ts index 8bf38d6efb..98cbc94807 100644 --- a/packages/e2e-playwright/pages/cards/card.fixture.ts +++ b/packages/e2e-playwright/pages/cards/card.fixture.ts @@ -1,4 +1,4 @@ -import { test as base, expect } from '@playwright/test'; +import { test as base, expect, Page } from '@playwright/test'; import { CardPage } from './card.page'; import { CardAvsPage } from './card.avs.page'; import { binLookupMock } from '../../mocks/binLookup/binLookup.mock'; @@ -8,13 +8,14 @@ type Fixture = { cardPage: CardPage; cardAvsPage: CardAvsPage; cardLegacyInputModePage: CardPage; + cardBrandingPage: CardPage; + cardExpiryDatePoliciesPage: CardPage; + cardInstallmentsPage: CardPage; }; const test = base.extend({ cardPage: async ({ page }, use) => { - const cardPage = new CardPage(page); - await cardPage.goto(); - await use(cardPage); + await useCardPage(page, use); }, cardAvsPage: async ({ page }, use) => { @@ -24,9 +25,8 @@ const test = base.extend({ "window.cardConfig = { billingAddressRequired: true, billingAddressRequiredFields: ['street', 'houseNumberOrName', 'postalCode', 'city']};" }); - const cardAvsPage = new CardAvsPage(page); - await cardAvsPage.goto(); - await use(cardAvsPage); + // @ts-ignore + await useCardPage(page, use, CardAvsPage); }, cardLegacyInputModePage: async ({ page }, use) => { @@ -34,10 +34,57 @@ const test = base.extend({ content: 'window.cardConfig = { legacyInputMode: true}' }); - const cardPage = new CardPage(page); - await cardPage.goto(); - await use(cardPage); + await useCardPage(page, use); + }, + + cardBrandingPage: async ({ page }, use) => { + const brands = JSON.stringify({ brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc'] }); + await page.addInitScript({ + content: `window.cardConfig = ${brands}` + }); + + await useCardPage(page, use); + }, + + cardExpiryDatePoliciesPage: async ({ page }, use) => { + const mainConfig = JSON.stringify({ + srConfig: { + moveFocus: false + } + }); + await page.addInitScript({ + content: `window.mainConfiguration = ${mainConfig}` + }); + + const brands = JSON.stringify({ brands: ['mc', 'visa', 'amex', 'synchrony_plcc'] }); + await page.addInitScript({ + content: `window.cardConfig = ${brands}` + }); + + await useCardPage(page, use); + }, + + cardInstallmentsPage: async ({ page }, use) => { + const installmentsConfig = JSON.stringify({ + installmentOptions: { + mc: { + values: [1, 2, 3], + plans: ['regular', 'revolving'] + } + } + }); + await page.addInitScript({ + content: `window.cardConfig = ${installmentsConfig}` + }); + + await useCardPage(page, use); } }); +const useCardPage = async (page: Page, use: any, PageType = CardPage) => { + const cardPage = new PageType(page); + await cardPage.goto(); + await use(cardPage); +}; + export { test, expect }; diff --git a/packages/e2e-playwright/pages/cards/card.page.ts b/packages/e2e-playwright/pages/cards/card.page.ts index f0e5ade2b0..7ce9955314 100644 --- a/packages/e2e-playwright/pages/cards/card.page.ts +++ b/packages/e2e-playwright/pages/cards/card.page.ts @@ -14,7 +14,7 @@ class CardPage { } async goto(url?: string) { - await this.page.goto('http://localhost:3024/'); + await this.page.goto('http://localhost:3024/cards'); } async pay() { diff --git a/packages/e2e-playwright/pages/issuerList/issuer-list.page.ts b/packages/e2e-playwright/pages/issuerList/issuer-list.page.ts index ca03835f00..654c4dfb74 100644 --- a/packages/e2e-playwright/pages/issuerList/issuer-list.page.ts +++ b/packages/e2e-playwright/pages/issuerList/issuer-list.page.ts @@ -2,7 +2,7 @@ import { Page } from '@playwright/test'; import { IssuerList } from '../../models/issuer-list'; class IssuerListPage { - private readonly page: Page; + readonly page: Page; public readonly issuerList: IssuerList; diff --git a/packages/e2e-playwright/tests/card/branding/card.branding.spec.ts b/packages/e2e-playwright/tests/card/branding/card.branding.spec.ts new file mode 100644 index 0000000000..527366d4e9 --- /dev/null +++ b/packages/e2e-playwright/tests/card/branding/card.branding.spec.ts @@ -0,0 +1,163 @@ +import { test, expect } from '../../../pages/cards/card.fixture'; +import { MAESTRO_CARD, TEST_CVC_VALUE, TEST_DATE_VALUE } from '../../utils/constants'; +import LANG from '../../../../lib/src/language/locales/en-US.json'; + +const CVC_LABEL = LANG['creditCard.cvcField.title']; +const CVC_LABEL_OPTIONAL = LANG['creditCard.cvcField.title.optional']; + +test.describe('Testing branding - especially regarding optional and hidden cvc fields', () => { + test( + '#1 Test for generic card icon & required CVC field' + + 'then enter number recognised as maestro (by our regEx), ' + + 'then add digit so it will be seen as a bcmc card (by our regEx) ,' + + 'then delete number (back to generic card)', + async ({ cardBrandingPage }) => { + const { card, page } = cardBrandingPage; + + await card.isComponentVisible(); + + // generic card + let brandingIconSrc = await card.brandingIcon.getAttribute('src'); + await expect(brandingIconSrc).toContain('nocard.svg'); + + // visible & required cvc field + await expect(card.cvcField).toBeVisible(); + await expect(card.cvcField).toHaveClass(/adyen-checkout__field__cvc/); // Note: "relaxed" regular expression to detect one class amongst several that are set on the element + await expect(card.cvcField).not.toHaveClass(/adyen-checkout__field__cvc--optional/); + + // with regular text + await expect(card.cvcLabelText).toHaveText(CVC_LABEL); + + // Partially fill card field with digits that will be recognised as maestro + await card.typeCardNumber('670'); + + // maestro card icon + brandingIconSrc = await card.brandingIcon.getAttribute('src'); + await expect(brandingIconSrc).toContain('maestro.svg'); + + // with "optional" text + await expect(card.cvcLabelText).toHaveText(CVC_LABEL_OPTIONAL); + // and optional class + await expect(card.cvcField).toHaveClass(/adyen-checkout__field__cvc--optional/); + + // Add digit so card is recognised as bcmc + await card.cardNumberInput.press('End'); /** NOTE: how to add text at end */ + await card.typeCardNumber('3'); + + // bcmc card icon + brandingIconSrc = await card.brandingIcon.getAttribute('src'); + await expect(brandingIconSrc).toContain('bcmc.svg'); + + // hidden cvc field + await expect(card.cvcField).not.toBeVisible(); + + // Delete number + await card.deleteCardNumber(); + + // Card is reset + brandingIconSrc = await card.brandingIcon.getAttribute('src'); + await expect(brandingIconSrc).toContain('nocard.svg'); + + // Visible cvc field + await expect(card.cvcField).toBeVisible(); + + // with regular text + await expect(card.cvcLabelText).toHaveText(CVC_LABEL); + + // and required cvc field + await expect(card.cvcField).toHaveClass(/adyen-checkout__field__cvc/); + await expect(card.cvcField).not.toHaveClass(/adyen-checkout__field__cvc--optional/); + } + ); + + test( + '#2 Test card is valid with maestro details (cvc optional)' + 'then test it is invalid (& brand reset) when number deleted', + async ({ cardBrandingPage }) => { + const { card, page } = cardBrandingPage; + + await card.isComponentVisible(); + + // Maestro + await card.typeCardNumber(MAESTRO_CARD); + await card.typeExpiryDate(TEST_DATE_VALUE); + + // maestro card icon + let brandingIconSrc = await card.brandingIcon.getAttribute('src'); + await expect(brandingIconSrc).toContain('maestro.svg'); + + // with "optional" text + await expect(card.cvcLabelText).toHaveText(CVC_LABEL_OPTIONAL); + // and optional class + await expect(card.cvcField).toHaveClass(/adyen-checkout__field__cvc--optional/); + + // Is valid + let cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + + await card.typeCvc(TEST_CVC_VALUE); + + // Headless test seems to need time for UI reset to register on state + await page.waitForTimeout(500); + + // Is valid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + + // Delete number + await card.deleteCardNumber(); + + // Card is reset to generic card + brandingIconSrc = await card.brandingIcon.getAttribute('src'); + await expect(brandingIconSrc).toContain('nocard.svg'); + + // Headless test seems to need time for UI change to register on state + await page.waitForTimeout(500); + + // Is not valid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(false); + } + ); + + test( + '#3 Test card is invalid if filled with maestro details but optional cvc field is left "in error" (partially filled)' + + 'then test it is valid if cvc completed' + + 'then test it is valid if cvc deleted', + async ({ cardBrandingPage }) => { + const { card, page } = cardBrandingPage; + + await card.isComponentVisible(); + + // Maestro + await card.typeCardNumber(MAESTRO_CARD); + await card.typeExpiryDate(TEST_DATE_VALUE); + + // Partial cvc + await card.typeCvc('73'); + + // Force blur event to fire + await card.cardNumberLabelElement.click(); + + // Wait for UI to render + await page.waitForTimeout(300); + + // Is not valid + let cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(false); + + // Complete cvc + await card.cvcInput.press('End'); /** NOTE: how to add text at end */ + await card.typeCvc('7'); + + // Is valid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + + await card.deleteCvc(); + + // Is valid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + } + ); +}); diff --git a/packages/e2e-playwright/tests/card/card.spec.ts b/packages/e2e-playwright/tests/card/card.spec.ts index 915268ee8f..f5b5a6d58a 100644 --- a/packages/e2e-playwright/tests/card/card.spec.ts +++ b/packages/e2e-playwright/tests/card/card.spec.ts @@ -6,45 +6,47 @@ const PAN_ERROR_NOT_VALID = LANG['error.va.sf-cc-num.01']; const PAN_ERROR_EMPTY = LANG['error.va.sf-cc-num.02']; const PAN_ERROR_NOT_COMPLETE = LANG['error.va.sf-cc-num.04']; -test('#1 Should fill in card fields and complete the payment', async ({ cardPage }) => { - const { card, page } = cardPage; +test.describe('Card - Standard flow', () => { + test('#1 Should fill in card fields and complete the payment', async ({ cardPage }) => { + const { card, page } = cardPage; - await card.typeCardNumber(REGULAR_TEST_CARD); - await card.typeCvc(TEST_CVC_VALUE); - await card.typeExpiryDate(TEST_DATE_VALUE); + await card.typeCardNumber(REGULAR_TEST_CARD); + await card.typeCvc(TEST_CVC_VALUE); + await card.typeExpiryDate(TEST_DATE_VALUE); - await cardPage.pay(); + await cardPage.pay(); - await expect(page.locator('#result-message')).toHaveText('Authorised'); -}); + await expect(page.locator('#result-message')).toHaveText('Authorised'); + }); -test('#2 PAN that consists of the same digit (but passes luhn) causes an error', async ({ cardPage }) => { - const { card, page } = cardPage; + test('#2 PAN that consists of the same digit (but passes luhn) causes an error', async ({ cardPage }) => { + const { card, page } = cardPage; - await card.typeCardNumber('3333 3333 3333 3333 3333'); + await card.typeCardNumber('3333 3333 3333 3333 3333'); - await cardPage.pay(); + await cardPage.pay(); - await expect(card.cardNumberErrorElement).toBeVisible(); - await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_VALID); -}); + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_VALID); + }); -test('#3 Clicking pay button with an empty PAN causes an "empty" error on the PAN field', async ({ cardPage }) => { - const { card, page } = cardPage; + test('#3 Clicking pay button with an empty PAN causes an "empty" error on the PAN field', async ({ cardPage }) => { + const { card, page } = cardPage; - await cardPage.pay(); + await cardPage.pay(); - await expect(card.cardNumberErrorElement).toBeVisible(); - await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_EMPTY); -}); + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_EMPTY); + }); -test('#4 PAN that consists of only 1 digit causes a "wrong length" error ', async ({ cardPage }) => { - const { card, page } = cardPage; + test('#4 PAN that consists of only 1 digit causes a "wrong length" error ', async ({ cardPage }) => { + const { card, page } = cardPage; - await card.typeCardNumber('4'); + await card.typeCardNumber('4'); - await cardPage.pay(); + await cardPage.pay(); - await expect(card.cardNumberErrorElement).toBeVisible(); - await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_COMPLETE); + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_COMPLETE); + }); }); diff --git a/packages/e2e-playwright/tests/card/expiryDate/card.expiryDatePolicies.hidden.spec.ts b/packages/e2e-playwright/tests/card/expiryDate/card.expiryDatePolicies.hidden.spec.ts new file mode 100644 index 0000000000..abcaae00a4 --- /dev/null +++ b/packages/e2e-playwright/tests/card/expiryDate/card.expiryDatePolicies.hidden.spec.ts @@ -0,0 +1,124 @@ +import { test, expect } from '../../../pages/cards/card.fixture'; +import { SYNCHRONY_PLCC_NO_DATE, TEST_CVC_VALUE, ENCRYPTED_CARD_NUMBER, ENCRYPTED_EXPIRY_DATE, ENCRYPTED_SECURITY_CODE } from '../../utils/constants'; +import LANG from '../../../../lib/src/language/locales/en-US.json'; + +const PAN_ERROR = LANG['error.va.sf-cc-num.02']; +const DATE_INVALID_ERROR = LANG['error.va.sf-cc-dat.01']; +const DATE_EMPTY_ERROR = LANG['error.va.sf-cc-dat.04']; +const CVC_ERROR = LANG['error.va.sf-cc-cvc.01']; + +test.describe('Test how Card Component handles hidden expiryDate policy', () => { + test('#1 how UI & state respond', async ({ cardExpiryDatePoliciesPage }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await card.isComponentVisible(); + + // Fill number to provoke binLookup response + await card.typeCardNumber(SYNCHRONY_PLCC_NO_DATE); + + // UI reflects that binLookup says expiryDate is hidden + await expect(card.expiryDateField).not.toBeVisible(); + + await card.typeCvc(TEST_CVC_VALUE); + + // Card seen as valid + let cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + + // Clear number + await card.deleteCardNumber(); + + // UI is reset + await expect(card.expiryDateField).toBeVisible(); + + // Card seen as invalid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(false); + }); + + test('#2 Validating fields first should see visible errors and then entering PAN should see errors cleared from state', async ({ + cardExpiryDatePoliciesPage + }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await card.isComponentVisible(); + + await cardExpiryDatePoliciesPage.pay(); + + // Expect errors in UI + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR); + await expect(card.expiryDateErrorElement).toBeVisible(); + await expect(card.expiryDateErrorElement).toHaveText(DATE_EMPTY_ERROR); + await expect(card.cvcErrorElement).toBeVisible(); + await expect(card.cvcErrorElement).toHaveText(CVC_ERROR); + + // Expect errors in state + let cardErrors: any = await page.evaluate('window.card.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_EXPIRY_DATE]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_SECURITY_CODE]).not.toBe(undefined); + + // Fill number to provoke binLookup response + await card.typeCardNumber(SYNCHRONY_PLCC_NO_DATE); + + // Headless test seems to need time for UI reset to register on state + await page.waitForTimeout(500); + + // Expect card & date errors to be cleared - since the fields were in error because they were empty + // but now the PAN field is filled and the date field is hidden & so these fields have re-rendered and updated state + cardErrors = await page.evaluate('window.card.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).toBe(null); + await expect(cardErrors[ENCRYPTED_EXPIRY_DATE]).toBe(null); + + // The cvc field should remain in error since it is required under this card brand's BIN + await expect(cardErrors[ENCRYPTED_SECURITY_CODE]).not.toBe(null); + }); + + test('#3 Hidden date field in error does not stop card becoming valid', async ({ cardExpiryDatePoliciesPage }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await card.isComponentVisible(); + + // Card out of date + await card.typeExpiryDate('12/90'); + + // Expect error in UI + await expect(card.expiryDateField).toBeVisible(); + await expect(card.expiryDateErrorElement).toBeVisible(); + await expect(card.expiryDateErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Force blur event to fire on date field + await card.cardNumberLabelElement.click(); + + // Fill number to provoke binLookup response + await card.typeCardNumber(SYNCHRONY_PLCC_NO_DATE); + + // UI reflects that binLookup says expiryDate is hidden + await expect(card.expiryDateField).not.toBeVisible(); + await expect(card.expiryDateErrorElement).not.toBeVisible(); + + // complete fields + await card.typeCvc(TEST_CVC_VALUE); + + // Card seen as valid (despite date field technically being in error) + let cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + + // Expect errors in state to remain + let cardErrors: any = await page.evaluate('window.card.state.errors'); + await expect(cardErrors[ENCRYPTED_EXPIRY_DATE]).not.toBe(undefined); + + // Clear number + await card.deleteCardNumber(); + + // Errors in UI visible again + await expect(card.expiryDateField).toBeVisible(); + await expect(card.expiryDateErrorElement).toBeVisible(); + await expect(card.expiryDateErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Card is not valid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(false); + }); +}); diff --git a/packages/e2e-playwright/tests/card/expiryDate/card.expiryDatePolicies.optional.spec.ts b/packages/e2e-playwright/tests/card/expiryDate/card.expiryDatePolicies.optional.spec.ts new file mode 100644 index 0000000000..7895e1d722 --- /dev/null +++ b/packages/e2e-playwright/tests/card/expiryDate/card.expiryDatePolicies.optional.spec.ts @@ -0,0 +1,176 @@ +import { test, expect } from '../../../pages/cards/card.fixture'; +import { ENCRYPTED_CARD_NUMBER, ENCRYPTED_EXPIRY_DATE, ENCRYPTED_SECURITY_CODE, REGULAR_TEST_CARD } from '../../utils/constants'; +import { binLookupMock } from '../../../mocks/binLookup/binLookup.mock'; +import { optionalDateAndCvcMock } from '../../../mocks/binLookup/binLookup.data'; +import LANG from '../../../../lib/src/language/locales/en-US.json'; + +const DATE_LABEL = LANG['creditCard.expiryDateField.title']; +const CVC_LABEL = LANG['creditCard.cvcField.title']; +const CVC_LABEL_OPTIONAL = LANG['creditCard.cvcField.title.optional']; +const OPTIONAL = LANG['field.title.optional']; +const PAN_ERROR = LANG['error.va.sf-cc-num.02']; +const DATE_INVALID_ERROR = LANG['error.va.sf-cc-dat.01']; +const DATE_EMPTY_ERROR = LANG['error.va.sf-cc-dat.04']; +const CVC_ERROR = LANG['error.va.sf-cc-cvc.01']; + +test.describe('Test how Card Component handles optional expiryDate policy', () => { + test('#1 how UI & state respond', async ({ cardExpiryDatePoliciesPage }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isComponentVisible(); + + // Regular date label + await expect(card.expiryDateLabelText).toHaveText(DATE_LABEL); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // UI reflects that binLookup says expiryDate is optional + await expect(card.expiryDateLabelText).toHaveText(`${DATE_LABEL} ${OPTIONAL}`); + + // ...and cvc is optional too + await expect(card.cvcLabelText).toHaveText(CVC_LABEL_OPTIONAL); + + // Card seen as valid + let cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + + // Clear number and see UI & state reset + await card.deleteCardNumber(); + + // date and cvc labels don't contain 'optional' + await expect(card.expiryDateLabelText).toHaveText(DATE_LABEL); + await expect(card.cvcLabelText).toHaveText(CVC_LABEL); + + // Headless test seems to need time for UI reset to register on state + await page.waitForTimeout(500); + + // Card seen as invalid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(false); + }); + + test('#2 how securedFields responds', async ({ cardExpiryDatePoliciesPage }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isComponentVisible(); + + // Expect iframe's expiryDate (& cvc) input fields to have an aria-required attr set to true + let dateAriaRequired = await card.expiryDateInput.getAttribute('aria-required'); + await expect(dateAriaRequired).toEqual('true'); + + let cvcAriaRequired = await card.cvcInput.getAttribute('aria-required'); + await expect(cvcAriaRequired).toEqual('true'); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // Expect iframe's expiryDate (& cvc) input fields to have an aria-required attr set to false + dateAriaRequired = await card.expiryDateInput.getAttribute('aria-required'); + await expect(dateAriaRequired).toEqual('false'); + + cvcAriaRequired = await card.cvcInput.getAttribute('aria-required'); + await expect(cvcAriaRequired).toEqual('false'); + + // Clear number and see SF's aria-required reset + await card.deleteCardNumber(); + + dateAriaRequired = await card.expiryDateInput.getAttribute('aria-required'); + await expect(dateAriaRequired).toEqual('true'); + + cvcAriaRequired = await card.cvcInput.getAttribute('aria-required'); + await expect(cvcAriaRequired).toEqual('true'); + }); + + test('#3 validating fields first and then entering PAN should see errors cleared from both UI & state', async ({ + cardExpiryDatePoliciesPage + }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isComponentVisible(); + + // press pay to generate errors + await cardExpiryDatePoliciesPage.pay(); + + // Expect errors in UI + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR); + await expect(card.expiryDateErrorElement).toBeVisible(); + await expect(card.expiryDateErrorElement).toHaveText(DATE_EMPTY_ERROR); + await expect(card.cvcErrorElement).toBeVisible(); + await expect(card.cvcErrorElement).toHaveText(CVC_ERROR); + + // Expect errors in state + let cardErrors: any = await page.evaluate('window.card.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_EXPIRY_DATE]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_SECURITY_CODE]).not.toBe(undefined); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // await page.waitForTimeout(5000); + + // Expect errors to be cleared - since the fields were in error because they were empty + // but now the PAN field is filled and the date & cvc field are optional & the fields have re-rendered and updated state + + // No errors in UI + await expect(card.cardNumberErrorElement).not.toBeVisible(); + await expect(card.expiryDateErrorElement).not.toBeVisible(); + await expect(card.cvcErrorElement).not.toBeVisible(); + + // No errors in state + cardErrors = await page.evaluate('window.card.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).toBe(null); + await expect(cardErrors[ENCRYPTED_EXPIRY_DATE]).toBe(null); + await expect(cardErrors[ENCRYPTED_SECURITY_CODE]).toBe(null); + }); + + test('#4 date field in error DOES stop card becoming valid', async ({ cardExpiryDatePoliciesPage }) => { + const { card, page } = cardExpiryDatePoliciesPage; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isComponentVisible(); + + // Card out of date + await card.typeExpiryDate('12/90'); + + // Expect error in UI + await expect(card.expiryDateErrorElement).toBeVisible(); + await expect(card.expiryDateErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Force blur event to fire on date field + await card.cardNumberLabelElement.click(); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // UI reflects that binLookup says expiryDate is optional + await expect(card.expiryDateLabelText).toHaveText(`${DATE_LABEL} ${OPTIONAL}`); + + // Visual errors persist in UI + await expect(card.expiryDateErrorElement).toBeVisible(); + await expect(card.expiryDateErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Card seen as invalid + let cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(false); + + // Delete erroneous date + await card.deleteExpiryDate(); + + // Headless test seems to need time for UI reset to register on state + await page.waitForTimeout(500); + + // Card now seen as valid + cardValid = await page.evaluate('window.card.isValid'); + await expect(cardValid).toEqual(true); + }); +}); diff --git a/packages/e2e-playwright/tests/card/installments/card.installments.spec.ts b/packages/e2e-playwright/tests/card/installments/card.installments.spec.ts new file mode 100644 index 0000000000..4ca0ac8f0e --- /dev/null +++ b/packages/e2e-playwright/tests/card/installments/card.installments.spec.ts @@ -0,0 +1,87 @@ +import { test, expect } from '../../../pages/cards/card.fixture'; +import { pressKeyboardToNextItem } from '../../utils/keyboard'; +import { REGULAR_TEST_CARD, TEST_CVC_VALUE, TEST_DATE_VALUE } from '../../utils/constants'; + +test.describe('Cards (Installments)', () => { + test('#1 should not add installments property to payload if one-time payment is selected (default selection)', async ({ + cardInstallmentsPage + }) => { + const { card, page } = cardInstallmentsPage; + + await card.isComponentVisible(); + + await card.typeCardNumber(REGULAR_TEST_CARD); + await card.typeExpiryDate(TEST_DATE_VALUE); + await card.typeCvc(TEST_CVC_VALUE); + + // Inspect card.data + const paymentDataInstallments: any = await page.evaluate('window.card.data.installments'); + await expect(paymentDataInstallments).toBe(undefined); + }); + + test('#2 should not add installments property to payload if 1x installment is selected', async ({ cardInstallmentsPage }) => { + const { card, page } = cardInstallmentsPage; + + await card.isComponentVisible(); + + await card.typeCardNumber(REGULAR_TEST_CARD); + await card.typeExpiryDate(TEST_DATE_VALUE); + await card.typeCvc(TEST_CVC_VALUE); + + // Select option + await card.installmentsPaymentLabel.click(); + + // Inspect card.data + const paymentDataInstallments: any = await page.evaluate('window.card.data.installments'); + await expect(paymentDataInstallments).toBe(undefined); + }); + + test('#3 should add revolving plan to payload if selected', async ({ cardInstallmentsPage }) => { + const { card, page } = cardInstallmentsPage; + + await card.isComponentVisible(); + + await card.typeCardNumber(REGULAR_TEST_CARD); + await card.typeExpiryDate(TEST_DATE_VALUE); + await card.typeCvc(TEST_CVC_VALUE); + + // Select option + await card.revolvingPaymentLabel.click(); + + // Headless test seems to need time for click to register on state + await page.waitForTimeout(500); + + // Inspect card.data + const paymentDataInstallments: any = await page.evaluate('window.card.data.installments'); + await expect(paymentDataInstallments.value).toEqual(1); + await expect(paymentDataInstallments.plan).toEqual('revolving'); + }); + + test('#4 should add installments value property if regular installment > 1 is selected', async ({ cardInstallmentsPage }) => { + const { card, page } = cardInstallmentsPage; + + await card.isComponentVisible(); + + await card.typeCardNumber(REGULAR_TEST_CARD); + await card.typeExpiryDate(TEST_DATE_VALUE); + await card.typeCvc(TEST_CVC_VALUE); + + // Select option + await card.installmentsPaymentLabel.click(); + + await card.installmentsDropdown.click(); + await pressKeyboardToNextItem(page); + await pressKeyboardToNextItem(page); + // await pressKeyboardToSelectItem(page); + + const listItem = await card.selectListItem('2'); + await listItem.click(); + + // Headless test seems to need time for UI interaction to register on state + await page.waitForTimeout(500); + + // Inspect card.data + const paymentDataInstallments: any = await page.evaluate('window.card.data.installments'); + await expect(paymentDataInstallments.value).toEqual(2); + }); +}); diff --git a/packages/e2e-playwright/tests/issuerList/issue-list.spec.ts b/packages/e2e-playwright/tests/issuerList/issue-list.spec.ts deleted file mode 100644 index cce6a5ba1b..0000000000 --- a/packages/e2e-playwright/tests/issuerList/issue-list.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { test, expect } from '../../pages/issuerList/issuer-list.fixture'; - -test('should select highlighted issuer and update pay button label', async ({ issuerListPage }) => { - const { issuerList } = issuerListPage; - - await issuerList.selectHighlightedIssuer('Test Issuer 5'); - await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 5'); - - await issuerList.selectHighlightedIssuer('Test Issuer 4'); - await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 4'); - - await expect(issuerList.highlightedIssuerButtonGroup.getByRole('button', { pressed: true })).toHaveText('Test Issuer 4'); - await expect(issuerList.selectorCombobox).toHaveValue('Select your bank'); -}); - -test('it should be able to filter and select using the keyboard', async ({ issuerListPage }) => { - const { issuerList } = issuerListPage; - - await expect(issuerList.submitButton).toHaveText('Continue'); - - await issuerList.clickOnSelector(); - await expect(issuerList.selectorList).toContainText('SNS'); - - await issuerList.typeOnSelectorField('Test'); - await expect(issuerList.selectorList).not.toContainText('SNS'); - - await issuerList.pressKeyboardToNextItem(); - await issuerList.pressKeyboardToNextItem(); - await issuerList.pressKeyboardToSelectItem(); - - await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 5'); - - // 1st press opens the dropdown - await issuerList.pressKeyboardToNextItem(); - // 2nd selects next items - await issuerList.pressKeyboardToNextItem(); - await issuerList.pressKeyboardToSelectItem(); - - await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 4'); -}); - -test('it should load a default when pressing enter', async ({ issuerListPage }) => { - const { issuerList } = issuerListPage; - - await issuerList.clickOnSelector(); - await issuerList.typeOnSelectorField('Test'); - await issuerList.pressKeyboardToSelectItem(); - - await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer'); -}); diff --git a/packages/e2e-playwright/tests/issuerList/issuer-list.spec.ts b/packages/e2e-playwright/tests/issuerList/issuer-list.spec.ts new file mode 100644 index 0000000000..3f48ae0743 --- /dev/null +++ b/packages/e2e-playwright/tests/issuerList/issuer-list.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from '../../pages/issuerList/issuer-list.fixture'; +import { pressKeyboardToNextItem, pressKeyboardToSelectItem } from '../utils/keyboard'; + +test.describe('Issuer List', () => { + test('should select highlighted issuer and update pay button label', async ({ issuerListPage }) => { + const { issuerList } = issuerListPage; + + await issuerList.selectHighlightedIssuer('Test Issuer 5'); + await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 5'); + + await issuerList.selectHighlightedIssuer('Test Issuer 4'); + await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 4'); + + await expect(issuerList.highlightedIssuerButtonGroup.getByRole('button', { pressed: true })).toHaveText('Test Issuer 4'); + }); + + test('it should be able to filter and select using the keyboard', async ({ issuerListPage }) => { + const { issuerList, page } = issuerListPage; + + await expect(issuerList.submitButton).toHaveText('Continue'); + + await issuerList.clickOnSelector(); + await expect(issuerList.selectorList).toContainText('SNS'); + + await issuerList.typeOnSelectorField('Test'); + await expect(issuerList.selectorList).not.toContainText('SNS'); + + await pressKeyboardToNextItem(page); + await pressKeyboardToNextItem(page); + await pressKeyboardToSelectItem(page); + + await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 5'); + + // 1st press opens the dropdown + await pressKeyboardToNextItem(page); + // 2nd selects next items + await pressKeyboardToNextItem(page); + await pressKeyboardToSelectItem(page); + + await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer 4'); + }); + + test('it should load a default when pressing enter', async ({ issuerListPage }) => { + const { issuerList, page } = issuerListPage; + + await issuerList.clickOnSelector(); + await issuerList.typeOnSelectorField('Test'); + await pressKeyboardToSelectItem(page); + + await expect(issuerList.submitButton).toHaveText('Continue to Test Issuer'); + }); +}); diff --git a/packages/e2e-playwright/tests/utils/constants.ts b/packages/e2e-playwright/tests/utils/constants.ts index e57f34aa13..f2f25375a5 100644 --- a/packages/e2e-playwright/tests/utils/constants.ts +++ b/packages/e2e-playwright/tests/utils/constants.ts @@ -1,6 +1,13 @@ export const BIN_LOOKUP_VERSION = 'v3'; export const REGULAR_TEST_CARD = '5500000000000004'; +export const AMEX_CARD = '370000000000002'; + +export const MAESTRO_CARD = '5000550000000029'; + +export const SYNCHRONY_PLCC_NO_LUHN = '6044100018023838'; // also, no date +export const SYNCHRONY_PLCC_WITH_LUHN = '6044141000018769'; // also, no date +export const SYNCHRONY_PLCC_NO_DATE = SYNCHRONY_PLCC_NO_LUHN; // no date export const TEST_DATE_VALUE = '03/30'; export const TEST_CVC_VALUE = '737'; @@ -8,9 +15,16 @@ export const TEST_CVC_VALUE = '737'; export const BIN_LOOKUP_URL = `https://checkoutshopper-test.adyen.com/checkoutshopper/${BIN_LOOKUP_VERSION}/bin/binLookup?token=${process.env.CLIENT_KEY}`; export const USER_TYPE_DELAY = 150; +export const KEYBOARD_DELAY = 300; export const SESSION_DATA_MOCK = 'AAAADEMOSESSIONDATAAAA'; export const ORDER_DATA_MOCK = 'BBBBORDERDATABBBB'; export const SESSION_RESULT_MOCK = 'CCCCSESIONRESULTCCCC'; + +export const ENCRYPTED_CARD_NUMBER = 'encryptedCardNumber'; +export const ENCRYPTED_EXPIRY_DATE = 'encryptedExpiryDate'; +export const ENCRYPTED_EXPIRY_MONTH = 'encryptedExpiryMonth'; +export const ENCRYPTED_EXPIRY_YEAR = 'encryptedExpiryYear'; +export const ENCRYPTED_SECURITY_CODE = 'encryptedSecurityCode'; diff --git a/packages/e2e-playwright/tests/utils/keyboard.ts b/packages/e2e-playwright/tests/utils/keyboard.ts new file mode 100644 index 0000000000..125fdcc4c2 --- /dev/null +++ b/packages/e2e-playwright/tests/utils/keyboard.ts @@ -0,0 +1,14 @@ +import { Page } from '@playwright/test'; +import { KEYBOARD_DELAY } from './constants'; + +export const pressKeyboardToNextItem = async (page: Page) => { + await page.keyboard.press('ArrowDown', { delay: KEYBOARD_DELAY }); +}; + +export const pressKeyboardToPreviousItem = async (page: Page) => { + await page.keyboard.press('ArrowUp', { delay: KEYBOARD_DELAY }); +}; + +export const pressKeyboardToSelectItem = async (page: Page) => { + await page.keyboard.press('Enter', { delay: KEYBOARD_DELAY }); +};