diff --git a/.changeset/cool-hotels-remember.md b/.changeset/cool-hotels-remember.md new file mode 100644 index 0000000000..efd0ccdafb --- /dev/null +++ b/.changeset/cool-hotels-remember.md @@ -0,0 +1,7 @@ +--- +'@adyen/adyen-web': patch +--- + +For all PaymentMethodItems we were adding a class `adyen-checkout__payment-method--{fundingSource}` (where fundingSource was either "credit" or "debit") + +This is meant to be a Card PM specific class to indicate, in the paymentMethods list, whether the card is a credit or debit card. diff --git a/.changeset/fifty-parrots-collect.md b/.changeset/fifty-parrots-collect.md new file mode 100644 index 0000000000..d81b6b64c1 --- /dev/null +++ b/.changeset/fifty-parrots-collect.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': minor +--- + +feature: adds new onAddressSelected to fill data when an item is selected in AddressSearch diff --git a/.changeset/lemon-emus-wait.md b/.changeset/lemon-emus-wait.md new file mode 100644 index 0000000000..7d3f2c95d6 --- /dev/null +++ b/.changeset/lemon-emus-wait.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': minor +--- + +Click to Pay - Enabling MC/Visa to drop cookies if the shopper gives consent diff --git a/.changeset/old-ears-heal.md b/.changeset/old-ears-heal.md new file mode 100644 index 0000000000..758a514739 --- /dev/null +++ b/.changeset/old-ears-heal.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': minor +--- + +Click to Pay - Replacing loading gif by animated SVGs diff --git a/.changeset/swift-rice-punch.md b/.changeset/swift-rice-punch.md new file mode 100644 index 0000000000..4cac281515 --- /dev/null +++ b/.changeset/swift-rice-punch.md @@ -0,0 +1,5 @@ +--- +'@adyen/adyen-web': patch +--- + +Pass the `browserInfo` in the `state.data` for the Redirect payments, in order to fix the mobile web integration for some redirect payments. 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/app/src/handlers.js b/packages/e2e-playwright/app/src/handlers.js index 27f182fef7..6020ee05f6 100644 --- a/packages/e2e-playwright/app/src/handlers.js +++ b/packages/e2e-playwright/app/src/handlers.js @@ -4,10 +4,10 @@ function removeComponent(component) { component.remove(); } -export function showAuthorised() { +export function showAuthorised(message = 'Authorised') { const resultElement = document.getElementById('result-message'); resultElement.classList.remove('hide'); - resultElement.innerText = 'Authorised'; + resultElement.innerText = message; } export function handleResponse(response, component) { diff --git a/packages/e2e-playwright/app/src/pages/ANCV/ANCV.html b/packages/e2e-playwright/app/src/pages/ANCV/ANCV.html index 0864f2f54d..ab7c551cc3 100644 --- a/packages/e2e-playwright/app/src/pages/ANCV/ANCV.html +++ b/packages/e2e-playwright/app/src/pages/ANCV/ANCV.html @@ -16,11 +16,12 @@

ANCV

