chain.js 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. /**
  2. * Adapted from https://github.com/mattphillips/jest-chain/blob/main/src/chain.js
  3. */
  4. import { evaluatedBy } from "./evaluatedBy.js";
  5. class JestlikeAssertionError extends Error {
  6. constructor(result, callsite) {
  7. super(typeof result.message === "function" ? result.message() : result.message);
  8. Object.defineProperty(this, "matcherResult", {
  9. enumerable: true,
  10. configurable: true,
  11. writable: true,
  12. value: void 0
  13. });
  14. this.matcherResult = result;
  15. if (Error.captureStackTrace) {
  16. Error.captureStackTrace(this, callsite);
  17. }
  18. }
  19. }
  20. const _wrapMatchers = (matchers, evaluator, originalArgs, originalExpect, staticPath = []) => {
  21. return Object.keys(matchers)
  22. .filter((name) => typeof matchers[name] === "function")
  23. .map((name) => {
  24. const newMatcher = async (...args) => {
  25. try {
  26. const score = await evaluatedBy(originalArgs[0], evaluator);
  27. let result = originalExpect(score);
  28. for (const pathEntry of staticPath) {
  29. result = result[pathEntry];
  30. }
  31. result = result[name](...args); // run matcher up to current state
  32. if (result && typeof result.then === "function") {
  33. return Object.assign(Promise.resolve(result), matchers);
  34. }
  35. else {
  36. return matchers;
  37. }
  38. }
  39. catch (error) {
  40. if (!error.matcherResult) {
  41. throw error;
  42. }
  43. else {
  44. throw new JestlikeAssertionError(error.matcherResult, newMatcher);
  45. }
  46. }
  47. };
  48. return { [name]: newMatcher };
  49. });
  50. };
  51. const addEvaluatedBy = (matchers, originalArgs, originalExpect, staticPath = []) => {
  52. let spreadMatchers = { ...matchers };
  53. // Handle Bun, which uses a class, and Vitest which uses something weird
  54. if (Object.keys(matchers).length === 0 ||
  55. !Object.keys(matchers).includes("toEqual")) {
  56. const prototypeProps = Object.getOwnPropertyNames(Object.getPrototypeOf(matchers));
  57. spreadMatchers = Object.fromEntries(prototypeProps.map((prop) => {
  58. try {
  59. return [prop, matchers[prop]];
  60. }
  61. catch (e) {
  62. // Ignore bizarre Bun bug
  63. return [];
  64. }
  65. }));
  66. }
  67. return Object.assign({}, matchers, {
  68. evaluatedBy: function (evaluator) {
  69. const mappedMatchers = _wrapMatchers(spreadMatchers, evaluator, originalArgs, originalExpect, []);
  70. // .not etc.
  71. const staticMatchers = Object.keys(spreadMatchers)
  72. .filter((name) => typeof matchers[name] !== "function")
  73. .map((name) => {
  74. return {
  75. [name]: Object.assign({}, ..._wrapMatchers(spreadMatchers, evaluator, originalArgs, originalExpect, staticPath.concat(name))),
  76. };
  77. });
  78. return Object.assign({}, ...mappedMatchers, ...staticMatchers);
  79. },
  80. });
  81. };
  82. export function wrapExpect(originalExpect) {
  83. // proxy the expect function
  84. const expectProxy = Object.assign((...args) => addEvaluatedBy(originalExpect(...args), args, originalExpect, []), // partially apply expect to get all matchers and chain them
  85. originalExpect // clone additional properties on expect
  86. );
  87. return expectProxy;
  88. }
  89. globalThis.expect = wrapExpect(globalThis.expect);