From cf628f5c5b60030b4ad63ba39e31c51f568f8efe Mon Sep 17 00:00:00 2001 From: Chris Barton Date: Mon, 30 Sep 2024 10:21:30 -0700 Subject: [PATCH 1/2] feat: Add support for tokenizing the CVV standalone Allows the `CardCvvElement` to tokenize the CVV by itself for use in CIT where the merchant wants the customer to confirm their CVV before checking out: ```js const cvvElement = recurly.elements.CardCvvElement({}); cvvElement.attach(document.querySelector('#recurly-elements')); ``` --- lib/recurly.js | 6 +- lib/recurly/element/card-cvv.js | 1 + lib/recurly/elements.js | 3 +- lib/recurly/hosted-fields.js | 6 +- lib/recurly/token.js | 18 +- lib/recurly/validate.js | 26 ++- .../fixtures/field.html.ejs | 11 +- .../views/e2e/hosted-fields-cvv.html.ejs | 4 + test/e2e/display.test.js | 3 +- test/e2e/implementation.state.test.js | 100 +++++----- test/e2e/recurly.test.js | 12 ++ test/e2e/support/helpers.js | 80 +++++--- test/unit/configure.test.js | 10 +- test/unit/elements.test.js | 3 +- test/unit/support/fixtures.js | 2 +- test/unit/token.test.js | 175 +++++++++++------- wdio.ci.conf.js | 2 +- 17 files changed, 283 insertions(+), 179 deletions(-) create mode 100644 packages/public-api-fixture-server/views/e2e/hosted-fields-cvv.html.ejs diff --git a/lib/recurly.js b/lib/recurly.js index 19181a9c0..b8c56aff8 100644 --- a/lib/recurly.js +++ b/lib/recurly.js @@ -5,6 +5,7 @@ import deepAssign from './util/deep-assign'; import deepFilter from 'deep-filter'; import Emitter from 'component-emitter'; import pick from 'lodash.pick'; +import uniq from 'array-unique'; import uid from './util/uid'; import errors from './recurly/errors'; import { bankAccount } from './recurly/bank-account'; @@ -79,6 +80,7 @@ const DEFAULTS = { } }, api: DEFAULT_API_URL, + required: ['number', 'month', 'year', 'first_name', 'last_name'], fields: { all: { style: {} @@ -286,7 +288,9 @@ export class Recurly extends Emitter { deepAssign(this.config.fields, options.fields); } - this.config.required = options.required || this.config.required || []; + if (Array.isArray(options.required)) { + this.config.required = uniq([...this.config.required, ...options.required]); + } // Begin parent role configuration and setup if (this.config.parent) { diff --git a/lib/recurly/element/card-cvv.js b/lib/recurly/element/card-cvv.js index 6dde05a10..cdc4002f7 100644 --- a/lib/recurly/element/card-cvv.js +++ b/lib/recurly/element/card-cvv.js @@ -7,4 +7,5 @@ export function factory (options) { export class CardCvvElement extends Element { static type = 'cvv'; static elementClassName = 'CardCvvElement'; + static supportsTokenization = true; } diff --git a/lib/recurly/elements.js b/lib/recurly/elements.js index beb307284..a2778c6b6 100644 --- a/lib/recurly/elements.js +++ b/lib/recurly/elements.js @@ -32,7 +32,8 @@ export default class Elements extends Emitter { static VALID_SETS = [ [ CardElement ], - [ CardNumberElement, CardMonthElement, CardYearElement, CardCvvElement ] + [ CardCvvElement ], + [ CardNumberElement, CardMonthElement, CardYearElement, CardCvvElement ], ]; constructor ({ recurly }) { diff --git a/lib/recurly/hosted-fields.js b/lib/recurly/hosted-fields.js index b57d28aed..4f46ab81a 100644 --- a/lib/recurly/hosted-fields.js +++ b/lib/recurly/hosted-fields.js @@ -88,10 +88,12 @@ export class HostedFields extends Emitter { } }); - // If we have a card hosted field, clear all missing target errors. + // If we have a card/cvv hosted field, clear all missing target errors. const cardFieldMissingErrorPresent = this.errors.some(e => e.type === 'card'); - if (cardFieldMissingErrorPresent) { + const onlyCvvFieldPresent = this.fields.length === 1 && this.fields[0].type === 'cvv'; + if (cardFieldMissingErrorPresent && !onlyCvvFieldPresent) { // If we are only missing the card field, clear the error + // If we only have a cvv field, clear the errors const missingFieldErrors = this.errors.filter(e => e.name === 'missing-hosted-field-target'); if (missingFieldErrors.length === 1) { this.errors = this.errors.filter(e => !(e.name === 'missing-hosted-field-target' && e.type === 'card')); diff --git a/lib/recurly/token.js b/lib/recurly/token.js index 6357c60b8..6c1fa333e 100644 --- a/lib/recurly/token.js +++ b/lib/recurly/token.js @@ -173,13 +173,17 @@ function token (customerData, bus, done) { } const { number, month, year, cvv } = inputs; - Risk.preflight({ recurly: this, number, month, year, cvv }) - .then(({ risk, tokenType }) => { - inputs.risk = risk; - if (tokenType) inputs.type = tokenType; - }) - .then(() => this.request.post({ route: '/token', data: inputs, done: complete })) - .done(); + if (number && month && year) { + Risk.preflight({ recurly: this, number, month, year, cvv }) + .then(({ risk, tokenType }) => { + inputs.risk = risk; + if (tokenType) inputs.type = tokenType; + }) + .then(() => this.request.post({ route: '/token', data: inputs, done: complete })) + .done(); + } else { + this.request.post({ route: '/token', data: inputs, done: complete }); + } } function complete (err, res) { diff --git a/lib/recurly/validate.js b/lib/recurly/validate.js index 3c35ac0fc..4b713b763 100644 --- a/lib/recurly/validate.js +++ b/lib/recurly/validate.js @@ -1,6 +1,6 @@ /*jshint -W058 */ -import { FIELDS as CARD_FIELDS } from './token'; +import { FIELDS as ADDRESS_FIELDS } from './token'; import each from 'component-each'; import find from 'component-find'; import { parseCard } from '../util/parse-card'; @@ -8,6 +8,14 @@ import CREDIT_CARD_TYPES from '../const/credit-card-types.json'; const debug = require('debug')('recurly:validate'); +const CARD_FIELDS = [ + ...ADDRESS_FIELDS, + 'number', + 'month', + 'year', + 'cvv', +]; + /** * Validation error messages * @type {String} @@ -197,25 +205,15 @@ export function validateCardInputs (recurly, inputs) { const format = formatFieldValidationError; let errors = []; - if (!cardNumber(inputs.number)) { + if (inputs.number && !cardNumber(inputs.number)) { errors.push(format('number', INVALID)); } - if (!expiry(inputs.month, inputs.year)) { + if (inputs.month && inputs.year && !expiry(inputs.month, inputs.year)) { errors.push(format('month', INVALID), format('year', INVALID)); } - if (!inputs.first_name) { - errors.push(format('first_name', BLANK)); - } - - if (!inputs.last_name) { - errors.push(format('last_name', BLANK)); - } - - if (~recurly.config.required.indexOf('cvv') && !inputs.cvv) { - errors.push(format('cvv', BLANK)); - } else if ((~recurly.config.required.indexOf('cvv') || inputs.cvv) && !cvv(inputs.cvv)) { + if (inputs.cvv && !cvv(inputs.cvv)) { errors.push(format('cvv', INVALID)); } diff --git a/packages/public-api-fixture-server/fixtures/field.html.ejs b/packages/public-api-fixture-server/fixtures/field.html.ejs index 1309f9e61..be99f92d4 100644 --- a/packages/public-api-fixture-server/fixtures/field.html.ejs +++ b/packages/public-api-fixture-server/fixtures/field.html.ejs @@ -19,9 +19,13 @@ } // Stub broker behavior - if (config().type === 'number') { - window.addEventListener('message', receivePostMessage, false); + function setStubTokenizationElementName (name) { + window.stubTokenizationElementName = name; + if (config().type === name) { + window.addEventListener('message', receivePostMessage, false); + } } + setStubTokenizationElementName('number'); sendMessage(prefix + ':ready', { type: config().type }); @@ -39,6 +43,9 @@ function onToken (body) { var recurly = new parent.recurly.Recurly(getRecurlyConfig()); + if (stubTokenizationElementName === 'cvv') { + recurly.config.required = ['cvv']; + } var inputs = body.inputs; var id = body.id; diff --git a/packages/public-api-fixture-server/views/e2e/hosted-fields-cvv.html.ejs b/packages/public-api-fixture-server/views/e2e/hosted-fields-cvv.html.ejs new file mode 100644 index 000000000..f7144204d --- /dev/null +++ b/packages/public-api-fixture-server/views/e2e/hosted-fields-cvv.html.ejs @@ -0,0 +1,4 @@ +<%- include('_head'); -%> + +
+<%- include('_foot'); -%> diff --git a/test/e2e/display.test.js b/test/e2e/display.test.js index dc4fcfa65..bb2437f0f 100644 --- a/test/e2e/display.test.js +++ b/test/e2e/display.test.js @@ -7,6 +7,7 @@ const { environmentIs, fillCardElement, fillDistinctCardElements, + fillCvvElement, init } = require('./support/helpers'); @@ -34,7 +35,7 @@ maybeDescribe('Display', () => { it('matches distinct elements baseline', async function () { const { CardElement, ...distinctElements } = ELEMENT_TYPES; for (const element in distinctElements) { - await createElement(element, { style: { fontFamily: 'Pacifico' }}); + await createElement(element, { style: { fontFamily: 'Pacifico' } }); } await fillDistinctCardElements(); await clickFirstName(); diff --git a/test/e2e/implementation.state.test.js b/test/e2e/implementation.state.test.js index 07269cb18..8ec37998f 100644 --- a/test/e2e/implementation.state.test.js +++ b/test/e2e/implementation.state.test.js @@ -8,6 +8,8 @@ const { environmentIs, EXAMPLES, fillCardElement, + fillDistinctCardElements, + fillElement, init } = require('./support/helpers'); @@ -68,6 +70,17 @@ describe('Field State', elementAndFieldSuite({ ); }); }, + cvvElement: async () => { + it('displays field state on the page', async function () { + await setupElementsStateOutput(); + await assertInputStateChange(() => fillElement(0, '.recurly-hosted-field-input', '123'), 0, { + empty: false, + length: 3, + focus: false, + valid: true + }); + }); + }, cardHostedField: async function () { it('displays field state on the page', async function () { // Skip Electron due to element blur incompatibility @@ -115,6 +128,19 @@ describe('Field State', elementAndFieldSuite({ hostedFieldState({ number, month, year, cvv }) ); }); + }, + cvvHostedField: async () => { + it('displays field state on the page', async function () { + const cvv = { + empty: false, + length: 3, + focus: false, + valid: true, + }; + + await setupHostedFieldStateOutput(); + await assertInputStateChange(() => fillElement(0, '.recurly-hosted-field-input', '123'), 0, { fields: { cvv } }); + }); } })); @@ -141,6 +167,7 @@ async function setupHostedFieldStateOutput () { } async function assertCardBehavior ({ wrap = obj => obj } = {}) { + const FRAME = 0; const expect = { valid: false, firstSix: '', @@ -164,29 +191,9 @@ async function assertCardBehavior ({ wrap = obj => obj } = {}) { valid: false } }; + const expectation = (changes) => wrap(Object.assign({}, expect, changes)); - const firstName = await $(sel.firstName); - const output = await $(sel.output); - const actual = async () => JSON.parse(await output.getText()); - const assertStateOutputIs = async changes => { - assert.deepStrictEqual( - await actual(), - wrap(Object.assign({}, expect, changes)) - ); - }; - - // await browser.switchToFrame(0); - // const number = await $(sel.number); - // await number.setValue(EXAMPLES.NUMBER); - // await browser.waitUntil(async () => (await number.getValue()).length >= 19); - // await browser.switchToFrame(null); - await fillCardElement({ - expiry: '', - cvv: '' - }); - await firstName.click(); - - await assertStateOutputIs({ + await assertInputStateChange(() => fillCardElement({ expiry: '', cvv: '' }), FRAME, expectation({ firstSix: '411111', lastFour: '1111', brand: 'visa', @@ -197,14 +204,9 @@ async function assertCardBehavior ({ wrap = obj => obj } = {}) { focus: false, valid: true } - }); - - await fillCardElement({ - cvv: '' - }); - await firstName.click(); + })); - await assertStateOutputIs({ + await assertInputStateChange(() => fillCardElement({ cvv: '' }), FRAME, expectation({ firstSix: '411111', lastFour: '1111', brand: 'visa', @@ -220,15 +222,9 @@ async function assertCardBehavior ({ wrap = obj => obj } = {}) { focus: false, valid: true } - }); + })); - // await browser.switchToFrame(0); - // await (await $(sel.cvv)).addValue(EXAMPLES.CVV); - // await browser.switchToFrame(null); - await fillCardElement(); - await firstName.click(); - - await assertStateOutputIs({ + await assertInputStateChange(() => fillCardElement(), FRAME, expectation({ firstSix: '411111', lastFour: '1111', brand: 'visa', @@ -250,7 +246,7 @@ async function assertCardBehavior ({ wrap = obj => obj } = {}) { focus: false, valid: true } - }); + })); } async function assertDistinctCardBehavior (...expectations) { @@ -260,28 +256,24 @@ async function assertDistinctCardBehavior (...expectations) { '28', '123' ]; - const firstName = await $(sel.firstName); - const output = await $(sel.output); - const actual = async () => JSON.parse(await output.getText()); - const assertStateOutputIs = async expect => assert.deepStrictEqual( - await actual(), - expect - ); for (const entry of entries) { const i = entries.indexOf(entry); - await browser.switchToFrame(i); - const input = await $('.recurly-hosted-field-input'); - await input.addValue(entry); - if (environmentIs(BROWSERS.EDGE)) { - await browser.waitUntil(async () => (await input.getValue()).replace(/ /g, '') === entry); - } - await browser.switchToFrame(null); - await firstName.click(); - await assertStateOutputIs(expectations[i]); + await assertInputStateChange(() => fillElement(i, '.recurly-hosted-field-input', entry), i, expectations[i]); } } +async function assertInputStateChange(example, frame, expectation) { + const blurTriggerEl = await $(sel.firstName); + const output = await $(sel.output); + + await example(); + + await blurTriggerEl.click(); + const actual = JSON.parse(await output.getText()); + assert.deepStrictEqual(actual, expectation); +} + function hostedFieldState ({ number, month, year, cvv }) { return { fields: { diff --git a/test/e2e/recurly.test.js b/test/e2e/recurly.test.js index 0fa6a467c..835f962a4 100644 --- a/test/e2e/recurly.test.js +++ b/test/e2e/recurly.test.js @@ -3,6 +3,7 @@ const { assertIsAToken, EXAMPLES, getValue, + fillElement, init, recurlyEnvironment, tokenize @@ -100,6 +101,17 @@ describe('Recurly.js', async function () { assertIsAToken(tokenWith); }); }); + + describe('when using standalone cvv hosted field', async function () { + beforeEach(init({ fixture: 'hosted-fields-cvv' })); + + it('creates a token', async function () { + await fillElement(0, sel.hostedFieldInput, EXAMPLES.CVV); + const [err, token] = await tokenize(sel.form); + assert.strictEqual(err, null); + assertIsAToken(token); + }); + }); }); describe('Bacs bank account', async function () { diff --git a/test/e2e/support/helpers.js b/test/e2e/support/helpers.js index 3d86f842c..1c2ccedf1 100644 --- a/test/e2e/support/helpers.js +++ b/test/e2e/support/helpers.js @@ -22,7 +22,7 @@ const ELEMENT_TYPES = { CardNumberElement: 'CardNumberElement', CardMonthElement: 'CardMonthElement', CardYearElement: 'CardYearElement', - CardCvvElement: 'CardCvvElement' + CardCvvElement: 'CardCvvElement', }; const FIELD_TYPES = { @@ -76,6 +76,8 @@ module.exports = { FIELD_TYPES, fillCardElement, fillDistinctCardElements, + fillCvvElement, + fillElement, getValue, init, recurlyEnvironment, @@ -106,8 +108,10 @@ module.exports = { function elementAndFieldSuite ({ cardElement, distinctCardElements, + cvvElement, cardHostedField, distinctCardHostedFields, + cvvHostedField, any }) { return () => { @@ -132,6 +136,13 @@ function elementAndFieldSuite ({ }); maybeRun(distinctCardElements); }); + + describe('distinct CardCvvElement', function () { + beforeEach(async function () { + await createElement(ELEMENT_TYPES.CardCvvElement); + }); + maybeRun(cvvElement); + }); }); describe('when using a card Hosted Field', function () { @@ -143,6 +154,11 @@ function elementAndFieldSuite ({ beforeEach(init({ fixture: 'hosted-fields-card-distinct' })); maybeRun(distinctCardHostedFields); }); + + describe('when using a cvv Hosted Field', function () { + beforeEach(init({ fixture: 'hosted-fields-cvv' })); + maybeRun(cvvHostedField); + }); }; } @@ -207,30 +223,9 @@ async function fillCardElement ({ expiry = EXAMPLES.EXPIRY, cvv = EXAMPLES.CVV } = {}) { - await browser.switchToFrame(0); - - const numberInput = await $(SELECTORS.CARD_ELEMENT.NUMBER); - const expiryInput = await $(SELECTORS.CARD_ELEMENT.EXPIRY); - const cvvInput = await $(SELECTORS.CARD_ELEMENT.CVV); - - // setvalue's underlying elementSendKeys is slow to act on Android. Thus we chunk the input. - if (environmentIs(DEVICES.ANDROID)) { - await numberInput.clearValue(); - for (const chunk of number.match(/.{1,2}/g)) { - await numberInput.addValue(chunk); - } - } else { - await numberInput.setValue(number); - } - - if (environmentIs(BROWSERS.EDGE)) { - await browser.waitUntil(async () => (await numberInput.getValue()).replace(/ /g, '') === number); - } - - await expiryInput.setValue(expiry); - await cvvInput.setValue(cvv); - - await browser.switchToFrame(null); + await fillElement(0, SELECTORS.CARD_ELEMENT.NUMBER, number); + await fillElement(0, SELECTORS.CARD_ELEMENT.EXPIRY, expiry); + await fillElement(0, SELECTORS.CARD_ELEMENT.CVV, cvv); } /** @@ -250,13 +245,40 @@ async function fillDistinctCardElements ({ const examples = [number, month, year, cvv]; for (const example of examples) { const i = examples.indexOf(example); - await browser.switchToFrame(i); - const input = await $(SELECTORS.HOSTED_FIELD_INPUT); - await input.setValue(example); - await browser.switchToFrame(null); + await fillElement(i, SELECTORS.HOSTED_FIELD_INPUT, example); } } +/** + * Fills a cvv Hosted Field with the given value + * + * @param {String} options.cvv + */ +async function fillCvvElement ({ cvv = EXAMPLES.CVV } = {}) { + await fillElement(0, SELECTORS.HOSTED_FIELD_INPUT, cvv); +} + +async function fillElement (frame, selector, val) { + await browser.switchToFrame(frame); + const input = await $(selector); + + // setvalue's underlying elementSendKeys is slow to act on Android. Thus we chunk the input. + if (environmentIs(DEVICES.ANDROID)) { + await input.clearValue(); + for (const chunk of val.match(/.{1,2}/g)) { + await input.addValue(chunk); + } + } else { + await input.setValue(val); + + if (environmentIs(BROWSERS.EDGE)) { + await browser.waitUntil(async () => (await input.getValue()).replace(/ /g, '') === val); + } + } + + await browser.switchToFrame(null); +} + // Action helpers /** diff --git a/test/unit/configure.test.js b/test/unit/configure.test.js index f1fafdaa5..9ce495f19 100644 --- a/test/unit/configure.test.js +++ b/test/unit/configure.test.js @@ -51,8 +51,6 @@ describe('Recurly.configure', function () { { publicKey: 'test', currency: 'USD' }, { publicKey: 'test', currency: 'AUD', api }, { publicKey: 'test', currency: 'AUD', api, cors: true }, - { publicKey: 'test', currency: 'USD', api, required: ['country'] }, - { publicKey: 'test', currency: 'USD', api, required: ['postal_code', 'country'] } ]; }); @@ -108,6 +106,14 @@ describe('Recurly.configure', function () { }); }); + describe('when options.required is given', function () { + it('appends the given value to the defaults', function () { + const { recurly } = this; + recurly.configure({ publicKey: 'test', required: ['number', 'postal_code'] }); + assert.deepEqual(recurly.config.required, ['number', 'month', 'year', 'first_name', 'last_name', 'postal_code']); + }); + }); + describe('when options.style is given (deprecated)', function () { const { api } = this; const example = { diff --git a/test/unit/elements.test.js b/test/unit/elements.test.js index ab77cc5c4..ddc12cc98 100644 --- a/test/unit/elements.test.js +++ b/test/unit/elements.test.js @@ -2,7 +2,6 @@ import assert from 'assert'; import Element from '../../lib/recurly/element'; import Elements from '../../lib/recurly/elements'; import { initRecurly } from './support/helpers'; -import { Recurly } from '../../lib/recurly'; const noop = () => {}; @@ -26,7 +25,7 @@ describe('Elements', function () { 'CardNumberElement', 'CardMonthElement', 'CardYearElement', - 'CardCvvElement' + 'CardCvvElement', ].forEach(elementName => { const elements = new Elements({ recurly: this.recurly }); const element = elements[elementName](); diff --git a/test/unit/support/fixtures.js b/test/unit/support/fixtures.js index cceca4ebd..5bbe40296 100644 --- a/test/unit/support/fixtures.js +++ b/test/unit/support/fixtures.js @@ -230,7 +230,7 @@ export function applyFixtures () { } export function fixture (name, opts = {}) { - const tpl = FIXTURES[name] || (() => {}); + const tpl = typeof name === 'function' ? name : FIXTURES[name] || (() => {}); testBed().innerHTML = tpl(opts); } diff --git a/test/unit/token.test.js b/test/unit/token.test.js index cdb412a57..a66a6c287 100644 --- a/test/unit/token.test.js +++ b/test/unit/token.test.js @@ -1,14 +1,12 @@ import assert from 'assert'; import after from 'lodash.after'; -import merge from 'lodash.merge'; -import each from 'lodash.foreach'; import clone from 'component-clone'; import Promise from 'promise'; import { Recurly } from '../../lib/recurly'; import { applyFixtures } from './support/fixtures'; import { initRecurly, testBed } from './support/helpers'; -describe(`Recurly.token`, function () { +describe('Recurly.token', function () { // Some of these tests can take a while to stand up fields and receive reponses this.timeout(15000); @@ -120,7 +118,7 @@ describe(`Recurly.token`, function () { cardMonthElement.attach(container); cardYearElement.attach(container); - recurly.token(form, (err, token) => { + recurly.token(form, (err) => { assert.strictEqual(err.code, 'elements-tokenization-not-possible'); assert.deepEqual(err.found, ['CardMonthElement', 'CardYearElement']); done(); @@ -129,16 +127,65 @@ describe(`Recurly.token`, function () { }); }); - function buildRecurly () { + describe('Cvv standalone', function () { + applyFixtures(); + buildRecurly(); + + describe('when using a HostedField', function () { + this.ctx.fixture = () => ` +
+
+ +
+ `; + beforeEach(function () { + this.recurly.hostedFields.fields[0].iframe.contentWindow.setStubTokenizationElementName('cvv'); + }); + + describe('when called with a plain object', function () { + cvvSuite(plainObjectBuilder); + }); + + describe('when called with an HTMLFormElement', function () { + cvvSuite(formBuilder); + }); + }); + + describe('when called with an Elements instance', function () { + this.ctx.fixture = () => ` +
+
+ +
+ `; + + function setupStub (builder) { + return function (...args) { + return builder.call(this, ...args).then(res => { + this.elements.elements[0].iframe.contentWindow.setStubTokenizationElementName('cvv'); + return res; + }); + }; + } + + cvvSuite(setupStub(elementsBuilder)); + + describe('when called with an HTMLFormElement', function () { + cvvSuite(setupStub(elementsFormOnlyBuilder)); + }); + }); + }); + + function buildRecurly (opts) { beforeEach(function (done) { - this.recurly = initRecurly(); + this.recurly = initRecurly(opts); this.recurly.ready(() => done()); }); afterEach(function () { this.recurly.destroy(); }); - }; + } /** * For each example, updates corresponding hosted fields and returns all others @@ -183,14 +230,14 @@ describe(`Recurly.token`, function () { */ function elementsBuilder (example) { const form = window.document.querySelector('#test-form'); - const container = form.querySelector(`#recurly-elements`); + const container = form.querySelector('#recurly-elements'); const elements = this.elements = this.recurly.Elements(); return Promise.all(Object.keys(example).map(key => { const val = example[key]; let el = form.querySelector(`[data-recurly=${key}]`); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { if (el && 'value' in el) { el.value = val; resolve(); @@ -490,58 +537,7 @@ describe(`Recurly.token`, function () { this.recurly.configure({ required: ['cvv'] }); }); - describe('when cvv is blank', function () { - prepareExample(Object.assign({}, valid, { - cvv: '' - }), builder); - it('produces a validation error', function (done) { - this.subject((err, token) => { - assert.strictEqual(err.code, 'validation'); - assert.strictEqual(err.fields.length, 1); - assert(~err.fields.indexOf('cvv')); - assert.strictEqual(err.details.length, 1); - assert.strictEqual(err.details[0].field, 'cvv'); - assert.strictEqual(err.details[0].messages.length, 1); - assert.strictEqual(err.details[0].messages[0], "can't be blank"); - assert(!token); - done(); - }); - }); - }); - - describe('when cvv is invalid', function () { - prepareExample(Object.assign({}, valid, { - cvv: '23783564' - }), builder); - - it('produces a validation error', function (done) { - this.subject((err, token) => { - assert.strictEqual(err.code, 'validation'); - assert.strictEqual(err.fields.length, 1); - assert(~err.fields.indexOf('cvv')); - assert.strictEqual(err.details.length, 1); - assert.strictEqual(err.details[0].field, 'cvv'); - assert.strictEqual(err.details[0].messages.length, 1); - assert.strictEqual(err.details[0].messages[0], 'is invalid'); - assert(!token); - done(); - }); - }); - }); - - describe('when cvv is valid', function () { - prepareExample(Object.assign({}, valid, { - cvv: '123' - }), builder); - - it('yields a token', function (done) { - this.subject((err, token) => { - assert(!err); - assert(token); - done(); - }); - }); - }); + cvvSuite(builder, valid); }); describe('when a tax_identifier is provided', function () { @@ -594,6 +590,61 @@ describe(`Recurly.token`, function () { }); } + function cvvSuite (builder, valid) { + describe('when cvv is blank', function () { + prepareExample(Object.assign({}, valid, { + cvv: '' + }), builder); + it('produces a validation error', function (done) { + this.subject((err, token) => { + assert.strictEqual(err.code, 'validation'); + assert.strictEqual(err.fields.length, 1); + assert(~err.fields.indexOf('cvv')); + assert.strictEqual(err.details.length, 1); + assert.strictEqual(err.details[0].field, 'cvv'); + assert.strictEqual(err.details[0].messages.length, 1); + assert.strictEqual(err.details[0].messages[0], "can't be blank"); + assert(!token); + done(); + }); + }); + }); + + describe('when cvv is invalid', function () { + prepareExample(Object.assign({}, valid, { + cvv: '23783564' + }), builder); + + it('produces a validation error', function (done) { + this.subject((err, token) => { + assert.strictEqual(err.code, 'validation'); + assert.strictEqual(err.fields.length, 1); + assert(~err.fields.indexOf('cvv')); + assert.strictEqual(err.details.length, 1); + assert.strictEqual(err.details[0].field, 'cvv'); + assert.strictEqual(err.details[0].messages.length, 1); + assert.strictEqual(err.details[0].messages[0], 'is invalid'); + assert(!token); + done(); + }); + }); + }); + + describe('when cvv is valid', function () { + prepareExample(Object.assign({}, valid, { + cvv: '123' + }), builder); + + it('yields a token', function (done) { + this.subject((err, token) => { + assert(!err); + assert(token); + done(); + }); + }); + }); + } + function tokenAllMarkupSuite (builder) { describe('when given additional required fields', function () { beforeEach(function (done) { @@ -636,7 +687,7 @@ describe(`Recurly.token`, function () { done(); }); }); - }) + }); }); describe('when given a blank postal_code', function () { diff --git a/wdio.ci.conf.js b/wdio.ci.conf.js index 0757eb086..ec9b102e4 100644 --- a/wdio.ci.conf.js +++ b/wdio.ci.conf.js @@ -7,7 +7,7 @@ const { } = require('./test/conf/browserstack'); const { - BROWSER = 'BrowserStackChrome', + BROWSER = 'Chrome-Remote', BROWSER_STACK_USERNAME: user, BROWSER_STACK_ACCESS_KEY: key, GITHUB_RUN_ID From d1fa962b9da8b1b6379270fe9156ee8b70144372 Mon Sep 17 00:00:00 2001 From: Chris Barton Date: Thu, 3 Oct 2024 18:31:03 -0700 Subject: [PATCH 2/2] chore: always publish artifact when unit tests pass The e2e tests can depend on upstream behavior that is not live, so we should still publish so that upstream behavior can be tested on a build artifact. --- .github/workflows/ci.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 560948df6..994954ab8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -140,8 +140,6 @@ jobs: - check - unit_test - unit_test_remote - - e2e_test - - integration_test steps: - uses: actions/checkout@v3 - uses: browser-actions/setup-chrome@v1