-
+
+ + + diff --git a/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js b/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js new file mode 100644 index 0000000000..52c42e08e8 --- /dev/null +++ b/packages/e2e-playwright/app/src/pages/Dropin/Dropin.js @@ -0,0 +1,27 @@ +import AdyenCheckout from '@adyen/adyen-web'; +import '@adyen/adyen-web/dist/es/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, + ...window.mainConfiguration + }); + + window.dropin = checkout.create('dropin', window.dropinConfig).mount('#dropin-container'); +}; + +initCheckout(); diff --git a/packages/e2e-playwright/mocks/binLookup/binLookup.data.ts b/packages/e2e-playwright/mocks/binLookup/binLookup.data.ts index dad8b80f5b..9e205e10d2 100644 --- a/packages/e2e-playwright/mocks/binLookup/binLookup.data.ts +++ b/packages/e2e-playwright/mocks/binLookup/binLookup.data.ts @@ -13,4 +13,18 @@ const optionalDateAndCvcMock = { requestedId: null }; -export { optionalDateAndCvcMock }; +const hiddenDateAndCvcMock = { + brands: [ + { + brand: 'mc', + enableLuhnCheck: true, + supported: true, + cvcPolicy: 'hidden', + expiryDatePolicy: 'hidden' + } + ], + issuingCountryCode: 'US', + requestedId: null +}; + +export { optionalDateAndCvcMock, hiddenDateAndCvcMock }; diff --git a/packages/e2e-playwright/mocks/status/status.mock.ts b/packages/e2e-playwright/mocks/status/status.mock.ts index 151bef7e24..5613dca8fd 100644 --- a/packages/e2e-playwright/mocks/status/status.mock.ts +++ b/packages/e2e-playwright/mocks/status/status.mock.ts @@ -1,10 +1,32 @@ import { Page } from '@playwright/test'; const STATUS_URL = 'https://checkoutshopper-*.adyen.com/checkoutshopper/services/PaymentInitiation/v1/status?*'; + +const numberOfPendingCalls = 3; + const statusMock = async (page: Page, mockedResponse: any): Promise => { + let numberOfCalls = 0; + await page.route(STATUS_URL, (route, request) => { - const requestData = JSON.parse(request.postData() || ''); + if (numberOfCalls < numberOfPendingCalls) { + numberOfCalls++; + + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + payload: 'encrypted-data', + resultCode: 'pending', + type: 'complete' + }), + headers: { + 'Access-Control-Allow-Origin': '*' + } + }); + return; + } + const requestData = JSON.parse(request.postData() || ''); route.fulfill({ status: 200, contentType: 'application/json', diff --git a/packages/e2e-playwright/models/ancv.ts b/packages/e2e-playwright/models/ancv.ts index 318ea21545..41f88fbcd5 100644 --- a/packages/e2e-playwright/models/ancv.ts +++ b/packages/e2e-playwright/models/ancv.ts @@ -20,7 +20,7 @@ class ANCV { this.rootElementSelector = rootElementSelector; this.ancvInput = this.rootElement.getByRole('textbox'); - this.submitButton = this.page.getByRole('button', { name: /Pay/i }); + this.submitButton = this.page.getByRole('button', { name: /Confirm purchase/i }); this.awaitText = this.rootElement.getByText('Use your ANCV application to confirm the payment.'); } 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/customCard.ts b/packages/e2e-playwright/models/customCard.ts new file mode 100644 index 0000000000..de8fc79438 --- /dev/null +++ b/packages/e2e-playwright/models/customCard.ts @@ -0,0 +1,183 @@ +import { Locator, Page } from '@playwright/test'; +import { USER_TYPE_DELAY } from '../tests/utils/constants'; +import LANG from '../../lib/src/language/locales/en-US.json'; + +const CARD_IFRAME_TITLE = LANG['creditCard.encryptedCardNumber.aria.iframeTitle']; +const EXPIRY_DATE_IFRAME_TITLE = LANG['creditCard.encryptedExpiryDate.aria.iframeTitle']; +const EXPIRY_MONTH_IFRAME_TITLE = LANG['creditCard.encryptedExpiryMonth.aria.iframeTitle']; +const EXPIRY_YEAR_IFRAME_TITLE = LANG['creditCard.encryptedExpiryYear.aria.iframeTitle']; +const CVC_IFRAME_TITLE = LANG['creditCard.encryptedSecurityCode.aria.iframeTitle']; + +const CARD_IFRAME_LABEL = LANG['creditCard.numberField.title']; +const EXPIRY_DATE_IFRAME_LABEL = LANG['creditCard.expiryDateField.title']; +const EXPIRY_MONTH_IFRAME_LABEL = LANG['creditCard.encryptedExpiryMonth.aria.label']; +const EXPIRY_YEAR_IFRAME_LABEL = LANG['creditCard.encryptedExpiryYear.aria.label']; +const CVC_IFRAME_LABEL = LANG['creditCard.cvcField.title']; + +class CustomCard { + 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 expiryDateLabelText: Locator; + readonly expiryDateInput: Locator; + readonly expiryDateIframeContextualElement: Locator; + readonly expiryDateErrorElement: Locator; + + readonly expiryMonthField: Locator; + readonly expiryMonthLabelText: Locator; + readonly expiryMonthErrorElement: Locator; + readonly expiryMonthInput: Locator; + readonly expiryMonthIframeContextualElement: Locator; + + readonly expiryYearField: Locator; + readonly expiryYearLabelText: Locator; + readonly expiryYearErrorElement: Locator; + readonly expiryYearInput: Locator; + readonly expiryYearIframeContextualElement: Locator; + + readonly cvcField: Locator; + readonly cvcLabelText: Locator; + readonly cvcErrorElement: Locator; + readonly cvcInput: Locator; + readonly cvcIframeContextualElement: Locator; + + constructor(page: Page, rootElementSelector = '.secured-fields') { + this.page = page; + + this.rootElement = page.locator(rootElementSelector); + this.rootElementSelector = rootElementSelector; + + /** + * Card Number elements, in Checkout + */ + this.cardNumberField = this.rootElement.locator('.pm-form-label-pan'); // Holder + this.cardNumberLabelElement = this.cardNumberField.locator('.pm-form-label__text'); + this.cardNumberErrorElement = this.cardNumberField.locator('.pm-form-label__error-text'); + + this.brandingIcon = this.rootElement.locator('.adyen-checkout__card__cardNumber__brandIcon'); + + /** + * Card Number elements, in iframe + */ + const cardNumberIframe = this.rootElement.frameLocator(`[title="${CARD_IFRAME_TITLE}"]`); + this.cardNumberInput = cardNumberIframe.locator(`input[aria-label="${CARD_IFRAME_LABEL}"]`); + + /** + * Expiry Date elements, in Checkout + */ + this.expiryDateField = this.rootElement.locator('.pm-form-label--exp-date'); // Holder + this.expiryDateLabelText = this.expiryDateField.locator('.pm-form-label__text'); + this.expiryDateErrorElement = this.expiryDateField.locator('.pm-form-label__error-text'); // Related error element + + /** + * Expiry Date elements, in iframe + */ + const expiryDateIframe = this.rootElement.frameLocator(`[title="${EXPIRY_DATE_IFRAME_TITLE}"]`); + this.expiryDateInput = expiryDateIframe.locator(`input[aria-label="${EXPIRY_DATE_IFRAME_LABEL}"]`); + this.expiryDateIframeContextualElement = expiryDateIframe.locator('.aria-context'); + + /** + * Expiry Month elements, in Checkout + */ + this.expiryMonthField = this.rootElement.locator('.pm-form-label--exp-month'); // Holder + this.expiryMonthLabelText = this.expiryMonthField.locator('.pm-form-label__text'); + this.expiryMonthErrorElement = this.expiryMonthField.locator('.pm-form-label__error-text'); // Related error element + + /** + * Expiry Month elements, in iframe + */ + const expiryMonthIframe = this.rootElement.frameLocator(`[title="${EXPIRY_MONTH_IFRAME_TITLE}"]`); + this.expiryMonthInput = expiryMonthIframe.locator(`input[aria-label="${EXPIRY_MONTH_IFRAME_LABEL}"]`); + this.expiryMonthIframeContextualElement = expiryMonthIframe.locator('.aria-context'); + + /** + * Expiry Year elements, in Checkout + */ + this.expiryYearField = this.rootElement.locator('.pm-form-label--exp-year'); // Holder + this.expiryYearLabelText = this.expiryYearField.locator('.pm-form-label__text'); + this.expiryYearErrorElement = this.expiryYearField.locator('.pm-form-label__error-text'); + + /** + * Expiry Month elements, in iframe + */ + const expiryYearIframe = this.rootElement.frameLocator(`[title="${EXPIRY_YEAR_IFRAME_TITLE}"]`); + this.expiryYearInput = expiryYearIframe.locator(`input[aria-label="${EXPIRY_YEAR_IFRAME_LABEL}"]`); + this.expiryYearIframeContextualElement = expiryYearIframe.locator('.aria-context'); + + /** + * Security code elements, in Checkout + */ + this.cvcField = this.rootElement.locator('.pm-form-label--cvc'); // Holder + this.cvcLabelText = this.cvcField.locator('.pm-form-label__text'); + this.cvcErrorElement = this.cvcField.locator('.pm-form-label__error-text'); // Related error element + + /** + * Security code elements, in iframe + */ + const cvcIframe = this.rootElement.frameLocator(`[title="${CVC_IFRAME_TITLE}"]`); + this.cvcInput = cvcIframe.locator(`input[aria-label="${CVC_IFRAME_LABEL}"]`); + this.cvcIframeContextualElement = cvcIframe.locator('.aria-context'); + } + + async isComponentVisible() { + await this.cardNumberInput.waitFor({ state: 'visible' }); + await this.expiryDateInput.waitFor({ state: 'visible' }); + await this.cvcInput.waitFor({ state: 'visible' }); + } + + async isSeparateComponentVisible() { + await this.cardNumberInput.waitFor({ state: 'visible' }); + await this.expiryMonthInput.waitFor({ state: 'visible' }); + await this.cvcInput.waitFor({ state: 'visible' }); + } + + async typeCardNumber(cardNumber: string) { + await this.cardNumberInput.type(cardNumber, { delay: USER_TYPE_DELAY }); + } + + async deleteCardNumber() { + await this.cardNumberInput.clear(); + } + + async deleteExpiryDate() { + await this.expiryDateInput.clear(); + } + + async deleteExpiryMonth() { + await this.expiryMonthInput.clear(); + } + + async deleteExpiryYear() { + await this.expiryYearInput.clear(); + } + + async deleteCvc() { + await this.cvcInput.clear(); + } + + async typeExpiryDate(expiryDate: string) { + await this.expiryDateInput.type(expiryDate, { delay: USER_TYPE_DELAY }); + } + + async typeExpiryMonth(expiryMonth: string) { + await this.expiryMonthInput.type(expiryMonth, { delay: USER_TYPE_DELAY }); + } + async typeExpiryYear(expiryYear: string) { + await this.expiryYearInput.type(expiryYear, { delay: USER_TYPE_DELAY }); + } + + async typeCvc(cvc: string) { + await this.cvcInput.type(cvc, { delay: USER_TYPE_DELAY }); + } +} + +export { CustomCard }; diff --git a/packages/e2e-playwright/models/dropin.ts b/packages/e2e-playwright/models/dropin.ts new file mode 100644 index 0000000000..7ca07b27c4 --- /dev/null +++ b/packages/e2e-playwright/models/dropin.ts @@ -0,0 +1,31 @@ +import { Locator, Page } from '@playwright/test'; + +class Dropin { + readonly page: Page; + + readonly rootElement: Locator; + readonly rootElementSelector: string; + + readonly pmList: Locator; + readonly creditCard: Locator; + readonly brandsHolder: Locator; + + constructor(page: Page, rootElementSelector = '.adyen-checkout__dropin') { + this.page = page; + + this.rootElement = page.locator(rootElementSelector); + this.rootElementSelector = rootElementSelector; + + this.pmList = this.rootElement.locator('.adyen-checkout__payment-methods-list'); + } + + async isComponentVisible() { + await this.pmList.waitFor({ state: 'visible' }); + } + + getPaymentMethodItem(pmName: string) { + return this.pmList.locator(`.adyen-checkout__payment-method:has-text("${pmName}")`); + } +} + +export { Dropin }; diff --git a/packages/e2e-playwright/models/dropinModelUtils/getDropinCardComp.ts b/packages/e2e-playwright/models/dropinModelUtils/getDropinCardComp.ts new file mode 100644 index 0000000000..b436357794 --- /dev/null +++ b/packages/e2e-playwright/models/dropinModelUtils/getDropinCardComp.ts @@ -0,0 +1,20 @@ +import { Dropin } from '../dropin'; +import { getImageCount } from '../../tests/utils/image'; + +export const getCreditCardPM = (dropin: Dropin) => { + const creditCard = dropin.getPaymentMethodItem('Credit Card'); + + const brandsHolder = creditCard.locator('.adyen-checkout__payment-method__brands'); + + const brandsText = brandsHolder.locator('.adyen-checkout__payment-method__brand-number'); + + const componentBrandsHolder = creditCard.locator('.adyen-checkout__card__brands'); + + return { + pm: creditCard, + brandsHolder, + brandsText, + componentBrandsHolder, + getImageCount + }; +}; 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/package.json b/packages/e2e-playwright/package.json index d6bfaf7c61..87de2821f2 100644 --- a/packages/e2e-playwright/package.json +++ b/packages/e2e-playwright/package.json @@ -7,11 +7,12 @@ "scripts": { "test:start-playground": "cross-env NODE_ENV=test webpack-dev-server --config app/config/webpack.config.js", "test:headless": "npx playwright test", - "test:headed": "npx playwright test --headed" + "test:headed": "npx playwright test --headed", + "test:ui-mode": "npx playwright test --ui" }, "devDependencies": { "@adyen/adyen-web-server": "1.0.0", - "@playwright/test": "1.30.0", + "@playwright/test": "1.39.0", "cross-env": "^7.0.3", "css-loader": "^6.0.0", "dotenv": "^16.0.2", diff --git a/packages/e2e-playwright/pages/ancv/ancv.fixture.ts b/packages/e2e-playwright/pages/ancv/ancv.fixture.ts index 8ae20cc6dd..771f060419 100644 --- a/packages/e2e-playwright/pages/ancv/ancv.fixture.ts +++ b/packages/e2e-playwright/pages/ancv/ancv.fixture.ts @@ -14,6 +14,7 @@ type Fixture = { const test = base.extend({ ancvPage: async ({ page }, use) => { const ancvPage = new AncvPage(page); + await sessionsMock(page, sessionsMockData); await setupMock(page, setupMockData); await ancvPage.goto(); diff --git a/packages/e2e-playwright/pages/ancv/ancv.page.ts b/packages/e2e-playwright/pages/ancv/ancv.page.ts index d7280a328a..11869cb9f7 100644 --- a/packages/e2e-playwright/pages/ancv/ancv.page.ts +++ b/packages/e2e-playwright/pages/ancv/ancv.page.ts @@ -2,7 +2,7 @@ import { Page } from '@playwright/test'; import { ANCV } from '../../models/ancv'; class AncvPage { - private readonly page: Page; + public readonly page: Page; public readonly ancv: ANCV; 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/customCard/customCard.fixture.ts b/packages/e2e-playwright/pages/customCard/customCard.fixture.ts index 37e88b8f56..8538634e93 100644 --- a/packages/e2e-playwright/pages/customCard/customCard.fixture.ts +++ b/packages/e2e-playwright/pages/customCard/customCard.fixture.ts @@ -3,6 +3,7 @@ import { CustomCardPage } from './customCard.page'; type Fixture = { customCardPage: CustomCardPage; + customCardPageSeparate: CustomCardPage; }; const test = base.extend({ @@ -10,6 +11,12 @@ const test = base.extend({ const cardPage = new CustomCardPage(page); await cardPage.goto(); await use(cardPage); + }, + + customCardPageSeparate: async ({ page }, use) => { + const cardPage = new CustomCardPage(page, '.secured-fields-2'); + await cardPage.goto(); + await use(cardPage); } }); diff --git a/packages/e2e-playwright/pages/customCard/customCard.page.ts b/packages/e2e-playwright/pages/customCard/customCard.page.ts index c3aaa87e3c..da586105ad 100644 --- a/packages/e2e-playwright/pages/customCard/customCard.page.ts +++ b/packages/e2e-playwright/pages/customCard/customCard.page.ts @@ -1,24 +1,27 @@ import { Locator, Page } from '@playwright/test'; -import { Card } from '../../models/card'; +import { CustomCard } from '../../models/customCard'; class CustomCardPage { readonly page: Page; - readonly card: Card; - readonly payButton: Locator; + readonly card: CustomCard; + readonly payButtonRegular: Locator; + readonly payButtonSeparate: Locator; - constructor(page: Page) { + constructor(page: Page, selector?: string) { this.page = page; - this.card = new Card(page, '.secured-fields'); - this.payButton = page.getByRole('button', { name: /Pay/i }); + this.card = new CustomCard(page, selector); + this.payButtonRegular = page.getByTestId('pay-customCardRegular'); + this.payButtonSeparate = page.getByTestId('pay-customCardSeparate'); } async goto(url?: string) { await this.page.goto('http://localhost:3024/customcards'); } - async pay() { - await this.payButton.click(); + async pay(which: string = 'Regular') { + await this[`payButton${which}`].scrollIntoViewIfNeeded(); + await this[`payButton${which}`].click(); } } diff --git a/packages/e2e-playwright/pages/dropin/dropin.fixture.ts b/packages/e2e-playwright/pages/dropin/dropin.fixture.ts new file mode 100644 index 0000000000..c157f2d5bf --- /dev/null +++ b/packages/e2e-playwright/pages/dropin/dropin.fixture.ts @@ -0,0 +1,90 @@ +import { test as base, expect, Page } from '@playwright/test'; +import { DropinPage } from './dropin.page'; + +type Fixture = { + dropinPage: DropinPage; + dropinPage_cardBrands_defaultView: DropinPage; + dropinPage_cardBrands_defaultView_withExcluded: DropinPage; + dropinPage_cardBrands_compactView: DropinPage; + dropinPage_cardBrands_compactView_withExcluded: DropinPage; +}; + +const test = base.extend({ + dropinPage: async ({ page }, use) => { + await useDropinPage(page, use); + }, + + dropinPage_cardBrands_defaultView: async ({ page }, use) => { + const pmsConfig = JSON.stringify({ + paymentMethodsConfiguration: { + card: { + showBrandsUnderCardNumber: false, + brands: ['visa', 'mc', 'amex', 'discover', 'cup', 'maestro', 'bijcard', 'diners', 'jcb', 'synchrony_cbcc'], + _disableClickToPay: true + } + } + }); + await page.addInitScript({ + content: `window.mainConfiguration = ${pmsConfig}` + }); + + await useDropinPage(page, use); + }, + + dropinPage_cardBrands_defaultView_withExcluded: async ({ page }, use) => { + const pmsConfig = JSON.stringify({ + paymentMethodsConfiguration: { + card: { + showBrandsUnderCardNumber: false, + brands: ['visa', 'mc', 'amex', 'discover', 'cup', 'maestro', 'nyce', 'accel', 'star', 'pulse'], + _disableClickToPay: true + } + } + }); + await page.addInitScript({ + content: `window.mainConfiguration = ${pmsConfig}` + }); + + await useDropinPage(page, use); + }, + + dropinPage_cardBrands_compactView: async ({ page }, use) => { + const pmsConfig = JSON.stringify({ + paymentMethodsConfiguration: { + card: { + brands: ['visa', 'mc', 'amex', 'discover', 'cup', 'maestro', 'bijcard', 'diners', 'jcb', 'synchrony_cbcc'], + _disableClickToPay: true + } + } + }); + await page.addInitScript({ + content: `window.mainConfiguration = ${pmsConfig}` + }); + + await useDropinPage(page, use); + }, + + dropinPage_cardBrands_compactView_withExcluded: async ({ page }, use) => { + const pmsConfig = JSON.stringify({ + paymentMethodsConfiguration: { + card: { + brands: ['visa', 'mc', 'amex', 'discover', 'cup', 'maestro', 'nyce', 'accel', 'star', 'pulse'], + _disableClickToPay: true + } + } + }); + await page.addInitScript({ + content: `window.mainConfiguration = ${pmsConfig}` + }); + + await useDropinPage(page, use); + } +}); + +const useDropinPage = async (page: Page, use: any, PageType = DropinPage) => { + const dropinPage = new PageType(page); + await dropinPage.goto(); + await use(dropinPage); +}; + +export { test, expect }; diff --git a/packages/e2e-playwright/pages/dropin/dropin.page.ts b/packages/e2e-playwright/pages/dropin/dropin.page.ts new file mode 100644 index 0000000000..3f5923a549 --- /dev/null +++ b/packages/e2e-playwright/pages/dropin/dropin.page.ts @@ -0,0 +1,25 @@ +import { Locator, Page } from '@playwright/test'; +import { Dropin } from '../../models/dropin'; + +class DropinPage { + readonly page: Page; + + readonly dropin: Dropin; + // readonly payButton: Locator; + + constructor(page: Page) { + this.page = page; + this.dropin = new Dropin(page); + // this.payButton = page.getByRole('button', { name: /Pay/i }); + } + + async goto(url?: string) { + await this.page.goto('http://localhost:3024'); + } + + // async pay() { + // await this.payButton.click(); + // } +} + +export { DropinPage }; 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/ancv/ancv.spec.ts b/packages/e2e-playwright/tests/ancv/ancv.spec.ts index 735f8827d0..bdc56da0d8 100644 --- a/packages/e2e-playwright/tests/ancv/ancv.spec.ts +++ b/packages/e2e-playwright/tests/ancv/ancv.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from '../../pages/ancv/ancv.fixture'; import { createOrderMock } from '../../mocks/createOrder/createOrder.mock'; import { orderCreatedMockData } from '../../mocks/createOrder/createOrder.data'; import { paymentsMock } from '../../mocks/payments/payments.mock'; -import { paymentsActionAncvMockData, paymentsSuccessCardMockData } from '../../mocks/payments/payments.data'; +import { paymentsActionAncvMockData } from '../../mocks/payments/payments.data'; import { paymentDetailsMock } from '../../mocks/paymentDetails/paymentDetails.mock'; import { paymentDetailsPartiallyAuthorisedAncvMockData } from '../../mocks/paymentDetails/paymentDetails.data'; import { setupWithAncvOrderMockData } from '../../mocks/setup/setup.data'; @@ -10,35 +10,19 @@ import { statusMockData } from '../../mocks/status/status.data'; import { setupMock } from '../../mocks/setup/setup.mock'; import { statusMock } from '../../mocks/status/status.mock'; -// test('should display the await component on successful payment', async ({ ancvPage }) => { -// const { ancv } = ancvPage; -// -// await createOrderMock(ancv.page, orderCreatedMockData); -// await paymentsMock(ancv.page, paymentsActionAncvMockData); -// -// await ancv.fillInID('ancv-id@example.com'); -// await ancv.clickOnSubmit(); -// -// await expect(ancv.awaitText).toBeVisible(); -// }); +test.describe('ANCV - Sessions', () => { + test('should call onOrderCreated when payment is partially authorised (Sessions flow)', async ({ ancvPage }) => { + const { ancv, page } = ancvPage; -test('should display card component after handling onOrderCreated', async ({ ancvPage }) => { - const { ancv } = ancvPage; + await createOrderMock(page, orderCreatedMockData); + await paymentsMock(page, paymentsActionAncvMockData); + await statusMock(page, statusMockData); + await paymentDetailsMock(page, paymentDetailsPartiallyAuthorisedAncvMockData); + await setupMock(page, setupWithAncvOrderMockData); - await createOrderMock(ancv.page, orderCreatedMockData); - await paymentsMock(ancv.page, paymentsActionAncvMockData); + await ancv.fillInID('ancv-id@example.com'); + await ancv.clickOnSubmit(); - await ancv.fillInID('ancv-id@example.com'); - await ancv.clickOnSubmit(); - - await paymentDetailsMock(ancv.page, paymentDetailsPartiallyAuthorisedAncvMockData); - await setupMock(ancv.page, setupWithAncvOrderMockData); - - await statusMock(ancv.page, statusMockData); - - await paymentsMock(ancv.page, paymentsSuccessCardMockData); - - const cardDisclaimerText = ancv.page.getByText('All fields are required unless marked otherwise.'); - - await expect(cardDisclaimerText).toBeVisible(); + await expect(page.locator('#result-message')).toHaveText('Partially Authorised'); + }); }); 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/customCard/expiryDate/customCard.regular.expiryDatePolicies.hidden.spec.ts b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.regular.expiryDatePolicies.hidden.spec.ts new file mode 100644 index 0000000000..e9365271e3 --- /dev/null +++ b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.regular.expiryDatePolicies.hidden.spec.ts @@ -0,0 +1,128 @@ +import { test, expect } from '../../../pages/customCard/customCard.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 { hiddenDateAndCvcMock } from '../../../mocks/binLookup/binLookup.data'; +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 Custom Card Component with regular date field handles hidden expiryDate policy', () => { + test('#1 how UI & state respond', async ({ customCardPage }) => { + const { card, page } = customCardPage; + + await binLookupMock(page, hiddenDateAndCvcMock); + + await card.isComponentVisible(); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // UI reflects that binLookup says expiryDate & cvc are hidden + await expect(card.expiryDateField).not.toBeVisible(); + await expect(card.cvcField).not.toBeVisible(); + + // Card seen as valid + let cardValid = await page.evaluate('window.customCard.isValid'); + await expect(cardValid).toEqual(true); + + // Clear number and see UI & state reset + await card.deleteCardNumber(); + await expect(card.expiryDateField).toBeVisible(); + await expect(card.cvcField).toBeVisible(); + }); + + test('#2 validating fields first and then entering PAN should see errors cleared from state', async ({ customCardPage }) => { + const { card, page } = customCardPage; + + await binLookupMock(page, hiddenDateAndCvcMock); + + await card.isComponentVisible(); + + // Click pay + await customCardPage.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.customCard.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); + + // Expect errors to be cleared - since the fields were in error because they were empty + // but now the PAN field is filled and the other fields are hidden, so the fields have re-rendered and updated state + cardErrors = await page.evaluate('window.customCard.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); + + // Card seen as valid + let cardValid = await page.evaluate('window.customCard.isValid'); + await expect(cardValid).toEqual(true); + }); + + test('#3 date field in error does not stop card becoming valid', async ({ customCardPage }) => { + const { card, page } = customCardPage; + + await binLookupMock(page, hiddenDateAndCvcMock); + + 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 (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // UI reflects that binLookup says expiryDate & cvc are hidden + await expect(card.expiryDateField).not.toBeVisible(); + await expect(card.cvcField).not.toBeVisible(); + + // Card seen as valid (despite date field technically being in error) + let cardValid = await page.evaluate('window.customCard.isValid'); + await expect(cardValid).toEqual(true); + + // Expect error in state to remain + let cardErrors: any = await page.evaluate('window.customCard.state.errors'); + await expect(cardErrors[ENCRYPTED_EXPIRY_DATE]).not.toBe(undefined); + + 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); + + // CVC visible again + await expect(card.cvcField).toBeVisible(); + + // Headless test seems to need time for UI change to register on state + await page.waitForTimeout(500); + + // Card is not valid + cardValid = await page.evaluate('window.customCard.isValid'); + await expect(cardValid).toEqual(false); + + // await page.waitForTimeout(5000); + }); +}); diff --git a/packages/e2e-playwright/tests/customCard/expiryDate/customCard.regular.expiryDatePolicies.optional.spec.ts b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.regular.expiryDatePolicies.optional.spec.ts new file mode 100644 index 0000000000..eb184d274b --- /dev/null +++ b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.regular.expiryDatePolicies.optional.spec.ts @@ -0,0 +1,175 @@ +import { test, expect } from '../../../pages/customCard/customCard.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 Custom Card Component with regular date field handles hidden expiryDate policy', () => { + test('#1 how UI & state respond', async ({ customCardPage }) => { + const { card, page } = customCardPage; + + 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.customCard.isValid'); + await expect(cardValid).toEqual(true); + + // Clear number and see UI & state reset + await card.deleteCardNumber(); + + // Headless test seems to need time for UI change to register on state + await page.waitForTimeout(500); + + // date and cvc labels don't contain 'optional' + await expect(card.expiryDateLabelText).toHaveText(DATE_LABEL); + await expect(card.cvcLabelText).toHaveText(CVC_LABEL); + + // Card seen as invalid + cardValid = await page.evaluate('window.customCard.isValid'); + await expect(cardValid).toEqual(false); + + // await page.waitForTimeout(3000); + }); + + test('#2 how securedField responds', async ({ customCardPage }) => { + const { card, page } = customCardPage; + + 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 ({ customCardPage }) => { + const { card, page } = customCardPage; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isComponentVisible(); + + // press pay to generate errors + await customCardPage.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.customCard.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); + + // 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.customCard.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 ({ customCardPage }) => { + const { card, page } = customCardPage; + + 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.customCard.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.customCard.isValid'); + await expect(cardValid).toEqual(true); + }); +}); diff --git a/packages/e2e-playwright/tests/customCard/expiryDate/customCard.separate.expiryDatePolicies.hidden.spec.ts b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.separate.expiryDatePolicies.hidden.spec.ts new file mode 100644 index 0000000000..8729823aab --- /dev/null +++ b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.separate.expiryDatePolicies.hidden.spec.ts @@ -0,0 +1,143 @@ +import { test, expect } from '../../../pages/customCard/customCard.fixture'; +import { + ENCRYPTED_CARD_NUMBER, + ENCRYPTED_EXPIRY_MONTH, + ENCRYPTED_EXPIRY_YEAR, + ENCRYPTED_SECURITY_CODE, + REGULAR_TEST_CARD +} from '../../utils/constants'; +import { binLookupMock } from '../../../mocks/binLookup/binLookup.mock'; +import { hiddenDateAndCvcMock } from '../../../mocks/binLookup/binLookup.data'; +import LANG from '../../../../lib/src/language/locales/en-US.json'; + +const PAN_ERROR = LANG['error.va.sf-cc-num.02']; +const MONTH_EMPTY_ERROR = LANG['error.va.sf-cc-mth.01']; +const YEAR_EMPTY_ERROR = LANG['error.va.sf-cc-yr.01']; +const DATE_INVALID_ERROR = LANG['error.va.sf-cc-dat.02']; +const CVC_ERROR = LANG['error.va.sf-cc-cvc.01']; + +test.describe('Test how Custom Card Component with separate date field handles hidden expiryDate policy', () => { + test('#1 how UI & state respond', async ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, hiddenDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // UI reflects that binLookup says date fields & cvc are hidden + await expect(card.expiryMonthField).not.toBeVisible(); + await expect(card.expiryYearField).not.toBeVisible(); + await expect(card.cvcField).not.toBeVisible(); + + // Card seen as valid + let cardValid = await page.evaluate('window.customCardSeparate.isValid'); + await expect(cardValid).toEqual(true); + + // Clear number and see UI & state reset + await card.deleteCardNumber(); + await expect(card.expiryMonthField).toBeVisible(); + await expect(card.expiryYearField).toBeVisible(); + await expect(card.cvcField).toBeVisible(); + }); + + test('#2 validating fields first and then entering PAN should see errors cleared from state', async ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, hiddenDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // Click pay + await customCardPageSeparate.pay('Separate'); + + // Expect errors in UI + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR); + await expect(card.expiryMonthErrorElement).toBeVisible(); + await expect(card.expiryMonthErrorElement).toHaveText(MONTH_EMPTY_ERROR); + await expect(card.expiryYearErrorElement).toBeVisible(); + await expect(card.expiryYearErrorElement).toHaveText(YEAR_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.customCardSeparate.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_EXPIRY_MONTH]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_EXPIRY_YEAR]).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); + + // Expect errors to be cleared - since the fields were in error because they were empty + // but now the PAN field is filled and the other fields are hidden, so the fields have re-rendered and updated state + cardErrors = await page.evaluate('window.customCardSeparate.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).toBe(null); + await expect(cardErrors[ENCRYPTED_EXPIRY_MONTH]).toBe(null); + await expect(cardErrors[ENCRYPTED_EXPIRY_YEAR]).toBe(null); + await expect(cardErrors[ENCRYPTED_SECURITY_CODE]).toBe(null); + + // Card seen as valid + let cardValid = await page.evaluate('window.customCardSeparate.isValid'); + await expect(cardValid).toEqual(true); + }); + + test('#3 date field in error does not stop card becoming valid', async ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, hiddenDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // Card out of date + await card.typeExpiryMonth('12'); + await card.typeExpiryYear('90'); + + // Expect error in UI + await expect(card.expiryYearErrorElement).toBeVisible(); + await expect(card.expiryYearErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Force blur event to fire on year field + await card.cardNumberLabelElement.click(); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // UI reflects that binLookup says date fields & cvc are hidden + await expect(card.expiryMonthField).not.toBeVisible(); + await expect(card.expiryYearField).not.toBeVisible(); + await expect(card.cvcField).not.toBeVisible(); + + // Card seen as valid (despite date technically being in error) + let cardValid = await page.evaluate('window.customCardSeparate.isValid'); + await expect(cardValid).toEqual(true); + + // Expect error in state to remain + let cardErrors: any = await page.evaluate('window.customCardSeparate.state.errors'); + await expect(cardErrors[ENCRYPTED_EXPIRY_YEAR]).not.toBe(undefined); + + await card.deleteCardNumber(); + + // Errors in UI visible again + await expect(card.expiryYearField).toBeVisible(); + await expect(card.expiryYearErrorElement).toBeVisible(); + await expect(card.expiryYearErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Other fields visible again + await expect(card.expiryMonthField).toBeVisible(); + await expect(card.cvcField).toBeVisible(); + + // Headless test seems to need time for UI change to register on state + await page.waitForTimeout(500); + + // Card is not valid + cardValid = await page.evaluate('window.customCardSeparate.isValid'); + await expect(cardValid).toEqual(false); + + // await page.waitForTimeout(3000); + }); +}); diff --git a/packages/e2e-playwright/tests/customCard/expiryDate/customCard.separate.expiryDatePolicies.optional.spec.ts b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.separate.expiryDatePolicies.optional.spec.ts new file mode 100644 index 0000000000..84a5f64d2b --- /dev/null +++ b/packages/e2e-playwright/tests/customCard/expiryDate/customCard.separate.expiryDatePolicies.optional.spec.ts @@ -0,0 +1,208 @@ +import { test, expect } from '../../../pages/customCard/customCard.fixture'; +import { + ENCRYPTED_CARD_NUMBER, + ENCRYPTED_EXPIRY_MONTH, + ENCRYPTED_EXPIRY_YEAR, + 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 MONTH_LABEL = 'Expiry month'; +const YEAR_LABEL = 'Expiry year'; +const CVC_LABEL = 'Security code'; +const OPTIONAL = LANG['field.title.optional']; + +const PAN_ERROR = LANG['error.va.sf-cc-num.02']; +const MONTH_EMPTY_ERROR = LANG['error.va.sf-cc-mth.01']; +const YEAR_EMPTY_ERROR = LANG['error.va.sf-cc-yr.01']; +const CVC_ERROR = LANG['error.va.sf-cc-cvc.01']; +const DATE_INVALID_ERROR = LANG['error.va.sf-cc-dat.02']; + +test.describe('Test how Custom Card Component with separate date fields handles hidden expiryDate policy', () => { + test('#1 how UI & state respond', async ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // Regular date labels + await expect(card.expiryMonthLabelText).toHaveText(MONTH_LABEL); + await expect(card.expiryYearLabelText).toHaveText(YEAR_LABEL); + + // Fill number to provoke (mock) binLookup response + await card.typeCardNumber(REGULAR_TEST_CARD); + + // await page.waitForTimeout(5000); + + // UI reflects that binLookup says date fields are optional + await expect(card.expiryMonthLabelText).toHaveText(`${MONTH_LABEL} ${OPTIONAL}`); + await expect(card.expiryYearLabelText).toHaveText(`${YEAR_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.customCardSeparate.isValid'); + await expect(cardValid).toEqual(true); + + // Clear number and see UI & state reset + await card.deleteCardNumber(); + + // Headless test seems to need time for UI change to register on state + await page.waitForTimeout(500); + + // date and cvc labels don't contain 'optional' + await expect(card.expiryMonthLabelText).toHaveText(MONTH_LABEL); + await expect(card.expiryYearLabelText).toHaveText(YEAR_LABEL); + await expect(card.cvcLabelText).toHaveText(CVC_LABEL); + + // Card seen as invalid + cardValid = await page.evaluate('window.customCard.isValid'); + await expect(cardValid).toEqual(false); + }); + + test('#2 how securedField responds', async ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // Expect iframe's date (& cvc) input fields to have an aria-required attr set to true + let monthAriaRequired = await card.expiryMonthInput.getAttribute('aria-required'); + await expect(monthAriaRequired).toEqual('true'); + + let yearAriaRequired = await card.expiryYearInput.getAttribute('aria-required'); + await expect(yearAriaRequired).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 date (& cvc) input fields to have an aria-required attr set to false + monthAriaRequired = await card.expiryMonthInput.getAttribute('aria-required'); + await expect(monthAriaRequired).toEqual('false'); + + yearAriaRequired = await card.expiryYearInput.getAttribute('aria-required'); + await expect(yearAriaRequired).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(); + + monthAriaRequired = await card.expiryMonthInput.getAttribute('aria-required'); + await expect(monthAriaRequired).toEqual('true'); + + yearAriaRequired = await card.expiryYearInput.getAttribute('aria-required'); + await expect(yearAriaRequired).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 ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // press pay to generate errors + await customCardPageSeparate.pay('Separate'); + + // Expect errors in UI + await expect(card.cardNumberErrorElement).toBeVisible(); + await expect(card.cardNumberErrorElement).toHaveText(PAN_ERROR); + await expect(card.expiryMonthErrorElement).toBeVisible(); + await expect(card.expiryMonthErrorElement).toHaveText(MONTH_EMPTY_ERROR); + await expect(card.expiryYearErrorElement).toBeVisible(); + await expect(card.expiryYearErrorElement).toHaveText(YEAR_EMPTY_ERROR); + await expect(card.cvcErrorElement).toBeVisible(); + await expect(card.cvcErrorElement).toHaveText(CVC_ERROR); + + // await page.waitForTimeout(3000); + + // Expect errors in state + let cardErrors: any = await page.evaluate('window.customCardSeparate.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_EXPIRY_MONTH]).not.toBe(undefined); + await expect(cardErrors[ENCRYPTED_EXPIRY_YEAR]).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); + + // 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.expiryMonthErrorElement).not.toBeVisible(); + await expect(card.expiryYearErrorElement).not.toBeVisible(); + await expect(card.cvcErrorElement).not.toBeVisible(); + + // No errors in state + cardErrors = await page.evaluate('window.customCardSeparate.state.errors'); + await expect(cardErrors[ENCRYPTED_CARD_NUMBER]).toBe(null); + await expect(cardErrors[ENCRYPTED_EXPIRY_MONTH]).toBe(null); + await expect(cardErrors[ENCRYPTED_EXPIRY_YEAR]).toBe(null); + await expect(cardErrors[ENCRYPTED_SECURITY_CODE]).toBe(null); + + // Card seen as valid - now we have a PAN and all the other fields are optional + let cardValid = await page.evaluate('window.customCardSeparate.isValid'); + await expect(cardValid).toEqual(true); + }); + + test('#4 date field in error DOES stop card becoming valid', async ({ customCardPageSeparate }) => { + const { card, page } = customCardPageSeparate; + + await binLookupMock(page, optionalDateAndCvcMock); + + await card.isSeparateComponentVisible(); + + // Card out of date + await card.typeExpiryMonth('12'); + await card.typeExpiryYear('90'); + + // Expect error in UI + await expect(card.expiryYearErrorElement).toBeVisible(); + await expect(card.expiryYearErrorElement).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.expiryMonthLabelText).toHaveText(`${MONTH_LABEL} ${OPTIONAL}`); + await expect(card.expiryYearLabelText).toHaveText(`${YEAR_LABEL} ${OPTIONAL}`); + + // Visual errors persist in UI + await expect(card.expiryYearErrorElement).toBeVisible(); + await expect(card.expiryYearErrorElement).toHaveText(DATE_INVALID_ERROR); + + // Card seen as invalid + let cardValid = await page.evaluate('window.customCardSeparate.isValid'); + await expect(cardValid).toEqual(false); + + // Delete erroneous date + await card.deleteExpiryMonth(); + await card.deleteExpiryYear(); + + // 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.customCardSeparate.isValid'); + await expect(cardValid).toEqual(true); + }); +}); diff --git a/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.compact.excluded.spec.ts b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.compact.excluded.spec.ts new file mode 100644 index 0000000000..d05bde957e --- /dev/null +++ b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.compact.excluded.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '../../../pages/dropin/dropin.fixture'; +import { getCreditCardPM } from '../../../models/dropinModelUtils/getDropinCardComp'; + +test.describe('Dropin: How Credit Card brand logos display with "showBrandsUnderCardNumber" set to its default, true; when "excluded" brands exists', () => { + test('#1 A truncated list of brands show up on the Card Payment Method and excluded items are also removed', async ({ + dropinPage_cardBrands_compactView_withExcluded + }) => { + const { dropin, page } = dropinPage_cardBrands_compactView_withExcluded; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + const imgCount = await creditCard.getImageCount(creditCard.brandsHolder); + + await expect(creditCard.brandsHolder).toBeVisible(); + + await expect(imgCount).toEqual(3); + + await expect(creditCard.brandsText).toBeVisible(); + await expect(creditCard.brandsText).toHaveText('+3'); + }); + + test('#2 After clicking on the Card PaymentMethodItem, the brands disappear from the header and show beneath Card Number (with excluded brands absent)', async ({ + dropinPage_cardBrands_compactView_withExcluded + }) => { + const { dropin, page } = dropinPage_cardBrands_compactView_withExcluded; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + await creditCard.pm.click(); + + await expect(creditCard.brandsHolder).not.toBeVisible(); + await expect(creditCard.brandsText).not.toBeVisible(); + + // Brands inside actual Credit Card component + const imgCount = await creditCard.getImageCount(creditCard.componentBrandsHolder); + + await expect(imgCount).toEqual(6); + }); +}); diff --git a/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.compact.spec.ts b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.compact.spec.ts new file mode 100644 index 0000000000..5336fee14d --- /dev/null +++ b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.compact.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '../../../pages/dropin/dropin.fixture'; +import { getCreditCardPM } from '../../../models/dropinModelUtils/getDropinCardComp'; + +test.describe('Dropin: How Credit Card brand logos display with "showBrandsUnderCardNumber" set to its default, true', () => { + test('#1 A truncated list of brands show up on the Card Payment Method', async ({ dropinPage_cardBrands_compactView }) => { + const { dropin, page } = dropinPage_cardBrands_compactView; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + const imgCount = await creditCard.getImageCount(creditCard.brandsHolder); + + await expect(creditCard.brandsHolder).toBeVisible(); + + await expect(imgCount).toEqual(3); + + await expect(creditCard.brandsText).toBeVisible(); + await expect(creditCard.brandsText).toHaveText('+7'); + }); + + test('#2 After clicking on the Card PaymentMethodItem, the brands disappear from the header and show beneath Card Number', async ({ + dropinPage_cardBrands_compactView + }) => { + const { dropin, page } = dropinPage_cardBrands_compactView; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + await creditCard.pm.click(); + + await expect(creditCard.brandsHolder).not.toBeVisible(); + await expect(creditCard.brandsText).not.toBeVisible(); + + // Brands inside actual Credit Card component + const imgCount = await creditCard.getImageCount(creditCard.componentBrandsHolder); + + await expect(imgCount).toEqual(10); + }); +}); diff --git a/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.default.excluded.spec.ts b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.default.excluded.spec.ts new file mode 100644 index 0000000000..1fc7e21113 --- /dev/null +++ b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.default.excluded.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '../../../pages/dropin/dropin.fixture'; +import { getCreditCardPM } from '../../../models/dropinModelUtils/getDropinCardComp'; + +test.describe('Dropin: How Credit Card brand logos display with "showBrandsUnderCardNumber" equals false when "excluded" brands exists', () => { + test('#1 Only a subset of brands show up on the Card PaymentMethodItem', async ({ dropinPage_cardBrands_defaultView_withExcluded }) => { + const { dropin, page } = dropinPage_cardBrands_defaultView_withExcluded; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + const imgCount = await creditCard.getImageCount(creditCard.brandsHolder); + + await expect(creditCard.brandsHolder).toBeVisible(); + + await expect(imgCount).toEqual(6); + + await expect(creditCard.brandsText).not.toBeVisible(); + }); + + test('#2 Only a subset of brands are kept in the Card PaymentMethodItem after clicking on it', async ({ + dropinPage_cardBrands_defaultView_withExcluded + }) => { + const { dropin, page } = dropinPage_cardBrands_defaultView_withExcluded; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + await creditCard.pm.click(); + + const imgCount = await creditCard.getImageCount(creditCard.brandsHolder); + + await expect(creditCard.brandsHolder).toBeVisible(); + + await expect(imgCount).toEqual(6); + + // Brands inside actual Credit Card component + await expect(creditCard.componentBrandsHolder).not.toBeVisible(); + }); +}); diff --git a/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.default.spec.ts b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.default.spec.ts new file mode 100644 index 0000000000..d498df0349 --- /dev/null +++ b/packages/e2e-playwright/tests/dropin/cardBrands/cardBrands.default.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from '../../../pages/dropin/dropin.fixture'; +import { getCreditCardPM } from '../../../models/dropinModelUtils/getDropinCardComp'; + +test.describe('Dropin: How Credit Card brand logos display with "showBrandsUnderCardNumber" equals false', () => { + test('#1 All available brands show up on the Card PaymentMethodItem', async ({ dropinPage_cardBrands_defaultView }) => { + const { dropin, page } = dropinPage_cardBrands_defaultView; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + const imgCount = await creditCard.getImageCount(creditCard.brandsHolder); + + await expect(creditCard.brandsHolder).toBeVisible(); + + await expect(imgCount).toEqual(10); + + await expect(creditCard.brandsText).not.toBeVisible(); + }); + + test('#2 All available brands are kept in the Card PaymentMethodItem after clicking on it', async ({ dropinPage_cardBrands_defaultView }) => { + const { dropin, page } = dropinPage_cardBrands_defaultView; + + await dropin.isComponentVisible(); + + const creditCard = getCreditCardPM(dropin); + await creditCard.pm.scrollIntoViewIfNeeded(); + + await creditCard.pm.click(); + + const imgCount = await creditCard.getImageCount(creditCard.brandsHolder); + + await expect(creditCard.brandsHolder).toBeVisible(); + + await expect(imgCount).toEqual(10); + + // Brands inside actual Credit Card component + await expect(creditCard.componentBrandsHolder).not.toBeVisible(); + }); +}); 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/image.ts b/packages/e2e-playwright/tests/utils/image.ts new file mode 100644 index 0000000000..48bbca666a --- /dev/null +++ b/packages/e2e-playwright/tests/utils/image.ts @@ -0,0 +1,5 @@ +import { Locator } from '@playwright/test'; + +export const getImageCount = async (who: Locator) => { + return await who.getByRole('img').count(); +}; 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 }); +}; diff --git a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.clientScripts.js b/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.clientScripts.js deleted file mode 100644 index 03d2c88fcf..0000000000 --- a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.clientScripts.js +++ /dev/null @@ -1,7 +0,0 @@ -window.mainConfiguration = { - paymentMethodsConfiguration: { - card: { - showBrandsUnderCardNumber: false - } - } -}; diff --git a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.compactView.excluded.test.js b/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.compactView.excluded.test.js deleted file mode 100644 index dcccce02a3..0000000000 --- a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.compactView.excluded.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Selector } from 'testcafe'; -import { DROPIN_SESSIONS_URL } from '../../../pages'; -import { getMock } from './availableBrands.mocks'; -import DropinPage from '../../../_models/Dropin.page'; - -let dropinPage = null; - -fixture`Cards - Available Brands ("with" excluded brands) (Compact view)` - .page(DROPIN_SESSIONS_URL) - .requestHooks([getMock('setupResponseWithExcludedBrands')]) - .beforeEach(() => { - dropinPage = new DropinPage({}); - }); - -test('#1 Not all brands show on the header of the payment method item, and excluded items are also removed', async t => { - await dropinPage.brandsHolder(); - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - - await t.expect(paymentItem.hasBrands).ok(); - await t.expect(paymentItem.extraBrandsText).eql('+3'); - await t.expect(paymentItem.numberOfBrandImages).eql(3); -}); - -test('#2 Clicking on the Payment method, brands disappear from header and show beneath Card Number (with excluded brands absent)', async t => { - await dropinPage.brandsHolder(); - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - await paymentItem.click(); - - await t.expect(paymentItem.hasBrands).notOk(); - const brandsInsidePaymentMethod = Selector('.adyen-checkout__card__brands'); - await t.expect(brandsInsidePaymentMethod.find('img').count).eql(6); -}); diff --git a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.compactView.test.js b/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.compactView.test.js deleted file mode 100644 index 8a161d2713..0000000000 --- a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.compactView.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { Selector } from 'testcafe'; -import { DROPIN_SESSIONS_URL } from '../../../pages'; -import { getMock } from './availableBrands.mocks'; -import DropinPage from '../../../_models/Dropin.page'; - -let dropinPage = null; - -fixture`Cards - Available Brands (Compact view)` - .page(DROPIN_SESSIONS_URL) - .requestHooks([getMock()]) - .beforeEach(() => { - dropinPage = new DropinPage({}); - }); - -test('Not all brands show on the header of the payment method item', async t => { - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - - await t.expect(paymentItem.hasBrands).ok(); - await t.expect(paymentItem.extraBrandsText).eql('+7'); - await t.expect(paymentItem.numberOfBrandImages).eql(3); -}); - -test('Clicking on the Payment method, brands disappear from header and show beneath Card Number', async t => { - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - await paymentItem.click(); - - await t.expect(paymentItem.hasBrands).notOk(); - const brandsInsidePaymentMethod = Selector('.adyen-checkout__card__brands'); - await t.expect(brandsInsidePaymentMethod.find('img').count).eql(10); -}); diff --git a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.defaultView.excluded.test.js b/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.defaultView.excluded.test.js deleted file mode 100644 index 7738918e85..0000000000 --- a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.defaultView.excluded.test.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Selector } from 'testcafe'; -import { DROPIN_SESSIONS_URL } from '../../../pages'; -import { getMock } from './availableBrands.mocks'; -import DropinPage from '../../../_models/Dropin.page'; - -let dropinPage = null; - -fixture`Cards - Available Brands ("with" excluded brands) (Default view)` - .page(DROPIN_SESSIONS_URL) - .requestHooks([getMock('setupResponseWithExcludedBrands')]) - .clientScripts('./availableBrands.clientScripts.js') - .beforeEach(() => { - dropinPage = new DropinPage({}); - }); - -test('All available brands show up on the Payment Method Item', async t => { - await dropinPage.brandsHolder(); - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - - await t.expect(paymentItem.hasBrands).ok(); - await t.expect(paymentItem.extraBrandsText).eql(''); - await t.expect(paymentItem.numberOfBrandImages).eql(6); -}); - -test('Brands are kept in the Payment Method Item after clicking on it', async t => { - await dropinPage.brandsHolder(); - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - await paymentItem.click(); - - await t.expect(paymentItem.hasBrands).ok(); - await t.expect(paymentItem.numberOfBrandImages).eql(6); - - const brandsInsidePaymentMethod = Selector('.adyen-checkout__card__brands'); - await t.expect(brandsInsidePaymentMethod.find('img').count).eql(0); -}); diff --git a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.defaultView.test.js b/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.defaultView.test.js deleted file mode 100644 index ea4db24f0b..0000000000 --- a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.defaultView.test.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Selector } from 'testcafe'; -import { DROPIN_SESSIONS_URL } from '../../../pages'; -import { getMock } from './availableBrands.mocks'; -import DropinPage from '../../../_models/Dropin.page'; - -let dropinPage = null; - -fixture`Cards - Available Brands (Default view)` - .page(DROPIN_SESSIONS_URL) - .requestHooks([getMock()]) - .clientScripts('./availableBrands.clientScripts.js') - .beforeEach(() => { - dropinPage = new DropinPage({}); - }); - -test('#1 All available brands show up on the Payment Method Item', async t => { - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - - await t.expect(paymentItem.hasBrands).ok(); - await t.expect(paymentItem.extraBrandsText).eql(''); - await t.expect(paymentItem.numberOfBrandImages).eql(10); -}); - -test('#2 Brands are kept in the Payment Method Item after clicking on it', async t => { - const paymentItem = dropinPage.getPaymentMethodItemSelector('Credit Card'); - await paymentItem.click(); - - await t.expect(paymentItem.hasBrands).ok(); - await t.expect(paymentItem.numberOfBrandImages).eql(10); - - const brandsInsidePaymentMethod = Selector('.adyen-checkout__card__brands'); - await t.expect(brandsInsidePaymentMethod.find('img').count).eql(0); -}); diff --git a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.mocks.js b/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.mocks.js deleted file mode 100644 index 42eab31f34..0000000000 --- a/packages/e2e/tests/cards/availableBrands/dropin/availableBrands.mocks.js +++ /dev/null @@ -1,93 +0,0 @@ -import { RequestMock } from 'testcafe'; -import { BASE_URL } from '../../../pages'; - -const path = require('path'); -require('dotenv').config({ path: path.resolve('../../', '.env') }); - -const MOCK_SESSION_ID = 'CS616D08FC28573F9C'; -const MOCK_SESSION_DATA = 'Ab02b4c0!BQABAgChW9EQ6U'; - -const sessionsUrl = 'http://localhost:3024/sessions'; -const setupUrl = `https://checkoutshopper-test.adyen.com/checkoutshopper/v1/sessions/${MOCK_SESSION_ID}/setup?clientKey=${process.env.CLIENT_KEY}`; - -const sessionsResponse = { - amount: { - currency: 'USD', - value: 25900 - }, - countryCode: 'US', - expiresAt: '2021-10-15T13:02:27+02:00', - id: MOCK_SESSION_ID, - merchantAccount: 'TestMerchantCheckout', - reference: 'ABC123', - returnUrl: 'http://localhost:3024/result', - shopperLocale: 'en-US', - shopperReference: 'newshoppert', - sessionData: MOCK_SESSION_DATA -}; - -const setupResponseRegular = { - amount: { - currency: 'USD', - value: 25900 - }, - countryCode: 'US', - expiresAt: '2021-10-15T13:02:27+02:00', - id: MOCK_SESSION_ID, - returnUrl: 'http://localhost:3024/result', - shopperLocale: 'en-US', - paymentMethods: { - paymentMethods: [ - { - brand: 'genericgiftcard', - name: 'Generic GiftCard', - type: 'giftcard' - }, - { - brands: ['visa', 'mc', 'amex', 'discover', 'cup', 'maestro', 'bijcard', 'diners', 'jcb', 'synchrony_cbcc'], - name: 'Credit Card', - type: 'scheme' - } - ] - }, - sessionData: MOCK_SESSION_DATA -}; - -const setupResponseWithExcludedBrands = { - amount: { - currency: 'USD', - value: 25900 - }, - countryCode: 'US', - expiresAt: '2021-10-15T13:02:27+02:00', - id: MOCK_SESSION_ID, - returnUrl: 'http://localhost:3024/result', - shopperLocale: 'en-US', - paymentMethods: { - paymentMethods: [ - { - brand: 'genericgiftcard', - name: 'Generic GiftCard', - type: 'giftcard' - }, - { - brands: ['visa', 'mc', 'amex', 'discover', 'cup', 'maestro', 'nyce', 'accel', 'star', 'pulse'], - name: 'Credit Card', - type: 'scheme' - } - ] - }, - sessionData: MOCK_SESSION_DATA -}; - -const setUpResponseObject = { setupResponseRegular, setupResponseWithExcludedBrands }; - -const getMock = (setupResponse = 'setupResponseRegular') => { - return RequestMock() - .onRequestTo(request => request.url === sessionsUrl) - .respond(sessionsResponse, 200, { 'Access-Control-Allow-Origin': BASE_URL }) - .onRequestTo(request => request.url === setupUrl && request.method === 'post') - .respond(setUpResponseObject[setupResponse], 200, { 'Access-Control-Allow-Origin': BASE_URL }); -}; - -export { getMock, MOCK_SESSION_DATA }; diff --git a/packages/e2e/tests/cards/branding/branding.clientScripts.js b/packages/e2e/tests/cards/branding/branding.clientScripts.js deleted file mode 100644 index f23d9a56d8..0000000000 --- a/packages/e2e/tests/cards/branding/branding.clientScripts.js +++ /dev/null @@ -1,4 +0,0 @@ -window.cardConfig = { - type: 'scheme', - brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc'] -}; diff --git a/packages/e2e/tests/cards/branding/branding.test.js b/packages/e2e/tests/cards/branding/branding.test.js deleted file mode 100644 index 7890e1e649..0000000000 --- a/packages/e2e/tests/cards/branding/branding.test.js +++ /dev/null @@ -1,224 +0,0 @@ -import { ClientFunction, Selector } from 'testcafe'; -import { start, getIframeSelector, getIsValid } from '../../utils/commonUtils'; -import cu from '../utils/cardUtils'; -import { CARDS_URL } from '../../pages'; -import { MAESTRO_CARD, TEST_DATE_VALUE, TEST_CVC_VALUE, BCMC_CARD } from '../utils/constants'; - -const cvcSpan = Selector('.card-field .adyen-checkout__field__cvc'); -const optionalCVCSpan = Selector('.card-field .adyen-checkout__field__cvc--optional'); -const cvcLabel = Selector('.card-field .adyen-checkout__label__text'); -const brandingIcon = Selector('.card-field .adyen-checkout__card__cardNumber__brandIcon'); - -const dualBrandingIconHolderActive = Selector('.card-field .adyen-checkout__card__dual-branding__buttons--active'); - -const getPropFromPMData = ClientFunction(prop => { - return window.card.formatData().paymentMethod[prop]; -}); - -/** - * Needed for hack detailed in 3rd test, below - */ -const setForceClick = ClientFunction(val => { - window.testCafeForceClick = val; -}); - -const TEST_SPEED = 1; - -const iframeSelector = getIframeSelector('.card-field iframe'); - -const cardUtils = cu(iframeSelector); - -fixture`Testing branding - especially regarding optional and hidden cvc fields`.page(CARDS_URL).clientScripts('branding.clientScripts.js'); - -test( - 'Test for generic card icon, ' + - '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 t => { - // Start, allow time for iframes to load - await start(t, 2000, TEST_SPEED); - - await t - // generic card icon - .expect(brandingIcon.getAttribute('src')) - .contains('nocard.svg') - - // visible cvc field - .expect(cvcSpan.filterVisible().exists) - .ok() - - // with regular text - .expect(cvcLabel.withExactText('Security code').exists) - .ok() - - // and not optional - .expect(optionalCVCSpan.exists) - .notOk(); - - // Partially fill card field with digits that will be recognised as maestro - await cardUtils.fillCardNumber(t, '670'); - - await t - // maestro card icon - .expect(brandingIcon.getAttribute('src')) - .contains('maestro.svg') - - // with "optional" text - .expect(cvcLabel.withExactText('Security code (optional)').exists) - .ok() - // and optional class - .expect(optionalCVCSpan.exists) - .ok(); - - await t.wait(500); - - // Add digit so card is recognised as bcmc - await cardUtils.fillCardNumber(t, '3'); - - await t - // bcmc icon - .expect(brandingIcon.getAttribute('src')) - .contains('bcmc.svg') - - // hidden cvc field - .expect(cvcSpan.filterHidden().exists) - .ok(); - - await t.wait(500); - - // Delete number - await cardUtils.deleteCardNumber(t); - - // Card is reset - await t - // generic card icon - .expect(brandingIcon.getAttribute('src')) - .contains('nocard.svg') - - // visible cvc field - .expect(cvcSpan.filterVisible().exists) - .ok() - - // with regular text - .expect(cvcLabel.withExactText('Security code').exists) - .ok() - - // and not optional - .expect(optionalCVCSpan.exists) - .notOk(); - } -); - -test('Test card is valid with maestro details (cvc optional) ' + 'then test it is invalid (& brand reset) when number deleted', async t => { - // Start, allow time for iframes to load - await start(t, 2000, TEST_SPEED); - - // generic card - await t.expect(brandingIcon.getAttribute('src')).contains('nocard.svg'); - - // Maestro - await cardUtils.fillCardNumber(t, MAESTRO_CARD); - await cardUtils.fillDate(t, TEST_DATE_VALUE); - - await t - // maestro card icon - .expect(brandingIcon.getAttribute('src')) - .contains('maestro.svg') - - // with "optional" text - .expect(cvcLabel.withExactText('Security code (optional)').exists) - .ok() - // and optional class - .expect(optionalCVCSpan.exists) - .ok(); - - await t.expect(getIsValid('card')).eql(true); - - // add cvc - await cardUtils.fillCVC(t, TEST_CVC_VALUE); - - // Is valid - await t.expect(getIsValid('card')).eql(true); - - // Delete number - await cardUtils.deleteCardNumber(t); - - // Card is reset - await t - // generic card icon - .expect(brandingIcon.getAttribute('src')) - .contains('nocard.svg'); - - // Is not valid - await t.expect(getIsValid('card')).eql(false); -}); - -/** - * 3rd Test - * - * NOTE: test doesn't work properly - the click away from the CVC field is not triggering a blur event within the securedField - * so the error event & focus:false event from the iframe are never sent. - * - * However if you run localhost:3024 and recreate the steps in the test, it does create the expected error - so this seems to be be a bug with TestCafe. - * - * The solution is a HORRIBLE HORRIBLE HACK - the setForceClick function - which sets a flag var that SecuredField.ts will look for if the url - * is running at the test port of 3024. Then SecuredField will call it's own onClickCallback - which was created to solve problems with iOS not - * registering iframes losing focus - */ -test( - '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 t => { - // Start, allow time for iframes to load - await start(t, 2000, TEST_SPEED); - - // generic card - await t.expect(brandingIcon.getAttribute('src')).contains('nocard.svg'); - - // Maestro - await cardUtils.fillCardNumber(t, MAESTRO_CARD); - await cardUtils.fillDate(t, TEST_DATE_VALUE); - - await t - // maestro card icon - .expect(brandingIcon.getAttribute('src')) - .contains('maestro.svg') - - // with "optional" text - .expect(cvcLabel.withExactText('Security code (optional)').exists) - .ok() - // and optional class - .expect(optionalCVCSpan.exists) - .ok(); - - // Partial cvc - await cardUtils.fillCVC(t, '73'); - - await setForceClick(true); - - // Click label - to force blur event that will trigger error and reset card.isValid - await t - .click('.adyen-checkout__label__text') - // Expect error - .expect(Selector('.adyen-checkout__field--error').exists) - .ok() - .wait(2000); - - // Is not valid - await t.expect(getIsValid('card')).eql(false); - - // Complete cvc - await cardUtils.fillCVC(t, '7'); - - // Is valid - await t.expect(getIsValid('card')).eql(true); - - // Delete CVC - await cardUtils.deleteCVC(t); - - // Is valid - await t.expect(getIsValid('card')).eql(true); - } -); diff --git a/packages/e2e/tests/cards/expiryDate/expiryDatePolicies.hidden.test.js b/packages/e2e/tests/cards/expiryDate/expiryDatePolicies.hidden.test.js deleted file mode 100644 index d268e3e965..0000000000 --- a/packages/e2e/tests/cards/expiryDate/expiryDatePolicies.hidden.test.js +++ /dev/null @@ -1,107 +0,0 @@ -import CardComponentPage from '../../_models/CardComponent.page'; -import { SYNCHRONY_PLCC_NO_DATE, TEST_CVC_VALUE } from '../utils/constants'; -import { turnOffSDKMocking } from '../../_common/cardMocks'; - -const cardPage = new CardComponentPage(); - -fixture`Test how Card Component handles hidden expiryDate policy` - .beforeEach(async t => { - await t.navigateTo(cardPage.pageUrl); - await turnOffSDKMocking(); - }) - .clientScripts('./expiryDate.clientScripts.js'); - -test('#1 Testing hidden expiryDatePolicy - how UI & state respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Fill number to provoke binLookup response - await cardPage.cardUtils.fillCardNumber(t, SYNCHRONY_PLCC_NO_DATE); - - // UI reflects that binLookup says expiryDate is hidden - await t.expect(cardPage.dateHolder.filterHidden().exists).ok(); - - // Card seen as valid (since CVC is hidden too) - await t.expect(cardPage.getFromState('isValid')).eql(false); - - // Clear number and see UI & state reset - await cardPage.cardUtils.deleteCardNumber(t); - await t - .expect(cardPage.dateHolder.filterVisible().exists) - .ok() - .expect(cardPage.cvcHolder.filterVisible().exists) - .ok() - .expect(cardPage.getFromState('isValid')) - .eql(false); -}); - -test('#2 Testing hidden expiryDatePolicy - validating fields first and then entering PAN should see errors cleared from state', async t => { - // Start, allow time for iframes to load so isValidated call to SF won't fail - await t.wait(1000); - - // Click pay - await t.click(cardPage.payButton); - - // Expect errors in UI - await t - .expect(cardPage.numLabelTextError.exists) - .ok() - .expect(cardPage.dateLabelTextError.exists) - .ok() - .expect(cardPage.cvcLabelTextError.exists) - .ok(); - - // Expect errors in (mapped) state - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(undefined) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .notEql(null); - - // Fill number to provoke binLookup response - await cardPage.cardUtils.fillCardNumber(t, SYNCHRONY_PLCC_NO_DATE, 'paste'); // TODO - shouldn't have to 'paste' here... but Testcafe is being flaky, again! - - // Expect errors to be cleared - since the fields were in error because they were empty - // and now the PAN field is filled and the date fields is now hidden... - - // ...State errors cleared for date - await t.expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')).eql(null); -}); - -test('#3 Testing hidden expiryDatePolicy - date field in error does not stop card becoming valid', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Card out of date - await cardPage.cardUtils.fillDate(t, '12/90'); - - // Expect errors in UI - await t.expect(cardPage.dateLabelTextError.exists).ok(); - - // Force blur event to fire on date field - await cardPage.setForceClick(true); - - // Fill number to provoke binLookup response - await cardPage.cardUtils.fillCardNumber(t, SYNCHRONY_PLCC_NO_DATE); - await cardPage.cardUtils.fillCVC(t, TEST_CVC_VALUE); - - // UI reflects that binLookup says expiryDate is hidden - await t.expect(cardPage.dateHolder.filterHidden().exists).ok(); - - // Card seen as valid (despite date field technically being in error) - await t.expect(cardPage.getFromState('isValid')).eql(true); - - // Expect errors in (mapped) state to remain - await t.expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')).notEql(null); - - // Delete number - await cardPage.cardUtils.deleteCardNumber(t); - - // Errors in UI visible again - await t.expect(cardPage.dateLabelTextError.exists).ok(); - - // Card is not valid - await t.expect(cardPage.getFromState('isValid')).eql(false); -}); diff --git a/packages/e2e/tests/cards/expiryDate/expiryDatePolicies.optional.test.js b/packages/e2e/tests/cards/expiryDate/expiryDatePolicies.optional.test.js deleted file mode 100644 index 047b60dc26..0000000000 --- a/packages/e2e/tests/cards/expiryDate/expiryDatePolicies.optional.test.js +++ /dev/null @@ -1,175 +0,0 @@ -import { binLookupUrl, getBinLookupMock, turnOffSDKMocking } from '../../_common/cardMocks'; -import CardComponentPage from '../../_models/CardComponent.page'; -import { REGULAR_TEST_CARD } from '../utils/constants'; -import { Selector } from 'testcafe'; - -const cardPage = new CardComponentPage(); - -/** - * NOTE - we are mocking the response until such time as we have a genuine card, - * that's not in our local RegEx, that returns the properties we want to test - */ -const mockedResponse = { - brands: [ - { - brand: 'cup', // keep as a recognised card brand (cup) until we have a genuine brand w. optional expiryDate - cvcPolicy: 'optional', - enableLuhnCheck: true, - expiryDatePolicy: 'optional', - supported: true - } - ], - issuingCountryCode: 'US', - requestId: null -}; - -const mock = getBinLookupMock(binLookupUrl, mockedResponse); - -fixture`Test how Card Component handles optional expiryDate policy` - .beforeEach(async t => { - await t.navigateTo(cardPage.pageUrl); - // For individual test suites (that rely on binLookup & perhaps are being run in isolation) - // - provide a way to ensure SDK bin mocking is turned off - await turnOffSDKMocking(); - }) - .requestHooks(mock) - .clientScripts('./expiryDate.clientScripts.js'); - -test('#1 Testing optional expiryDatePolicy - how UI & state respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Regular date label - await t.expect(cardPage.dateLabelText.withText('(optional)').exists).notOk(); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is optional - await t.expect(cardPage.dateLabelText.withText('(optional)').exists).ok(); - - // ...and cvc is optional too - await t.expect(cardPage.cvcLabelText.withText('(optional)').exists).ok(); - - // Card seen as valid (since CVC is optional too) - await t.expect(cardPage.getFromState('isValid')).eql(true); - - // Clear number and see UI & state reset - await cardPage.cardUtils.deleteCardNumber(t); - await t - .expect(cardPage.dateLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.cvcLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.getFromState('isValid')) - .eql(false); -}); - -test('#2 Testing optional expiryDatePolicy - how securedField responds', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Expect iframe to exist in expiryDate field with aria-required attr set to true - await t - .switchToIframe(cardPage.iframeSelector.nth(1)) - .expect(Selector('[data-fieldtype="encryptedExpiryDate"]').getAttribute('aria-required')) - .eql('true') - .switchToMainWindow(); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // Expect iframe to exist in expiryDate field and with aria-required attr set to false - await t - .switchToIframe(cardPage.iframeSelector.nth(1)) - .expect(Selector('[data-fieldtype="encryptedExpiryDate"]').getAttribute('aria-required')) - .eql('false') - .switchToMainWindow(); - - // Clear number and see SF's aria-required reset - await cardPage.cardUtils.deleteCardNumber(t); - - await t - .switchToIframe(cardPage.iframeSelector.nth(1)) - .expect(Selector('[data-fieldtype="encryptedExpiryDate"]').getAttribute('aria-required')) - .eql('true') - .switchToMainWindow(); -}); - -test('#3 Testing optional expiryDatePolicy - validating fields first and then entering PAN should see errors cleared from both UI & state', async t => { - // Start, allow time for iframes to load so isValidated call to SF won't fail - await t.wait(1000); - - // Click pay - await t.click(cardPage.payButton); - - // Expect errors in UI - await t - .expect(cardPage.numLabelTextError.exists) - .ok() - .expect(cardPage.dateLabelTextError.exists) - .ok() - .expect(cardPage.cvcLabelTextError.exists) - .ok(); - - // Expect errors in (mapped) state - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .notEql(null); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD, 'paste'); // TODO - shouldn't have to 'paste' here... but Testcafe is being flaky, again! - - // Expect errors to be cleared - since the fields were in error because they were empty - // and now the PAN field is filled and the date & cvc fields are now optional... - - // ...UI errors cleared... - await t - .expect(cardPage.numLabelTextError.exists) - .notOk() - .expect(cardPage.dateLabelTextError.exists) - .notOk() - .expect(cardPage.cvcLabelTextError.exists) - .notOk(); - - // ...State errors cleared - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .eql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .eql(null); -}); - -test('#4 Testing optional expiryDatePolicy - date field in error DOES stop card becoming valid', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Card out of date - await cardPage.cardUtils.fillDate(t, '12/90'); - - // Expect errors in UI - await t.expect(cardPage.dateLabelTextError.exists).ok(); - - // Force blur event to fire on date field - await cardPage.setForceClick(true); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is optional - await t.expect(cardPage.dateLabelText.withText('(optional)').exists).ok(); - - // Visual errors persist in UI - await t.expect(cardPage.dateLabelTextError.exists).ok(); - - // Card not seen as valid - await t.expect(cardPage.getFromState('isValid')).eql(false); - - // Delete erroneous date - await cardPage.cardUtils.deleteDate(t); - - // Card is now valid - await t.expect(cardPage.getFromState('isValid')).eql(true); -}); diff --git a/packages/e2e/tests/cards/installments/cards.installments.clientScripts.js b/packages/e2e/tests/cards/installments/cards.installments.clientScripts.js deleted file mode 100644 index a0af3a0f6d..0000000000 --- a/packages/e2e/tests/cards/installments/cards.installments.clientScripts.js +++ /dev/null @@ -1,8 +0,0 @@ -window.cardConfig = { - installmentOptions: { - mc: { - values: [1, 2, 3], - plans: ['regular', 'revolving'] - } - } -}; diff --git a/packages/e2e/tests/cards/installments/cards.installments.mocks.js b/packages/e2e/tests/cards/installments/cards.installments.mocks.js deleted file mode 100644 index a31b42fce2..0000000000 --- a/packages/e2e/tests/cards/installments/cards.installments.mocks.js +++ /dev/null @@ -1,16 +0,0 @@ -import { RequestMock, RequestLogger } from 'testcafe'; -import { BASE_URL } from '../../pages'; - -const paymentUrl = `http://localhost:3024/payments`; - -const paymentResponse = { - resultCode: 'Authorised' -}; - -const paymentLogger = RequestLogger({ url: paymentUrl, method: 'post' }, { logRequestBody: true }); - -const mock = RequestMock() - .onRequestTo(request => request.url === paymentUrl && request.method === 'post') - .respond(paymentResponse, 200, { 'Access-Control-Allow-Origin': BASE_URL }); - -export { mock, paymentLogger }; diff --git a/packages/e2e/tests/cards/installments/cards.installments.test.js b/packages/e2e/tests/cards/installments/cards.installments.test.js deleted file mode 100644 index 943d48e817..0000000000 --- a/packages/e2e/tests/cards/installments/cards.installments.test.js +++ /dev/null @@ -1,88 +0,0 @@ -import { CARDS_URL } from '../../pages'; -import CardComponentPage from '../../_models/CardComponent.page'; -import { REGULAR_TEST_CARD } from '../utils/constants'; -import { mock, paymentLogger } from './cards.installments.mocks'; -import InstallmentsComponent from '../../_models/Installments.component'; - -let cardComponent = null; - -fixture`Cards (Installments)` - .page(CARDS_URL) - .clientScripts('./cards.installments.clientScripts.js') - .requestHooks([mock, paymentLogger]) - .beforeEach(async t => { - // handler for alert that's triggered on successful payment - await t.setNativeDialogHandler(() => true); - cardComponent = new CardComponentPage('.card-field', { installments: new InstallmentsComponent() }); - }); - -test('#1 should not add installments property to payload if one-time payment is selected', async t => { - await cardComponent.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - await cardComponent.cardUtils.fillDateAndCVC(t); - - await t - .click(cardComponent.payButton) - .expect(paymentLogger.count(() => true)) - .eql(1) - .expect( - paymentLogger.contains(record => { - const { installments } = JSON.parse(record.request.body); - return installments === undefined; - }) - ) - .ok('payment payload has the expected payload'); -}); - -test('#2 should not add installments property to payload if 1x installment is selected', async t => { - await cardComponent.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - await cardComponent.cardUtils.fillDateAndCVC(t); - await cardComponent.installments.selectInstallment(1); - - await t - .click(cardComponent.payButton) - .expect(paymentLogger.count(() => true)) - .eql(1) - .expect( - paymentLogger.contains(record => { - const { installments } = JSON.parse(record.request.body); - return installments === undefined; - }) - ) - .ok('payment payload has the expected payload'); -}); - -test('#3 should add revolving plan to payload if selected', async t => { - await cardComponent.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - await cardComponent.cardUtils.fillDateAndCVC(t); - await cardComponent.installments.selectRevolving(); - - await t - .click(cardComponent.payButton) - .expect(paymentLogger.count(() => true)) - .eql(1) - .expect( - paymentLogger.contains(record => { - const { installments } = JSON.parse(record.request.body); - return installments.value === 1 && installments.plan === 'revolving'; - }) - ) - .ok('payment payload has the expected payload'); -}); - -test('#4 should add installments value property if regular installment > 1 is selected', async t => { - await cardComponent.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - await cardComponent.cardUtils.fillDateAndCVC(t); - await cardComponent.installments.selectInstallment(2); - - await t - .click(cardComponent.payButton) - .expect(paymentLogger.count(() => true)) - .eql(1) - .expect( - paymentLogger.contains(record => { - const { installments } = JSON.parse(record.request.body); - return installments.value === 2; - }) - ) - .ok('payment payload has the expected payload'); -}); diff --git a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.hidden.regular.test.js b/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.hidden.regular.test.js deleted file mode 100644 index 06b25f9b36..0000000000 --- a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.hidden.regular.test.js +++ /dev/null @@ -1,134 +0,0 @@ -import { binLookupUrl, getBinLookupMock, turnOffSDKMocking } from '../../_common/cardMocks'; -import CustomCardComponentPage from '../../_models/CustomCardComponent.page'; -import { REGULAR_TEST_CARD } from '../../cards/utils/constants'; - -const cardPage = new CustomCardComponentPage(); - -const BASE_REF = 'securedFields'; - -const mockedResponse = { - brands: [ - { - brand: 'cup', // keep as a recognised card brand (cup) until we have a genuine brand w. optional expiryDate - cvcPolicy: 'hidden', - enableLuhnCheck: true, - expiryDatePolicy: 'hidden', - supported: true - } - ], - issuingCountryCode: 'US', - requestId: null -}; - -const mock = getBinLookupMock(binLookupUrl, mockedResponse); - -fixture`Test how Custom Card Component with regular date field handles hidden expiryDate policy` - .beforeEach(async t => { - await t.navigateTo(cardPage.pageUrl); - // For individual test suites (that rely on binLookup & perhaps are being run in isolation) - // - provide a way to ensure SDK bin mocking is turned off - await turnOffSDKMocking(); - }) - .requestHooks(mock) - .clientScripts('./expiryDate.clientScripts.js'); - -test('#1 Testing hidden expiryDatePolicy - how UI & state respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is hidden - await t.expect(cardPage.dateHolder.filterHidden().exists).ok(); - - // ...and cvc is hidden too - await t.expect(cardPage.cvcHolder.filterHidden().exists).ok(); - - // Card seen as valid (since CVC is hidden too) - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); - - // Clear number and see UI & state reset - await cardPage.cardUtils.deleteCardNumber(t); - await t - .expect(cardPage.dateHolder.filterVisible().exists) - .ok() - .expect(cardPage.cvcHolder.filterVisible().exists) - .ok() - .expect(cardPage.getFromState(BASE_REF, 'isValid')) - .eql(false); -}); - -test('#2 Testing hidden expiryDatePolicy - validating fields first and then entering PAN should see errors cleared from state', async t => { - // Start, allow time for iframes to load so isValidated call to SF won't fail - await t.wait(1000); - - // Click pay - await t.click(cardPage.payButton); - - // Expect errors in UI - await t - .expect(cardPage.numErrorText.filterVisible().exists) - .ok() - .expect(cardPage.dateErrorText.filterVisible().exists) - .ok() - .expect(cardPage.cvcErrorText.filterVisible().exists) - .ok(); - - // Expect errors in (mapped) state - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(undefined) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .notEql(null); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // Expect errors to be cleared - since the fields were in error because they were empty - // and now the PAN field is filled and the date & cvc fields are now hidden... - - // ...State errors cleared - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .eql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .eql(null); -}); - -test('#3 Testing hidden expiryDatePolicy - date field in error does not stop card becoming valid', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Card out of date - await cardPage.cardUtils.fillDate(t, '12/90'); - - // Expect errors in UI - await t.expect(cardPage.dateErrorText.filterVisible().exists).ok(); - - // Force blur event to fire on date field - await cardPage.setForceClick(true); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is hidden - await t.expect(cardPage.dateHolder.filterHidden().exists).ok(); - - // Card seen as valid (despite date field technically being in error) - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); - - // Expect errors in (mapped) state to remain - await t.expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')).notEql(null); - - // Delete number - await cardPage.cardUtils.deleteCardNumber(t); - - // Errors in UI visible again - await t.expect(cardPage.dateErrorText.filterVisible().exists).ok(); - - // Card is not valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(false); -}); diff --git a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.hidden.separate.test.js b/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.hidden.separate.test.js deleted file mode 100644 index 4a89536640..0000000000 --- a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.hidden.separate.test.js +++ /dev/null @@ -1,127 +0,0 @@ -import { turnOffSDKMocking } from '../../_common/cardMocks'; -import CustomCardComponentPage from '../../_models/CustomCardComponent.page'; -import { SYNCHRONY_PLCC_NO_DATE, TEST_CVC_VALUE } from '../../cards/utils/constants'; - -const cardPage = new CustomCardComponentPage('.secured-fields-2'); - -const BASE_REF = 'securedFields2'; - -fixture`Test how Custom Card Component with separate date fields handles hidden expiryDate policy` - .beforeEach(async t => { - await t.navigateTo(cardPage.pageUrl); - // For individual test suites (that rely on binLookup & perhaps are being run in isolation) - // - provide a way to ensure SDK bin mocking is turned off - await turnOffSDKMocking(); - }) - .clientScripts('./expiryDate.clientScripts.js'); - -test('#1 Testing hidden expiryDatePolicy - how UI & state respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Fill number to provoke binLookup response - await cardPage.cardUtils.fillCardNumber(t, SYNCHRONY_PLCC_NO_DATE); - - // UI reflects that binLookup says expiryDate is hidden - await t.expect(cardPage.monthHolder.filterHidden().exists).ok(); - await t.expect(cardPage.yearHolder.filterHidden().exists).ok(); - - await cardPage.customCardUtils.fillCVC(t, TEST_CVC_VALUE); - - // Card seen as valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); - - // Clear number and see UI & state reset - await cardPage.cardUtils.deleteCardNumber(t); - await t - .expect(cardPage.monthHolder.filterVisible().exists) - .ok() - .expect(cardPage.yearHolder.filterVisible().exists) - .ok() - .expect(cardPage.cvcHolder.filterVisible().exists) - .ok() - .expect(cardPage.getFromState(BASE_REF, 'isValid')) - .eql(false); -}); - -test('#2 Testing hidden expiryDatePolicy - validating fields first and then entering PAN should see errors cleared from state', async t => { - // Start, allow time for iframes to load so isValidated call to SF won't fail - await t.wait(1000); - - // Click pay - await t.click(cardPage.payButton); - - // Expect errors in UI - await t - .expect(cardPage.numErrorText.filterVisible().exists) - .ok() - .expect(cardPage.monthErrorText.filterVisible().exists) - .ok() - .expect(cardPage.yearErrorText.filterVisible().exists) - .ok() - .expect(cardPage.cvcErrorText.filterVisible().exists) - .ok(); - - // Expect errors in (mapped) state - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryMonth')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryMonth')) - .notEql(undefined) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryYear')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .notEql(null); - - // Fill number to provoke binLookup response - await cardPage.cardUtils.fillCardNumber(t, SYNCHRONY_PLCC_NO_DATE); - - // Expect errors to be cleared - since the fields were in error because they were empty - // and now the PAN field is filled and the date & cvc fields are now hidden... - - // ...State errors cleared - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryMonth')) - .eql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryYear')) - .eql(null); -}); - -test('#3 Testing hidden expiryDatePolicy - date field in error does not stop card becoming valid', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Card out of date - await cardPage.customCardUtils.fillMonth(t, '12'); - await cardPage.customCardUtils.fillYear(t, '90'); - - // Expect errors in UI - await t.expect(cardPage.yearErrorText.filterVisible().exists).ok(); - - // Force blur event to fire on date field - await cardPage.setForceClick(true); - - // Fill number to provoke binLookup response - await cardPage.cardUtils.fillCardNumber(t, SYNCHRONY_PLCC_NO_DATE); - - // UI reflects that binLookup says expiryDate is hidden - await t.expect(cardPage.monthHolder.filterHidden().exists).ok(); - await t.expect(cardPage.yearHolder.filterHidden().exists).ok(); - - await cardPage.customCardUtils.fillCVC(t, TEST_CVC_VALUE); - - // Card seen as valid (despite date field technically being in error) - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); - - // Expect errors in (mapped) state to remain - await t.expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryYear')).notEql(null); - - // Delete number - await cardPage.cardUtils.deleteCardNumber(t); - - // Errors in UI visible again - await t.expect(cardPage.yearErrorText.filterVisible().exists).ok(); - - // Card is not valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(false); -}); diff --git a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.optional.regular.test.js b/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.optional.regular.test.js deleted file mode 100644 index 904f597e66..0000000000 --- a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.optional.regular.test.js +++ /dev/null @@ -1,166 +0,0 @@ -import { binLookupUrl, getBinLookupMock, turnOffSDKMocking } from '../../_common/cardMocks'; -import CustomCardComponentPage from '../../_models/CustomCardComponent.page'; -import { REGULAR_TEST_CARD } from '../../cards/utils/constants'; - -const cardPage = new CustomCardComponentPage(); - -const BASE_REF = 'securedFields'; - -/** - * NOTE - we are mocking the response until such time as we have a genuine card, - * that's not in our local RegEx, that returns the properties we want to test - */ -const mockedResponse = { - brands: [ - { - brand: 'cup', // keep as a recognised card brand (cup) until we have a genuine brand w. optional expiryDate - cvcPolicy: 'optional', - enableLuhnCheck: true, - expiryDatePolicy: 'optional', - supported: true - } - ], - issuingCountryCode: 'US', - requestId: null -}; - -const mock = getBinLookupMock(binLookupUrl, mockedResponse); - -fixture`Test how regular Custom Card Component handles optional expiryDate policy` - .beforeEach(async t => { - await t.navigateTo(cardPage.pageUrl); - // For individual test suites (that rely on binLookup & perhaps are being run in isolation) - // - provide a way to ensure SDK bin mocking is turned off - await turnOffSDKMocking(); - }) - .requestHooks(mock) - .clientScripts('./expiryDate.clientScripts.js'); - -test('#1 Testing optional expiryDatePolicy - how UI & state respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Regular date label - await t.expect(cardPage.dateLabelText.withText('(optional)').exists).notOk(); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is optional - await t.expect(cardPage.dateLabelText.withText('(optional)').exists).ok(); - - // ...and cvc is optional too - await t.expect(cardPage.cvcLabelText.withText('(optional)').exists).ok(); - - // Card seen as valid (since CVC is optional too) - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); - - // Clear number and see UI & state reset - await cardPage.cardUtils.deleteCardNumber(t); - await t - .expect(cardPage.dateLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.cvcLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.getFromState(BASE_REF, 'isValid')) - .eql(false); -}); - -test('#2 Testing optional expiryDatePolicy, in regular Custom Card - how securedField responds', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Expect iframe to exist in expiryDate field with aria-required attr set to true - await cardPage.cardUtils.checkIframeForAttrVal(t, 1, 'encryptedExpiryDate', 'aria-required', 'true'); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // Expect iframe to exist in expiryDate field and with aria-required attr set to false - await cardPage.cardUtils.checkIframeForAttrVal(t, 1, 'encryptedExpiryDate', 'aria-required', 'false'); - - // Clear number and see SF's aria-required reset - await cardPage.cardUtils.deleteCardNumber(t); - - await cardPage.cardUtils.checkIframeForAttrVal(t, 1, 'encryptedExpiryDate', 'aria-required', 'true'); -}); - -test('#3 Testing optional expiryDatePolicy - validating fields first and then entering PAN should see errors cleared from both UI & state', async t => { - // Start, allow time for iframes to load so isValidated call to SF won't fail - await t.wait(1000); - - // Click pay - await t.click(cardPage.payButton); - - // Expect errors in UI - await t - .expect(cardPage.numErrorText.filterVisible().exists) - .ok() - .expect(cardPage.dateErrorText.filterVisible().exists) - .ok() - .expect(cardPage.cvcErrorText.filterVisible().exists) - .ok(); - - // Expect errors in (mapped) state - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .notEql(undefined) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .notEql(null); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // Expect errors to be cleared - since the fields were in error because they were empty - // and now the PAN field is filled and the date & cvc fields are now optional... - - // ...UI errors cleared... - await t - .expect(cardPage.numErrorText.filterHidden().exists) - .ok() - .expect(cardPage.dateErrorText.filterHidden().exists) - .ok() - .expect(cardPage.cvcErrorText.filterHidden().exists) - .ok(); - - // ...State errors cleared - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryDate')) - .eql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .eql(null); -}); - -test('#4 Testing optional expiryDatePolicy - date field in error DOES stop card becoming valid', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Card out of date - await cardPage.cardUtils.fillDate(t, '12/90'); - - // Expect errors in UI - await t.expect(cardPage.dateErrorText.filterVisible().exists).ok(); - - // Force blur event to fire on date field - await cardPage.setForceClick(true); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is optional - await t.expect(cardPage.dateLabelText.withText('(optional)').exists).ok(); - - // Visual errors persist in UI - await t.expect(cardPage.dateErrorText.filterVisible().exists).ok(); - - // Card not seen as valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(false); - - // Delete erroneous date - await cardPage.cardUtils.deleteDate(t); - - // Card is now valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); -}); diff --git a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.optional.separate.test.js b/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.optional.separate.test.js deleted file mode 100644 index 9541c35bc1..0000000000 --- a/packages/e2e/tests/customcard/expiryDate/expiryDatePolicies.optional.separate.test.js +++ /dev/null @@ -1,197 +0,0 @@ -import { binLookupUrl, getBinLookupMock, turnOffSDKMocking } from '../../_common/cardMocks'; -import CustomCardComponentPage from '../../_models/CustomCardComponent.page'; -import { REGULAR_TEST_CARD } from '../../cards/utils/constants'; - -const cardPage = new CustomCardComponentPage('.secured-fields-2'); - -const BASE_REF = 'securedFields2'; - -/** - * NOTE - we are mocking the response until such time as we have a genuine card, - * that's not in our local RegEx, that returns the properties we want to test - */ -const mockedResponse = { - brands: [ - { - brand: 'cup', // keep as a recognised card brand (cup) until we have a genuine brand w. optional expiryDate - cvcPolicy: 'optional', - enableLuhnCheck: true, - expiryDatePolicy: 'optional', - supported: true - } - ], - issuingCountryCode: 'US', - requestId: null -}; - -const mock = getBinLookupMock(binLookupUrl, mockedResponse); - -fixture`Test how Custom Card Component with separate date fields handles optional expiryDate policy` - .beforeEach(async t => { - await t.navigateTo(cardPage.pageUrl); - // For individual test suites (that rely on binLookup & perhaps are being run in isolation) - // - provide a way to ensure SDK bin mocking is turned off - await turnOffSDKMocking(); - }) - .requestHooks(mock) - .clientScripts('./expiryDate.clientScripts.js'); - -test('#1 Testing optional expiryDatePolicy - how UI & state respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Regular date labels - await t - .expect(cardPage.monthLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.yearLabelText.withText('(optional)').exists) - .notOk(); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is optional - await t - .expect(cardPage.monthLabelText.withText('(optional)').exists) - .ok() - .expect(cardPage.yearLabelText.withText('(optional)').exists) - .ok(); - - // ...and cvc is optional too - await t.expect(cardPage.cvcLabelText.withText('(optional)').exists).ok(); - - // Card seen as valid (since CVC is optional too) - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); - - // Clear number and see UI & state reset - await cardPage.cardUtils.deleteCardNumber(t); - await t - .expect(cardPage.monthLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.yearLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.cvcLabelText.withText('(optional)').exists) - .notOk() - .expect(cardPage.getFromState(BASE_REF, 'isValid')) - .eql(false); -}); - -test('#2 Testing optional expiryDatePolicy, in Custom Card w. separate date fields - how securedFields respond', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Expect iframe to exist in expiryMonth field with aria-required attr set to true - await cardPage.cardUtils.checkIframeForAttrVal(t, 1, 'encryptedExpiryMonth', 'aria-required', 'true'); - - // Expect iframe to exist in expiryYear field with aria-required attr set to true - await cardPage.cardUtils.checkIframeForAttrVal(t, 2, 'encryptedExpiryYear', 'aria-required', 'true'); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // Expect iframe to exist in expiryMonth field and with aria-required attr set to false - await cardPage.cardUtils.checkIframeForAttrVal(t, 1, 'encryptedExpiryMonth', 'aria-required', 'false'); - - // Expect iframe to exist in expiryYear field and with aria-required attr set to false - await cardPage.cardUtils.checkIframeForAttrVal(t, 2, 'encryptedExpiryYear', 'aria-required', 'false'); - - // Clear number and see SF's aria-required reset - await cardPage.cardUtils.deleteCardNumber(t); - - await cardPage.cardUtils.checkIframeForAttrVal(t, 1, 'encryptedExpiryMonth', 'aria-required', 'true'); - await cardPage.cardUtils.checkIframeForAttrVal(t, 2, 'encryptedExpiryYear', 'aria-required', 'true'); -}); - -test('#3 Testing optional expiryDatePolicy - validating fields first and then entering PAN should see errors cleared from both UI & state', async t => { - // Start, allow time for iframes to load so isValidated call to SF won't fail - await t.wait(1000); - - // Click pay - await t.click(cardPage.payButton); - - // Expect errors in UI - await t - .expect(cardPage.numErrorText.filterVisible().exists) - .ok() - .expect(cardPage.monthErrorText.filterVisible().exists) - .ok() - .expect(cardPage.yearErrorText.filterVisible().exists) - .ok() - .expect(cardPage.cvcErrorText.filterVisible().exists) - .ok(); - - // Expect errors in (mapped) state - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryMonth')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryMonth')) - .notEql(undefined) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryYear')) - .notEql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .notEql(null); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // Expect errors to be cleared - since the fields were in error because they were empty - // and now the PAN field is filled and the date & cvc fields are now optional... - - // ...UI errors cleared... - await t - .expect(cardPage.numErrorText.filterHidden().exists) - .ok() - .expect(cardPage.monthErrorText.filterHidden().exists) - .ok() - .expect(cardPage.yearErrorText.filterHidden().exists) - .ok() - .expect(cardPage.cvcErrorText.filterHidden().exists) - .ok(); - - // ...State errors cleared - await t - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryMonth')) - .eql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedExpiryYear')) - .eql(null) - .expect(cardPage.getFromWindow('mappedStateErrors.encryptedSecurityCode')) - .eql(null); -}); - -test('#4 Testing optional expiryDatePolicy - date field in error DOES stop card becoming valid', async t => { - // Wait for field to appear in DOM - await cardPage.numHolder(); - - // Card out of date - await cardPage.customCardUtils.fillMonth(t, '12'); - await cardPage.customCardUtils.fillYear(t, '90'); - - // Expect errors in UI - await t.expect(cardPage.yearErrorText.filterVisible().exists).ok(); - - // Force blur event to fire on date field - await cardPage.setForceClick(true); - - // Fill number to provoke (mock) binLookup response - await cardPage.cardUtils.fillCardNumber(t, REGULAR_TEST_CARD); - - // UI reflects that binLookup says expiryDate is optional - await t - .expect(cardPage.monthLabelText.withText('(optional)').exists) - .ok() - .expect(cardPage.yearLabelText.withText('(optional)').exists) - .ok(); - - // Visual errors persist in UI - await t.expect(cardPage.yearErrorText.filterVisible().exists).ok(); - - // Card not seen as valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(false); - - // Delete erroneous date - await cardPage.customCardUtils.deleteMonth(t); - await cardPage.customCardUtils.deleteYear(t); - - // Card is now valid - await t.expect(cardPage.getFromState(BASE_REF, 'isValid')).eql(true); -}); diff --git a/packages/lib/package.json b/packages/lib/package.json index 95b94f3369..44e6dbc02f 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -75,11 +75,11 @@ "@storybook/manager-api": "^7.2.0", "@storybook/preact": "^7.2.0", "@storybook/preact-vite": "^7.2.0", - "@testing-library/jest-dom": "5.17.0", + "@testing-library/jest-dom": "6.1.4", "@testing-library/preact": "3.2.3", "@testing-library/preact-hooks": "1.1.0", "@testing-library/user-event": "14.5.1", - "@types/jest": "29.5.7", + "@types/jest": "29.5.10", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "autoprefixer": "10.4.16", @@ -94,7 +94,7 @@ "eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-react": "^7.31.8", "eslint-plugin-storybook": "^0.6.13", - "eslint-plugin-testing-library": "^5.9.1", + "eslint-plugin-testing-library": "6.2.0", "eslint-plugin-tsdoc": "^0.2.17", "filesize": "^10.0.0", "gzip-size": "^6.0.0", diff --git a/packages/lib/src/components/ANCV/ANCV.tsx b/packages/lib/src/components/ANCV/ANCV.tsx index 119cb28cce..7718c89e9a 100644 --- a/packages/lib/src/components/ANCV/ANCV.tsx +++ b/packages/lib/src/components/ANCV/ANCV.tsx @@ -46,8 +46,15 @@ export class ANCVElement extends UIElement { } }; + /** + * Called when the /paymentDetails endpoint returns PartiallyAuthorised. The /paymentDetails happens once the /status + * returns PartiallyAuthorised + * + * @param order + */ protected handleOrder = ({ order }: PaymentResponse) => { this.updateParent({ order }); + if (this.props.session && this.props.onOrderCreated) { return this.props.onOrderCreated(order); } diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 0cd0ddbcbb..d65a321d88 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -493,6 +493,8 @@ const CardInput: FunctionalComponent = props => { partialAddressSchema={partialAddressSchema} handleAddress={handleAddress} onAddressLookup={props.onAddressLookup} + onAddressSelected={props.onAddressSelected} + addressSearchDebounceMs={props.addressSearchDebounceMs} // iOSFocusedField={iOSFocusedField} /> diff --git a/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx b/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx index 84904a73e6..d62d4be4df 100644 --- a/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx +++ b/packages/lib/src/components/Card/components/CardInput/components/CardFieldsWrapper.tsx @@ -45,6 +45,8 @@ export const CardFieldsWrapper = ({ setAddressRef, partialAddressSchema, onAddressLookup, + onAddressSelected, + addressSearchDebounceMs, // For this comp (props passed through from CardInput) amount, billingAddressRequired, @@ -159,6 +161,8 @@ export const CardFieldsWrapper = ({ specifications={partialAddressSchema} iOSFocusedField={iOSFocusedField} onAddressLookup={onAddressLookup} + onAddressSelected={onAddressSelected} + addressSearchDebounceMs={addressSearchDebounceMs} /> )} diff --git a/packages/lib/src/components/Card/components/CardInput/types.ts b/packages/lib/src/components/Card/components/CardInput/types.ts index edb24ac695..71d3e9ed1d 100644 --- a/packages/lib/src/components/Card/components/CardInput/types.ts +++ b/packages/lib/src/components/Card/components/CardInput/types.ts @@ -13,7 +13,7 @@ import Analytics from '../../../../core/Analytics'; import RiskElement from '../../../../core/RiskModule'; import { ComponentMethodsRef } from '../../../types'; import { DisclaimerMsgObject } from '../../../internal/DisclaimerMessage/DisclaimerMessage'; -import { OnAddressLookupType } from '../../../internal/Address/components/AddressSearch'; +import { OnAddressLookupType, OnAddressSelectedType } from '../../../internal/Address/components/AddressSearch'; export interface CardInputValidState { holderName?: boolean; @@ -108,6 +108,8 @@ export interface CardInputProps { onFocus?: (e) => {}; onLoad?: () => {}; onAddressLookup?: OnAddressLookupType; + onAddressSelected?: OnAddressSelectedType; + addressSearchDebounceMs?: number; payButton?: (obj) => {}; placeholders?: Placeholders; positionHolderNameOnTop?: boolean; diff --git a/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodItem.tsx b/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodItem.tsx index aac90ff497..82ac7b30bd 100644 --- a/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodItem.tsx +++ b/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodItem.tsx @@ -71,11 +71,13 @@ class PaymentMethodItem extends Component { return null; } + const isCard = paymentMethod.props.type === 'card' || paymentMethod.props.type === 'scheme'; + const paymentMethodClassnames = classNames({ 'adyen-checkout__payment-method': true, [styles['adyen-checkout__payment-method']]: true, [`adyen-checkout__payment-method--${paymentMethod.props.type}`]: true, - [`adyen-checkout__payment-method--${paymentMethod.props.fundingSource ?? 'credit'}`]: true, + ...(isCard && { [`adyen-checkout__payment-method--${paymentMethod.props.fundingSource ?? 'credit'}`]: true }), 'adyen-checkout__payment-method--selected': isSelected, [styles['adyen-checkout__payment-method--selected']]: isSelected, 'adyen-checkout__payment-method--loading': isLoading, diff --git a/packages/lib/src/components/Giropay/Giropay.tsx b/packages/lib/src/components/Giropay/Giropay.tsx index bf6fe04f86..31094ae548 100644 --- a/packages/lib/src/components/Giropay/Giropay.tsx +++ b/packages/lib/src/components/Giropay/Giropay.tsx @@ -13,17 +13,6 @@ class GiropayElement extends RedirectElement { }; } - /** - * Formats the component data output - */ - formatData() { - return { - paymentMethod: { - type: GiropayElement.type - } - }; - } - get displayName() { return this.props.name || this.constructor['type']; } diff --git a/packages/lib/src/components/Redirect/Redirect.test.tsx b/packages/lib/src/components/Redirect/Redirect.test.tsx index 2d65fcb4e9..d128c00de6 100644 --- a/packages/lib/src/components/Redirect/Redirect.test.tsx +++ b/packages/lib/src/components/Redirect/Redirect.test.tsx @@ -2,6 +2,7 @@ import { mount } from 'enzyme'; import { h } from 'preact'; import Redirect from './Redirect'; import RedirectShopper from './components/RedirectShopper'; +import RedirectElement from './Redirect'; jest.mock('../../utils/detectInIframe', () => { return jest.fn().mockImplementation(() => { @@ -21,6 +22,7 @@ describe('Redirect', () => { test('Accepts a POST redirect status', () => { window.HTMLFormElement.prototype.submit = jest.fn(); + // @ts-ignore ignore const wrapper = mount(); expect(wrapper.find('form')).toHaveLength(1); @@ -32,6 +34,7 @@ describe('Redirect', () => { test('Accepts a POST redirect status, setting target to _top, when the config prop tells it to', () => { window.HTMLFormElement.prototype.submit = jest.fn(); + // @ts-ignore ignore const wrapper = mount(); expect(wrapper.find('form')).toHaveLength(1); @@ -39,4 +42,11 @@ describe('Redirect', () => { setTimeout(() => expect(window.HTMLFormElement.prototype.submit).toHaveBeenCalled(), 0); }); }); + + describe('Redirect formatData', () => { + test('should send browserInfo in the data', () => { + const redirectElement = new RedirectElement({}); + expect(redirectElement.formatData().browserInfo).not.toBeNull(); + }); + }); }); diff --git a/packages/lib/src/components/Redirect/Redirect.tsx b/packages/lib/src/components/Redirect/Redirect.tsx index 1547c74126..0eee5ca75f 100644 --- a/packages/lib/src/components/Redirect/Redirect.tsx +++ b/packages/lib/src/components/Redirect/Redirect.tsx @@ -4,6 +4,7 @@ import UIElement from '../UIElement'; import CoreProvider from '../../core/Context/CoreProvider'; import RedirectShopper from './components/RedirectShopper'; import RedirectButton from '../internal/RedirectButton'; +import collectBrowserInfo from '../../utils/browserInfo'; /** * RedirectElement @@ -30,7 +31,8 @@ class RedirectElement extends UIElement { return { paymentMethod: { type: this.props.type - } + }, + browserInfo: this.browserInfo }; } @@ -41,6 +43,10 @@ class RedirectElement extends UIElement { return true; } + get browserInfo() { + return collectBrowserInfo(); + } + render() { if (this.props.url && this.props.method) { return ; diff --git a/packages/lib/src/components/internal/Address/Address.tsx b/packages/lib/src/components/internal/Address/Address.tsx index 2ccc87b449..ea23d1861d 100644 --- a/packages/lib/src/components/internal/Address/Address.tsx +++ b/packages/lib/src/components/internal/Address/Address.tsx @@ -1,5 +1,5 @@ import { Fragment, h } from 'preact'; -import { useEffect, useMemo, useRef, useState } from 'preact/hooks'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks'; import Fieldset from '../FormFields/Fieldset'; import ReadOnlyAddress from './components/ReadOnlyAddress'; import { getAddressValidationRules } from './validate'; @@ -49,22 +49,25 @@ export default function Address(props: AddressProps) { formatters: addressFormatters }); - const setSearchData = selectedAddress => { - const propsKeysToProcess = ADDRESS_SCHEMA; - propsKeysToProcess.forEach(propKey => { - // Make sure the data provided by the merchant is always strings - const providedValue = selectedAddress[propKey]; - if (providedValue === null || providedValue === undefined) return; - // Cast everything to string - setData(propKey, String(providedValue)); + const setSearchData = useCallback( + (selectedAddress: AddressData) => { + const propsKeysToProcess = ADDRESS_SCHEMA; + propsKeysToProcess.forEach(propKey => { + // Make sure the data provided by the merchant is always strings + const providedValue = selectedAddress[propKey]; + if (providedValue === null || providedValue === undefined) return; + // Cast everything to string + setData(propKey, String(providedValue)); + }); triggerValidation(); - }); - setHasSelectedAddress(true); - }; + setHasSelectedAddress(true); + }, + [setHasSelectedAddress, triggerValidation, setData] + ); - const onManualAddress = () => { + const onManualAddress = useCallback(() => { setUseManualAddress(true); - }; + }, []); // Expose method expected by (parent) Address.tsx addressRef.current.showValidation = () => { @@ -172,10 +175,12 @@ export default function Address(props: AddressProps) { {showAddressSearch && ( )} {showAddressFields && ( diff --git a/packages/lib/src/components/internal/Address/components/AddressSearch.test.tsx b/packages/lib/src/components/internal/Address/components/AddressSearch.test.tsx new file mode 100644 index 0000000000..6abc8b74ed --- /dev/null +++ b/packages/lib/src/components/internal/Address/components/AddressSearch.test.tsx @@ -0,0 +1,191 @@ +import { h } from 'preact'; +import userEvent from '@testing-library/user-event'; +import { render, screen, waitFor } from '@testing-library/preact'; +import AddressSearch from './AddressSearch'; + +const ADDRESS_LOOKUP_RESULT = [ + { + id: 1, + name: 'Road 1, 2000, UK', + street: 'Road 1' + }, + { + id: 2, + name: 'Road 2, 2500, UK', + street: 'Road 2' + }, + { + id: 3, + name: 'Road 3, 3000, UK', + street: 'Road 3' + } +]; + +const ADDRESS_SELECT_RESULT = { + id: 1, + name: 'Road 1, 2000, UK', + street: 'Road 1', + city: 'London', + houseNumberOrName: '1', + postalCode: '2000', + country: 'GB', + raw: { + id: 1, + raw: 'RAW_DATA_MOCK' + } +}; +const onAddressLookupMockFn = async (value, { resolve }) => { + resolve(ADDRESS_LOOKUP_RESULT); +}; +const onAddressSelectMockFn = async (value, { resolve }) => { + resolve(ADDRESS_SELECT_RESULT); +}; + +const onAddressSelectMockFnReject = + rejectReason => + async (value, { reject }) => { + reject(rejectReason); + }; + +test('onAddressLookupMock should be triggered when typing', async () => { + const user = userEvent.setup({ delay: 100 }); + + const onAddressLookupMock = jest.fn(onAddressLookupMockFn); + + render( + {}} + onManualAddress={() => {}} + externalErrorMessage={'failed'} + onAddressLookup={onAddressLookupMock} + hideManualButton={true} + addressSearchDebounceMs={0} + /> + ); + + // Helps to make sure all tests ran + expect.assertions(5); + + // Get the input + const searchBar = screen.getByRole('combobox'); + await user.click(searchBar); + expect(searchBar).toHaveFocus(); + + await user.keyboard('Test'); + expect(searchBar).toHaveValue('Test'); + + // Test if onAddressLookup is called with the correct values + await waitFor(() => expect(onAddressLookupMock).toHaveBeenCalledTimes(4)); + await waitFor(() => expect(onAddressLookupMock.mock.lastCall[0]).toBe('Test')); + // Test if the return of the function is displayed + const resultList = screen.getByRole('listbox'); + expect(resultList).toHaveTextContent('Road 1, 2000, UK'); +}); + +test('onSelect is triggered with correct data', async () => { + const user = userEvent.setup({ delay: 100 }); + + const onAddressLookupMock = jest.fn(onAddressLookupMockFn); + const onAddressSelectMock = jest.fn(onAddressSelectMockFn); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const internalSetDataMock = jest.fn(data => {}); + + render( + {}} + externalErrorMessage={'failed'} + onAddressLookup={onAddressLookupMock} + onAddressSelected={onAddressSelectMock} + hideManualButton={true} + addressSearchDebounceMs={0} + /> + ); + const searchBar = screen.getByRole('combobox'); + + await user.click(searchBar); + await user.keyboard('Test'); + + // Move down with the keyboard and select the first option + await user.keyboard('[ArrowDown][Enter]'); + await waitFor(() => expect(onAddressSelectMock).toHaveBeenCalledTimes(1)); + await waitFor(() => + expect(onAddressSelectMock.mock.lastCall[0]).toStrictEqual({ + id: 1, + name: 'Road 1, 2000, UK', + street: 'Road 1' + }) + ); + + // CHeck if the parents select function is called with full data + await waitFor(() => expect(internalSetDataMock.mock.lastCall[0]).toStrictEqual(ADDRESS_SELECT_RESULT)); +}); + +test('rejecting onAddressLookupMock should not trigger error', async () => { + const user = userEvent.setup({ delay: 100 }); + + const onAddressLookupMock = jest.fn(onAddressSelectMockFnReject({})); + + render( + {}} + onManualAddress={() => {}} + externalErrorMessage={'failed'} + onAddressLookup={onAddressLookupMock} + hideManualButton={true} + addressSearchDebounceMs={0} + /> + ); + + // Helps to make sure all tests ran + expect.assertions(3); + + // Get the input + const searchBar = screen.getByRole('combobox'); + await user.click(searchBar); + await user.keyboard('Test'); + + // Still test if correct values are being called + await waitFor(() => expect(onAddressLookupMock).toHaveBeenCalledTimes(4)); + await waitFor(() => expect(onAddressLookupMock.mock.lastCall[0]).toBe('Test')); + + // Test if no options are displayed + const resultList = screen.getByRole('listbox'); + expect(resultList).toHaveTextContent('No options found'); + + // TODO fix this + // const resultError = screen.getByText('failed'); + // expect(resultError).not.toBeVisible(); +}); + +test('rejecting onAddressLookupMock with errorMessage displays error and message', async () => { + const user = userEvent.setup({ delay: 100 }); + + const onAddressLookupMock = jest.fn(onAddressSelectMockFnReject({ errorMessage: 'Refused Mock' })); + + render( + {}} + onManualAddress={() => {}} + externalErrorMessage={'failed'} + onAddressLookup={onAddressLookupMock} + hideManualButton={true} + addressSearchDebounceMs={0} + /> + ); + + // Helps to make sure all tests ran + expect.assertions(3); + + // Get the input + const searchBar = screen.getByRole('combobox'); + await user.click(searchBar); + await user.keyboard('Test'); + + await waitFor(() => expect(onAddressLookupMock).toHaveBeenCalledTimes(4)); + await waitFor(() => expect(onAddressLookupMock.mock.lastCall[0]).toBe('Test')); + + // Test if no options are displayed + const resultError = screen.getByText('Refused Mock'); + expect(resultError).toBeVisible(); +}); diff --git a/packages/lib/src/components/internal/Address/components/AddressSearch.tsx b/packages/lib/src/components/internal/Address/components/AddressSearch.tsx index 6786f67cd6..0538ee1c1b 100644 --- a/packages/lib/src/components/internal/Address/components/AddressSearch.tsx +++ b/packages/lib/src/components/internal/Address/components/AddressSearch.tsx @@ -1,11 +1,12 @@ import Field from '../../FormFields/Field'; -import { Fragment, h } from 'preact'; +import { h } from 'preact'; import { AddressLookupItem } from '../types'; -import { useCallback, useEffect, useMemo, useState } from 'preact/hooks'; +import { useCallback, useEffect, useState, useMemo } from 'preact/hooks'; import './AddressSearch.scss'; import useCoreContext from '../../../../core/Context/useCoreContext'; import { debounce } from '../utils'; import Select from '../../FormFields/Select'; +import { AddressData } from '../../../../types'; export type OnAddressLookupType = ( value: string, @@ -15,15 +16,37 @@ export type OnAddressLookupType = ( } ) => Promise; +export type OnAddressSelectedType = ( + value: string, + actions: { + resolve: (value: AddressLookupItem) => void; + reject: (reason?: any) => void; + } +) => Promise; + interface AddressSearchProps { onAddressLookup?: OnAddressLookupType; - onSelect: any; //TODO + onAddressSelected?: OnAddressSelectedType; + onSelect: (addressItem: AddressData) => void; onManualAddress: any; externalErrorMessage: string; hideManualButton: boolean; + addressSearchDebounceMs?: number; +} + +interface RejectionReason { + errorMessage: string; } -export default function AddressSearch({ onAddressLookup, onSelect, onManualAddress, externalErrorMessage, hideManualButton }: AddressSearchProps) { +export default function AddressSearch({ + onAddressLookup, + onAddressSelected, + onSelect, + onManualAddress, + externalErrorMessage, + hideManualButton, + addressSearchDebounceMs +}: Readonly) { const [formattedData, setFormattedData] = useState([]); const [originalData, setOriginalData] = useState([]); @@ -32,20 +55,23 @@ export default function AddressSearch({ onAddressLookup, onSelect, onManualAddre const { i18n } = useCoreContext(); const mapDataToSelect = data => data.map(({ id, name }) => ({ id, name })); - const onInput = useCallback( - async event => { + const handlePromiseReject = useCallback((reason: RejectionReason) => { + if (reason?.errorMessage) { + setErrorMessage(reason.errorMessage); + } + }, []); + + const onTextInput = useCallback( + async (inputValue: string) => { new Promise>((resolve, reject) => { - onAddressLookup(event, { resolve, reject }); + onAddressLookup(inputValue, { resolve, reject }); }) - .then(data => { - setOriginalData(data); - setFormattedData(mapDataToSelect(data)); + .then(searchArray => { + setOriginalData(searchArray); + setFormattedData(mapDataToSelect(searchArray)); setErrorMessage(''); }) - .catch(reason => { - setErrorMessage(reason); - console.error('error', reason); - }); + .catch(reason => handlePromiseReject(reason)); }, [onAddressLookup] ); @@ -55,44 +81,57 @@ export default function AddressSearch({ onAddressLookup, onSelect, onManualAddre setErrorMessage(externalErrorMessage); }, [externalErrorMessage]); - const onChange = event => { + const onSelectItem = async event => { if (!event.target.value) { setErrorMessage(i18n.get('address.errors.incomplete')); return; } const value = originalData.find(item => item.id === event.target.value); - onSelect(value); - setFormattedData([]); + + // 1. in case we don't get a function just select item + if (typeof onAddressSelected !== 'function') { + onSelect(value); + setFormattedData([]); + return; + } + + // 2. in case callback is provided, create and call onAddressSelected + new Promise((resolve, reject) => { + onAddressSelected(value, { resolve, reject }); + }) + .then(fullData => { + onSelect(fullData); + setFormattedData([]); + }) + .catch(reason => handlePromiseReject(reason)); }; - const debounceInputHandler = useMemo(() => debounce(onInput), []); + const debounceInputHandler = useMemo(() => debounce(onTextInput, addressSearchDebounceMs), []); return ( - -
- - + + {!hideManualButton && ( + + + + )} +
); } diff --git a/packages/lib/src/components/internal/Address/types.ts b/packages/lib/src/components/internal/Address/types.ts index bd530ed6e2..377d1cc94d 100644 --- a/packages/lib/src/components/internal/Address/types.ts +++ b/packages/lib/src/components/internal/Address/types.ts @@ -2,7 +2,7 @@ import { AddressField, AddressData } from '../../../types'; import Specifications from './Specifications'; import { ValidatorRules } from '../../../utils/Validator/types'; import { ValidationRuleResult } from '../../../utils/Validator/ValidationRuleResult'; -import { OnAddressLookupType } from './components/AddressSearch'; +import { OnAddressLookupType, OnAddressSelectedType } from './components/AddressSearch'; // Describes an object with unknown keys whose value is always a string export type StringObject = { @@ -16,6 +16,8 @@ export interface AddressProps { label?: string; onChange: (newState) => void; onAddressLookup?: OnAddressLookupType; + onAddressSelected?: OnAddressSelectedType; + addressSearchDebounceMs?: number; requiredFields?: string[]; ref?: any; specifications?: AddressSpecifications; diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.scss b/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.scss index 0e3ced8909..478b99f213 100644 --- a/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.scss +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.scss @@ -1,14 +1,173 @@ -.adyen-checkout-ctp__loading { - &-image { - display: block; - margin: 30px auto auto; +@keyframes card-bounce { + 0% { + -webkit-translate: 0 -60%; + -moz-translate: 0 -60%; + -ms-translate: 0 -60%; + translate: 0 -60%; + animation-timing-function: cubic-bezier(0.17, 0.17, 0.32, 1); } - &-subtitle { - font-size: 16px; - line-height: 19px; - max-width: 280px; - text-align: center; - margin: 0 auto 58px; + 9.70874% { + -webkit-translate: 0 8.5%; + -moz-translate: 0 8.5%; + -ms-translate: 0 8.5%; + translate: 0 8.5%; + animation-timing-function: cubic-bezier(0.41, 0, 0.49, 1); } + + 23.4466% { + -webkit-translate: 0 -22.5%; + -moz-translate: 0 -22.5%; + -ms-translate: 0 -22.5%; + translate: 0 -22.5%; + animation-timing-function: cubic-bezier(0.29, 0, 0.26, 1); + } + + 47.7184% { + -webkit-translate: 0; + -moz-translate: 0; + -ms-translate: 0; + translate: 0; + } + + 63.8835% { + -webkit-translate: 0; + -moz-translate: 0; + -ms-translate: 0; + translate: 0; + animation-timing-function: cubic-bezier(1, 0, 0.78, 1); + } + + 86.5534% { + -webkit-translate: 0 40.7%; + -moz-translate: 0 40.7%; + -ms-translate: 0 40.7%; + translate: 0 40.7%; + } + + 100% { + -webkit-translate: 0 40.7%; + -moz-translate: 0 40.7%; + -ms-translate: 0 40.7%; + translate: 0 40.7%; + } +} + +@keyframes card-fade-100 { + 0% { + opacity: 0; + animation-timing-function: linear; + } + + 8.1068% { + opacity: 1; + } + + 78.4466% { + opacity: 1; + } + + 86.5534% { + opacity: 0; + } + + 100% { + opacity: 0; + } +} + +@keyframes card-fade-60 { + 0% { + opacity: 0; + animation-timing-function: linear; + } + + 8.1068% { + opacity: 0.6; + } + + 78.4466% { + opacity: 0.6; + } + + 86.5534% { + opacity: 0; + } + + 100% { + opacity: 0; + } +} + +@keyframes card-fade-20 { + 0% { + opacity: 0; + animation-timing-function: linear; + } + + 8.1068% { + opacity: 0.2; + } + + 78.4466% { + opacity: 0.2; + } + + 86.5534% { + opacity: 0; + } + + 100% { + opacity: 0; + } +} + +.adyen-checkout-ctp__card-animation { + position: relative; + max-width: 100%; + aspect-ratio: 1/0.4380; + filter: grayscale(1); + width: 140px; + margin: 40px auto 50px; +} + +.adyen-checkout-ctp__card-animation >.adyen-checkout-ctp__card-animation-layer { + opacity: 0; + width: 100%; + height: 100%; + position: absolute; + background-size: contain!important; +} + +.adyen-checkout-ctp__card-animation >.adyen-checkout-ctp__card-animation-layer:nth-of-type(1) { + z-index: 3; + opacity: 1; + background: url('data:image/svg+xml,') left top no-repeat,url('data:image/svg+xml,') left top no-repeat; + animation: card-bounce 2060ms linear infinite both, card-fade-100 2060ms linear infinite both; + animation-delay: 100ms; +} + +.adyen-checkout-ctp__card-animation >.adyen-checkout-ctp__card-animation-layer:nth-of-type(2) { + z-index: 2; + opacity: 0.6; + transform: translateY(25%); + background: url('data:image/svg+xml,') left top no-repeat; + animation: card-bounce 2060ms linear infinite both, card-fade-60 2060ms linear infinite both; + animation-delay: 50ms; +} + +.adyen-checkout-ctp__card-animation >.adyen-checkout-ctp__card-animation-layer:nth-of-type(3) { + z-index: 1; + opacity: 0.2; + transform: translateY(50%); + background: url('data:image/svg+xml,') left top no-repeat; + animation: card-bounce 2060ms linear infinite both, card-fade-20 2060ms linear infinite both; +} + +.adyen-checkout-ctp__loading-subtitle { + font-size: 16px; + line-height: 19px; + max-width: 280px; + text-align: center; + margin: 0 auto 58px; } diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.tsx b/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.tsx index 0177b82c13..719073158a 100644 --- a/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.tsx +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPLoader/CtPLoader.tsx @@ -1,21 +1,18 @@ import { Fragment, h } from 'preact'; import useCoreContext from '../../../../../core/Context/useCoreContext'; -import useImage from '../../../../../core/Context/useImage'; -import Img from '../../../Img'; import './CtPLoader.scss'; const CtPLoader = (): h.JSX.Element => { const { i18n } = useCoreContext(); - const getImage = useImage(); return ( - +
+
+
+
+
+
{i18n.get('ctp.loading.intro')}
); diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtPLoginInput.tsx b/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtPLoginInput.tsx index 244b8df5fe..4a89bedc77 100644 --- a/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtPLoginInput.tsx +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtPLoginInput.tsx @@ -4,7 +4,7 @@ import { loginValidationRules } from './validate'; import useCoreContext from '../../../../../core/Context/useCoreContext'; import useForm from '../../../../../utils/useForm'; import Field from '../../../FormFields/Field'; -import InputText from '../../../FormFields/InputText'; +import InputEmail from '../../../FormFields/InputEmail'; type OnChangeProps = { data: CtPLoginInputDataState; valid; errors; isValid: boolean }; @@ -73,7 +73,7 @@ const CtPLoginInput = (props: CtPLoginInputProps): h.JSX.Element => { errorMessage={isLoginInputDirty ? props.errorMessage || !!errors.shopperLogin : null} classNameModifiers={['shopperLogin']} > - (), configuration = {} } = {}) => { + return render( + + + {ui} + + + ); +}; + +describe('Click to Pay - CtPOneTimePassword', () => { + test('should set to store the cookie if shopper ticks the checkbox', async () => { + const user = userEvent.setup(); + const ctpServiceMock = mock(); + ctpServiceMock.schemes = ['visa', 'mc']; + // const onResendCodeMock = jest.fn(); + + customRender(, { clickToPayService: ctpServiceMock }); + + // Default false + const checkbox = (await screen.findByLabelText('Skip verification next time')) as HTMLInputElement; + expect(checkbox.checked).toBe(false); + + // Checked + await user.click(checkbox); + expect(checkbox.checked).toBe(true); + expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledWith(true); + expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledTimes(1); + + // Unchecked + await user.click(checkbox); + expect(checkbox.checked).toBe(false); + expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledWith(false); + expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledTimes(2); + }); + + test('should pass OTP to ClickToPay service', async () => { + const user = userEvent.setup({ delay: 100 }); + const ctpServiceMock = mock(); + ctpServiceMock.schemes = ['visa', 'mc']; + + customRender(, { clickToPayService: ctpServiceMock }); + + await screen.findByLabelText('One time code', { exact: false }); + + await user.keyboard('654321'); + await user.keyboard('[Enter]'); + + expect(ctpServiceMock.finishIdentityValidation).toHaveBeenCalledWith('654321'); + }); +}); diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPOneTimePassword.tsx b/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPOneTimePassword.tsx index 825c9173be..576aa2380b 100644 --- a/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPOneTimePassword.tsx +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPOneTimePassword.tsx @@ -8,6 +8,7 @@ import { CtPInfo } from '../CtPInfo'; import CtPSection from '../CtPSection'; import useCoreContext from '../../../../../core/Context/useCoreContext'; import './CtPOneTimePassword.scss'; +import CtPSaveCookiesCheckbox from './CtPSaveCookiesCheckbox'; type CtPOneTimePasswordProps = { onDisplayCardComponent?(): void; @@ -80,6 +81,9 @@ const CtPOneTimePassword = ({ onDisplayCardComponent }: CtPOneTimePasswordProps) onResendCode={onResendCode} isValidatingOtp={isValidatingOtp} /> + + + +
+ ) : ( + {i18n.get('ctp.otp.saveCookiesCheckbox.information')} + )} +

+ + ); +} + +export default CtPSaveCookiesCheckbox; diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPSaveCookiesCheckbox/index.ts b/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPSaveCookiesCheckbox/index.ts new file mode 100644 index 0000000000..bfaacd0df1 --- /dev/null +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPOneTimePassword/CtPSaveCookiesCheckbox/index.ts @@ -0,0 +1 @@ +export { default } from './CtPSaveCookiesCheckbox'; diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPSection.scss b/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPSection.scss index 24091c246f..89d0f72884 100644 --- a/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPSection.scss +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPSection.scss @@ -3,7 +3,7 @@ .adyen-checkout-ctp__section { position: relative; background-color: white; - box-shadow: 0 8px 24px rgb(0 0 0 / 15%); + box-shadow: 0 6px 12px rgb(0 17 44 / 8%), 0 2px 4px rgb(0 17 44 / 4%); border-radius: 12px; padding: 16px; @@ -11,7 +11,7 @@ display: flex; align-items: center; height: 18px; - margin-bottom: 14px; + margin-bottom: 16px; } &--standalone { @@ -31,8 +31,8 @@ font-size: 17px; font-weight: 600; line-height: 22px; - margin: 0 0 4px; padding: 0; + margin: 0; width: auto; @media screen and (max-width: 400px) { @@ -49,7 +49,7 @@ font-size: 13px; font-weight: 400; line-height: 19px; - color: $color-gray-darker; + color: $color-new-gray-darker; margin: 0 0 16px; } diff --git a/packages/lib/src/components/internal/ClickToPay/context/ClickToPayContext.ts b/packages/lib/src/components/internal/ClickToPay/context/ClickToPayContext.ts index 4bee65d829..be561ef336 100644 --- a/packages/lib/src/components/internal/ClickToPay/context/ClickToPayContext.ts +++ b/packages/lib/src/components/internal/ClickToPay/context/ClickToPayContext.ts @@ -11,8 +11,10 @@ export interface IClickToPayContext extends Pick { isStandaloneComponent: boolean; isCtpPrimaryPaymentMethod: boolean; + isStoringCookies: boolean; setIsCtpPrimaryPaymentMethod(isPrimary: boolean): void; logoutShopper(): Promise; + updateStoreCookiesConsent(shouldStore: boolean): void; ctpState: CtpState; cards: ShopperCard[]; schemes: string[]; @@ -37,8 +39,10 @@ const ClickToPayContext = createContext({ configuration: null, isStandaloneComponent: null, isCtpPrimaryPaymentMethod: null, + isStoringCookies: false, setIsCtpPrimaryPaymentMethod: null, logoutShopper: null, + updateStoreCookiesConsent: null, ctpState: null, cards: [], schemes: [], diff --git a/packages/lib/src/components/internal/ClickToPay/context/ClickToPayProvider.tsx b/packages/lib/src/components/internal/ClickToPay/context/ClickToPayProvider.tsx index ed0916994a..41f2add472 100644 --- a/packages/lib/src/components/internal/ClickToPay/context/ClickToPayProvider.tsx +++ b/packages/lib/src/components/internal/ClickToPay/context/ClickToPayProvider.tsx @@ -90,6 +90,13 @@ const ClickToPayProvider = ({ await ctpService?.logout(); }, [ctpService]); + const updateStoreCookiesConsent = useCallback( + (shouldStore: boolean) => { + ctpService.updateStoreCookiesConsent(shouldStore); + }, + [ctpService] + ); + return ( diff --git a/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.test.ts b/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.test.ts index b543b90723..1ecd73f515 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.test.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.test.ts @@ -4,8 +4,94 @@ import { ISrcSdkLoader } from './sdks/SrcSdkLoader'; import VisaSdk from './sdks/VisaSdk'; import MastercardSdk from './sdks/MastercardSdk'; import { IdentityLookupParams, SchemesConfiguration } from './types'; -import { SrciIdentityLookupResponse, SrcProfile } from './sdks/types'; +import { SrciCheckoutResponse, SrciIdentityLookupResponse, SrcProfile } from './sdks/types'; import SrciError from './sdks/SrciError'; +import ShopperCard from '../models/ShopperCard'; + +test('should be able to tweak the configuration to store the cookie', () => { + const visa = mock(); + const schemesConfig = mock(); + const sdkLoader = mock(); + sdkLoader.load.mockResolvedValue([visa]); + + const service = new ClickToPayService(schemesConfig, sdkLoader, 'test'); + expect(service.storeCookies).toBe(false); + + service.updateStoreCookiesConsent(true); + expect(service.storeCookies).toBe(true); + + service.updateStoreCookiesConsent(false); + expect(service.storeCookies).toBe(false); +}); + +test('should pass the complianceSettings if the cookie is set to be stored', async () => { + const checkoutResponseMock = mock(); + checkoutResponseMock.dcfActionCode = 'COMPLETE'; + + const profileFromVisaSrcSystem: SrcProfile = { + srcCorrelationId: '123456', + profiles: [ + { + maskedCards: [ + { + srcDigitalCardId: 'xxxx', + panLastFour: '8902', + dateOfCardLastUsed: '2019-09-28T08:10:02.312Z', + paymentCardDescriptor: 'visa', + panExpirationMonth: '12', + panExpirationYear: '2020', + digitalCardData: { + descriptorName: 'Visa', + artUri: 'https://image.com/visa' + }, + tokenId: '9w8e8e' + } + ] + } + ] + }; + + const visa = mock(); + // @ts-ignore Mocking readonly property + visa.schemeName = 'visa'; + visa.checkout.mockResolvedValue(checkoutResponseMock); + visa.init.mockResolvedValue(); + visa.isRecognized.mockResolvedValue({ recognized: true, idTokens: ['id-token'] }); + visa.getSrcProfile.mockResolvedValue(profileFromVisaSrcSystem); + + const sdkLoader = mock(); + const schemesConfig = mock(); + + const shopperCard = mock(); + shopperCard.srcDigitalCardId = 'xxxx'; + shopperCard.srcCorrelationId = 'zzzz'; + shopperCard.scheme = 'visa'; + Object.defineProperty(shopperCard, 'isDcfPopupEmbedded', { + get: jest.fn(() => false) + }); + + sdkLoader.load.mockResolvedValue([visa]); + + const service = new ClickToPayService(schemesConfig, sdkLoader, 'test'); + service.updateStoreCookiesConsent(true); + + await service.initialize(); + await service.checkout(shopperCard); + + expect(visa.checkout).toHaveBeenCalledTimes(1); + expect(visa.checkout).toHaveBeenCalledWith({ + complianceSettings: { + complianceResources: [ + { + complianceType: 'REMEMBER_ME', + uri: '' + } + ] + }, + srcCorrelationId: 'zzzz', + srcDigitalCardId: 'xxxx' + }); +}); test('should pass the correct configuration to the respective scheme SDKs', async () => { const visa = mock(); diff --git a/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.ts b/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.ts index 3b2e47b109..a23c882620 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/ClickToPayService.ts @@ -55,6 +55,11 @@ class ClickToPayService implements IClickToPayService { public shopperCards: ShopperCard[] = null; public identityValidationData: IdentityValidationData = null; + /** + * Indicates if the shopper opted for saving cookies during the transaction + */ + public storeCookies = false; + constructor( schemesConfig: SchemesConfiguration, sdkLoader: ISrcSdkLoader, @@ -77,12 +82,17 @@ class ClickToPayService implements IClickToPayService { return this.sdkLoader.schemes; } + public updateStoreCookiesConsent(shouldStore: boolean) { + this.storeCookies = shouldStore; + } + public async initialize(): Promise { this.setState(CtpState.Loading); try { this.sdks = await this.sdkLoader.load(this.environment); await this.initiateSdks(); + const { recognized = false, idTokens = null } = await this.verifyIfShopperIsRecognized(); if (recognized) { @@ -104,10 +114,14 @@ class ClickToPayService implements IClickToPayService { this.setState(CtpState.NotAvailable); } catch (error) { - if (error instanceof SrciError) console.warn(`Error at ClickToPayService # init: ${error.toString()}`); - if (error instanceof TimeoutError) { + if (error instanceof SrciError && error?.reason === 'REQUEST_TIMEOUT') { + const timeoutError = new TimeoutError(`ClickToPayService - Timeout during ${error.source}() of the scheme '${error.scheme}'`); + this.onTimeout?.(timeoutError); + } else if (error instanceof TimeoutError) { console.warn(error.toString()); this.onTimeout?.(error); + } else if (error instanceof SrciError) { + console.warn(`Error at ClickToPayService # init: ${error.toString()}`); } else { console.warn(error); } @@ -168,7 +182,8 @@ class ClickToPayService implements IClickToPayService { const checkoutResponse = await checkoutSdk.checkout({ srcDigitalCardId: card.srcDigitalCardId, srcCorrelationId: card.srcCorrelationId, - ...(card.isDcfPopupEmbedded && { windowRef: window.frames[CTP_IFRAME_NAME] }) + ...(card.isDcfPopupEmbedded && { windowRef: window.frames[CTP_IFRAME_NAME] }), + ...(this.storeCookies && { complianceSettings: { complianceResources: [{ complianceType: 'REMEMBER_ME', uri: '' }] } }) }); if (checkoutResponse.dcfActionCode !== 'COMPLETE') { diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.ts index 901439a5ae..0df9d20d75 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.ts @@ -33,12 +33,17 @@ class MastercardSdk extends AbstractSrcInitiator { } public async init(params: SrcInitParams, srciTransactionId: string): Promise { - const sdkProps = { - ...params, - ...getMastercardSettings(this.customSdkConfiguration), - srciTransactionId - }; - await this.schemeSdk.init(sdkProps); + try { + const sdkProps = { + ...params, + ...getMastercardSettings(this.customSdkConfiguration), + srciTransactionId + }; + await this.schemeSdk.init(sdkProps); + } catch (err) { + const srciError = new SrciError(err, 'init', this.schemeName); + throw srciError; + } } public async identityLookup({ identityValue, type }: SrcIdentityLookupParams): Promise { diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.ts index bbe3b3ae35..90eb4249b6 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.ts @@ -33,13 +33,18 @@ class VisaSdk extends AbstractSrcInitiator { } public async init(params: SrcInitParams, srciTransactionId: string): Promise { - const sdkProps = { - ...params, - ...getVisaSetttings(this.customSdkConfiguration), - srciTransactionId - }; + try { + const sdkProps = { + ...params, + ...getVisaSetttings(this.customSdkConfiguration), + srciTransactionId + }; - await this.schemeSdk.init(sdkProps); + await this.schemeSdk.init(sdkProps); + } catch (err) { + const srciError = new SrciError(err, 'init', this.schemeName); + throw srciError; + } } public async identityLookup({ identityValue, type }: SrcIdentityLookupParams): Promise { diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts index b52152f2f6..38e09149c1 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts @@ -59,6 +59,16 @@ export type SrcCard = { export type SrcCheckoutParams = { srcCorrelationId: string; srcDigitalCardId: string; + complianceSettings?: { + complianceResources: [ + { + complianceType: 'REMEMBER_ME'; + uri: string; + version?: string; + dataPublished?: string; + } + ]; + }; windowRef?: Window; }; diff --git a/packages/lib/src/components/internal/ClickToPay/services/types.ts b/packages/lib/src/components/internal/ClickToPay/services/types.ts index c6472d48b6..e42fe3fe0e 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/types.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/types.ts @@ -9,6 +9,8 @@ export interface IClickToPayService { identityValidationData: IdentityValidationData; schemes: string[]; shopperAccountFound: boolean; + storeCookies: boolean; + updateStoreCookiesConsent(shouldStore: boolean): void; initialize(): Promise; checkout(card: ShopperCard): Promise; logout(): Promise; diff --git a/packages/lib/src/components/internal/FormFields/Checkbox/Checkbox.scss b/packages/lib/src/components/internal/FormFields/Checkbox/Checkbox.scss index 6868c4d997..bd6d2a8667 100644 --- a/packages/lib/src/components/internal/FormFields/Checkbox/Checkbox.scss +++ b/packages/lib/src/components/internal/FormFields/Checkbox/Checkbox.scss @@ -7,7 +7,6 @@ position: relative; padding-left: 24px; cursor: pointer; - display: inline-block; line-height: 19px; color: $color-black; font-size: $font-size-small; diff --git a/packages/lib/src/components/internal/FormFields/InputEmail.tsx b/packages/lib/src/components/internal/FormFields/InputEmail.tsx index 888601615f..9da03e0d6c 100644 --- a/packages/lib/src/components/internal/FormFields/InputEmail.tsx +++ b/packages/lib/src/components/internal/FormFields/InputEmail.tsx @@ -1,6 +1,6 @@ import { h } from 'preact'; -import InputBase from './InputBase'; +import InputBase, { InputBaseProps } from './InputBase'; -export default function InputEmail(props) { +export default function InputEmail(props: InputBaseProps) { return ; } diff --git a/packages/lib/src/components/internal/FormFields/Select/Select.tsx b/packages/lib/src/components/internal/FormFields/Select/Select.tsx index ada51a00cf..178809e803 100644 --- a/packages/lib/src/components/internal/FormFields/Select/Select.tsx +++ b/packages/lib/src/components/internal/FormFields/Select/Select.tsx @@ -27,7 +27,8 @@ function Select({ uniqueId, disabled, disableTextFilter, - clearOnSelect + clearOnSelect, + blurOnClose }: SelectProps) { const filterInputRef = useRef(null); const selectContainerRef = useRef(null); @@ -75,6 +76,8 @@ function Select({ * Closes the selectList, empties the text filter and focuses the button element */ const closeList = () => { + //blurs the field when the list is closed, makes for a better UX for most users, needs more testing + blurOnClose && filterInputRef.current.blur(); setShowList(false); }; diff --git a/packages/lib/src/components/internal/FormFields/Select/types.ts b/packages/lib/src/components/internal/FormFields/Select/types.ts index bed81faa85..20580da838 100644 --- a/packages/lib/src/components/internal/FormFields/Select/types.ts +++ b/packages/lib/src/components/internal/FormFields/Select/types.ts @@ -32,6 +32,7 @@ export interface SelectProps { disabled?: boolean; disableTextFilter?: boolean; clearOnSelect?: boolean; + blurOnClose?: boolean; } export interface SelectButtonProps { diff --git a/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.scss b/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.scss index 0d4bc2eda6..80a89bd5b5 100644 --- a/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.scss +++ b/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.scss @@ -1,7 +1,7 @@ @import "../../../style/index"; .adyen-checkout__open-invoice .adyen-checkout__fieldset--billingAddress { - padding-bottom: 8px; + margin-bottom: 16px; } .adyen-checkout__open-invoice .adyen-checkout__fieldset--deliveryAddress { @@ -14,7 +14,7 @@ } .adyen-checkout__open-invoice .adyen-checkout__field--consentCheckbox { - margin-top: 22px; + margin-top: 16px; } .adyen-checkout__input--separateDeliveryAddress + .adyen-checkout__checkbox__label { diff --git a/packages/lib/src/language/locales/ar.json b/packages/lib/src/language/locales/ar.json index 56286473c5..65f3ca402f 100644 --- a/packages/lib/src/language/locales/ar.json +++ b/packages/lib/src/language/locales/ar.json @@ -4,6 +4,7 @@ "payButton.with": "ادفع %{value} باستخدام %{maskedData}", "close": "إغلاق", "storeDetails": "حفظ لمدفوعاتي القادمة", + "readMore": "اقرأ المزيد", "creditCard.holderName": "الاسم على البطاقة", "creditCard.holderName.placeholder": "جميل سعيد", "creditCard.holderName.invalid": "أدخل الاسم كما هو موضح على البطاقة", @@ -22,7 +23,7 @@ "creditCard.cvcField.placeholder.3digits": "3 أرقام", "creditCard.taxNumber.placeholder": "يوم شهر سنة / 0123456789", "installments": "عدد الأقساط", - "installmentOption": "%{times} x %{partialValue}", + "installmentOption": "%{times} × %{partialValue}", "installmentOptionMonths": "%{times} أشهر", "installments.oneTime": "الدفع مرة واحدة", "installments.installments": "الدفع على أقساط", @@ -260,6 +261,7 @@ "ctp.otp.subtitle": "أدخل الرمز %@ الذي أرسلناه إلى ٪@ للتحقق من هويتك.", "ctp.otp.saveCookiesCheckbox.label": "تخطي التحقق في المرة القادمة", "ctp.otp.saveCookiesCheckbox.information": "حدد حفظ البيانات على جهازك ومتصفحك في المتاجر المشاركة لإتمام عملية الدفع بشكل أسرع. لا يوصى به للأجهزة المشتركة.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "حدد تذكرني على جهازك ومتصفحك.", "ctp.emptyProfile.message": "لا توجد بطاقات مسجلة في هذا الملف التعريفي على Click to Pay", "ctp.separatorText": "أو استخدم", "ctp.cards.title": "أكمل الدفع باستخدام بطاقة Click to Pay", @@ -300,4 +302,4 @@ "ancv.confirmPayment": "استخدم تطبيق ANCV الخاص بك لتأكيد الدفع.", "ancv.form.instruction": "يعد تطبيق Cheque-Vacances ضروريًا للمصادقة على هذه المدفوعات.", "ancv.beneficiaryId.invalid": "أدخل عنوان بريد إلكتروني صحيحًا أو معرف ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/cs-CZ.json b/packages/lib/src/language/locales/cs-CZ.json index 81ba67bbff..d20cded11e 100644 --- a/packages/lib/src/language/locales/cs-CZ.json +++ b/packages/lib/src/language/locales/cs-CZ.json @@ -4,6 +4,7 @@ "payButton.with": "Zaplatit %{value} pomocí %{maskedData}", "close": "Zavřít", "storeDetails": "Uložit pro příští platby", + "readMore": "Přečtěte si více", "creditCard.holderName": "Jméno na kartě", "creditCard.holderName.placeholder": "Jan Novák", "creditCard.holderName.invalid": "Zadejte jméno, jak je uvedeno na kartě", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Zadejte kód %@, který jsme vám odeslali na %@ a ověřte sami sebe.", "ctp.otp.saveCookiesCheckbox.label": "Příště přeskočte ověřování", "ctp.otp.saveCookiesCheckbox.information": "Zvolte, že si je chcete zapamatovat ve svém zařízení a prohlížeči v zúčastněných obchodech, abyste se rychleji odhlásili. Nedoporučuje se pro sdílená zařízení.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Vyberte, abyste si je zapamatovali ve svém zařízení a prohlížeči", "ctp.emptyProfile.message": "V tomto profilu Click to Pay nejsou zaregistrovány žádné karty", "ctp.separatorText": "nebo použijte", "ctp.cards.title": "Dokončete platbu pomocí Click to Pay", @@ -296,4 +298,4 @@ "ancv.confirmPayment": "Pro potvrzení platby použijte aplikaci ANCV.", "ancv.form.instruction": "K potvrzení této platby je nutná aplikace Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Zadejte platnou e-mailovou adresu nebo ID ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/da-DK.json b/packages/lib/src/language/locales/da-DK.json index b24f6e6c17..de494d2fd6 100644 --- a/packages/lib/src/language/locales/da-DK.json +++ b/packages/lib/src/language/locales/da-DK.json @@ -4,6 +4,7 @@ "payButton.with": "Betal %{value} med %{maskedData}", "close": "Luk", "storeDetails": "Gem til min næste betaling", + "readMore": "Læs mere", "creditCard.holderName": "Navn på kort", "creditCard.holderName.placeholder": "J. Hansen", "creditCard.holderName.invalid": "Indtast navn som vist på kortet", @@ -260,6 +261,7 @@ "ctp.otp.subtitle": "Indtast den kode, vi har sendt til %@ for at bekræfte, at det er dig.", "ctp.otp.saveCookiesCheckbox.label": "Spring bekræftelse over næste gang", "ctp.otp.saveCookiesCheckbox.information": "Vælg dette for at blive husket på din enhed og browser i deltagende butikker for hurtigere betaling. Anbefales ikke på delte enheder.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Vælg dette for at blive husket på din enhed og browser", "ctp.emptyProfile.message": "Ingen kort registreret i denne Click to Pay-profil", "ctp.separatorText": "eller brug", "ctp.cards.title": "Gennemfør betaling med Click to Pay", @@ -297,4 +299,4 @@ "ancv.confirmPayment": "Brug din ANCV-applikation til at bekræfte betalingen.", "ancv.form.instruction": "Cheque-Vacances-applikationen er nødvendig for at validere denne betaling.", "ancv.beneficiaryId.invalid": "Indtast en gyldig e-mailadresse eller ANCV-id" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/de-DE.json b/packages/lib/src/language/locales/de-DE.json index 52249167f9..a00f50be52 100644 --- a/packages/lib/src/language/locales/de-DE.json +++ b/packages/lib/src/language/locales/de-DE.json @@ -4,6 +4,7 @@ "payButton.with": "%{value} mit %{maskedData} zahlen", "close": "Schließen", "storeDetails": "Für zukünftige Zahlvorgänge speichern", + "readMore": "Mehr lesen", "creditCard.holderName": "Name auf der Karte", "creditCard.holderName.placeholder": "A. Müller", "creditCard.holderName.invalid": "Geben Sie den Namen wie auf der Karte gezeigt ein", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Geben Sie den Code ein, der von %@ an %@ gesendet wurde, um Ihre Identität zu bestätigen.", "ctp.otp.saveCookiesCheckbox.label": "Verifizierung beim nächsten Mal überspringen", "ctp.otp.saveCookiesCheckbox.information": "Wählen Sie diese Option, um bei teilnehmenden Geschäften auf Ihrem Gerät und Browser gespeichert zu werden, um den Bestellvorgang zu beschleunigen. Nicht für gemeinsam genutzte Geräte empfohlen.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Wählen Sie diese Option aus, um auf Ihrem Gerät und Browser gespeichert zu werden", "ctp.emptyProfile.message": "In diesem Click-to-Pay-Profil sind keine Karten registriert", "ctp.separatorText": "oder verwenden", "ctp.cards.title": "Zahlung mit Click to Pay abschließen", @@ -296,4 +298,4 @@ "ancv.confirmPayment": "Bestätigen Sie die Zahlung mit Ihrem ANCV-Antrag.", "ancv.form.instruction": "Zur Validierung dieser Zahlung ist der Antrag „Cheque-Vacances“ erforderlich.", "ancv.beneficiaryId.invalid": "Geben Sie eine gültige E-Mail-Adresse oder ANCV-ID ein" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/el-GR.json b/packages/lib/src/language/locales/el-GR.json index 66cae5d4ea..7768ed1715 100644 --- a/packages/lib/src/language/locales/el-GR.json +++ b/packages/lib/src/language/locales/el-GR.json @@ -4,6 +4,7 @@ "payButton.with": "Πληρωμή %{value} με %{maskedData}", "close": "Κλείσιμο", "storeDetails": "Αποθήκευση για την επόμενη πληρωμή μου", + "readMore": "Ανάγνωση περισσότερων", "creditCard.holderName": "Όνομα στην κάρτα", "creditCard.holderName.placeholder": "Γ. Παπαδάκης", "creditCard.holderName.invalid": "Εισαγάγετε το όνομα όπως αναγράφεται στην κάρτα", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Εισαγάγετε τον κωδικό %@ που στάλθηκε στο %@ για να επαληθεύσετε ότι είστε εσείς.", "ctp.otp.saveCookiesCheckbox.label": "Παράβλεψη επαλήθευσης την επόμενη φορά", "ctp.otp.saveCookiesCheckbox.information": "Επιλέξτε να απομνημονεύεται στη συσκευή και στο πρόγραμμα περιήγησής σας στα συμμετέχοντα καταστήματα για ταχύτερη ολοκλήρωση της πληρωμής. Δεν συνιστάται για κοινόχρηστες συσκευές.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Επιλέξτε να απομνημονεύεται στη συσκευή και στο πρόγραμμα περιήγησής σας", "ctp.emptyProfile.message": "Δεν υπάρχουν καταχωρισμένες κάρτες σε αυτό το προφίλ Click to Pay", "ctp.separatorText": "ή χρησιμοποιήστε το", "ctp.cards.title": "Ολοκληρώστε την πληρωμή με το Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Χρησιμοποιήστε την εφαρμογή ANCV για επιβεβαίωση της πληρωμής.", "ancv.form.instruction": "Η εφαρμογή Cheque-Vacances είναι απαραίτητη για επικύρωση της πληρωμής αυτής.", "ancv.beneficiaryId.invalid": "Εισαγάγετε έγκυρη διεύθυνση email ή αναγνωριστικό ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/en-US.json b/packages/lib/src/language/locales/en-US.json index e2b655c912..5339048cf8 100644 --- a/packages/lib/src/language/locales/en-US.json +++ b/packages/lib/src/language/locales/en-US.json @@ -4,6 +4,7 @@ "payButton.with": "Pay %{value} with %{maskedData}", "close": "Close", "storeDetails": "Save for my next payment", + "readMore": "Read more", "creditCard.holderName": "Name on card", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Enter name as shown on card", @@ -261,6 +262,7 @@ "ctp.otp.subtitle": "Enter the code %@ sent to %@ to verify it‘s you.", "ctp.otp.saveCookiesCheckbox.label": "Skip verification next time", "ctp.otp.saveCookiesCheckbox.information": "Select to be remembered on your device and browser at participating stores for faster checkout. Not recommended for shared devices.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Select to be remembered on your device and browser", "ctp.emptyProfile.message": "No cards registered in this Click to Pay profile", "ctp.separatorText": "or use", "ctp.cards.title": "Complete payment with Click to Pay", @@ -301,4 +303,4 @@ "ancv.confirmPayment": "Use your ANCV application to confirm the payment.", "ancv.form.instruction": "The Cheque-Vacances application is necessary to validate this payment.", "ancv.beneficiaryId.invalid": "Enter a valid email address or ANCV ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/es-ES.json b/packages/lib/src/language/locales/es-ES.json index 1f2dcd2f11..db9365554a 100644 --- a/packages/lib/src/language/locales/es-ES.json +++ b/packages/lib/src/language/locales/es-ES.json @@ -4,6 +4,7 @@ "payButton.with": "Pague %{value} con %{maskedData}", "close": "Cerrar", "storeDetails": "Recordar para mi próximo pago", + "readMore": "Leer más", "creditCard.holderName": "Nombre en la tarjeta", "creditCard.holderName.placeholder": "Juan Pérez", "creditCard.holderName.invalid": "Introduzca el nombre como se muestra en la tarjeta", @@ -254,6 +255,7 @@ "ctp.otp.subtitle": "Introduzca el código %@ que le hemos enviado a %@ para verificar que es usted.", "ctp.otp.saveCookiesCheckbox.label": "Omitir verificación la próxima vez", "ctp.otp.saveCookiesCheckbox.information": "Seleccione esta opción para recordarle en su dispositivo y navegador en las tiendas participantes para agilizar el proceso de pago. No lo recomendamos para dispositivos compartidos.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Seleccione esta opción para recordarle en su dispositivo y navegador", "ctp.emptyProfile.message": "No hay tarjetas registradas en este perfil de Click to Pay", "ctp.separatorText": "o utilice", "ctp.cards.title": "Completar el pago con Click to Pay", @@ -291,4 +293,4 @@ "ancv.confirmPayment": "Utilice su solicitud de la ANCV para confirmar el pago.", "ancv.form.instruction": "La aplicación de Cheque-Vacances es necesaria para validar este pago.", "ancv.beneficiaryId.invalid": "Introduzca una dirección de correo electrónico válida o un documento de identidad de la ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/fi-FI.json b/packages/lib/src/language/locales/fi-FI.json index 1a4c602c15..2a9dde395a 100644 --- a/packages/lib/src/language/locales/fi-FI.json +++ b/packages/lib/src/language/locales/fi-FI.json @@ -4,6 +4,7 @@ "payButton.with": "Maksa %{value} käyttäen maksutapaa %{maskedData}", "close": "Sulje", "storeDetails": "Tallenna seuraavaa maksuani varten", + "readMore": "Lue lisää", "creditCard.holderName": "Nimi kortilla", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Syötä nimi sellaisena kuin se on kortissa", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Syötä koodi, jonka %@ lähetti osoitteeseen %@ vahvistaaksesi, että kyseessä olet sinä.", "ctp.otp.saveCookiesCheckbox.label": "Ohita vahvistus seuraavalla kerralla", "ctp.otp.saveCookiesCheckbox.information": "Valitse, että sinut muistetaan laitteellasi ja selaimessa osallistuvissa myymälöissä, jotta voit maksaa nopeammin. Ei suositella jaetuilla laitteilla.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Valitse, jotta sinut muistetaan laitteellasi ja selaimessa", "ctp.emptyProfile.message": "Tähän Click to Pay -profiiliin ei ole rekisteröity kortteja", "ctp.separatorText": "tai käytä", "ctp.cards.title": "Suorita maksu Click to Paylla", @@ -296,4 +298,4 @@ "ancv.confirmPayment": "Vahvista maksusi ANCV-sovelluksella.", "ancv.form.instruction": "Tämän maksun vahvistaminen edellyttää Cheque-Vacances -sovelluksen.", "ancv.beneficiaryId.invalid": "Anna kelvollinen sähköpostiosoite tai ANCV-tunnus" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/fr-FR.json b/packages/lib/src/language/locales/fr-FR.json index b93cce364b..f4ad64e25a 100644 --- a/packages/lib/src/language/locales/fr-FR.json +++ b/packages/lib/src/language/locales/fr-FR.json @@ -4,6 +4,7 @@ "payButton.with": "Payer %{value} avec %{maskedData}", "close": "Fermer", "storeDetails": "Sauvegarder pour mon prochain paiement", + "readMore": "Lire la suite", "creditCard.holderName": "Nom sur la carte", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Entrez le nom tel qu'il apparaît sur la carte", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Saisissez le code %@ envoyé à %@ pour vérifier votre identité.", "ctp.otp.saveCookiesCheckbox.label": "Ignorer la vérification la prochaine fois", "ctp.otp.saveCookiesCheckbox.information": "Sélectionnez cette option pour mémoriser les informations sur votre appareil et votre navigateur afin d'accélérer le paiement dans les magasins participants. Non recommandé pour les appareils partagés.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Sélectionner cette option pour mémoriser les informations sur votre appareil et votre navigateur", "ctp.emptyProfile.message": "Aucune carte enregistrée dans ce profil Click to Pay", "ctp.separatorText": "ou utilisez", "ctp.cards.title": "Terminez le paiement avec Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Utilisez votre application ANCV pour confirmer le paiement.", "ancv.form.instruction": "L'application Chèque-Vacances est nécessaire pour valider ce paiement.", "ancv.beneficiaryId.invalid": "Saisissez une adresse e-mail ou un identifiant ANCV valide" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/hr-HR.json b/packages/lib/src/language/locales/hr-HR.json index e0ac43f501..2277f45fdd 100644 --- a/packages/lib/src/language/locales/hr-HR.json +++ b/packages/lib/src/language/locales/hr-HR.json @@ -4,6 +4,7 @@ "payButton.with": "Platite iznos od %{value} uporabom stavke %{maskedData}", "close": "Zatvori", "storeDetails": "Pohrani za moje sljedeće plaćanje", + "readMore": "Opširnije", "creditCard.holderName": "Ime na kartici", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Unesite ime kako je prikazano na kartici", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Upišite kôd %@ koji smo poslali na %@ da bismo potvrdili da ste to vi.", "ctp.otp.saveCookiesCheckbox.label": "Sljedeći puta preskoči provjeru", "ctp.otp.saveCookiesCheckbox.information": "Odaberite kako biste bili zapamćeni na svom uređaju i pregledniku u uključenim trgovinama za bržu naplatu. Ne preporučuje se za zajedničke uređaje.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Odaberite ako želite da vas se zapamti na vašem uređaju i pregledniku", "ctp.emptyProfile.message": "Nema kartica registriranih u sklopu ovog profila usluge Click to Pay", "ctp.separatorText": "ili upotrijebite", "ctp.cards.title": "Dovrši plaćanje uslugom Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Koristite svoju ANCV aplikaciju za potvrdu plaćanja.", "ancv.form.instruction": "Za potvrdu ove uplate neophodna je aplikacija Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Unesite valjanu adresu e-pošte ili ANCV ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/hu-HU.json b/packages/lib/src/language/locales/hu-HU.json index 1a32d70d1e..cff15e4878 100644 --- a/packages/lib/src/language/locales/hu-HU.json +++ b/packages/lib/src/language/locales/hu-HU.json @@ -4,6 +4,7 @@ "payButton.with": "%{value} fizetése a következővel: %{maskedData}", "close": "Bezárás", "storeDetails": "Mentés a következő fizetéshez", + "readMore": "Bővebben", "creditCard.holderName": "A kártyán szereplő név", "creditCard.holderName.placeholder": "Gipsz Jakab", "creditCard.holderName.invalid": "Adja meg a kártyán szereplő nevet", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Írja be a kódot, amit a(z) %@ küldött %@ részére ellenőrzés céljából.", "ctp.otp.saveCookiesCheckbox.label": "Ellenőrzés kihagyása a következő alkalommal", "ctp.otp.saveCookiesCheckbox.information": "Jelölje be, hogy a résztvevő üzletek a gyorsabb fizetés érdekében emlékezzenek az eszközre és a böngészőre. Megosztott eszközök esetén nem ajánlott.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Jelölje be, hogy a rendszer emlékezzen az eszközre és a böngészőre", "ctp.emptyProfile.message": "Ebben a Click to Pay-profilban nincs regisztrált kártya", "ctp.separatorText": "vagy használja a következőt", "ctp.cards.title": "Fizetés Click to Pay használatával", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "A fizetés megerősítéséhez használja az ANCV alkalmazást.", "ancv.form.instruction": "A fizetés érvényesítéséhez a Cheque-Vacances alkalmazás szükséges.", "ancv.beneficiaryId.invalid": "Adjon meg egy érvényes e-mail-címet vagy ANCV-azonosítót" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/it-IT.json b/packages/lib/src/language/locales/it-IT.json index 6c605de00e..8ac6d512c9 100644 --- a/packages/lib/src/language/locales/it-IT.json +++ b/packages/lib/src/language/locales/it-IT.json @@ -4,6 +4,7 @@ "payButton.with": "Paga %{value} con %{maskedData}", "close": "Chiudi", "storeDetails": "Salva per il prossimo pagamento", + "readMore": "Leggi di più", "creditCard.holderName": "Titolare carta", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Immetti il nome riportato sulla carta", @@ -257,6 +258,7 @@ "ctp.otp.subtitle": "Inserisci il codice che %@ ha inviato a %@ per verificare la tua identità.", "ctp.otp.saveCookiesCheckbox.label": "Salta la verifica la prossima volta", "ctp.otp.saveCookiesCheckbox.information": "Scegli di essere ricordato sul dispositivo e il browser che utilizzi per velocizzare il pagamento nei negozi partecipanti. Non consigliato per dispositivi condivisi.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Seleziona per essere ricordato sul tuo dispositivo e sul browser", "ctp.emptyProfile.message": "Non esistono carte registrate in questo profilo Click to Pay", "ctp.separatorText": "o utilizza", "ctp.cards.title": "Completa il pagamento con Click to Pay", @@ -297,4 +299,4 @@ "ancv.confirmPayment": "Utilizza la tua richiesta ANCV per confermare il pagamento.", "ancv.form.instruction": "Per confermare il pagamento è necessario utilizzare la richiesta Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Inserisci un indirizzo e-mail o un identificativo ANCV valido" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/ja-JP.json b/packages/lib/src/language/locales/ja-JP.json index df0f9dd89e..20dfef21dd 100644 --- a/packages/lib/src/language/locales/ja-JP.json +++ b/packages/lib/src/language/locales/ja-JP.json @@ -4,6 +4,7 @@ "payButton.with": "%{value}を%{maskedData}で支払う", "close": "終了", "storeDetails": "次回のお支払いのため詳細を保存", + "readMore": "詳細を確認", "creditCard.holderName": "カード上の名前", "creditCard.holderName.placeholder": "Taro Yamada", "creditCard.holderName.invalid": "カードに記載されている名前を入力してください", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "%@から%@に送信されたコードを入力して、ご本人であることを確認してください。", "ctp.otp.saveCookiesCheckbox.label": "次回は検証をスキップする", "ctp.otp.saveCookiesCheckbox.information": "対象店舗で、お使いのデバイスとブラウザで記憶させるように選択すると、より迅速なチェックアウトが可能になります。共有デバイスでは推奨されません。", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "選択すると、お使いのデバイスとブラウザに記憶されます", "ctp.emptyProfile.message": "このClick to Payプロフィールに登録されているカードはありません。", "ctp.separatorText": "または以下を使用", "ctp.cards.title": "Click to Payで支払いを完了する", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "ANCVアプリケーションを使用して、支払を確認してください。", "ancv.form.instruction": "この支払を検証するには、Cheque-Vacancesアプリケーションが必要です。", "ancv.beneficiaryId.invalid": "有効なメールアドレスまたはANCV IDを入力してください" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/ko-KR.json b/packages/lib/src/language/locales/ko-KR.json index 20de9be36b..f362ce5006 100644 --- a/packages/lib/src/language/locales/ko-KR.json +++ b/packages/lib/src/language/locales/ko-KR.json @@ -4,6 +4,7 @@ "payButton.with": "%{maskedData}(으)로 %{value} 결제", "close": "닫기", "storeDetails": "다음 결제를 위해 이 수단 저장", + "readMore": "자세히 보기", "creditCard.holderName": "카드상의 이름", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "카드에 표시된 대로 이름을 입력합니다.", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "%@로 전송된 %@ 코드를 입력하여 본인 인증을 진행합니다.", "ctp.otp.saveCookiesCheckbox.label": "다음 번에는 인증 건너뛰기", "ctp.otp.saveCookiesCheckbox.information": "더 빠른 결제를 위해 사용자 기기와 참여 매장의 브라우저에서 정보를 기억하도록 선택하세요. 공유 기기에는 권장하지 않습니다.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "장치와 브라우저에서 기억되도록 선택하기", "ctp.emptyProfile.message": "해당 Click to Pay 프로필에 등록된 카드가 없습니다", "ctp.separatorText": "또는 사용", "ctp.cards.title": "Click to Pay로 결제 완료", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "ANCV 애플리케이션을 사용해 결제를 확인하세요.", "ancv.form.instruction": "이 결제를 인증하려면 Cheque-Vacances 애플리케이션이 필요합니다.", "ancv.beneficiaryId.invalid": "유효한 이메일 주소나 ANCV ID를 입력하세요." -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/nl-NL.json b/packages/lib/src/language/locales/nl-NL.json index b739f72a6e..d4e58b529e 100644 --- a/packages/lib/src/language/locales/nl-NL.json +++ b/packages/lib/src/language/locales/nl-NL.json @@ -4,6 +4,7 @@ "payButton.with": "Betaal %{value} met %{maskedData}", "close": "Sluiten", "storeDetails": "Bewaar voor mijn volgende betaling", + "readMore": "Verder lezen", "creditCard.holderName": "Naam op kaart", "creditCard.holderName.placeholder": "J. Janssen", "creditCard.holderName.invalid": "Voer naam in zoals weergegeven op kaart", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Voer de code %@ in die naar %@ is gestuurd, zodat we kunnen verifiëren dat u het bent.", "ctp.otp.saveCookiesCheckbox.label": "Verificatie de volgende keer overslaan", "ctp.otp.saveCookiesCheckbox.information": "Selecteer om te worden onthouden op uw apparaat en in uw browser bij deelnemende winkels, zodat u sneller kunt afrekenen. Niet aanbevolen voor gedeelde apparaten.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Selecteer om onthouden te worden op uw apparaat en browser", "ctp.emptyProfile.message": "Geen kaarten geregistreerd voor dit Click to Pay-profiel", "ctp.separatorText": "of gebruik", "ctp.cards.title": "Voltooi de betaling met Click to Pay", @@ -296,4 +298,4 @@ "ancv.confirmPayment": "Gebruik uw ANCV-toepassing om de betaling te bevestigen.", "ancv.form.instruction": "De Cheque-Vacances applicatie is nodig om deze betaling te valideren.", "ancv.beneficiaryId.invalid": "Voer een geldig e-mailadres of ANCV-id in" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/no-NO.json b/packages/lib/src/language/locales/no-NO.json index 64b5b2b970..69efa2fbb3 100644 --- a/packages/lib/src/language/locales/no-NO.json +++ b/packages/lib/src/language/locales/no-NO.json @@ -4,6 +4,7 @@ "payButton.with": "Betal %{value} med %{maskedData}", "close": "Lukk", "storeDetails": "Lagre til min neste betaling", + "readMore": "Les mer", "creditCard.holderName": "Navn på kortet", "creditCard.holderName.placeholder": "O. Nordmann", "creditCard.holderName.invalid": "Skriv inn navnet som vist på kortet", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Tast inn koden vi sendte til %@, for å verifisere at det er deg.", "ctp.otp.saveCookiesCheckbox.label": "Hopp over verifisering neste gang", "ctp.otp.saveCookiesCheckbox.information": "Velg for å bli husket på enheten og i nettleseren din i deltakende butikker for raskere betaling. Anbefales ikke for delte enheter.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Velg for å bli husket på enheten og i nettleseren din", "ctp.emptyProfile.message": "Ingen kort er registrert i denne Click to Pay-profilen", "ctp.separatorText": "eller bruk", "ctp.cards.title": "Fullfør betalingen med Click to Pay", @@ -296,4 +298,4 @@ "ancv.confirmPayment": "Bruk ANCV-appen for å bekrefte betalingen.", "ancv.form.instruction": "Cheque-Vacances-appen er nødvendig for å validere denne betalingen.", "ancv.beneficiaryId.invalid": "Oppgi en gyldig e-postadresse eller ANCV-ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/pl-PL.json b/packages/lib/src/language/locales/pl-PL.json index d32d1ead3b..878a8d3250 100644 --- a/packages/lib/src/language/locales/pl-PL.json +++ b/packages/lib/src/language/locales/pl-PL.json @@ -4,6 +4,7 @@ "payButton.with": "Zapłać %{value} za pomocą %{maskedData}", "close": "Zamknij", "storeDetails": "Zapisz na potrzeby następnej płatności", + "readMore": "Czytaj więcej", "creditCard.holderName": "Imię i nazwisko na karcie", "creditCard.holderName.placeholder": "J. Kowalski", "creditCard.holderName.invalid": "Wpisz imię i nazwisko w takim formacie, jak na karcie", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Wprowadź kod %@ wysłany na adres %@, aby potwierdzić swoją tożsamość.", "ctp.otp.saveCookiesCheckbox.label": "Następnym razem pomiń weryfikację", "ctp.otp.saveCookiesCheckbox.information": "Wybierz opcję zapamiętywania na urządzeniu i w przeglądarce w sklepach uczestniczących w programie, aby przyspieszyć realizację transakcji. Niezalecane dla współużytkowanych urządzeń.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Wybierz, aby zapamiętać dane logowania na Twoim urządzeniu i przeglądarce", "ctp.emptyProfile.message": "W tym profilu Click to Pay nie zarejestrowano żadnych kart", "ctp.separatorText": "lub przypadków jej użycia", "ctp.cards.title": "Dokończ płatność za pomocą funkcji Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Użyj aplikacji ANCV, aby potwierdzić płatność.", "ancv.form.instruction": "Do zatwierdzenia tej płatności konieczna jest aplikacja Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Wprowadź poprawny adres e-mail lub identyfikator ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/pt-BR.json b/packages/lib/src/language/locales/pt-BR.json index 44ef4530ae..4db38b0ce8 100644 --- a/packages/lib/src/language/locales/pt-BR.json +++ b/packages/lib/src/language/locales/pt-BR.json @@ -4,6 +4,7 @@ "payButton.with": "Pague %{value} com %{maskedData}", "close": "Fechar", "storeDetails": "Salvar para meu próximo pagamento", + "readMore": "Leia mais", "creditCard.holderName": "Nome no cartão", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Digite o nome conforme mostrado no cartão", @@ -257,6 +258,7 @@ "ctp.otp.subtitle": "Digite o código %@ enviado para %@ para confirmar que é você.", "ctp.otp.saveCookiesCheckbox.label": "Ignorar verificação da próxima vez", "ctp.otp.saveCookiesCheckbox.information": "Selecione esta opção para ser lembrado no seu dispositivo e navegador, agilizando o pagamento nas lojas participantes. Não recomendado para dispositivos compartilhados.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Selecione para ser lembrado em seu dispositivo e navegador", "ctp.emptyProfile.message": "Não há nenhum cartão cadastrado neste perfil do Click to Pay", "ctp.separatorText": "ou use", "ctp.cards.title": "Concluir o pagamento com o Click to Pay", @@ -297,4 +299,4 @@ "ancv.confirmPayment": "Use o aplicativo da ANCV para confirmar o pagamento.", "ancv.form.instruction": "É necessário ter o aplicativo da Cheque-Vacances para validar este pagamento.", "ancv.beneficiaryId.invalid": "Insira um endereço de e-mail válido ou uma ID da ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/pt-PT.json b/packages/lib/src/language/locales/pt-PT.json index 88f21a05e3..90d8f5472b 100644 --- a/packages/lib/src/language/locales/pt-PT.json +++ b/packages/lib/src/language/locales/pt-PT.json @@ -4,6 +4,7 @@ "payButton.with": "Pagar %{value} com %{maskedData}", "close": "Fechar", "storeDetails": "Guardar para o meu próximo pagamento", + "readMore": "Ler mais", "creditCard.holderName": "Nome no cartão", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Introduzir o nome como mostrado no cartão", @@ -261,6 +262,7 @@ "ctp.otp.subtitle": "Introduza o código %@ enviado para %@ para verificar se é você.", "ctp.otp.saveCookiesCheckbox.label": "Saltar verificação da próxima vez", "ctp.otp.saveCookiesCheckbox.information": "Selecione para ser memorizado no seu dispositivo e navegador nas lojas participantes, para uma finalização de compra mais rápida. Não recomendado para dispositivos partilhados.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Selecione para ser memorizado no seu dispositivo e navegador", "ctp.emptyProfile.message": "Nenhum cartão registado neste perfil Click to Pay", "ctp.separatorText": "ou utilize", "ctp.cards.title": "Pagamento completo com Click to Pay", @@ -301,4 +303,4 @@ "ancv.confirmPayment": "Use a sua aplicação ANCV para confirmar o pagamento.", "ancv.form.instruction": "A app Cheque-Vacances é necessária para validar este pagamento.", "ancv.beneficiaryId.invalid": "Insira um endereço de e-mail ou ID válido de ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/ro-RO.json b/packages/lib/src/language/locales/ro-RO.json index 918b1b423c..01d394ad19 100644 --- a/packages/lib/src/language/locales/ro-RO.json +++ b/packages/lib/src/language/locales/ro-RO.json @@ -4,6 +4,7 @@ "payButton.with": "Plătiți %{value} cu %{maskedData}", "close": "Închidere", "storeDetails": "Salvează pentru următoarea mea plată", + "readMore": "Citiți mai mult", "creditCard.holderName": "Numele de pe card", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "Completați numele după cum figurează pe card", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Introduceți codul %@ pe care l-am trimis la %@ pentru a confirma că sunteți dvs.", "ctp.otp.saveCookiesCheckbox.label": "Omiteți verificarea data viitoare", "ctp.otp.saveCookiesCheckbox.information": "Selectați pentru a fi reținut pe dispozitiv și în browserul dvs. la magazinele participante pentru o plată mai rapidă. Nerecomandat pentru dispozitivele partajate.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Selectați pentru a fi memorat pe dispozitiv și browser", "ctp.emptyProfile.message": "Niciun card înregistrat în acest profil Click to Pay", "ctp.separatorText": "sau folosiți", "ctp.cards.title": "Finalizați plata cu Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Utilizați aplicația ANCV pentru a confirma plata.", "ancv.form.instruction": "Aplicația Cheque-Vacances este necesară pentru a valida această plată.", "ancv.beneficiaryId.invalid": "Completați o adresă de e-mail sau un ID ANCV valabil" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/ru-RU.json b/packages/lib/src/language/locales/ru-RU.json index d3153a02d8..ef3ffe53dc 100644 --- a/packages/lib/src/language/locales/ru-RU.json +++ b/packages/lib/src/language/locales/ru-RU.json @@ -4,6 +4,7 @@ "payButton.with": "Оплатить %{value} %{maskedData}", "close": "Закрыть", "storeDetails": "Сохранить для следующего платежа", + "readMore": "Подробнее", "creditCard.holderName": "Имя на карте", "creditCard.holderName.placeholder": "И. Петров", "creditCard.holderName.invalid": "Введите имя, как оно указано на карте", @@ -256,6 +257,7 @@ "ctp.otp.subtitle": "Для подтверждения своей личности введите код %@, отправленный %@.", "ctp.otp.saveCookiesCheckbox.label": "Пропустить подтверждение в следующий раз", "ctp.otp.saveCookiesCheckbox.information": "Чтобы в соответствующих магазинах запомнили ваше устройство и браузер, выберите эту опцию. Это позволит быстрее оформлять заказы. Не рекомендуется на устройствах совместного использования.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Выберите, чтобы запомнили ваше устройство и браузер", "ctp.emptyProfile.message": "В данном профиле Click to Pay нет зарегистрированных карт", "ctp.separatorText": "или используйте", "ctp.cards.title": "Оформите оплату с помощью Click to Pay", @@ -296,4 +298,4 @@ "ancv.confirmPayment": "Используйте приложение ANCV для подтверждения платежа.", "ancv.form.instruction": "Для подтверждения этого платежа необходимо приложение Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Введите действительный адрес электронной почты или идентификатор ANCV" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/sk-SK.json b/packages/lib/src/language/locales/sk-SK.json index 573beb0e7e..476a4415a0 100644 --- a/packages/lib/src/language/locales/sk-SK.json +++ b/packages/lib/src/language/locales/sk-SK.json @@ -4,6 +4,7 @@ "payButton.with": "Zaplatiť %{value} pomocou %{maskedData}", "close": "Zavrieť", "storeDetails": "Uložiť pre moju ďalšiu platbu", + "readMore": "Prečítajte si viac", "creditCard.holderName": "Meno na karte", "creditCard.holderName.placeholder": "J. Novák", "creditCard.holderName.invalid": "Zadajte meno tak, ako je uvedené na karte", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Zadajte kód %@, ktorý sme poslali na adresu %@, aby sme overili, že ste to vy.", "ctp.otp.saveCookiesCheckbox.label": "Nabudúce preskočiť overovanie", "ctp.otp.saveCookiesCheckbox.information": "Vyberte túto možnosť na zapamätanie v zariadení a prehliadači v zúčastnených obchodoch, čím urýchlite platbu. Neodporúča sa pre zdieľané zariadenia.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Vyberte túto možnosť na zapamätanie v zariadení a prehliadači", "ctp.emptyProfile.message": "V tomto profile Click to Pay nie sú registrované žiadne karty", "ctp.separatorText": "alebo použite", "ctp.cards.title": "Dokončite platbu v službe Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Na potvrdenie platby použite svoju aplikáciu ANCV.", "ancv.form.instruction": "Na potvrdenie tejto platby je potrebná aplikácia Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Zadajte platnú e-mailovú adresu alebo ANCV ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/sl-SI.json b/packages/lib/src/language/locales/sl-SI.json index bfd883eaef..f50c1f8017 100644 --- a/packages/lib/src/language/locales/sl-SI.json +++ b/packages/lib/src/language/locales/sl-SI.json @@ -4,6 +4,7 @@ "payButton.with": "Plačajte %{value} z %{maskedData}", "close": "Zapri", "storeDetails": "Shrani za moje naslednje plačilo", + "readMore": "Preberi več", "creditCard.holderName": "Ime na kartici", "creditCard.holderName.placeholder": "J. Novak", "creditCard.holderName.invalid": "Vnesite ime, kot je prikazano na kartici", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Vnesite kodo %@, ki smo jo poslali na %@, da potrdite, da ste to res vi.", "ctp.otp.saveCookiesCheckbox.label": "Naslednjič preskoči preverjanje", "ctp.otp.saveCookiesCheckbox.information": "Izberite če želite, da se v vaši napravi in brskalniku v sodelujočih trgovinah vaši podatki shranijo za hitrejši zaključek zakupa. Ni priporočljivo za naprave v skupni rabi.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Izberite, če želite, da se v vaši napravi in brskalniku shranijo vaši podatki", "ctp.emptyProfile.message": "V tem profilu Click to Pay ni registriranih nobenih kartic", "ctp.separatorText": "ali uporabite", "ctp.cards.title": "Dokončajte plačilo s storitvijo Click to Pay", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "Za potrditev plačila uporabite aplikacijo ANCV.", "ancv.form.instruction": "Za potrditev tega plačila je potrebna aplikacija Cheque-Vacances.", "ancv.beneficiaryId.invalid": "Vnesite veljaven e-poštni naslov ali ANCV ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/sv-SE.json b/packages/lib/src/language/locales/sv-SE.json index 3004465921..a841bcfa39 100644 --- a/packages/lib/src/language/locales/sv-SE.json +++ b/packages/lib/src/language/locales/sv-SE.json @@ -4,6 +4,7 @@ "payButton.with": "Betala %{value} med %{maskedData}", "close": "Stäng", "storeDetails": "Spara till min nästa betalning", + "readMore": "Läs mer", "creditCard.holderName": "Namn på kort", "creditCard.holderName.placeholder": "A. Andersson", "creditCard.holderName.invalid": "Ange namnet som det står på kortet", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "Ange koden som %@ skickade till %@ för att verifiera att det är du.", "ctp.otp.saveCookiesCheckbox.label": "Hoppa över verifieringen nästa gång", "ctp.otp.saveCookiesCheckbox.information": "Välj att bli ihågkommen på din enhet och webbläsare i deltagande butiker för snabbare utcheckning. Rekommenderas inte för delade enheter.", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "Välj att bli ihågkommen på din enhet och i din webbläsare", "ctp.emptyProfile.message": "Inga kort registrerade i denna Click to Pay-profil", "ctp.separatorText": "eller använd", "ctp.cards.title": "Slutför betalningen med Click to Pay", @@ -297,4 +299,4 @@ "ancv.confirmPayment": "Bekräfta betalningen i din ANCV-app.", "ancv.form.instruction": "Appen Cheque-Vacances krävs för att validera denna betalning.", "ancv.beneficiaryId.invalid": "Ange en giltig e-postadress eller ANCV-ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/zh-CN.json b/packages/lib/src/language/locales/zh-CN.json index 5fe6f7a363..71cb8f968f 100644 --- a/packages/lib/src/language/locales/zh-CN.json +++ b/packages/lib/src/language/locales/zh-CN.json @@ -4,6 +4,7 @@ "payButton.with": "使用 %{maskedData} 支付 %{value}", "close": "关闭", "storeDetails": "保存以便下次支付使用", + "readMore": "阅读更多", "creditCard.holderName": "卡片上的姓名", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "输入卡片上显示的姓名", @@ -258,6 +259,7 @@ "ctp.otp.subtitle": "请输入我们发送至 %@ 的代码 (%@),以验证是您本人。", "ctp.otp.saveCookiesCheckbox.label": "下次跳过验证", "ctp.otp.saveCookiesCheckbox.information": "在您的设备和浏览器上,在参与活动的店铺中选择记住该选项,以便更快地结账。不建议用于共享设备。", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "选择在您的设备和浏览器上记住", "ctp.emptyProfile.message": "此 Click to Pay 个人资料中尚未注册任何卡片", "ctp.separatorText": "或使用", "ctp.cards.title": "借助 Click to Pay 完成付款", @@ -298,4 +300,4 @@ "ancv.confirmPayment": "使用您的 ANCV 应用以确认付款。", "ancv.form.instruction": "要验证这笔付款,必须使用 Checke-Vacances 应用。", "ancv.beneficiaryId.invalid": "输入有效的电子邮件地址或 ANCV ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/language/locales/zh-TW.json b/packages/lib/src/language/locales/zh-TW.json index 2ea7e5ac01..c3525f9c64 100644 --- a/packages/lib/src/language/locales/zh-TW.json +++ b/packages/lib/src/language/locales/zh-TW.json @@ -4,6 +4,7 @@ "payButton.with": "用 %{maskedData} 支付 %{value}", "close": "關閉", "storeDetails": "儲存以供下次付款使用", + "readMore": "閱讀全文", "creditCard.holderName": "信用卡上的姓名", "creditCard.holderName.placeholder": "J. Smith", "creditCard.holderName.invalid": "輸入卡上所示的姓名", @@ -259,6 +260,7 @@ "ctp.otp.subtitle": "請輸入我們傳送至 %@ 的驗證碼 %@,以驗證是您本人。", "ctp.otp.saveCookiesCheckbox.label": "下次略過驗證", "ctp.otp.saveCookiesCheckbox.information": "選擇在裝置和瀏覽器中記住參與商店的驗證資訊,以加快結帳速度。不建議在共享裝置上選取該設定。", + "ctp.otp.saveCookiesCheckbox.shorterInfo": "選擇在裝置和瀏覽器中記住", "ctp.emptyProfile.message": "此 Click to Pay 個人資料中未註冊任何卡", "ctp.separatorText": "或使用", "ctp.cards.title": "使用 Click to Pay 完成付款", @@ -299,4 +301,4 @@ "ancv.confirmPayment": "使用您的 ANCV 應用程式確認付款。", "ancv.form.instruction": "必須申請 Cheque-Vacances 才能驗證此付款。", "ancv.beneficiaryId.invalid": "輸入有效的電子郵件地址或 ANCV ID" -} \ No newline at end of file +} diff --git a/packages/lib/src/utils/isScreenSmall.ts b/packages/lib/src/utils/isScreenSmall.ts new file mode 100644 index 0000000000..3dd210b0ab --- /dev/null +++ b/packages/lib/src/utils/isScreenSmall.ts @@ -0,0 +1,3 @@ +const isScreenSmall = (): boolean => window.matchMedia('(max-width: 480px)').matches; + +export default isScreenSmall; diff --git a/packages/lib/src/utils/useForm/useForm.test.tsx b/packages/lib/src/utils/useForm/useForm.test.tsx index e9e6705e59..c5f8e10ffd 100644 --- a/packages/lib/src/utils/useForm/useForm.test.tsx +++ b/packages/lib/src/utils/useForm/useForm.test.tsx @@ -12,13 +12,11 @@ describe('useForm', () => { describe('schema', () => { let useFormHook; - beforeEach(() => { - // eslint-disable-next-line testing-library/no-render-in-setup + + it('should set a default schema', () => { const { result } = renderHook(() => useForm({ schema: defaultSchema })); useFormHook = result; - }); - it('should set a default schema', () => { expect(useFormHook.current.schema).toEqual(defaultSchema); expect(useFormHook.current.data[defaultSchema[0]]).toEqual(null); expect(useFormHook.current.errors[defaultSchema[0]]).toEqual(null); @@ -29,6 +27,9 @@ describe('useForm', () => { }); it('should update the schema', () => { + const { result } = renderHook(() => useForm({ schema: defaultSchema })); + useFormHook = result; + act(() => { useFormHook.current.setSchema(['email']); }); diff --git a/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx b/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx index 1f6ef7d9a8..8028915dea 100644 --- a/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx +++ b/packages/lib/storybook/stories/wallets/ApplePayExpress.stories.tsx @@ -17,10 +17,16 @@ const meta: Meta = { }; export default meta; -const getShippingMethods = (countryCode: string): ApplePayJS.ApplePayShippingMethod[] => { +/** + * Method that fetches the shipping options according to the country. + * This function in most of the cases is asynchronous, as it will request shipping options in the backend + * + * @param countryCode - country code + */ +const getShippingMethods = (countryCode: string): Promise => { switch (countryCode) { case 'US': { - return [ + return Promise.resolve([ { label: 'Standard Shipping', detail: 'Arrives in 5 to 7 days', @@ -33,11 +39,11 @@ const getShippingMethods = (countryCode: string): ApplePayJS.ApplePayShippingMet amount: '10.99', identifier: 'Express' } - ]; + ]); } case 'NL': default: { - return [ + return Promise.resolve([ { label: 'Free Shipping', detail: 'Arrives in 10 to 15 days', @@ -56,7 +62,7 @@ const getShippingMethods = (countryCode: string): ApplePayJS.ApplePayShippingMet amount: '15.99', identifier: 'Express' } - ]; + ]); } } }; @@ -176,7 +182,7 @@ export const Express: Story = { return; } - const newShippingMethods = getShippingMethods(countryCode); + const newShippingMethods = await getShippingMethods(countryCode); const newLineItems = createLineItems(newShippingMethods[0]); const newTotal = createApplePayTotal(newLineItems); diff --git a/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx b/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx index 1fe2295115..31f5417bce 100644 --- a/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx +++ b/packages/lib/storybook/stories/wallets/GooglePayExpress.stories.tsx @@ -19,6 +19,11 @@ const meta: Meta = { }; export default meta; +/** + * Method that calculate the shipping costs based on the country/shipping options + * + * @param countryCode - country code + */ function getShippingCost(countryCode) { switch (countryCode) { case 'BR': @@ -36,10 +41,16 @@ function getShippingCost(countryCode) { } } +/** + * Method that fetches the shipping options according to the country. + * This function in most of the cases is asynchronous, as it will request shipping options in the backend + * + * @param countryCode - country code + */ function getShippingOptions(countryCode?: string) { switch (countryCode) { case 'BR': { - return { + return Promise.resolve({ defaultSelectedOptionId: 'shipping-001', shippingOptions: [ { @@ -53,10 +64,10 @@ function getShippingOptions(countryCode?: string) { description: 'Standard shipping delivered in 2 business days.' } ] - }; + }); } default: { - return { + return Promise.resolve({ defaultSelectedOptionId: 'shipping-001', shippingOptions: [ { @@ -75,7 +86,7 @@ function getShippingOptions(countryCode?: string) { description: 'Express shipping delivered in 1 business day.' } ] - }; + }); } } } @@ -179,7 +190,7 @@ export const Express: Story = { paymentDataCallbacks: { onPaymentDataChanged(intermediatePaymentData) { - return new Promise(resolve => { + return new Promise(async resolve => { const { callbackTrigger, shippingAddress, shippingOptionData } = intermediatePaymentData; const paymentDataRequestUpdate: google.payments.api.PaymentDataRequestUpdate = {}; @@ -193,7 +204,7 @@ export const Express: Story = { } /** If it initializes or changes the shipping address, we calculate the shipping options and transaction info */ if (callbackTrigger === 'INITIALIZE' || callbackTrigger === 'SHIPPING_ADDRESS') { - paymentDataRequestUpdate.newShippingOptionParameters = getShippingOptions(shippingAddress.countryCode); + paymentDataRequestUpdate.newShippingOptionParameters = await getShippingOptions(shippingAddress.countryCode); const selectedShippingOptionId = paymentDataRequestUpdate.newShippingOptionParameters.defaultSelectedOptionId; paymentDataRequestUpdate.newTransactionInfo = calculateNewTransactionInfo( shippingAddress.countryCode, diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json index fe3974b800..059bd0e97b 100644 --- a/packages/lib/tsconfig.json +++ b/packages/lib/tsconfig.json @@ -22,5 +22,5 @@ "baseUrl": "./" }, "include": ["src/"], - "exclude": ["node_modules", "**/*.test.*"] + "exclude": ["node_modules", "**/*.spec.*", "**/*.test.*"] } diff --git a/packages/playground/src/config/commonConfig.js b/packages/playground/src/config/commonConfig.js index 8d452b0d51..322773934b 100644 --- a/packages/playground/src/config/commonConfig.js +++ b/packages/playground/src/config/commonConfig.js @@ -18,7 +18,7 @@ export const amount = { export const useSession = urlParams.session !== 'manual'; -export const returnUrl = 'http://localhost:3020/result'; +export const returnUrl = `${window.location.protocol}//localhost:3020/result`; export default { amount, diff --git a/packages/playground/src/pages/Cards/Cards.js b/packages/playground/src/pages/Cards/Cards.js index dca46e6939..28aaea7026 100644 --- a/packages/playground/src/pages/Cards/Cards.js +++ b/packages/playground/src/pages/Cards/Cards.js @@ -188,7 +188,7 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsResponse = mcSrcClientId: '6d41d4d6-45b1-42c3-a5d0-a28c0e69d4b1' }, clickToPayConfiguration: { - shopperEmail: 'shopper@example.com', + shopperEmail: 'shopper@adyen.com', merchantDisplayName: 'Adyen Merchant Name', onReady: () => { console.log('Component is ready to be used'); diff --git a/packages/playground/src/pages/Dropin/manual.js b/packages/playground/src/pages/Dropin/manual.js index e9e5ecbf10..974efbca99 100644 --- a/packages/playground/src/pages/Dropin/manual.js +++ b/packages/playground/src/pages/Dropin/manual.js @@ -1,7 +1,7 @@ import AdyenCheckout from '@adyen/adyen-web'; import '@adyen/adyen-web/dist/es/adyen.css'; import { getPaymentMethods, makePayment, checkBalance, createOrder, cancelOrder, makeDetailsCall } from '../../services'; -import { amount, shopperLocale, countryCode, returnUrl } from '../../config/commonConfig'; +import { amount, shopperLocale, countryCode } from '../../config/commonConfig'; import { getSearchParameters } from '../../utils'; export async function initManual() { diff --git a/yarn.lock b/yarn.lock index 4a4cb0a1e8..04e3eb1f67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7,7 +7,7 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe/css-tools@^4.0.1": +"@adobe/css-tools@^4.3.1": version "4.3.1" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== @@ -2638,7 +2638,7 @@ core-js-pure "^3.30.2" regenerator-runtime "^0.14.0" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== @@ -2652,6 +2652,13 @@ dependencies: regenerator-runtime "^0.13.11" +"@babel/runtime@^7.23.2": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e" + integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": version "7.21.9" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb" @@ -3685,13 +3692,12 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@playwright/test@1.30.0": - version "1.30.0" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.30.0.tgz#8c0c4930ff2c7be7b3ec3fd434b2a3b4465ed7cb" - integrity sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw== +"@playwright/test@1.39.0": + version "1.39.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.39.0.tgz#d10ba8e38e44104499e25001945f07faa9fa91cd" + integrity sha512-3u1iFqgzl7zr004bGPYiN/5EZpRUSFddQBra8Rqll5N0/vfpqlP9I9EXqAoGacuAbX6c9Ulg/Cjqglp5VkK6UQ== dependencies: - "@types/node" "*" - playwright-core "1.30.0" + playwright "1.39.0" "@preact/preset-vite@^2.0.0": version "2.5.0" @@ -4722,14 +4728,13 @@ lz-string "^1.4.4" pretty-format "^27.0.2" -"@testing-library/jest-dom@5.17.0": - version "5.17.0" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz#5e97c8f9a15ccf4656da00fecab505728de81e0c" - integrity sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg== +"@testing-library/jest-dom@6.1.4": + version "6.1.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz#cf0835c33bc5ef00befb9e672b1e3e6a710e30e3" + integrity sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw== dependencies: - "@adobe/css-tools" "^4.0.1" + "@adobe/css-tools" "^4.3.1" "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.9.1" aria-query "^5.0.0" chalk "^3.0.0" css.escape "^1.5.1" @@ -4989,18 +4994,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@*": - version "29.5.1" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.1.tgz#83c818aa9a87da27d6da85d3378e5a34d2f31a47" - integrity sha512-tEuVcHrpaixS36w7hpsfLBLpjtMRJUE09/MHXn923LOVojDwyC14cWcfc0rDs0VEfUyYmt/+iX1kxxp+gZMcaQ== - dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" - -"@types/jest@29.5.7": - version "29.5.7" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.7.tgz#2c0dafe2715dd958a455bc10e2ec3e1ec47b5036" - integrity sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g== +"@types/jest@29.5.10": + version "29.5.10" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.10.tgz#a10fc5bab9e426081c12b2ef73d24d4f0c9b7f50" + integrity sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -5181,13 +5178,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/testing-library__jest-dom@^5.9.1": - version "5.14.6" - resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.6.tgz#4887f6e1af11215428ab02777873bcede98a53b0" - integrity sha512-FkHXCb+ikSoUP4Y4rOslzTdX5sqYwMxfefKh1GmZ8ce1GOkEHntSp6b5cGadmNfp5e4BMEWOMx+WSKd5/MqlDA== - dependencies: - "@types/jest" "*" - "@types/tough-cookie@*": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397" @@ -5790,13 +5780,20 @@ aria-hidden@^1.1.1: dependencies: tslib "^2.0.0" -aria-query@^5.0.0, aria-query@^5.1.3: +aria-query@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: deep-equal "^2.0.5" +aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + array-buffer-byte-length@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" @@ -5891,7 +5888,7 @@ array.prototype.flat@^1.2.3: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" -array.prototype.flat@^1.3.2: +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== @@ -5989,10 +5986,10 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag== +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== ast-types@0.15.2: version "0.15.2" @@ -6077,22 +6074,22 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== + axe-core@^4.2.0: version "4.7.2" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0" integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g== -axe-core@^4.6.2: - version "4.7.1" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.1.tgz#04392c9ccb3d7d7c5d2f8684f148d56d3442f33d" - integrity sha512-sCXXUhA+cljomZ3ZAwb8i1p3oOlkABzPy08ZDAoGcYuvtBPlQ1Ytde129ArXyHWDhfeewq7rlx9F+cUx2SSlkg== - -axobject-query@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" - integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== dependencies: - deep-equal "^2.0.5" + dequal "^2.0.3" babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" @@ -7641,7 +7638,7 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== -dequal@^2.0.2: +dequal@^2.0.2, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== @@ -8172,7 +8169,7 @@ es-get-iterator@^1.1.3: isarray "^2.0.5" stop-iteration-iterator "^1.0.0" -es-iterator-helpers@^1.0.12: +es-iterator-helpers@^1.0.12, es-iterator-helpers@^1.0.15: version "1.0.15" resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz#bd81d275ac766431d19305923707c3efd9f1ae40" integrity sha512-GhoY8uYqd6iwUl2kgjTm4CZAf6oo5mHK7BPqx3rKgx893YSsy0LGHV6gfqqQvZt/8xM8xeOnfXBCfqclMKkJ5g== @@ -8354,26 +8351,26 @@ eslint-plugin-import@^2.26.0: tsconfig-paths "^3.14.2" eslint-plugin-jsx-a11y@^6.6.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976" - integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA== + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== dependencies: - "@babel/runtime" "^7.20.7" - aria-query "^5.1.3" - array-includes "^3.1.6" - array.prototype.flatmap "^1.3.1" - ast-types-flow "^0.0.7" - axe-core "^4.6.2" - axobject-query "^3.1.1" + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" damerau-levenshtein "^1.0.8" emoji-regex "^9.2.2" - has "^1.0.3" - jsx-ast-utils "^3.3.3" - language-tags "=1.0.5" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" minimatch "^3.1.2" - object.entries "^1.1.6" - object.fromentries "^2.0.6" - semver "^6.3.0" + object.entries "^1.1.7" + object.fromentries "^2.0.7" eslint-plugin-react@^7.31.8: version "7.33.2" @@ -8407,10 +8404,10 @@ eslint-plugin-storybook@^0.6.13: requireindex "^1.1.0" ts-dedent "^2.2.0" -eslint-plugin-testing-library@^5.9.1: - version "5.11.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.11.1.tgz#5b46cdae96d4a78918711c0b4792f90088e62d20" - integrity sha512-5eX9e1Kc2PqVRed3taaLnAAqPZGEX75C+M/rXzUAI3wIg/ZxzUm1OVAwfe/O+vE+6YXOLetSe9g5GKD2ecXipw== +eslint-plugin-testing-library@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.0.tgz#af3340b783c881eb19ec5ac6b3a4bfe8ab4a1f74" + integrity sha512-+LCYJU81WF2yQ+Xu4A135CgK8IszcFcyMF4sWkbiu6Oj+Nel0TrkZq/HvDw0/1WuO3dhDQsZA/OpEMGd0NfcUw== dependencies: "@typescript-eslint/utils" "^5.58.0" @@ -9167,7 +9164,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -11252,7 +11249,7 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.3: +"jsx-ast-utils@^2.4.1 || ^3.0.0": version "3.3.3" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea" integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw== @@ -11260,6 +11257,16 @@ jsprim@^1.2.2: array-includes "^3.1.5" object.assign "^4.1.3" +jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -11314,17 +11321,17 @@ kolorist@^1.2.10: resolved "https://registry.yarnpkg.com/kolorist/-/kolorist-1.8.0.tgz#edddbbbc7894bc13302cdf740af6374d4a04743c" integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ== -language-subtag-registry@~0.3.2: +language-subtag-registry@^0.3.20: version "0.3.22" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== -language-tags@=1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ== +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== dependencies: - language-subtag-registry "~0.3.2" + language-subtag-registry "^0.3.20" launch-editor@^2.6.0: version "2.6.0" @@ -12198,6 +12205,15 @@ object.entries@^1.1.1, object.entries@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +object.entries@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.7.tgz#2b47760e2a2e3a752f39dd874655c61a7f03c131" + integrity sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + object.fromentries@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" @@ -12723,10 +12739,19 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -playwright-core@1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.30.0.tgz#de987cea2e86669e3b85732d230c277771873285" - integrity sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g== +playwright-core@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.39.0.tgz#efeaea754af4fb170d11845b8da30b2323287c63" + integrity sha512-+k4pdZgs1qiM+OUkSjx96YiKsXsmb59evFoqv8SKO067qBA+Z2s/dCzJij/ZhdQcs2zlTAgRKfeiiLm8PQ2qvw== + +playwright@1.39.0: + version "1.39.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.39.0.tgz#184c81cd6478f8da28bcd9e60e94fcebf566e077" + integrity sha512-naE5QT11uC/Oiq0BwZ50gDmy8c8WLPRTEWuSSFVG2egBka/1qMoSqYQcROMT9zLwJ86oPofcTH2jBY/5wWOgIw== + dependencies: + playwright-core "1.39.0" + optionalDependencies: + fsevents "2.3.2" pngjs@^3.3.1: version "3.4.0"