Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/migrate branding reset tests #3117

Merged
merged 17 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 0 additions & 19 deletions packages/e2e-playwright/models/bcmc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,10 @@ const CVC_IFRAME_TITLE = LANG['creditCard.encryptedSecurityCode.aria.iframeTitle
const CVC_IFRAME_LABEL = LANG['creditCard.securityCode.label'];

class BCMC extends Card {
get brands() {
return this.cardNumberField.locator('.adyen-checkout__card__cardNumber__brandIcon').all();
}

async waitForVisibleBrands(expectedNumber = 2) {
return await this.page.waitForFunction(
expectedLength => [...document.querySelectorAll('.adyen-checkout__card__cardNumber__brandIcon')].length === expectedLength,
expectedNumber
);
}

async isComponentVisible() {
await this.cardNumberInput.waitFor({ state: 'visible' });
await this.expiryDateInput.waitFor({ state: 'visible' });
}
async selectBrand(
text: string | RegExp,
options?: {
exact?: boolean;
}
) {
await this.cardNumberField.getByAltText(text, options).click();
}

/**
* When in the context of the Dropin, if storedPMs are not hidden - the cvcInput locator will find 2 CVC inputs
Expand Down
31 changes: 31 additions & 0 deletions packages/e2e-playwright/models/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,41 @@ class Card extends Base {
this.threeDs2Challenge = new ThreeDs2Challenge(page);
}

// The brands as displayed under the CardNumber field
get availableBrands() {
return this.rootElement.locator('.adyen-checkout__card__brands').getByRole('img').all();
}

// The holder for the icons in the CardNumber field (when dual branding occurs)
get dualBrandingIconsHolder() {
return this.rootElement.locator('.adyen-checkout__card__dual-branding__buttons');
}

// The brands as displayed directly in the CardNumber field (when dual branding occurs)
async waitForVisibleBrands(expectedNumber = 2) {
return await this.page.waitForFunction(
expectedLength => [...document.querySelectorAll('.adyen-checkout__card__cardNumber__brandIcon')].length === expectedLength,
expectedNumber
);
}

// Retrieve dual brands
get brands() {
return this.cardNumberField.locator('.adyen-checkout__card__cardNumber__brandIcon').all();
}

// Select one of the dual brands
async selectBrand(
text: string | RegExp,
options?: {
exact?: boolean;
},
force = false
) {
await this.cardNumberField.getByAltText(text, options).click({ force });
}
// --

async goto(url: string = URL_MAP.card) {
await this.page.goto(url);
await this.isComponentVisible();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test, expect } from '../../../../../fixtures/card.fixture';
import { REGULAR_TEST_CARD, UNKNOWN_BIN_CARD_REGEX_VISA } from '../../../../utils/constants';
import { URL_MAP } from '../../../../../fixtures/URL_MAP';

test.describe('Card - Testing resetting brand after binLookup has occurred', () => {
test('#1 Fill in regular MC card, then replace it with an unrecognised one, but see our regEx detects the brand & sets it in the UI', async ({
card,
page
}) => {
await card.goto(URL_MAP.card);
await card.typeCardNumber(REGULAR_TEST_CARD);

// Check brand has been set in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === 'mc');

// Paste in card unrecognised by /binLookuo but which our regEx recognises as Visa
await card.fillCardNumber(UNKNOWN_BIN_CARD_REGEX_VISA);

// Check brand has been reset in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === undefined);

// Check regEx recognises brand
let brandingIconSrc = await card.brandingIcon.getAttribute('src');
expect(brandingIconSrc).toContain('visa.svg');
});

test('#2 Fill in regular MC card, see UI reflects it, then delete it, and see the brand is reset in the UI', async ({ card, page }) => {
await card.goto(URL_MAP.card);

let brandingIconSrc = await card.brandingIcon.getAttribute('src');
expect(brandingIconSrc).toContain('nocard.svg');

await card.typeCardNumber(REGULAR_TEST_CARD);

brandingIconSrc = await card.brandingIcon.getAttribute('src');
expect(brandingIconSrc).toContain('mc.svg');

await card.deleteCardNumber();

let cardData: any = await page.evaluate('window.component.data');

// Check brand has been reset in paymentMethod data
expect(cardData.paymentMethod.brand).toBe(undefined);

// Check brand is reset in the UI
brandingIconSrc = await card.brandingIcon.getAttribute('src');
expect(brandingIconSrc).toContain('nocard.svg');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { expect, test } from '../../../../../fixtures/card.fixture';
import { getStoryUrl } from '../../../../utils/getStoryUrl';
import { URL_MAP } from '../../../../../fixtures/URL_MAP';
import { BCMC_CARD, BCMC_DUAL_BRANDED_VISA, REGULAR_TEST_CARD, UNKNOWN_BIN_CARD_REGEX_VISA } from '../../../../utils/constants';

const componentConfig = {
brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc']
};

test.describe('Card - Testing resetting after binLookup has given a dual brand result', () => {
test(
'#1 Fill in dual branded card then ' +
'check that brands have been sorted to place Bcmc first then ' +
'ensure only generic logo shows after deleting digits',
async ({ card }) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

await card.fillCardNumber(BCMC_CARD);

await card.waitForVisibleBrands();

// Dual brand holder visible
await expect(card.dualBrandingIconsHolder).toBeVisible();

let [firstBrand, secondBrand, thirdBrand] = await card.brands;

// Expect original icon to be hidden, leaving only 2 brands
expect(thirdBrand).toBeUndefined();

// 2 brand icons, in correct order
expect(firstBrand).toHaveAttribute('data-value', 'bcmc');
expect(secondBrand).toHaveAttribute('data-value', 'maestro');

await card.deleteCardNumber();

await card.waitForVisibleBrands(1);

[firstBrand, secondBrand] = await card.brands;

// Now only a single, generic, brand
const brandingIconSrc = await firstBrand.getAttribute('src');
expect(brandingIconSrc).toContain('nocard.svg');

expect(secondBrand).toBeUndefined();

// Dual brand holder hidden
await expect(card.dualBrandingIconsHolder).not.toBeVisible();
}
);

test(
'#2 Fill in dual branded card, do not make selection and see that brand is not set, ' +
' then replace it with an unrecognised one, but see our regEx detects the brand & sets it in the UI',
async ({ card, page }) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));
await card.typeCardNumber(BCMC_DUAL_BRANDED_VISA);

// Check brand has not been set in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === undefined);

// Need this - it allows time for the UI to update when the icon changes
const responsePromise = page.waitForResponse(response => response.url().includes('/binLookup') && response.request().method() === 'POST');

// Paste in card unrecognised by /binLookuo but which our regEx recognises as Visa
await card.fillCardNumber(UNKNOWN_BIN_CARD_REGEX_VISA);

await responsePromise;

// Check brand has been reset in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === undefined);

// Check regEx recognises brand
let brandingIconSrc = await card.brandingIcon.getAttribute('src');
expect(brandingIconSrc).toContain('visa.svg');
}
);

test('#3 Fill in dual branded card, make selection and see that brand is set, then delete digits and see that brand is reset', async ({
card,
page
}) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));
await card.typeCardNumber(BCMC_DUAL_BRANDED_VISA);

// Select brand
await card.selectBrand(/visa/i);

// Check brand has been set in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === 'visa');

// Click second brand
await card.selectBrand('Bancontact card');

// Check brand has been set in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === 'bcmc');

// Delete number
await card.deleteCardNumber();

// Check brand has been reset in paymentMethod data
await page.waitForFunction(() => window['component'].data.paymentMethod.brand === undefined);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { expect, test } from '../../../../../fixtures/card.fixture';
import { getStoryUrl } from '../../../../utils/getStoryUrl';
import { URL_MAP } from '../../../../../fixtures/URL_MAP';
import { BCMC_DUAL_BRANDED_VISA, DUAL_BRANDED_CARD_EXCLUDED } from '../../../../utils/constants';

import LANG from '../../../../../../server/translations/en-US.json';

const PAN_ERROR_NOT_COMPLETE = LANG['cc.num.901'];

const componentConfig = {
brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc', 'star']
};

test.describe('Card - Testing UI after binLookup has given a dual brand result', () => {
test('#1 Fill in dual branded card, but do not complete the number, see dual brand icons are inactive until the number is completed', async ({
card,
page
}) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

const firstDigits = BCMC_DUAL_BRANDED_VISA.substring(0, 11);
const lastDigits = BCMC_DUAL_BRANDED_VISA.substring(11, 16);

// Type enough digits to get a binLookup result, but not enough for the field to be complete
await card.typeCardNumber(firstDigits);

// Since the dominant brand is bcmc - expect the cvc field to be hidden
await expect(card.cvcField).not.toBeVisible();

// Expect dual brand icons to be visible
await expect(card.dualBrandingIconsHolder).toBeVisible();

// Since the dual brands are not yet active, trying to click one should force an error in the UI
await card.selectBrand(/visa/i, null, true);

// We should get a error on the number field
await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_COMPLETE);

// Complete the number
await card.cardNumberInput.focus(); // Focus the input field
await page.keyboard.press('End');
await card.typeCardNumber(lastDigits);

// Click a brand and see that, now they are active...
await card.selectBrand(/visa/i);

// Expect error to go away
await expect(card.cardNumberErrorElement).not.toBeVisible();

// Now we have selected visa - expect cvc to be visible
await expect(card.cvcField).toBeVisible();
});

test(
'#2 Fill in dual branded card, ' +
'then select one of the dual brands,' +
'then check the other brand icon is at reduced alpha,' +
'then repeat with the other icon',
async ({ card, page }) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

// Type dual branded card
await card.typeCardNumber(BCMC_DUAL_BRANDED_VISA);

const [firstBrand, secondBrand] = await card.brands;

// Check that both icons do not have the class that would cause their opacity to reduce
await expect(firstBrand).not.toHaveClass(/adyen-checkout__card__cardNumber__brandIcon--not-selected/);
await expect(secondBrand).not.toHaveClass(/adyen-checkout__card__cardNumber__brandIcon--not-selected/);

// Click first brand
await card.selectBrand('Bancontact card');

// Check that class that adds opacity ISN'T present
await expect(firstBrand).not.toHaveClass(/adyen-checkout__card__cardNumber__brandIcon--not-selected/);
// Check that class that adds opacity IS present
await expect(secondBrand).toHaveClass(/adyen-checkout__card__cardNumber__brandIcon--not-selected/);

// Click second brand
await card.selectBrand(/visa/i);

// Check that opacities have switched
await expect(firstBrand).toHaveClass(/adyen-checkout__card__cardNumber__brandIcon--not-selected/);
await expect(secondBrand).not.toHaveClass(/adyen-checkout__card__cardNumber__brandIcon--not-selected/);
}
);

test('#3 Fill in dual branded card, see dual brand icons are at reduced alpha until the number is completed', async ({ card, page }) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

const firstDigits = BCMC_DUAL_BRANDED_VISA.substring(0, 11);
const lastDigits = BCMC_DUAL_BRANDED_VISA.substring(11, 16);

// Type enough digits to get a binLookup result, but not enough for the field to be complete
await card.typeCardNumber(firstDigits);

// Expect icon holder not to have class adding full opacity
await expect(card.dualBrandingIconsHolder).not.toHaveClass(/adyen-checkout__card__dual-branding__buttons--active/);

// Complete the number
await card.cardNumberInput.focus();
await page.keyboard.press('End');
await card.typeCardNumber(lastDigits);

// Expect icon holder to have class adding full opacity
await expect(card.dualBrandingIconsHolder).toHaveClass(/adyen-checkout__card__dual-branding__buttons--active/);
});

test(
'#4 Fill in dual branded card, ' +
'but one of the brands should be excluded from the UI, ' +
'(meaning also that no brand should be set in the PM data), ' +
'then check PM data does not have a brand property,' +
'and check there are no dual branding icons/buttons',
async ({ card, page }) => {
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

await card.typeCardNumber(DUAL_BRANDED_CARD_EXCLUDED);

// Check brand has not been set in paymentMethod data
let cardData: any = await page.evaluate('window.component.data');
expect(cardData.paymentMethod.brand).toBe(undefined);

// Expect dual brand icons not to be visible
await expect(card.dualBrandingIconsHolder).not.toBeVisible();
}
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,11 @@ test.describe('Testing binLookup/plcc/pasting fny: test what happens when cards
await card.isComponentVisible();

/**
* Type number that identifies as plcc, no luhn required, but that fails luhn
* Type number that identifies as plcc, no date, with luhn required, but that fails luhn
*/
await card.fillCardNumber(PLCC_WITH_LUHN_NO_DATE_WOULD_FAIL_LUHN);
await page.waitForTimeout(100);

await card.typeExpiryDate(TEST_DATE_VALUE);
await card.typeCvc(TEST_CVC_VALUE);

// Expect the card not to be valid
Expand All @@ -30,7 +29,7 @@ test.describe('Testing binLookup/plcc/pasting fny: test what happens when cards
await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_VALID);

// "Paste" number that identifies as plcc, luhn required
// "Paste" number that identifies as plcc, no luhn required
await card.fillCardNumber(PLCC_NO_LUHN_NO_DATE);
await page.waitForTimeout(100);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ test.describe('Testing binLookup endpoint for a response that should indicate a

await card.isComponentVisible();

// Number that identifies as plcc but fails luhn
// Number that identifies as plcc, with no luhn required, but that also fails luhn
await card.typeCardNumber(PLCC_NO_LUHN_NO_DATE_WOULD_FAIL_LUHN);

// Confirm plcc brand
Expand All @@ -39,6 +39,9 @@ test.describe('Testing binLookup endpoint for a response that should indicate a
brandingIconSrc = await card.brandingIcon.getAttribute('src');
expect(brandingIconSrc).toContain('nocard.svg');

// Confirm date is visible again
await expect(card.expiryDateField).toBeVisible();

// PM is not valid
cardValid = await page.evaluate('window.component.isValid');
expect(cardValid).toEqual(false);
Expand Down
Loading
Loading