Skip to content

Commit

Permalink
fix(testing): allow assertions in chai retry to have signature and be…
Browse files Browse the repository at this point in the history
… nested (#602)
  • Loading branch information
0xRezaa authored Feb 8, 2024
1 parent e97dbd8 commit 31b570f
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 41 deletions.
74 changes: 42 additions & 32 deletions packages/testing/src/chai-retry-plugin/chai-retry-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Chai from 'chai';

import { retryFunctionAndAssertions } from './helpers';
import type { AssertionMethod, FunctionToRetry, AssertionStackItem, RetryOptions } from './types';
import { PromiseLikeAssertion } from '../types';
import type { AssertionMethod, FunctionToRetry, AssertionStackItem, RetryOptions, Assertion } from './types';
import type { PromiseLikeAssertion } from '../types';

/**
* Plugin that allows to re-run function passed to `expect`, in order to achieve that use new `retry` method, retrying would be performed until
Expand Down Expand Up @@ -49,36 +49,7 @@ export const chaiRetryPlugin = function (_: typeof Chai, { flag, inspect }: Chai

const assertionProxy: PromiseLikeAssertion = Object.assign(
new Proxy(proxyTarget, {
get: function (target: Chai.Assertion, key: string, proxySelf: Chai.Assertion) {
let value: Chai.Assertion | undefined;

try {
// if `value` is a getter property that may immediately perform the assertion and throw the AssertionError
value = target[key as keyof Chai.Assertion] as Chai.Assertion;
} catch {
//
}

if (typeof value === 'function') {
return (...args: unknown[]) => {
if (key === 'then') {
return (value as unknown as AssertionMethod)(...args);
}

assertionStack.push({
propertyName: key as keyof Chai.Assertion,
method: value as unknown as AssertionMethod,
args,
});

return proxySelf;
};
} else {
assertionStack.push({ propertyName: key as keyof Chai.Assertion });
}

return proxySelf;
},
get: proxyGetter,
}),
{
then: (resolve: () => void, reject: () => void) => {
Expand All @@ -93,6 +64,45 @@ export const chaiRetryPlugin = function (_: typeof Chai, { flag, inspect }: Chai
) as unknown as PromiseLikeAssertion;

return assertionProxy;

function proxyGetter(target: Assertion, key: string, proxySelf: Assertion): Chai.Assertion {
let value: Chai.Assertion | undefined;

try {
// if `value` is a getter property that may immediately perform the assertion and throw the AssertionError
value = target[key as keyof Chai.Assertion] as Assertion;
} catch {
//
}

const assertionStackItem: AssertionStackItem = {
propertyName: key as keyof Chai.Assertion,
};
if (typeof value === 'function') {
if (key !== 'then') {
assertionStack.push(assertionStackItem);
}
return new Proxy(value, {
get: function (target, key: string) {
return proxyGetter(target as Assertion, key as keyof Chai.Assertion, proxySelf);
},
apply: function (_, __, args: unknown[]) {
if (key === 'then') {
return (value as unknown as AssertionMethod)(...args);
}

assertionStackItem.method = value as unknown as AssertionMethod;
assertionStackItem.args = args;

return proxySelf;
},
}) as Assertion;
} else {
assertionStack.push(assertionStackItem);
}

return proxySelf;
}
},
writable: false,
configurable: false,
Expand Down
8 changes: 6 additions & 2 deletions packages/testing/src/chai-retry-plugin/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ declare global {
* - `delay`: The delay in milliseconds between retries.
* @default { timeout: 5000, delay: 0, retries: Infinity }
*/
retry(options?: RetryOptions): PromiseLikeAssertion;
retry(options?: RetryOptions): PromiseLikeAssertion<Assertion>;
}
}
}

export type AssertionMethod = (...args: unknown[]) => Chai.Assertion | Promise<Chai.Assertion>;
export interface Assertion extends Chai.Assertion {
(...args: unknown[]): Chai.Assertion;
}

export type AssertionMethod = (...args: unknown[]) => Assertion | Promise<Assertion>;

// Function provided as argument of `expect`
export type FunctionToRetry = (...args: unknown[]) => unknown;
Expand Down
10 changes: 10 additions & 0 deletions packages/testing/src/test/chai-retry-plugin.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,16 @@ describe('chai-retry-plugin', () => {
.to.matchCode(`const source = true;`);
});
});

describe('should work with assertions with signatures and nested assertions', function () {
it('should allow use of both include and include.members as assertion', async () => {
const getExpected = () => [1, 2, 3];

await expect(getExpected).retry().to.include(2);

await expect(getExpected).retry().to.include.members([1, 2, 2, 1]);
});
});
});

const withCallCount = (func: (callCount: number) => unknown) => {
Expand Down
16 changes: 9 additions & 7 deletions packages/testing/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
type PromisifiedType<T> = Promisify<T> & PromiseLike<any>;

// Helper type to convert a type T into a Promise-like version of itself
export type Promisify<T> = {
[Key in keyof T]: T[Key] extends T
? Promisify<T[Key]> & PromiseLike<any>
: T[Key] extends (...args: any) => any
? (...args: Parameters<T[Key]>) => Promisify<ReturnType<T[Key]>> & PromiseLike<any>
: Promisify<T[Key]> & PromiseLike<any>;
type Promisify<T> = {
[Key in keyof T]: T[Key] extends (...args: any) => any
? keyof T[Key] extends never
? (...args: Parameters<T[Key]>) => PromisifiedType<ReturnType<T[Key]>>
: PromisifiedType<T[Key]> & { (...args: Parameters<T[Key]>): PromisifiedType<ReturnType<T[Key]>> }
: PromisifiedType<T[Key]>;
};

export type PromiseLikeAssertion = Promisify<Chai.Assertion> & PromiseLike<void>;
export type PromiseLikeAssertion<T extends Chai.Assertion = Chai.Assertion> = Promisify<T> & PromiseLike<void>;

0 comments on commit 31b570f

Please sign in to comment.