Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jest-each]: Add support for keyPaths in test titles #6457

Merged
merged 10 commits into from
Jun 20, 2018
61 changes: 60 additions & 1 deletion packages/jest-each/src/__tests__/template.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,66 @@ describe('jest-each', () => {
);
});

test('calls global with cb function with object built from tabel headings and values', () => {
test('calls global with title containing $key in multiple positions', () => {
const globalTestMocks = getGlobalTestMocks();
const eachObject = each.withGlobal(globalTestMocks)`
a | b | expected
${0} | ${1} | ${1}
${1} | ${1} | ${2}
`;
const testFunction = get(eachObject, keyPath);
testFunction(
'add($a, $b) expected string: a=$a, b=$b, expected=$expected',
noop,
);

const globalMock = get(globalTestMocks, keyPath);
expect(globalMock).toHaveBeenCalledTimes(2);
expect(globalMock).toHaveBeenCalledWith(
'add(0, 1) expected string: a=0, b=1, expected=1',
expectFunction,
);
expect(globalMock).toHaveBeenCalledWith(
'add(1, 1) expected string: a=1, b=1, expected=2',
expectFunction,
);
});

test('calls global with title containing $key.path', () => {
const globalTestMocks = getGlobalTestMocks();
const eachObject = each.withGlobal(globalTestMocks)`
a
${{foo: {bar: 'baz'}}}
`;
const testFunction = get(eachObject, keyPath);
testFunction('interpolates object keyPath to value: $a.foo.bar', noop);

const globalMock = get(globalTestMocks, keyPath);
expect(globalMock).toHaveBeenCalledTimes(1);
expect(globalMock).toHaveBeenCalledWith(
'interpolates object keyPath to value: "baz"',
expectFunction,
);
});

test('calls global with title containing last seen object when $key.path is invalid', () => {
const globalTestMocks = getGlobalTestMocks();
const eachObject = each.withGlobal(globalTestMocks)`
a
${{foo: {bar: 'baz'}}}
`;
const testFunction = get(eachObject, keyPath);
testFunction('interpolates object keyPath to value: $a.foo.qux', noop);

const globalMock = get(globalTestMocks, keyPath);
expect(globalMock).toHaveBeenCalledTimes(1);
expect(globalMock).toHaveBeenCalledWith(
'interpolates object keyPath to value: {"bar": "baz"}',
expectFunction,
);
});

test('calls global with cb function with object built from table headings and values', () => {
const globalTestMocks = getGlobalTestMocks();
const testCallBack = jest.fn();
const eachObject = each.withGlobal(globalTestMocks)`
Expand Down
68 changes: 63 additions & 5 deletions packages/jest-each/src/bind.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,12 +129,20 @@ const buildTable = (
),
);

const getMatchingKeyPaths = title => (matches, key) =>
matches.concat(title.match(new RegExp(`\\$${key}[\\.\\w]*`, 'g')) || []);

const replaceKeyPathWithValue = data => (title, match) => {
const keyPath = match.replace('$', '').split('.');
const result = getPath(data, keyPath);
const value = result.hasEndProp ? result.value : result.lastTraversedObject;
return title.replace(match, pretty(value, {maxDepth: 1, min: true}));
};

const interpolate = (title: string, data: any) =>
Object.keys(data).reduce(
(acc, key) =>
acc.replace('$' + key, pretty(data[key], {maxDepth: 1, min: true})),
title,
);
Object.keys(data)
.reduce(getMatchingKeyPaths(title), []) // aka flatMap
.reduce(replaceKeyPathWithValue(data), title);

const applyObjectParams = (obj: any, test: Function) => {
if (test.length > 1) return done => test(obj, done);
Expand All @@ -144,3 +152,53 @@ const applyObjectParams = (obj: any, test: Function) => {

const pluralize = (word: string, count: number) =>
word + (count === 1 ? '' : 's');

const hasOwnProperty = (object: Object, value: string) =>
Object.prototype.hasOwnProperty.call(object, value) ||
Object.prototype.hasOwnProperty.call(object.constructor.prototype, value);

const getPath = (object: Object, propertyPath: string | Array<string>) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've inlined this for now to get it working, it shouldn't stay here :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we just use lodash.get or something?

Copy link
Contributor Author

@mattphillips mattphillips Jun 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think we were using lodash anywhere? This getPath algorithm is used in the toHaveProperty matcher and offers a nicer returned value than lodash as it tracks the last visited object.

EDIT: see expect utils

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sticking it in jest-utils should be fine for sharing (although @cpojer hates the util package :D)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I move it as part of this PR or would it be better in a separate PR?

if (!Array.isArray(propertyPath)) {
propertyPath = propertyPath.split('.');
}

if (propertyPath.length) {
const lastProp = propertyPath.length === 1;
const prop = propertyPath[0];
const newObject = object[prop];

if (!lastProp && (newObject === null || newObject === undefined)) {
// This is not the last prop in the chain. If we keep recursing it will
// hit a `can't access property X of undefined | null`. At this point we
// know that the chain has broken and we can return right away.
return {
hasEndProp: false,
lastTraversedObject: object,
traversedPath: [],
};
}

const result = getPath(newObject, propertyPath.slice(1));

if (result.lastTraversedObject === null) {
result.lastTraversedObject = object;
}

result.traversedPath.unshift(prop);

if (lastProp) {
result.hasEndProp = hasOwnProperty(object, prop);
if (!result.hasEndProp) {
result.traversedPath.shift();
}
}

return result;
}

return {
lastTraversedObject: null,
traversedPath: [],
value: object,
};
};