Skip to content

Commit

Permalink
Confirming that .fill triggers required events in SF (#2982)
Browse files Browse the repository at this point in the history
* Confirming that .fill triggers required events in SF

* Cleanup

* removing clipboard permissions

* reverting value for sf version

* Add extra check that card is valid

* Added (or moved) remaining tests related to unsupported.card.number.spec.ts

* Slow down typing on test that is always flaky (because PAN doesn't fill properly on Webkit)

* Removed commented out line

* Removed commented out line

* Added comments
  • Loading branch information
sponglord authored Dec 9, 2024
1 parent b22744f commit ba132fc
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 82 deletions.
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);
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();
}
);
37 changes: 37 additions & 0 deletions packages/e2e-playwright/tests/ui/card/errors/card.errors.spec.ts
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

0 comments on commit ba132fc

Please sign in to comment.