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

Confirming that .fill triggers required events in SF #2982

Merged
merged 12 commits into from
Dec 9, 2024
20 changes: 14 additions & 6 deletions packages/e2e-playwright/models/card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,24 @@ class Card extends Base {
await this.cvcInput.waitFor({ state: 'visible' });
}

/**
* Locator.fill:
* - checks field is visible, enabled & editable
* - focuses a field
* - writes to the field's value prop
* - fires an input event
*
* For most of our test cases .fill can be seen to mimic a paste event
*/
async fillCardNumber(cardNumber: string) {
// reason: https://playwright.dev/docs/api/class-locator#locator-type
// use-case when we don't need to inspect keyboard events
await this.cardNumberInput.fill(cardNumber);
}

/**
* Locator.pressSequentially:
* - Focuses the element
* - then sends a keydown, keypress/input, and keyup event for each character in the text.
*/
async typeCardNumber(cardNumber: string) {
await this.cardNumberInput.pressSequentially(cardNumber, { delay: USER_TYPE_DELAY });
}
Expand All @@ -160,8 +172,6 @@ class Card extends Base {
}

async fillExpiryDate(expiryDate: string) {
// reason: https://playwright.dev/docs/api/class-locator#locator-type
// use-case when we don't need to inspect keyboard events
await this.expiryDateInput.fill(expiryDate);
}

Expand All @@ -170,8 +180,6 @@ class Card extends Base {
}

async fillCvc(cvc: string) {
// reason: https://playwright.dev/docs/api/class-locator#locator-type
// use-case when we don't need to inspect keyboard events
await this.cvcInput.fill(cvc);
}

Expand Down
6 changes: 1 addition & 5 deletions packages/e2e-playwright/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,7 @@ const config: PlaywrightTestConfig = {
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
contextOptions: {
// chromium-specific permissions
permissions: ['clipboard-read', 'clipboard-write']
}
...devices['Desktop Chrome']
}
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { getStoryUrl } from '../../../../utils/getStoryUrl';
import { URL_MAP } from '../../../../../fixtures/URL_MAP';
import { binLookupMock } from '../../../../../mocks/binLookup/binLookup.mock';
import { kcpMockOptionalDateAndCvcWithPanLengthMock } from '../../../../../mocks/binLookup/binLookup.data';
import { REGULAR_TEST_CARD } from '../../../../utils/constants';
import { CARD_WITH_PAN_LENGTH, REGULAR_TEST_CARD } from '../../../../utils/constants';

const componentConfig = {
brands: ['mc', 'visa', 'amex', 'korean_local_card'],
Expand All @@ -30,24 +30,13 @@ test.describe('Test how Card Component handles binLookup returning a panLength p
});

