├── babel.config.js ├── tsconfig.es6.json ├── rollup.config.js ├── src ├── types.ts ├── scheduler.ts ├── test-observables.ts ├── map-symbols-to-notifications.ts ├── marble-unparser.ts ├── utils.ts └── matcher.ts ├── README.md ├── tsconfig.json ├── .gitignore ├── LICENSE ├── spec ├── marble-unparser.spec.ts ├── map-symbols-to-notifications.spec.ts └── integration.spec.ts ├── .circleci └── config.yml ├── index.ts ├── package.json ├── jest.config.ts └── CHANGELOG.md /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [['@babel/preset-env', {targets: {node: 'current'}}], 4 | '@babel/preset-typescript', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "release/es6", 5 | "module": "es2015", 6 | "target": "es6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | input: './release/es6/index.js', 3 | output: { 4 | file: './release/bundles/jasmine-marbles.umd.js', 5 | name: 'jasmine-marbles', 6 | format: 'umd', 7 | globals: { 8 | 'rxjs': 'Rx', 9 | 'rxjs/testing': 'Rx', 10 | 'lodash': '_' 11 | }, 12 | }, 13 | external: [ 14 | 'rxjs', 15 | 'rxjs/testing', 16 | 'lodash' 17 | ], 18 | context: 'window' 19 | }; 20 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { TestScheduler } from 'rxjs/testing'; 2 | 3 | /** 4 | * Exported return type of TestMessage[] to avoid importing internal APIs. 5 | */ 6 | export type TestMessages = ReturnType; 7 | 8 | /** 9 | * Exported return type of SubscriptionLog to avoid importing internal APIs. 10 | */ 11 | export type SubscriptionLogs = ReturnType< 12 | typeof TestScheduler.parseMarblesAsSubscriptions 13 | >; 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jasmine-marbles 2 | 3 | Marble testing helpers for RxJS and Jasmine 4 | 5 | Helpful Links 6 | 7 | * [Marble Syntax](https://github.com/ReactiveX/rxjs/blob/master/apps/rxjs.dev/content/guide/testing/marble-testing.md#marble-syntax "ReactiveX Documentation") 8 | * [Providing Mock Actions for testing NgRx Actions with jasmine-marbles](https://ngrx.io/guide/effects/testing "NgRx Documentation") 9 | 10 | # Supported RxJS versions 11 | 12 | * 0.2.0 supports RxJS 5.x 13 | * => 0.3.* supports RxJS 6.x 14 | * => 0.9.0 supports RxJS 7.x 15 | -------------------------------------------------------------------------------- /src/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { TestScheduler } from 'rxjs/testing'; 2 | 3 | import { observableMatcher } from './matcher'; 4 | 5 | let scheduler: TestScheduler | null; 6 | 7 | export function initTestScheduler(): void { 8 | scheduler = new TestScheduler(observableMatcher); 9 | scheduler['runMode'] = true; 10 | } 11 | 12 | export function getTestScheduler(): TestScheduler { 13 | if (scheduler) { 14 | return scheduler; 15 | } 16 | 17 | throw new Error('No test scheduler initialized'); 18 | } 19 | 20 | export function resetTestScheduler(): void { 21 | scheduler = null; 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "declaration": true, 5 | "stripInternal": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "noEmitOnError": false, 11 | "outDir": "./release", 12 | "rootDir": ".", 13 | "sourceMap": true, 14 | "inlineSources": true, 15 | "lib": ["es2015", "dom"], 16 | "target": "es5", 17 | "skipLibCheck": true, 18 | "strictNullChecks": true, 19 | "strict": true 20 | }, 21 | "files": [ 22 | "index.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # IDE while developing the package 4 | .idea/ 5 | .settings/ 6 | modules/.settings 7 | .vscode 8 | modules/.vscode 9 | 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules 40 | jspm_packages 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional REPL history 46 | .node_repl_history 47 | 48 | # Build Output 49 | release 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Synapse Wireless Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test-observables.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs'; 2 | 3 | import { getTestScheduler } from './scheduler'; 4 | import { SubscriptionLogs } from './types'; 5 | 6 | export class TestColdObservable extends Observable { 7 | constructor( 8 | public marbles: string, 9 | public values?: { [name: string]: any }, 10 | public error?: any, 11 | ) { 12 | super(); 13 | 14 | const scheduler = getTestScheduler(); 15 | const cold = scheduler.createColdObservable(marbles, values, error); 16 | 17 | this.source = cold; 18 | } 19 | 20 | getSubscriptions(): SubscriptionLogs[] { 21 | return (this.source as any)['subscriptions']; 22 | } 23 | } 24 | 25 | export class TestHotObservable extends Observable { 26 | constructor( 27 | public marbles: string, 28 | public values?: { [name: string]: any }, 29 | public error?: any, 30 | ) { 31 | super(); 32 | 33 | const scheduler = getTestScheduler(); 34 | const hot = scheduler.createHotObservable(marbles, values, error); 35 | 36 | this.source = hot; 37 | } 38 | 39 | getSubscriptions(): SubscriptionLogs[] { 40 | return (this.source as any)['subscriptions']; 41 | } 42 | } 43 | 44 | export type TestObservable = TestColdObservable | TestHotObservable; 45 | -------------------------------------------------------------------------------- /src/map-symbols-to-notifications.ts: -------------------------------------------------------------------------------- 1 | import { ObservableNotification } from 'rxjs'; 2 | 3 | import { TestMessages } from './types'; 4 | 5 | export function mapSymbolsToNotifications( 6 | marbles: string, 7 | messagesArg: TestMessages, 8 | ): { [key: string]: ObservableNotification } { 9 | const messages = messagesArg.slice(); 10 | const result: { [key: string]: ObservableNotification } = {}; 11 | 12 | for (let i = 0; i < marbles.length; i++) { 13 | const symbol = marbles[i]; 14 | 15 | switch (symbol) { 16 | case ' ': 17 | case '-': 18 | case '^': 19 | case '(': 20 | case ')': 21 | break; 22 | case '#': 23 | case '|': { 24 | messages.shift(); 25 | break; 26 | } 27 | default: { 28 | if ((symbol.match(/^[0-9]$/) && i === 0) || marbles[i - 1] === ' ') { 29 | const buffer = marbles.slice(i); 30 | const match = buffer.match(/^([0-9]+(?:\.[0-9]+)?)(ms|s|m) /); 31 | if (match) { 32 | i += match[0].length - 1; 33 | } 34 | break; 35 | } 36 | 37 | const message = messages.shift()!; 38 | result[symbol] = message.notification; 39 | } 40 | } 41 | } 42 | 43 | return result; 44 | } 45 | -------------------------------------------------------------------------------- /spec/marble-unparser.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestScheduler } from 'rxjs/testing'; 2 | import { unparseMarble } from '../src/marble-unparser'; 3 | 4 | describe('Marble unparser', () => { 5 | describe('Basic unparsing with single frame symbol', () => { 6 | it('should unparse single frame', () => { 7 | expectToUnparse('a', 'a'); 8 | }); 9 | 10 | it('should respect empty frames', () => { 11 | expectToUnparse('---a-aaa--a', '---a-aaa--a'); 12 | }); 13 | 14 | it('should trim empty suffix frames', () => { 15 | expectToUnparse('---a-aaa--a----', '---a-aaa--a'); 16 | }); 17 | 18 | it('should support errors', () => { 19 | expectToUnparse('--a-#', '--a-#'); 20 | }); 21 | 22 | it('should support stream completion', () => { 23 | expectToUnparse('--a-|', '--a-|'); 24 | }); 25 | 26 | it('should support time progression', () => { 27 | expectToUnparse('- 20ms -a', '----a'); 28 | }); 29 | 30 | it('should support groups', () => { 31 | expectToUnparse('-(aa)--a', '-(aa)--a'); 32 | }); 33 | 34 | function expectToUnparse(sourceMarble: string, expectedMarble: string) { 35 | const testMessage = TestScheduler.parseMarbles( 36 | sourceMarble, 37 | { a: 1 }, 38 | undefined, 39 | true, 40 | true, 41 | ); 42 | 43 | expect(unparseMarble(testMessage, (n) => 'a')).toBe(expectedMarble); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # This file configures the build at https://circleci.com/gh/ngrx/platform 2 | 3 | # Opt-in to newer CircleCI system 4 | # Complete documentation is at https://circleci.com/docs/2.0/ 5 | version: 2.1 6 | 7 | # Note: YAML anchors allow an object to be re-used, reducing duplication. 8 | # The ampersand declares an alias for an object, then later the `<<: *name` 9 | # syntax dereferences it. 10 | # See https://blog.daemonl.com/2016/02/yaml.html 11 | # To validate changes, use an online parser, eg. 12 | # https://yaml-online-parser.appspot.com/ 13 | var_1: &cache_key yarn-cache-{{ checksum "yarn.lock" }}-0.9.0 14 | var_2: &run_in_node 15 | docker: 16 | - image: cimg/node:14.17.0 17 | 18 | jobs: 19 | install: 20 | <<: *run_in_node 21 | steps: 22 | - checkout 23 | - restore_cache: 24 | key: *cache_key 25 | - run: yarn --frozen-lockfile --non-interactive 26 | - save_cache: 27 | key: *cache_key 28 | paths: 29 | - ~/.cache/yarn 30 | - node_modules 31 | 32 | test_and_build: 33 | <<: *run_in_node 34 | steps: 35 | - checkout 36 | - restore_cache: 37 | key: *cache_key 38 | 39 | # Test w/jasmine as test runner 40 | - run: yarn test --testRunner jest-jasmine2 41 | # Test w/jest-circus as test runner 42 | - run: yarn test --testRunner jest-circus 43 | # Build 44 | - run: yarn build 45 | 46 | workflows: 47 | version: 2 48 | build-test: 49 | jobs: 50 | - install 51 | - test_and_build: 52 | requires: 53 | - install 54 | -------------------------------------------------------------------------------- /src/marble-unparser.ts: -------------------------------------------------------------------------------- 1 | import { ObservableNotification } from 'rxjs'; 2 | 3 | import { TestMessages } from './types'; 4 | 5 | export function unparseMarble( 6 | result: TestMessages, 7 | assignSymbolFn: (a: ObservableNotification) => string, 8 | ): string { 9 | const FRAME_TIME_FACTOR = 10; // need to be up to date with `TestScheduler.frameTimeFactor` 10 | let frames = 0; 11 | let marble = ''; 12 | let isInGroup = false; 13 | let groupMembersAmount = 0; 14 | let index = 0; 15 | 16 | const isNextMessageInTheSameFrame = () => { 17 | const nextMessage = result[index + 1]; 18 | return nextMessage && nextMessage.frame === result[index].frame; 19 | }; 20 | 21 | result.forEach((testMessage, i) => { 22 | index = i; 23 | 24 | const framesDiff = testMessage.frame - frames; 25 | const emptyFramesAmount = 26 | framesDiff > 0 ? framesDiff / FRAME_TIME_FACTOR : 0; 27 | marble += '-'.repeat(emptyFramesAmount); 28 | 29 | if (isNextMessageInTheSameFrame()) { 30 | if (!isInGroup) { 31 | marble += '('; 32 | } 33 | isInGroup = true; 34 | } 35 | 36 | switch (testMessage.notification.kind) { 37 | case 'N': 38 | marble += assignSymbolFn(testMessage.notification); 39 | break; 40 | case 'E': 41 | marble += '#'; 42 | break; 43 | case 'C': 44 | marble += '|'; 45 | break; 46 | } 47 | 48 | if (isInGroup) { 49 | groupMembersAmount += 1; 50 | } 51 | 52 | if (!isNextMessageInTheSameFrame() && isInGroup) { 53 | marble += ')'; 54 | isInGroup = false; 55 | frames += (groupMembersAmount + 1) * FRAME_TIME_FACTOR; 56 | groupMembersAmount = 0; 57 | } else { 58 | frames = testMessage.frame + FRAME_TIME_FACTOR; 59 | } 60 | }); 61 | 62 | return marble; 63 | } 64 | -------------------------------------------------------------------------------- /spec/map-symbols-to-notifications.spec.ts: -------------------------------------------------------------------------------- 1 | import { NextNotification, ObservableNotification } from 'rxjs'; 2 | import { TestScheduler } from 'rxjs/testing'; 3 | import { mapSymbolsToNotifications } from '../src/map-symbols-to-notifications'; 4 | 5 | function encodeSymbols( 6 | marbles: string, 7 | values: { [key: string]: any }, 8 | ): { [key: string]: ObservableNotification } { 9 | const expected = TestScheduler.parseMarbles( 10 | marbles, 11 | values, 12 | undefined, 13 | true, 14 | true, 15 | ); 16 | 17 | return mapSymbolsToNotifications(marbles, expected); 18 | } 19 | 20 | describe('Map symbols to frames', () => { 21 | it('should map single symbol', () => { 22 | const result = encodeSymbols('a', { a: 1 }); 23 | expect((result.a as NextNotification).value).toEqual(1); 24 | }); 25 | 26 | it('should map multiple symbols', () => { 27 | const result = encodeSymbols('---a--bc-d|', { a: 1, b: 2, c: 3, d: 4 }); 28 | expect((result.a as NextNotification).value).toEqual(1); 29 | expect((result.b as NextNotification).value).toEqual(2); 30 | expect((result.c as NextNotification).value).toEqual(3); 31 | expect((result.d as NextNotification).value).toEqual(4); 32 | }); 33 | 34 | it('should support groups', () => { 35 | const result = encodeSymbols('---(abc)--(aa)-|', { 36 | a: 1, 37 | b: 2, 38 | c: 3, 39 | d: 4, 40 | }); 41 | expect((result.a as NextNotification).value).toEqual(1); 42 | expect((result.b as NextNotification).value).toEqual(2); 43 | expect((result.c as NextNotification).value).toEqual(3); 44 | }); 45 | 46 | it('should support subscription point', () => { 47 | const result = encodeSymbols('---a-^-b-|', { a: 1, b: 2 }); 48 | expect((result.a as NextNotification).value).toEqual(1); 49 | expect((result.b as NextNotification).value).toEqual(2); 50 | }); 51 | 52 | it('should support time progression', () => { 53 | const result = encodeSymbols('-- 100ms -a-^-b-|', { a: 1, b: 2 }); 54 | expect((result.a as NextNotification).value).toEqual(1); 55 | expect((result.b as NextNotification).value).toEqual(2); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getTestScheduler, 3 | initTestScheduler, 4 | resetTestScheduler, 5 | } from './src/scheduler'; 6 | 7 | import { 8 | TestColdObservable, 9 | TestHotObservable, 10 | TestObservable, 11 | } from './src/test-observables'; 12 | 13 | import { 14 | toHaveSubscriptionsComparer, 15 | toBeObservableComparer, 16 | } from './src/utils'; 17 | 18 | export { 19 | getTestScheduler, 20 | initTestScheduler, 21 | resetTestScheduler, 22 | } from './src/scheduler'; 23 | 24 | export function hot( 25 | marbles: string, 26 | values?: any, 27 | error?: any, 28 | ): TestHotObservable { 29 | return new TestHotObservable(marbles.trim(), values, error); 30 | } 31 | 32 | export function cold( 33 | marbles: string, 34 | values?: any, 35 | error?: any, 36 | ): TestColdObservable { 37 | return new TestColdObservable(marbles.trim(), values, error); 38 | } 39 | 40 | export function time(marbles: string): number { 41 | return getTestScheduler().createTime(marbles.trim()); 42 | } 43 | 44 | declare global { 45 | namespace jasmine { 46 | interface Matchers { 47 | toBeObservable(expected: TestObservable): boolean; 48 | toHaveSubscriptions(marbles: string | string[]): boolean; 49 | } 50 | } 51 | namespace jest { 52 | interface Matchers { 53 | toBeObservable(expected: TestObservable): R; 54 | toHaveSubscriptions(marbles: string | string[]): R; 55 | } 56 | } 57 | } 58 | 59 | export function addMatchers() { 60 | /** 61 | * expect.extend is an API exposed by jest-circus, 62 | * the default runner as of Jest v27. If that method 63 | * is not available, assume we're in a Jasmine test 64 | * environment. 65 | */ 66 | if (!expect.extend) { 67 | jasmine.addMatchers({ 68 | toHaveSubscriptions: () => ({ 69 | compare: toHaveSubscriptionsComparer, 70 | }), 71 | toBeObservable: (_utils) => ({ 72 | compare: toBeObservableComparer, 73 | }), 74 | }); 75 | } else { 76 | expect.extend({ 77 | toHaveSubscriptions: toHaveSubscriptionsComparer, 78 | toBeObservable: toBeObservableComparer, 79 | }); 80 | } 81 | } 82 | 83 | export function setupEnvironment() { 84 | beforeAll(() => addMatchers()); 85 | 86 | beforeEach(() => initTestScheduler()); 87 | afterEach(() => { 88 | getTestScheduler().flush(); 89 | resetTestScheduler(); 90 | }); 91 | } 92 | 93 | setupEnvironment(); 94 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasmine-marbles", 3 | "description": "Marble testing helpers for RxJS and Jasmine", 4 | "keywords": [ 5 | "jasmine", 6 | "rxjs", 7 | "ngrx", 8 | "testing" 9 | ], 10 | "version": "0.9.2", 11 | "module": "index.js", 12 | "es2015": "es6/index.js", 13 | "main": "bundles/jasmine-marbles.umd.js", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/synapse-wireless-labs/jasmine-marbles.git" 17 | }, 18 | "homepage": "https://github.com/synapse-wireless-labs/jasmine-marbles#readme", 19 | "bugs": "https://github.com/synapse-wireless-labs/jasmine-marbles/issues", 20 | "license": "MIT", 21 | "scripts": { 22 | "clean": "rimraf release", 23 | "build:ts": "tsc", 24 | "build:es6": "tsc -p tsconfig.es6.json", 25 | "build:docs": "cpy LICENSE package.json README.md release", 26 | "prebuild": "npm run clean", 27 | "build": "npm run build:ts && npm run build:umd && npm run build:docs", 28 | "postversion": "npm run build", 29 | "build:umd": "npm run build:es6 && rollup -c rollup.config.js", 30 | "test": "jest", 31 | "precommit": "yarn run prettier", 32 | "prettier": "prettier --parser typescript --single-quote --trailing-comma all --write \"./**/*.ts\"", 33 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 34 | }, 35 | "nyc": { 36 | "extension": [ 37 | ".ts" 38 | ], 39 | "exclude": [ 40 | "**/*.spec", 41 | "**/spec/**/*" 42 | ], 43 | "include": [ 44 | "src/**/*.ts" 45 | ], 46 | "reporter": [ 47 | "text-summary", 48 | "html" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "@babel/core": "^7.14.6", 53 | "@babel/preset-env": "^7.14.7", 54 | "@babel/preset-typescript": "^7.14.5", 55 | "@types/jest": "^26.0.23", 56 | "@types/lodash": "^4.14.106", 57 | "@types/node": "^14.17.0", 58 | "babel-jest": "^27.0.6", 59 | "conventional-changelog": "^3.1.12", 60 | "conventional-changelog-cli": "^2.0.25", 61 | "cpy-cli": "^1.0.1", 62 | "husky": "^0.14.3", 63 | "jasmine": "^3.7.0", 64 | "jest": "^27.0.6", 65 | "jest-jasmine2": "^27.0.6", 66 | "nyc": "^15.1.0", 67 | "prettier": "^2.3.0", 68 | "rimraf": "^2.6.1", 69 | "rollup": "^2.49.0", 70 | "rxjs": "^7.0.0", 71 | "ts-node": "^9.1.1", 72 | "typescript": "^4.2.0" 73 | }, 74 | "peerDependencies": { 75 | "rxjs": "^7.0.0" 76 | }, 77 | "dependencies": { 78 | "lodash": "^4.17.20" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /spec/integration.spec.ts: -------------------------------------------------------------------------------- 1 | import { tap, mapTo } from 'rxjs/operators'; 2 | 3 | import { of, timer, Subject, concat } from 'rxjs'; 4 | 5 | import { 6 | cold, 7 | getTestScheduler, 8 | hot, 9 | initTestScheduler, 10 | resetTestScheduler, 11 | time, 12 | } from '../index'; 13 | import { TestColdObservable } from '../src/test-observables'; 14 | 15 | describe('Integration', () => { 16 | it('should work with a cold observable', () => { 17 | const provided = of(1); 18 | 19 | const expected = cold('(b|)', { b: 1 }); 20 | 21 | expect(provided).toBeObservable(expected); 22 | }); 23 | 24 | it('should work with a hot observable', () => { 25 | const provided = new Subject(); 26 | 27 | const expected = hot('--a--b', { a: 1, b: 2 }); 28 | 29 | expect(expected.pipe(tap((v) => provided.next(v)))).toBeObservable( 30 | expected, 31 | ); 32 | }); 33 | 34 | it('should trim spaces of marble string, if any', () => { 35 | const source = hot(' -a-^(bc)-| '); 36 | const expected = cold(' -(bc)-| '); 37 | 38 | expect(source).toBeObservable(expected); 39 | }); 40 | 41 | it('should support testing subscriptions on hot observable', () => { 42 | const source = hot('-a-^b---c-|'); 43 | const expected = cold('-b---c-|'); 44 | const subscription = '^------!'; 45 | 46 | expect(source).toBeObservable(expected); 47 | expect(source).toHaveSubscriptions(subscription); 48 | }); 49 | 50 | it('should identify subscription points', () => { 51 | const obs1 = cold('-a---b-|'); 52 | const obs2 = cold('-c---d-|'); 53 | const expected = cold('-a---b--c---d-|'); 54 | const sub1 = '^------!'; 55 | const sub2 = '-------^------!'; 56 | 57 | expect(concat(obs1, obs2)).toBeObservable(expected); 58 | expect(obs1).toHaveSubscriptions(sub1); 59 | expect(obs2).toHaveSubscriptions(sub2); 60 | }); 61 | 62 | it('should work with the test scheduler', () => { 63 | const delay = time('-----a|'); 64 | const val = 1; 65 | const provided = timer(delay, getTestScheduler()).pipe(mapTo(val)); 66 | 67 | const expected = hot('------(a|)', { a: val }); 68 | 69 | expect(provided).toBeObservable(expected); 70 | }); 71 | 72 | it('should throw if the TestScheduler is not initialized', () => { 73 | resetTestScheduler(); 74 | 75 | try { 76 | getTestScheduler(); 77 | } catch (err) { 78 | expect(err).toEqual(new Error('No test scheduler initialized')); 79 | } 80 | 81 | initTestScheduler(); 82 | }); 83 | 84 | it('should support time progression syntax for hot', () => { 85 | const provided = timer(100, getTestScheduler()).pipe(mapTo('a')); 86 | const expected = hot('100ms (a|)'); 87 | 88 | expect(provided).toBeObservable(expected); 89 | }); 90 | 91 | it('should support time progression syntax for cold', () => { 92 | const provided = timer(100, getTestScheduler()).pipe(mapTo('a')); 93 | const expected = cold('100ms (a|)'); 94 | 95 | expect(provided).toBeObservable(expected); 96 | }); 97 | 98 | it('should support TestScheduler.run()', () => { 99 | const scheduler = getTestScheduler(); 100 | 101 | scheduler.run(({ expectObservable }) => { 102 | const delay = time('-----a|'); 103 | const val = 1; 104 | const provided = timer(delay, scheduler).pipe(mapTo(val)); 105 | 106 | expectObservable(provided).toBe('------(a|)', { a: val }); 107 | }); 108 | }); 109 | 110 | it('should support "not.toBeObservable"', () => { 111 | const provided = of(1); 112 | 113 | const expected = cold('(b|)', { b: 2 }); 114 | 115 | expect(provided).not.toBeObservable(expected); 116 | }); 117 | 118 | it('should support undefined values', () => { 119 | const provided = of({ myValue: 1, someOtherVal: undefined }); 120 | 121 | const expected = cold('(b|)', { b: { myValue: 1 } }); 122 | 123 | expect(provided).toBeObservable(expected); 124 | }); 125 | 126 | it('should support jasmine.anything()', () => { 127 | if ((globalThis as any).jasmine) { 128 | const provided = cold('a', { a: { someProp: 3 } }); 129 | const expected = cold('a', { a: jasmine.anything() }); 130 | 131 | expect(provided).toBeObservable(expected); 132 | } 133 | }); 134 | 135 | it('should support expect.any()', () => { 136 | if (!(globalThis as any).jasmine) { 137 | const provided = cold('a', { a: { someProp: 'message' } }); 138 | const expected = cold('a', { a: { someProp: expect.any(String) } }); 139 | 140 | expect(provided).toBeObservable(expected); 141 | } 142 | }); 143 | 144 | it('should support objectContaining()', () => { 145 | const provided = cold('a', { 146 | a: { foo: 'bar', value: '1', type: 'myType' }, 147 | }); 148 | let expected: TestColdObservable; 149 | 150 | if ((globalThis as any).jasmine) { 151 | expected = cold('b', { 152 | b: jasmine.objectContaining({ value: '1', type: 'myType' }), 153 | }); 154 | } else { 155 | expected = cold('b', { 156 | b: expect.objectContaining({ value: '1', type: 'myType' }), 157 | }); 158 | } 159 | 160 | expect(provided).toBeObservable(expected); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CompleteNotification, 3 | ErrorNotification, 4 | NextNotification, 5 | Observable, 6 | ObservableNotification, 7 | Subscription, 8 | } from 'rxjs'; 9 | import { TestScheduler } from 'rxjs/testing'; 10 | import { isEqual } from 'lodash'; 11 | import { getTestScheduler } from './scheduler'; 12 | import { TestObservable } from './test-observables'; 13 | import { mapSymbolsToNotifications } from './map-symbols-to-notifications'; 14 | import { TestMessages } from './types'; 15 | import { unparseMarble } from './marble-unparser'; 16 | 17 | /* 18 | * Based on source code found in rxjs library 19 | * https://github.com/ReactiveX/rxjs/blob/master/src/testing/TestScheduler.ts 20 | * 21 | */ 22 | export function materializeInnerObservable( 23 | observable: Observable, 24 | outerFrame: number, 25 | ): TestMessages { 26 | const messages: TestMessages = []; 27 | const scheduler = getTestScheduler(); 28 | 29 | observable.subscribe({ 30 | next: (value: any) => { 31 | messages.push({ 32 | frame: scheduler.frame - outerFrame, 33 | notification: { 34 | kind: 'N', 35 | value, 36 | error: undefined, 37 | } as NextNotification, 38 | }); 39 | }, 40 | error: (error: any) => { 41 | messages.push({ 42 | frame: scheduler.frame - outerFrame, 43 | notification: { 44 | kind: 'E', 45 | value: undefined, 46 | error, 47 | } as ErrorNotification, 48 | }); 49 | }, 50 | complete: () => { 51 | messages.push({ 52 | frame: scheduler.frame - outerFrame, 53 | notification: { 54 | kind: 'C', 55 | value: undefined, 56 | error: undefined, 57 | } as CompleteNotification, 58 | }); 59 | }, 60 | }); 61 | return messages; 62 | } 63 | 64 | export function toHaveSubscriptionsComparer( 65 | actual: TestObservable, 66 | marbles: string | string[], 67 | ) { 68 | const marblesArray: string[] = 69 | typeof marbles === 'string' ? [marbles] : marbles; 70 | const results = marblesArray.map((marbles) => 71 | TestScheduler.parseMarblesAsSubscriptions(marbles), 72 | ); 73 | 74 | expect(results).toEqual(actual.getSubscriptions()); 75 | 76 | return { pass: true, message: () => '' }; 77 | } 78 | 79 | export function toBeObservableComparer( 80 | actual: TestObservable, 81 | fixture: TestObservable, 82 | ) { 83 | const results: TestMessages = []; 84 | let subscription: Subscription; 85 | const scheduler = getTestScheduler(); 86 | 87 | scheduler.schedule(() => { 88 | subscription = actual.subscribe({ 89 | next: (x: any) => { 90 | let value = x; 91 | 92 | // Support Observable-of-Observables 93 | if (x instanceof Observable) { 94 | value = materializeInnerObservable(value, scheduler.frame); 95 | } 96 | 97 | results.push({ 98 | frame: scheduler.frame, 99 | notification: { 100 | kind: 'N', 101 | value, 102 | error: undefined, 103 | } as NextNotification, 104 | }); 105 | }, 106 | error: (error: any) => { 107 | results.push({ 108 | frame: scheduler.frame, 109 | notification: { 110 | kind: 'E', 111 | value: undefined, 112 | error, 113 | } as ErrorNotification, 114 | }); 115 | }, 116 | complete: () => { 117 | results.push({ 118 | frame: scheduler.frame, 119 | notification: { 120 | kind: 'C', 121 | value: undefined, 122 | error: undefined, 123 | } as CompleteNotification, 124 | }); 125 | }, 126 | }); 127 | }); 128 | scheduler.flush(); 129 | 130 | const expected = TestScheduler.parseMarbles( 131 | fixture.marbles, 132 | fixture.values, 133 | fixture.error, 134 | true, 135 | true, 136 | ); 137 | 138 | try { 139 | expect(results).toEqual(expected); 140 | 141 | return { pass: true, message: () => '' }; 142 | } catch (e) { 143 | const mapNotificationToSymbol = buildNotificationToSymbolMapper( 144 | fixture.marbles, 145 | expected, 146 | isEqual, 147 | ); 148 | const receivedMarble = unparseMarble(results, mapNotificationToSymbol); 149 | 150 | const message = formatMessage( 151 | fixture.marbles, 152 | expected, 153 | receivedMarble, 154 | results, 155 | ); 156 | return { pass: false, message: () => message }; 157 | } 158 | } 159 | 160 | function buildNotificationToSymbolMapper( 161 | expectedMarbles: string, 162 | expectedMessages: TestMessages, 163 | equalityFn: (a: any, b: any) => boolean, 164 | ) { 165 | const symbolsToNotificationsMap = mapSymbolsToNotifications( 166 | expectedMarbles, 167 | expectedMessages, 168 | ); 169 | return (notification: ObservableNotification) => { 170 | const mapped = Object.keys(symbolsToNotificationsMap).find((key) => 171 | equalityFn(symbolsToNotificationsMap[key], notification), 172 | )!; 173 | 174 | return mapped || '?'; 175 | }; 176 | } 177 | 178 | function formatMessage( 179 | expectedMarbles: string, 180 | expectedMessages: TestMessages, 181 | receivedMarbles: string, 182 | receivedMessages: TestMessages, 183 | ) { 184 | return ` 185 | Expected: ${expectedMarbles}, 186 | Received: ${receivedMarbles}, 187 | 188 | Expected: 189 | ${JSON.stringify(expectedMessages)} 190 | 191 | Received: 192 | ${JSON.stringify(receivedMessages)}, 193 | `; 194 | } 195 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property and type check, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | export default { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | // bail: 0, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/mx/dk2_v5y11kdbds_st23dbxkr0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls and instances between every test 17 | // clearMocks: false, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | // collectCoverage: false, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | // coverageDirectory: undefined, 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | // coveragePathIgnorePatterns: [ 30 | // "/node_modules/" 31 | // ], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // Force coverage collection from ignored files using an array of glob patterns 54 | // forceCoverageMatch: [], 55 | 56 | // A path to a module which exports an async function that is triggered once before all test suites 57 | // globalSetup: undefined, 58 | 59 | // A path to a module which exports an async function that is triggered once after all test suites 60 | // globalTeardown: undefined, 61 | 62 | // A set of global variables that need to be available in all test environments 63 | // globals: {}, 64 | 65 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 66 | // maxWorkers: "50%", 67 | 68 | // An array of directory names to be searched recursively up from the requiring module's location 69 | // moduleDirectories: [ 70 | // "node_modules" 71 | // ], 72 | 73 | // An array of file extensions your modules use 74 | // moduleFileExtensions: [ 75 | // "js", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | // moduleNameMapper: {}, 85 | 86 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 87 | // modulePathIgnorePatterns: [], 88 | 89 | // Activates notifications for test results 90 | // notify: false, 91 | 92 | // An enum that specifies notification mode. Requires { notify: true } 93 | // notifyMode: "failure-change", 94 | 95 | // A preset that is used as a base for Jest's configuration 96 | // preset: undefined, 97 | 98 | // Run tests from one or more projects 99 | // projects: undefined, 100 | 101 | // Use this configuration option to add custom reporters to Jest 102 | // reporters: undefined, 103 | 104 | // Automatically reset mock state between every test 105 | // resetMocks: false, 106 | 107 | // Reset the module registry before running each individual test 108 | // resetModules: false, 109 | 110 | // A path to a custom resolver 111 | // resolver: undefined, 112 | 113 | // Automatically restore mock state between every test 114 | // restoreMocks: false, 115 | 116 | // The root directory that Jest should scan for tests and modules within 117 | // rootDir: undefined, 118 | 119 | // A list of paths to directories that Jest should use to search for files in 120 | // roots: [ 121 | // "" 122 | // ], 123 | 124 | // Allows you to use a custom runner instead of Jest's default test runner 125 | // runner: "jest-runner", 126 | 127 | // The paths to modules that run some code to configure or set up the testing environment before each test 128 | // setupFiles: [], 129 | 130 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 131 | // setupFilesAfterEnv: [], 132 | 133 | // The number of seconds after which a test is considered as slow and reported as such in the results. 134 | // slowTestThreshold: 5, 135 | 136 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 137 | // snapshotSerializers: [], 138 | 139 | // The test environment that will be used for testing 140 | // testEnvironment: "jest-environment-node", 141 | 142 | // Options that will be passed to the testEnvironment 143 | // testEnvironmentOptions: {}, 144 | 145 | // Adds a location field to test results 146 | // testLocationInResults: false, 147 | 148 | // The glob patterns Jest uses to detect test files 149 | // testMatch: [ 150 | // "**/__tests__/**/*.[jt]s?(x)", 151 | // "**/?(*.)+(spec|test).[tj]s?(x)" 152 | // ], 153 | 154 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 155 | // testPathIgnorePatterns: [ 156 | // "/node_modules/" 157 | // ], 158 | 159 | // The regexp pattern or array of patterns that Jest uses to detect test files 160 | // testRegex: [], 161 | 162 | // This option allows the use of a custom results processor 163 | // testResultsProcessor: undefined, 164 | 165 | // This option allows use of a custom test runner 166 | testRunner: 'jest-jasmine2', 167 | 168 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 169 | // testURL: "http://localhost", 170 | 171 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 172 | // timers: "real", 173 | 174 | // A map from regular expressions to paths to transformers 175 | // transform: undefined, 176 | 177 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 178 | // transformIgnorePatterns: [ 179 | // "/node_modules/", 180 | // "\\.pnp\\.[^\\/]+$" 181 | // ], 182 | 183 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 184 | // unmockedModulePathPatterns: undefined, 185 | 186 | // Indicates whether each individual test should be reported during the run 187 | // verbose: undefined, 188 | 189 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 190 | // watchPathIgnorePatterns: [], 191 | 192 | // Whether to use watchman for file crawling 193 | // watchman: true, 194 | }; 195 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.9.2](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.9.1...0.9.2) (2022-03-02) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * remove equalityTested to avoid deprecation error with jasmine 4.0 ([#91](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/91)) ([06b849f](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/06b849fb25d147e0fb5e44d2003993ac51a20919)), closes [#89](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/89) 7 | 8 | 9 | 10 | ## [0.9.1](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.10.0-beta.0...0.9.1) (2021-09-28) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * fix comparison for undefined properties and value matchers with Jasmine and Jest ([#87](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/87)) ([75f70c0](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/75f70c0faa37f0e84adcd450a17c0f13a627ad75)) 16 | 17 | 18 | 19 | # [0.10.0-beta.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.9.0...0.10.0-beta.0) (2021-07-01) 20 | 21 | 22 | ### Features 23 | 24 | * add support for jest-circus ([#78](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/78)) ([bb9edf9](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/bb9edf9e7b0de7c6ca35da3995124d3180441dbd)) 25 | 26 | 27 | 28 | # [0.9.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.9.0-beta.0...0.9.0) (2021-06-16) 29 | 30 | 31 | 32 | # [0.9.0-beta.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.8.1...0.9.0-beta.0) (2021-06-10) 33 | 34 | 35 | ### Features 36 | 37 | * update RxJS dependencies to 7.x.x ([#67](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/67)) ([a7fb790](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/a7fb7904476f07199f3cc8fb9f1cac5a6133a8e8)) 38 | 39 | 40 | ### BREAKING CHANGES 41 | 42 | * Minimum dependencies for TypeScript and RxJS have been updated. 43 | 44 | BEFORE: 45 | 46 | Minimum dependencies: 47 | TypeScript 4.1.x 48 | RxJS 6.5.x 49 | 50 | AFTER: 51 | 52 | Minimum dependencies: 53 | TypeScript 4.2.x 54 | RxJS 7.x.x 55 | 56 | 57 | 58 | ## [0.8.1](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.8.0...0.8.1) (2021-02-18) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * use lodash for internal isEqual check ([#65](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/65)) ([76bd638](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/76bd63898b7a3debe166a886c517c696bb91218d)) 64 | 65 | 66 | 67 | # [0.8.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/0.7.0...0.8.0) (2021-02-18) 68 | 69 | 70 | ### Code Refactoring 71 | 72 | * remove usage of internal RxJS testing APIs ([#64](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/64)) ([fb4d603](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/fb4d603fc28e634c88d404221bb003059d7c9557)), closes [#39](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/39) [#44](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/44) 73 | 74 | 75 | ### BREAKING CHANGES 76 | 77 | * RxJS minimum peer dependency has been updated 78 | 79 | BEFORE: 80 | 81 | RxJS minimum peer dependency is ^6.4.0 82 | 83 | AFTER: 84 | 85 | RxJS minimum peer dependency is ^6.5.3 86 | 87 | 88 | 89 | # [0.7.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.6.0...0.7.0) (2021-02-18) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * remove warning for spec with no expectations ([#60](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/60)) ([aa28304](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/aa2830465a9e0a890c6a7a080a2e902ea7650144)), closes [#59](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/59) 95 | 96 | 97 | ### Features 98 | 99 | * add better error reporting/tests/bug fixes ([#55](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/55)) ([a1fca8d](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/a1fca8d53505f5e74a79f48d02d9dbda46e4a5d1)), closes [#51](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/51) [#11](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/11) 100 | * add proper types for matchers ([#58](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/58)) ([89bf847](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/89bf847d1ea8ecb2be172b452376262b0a4fb64a)) 101 | 102 | 103 | 104 | # [0.6.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.5.0...v0.6.0) (2019-10-19) 105 | 106 | 107 | ### Features 108 | 109 | * export environment setup function for jasmine and remove module exports check ([#41](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/41)) ([b57472a](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/b57472adeba487474203d2432862faf17920a835)), closes [#21](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/21) [#37](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/37) [#40](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/40) 110 | 111 | 112 | 113 | # [0.5.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.4.1...v0.5.0) (2019-04-15) 114 | 115 | 116 | ### chore 117 | 118 | * update RxJS dependencies to 6.4.x ([d530594](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/d5305941a2651a37d4812a6026b7592bd5e6307d)) 119 | 120 | 121 | ### Features 122 | 123 | * add support for time progression syntax ([#38](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/38)) ([2f28eb3](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/2f28eb345fdda218dfaf42584922600161953a44)), closes [#30](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/30) 124 | 125 | 126 | ### BREAKING CHANGES 127 | 128 | * Minimum dependency on RxJS is now 6.4.x 129 | 130 | 131 | 132 | ## [0.4.1](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.4.0...v0.4.1) (2018-11-28) 133 | 134 | 135 | ### Bug Fixes 136 | 137 | * revert use of browser entry point ([#35](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/35)) ([7a342b2](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/7a342b23142ce7609be544feda32affd602f1e4c)) 138 | * update types for jest.Matchers ([#36](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/36)) ([939332b](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/939332bca02af7c8c9b7e973f0a6670904d1abaa)), closes [#28](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/28) 139 | 140 | 141 | 142 | # [0.4.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.3.0...v0.4.0) (2018-09-13) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * **lib:** Change script for main entry point ([#18](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/18)) ([f3f3f74](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/f3f3f74fd3a8ef08d3d80c1610a68cabd83d0984)) 148 | 149 | 150 | ### Features 151 | 152 | * Add typings support for jest namespace ([#29](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/29)) ([2bc640e](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/2bc640e73042d0417a6ca967081b9d78a799ebbc)) 153 | * trim marbles string ([#26](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/26)) ([6eb255b](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/6eb255b164021f32991532e490c9ac93a0b59b38)), closes [#3](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/3) 154 | 155 | 156 | 157 | # [0.3.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.2.0...v0.3.0) (2018-03-27) 158 | 159 | 160 | ### Features 161 | 162 | * **deps:** Add support for RxJS 6 ([8085938](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/80859387191bf068fa8f5e08c888e9d8ec7616b8)) 163 | * **subscriptions:** add ability to test subscriptions ([f6f06ee](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/f6f06eeb24ed1c1a66ad39ce1318ab64e24ee6ee)) 164 | 165 | 166 | ### BREAKING CHANGES 167 | 168 | * **deps:** Update peer dependency and import paths to RxJS v6 169 | 170 | BEFORE: 171 | Depends on RxJS ~5.0.0 172 | 173 | AFTER: 174 | Depends on RxJS ~6.0.0 175 | 176 | 177 | 178 | # [0.2.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.1.0...v0.2.0) (2017-10-09) 179 | 180 | 181 | ### Features 182 | 183 | * **matcher:** Update comparative output for observable matcher ([#7](https://github.com/synapse-wireless-labs/jasmine-marbles/issues/7)) ([31f6b76](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/31f6b76667d0ee66b53ab1941e5e128549f2ca3d)) 184 | 185 | 186 | 187 | # [0.1.0](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.0.2...v0.1.0) (2017-07-18) 188 | 189 | 190 | 191 | ## [0.0.2](https://github.com/synapse-wireless-labs/jasmine-marbles/compare/v0.0.1...v0.0.2) (2017-02-27) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * Release with CommonJS modules ([4e5358c](https://github.com/synapse-wireless-labs/jasmine-marbles/commit/4e5358c1344423fc4a2ae5537034dc6d23fd35e3)) 197 | 198 | 199 | 200 | ## 0.0.1 (2017-02-27) 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /src/matcher.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | 3 | /** 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright 2015-2016 Netflix, Inc., Microsoft Corp. and contributors 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | */ 206 | 207 | import { isEqual } from 'lodash'; 208 | 209 | /** 210 | * @see https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/observableMatcher.ts 211 | */ 212 | function stringify(x: any): string { 213 | return JSON.stringify(x, function (_key: string, value: any) { 214 | if (Array.isArray(value)) { 215 | return ( 216 | '[' + 217 | value.map(function (i) { 218 | return '\n\t' + stringify(i); 219 | }) + 220 | '\n]' 221 | ); 222 | } 223 | return value; 224 | }) 225 | .replace(/\\"/g, '"') 226 | .replace(/\\t/g, '\t') 227 | .replace(/\\n/g, '\n'); 228 | } 229 | 230 | /** 231 | * @see https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/observableMatcher.ts 232 | */ 233 | function deleteErrorNotificationStack(marble: any) { 234 | const { notification } = marble; 235 | if (notification) { 236 | const { kind, error } = notification; 237 | if (kind === 'E' && error instanceof Error) { 238 | notification.error = { name: error.name, message: error.message }; 239 | } 240 | } 241 | return marble; 242 | } 243 | 244 | /** 245 | * @see https://github.com/ReactiveX/rxjs/blob/master/spec/helpers/observableMatcher.ts 246 | */ 247 | export function observableMatcher(actual: any, expected: any): void { 248 | if (Array.isArray(actual) && Array.isArray(expected)) { 249 | actual = actual.map(deleteErrorNotificationStack); 250 | expected = expected.map(deleteErrorNotificationStack); 251 | const passed = isEqual(actual, expected); 252 | if (passed) { 253 | expect(passed).toBe(true); 254 | return; 255 | } 256 | 257 | let message = '\nExpected \n'; 258 | actual.forEach((x: any) => (message += `\t${stringify(x)}\n`)); 259 | 260 | message += '\t\nto deep equal \n'; 261 | expected.forEach((x: any) => (message += `\t${stringify(x)}\n`)); 262 | 263 | fail(message); 264 | } else { 265 | expect(actual).toEqual(expected); 266 | } 267 | } 268 | --------------------------------------------------------------------------------