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

fix(chai-retry-plugin): collision with chai-as-promised plugin #392

Merged
merged 2 commits into from
May 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 59 additions & 54 deletions packages/testing/src/chai-retry-plugin/chai-retry-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { retryFunctionAndAssertions } from './helpers';
import type { AssertionMethod, FunctionToRetry, AssertionStackItem, RetryOptions, PromiseLikeAssertion } from './types';

/**
* Plugin that allows to re-run function passed to `expect` with new `retry` method, retrying would be performed until
* the result will pass the chained assertion or timeout exceeded or retries limit reached.
* Plugin that allows to re-run function passed to `expect`, in order to achieve that use new `retry` method, retrying would be performed until
Copy link
Contributor

@alisey alisey May 2, 2023

Choose a reason for hiding this comment

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

I don't understand this. The words don't connect into a sentence.

Change to:

This plugin allows to retry an assertion until it succeeds or a timeout is reached.

* the result will pass the chained assertion or timeout exceeded or retries limit reached. The assertion chain eventually returns the last
* successfully asserted value.
* Should be applied through `Chai.use` function, for example:
* @example
* ```ts
Expand All @@ -29,67 +30,71 @@ import type { AssertionMethod, FunctionToRetry, AssertionStackItem, RetryOptions
* await expect(funcToRetry).retry().and.have.property('success').and.be.true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a more realistic example:

await expect(() => document.title).retry({ timeout: 10_000 }).to.equal('hello');

* ```
*/
export const chaiRetryPlugin = function (_: typeof Chai, utils: Chai.ChaiUtils) {
Chai.Assertion.addMethod('retry', function (retryOptions: RetryOptions = {}): PromiseLikeAssertion {
const functionToRetry: FunctionToRetry = this._obj as FunctionToRetry;
const description = utils.flag(this, 'message') as string;
export const chaiRetryPlugin = function (_: typeof Chai, { flag, inspect }: Chai.ChaiUtils) {
Object.defineProperty(Chai.Assertion.prototype, 'retry', {
value: function (retryOptions: RetryOptions = {}): PromiseLikeAssertion {
Copy link
Contributor

Choose a reason for hiding this comment

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

Extract this to a named function instead of increasing indentation even further.

const functionToRetry: FunctionToRetry = flag(this as Chai.AssertStatic, 'object') as FunctionToRetry;
const description = flag(this as Chai.AssertStatic, 'message') as string;

if (typeof functionToRetry !== 'function') {
throw new TypeError(utils.inspect(functionToRetry) + ' is not a function.');
}
if (typeof functionToRetry !== 'function') {
throw new TypeError(inspect(functionToRetry) + ' is not a function.');
}

const defaultRetryOptions: Required<RetryOptions> = { timeout: 5000, retries: Infinity, delay: 0 };
const options: Required<RetryOptions> = { ...defaultRetryOptions, ...retryOptions };
const defaultRetryOptions: Required<RetryOptions> = { timeout: 5000, retries: Infinity, delay: 0 };
const options: Required<RetryOptions> = { ...defaultRetryOptions, ...retryOptions };

const assertionStack: AssertionStackItem[] = [];
// Fake assertion object for catching calls of chained methods
const proxyTarget = new Chai.Assertion({});
const assertionStack: AssertionStackItem[] = [];
// Fake assertion object for catching calls of chained methods
const proxyTarget = new Chai.Assertion({});

const assertionProxy: PromiseLikeAssertion = Object.assign(
new Proxy(proxyTarget, {
get: function (target: Chai.Assertion, key: string, proxySelf: Chai.Assertion) {
let value: Chai.Assertion | undefined;
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 {
//
}
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);
}
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,
});
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;
};
} else {
assertionStack.push({ propertyName: key as keyof Chai.Assertion });
}

return proxySelf;
},
}),
{
then: (resolve: () => void, reject: () => void) => {
return retryFunctionAndAssertions({
functionToRetry,
options,
assertionStack,
description,
}).then(resolve, reject);
},
}
) as unknown as PromiseLikeAssertion;
return proxySelf;
},
}),
{
then: (resolve: () => void, reject: () => void) => {
return retryFunctionAndAssertions({
functionToRetry,
options,
assertionStack,
description,
}).then(resolve, reject);
},
}
) as unknown as PromiseLikeAssertion;

return assertionProxy;
return assertionProxy;
},
writable: false,
configurable: false,
});
};
6 changes: 3 additions & 3 deletions packages/testing/src/chai-retry-plugin/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ function sleepWithSafetyMargin(ms: number): Promise<void> {
return promiseHelpers.sleep(ms);
}

function timeoutWithSafetyMargin(promise: Promise<void>, ms: number, getTimeoutError: () => string): Promise<void> {
function timeoutWithSafetyMargin(promise: Promise<unknown>, ms: number, getTimeoutError: () => string): Promise<unknown> {
addTimeoutSafetyMargin(ms);
return promiseHelpers.timeout(promise, ms, getTimeoutError);
}

export const retryFunctionAndAssertions = async (retryAndAssertArguments: RetryAndAssertArguments): Promise<void> => {
export const retryFunctionAndAssertions = async (retryAndAssertArguments: RetryAndAssertArguments): Promise<unknown> => {
let assertionError: Error | undefined;
let isTimeoutExceeded = false;

Expand Down Expand Up @@ -52,7 +52,7 @@ export const retryFunctionAndAssertions = async (retryAndAssertArguments: RetryA
}
}

return;
return valueToAssert;
} catch (error: unknown) {
assertionError = error as Error;
await sleepWithSafetyMargin(delay);
Expand Down
19 changes: 18 additions & 1 deletion packages/testing/src/test/chai-retry-plugin.unit.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import Chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { sleep } from 'promise-assist';

import { chaiRetryPlugin } from '../chai-retry-plugin/chai-retry-plugin';

Chai.use(chaiRetryPlugin);
// `chai-as-promised` should be used in order to test collision between plugins
Copy link
Contributor

Choose a reason for hiding this comment

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

"Should be used" or "is used"?

Chai.use(chaiAsPromised);

describe('chai-retry-plugin', () => {
it('should retry a function that eventually succeeds', async () => {
Expand Down Expand Up @@ -68,7 +72,20 @@ describe('chai-retry-plugin', () => {
return 'Success';
});

await expect(resultFunction).to.retry({ delay: 200 }).to.equal('Success');
await expect(resultFunction).retry({ delay: 200 }).to.equal('Success');
});

it('should return value that was asserted successfully', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you mean by "asserted successfully"? There's no assertion here.

const { resultFunction } = withCallCount((callCount: number) => {
if (callCount < 3) {
throw new Error('Failed');
}
return 'Success';
});

const result = await expect(resultFunction).retry();

expect(result).to.equal('Success');
});
});

Expand Down