Skip to content

Commit 873760e

Browse files
author
Dane Mauland
committed
fix: not.toHaveLog no longer returns a false pass when waiting for CloudWatch to populate
1 parent d1091b8 commit 873760e

File tree

3 files changed

+128
-3
lines changed

3 files changed

+128
-3
lines changed

src/jest/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { toHaveRecord } from './kinesis';
88
import { toHaveObject } from './s3';
99
import { toHaveMessage } from './sqs';
1010
import { toBeAtState, toHaveState } from './stepFunctions';
11-
import { wrapWithRetries } from './utils';
11+
import { wrapWithRetries, wrapWithRetryUntilPass } from './utils';
1212

1313
declare global {
1414
namespace jest {
@@ -32,7 +32,7 @@ declare global {
3232
expect.extend({
3333
toBeAtState: wrapWithRetries(toBeAtState) as typeof toBeAtState,
3434
toHaveItem: wrapWithRetries(toHaveItem) as typeof toHaveItem,
35-
toHaveLog: wrapWithRetries(toHaveLog) as typeof toHaveLog,
35+
toHaveLog: wrapWithRetryUntilPass(toHaveLog) as typeof toHaveLog,
3636
toHaveMessage: wrapWithRetries(toHaveMessage) as typeof toHaveMessage,
3737
toHaveObject: wrapWithRetries(toHaveObject) as typeof toHaveObject,
3838
toHaveRecord, // has built in timeout mechanism due to how kinesis consumer works

src/jest/utils.test.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22
import { ICommonProps } from '../common';
3-
import { wrapWithRetries } from './utils';
3+
import { wrapWithRetries, wrapWithRetryUntilPass } from './utils';
44

55
jest.mock('../common');
66

@@ -111,4 +111,95 @@ describe('utils', () => {
111111
expect(sleep).toHaveBeenCalledWith(500); // default pollEvery
112112
});
113113
});
114+
115+
describe('wrapWithRetryUntilPass', () => {
116+
beforeEach(() => {
117+
jest.clearAllMocks();
118+
});
119+
120+
test('should retry once on pass === true', async () => {
121+
const toWrap = jest.fn();
122+
const expectedResult = { pass: true, message: () => '' };
123+
toWrap.mockReturnValue(Promise.resolve(expectedResult));
124+
125+
const matcherUtils = {} as jest.MatcherUtils;
126+
127+
const props = { region: 'region' } as ICommonProps;
128+
const key = 'key';
129+
130+
const wrapped = wrapWithRetryUntilPass(toWrap);
131+
const result = await wrapped.bind(matcherUtils)(props, key);
132+
133+
expect(toWrap).toHaveBeenCalledTimes(1);
134+
expect(toWrap).toHaveBeenCalledWith(props, key);
135+
expect(result).toBe(expectedResult);
136+
});
137+
138+
test('should exhaust timeout on pass === false', async () => {
139+
const { sleep } = require('../common');
140+
141+
const mockedNow = jest.fn();
142+
Date.now = mockedNow;
143+
mockedNow.mockReturnValueOnce(0);
144+
mockedNow.mockReturnValueOnce(250);
145+
mockedNow.mockReturnValueOnce(500);
146+
mockedNow.mockReturnValueOnce(750);
147+
mockedNow.mockReturnValueOnce(1000);
148+
mockedNow.mockReturnValueOnce(1250);
149+
150+
const toWrap = jest.fn();
151+
const expectedResult = { pass: false, message: () => '' };
152+
toWrap.mockReturnValue(Promise.resolve(expectedResult));
153+
154+
const matcherUtils = {} as jest.MatcherUtils;
155+
156+
const props = { timeout: 1001, pollEvery: 250 } as ICommonProps;
157+
const key = 'key';
158+
159+
const wrapped = wrapWithRetryUntilPass(toWrap);
160+
const result = await wrapped.bind(matcherUtils)(props, key);
161+
162+
expect(toWrap).toHaveBeenCalledTimes(5);
163+
expect(toWrap).toHaveBeenCalledWith(props, key);
164+
expect(result).toBe(expectedResult);
165+
expect(sleep).toHaveBeenCalledTimes(4);
166+
expect(sleep).toHaveBeenCalledWith(props.pollEvery);
167+
});
168+
169+
test('should retry twice, { pass: false, isNot: false } => { pass: true, isNot: false }', async () => {
170+
const { sleep } = require('../common');
171+
172+
const mockedNow = jest.fn();
173+
Date.now = mockedNow;
174+
mockedNow.mockReturnValueOnce(0);
175+
mockedNow.mockReturnValueOnce(250);
176+
mockedNow.mockReturnValueOnce(500);
177+
178+
const toWrap = jest.fn();
179+
// first attempt returns pass === false
180+
toWrap.mockReturnValueOnce(
181+
Promise.resolve({ pass: false, message: () => '' }),
182+
);
183+
184+
// second attempt returns pass === true
185+
const expectedResult = { pass: true, message: () => '' };
186+
toWrap.mockReturnValueOnce(Promise.resolve(expectedResult));
187+
188+
const matcherUtils = {
189+
isNot: false,
190+
} as jest.MatcherUtils;
191+
192+
const props = {} as ICommonProps;
193+
const key = 'key';
194+
195+
const wrapped = wrapWithRetryUntilPass(toWrap);
196+
const result = await wrapped.bind(matcherUtils)(props, key);
197+
198+
expect(toWrap).toHaveBeenCalledTimes(2);
199+
expect(toWrap).toHaveBeenCalledWith(props, key);
200+
expect(result).toBe(expectedResult);
201+
expect(sleep).toHaveBeenCalledTimes(1);
202+
expect(sleep).toHaveBeenCalledWith(500); // default pollEvery
203+
});
204+
});
114205
});

src/jest/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,37 @@ export const wrapWithRetries = (
4242
}
4343
return wrapped;
4444
};
45+
46+
export const wrapWithRetryUntilPass = (
47+
matcher: (this: jest.MatcherUtils, ...args: any[]) => Promise<IMatchResult>,
48+
) => {
49+
async function wrapped(
50+
this: jest.MatcherUtils,
51+
props: ICommonProps,
52+
...args: any[]
53+
) {
54+
const { timeout = 2500, pollEvery = 500 } = props;
55+
56+
const start = Date.now();
57+
let result = await (matcher.apply(this, [
58+
props,
59+
...args,
60+
]) as Promise<IMatchResult>);
61+
while (Date.now() - start < timeout) {
62+
// return since result is found
63+
if (result.pass) {
64+
return result;
65+
}
66+
67+
// retry until result is found or timeout
68+
await sleep(pollEvery);
69+
70+
result = await (matcher.apply(this, [
71+
props,
72+
...args,
73+
]) as Promise<IMatchResult>);
74+
}
75+
return result;
76+
}
77+
return wrapped;
78+
};

0 commit comments

Comments
 (0)