Skip to content

Commit

Permalink
feat: add support for jest-circus (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbarba-nx authored Jul 1, 2021
1 parent d5c42ad commit bb9edf9
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 197 deletions.
7 changes: 5 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ jobs:
- restore_cache:
key: *cache_key

# Build and Test
- run: yarn test
# Test w/jasmine as test runner
- run: yarn test --testRunner jest-jasmine2
# Test w/jest-circus as test runner
- run: yarn test --testRunner jest-circus
# Build
- run: yarn build

workflows:
Expand Down
216 changes: 27 additions & 189 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
import {
CompleteNotification,
ErrorNotification,
NextNotification,
Observable,
ObservableNotification,
Subscription,
} from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { isEqual } from 'lodash';

import {
getTestScheduler,
initTestScheduler,
resetTestScheduler,
} from './src/scheduler';

import {
TestColdObservable,
TestHotObservable,
TestObservable,
} from './src/test-observables';
import { unparseMarble } from './src/marble-unparser';
import { mapSymbolsToNotifications } from './src/map-symbols-to-notifications';
import { TestMessages } from './src/types';

import {
toHaveSubscriptionsComparer,
toBeObservableComparer,
} from './src/utils';

export {
getTestScheduler,
Expand Down Expand Up @@ -64,182 +56,28 @@ declare global {
}
}

/*
* Based on source code found in rxjs library
* https://github.com/ReactiveX/rxjs/blob/master/src/testing/TestScheduler.ts
*
*/
function materializeInnerObservable<T>(
observable: Observable<T>,
outerFrame: number,
): TestMessages {
const messages: TestMessages = [];
const scheduler = getTestScheduler();

observable.subscribe({
next: (value: any) => {
messages.push({
frame: scheduler.frame - outerFrame,
notification: {
kind: 'N',
value,
error: undefined,
} as NextNotification<T>,
});
},
error: (error: any) => {
messages.push({
frame: scheduler.frame - outerFrame,
notification: {
kind: 'E',
value: undefined,
error,
} as ErrorNotification,
});
},
complete: () => {
messages.push({
frame: scheduler.frame - outerFrame,
notification: {
kind: 'C',
value: undefined,
error: undefined,
} as CompleteNotification,
});
},
});
return messages;
}

export function addMatchers() {
jasmine.addMatchers({
toHaveSubscriptions: () => ({
compare: function (actual: TestObservable, marbles: string | string[]) {
const marblesArray: string[] =
typeof marbles === 'string' ? [marbles] : marbles;
const results = marblesArray.map((marbles) =>
TestScheduler.parseMarblesAsSubscriptions(marbles),
);

expect(results).toEqual(actual.getSubscriptions());

return { pass: true };
},
}),
toBeObservable: (_utils, _equalityTester) => ({
compare: function (actual: TestObservable, fixture: TestObservable) {
const results: TestMessages = [];
let subscription: Subscription;
const scheduler = getTestScheduler();

scheduler.schedule(() => {
subscription = actual.subscribe({
next: (x: any) => {
let value = x;

// Support Observable-of-Observables
if (x instanceof Observable) {
value = materializeInnerObservable(value, scheduler.frame);
}

results.push({
frame: scheduler.frame,
notification: {
kind: 'N',
value,
error: undefined,
} as NextNotification<any>,
});
},
error: (error: any) => {
results.push({
frame: scheduler.frame,
notification: {
kind: 'E',
value: undefined,
error,
} as ErrorNotification,
});
},
complete: () => {
results.push({
frame: scheduler.frame,
notification: {
kind: 'C',
value: undefined,
error: undefined,
} as CompleteNotification,
});
},
});
});
scheduler.flush();

const expected = TestScheduler.parseMarbles(
fixture.marbles,
fixture.values,
fixture.error,
true,
true,
);

if (isEqual(results, expected)) {
return { pass: true };
}

const mapNotificationToSymbol = buildNotificationToSymbolMapper(
fixture.marbles,
expected,
isEqual,
);
const receivedMarble = unparseMarble(results, mapNotificationToSymbol);

const message = formatMessage(
fixture.marbles,
expected,
receivedMarble,
results,
);
return { pass: false, message };
},
}),
});
}

function buildNotificationToSymbolMapper(
expectedMarbles: string,
expectedMessages: TestMessages,
equalityFn: (a: any, b: any) => boolean,
) {
const symbolsToNotificationsMap = mapSymbolsToNotifications(
expectedMarbles,
expectedMessages,
);
return (notification: ObservableNotification<any>) => {
const mapped = Object.keys(symbolsToNotificationsMap).find((key) =>
equalityFn(symbolsToNotificationsMap[key], notification),
)!;

return mapped || '?';
};
}

function formatMessage(
expectedMarbles: string,
expectedMessages: TestMessages,
receivedMarbles: string,
receivedMessages: TestMessages,
) {
return `
Expected: ${expectedMarbles},
Received: ${receivedMarbles},
Expected:
${JSON.stringify(expectedMessages)}
Received:
${JSON.stringify(receivedMessages)},
`;
/**
* expect.extend is an API exposed by jest-circus,
* the default runner as of Jest v27. If that method
* is not available, assume we're in a Jasmine test
* environment.
*/
if (!expect.extend) {
jasmine.addMatchers({
toHaveSubscriptions: () => ({
compare: toHaveSubscriptionsComparer,
}),
toBeObservable: (_utils, _equalityTester) => ({
compare: toBeObservableComparer,
}),
});
} else {
expect.extend({
toHaveSubscriptions: toHaveSubscriptionsComparer,
toBeObservable: toBeObservableComparer,
});
}
}

export function setupEnvironment() {
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.14.7",
"@babel/preset-typescript": "^7.14.5",
"@types/jasmine": "^3.7.4",
"@types/jest": "^26.0.23",
"@types/lodash": "^4.14.106",
"@types/node": "^14.17.0",
Expand Down
Loading

0 comments on commit bb9edf9

Please sign in to comment.