Chatwoot/app/test-matchers.js

424 lines
12 KiB
JavaScript
Raw Normal View History

import { fail } from 'jest';
function runAssertions(ctx, func) {
try {
const message = func() || '';
return {
message: typeof message === 'function' ? message : () => message,
pass: true,
};
} catch (e) {
return {
pass: false,
message: () => e.message || e,
};
}
}
function assert(expr, failMessage) {
if (!expr) {
const finalMessage =
typeof failMessage === 'function' ? failMessage() : failMessage;
throw new Error(finalMessage);
}
}
function prettyPrint(obj) {
return JSON.stringify(obj, null, 2);
}
async function sleep(ms) {
return new Promise(resolve => {
window.setTimeout(() => {
resolve();
}, ms);
});
}
function assertHasKeys(obj, keys, msg) {
assert(obj, 'actual is not set');
assert(typeof obj === 'object', 'actual is not an object');
assert(
(() => {
const objectKeys = Object.keys(obj);
return keys.reduce(
(acc, cur) => acc && objectKeys.indexOf(cur) > -1,
true
);
})(),
msg
);
return msg;
}
function notFor(self) {
return self.isNot ? ' not ' : ' ';
}
function testIsInstance(actual, ctor) {
assert(actual !== undefined, 'actual is undefined');
assert(actual !== null, 'actual is null');
assert(
actual instanceof ctor,
`Expected instance of ${Object.prototype.toString.call(
ctor
)} but got ${actual}`
);
}
async function runAssertionsAsync(ctx, func) {
try {
await func();
return {
message: () => '',
pass: !ctx.isNot,
};
} catch (e) {
return {
pass: false,
message: () => e.message || e,
};
}
}
beforeAll(() => {
expect.extend({
toHaveKeys(actual, ...expected) {
return runAssertions(this, () => {
assert(expected, 'keys are not set');
const msg = () =>
`expected\n${prettyPrint(actual)}\nto have keys\n${prettyPrint(
expected
)}`;
return assertHasKeys(actual, expected, msg);
});
},
toHaveKey(actual, expected) {
return runAssertions(this, () => {
assert(expected, 'key is not set');
const msg = () =>
`expected ${prettyPrint(actual)} to have key "${expected}"`;
return assertHasKeys(actual, [expected], msg);
});
},
toBeEquivalentTo(actual, expected) {
return runAssertions(this, () => {
const msg = () =>
`expected collection equivalent to\n${prettyPrint(
expected
)}\nbut got\n${prettyPrint(actual)}`;
assert(Array.isArray(actual), () => `${actual} is not an array`);
assert(Array.isArray(expected), () => `${expected} is not an array`);
assert(actual.length === expected.length, msg);
assert(
actual.reduce((acc, cur) => acc && expected.indexOf(cur) > -1, true),
msg
);
return msg;
});
},
toBePrototypical(actual) {
return runAssertions(this, () => {
const msg = () =>
`expected${notFor(this)}prototype, but got ${prettyPrint(actual)}`;
assert(actual, msg);
assert(actual.prototype, msg);
return msg;
});
},
toBeAsyncFunction(actual) {
return runAssertions(this, () => {
const msg = () =>
`expected${notFor(this)}async function but got ${prettyPrint(
actual
)}`;
assert(
Object.prototype.toString.call(actual) === '[object AsyncFunction]' ||
Object.prototype.toString.call(actual) === '[object Function]',
msg
);
return msg;
});
},
toBePromiseLike(actual) {
return runAssertions(this, () => {
const err = moreInfo => {
return `expected something${notFor(
this
)}promise-like, but got ${actual}${
moreInfo ? '\n\t(' : ''
}${moreInfo}${moreInfo ? ')' : ''}`;
};
assert(actual, err);
assert(typeof actual === 'object', err);
assert(
actual.then && typeof actual.then === 'function',
'should have a then function'
);
return () => err();
});
},
toBeConstructor(actual) {
return runAssertions(this, () => {
const err = () => {
return `expected ${actual}${notFor(this)}to be a constructor`;
};
assert(actual, err);
assert(actual.prototype, err);
return err;
});
},
toBeA(actual, ctor) {
return runAssertions(this, () => {
testIsInstance(actual, ctor);
return () =>
`expected${notFor(
this
)}to get instance of ${ctor}, but received ${actual}`;
});
},
toBeAn(actual, ctor) {
return runAssertions(this, () => {
testIsInstance(actual, ctor);
return () =>
`expected${notFor(
this
)}to get instance of ${ctor}, but received ${actual}`;
});
},
toBeVueComponent(actual, withName) {
return runAssertions(this, () => {
assert(
typeof actual.render === 'function',
`actual does not have a render function -- are you sure it's a Vue component?`
);
assert(
actual.name === withName,
`Expected component${notFor(
this
)}to have name "${withName}", but found "${actual.name}"`
);
return () =>
`Expected${notFor(
this
)}to receive a Vue component with name ${withName}`;
});
},
toBeNumericInput(htmlElement) {
return runAssertions(this, () => {
const msg = () =>
`Expected${notFor(this)}to receive numeric input but got: ${
htmlElement.outerHTML
}`;
assert(htmlElement.type === 'number', msg);
return msg;
});
},
toHaveCssClass(actual, cssClass) {
return runAssertions(this, () => {
const msg = () =>
`Expected ${actual.outerHTML}${notFor(
this
)}to have css class "${cssClass}"`;
const el = actual.$el || actual;
assert(el.classList.contains(cssClass), msg);
return msg;
});
},
toHaveBeenCalledOnce(actual) {
return runAssertions(this, () => {
if (this.isNot) {
throw new Error(
[
"Negation of 'toHaveBeenCalledOnce' is ambiguous ",
"(do you mean 'not at all' or 'any number except 1'?)",
].join('')
);
}
expect(actual).toHaveBeenCalledTimes(1);
return () => `Expected${notFor(this)}to have been called once`;
});
},
toHaveBeenCalledOnceWith(actual, ...args) {
return runAssertions(this, () => {
expect(actual).toHaveBeenCalledTimes(1);
expect(actual).toHaveBeenCalledWith(...args);
return () =>
`Expected${notFor(this)}to have been called once with ${args}`;
});
},
toBeHidden(actual) {
return runAssertions(this, () => {
const msg = () =>
`Expected '${actual.outerHTML}'${notFor(this)}to be hidden`;
assert(actual, 'actual does not exist');
assert(actual.style, 'actual may not be an html element?');
assert(
actual.style.display === 'none' ||
actual.style.visibility === 'hidden' ||
actual.style.visibility === 'collapse',
msg
);
return msg;
});
},
toBeVisible(htmlElement) {
return runAssertions(this, () => {
const msg = () =>
`Expected '${htmlElement.outerHTML}'${notFor(this)}to be hidden`;
assert(htmlElement, 'actual does not exist');
assert(
htmlElement.style.display !== 'none' &&
htmlElement.style.visibility !== 'hidden' &&
htmlElement.style.visibility !== 'collapse',
msg
);
return msg;
});
},
async toBeCompleted(actual) {
return runAssertionsAsync(this, async () => {
let completed = false;
let state = 'pending';
const msg = () =>
`expected${notFor(this)}to complete promise (final state: ${state})`;
actual
.then(() => {
state = 'resolved';
completed = true;
})
.catch(() => {
state = 'rejected';
completed = true;
});
await sleep(50);
if (completed && this.isNot) {
fail(msg());
} else if (!completed && !this.isNot) {
fail(msg());
}
});
},
async toBeResolved(actual, message, timeoutMs) {
return runAssertionsAsync(this, async () => {
let resolved = null;
const timeout = timeoutMs || 50;
const msg = () =>
`expected${notFor(this)}to resolve promise, but ${
resolved === null ? 'it never completed' : 'it rejected'
}${message ? `More info: ${message}` : ''}`;
actual
.then(() => {
resolved = true;
})
.catch(() => {
resolved = false;
});
let slept = 0;
while (resolved === null && slept < timeout) {
// eslint-disable-next-line no-await-in-loop
await sleep(50);
slept += 50;
}
if (resolved && this.isNot) {
fail(msg());
} else if (!resolved && !this.isNot) {
fail(msg());
}
});
},
async toBeRejected(actual, message, timeoutMs) {
return runAssertionsAsync(this, async () => {
let rejected = null;
const timeout = timeoutMs || 50;
const msg = () =>
`expected${notFor(this)}to reject promise, but ${
rejected === null ? 'it never completed' : 'it resolved'
}${message ? `More info: ${message}` : ''}`;
actual
.then(() => {
rejected = false;
})
.catch(() => {
rejected = true;
});
let slept = 0;
while (rejected === null && slept < timeout) {
// eslint-disable-next-line no-await-in-loop
await sleep(50);
slept += 50;
}
if (rejected && this.isNot) {
fail(msg());
} else if (!rejected && !this.isNot) {
fail(msg());
}
});
},
toExist(actual) {
return runAssertions(this, () => {
const msg = () => `Expected ${actual}${notFor(this)}to exist`;
assert(actual !== null && actual !== undefined, msg);
return msg;
});
},
toBeDisabled(actual) {
return runAssertions(this, () => {
const msg = () => `Expected ${actual}${notFor(this)}to be disabled`;
assert(actual.disabled, msg);
return msg;
});
},
toHaveReceivedNoCallsAtAll(mockedObject) {
return runAssertions(this, () => {
const called = Object.getOwnPropertyNames(
Object.getPrototypeOf(mockedObject)
).reduce((acc, cur) => {
const prop = mockedObject[cur];
if (typeof prop.mock === 'undefined') {
return acc;
}
if (prop.mock.calls && prop.mock.calls.length) {
acc.push(cur);
}
return acc;
}, []);
const msg = () =>
`expected${notFor(
this
)}to have received any calls, but got ${called}`;
assert(!called.length, msg);
return msg;
});
},
toHaveReceivedOnly(mockedObject, ...calls) {
return runAssertions(this, () => {
const called = Object.getOwnPropertyNames(
Object.getPrototypeOf(mockedObject)
).reduce((acc, cur) => {
const prop = mockedObject[cur];
if (typeof prop.mock === 'undefined') {
return acc;
}
if (
prop.mock.calls &&
prop.mock.calls.length &&
calls.indexOf(cur) === -1
) {
acc.push(cur);
}
return acc;
}, []);
const msg = () =>
`expected${notFor(
this
)}to have received any calls, but got ${called}`;
assert(!called.length, msg);
return msg;
});
},
});
});