test('#2 Paste non KCP PAN and see focus move to date field', async ({ cardWithKCP, page, browserName }) => {
test.skip(browserName === 'webkit', 'This test is not run for Safari because it always fails on the CI due to the "pasting"');

await cardWithKCP.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

await cardWithKCP.isComponentVisible();

// Place focus on the input
await cardWithKCP.cardNumberLabelElement.click();

// Copy text to clipboard
await page.evaluate(() => navigator.clipboard.writeText('4000620000000007')); // Can't use the constant for some reason

await page.waitForTimeout(1000);

// Paste text from clipboard
await page.keyboard.press('ControlOrMeta+V');

await page.waitForTimeout(1000);
// "Paste" number
await cardWithKCP.fillCardNumber(CARD_WITH_PAN_LENGTH);
await page.waitForTimeout(100);

// Expect UI change - expiryDate field has focus
await expect(cardWithKCP.cardNumberInput).not.toBeFocused();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,34 +108,21 @@ test.describe('Test Card, & binLookup w. panLength property', () => {
// Card out of date
await card.fillExpiryDate('12/90');

await card.typeCardNumber(CARD_WITH_PAN_LENGTH);

await page.waitForTimeout(500);
await card.typeCardNumber(CARD_WITH_PAN_LENGTH, 300);

// Expect UI change - expiryDate field has focus
await expect(card.cardNumberInput).not.toBeFocused();
await expect(card.expiryDateInput).toBeFocused();
});

test('#6 Fill out PAN by **pasting** number & see that that focus moves to expiryDate', async ({ card, page, browserName }) => {
test.skip(browserName === 'webkit', 'This test is not run for Safari because it always fails on the CI due to the "pasting"');

await card.goto(URL_MAP.card);

await card.isComponentVisible();

// Place focus on the input
await card.cardNumberLabelElement.click();

// Copy text to clipboard
await page.evaluate(() => navigator.clipboard.writeText('4000620000000007')); // Can't use the constant for some reason

await page.waitForTimeout(1000);

// Paste text from clipboard
await page.keyboard.press('ControlOrMeta+V');

await page.waitForTimeout(1000);
// "Paste" number
await card.fillCardNumber(CARD_WITH_PAN_LENGTH);
sponglord marked this conversation as resolved.
Show resolved Hide resolved
await page.waitForTimeout(100);

// Expect UI change - expiryDate field has focus
await expect(card.cardNumberInput).not.toBeFocused();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test, expect } from '../../../../../fixtures/card.fixture';
import { getStoryUrl } from '../../../../utils/getStoryUrl';
import { PLCC_NO_LUHN_NO_DATE, PLCC_WITH_LUHN_NO_DATE_WOULD_FAIL_LUHN, TEST_CVC_VALUE, TEST_DATE_VALUE } from '../../../../utils/constants';
import { URL_MAP } from '../../../../../fixtures/URL_MAP';
import LANG from '../../../../../../server/translations/en-US.json';

const PAN_ERROR_NOT_VALID = LANG['cc.num.902'];

test.describe('Testing binLookup/plcc/pasting fny: test what happens when cards that do, or do not, require a luhn check, are pasted in', () => {
test('#1 Test that the paste event triggers the correct response and the validation rules are updated accordingly', async ({ card, page }) => {
//
const componentConfig = { brands: ['mc', 'visa', 'amex', 'bcmc', 'synchrony_plcc'] };

await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

await card.isComponentVisible();

/**
* Type number that identifies as plcc, no 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
await card.pay();

await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_VALID);

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

// If correct events have fired expect the card to be valid i.e. no error message when pressing pay
await card.pay();
await expect(card.cardNumberErrorElement).not.toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -1,45 +1,91 @@
import { test } from '../../../../fixtures/card.fixture';
import { expect, test } from '../../../../fixtures/card.fixture';
import { getStoryUrl } from '../../../utils/getStoryUrl';
import { URL_MAP } from '../../../../fixtures/URL_MAP';
import { PAYMENT_RESULT, REGULAR_TEST_CARD, TEST_CVC_VALUE, TEST_DATE_VALUE, UNKNOWN_BIN_CARD, VISA_CARD } from '../../../utils/constants';
import LANG from '../../../../../server/translations/en-US.json';

const PAN_ERROR_NOT_SUPPORTED = LANG['cc.num.903'];

test('#1 Test that after an unsupported card has been entered we see errors, PASTING in a full supported card clears errors & makes it possible to pay', async ({
card,
page
}) => {
//
const componentConfig = { brands: ['mc'] };

await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

await card.isComponentVisible();

// Fill unsupported card
await card.fillCardNumber(VISA_CARD);
await page.waitForTimeout(100);

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

await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_SUPPORTED);

// "Paste" number that is supported
await card.fillCardNumber(REGULAR_TEST_CARD);
await page.waitForTimeout(100);

// If correct events have fired expect the card to not have errors
await expect(card.cardNumberErrorElement).not.toBeVisible();

// And to be valid
await card.pay();
await expect(card.paymentResult).toHaveText(PAYMENT_RESULT.authorised);
});

test(
'#1 Enter number of unsupported card, ' + 'then check UI shows an error ' + 'then PASTE supported card & check UI error is cleared',
async () => {
// Wait for field to appear in DOM
// Fill card field with unsupported number
// Test UI shows "Unsupported card" error
// Past card field with supported number
// Test UI shows "Unsupported card" error has gone
'#2 Enter number of unsupported card, ' + 'then check UI shows an error ' + 'then PASTE card not in db & check UI error is cleared',
async ({ card, page }) => {
const componentConfig = { brands: ['mc'] };

await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

await card.isComponentVisible();

// Fill unsupported card
await card.fillCardNumber(VISA_CARD);
await page.waitForTimeout(100);

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

await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_SUPPORTED);

// "Paste" number that is unknown
await card.fillCardNumber(UNKNOWN_BIN_CARD);
await page.waitForTimeout(100);

// If correct events have fired expect the card to not have errors
await expect(card.cardNumberErrorElement).not.toBeVisible();
}
);

test(
'#2 Enter number of unsupported card, ' +
'then check UI shows an error ' +
'then press the Pay button ' +
'then check UI shows more errors ' +
'then PASTE supported card & check PAN UI errors are cleared whilst others persist',
async () => {
// Wait for field to appear in DOM
// Fill card field with unsupported number
// Test UI shows "Unsupported card" error
// Click Pay (which will call showValidation on all fields)
// Past card field with supported number
// Test UI shows "Unsupported card" error has gone
// PAN error cleared but other errors persist
}
);
'#3 Enter number of unsupported card, ' + 'then check UI shows an error ' + 'then delete PAN & check UI error is cleared',
async ({ card, page }) => {
const componentConfig = { brands: ['mc'] };

test('#3 Enter number of unsupported card, ' + 'then check UI shows an error ' + 'then PASTE card not in db check UI error is cleared', async () => {
// Wait for field to appear in DOM
// Fill card field with unsupported number
// Test UI shows "Unsupported card" error
// Past card field with supported number
// Test UI shows "Unsupported card" error has gone
});
await card.goto(getStoryUrl({ baseUrl: URL_MAP.card, componentConfig }));

test('#4 Enter number of unsupported card, ' + 'then check UI shows an error ' + 'then delete PAN & check UI error is cleared', async () => {
// Wait for field to appear in DOM
// Fill card field with unsupported number
// Test UI shows "Unsupported card" error
// delete card number
// Test UI shows "Unsupported card" error has gone
});
await card.isComponentVisible();

// Fill unsupported card
await card.fillCardNumber(VISA_CARD);
await page.waitForTimeout(100);

await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR_NOT_SUPPORTED);

await page.waitForTimeout(300); // leave time for focus to shift

await card.deleteCardNumber();
await expect(card.cardNumberErrorElement).not.toBeVisible();
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect } from '../../../../fixtures/card.fixture';
import { REGULAR_TEST_CARD, TEST_CVC_VALUE, TEST_DATE_VALUE } from '../../../utils/constants';
import { URL_MAP } from '../../../../fixtures/URL_MAP';
import LANG from '../../../../../server/translations/en-US.json';

const ERROR_ENTER_PAN = LANG['cc.num.900'];
const ERROR_ENTER_DATE = LANG['cc.dat.910'];
const ERROR_ENTER_CVC = LANG['cc.cvc.920'];

test.describe('Card - UI errors', () => {
test('#1 Not filling in card fields should lead to errors, which are cleared when fields are filled', async ({ card, page }) => {
await card.goto(URL_MAP.card);
await card.isComponentVisible();
await card.pay();

// Expect errors
await expect(card.cardNumberErrorElement).toBeVisible();
await expect(card.cardNumberErrorElement).toHaveText(ERROR_ENTER_PAN);

await expect(card.expiryDateErrorElement).toBeVisible();
await expect(card.expiryDateErrorElement).toHaveText(ERROR_ENTER_DATE);

await expect(card.cvcErrorElement).toBeVisible();
await expect(card.cvcErrorElement).toHaveText(ERROR_ENTER_CVC);

await page.waitForTimeout(300); // leave time for focus to shift

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

// Expect no errors
await expect(card.cardNumberErrorElement).not.toBeVisible();
await expect(card.expiryDateErrorElement).not.toBeVisible();
await expect(card.cvcErrorElement).not.toBeVisible();
});
});
3 changes: 2 additions & 1 deletion packages/e2e-playwright/tests/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export const UNKNOWN_VISA_CARD = '41111111'; // card is now in the test DBs (vis

export const PLCC_NO_LUHN_NO_DATE = '6044100018023838'; // binLookup gives luhn check and date not required
export const PLCC_WITH_LUHN_NO_DATE = '6044141000018769'; // binLookup gives luhn check required but date not required
export const PLCC_NO_LUHN_NO_DATE_WOULD_FAIL_LUHN = '6044100033327222'; // A PAN that identifies as a plcc that doesn't require a luhn check BUT that would fail the luhn check if it was required_
export const PLCC_WITH_LUHN_NO_DATE_WOULD_FAIL_LUHN = '6044141000018768'; // binLookup gives luhn check required, date not required, BUT that will fail the luhn check
export const PLCC_NO_LUHN_NO_DATE_WOULD_FAIL_LUHN = '6044100033327222'; // A PAN that identifies as a plcc that doesn't require a luhn check BUT that would fail the luhn check if it was required

// intersolve (plastix)
export const GIFTCARD_NUMBER = '4010100000000000000';
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/storybook/stories/cards/Card.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const Default: CardStory = {
componentConfiguration: getComponentConfigFromUrl() ?? {
_disableClickToPay: true,
autoFocus: true,
// brands: ['mc'],
// brands: ['mc', 'synchrony_plcc'],
// brandsConfiguration: { visa: { icon: 'http://localhost:3000/nocard.svg', name: 'altVisa' } },
challengeWindowSize: '02',
// configuration: {socialSecurityNumberMode: 'auto'}
Expand Down
Loading