Skip to content

Commit

Permalink
Implementing handling an optional expiry date as determined by binLoo…
Browse files Browse the repository at this point in the history
…kup (#1328)

* Implementing handling an optional expiry date as determined by binLookup

* Add 'korean_local_card' brand to playground KCP example

* Updating securedFields playground file to work with v5

* Refactor how we detect single date field and clear error (if optional or hidden)

* Fixed tests in customcard.unsupportedCard.test.js

* Removed unused imports from test

* Fix so that ClientFunction runs for test (removed ref to DOM element)

* Fixed bug where card could not become valid if dateField eas optional but had an error due to an isValidate call

* Also clear isValidated errors for optional/hidden separate date fields (custom card scenario)

* Add classes for ExpirationDate component in same format as for CVC

* Create e2e Page Model for testing Card Component

* Added first e2e tests (using Page Model) for optional expiryDate

* Another test added

* Improved CardPage.getFromWindow fn

* Improved routine to remove DOM elements from state.errors

* Added assertion timeouts to 3DS2 tests to allow flexibility in how long it takes for challenge window to load

* Slowed down 3rd expiryDate test more so that it works better when part of a batch of tests (testcafe bug)

* Mock response fny made generic (part of BasePage) for more easy reuse

* Added tests for expiryDatePolicy = 'hidden'

* Create type for window.mockBinCount so that e2e tests have a way to know/turn off SDK bin mocking (in case it's accidentally been left on)

* Added functionality for e2e tests to know/turn off SDK bin mocking (in case it's accidentally been left on)

* Added line to 'hidden' test

* Moved checkMocking.test so that it is always the first run

* Adding clientScripts to fixture last seems to allow onChange event to always fire (was sometimes failing when entire test suite was run)

* Adding code to custom card playground & e2e files to handle 'optional' date & cvc policies

* Removed id's from playground & e2e custom card examples. Allowed separate date field example to support dual branding

* Added CustomCard Page model & tests for optional expiryDate on the "regular" custom card

* Aligning class names for the "separate" custom card

* Added new utils for Card pages

* Added tests for optional expiryDate on the "separate" custom card

* Added utils for retrieving iframe input & aria error fields

* Convert customcard.unsupportedCard.test.js to use Custom card Page model (& stripped repeated test code)

* onChange function for Cards that maps state.errors to remove DOM nodes - moved to Cards.js (it's just not always picked up from the expiryDate.clientScript.js for some reason)

* Comments added

* Added assertion in e2e test for undefined object

* removing unused code

* Replaced zeroPad fn with String.padStart

* Mock related functionality moved out from BasePage to a mocks util for the Card related pages & tests
  • Loading branch information
sponglord authored Oct 29, 2021
1 parent 8d182ab commit 2b20d30
Show file tree
Hide file tree
Showing 55 changed files with 1,823 additions and 403 deletions.
19 changes: 18 additions & 1 deletion packages/e2e/app/src/pages/Cards/Cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,24 @@ const initCheckout = async () => {
window.card = checkout
.create('card', {
brands: ['mc', 'visa', 'amex', 'maestro', 'bcmc'],
onChange: state => console.log(state),
onChange: state => {
/**
* Needed now that, for v5, we enhance the securedFields state.errors object with a rootNode prop
* - Testcafe doesn't like a ClientFunction retrieving an object with a DOM node in it!?
*
* AND, for some reason, if you place this onChange function in expiryDate.clientScripts.js it doesn't always get read.
* It'll work when it's part of a small batch but if part of the full test suite it gets ignored - so the tests that rely on
* window.mappedStateErrors fail
*/
if (!!Object.keys(state.errors).length) {
// Replace any rootNode values in the objects in state.errors with an empty string
const nuErrors = Object.entries(state.errors).reduce((acc, [fieldType, error]) => {
acc[fieldType] = error ? { ...error, rootNode: '' } : error;
return acc;
}, {});
window.mappedStateErrors = nuErrors;
}
},
...window.cardConfig
})
.mount('.card-field');
Expand Down
28 changes: 20 additions & 8 deletions packages/e2e/app/src/pages/CustomCards/CustomCards.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@ <h3 class="card-text sf-text">CustomCard #1</h3>
<div class="merchant-checkout__payment-method__details secured-fields" >
<span class="pm-image">
<img
id="pmImage"
class="pm-image-1"
width="40"
src="https://checkoutshopper-test.adyen.com/checkoutshopper/images/logos/nocard.svg"
alt="card"
/>
</span>
<span class="pm-image-dual">
<img
id="pmImageDual1"
class="pm-image-dual-1"
width="40"
alt=""
/>
<img
id="pmImageDual2"
class="pm-image-dual-2"
width="40"
alt=""
/>
</span>
<label class="pm-form-label">
<label class="pm-form-label pm-form-label-pan">
<span class="pm-form-label__text">Card number:</span>
<span class="pm-input-field" data-cse="encryptedCardNumber"></span>
<span class="pm-form-label__error-text">Please enter a valid credit card number</span>
Expand Down Expand Up @@ -67,23 +67,35 @@ <h3 class="card-text sf-text">CustomCard #2</h3>
<div class="merchant-checkout__payment-method__details secured-fields-2">
<span class="pm-image">
<img
id="pmImage2"
class="pm-image-1"
width="40"
src="https://checkoutshopper-test.adyen.com/checkoutshopper/images/logos/nocard.svg"
alt=""
/>
</span>
<label class="pm-form-label">
<span class="pm-image-dual">
<img
class="pm-image-dual-1"
width="40"
alt=""
/>
<img
class="pm-image-dual-2"
width="40"
alt=""
/>
</span>
<label class="pm-form-label pm-form-label-pan">
<span class="pm-form-label__text">Card number:</span>
<span class="pm-input-field" data-cse="encryptedCardNumber"></span>
<span class="pm-form-label__error-text">Please enter a valid credit card number</span>
</label>
<label class="pm-form-label exp-month">
<label class="pm-form-label pm-form-label--exp-month">
<span class="pm-form-label__text">Expiry month:</span>
<span class="pm-input-field" data-cse="encryptedExpiryMonth"></span>
<span class="pm-form-label__error-text">Date error text</span>
</label>
<label class="pm-form-label exp-year">
<label class="pm-form-label pm-form-label--exp-year">
<span class="pm-form-label__text">Expiry year:</span>
<span class="pm-input-field" data-cse="encryptedExpiryYear"></span>
<span class="pm-form-label__error-text">Date error text</span>
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/app/src/pages/CustomCards/CustomCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ const initCheckout = async () => {
.create('securedfields', {
type: 'card',
brands: ['mc', 'visa', 'amex', 'bcmc', 'maestro', 'cartebancaire'],
onConfigSuccess,
onBrand,
onFocus: setFocus,
onBinLookup,
onChange,
...window.cardConfig
})
Expand Down
75 changes: 56 additions & 19 deletions packages/e2e/app/src/pages/CustomCards/customCards.config.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
let hideCVC = false;
let optionalCVC = false;
let hideDate = false;
let optionalDate = false;
let isDualBranding = false;

function setAttributes(el, attrs) {
for (var key in attrs) {
for (const key in attrs) {
el.setAttribute(key, attrs[key]);
}
}
Expand Down Expand Up @@ -42,17 +44,13 @@ export function onConfigSuccess(pCallbackObj) {
}

export function setCCErrors(pCallbackObj) {
if (pCallbackObj.error === 'originKeyError') {
document.querySelector('.card-input__spinner__holder').style.display = 'none';
pCallbackObj.rootNode.style.display = 'block';
return;
}

if (!pCallbackObj.rootNode) return;

const sfNode = pCallbackObj.rootNode.querySelector(`[data-cse="${pCallbackObj.fieldType}"]`);
const errorNode = sfNode.parentNode.querySelector('.pm-form-label__error-text');

if (errorNode.innerText === '' && pCallbackObj.error === '') return;

if (pCallbackObj.error !== '') {
errorNode.style.display = 'block';
errorNode.innerText = pCallbackObj.errorI18n;
Expand Down Expand Up @@ -102,26 +100,52 @@ export function onBrand(pCallbackObj) {
cvcNode.style.display = 'block';
}

// Optional cvc fields
if (pCallbackObj.cvcPolicy === 'optional' && !optionalCVC) {
optionalCVC = true;
if (cvcNode) cvcNode.querySelector('.pm-form-label__text').innerText = 'CVV/CVC (optional):';
}

if (optionalCVC && pCallbackObj.cvcPolicy !== 'optional') {
optionalCVC = false;
if (cvcNode) cvcNode.querySelector('.pm-form-label__text').innerText = 'CVV/CVC:';
}

/**
* Deal with showing/hiding date field(s)
*/
const dateNode = pCallbackObj.rootNode.querySelector('.pm-form-label--exp-date');
const monthNode = pCallbackObj.rootNode.querySelector('.pm-form-label.exp-month');
const yearNode = pCallbackObj.rootNode.querySelector('.pm-form-label.exp-year');
const monthNode = pCallbackObj.rootNode.querySelector('.pm-form-label--exp-month');
const yearNode = pCallbackObj.rootNode.querySelector('.pm-form-label--exp-year');

if (pCallbackObj.datePolicy === 'hidden' && !hideDate) {
if (pCallbackObj.expiryDatePolicy === 'hidden' && !hideDate) {
hideDate = true;
if (dateNode) dateNode.style.display = 'none';
if (monthNode) monthNode.style.display = 'none';
if (yearNode) yearNode.style.display = 'none';
}

if (hideDate && pCallbackObj.datePolicy !== 'hidden') {
if (hideDate && pCallbackObj.expiryDatePolicy !== 'hidden') {
hideDate = false;
if (dateNode) dateNode.style.display = 'block';
if (monthNode) monthNode.style.display = 'block';
if (yearNode) yearNode.style.display = 'block';
}

// Optional date fields
if (pCallbackObj.expiryDatePolicy === 'optional' && !optionalDate) {
optionalDate = true;
if (dateNode) dateNode.querySelector('.pm-form-label__text').innerText = 'Expiry date (optional):';
if (monthNode) monthNode.querySelector('.pm-form-label__text').innerText = 'Expiry month (optional):';
if (yearNode) yearNode.querySelector('.pm-form-label__text').innerText = 'Expiry year (optional):';
}

if (optionalDate && pCallbackObj.expiryDatePolicy !== 'optional') {
optionalDate = false;
if (dateNode) dateNode.querySelector('.pm-form-label__text').innerText = 'Expiry date:';
if (monthNode) monthNode.querySelector('.pm-form-label__text').innerText = 'Expiry month:';
if (yearNode) yearNode.querySelector('.pm-form-label__text').innerText = 'Expiry year:';
}
}

function dualBrandListener(e) {
Expand All @@ -133,19 +157,19 @@ function resetDualBranding(rootNode) {

setLogosActive(rootNode);

const brandLogo1 = rootNode.querySelector('#pmImageDual1');
const brandLogo1 = rootNode.querySelector('.pm-image-dual-1');
brandLogo1.removeEventListener('click', dualBrandListener);

const brandLogo2 = rootNode.querySelector('#pmImageDual2');
const brandLogo2 = rootNode.querySelector('.pm-image-dual-2');
brandLogo2.removeEventListener('click', dualBrandListener);
}

/**
* Implementing dual branding
*/
function onDualBrand(pCallbackObj) {
const brandLogo1 = pCallbackObj.rootNode.querySelector('#pmImageDual1');
const brandLogo2 = pCallbackObj.rootNode.querySelector('#pmImageDual2');
const brandLogo1 = pCallbackObj.rootNode.querySelector('.pm-image-dual-1');
const brandLogo2 = pCallbackObj.rootNode.querySelector('.pm-image-dual-2');

isDualBranding = true;

Expand Down Expand Up @@ -189,13 +213,12 @@ export function onBinLookup(pCallbackObj) {
}

export function onChange(state, component) {
// From v5 the onError handler is no longer only for card comp related errors - so watch state.errors and call the card specific setCCErrors based on this
if (!!Object.keys(state.errors).length) {
const errors = Object.entries(state.errors).map(([fieldType, error]) => {
return {
fieldType,
error: !!error ? error.toString() : '',
rootNode: component._node,
errorI18n: component.props.i18n.translations[error]
...(error ? error : { error: '', rootNode: component._node })
};
});
errors.forEach(setCCErrors);
Expand All @@ -207,7 +230,21 @@ export function onChange(state, component) {
*/
if (isDualBranding) {
const mode = state.valid.encryptedCardNumber ? 'dualBranding_valid' : 'dualBranding_notValid';
setLogosActive(document.querySelector('.secured-fields'), mode);
setLogosActive(component._node, mode);
}

/**
* For running the e2e tests in testcafe - we need a mapped version of state.errors
* since, for v5, we enhance the securedFields state.errors object with a rootNode prop
* & Testcafe doesn't like a ClientFunction retrieving an object with a DOM node in it!?
*/
if (!!Object.keys(state.errors).length) {
// Replace any rootNode values in the objects in state.errors with an empty string
const nuErrors = Object.entries(state.errors).reduce((acc, [fieldType, error]) => {
acc[fieldType] = error ? { ...error, rootNode: '' } : error;
return acc;
}, {});
window.mappedStateErrors = nuErrors;
}
}

Expand Down
44 changes: 44 additions & 0 deletions packages/e2e/tests/_common/cardMocks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { ClientFunction, RequestMock } from 'testcafe';
import { BASE_URL } from '../pages';

import path from 'path';
require('dotenv').config({ path: path.resolve('../../', '.env') });

export const binLookupUrl = `https://checkoutshopper-test.adyen.com/checkoutshopper/v2/bin/binLookup?token=${process.env.CLIENT_KEY}`;

/**
* Functionality for mocking a /binLookup API response via testcafe's fixture.requestHooks()
*
* @param requestURL
* @param mockedResponse
* @returns {RequestMock}
*/
export const getBinLookupMock = (requestURL, mockedResponse) => {
return RequestMock()
.onRequestTo(request => {
return request.url === requestURL && request.method === 'post';
})
.respond(
(req, res) => {
const body = JSON.parse(req.body);
mockedResponse.requestId = body.requestId;
res.setBody(mockedResponse);
},
200,
{
'Access-Control-Allow-Origin': BASE_URL
}
);
};

// For the tests as a whole - throw an error if SDK binLookup mocking is turned on
export const checkSDKMocking = ClientFunction(() => {
if (window.mockBinCount > 0) {
throw new Error('SDK bin mocking is turned on - this will affect/break the tests - so turn it off in triggerBinLookup.ts');
}
});

// For individual test suites (perhaps being run in isolation) - provide a way to ensure SDK bin mocking is turned off
export const turnOffSDKMocking = ClientFunction(() => {
window.mockBinCount = 0;
});
14 changes: 14 additions & 0 deletions packages/e2e/tests/_common/checkMocking.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { checkSDKMocking } from './cardMocks';
import CardComponentPage from '../_models/CardComponent.page';

const cardPage = new CardComponentPage();

fixture`Test that bin mocking isn't turned on in the SDK`.page(cardPage.pageUrl);

/**
* Check that bin mocking isn't turned on in the SDK
* - this is used for testing (in triggerBinLookup.ts) but if left on will break or skew the tests
*/
test('Check for SDK Bin mocking', async () => {
await checkSDKMocking();
});
25 changes: 25 additions & 0 deletions packages/e2e/tests/_models/BasePage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BASE_URL } from '../pages';
import { ClientFunction } from 'testcafe';

export default class BasePage {
constructor(url) {
this.pageUrl = `${BASE_URL}/${url}`;
}

/**
* Client function that accesses properties on the window object
*/
getFromWindow = ClientFunction(path => {
const splitPath = path.split('.');
const reducer = (xs, x) => (xs && xs[x] !== undefined ? xs[x] : undefined);

return splitPath.reduce(reducer, window);
});

/**
* Hack to force testcafe to fire expected blur events as (securedFields) switch focus
*/
setForceClick = ClientFunction(val => {
window.testCafeForceClick = val;
});
}
Loading

0 comments on commit 2b20d30

Please sign in to comment.