Skip to content
This repository has been archived by the owner on Jan 13, 2025. It is now read-only.

Commit

Permalink
chore(infrastructure): Better error messages in verifyDefaultAdapter (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
acdvorak authored and kfranqueiro committed Sep 7, 2018
1 parent 3d285d9 commit 92fd9a7
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 32 deletions.
148 changes: 119 additions & 29 deletions test/unit/helpers/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,134 @@
import {assert} from 'chai';
import td from 'testdouble';

// Sanity tests to ensure the following:
// - Default adapters contain functions
// - All expected adapter functions are accounted for
// - Invoking any of the default methods does not throw an error.
// Every foundation test suite include this verification.
export function verifyDefaultAdapter(FoundationClass, expectedMethods) {
/**
* Sanity tests to ensure the following:
* - Default adapters contain functions
* - All expected adapter functions are accounted for
* - Invoking any of the default methods does not throw an error.
* Every foundation test suite include this verification.
* @param {!F.} FoundationClass
* @param {!Array<string>} expectedMethodNames
* @template F
*/
export function verifyDefaultAdapter(FoundationClass, expectedMethodNames) {
const {defaultAdapter} = FoundationClass;
const methods = Object.keys(defaultAdapter).filter((k) => typeof defaultAdapter[k] === 'function');
const adapterKeys = Object.keys(defaultAdapter);
const actualMethodNames = adapterKeys.filter((key) => typeof defaultAdapter[key] === 'function');

assert.equal(actualMethodNames.length, adapterKeys.length, 'Every adapter key must be a function');

assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
// Test for equality without requiring that the array be in a specific order
assert.deepEqual(methods.slice().sort(), expectedMethods.slice().sort());
const actualArray = actualMethodNames.slice().sort();
const expectedArray = expectedMethodNames.slice().sort();
assert.deepEqual(actualArray, expectedArray, getUnequalArrayMessage(actualArray, expectedArray));

// Test default methods
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
actualMethodNames.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
}

// Returns an object that intercepts calls to an adapter method used to register event handlers, and adds
// it to that object where the key is the event name and the value is the function being used. This is the
// preferred way of testing interaction handlers.
//
// ```javascript
// test('#init adds a click listener which adds a "foo" class', (t) => {
// const {foundation, mockAdapter} = setupTest();
// const handlers = captureHandlers(mockAdapter, 'registerInteractionHandler');
// foundation.init();
// handlers.click(/* you can pass event info in here */ {type: 'click'});
// t.doesNotThrow(() => td.verify(mockAdapter.addClass('foo')));
// t.end();
// });
// ```
//
// Note that `handlerCaptureMethod` _must_ have a signature of `(string, EventListener) => any` in order to
// be effective.
export function captureHandlers(adapter, handlerCaptureMethod) {
/**
* Returns an object that intercepts calls to an adapter method used to register event handlers, and adds
* it to that object where the key is the event name and the value is the function being used. This is the
* preferred way of testing interaction handlers.
*
* ```javascript
* test('#init adds a click listener which adds a "foo" class', (t) => {
* const {foundation, mockAdapter} = setupTest();
* const handlers = captureHandlers(mockAdapter, 'registerInteractionHandler');
* foundation.init();
* handlers.click(/* you can pass event info in here *\/ {type: 'click'});
* t.doesNotThrow(() => td.verify(mockAdapter.addClass('foo')));
* t.end();
* });
* ```
*
* Note that `handlerCaptureMethodName` _must_ have a signature of `(string, EventListener) => any` in order to
* be effective.
*
* @param {!A} adapter
* @param {string} handlerCaptureMethodName
* @template A
*/
export function captureHandlers(adapter, handlerCaptureMethodName) {
const {isA} = td.matchers;
const handlers = {};
td.when(adapter[handlerCaptureMethod](isA(String), isA(Function))).thenDo((type, handler) => {
td.when(adapter[handlerCaptureMethodName](isA(String), isA(Function))).thenDo((type, handler) => {
handlers[type] = (evtInfo = {}) => handler(Object.assign({type}, evtInfo));
});
return handlers;
}

/**
* @param {!Array<string>} actualArray
* @param {!Array<string>} expectedArray
* @return {string}
*/
function getUnequalArrayMessage(actualArray, expectedArray) {
/**
* @param {!Array<string>} values
* @param {string} singularName
* @return {string}
*/
const format = (values, singularName) => {
const count = values.length;
if (count === 0) {
return '';
}
const plural = count === 1 ? '' : 's';
const str = values.join(', ');
return `${count} ${singularName}${plural}: ${str}`;
};

/**
* @param {!Set<string>} actualSet
* @param {!Set<string>} expectedSet
* @return {string}
*/
const getAddedStr = (actualSet, expectedSet) => {
const addedArray = [];
actualSet.forEach((val) => {
if (!expectedSet.has(val)) {
addedArray.push(val);
}
});
return format(addedArray, 'unexpected method');
};

/**
* @param {!Set<string>} actualSet
* @param {!Set<string>} expectedSet
* @return {string}
*/
const getRemovedStr = (actualSet, expectedSet) => {
const removedArray = [];
expectedSet.forEach((val) => {
if (!actualSet.has(val)) {
removedArray.push(val);
}
});
return format(removedArray, 'missing method');
};

/**
* @param {!Array<string>} array
* @return {!Set<string>}
*/
const toSet = (array) => {
const set = new Set();
array.forEach((value) => set.add(value));
return set;
};

const actualSet = toSet(actualArray);
const expectedSet = toSet(expectedArray);
const addedStr = getAddedStr(actualSet, expectedSet);
const removedStr = getRemovedStr(actualSet, expectedSet);
const messages = [addedStr, removedStr].filter((val) => val.length > 0);

if (messages.length === 0) {
return '';
}

return `Found ${messages.join('; ')}`;
}
11 changes: 9 additions & 2 deletions test/unit/helpers/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@

import td from 'testdouble';

// Returns a foundation configured to use a mock object with the same api as a default adapter,
// as well as that adapter itself.
/**
* Returns a foundation configured to use a mock object with the same API as a default adapter,
* as well as that adapter itself.
* The trailing `.` in the `@param` type below is intentional: It indicates a reference to the class itself instead of
* an instance of the class.
* See https://youtrack.jetbrains.com/issue/WEB-10214#focus=streamItem-27-1305930-0-0
* @param {!MDCFoundation.} FoundationClass
* @return {{mockAdapter: !Object, foundation: !MDCFoundation}}
*/
export function setupFoundationTest(FoundationClass) {
const mockAdapter = td.object(FoundationClass.defaultAdapter);
const foundation = new FoundationClass(mockAdapter);
Expand Down
3 changes: 2 additions & 1 deletion test/unit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@

/** @fileoverview Bootstraps the test bundle for karma-webpack. */

const testsContext = require.context('.', true, /\.test\.js$/);
// https://github.com/webpack/docs/wiki/context#requirecontext
const testsContext = require.context(/* directory */ '.', /* useSubdirectories */ true, /\.test\.js$/);
testsContext.keys().forEach(testsContext);

0 comments on commit 92fd9a7

Please sign in to comment.