From 2b5f78bacbbae30ab02f62d1e603816cadba03ee Mon Sep 17 00:00:00 2001 From: StephanGerbeth Date: Wed, 20 Nov 2024 14:34:54 +0100 Subject: [PATCH] fix(operators): marble testing --- package-lock.json | 21 ++++- packages/operators/package.json | 2 + packages/operators/src/log.js | 34 +++++-- .../src/request/autoPagination.test.js | 72 +++++++-------- packages/operators/src/request/cache.js | 4 +- packages/operators/src/request/cache.test.js | 46 ++++++++-- .../src/request/concurrentRequest.test.js | 43 ++++----- .../src/request/lazyPagination.test.js | 91 +++++++++---------- .../operators/src/request/polling.test.js | 73 +++++++++++++-- .../operators/src/request/request.test.js | 8 +- .../operators/src/request/response.test.js | 33 +++---- packages/operators/src/request/retry.test.js | 21 +++-- packages/test-utils/response.js | 5 + packages/test-utils/utils.js | 0 14 files changed, 292 insertions(+), 161 deletions(-) create mode 100644 packages/test-utils/response.js create mode 100644 packages/test-utils/utils.js diff --git a/package-lock.json b/package-lock.json index b8b3565..3067058 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2627,6 +2627,14 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", @@ -3298,6 +3306,14 @@ "dev": true, "license": "ISC" }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/conventional-changelog-angular": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.0.0.tgz", @@ -11047,7 +11063,6 @@ "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", "dev": true, - "license": "MIT", "dependencies": { "chalk": "^2.3.2", "figures": "^2.0.0", @@ -12599,10 +12614,12 @@ }, "packages/operators": { "name": "@rxjs-collection/operators", - "version": "1.0.5", + "version": "1.0.6-beta.1", "license": "MIT", "dependencies": { "@rxjs-collection/observables": "*", + "ansi-colors": "^4.1.3", + "consola": "^3.2.3", "fast-equals": "5.0.1", "rxjs": "7.8.1" }, diff --git a/packages/operators/package.json b/packages/operators/package.json index 39dae6e..fcf81e5 100644 --- a/packages/operators/package.json +++ b/packages/operators/package.json @@ -19,6 +19,8 @@ }, "dependencies": { "@rxjs-collection/observables": "*", + "ansi-colors": "^4.1.3", + "consola": "^3.2.3", "fast-equals": "5.0.1", "rxjs": "7.8.1" }, diff --git a/packages/operators/src/log.js b/packages/operators/src/log.js index 553bf54..bbc5802 100644 --- a/packages/operators/src/log.js +++ b/packages/operators/src/log.js @@ -1,20 +1,30 @@ -import { Observable } from 'rxjs'; +import { bgGreen } from 'ansi-colors'; +import debug from 'debug'; +import { connectable, finalize, Observable, Subject } from 'rxjs'; -export const log = (active = true) => { - if (active) { +export const enableLog = tag => { + debug.enable(tag); +}; + +export const log = tag => { + var logger = debug(tag); + logger.log = console.log.bind(console); + var error = debug(`${tag}:error`); + + if (debug.enabled(tag)) { return source => { return new Observable(observer => { return source.subscribe( val => { - console.log(val); + logger(val); observer.next(val); }, err => { - console.error(err); + error(err); observer.error(err); }, () => { - console.log('%ccomplete', 'color: green'); + logger(bgGreen.bold('Complete!')); observer.complete(); } ); @@ -24,3 +34,15 @@ export const log = (active = true) => { return source => source; } }; + +export const logResult = (tag, observable) => { + return new Promise(done => { + connectable( + observable.pipe( + log(tag), + finalize(() => done()) + ), + { connector: () => new Subject() } + ).connect(); + }); +}; diff --git a/packages/operators/src/request/autoPagination.test.js b/packages/operators/src/request/autoPagination.test.js index f9a8443..4601294 100644 --- a/packages/operators/src/request/autoPagination.test.js +++ b/packages/operators/src/request/autoPagination.test.js @@ -1,16 +1,16 @@ import { concatAll, concatMap, delay, from, map, of, toArray } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; -import { log } from '../log'; +import { log, logOutput, logResult } from '../log'; import { resolveJSON } from './response'; -describe('auto pagination - mocked', function () { +describe('auto pagination - mocked', () => { const testScheduler = new TestScheduler((actual, expected) => { expect(actual).to.eql(expected); }); - beforeEach(function () { + beforeEach(() => { vi.doMock('./request', importOriginal => ({ request: () => source => source.pipe(concatMap(({ v, t }) => of(v).pipe(delay(t)))) })); @@ -25,6 +25,10 @@ describe('auto pagination - mocked', function () { vi.doUnmock('./request'); }); + afterAll(() => { + vi.resetModules(); + }); + test('classic testing', async () => { const { autoPagination } = await import('./autoPagination'); @@ -76,47 +80,41 @@ describe('auto pagination - mocked', function () { autoPagination({ resolveRoute: (conf, resp) => ((!resp || resp.next) && [triggerVal[resp?.next || 'a']]) || [] - }) + }), + log('marble:result') ) ).toBe('---a----b--cd---e----', expectedVal); }); }); }); -describe.skip('auto pagination - demo', function () { - beforeEach(function () { - vi.resetModules(); - }); - - test('sample testing', async function () { +describe('auto pagination - demo', () => { + test('sample testing', async () => { const { autoPagination } = await import('./autoPagination'); - return new Promise(done => { - return of(new URL('https://dummyjson.com/products')) - .pipe( - autoPagination({ - resolveRoute: async (url, resp) => { - const data = (await resp?.json()) || { skip: -10, limit: 10 }; - - if (!data.total || data.total > data.skip + data.limit) { - const newUrl = new URL(`${url}`); - newUrl.searchParams.set('skip', data.skip + data.limit); - newUrl.searchParams.set('limit', data.limit); - newUrl.searchParams.set('select', 'title,price'); - return newUrl; - } + await logResult( + 'demo', + of(new URL('https://dummyjson.com/products')).pipe( + autoPagination({ + resolveRoute: async (url, resp) => { + const data = (await resp?.json()) || { skip: -10, limit: 10 }; + + if (!data.total || data.total > data.skip + data.limit) { + const newUrl = new URL(`${url}`); + newUrl.searchParams.set('skip', data.skip + data.limit); + newUrl.searchParams.set('limit', data.limit); + newUrl.searchParams.set('select', 'title,price'); + return newUrl; } - }), - log(false), - resolveJSON(), - log(false), - map(({ products }) => products), - concatAll() - ) - .subscribe({ - next: e => console.log(e), - complete: () => done() - }); - }); + } + }), + log('demo:response'), + resolveJSON(), + log('demo:response:json'), + map(({ products }) => products), + log('demo:response:result'), + concatAll() + ) + ); }); }); diff --git a/packages/operators/src/request/cache.js b/packages/operators/src/request/cache.js index d5300db..2f16c1b 100644 --- a/packages/operators/src/request/cache.js +++ b/packages/operators/src/request/cache.js @@ -4,11 +4,9 @@ export const cache = ttl => { return source => source.pipe( share({ - // TODO: check if a buffer size is neccessary connector: () => new ReplaySubject(), - // resetOnError: false, resetOnComplete: () => timer(ttl), - resetOnRefCountZero: false + resetOnRefCountZero: () => timer(ttl) }) ); }; diff --git a/packages/operators/src/request/cache.test.js b/packages/operators/src/request/cache.test.js index f2835ea..0969cb7 100644 --- a/packages/operators/src/request/cache.test.js +++ b/packages/operators/src/request/cache.test.js @@ -1,21 +1,49 @@ import fetchMock from 'fetch-mock'; -import { defer, delay, from, interval, map, mapTo, of, tap, throttleTime } from 'rxjs'; +import { defer, delay, map, of, tap } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { cache } from './cache'; import { requestText } from './request'; -describe('cache - mocked', function () { - beforeEach(function () { +describe('cache - mocked', () => { + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).deep.equal(expected); + }); + + beforeEach(() => { // }); - afterEach(function () { + afterEach(() => { // }); - test.skip('cache resetted after 100ms', async function () { + test('marble testing', () => { + const initial = new Response('initial', { status: 200 }); + const updated = new Response('updated', { status: 200 }); + const orderedResponses = [initial, updated]; + + testScheduler.run(({ cold, expectObservable }) => { + const stream = cold('a-----------', { + a: () => orderedResponses.shift() + }).pipe( + map(fn => fn()), + cache(2) + ); + + const unsubA = '-^!'; + expectObservable(stream, unsubA).toBe('-a', { a: initial }, new Error()); + + const unsubB = '----^!'; + expectObservable(stream, unsubB).toBe('----a', { a: initial }, new Error()); + + const unsubC = '---------^--!'; + expectObservable(stream, unsubC).toBe('---------a', { a: updated }, new Error()); + }); + }); + + test('cache resetted after 100ms', async () => { let counter = 0; const a = of(counter).pipe( tap(e => console.log('U', e)), @@ -39,8 +67,8 @@ describe('cache - mocked', function () { }); }); -describe('cache', function () { - beforeEach(function () { +describe('cache', () => { + beforeEach(() => { let counter = 0; fetchMock.mockGlobal().get( 'https://httpbin.org/my-url-fast', @@ -53,11 +81,11 @@ describe('cache', function () { ); }); - afterEach(function () { + afterEach(() => { fetchMock.unmockGlobal(); }); - test('cache resetted after 100ms', async function () { + test('cache resetted after 100ms', async () => { const a = of('https://httpbin.org/my-url-fast').pipe( requestText(), tap(() => console.log('CHECK')), diff --git a/packages/operators/src/request/concurrentRequest.test.js b/packages/operators/src/request/concurrentRequest.test.js index 8a3b7da..7aa6ac6 100644 --- a/packages/operators/src/request/concurrentRequest.test.js +++ b/packages/operators/src/request/concurrentRequest.test.js @@ -1,16 +1,16 @@ import { concatAll, concatMap, delay, from, map, of, tap, toArray } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; -import { log } from '../log'; +import { log, logResult } from '../log'; import { resolveJSON } from './response'; -describe('concurrent request - mocked', function () { +describe('concurrent request - mocked', () => { const testScheduler = new TestScheduler((actual, expected) => { expect(actual).to.eql(expected); }); - beforeEach(function () { + beforeEach(() => { vi.doMock('./request', importOriginal => ({ request: () => source => source.pipe(concatMap(({ v, t }) => of(v).pipe(delay(t)))) })); @@ -20,6 +20,10 @@ describe('concurrent request - mocked', function () { vi.doUnmock('./request'); }); + afterAll(function () { + vi.resetModules(); + }); + test('classic testing', async () => { const { concurrentRequest } = await import('./concurrentRequest'); @@ -63,15 +67,12 @@ describe('concurrent request - mocked', function () { }); }); -describe.skip('concurrent request - demo', function () { - beforeAll(function () { - vi.resetModules(); - }); - +describe('concurrent request - demo', () => { test('sample testing', async () => { const { concurrentRequest } = await import('./concurrentRequest'); - await new Promise(done => { + await logResult( + 'demo', of( new URL('https://dummyjson.com/products?limit=10&skip=0&select=title,price'), new URL('https://dummyjson.com/products?limit=10&skip=10&select=title,price'), @@ -82,19 +83,15 @@ describe.skip('concurrent request - demo', function () { new URL('https://dummyjson.com/products?limit=10&skip=60&select=title,price'), new URL('https://dummyjson.com/products?limit=10&skip=70&select=title,price'), new URL('https://dummyjson.com/products?limit=10&skip=80&select=title,price') + ).pipe( + concurrentRequest(4), + log('demo:response'), + resolveJSON(), + log('demo:response:json'), + map(({ products }) => products), + log('demo:response:result'), + concatAll() ) - .pipe( - concurrentRequest(4), - log(false), - resolveJSON(), - log(false), - map(({ products }) => products), - concatAll() - ) - .subscribe({ - next: e => console.log(e), - complete: () => done() - }); - }); + ); }); }); diff --git a/packages/operators/src/request/lazyPagination.test.js b/packages/operators/src/request/lazyPagination.test.js index 0980813..5167ba5 100644 --- a/packages/operators/src/request/lazyPagination.test.js +++ b/packages/operators/src/request/lazyPagination.test.js @@ -1,15 +1,15 @@ import { concatAll, concatMap, delay, map, of, Subject, tap } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; -import { log } from '../log'; +import { log, logResult } from '../log'; -describe('lazy pagination - mocked', function () { +describe('lazy pagination - mocked', () => { const testScheduler = new TestScheduler((actual, expected) => { expect(actual).to.eql(expected); }); - beforeEach(function () { + beforeEach(() => { vi.doMock('./request', importOriginal => ({ request: () => source => source.pipe(concatMap(({ v, t }) => of(v).pipe(delay(t)))) })); @@ -19,6 +19,10 @@ describe('lazy pagination - mocked', function () { vi.doUnmock('./request'); }); + afterAll(() => { + vi.resetModules(); + }); + test('classic testing', () => { // }); @@ -63,51 +67,46 @@ describe('lazy pagination - mocked', function () { }); }); -describe.skip('lazy pagination - demo', function () { - beforeAll(function () { - vi.resetModules(); - }); - - test('sample testing', async function () { +describe('lazy pagination - demo', () => { + test('sample testing', async () => { const { lazyPagination } = await import('./lazyPagination'); const { resolveJSON } = await import('./response'); + const pager = new Subject(); - return new Promise(done => { - of({ url: new URL('https://dummyjson.com/products') }) - .pipe( - lazyPagination({ - pager, - concurrent: 4, - resolveRoute: (url, { value, limit = 10 }) => { - const newUrl = new URL(`${url}`); - newUrl.searchParams.set('skip', value * limit); - newUrl.searchParams.set('limit', limit); - newUrl.searchParams.set('select', 'title,price'); - return newUrl; - } - }), - log(false), - resolveJSON(), - log(false), - map(({ products }) => products), - concatAll(), - log(false) - ) - .subscribe({ - next: e => console.log(e), - complete: () => done() - }); - - pager.next({ value: 2 }); - pager.next({ value: 3 }); - pager.next({ value: 12 }); - pager.next({ value: 5 }); - pager.next({ value: 6 }); - pager.next({ value: 7 }); - pager.next({ value: 8 }); - pager.next({ value: 9 }); - pager.complete(); - }); + const result = logResult( + 'demo', + of({ url: new URL('https://dummyjson.com/products') }).pipe( + lazyPagination({ + pager, + concurrent: 4, + resolveRoute: (url, { value, limit = 10 }) => { + const newUrl = new URL(`${url}`); + newUrl.searchParams.set('skip', value * limit); + newUrl.searchParams.set('limit', limit); + newUrl.searchParams.set('select', 'title,price'); + return newUrl; + } + }), + log('demo:response'), + resolveJSON(), + log('demo:response:json'), + map(({ products }) => products), + log('demo:response:result'), + concatAll() + ) + ); + + pager.next({ value: 2 }); + pager.next({ value: 3 }); + pager.next({ value: 12 }); + pager.next({ value: 5 }); + pager.next({ value: 6 }); + pager.next({ value: 7 }); + pager.next({ value: 8 }); + pager.next({ value: 9 }); + pager.complete(); + + await result; }); }); diff --git a/packages/operators/src/request/polling.test.js b/packages/operators/src/request/polling.test.js index 8904ce7..825e7f3 100644 --- a/packages/operators/src/request/polling.test.js +++ b/packages/operators/src/request/polling.test.js @@ -1,12 +1,72 @@ import fetchMock from 'fetch-mock'; -import { of, take } from 'rxjs'; -import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +import { concatMap, map, of, take, tap } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; +import { createResponse } from '../../../test-utils/response'; import { log } from '../log'; -import { polling } from './polling'; import { resolveJSON } from './response'; -describe('polling', function () { +describe('polling - mocked', () => { + let triggerVal; + let expectedArrayBuffer; + + const testScheduler = new TestScheduler((actual, expected) => { + expect(actual).to.eql(expected); + }); + + beforeEach(async () => { + triggerVal = [ + createResponse('https://example.com/', 'a'), + createResponse('https://example.com/', 'a'), + createResponse('https://example.com/', 'a'), + createResponse('https://example.com/', 'b'), + createResponse('https://example.com/', 'b'), + createResponse('https://example.com/', 'c'), + createResponse('https://example.com/', 'c') + ]; + + expectedArrayBuffer = await Promise.all(triggerVal.map(e => e.clone().arrayBuffer())); + + let counter = 0; + vi.doMock('./request', importOriginal => ({ + request: () => source => source.pipe(map(() => triggerVal[counter++])) + })); + + vi.spyOn(Response.prototype, 'arrayBuffer').mockImplementation(() => { + return of(expectedArrayBuffer[counter - 1]); + }); + }); + + afterEach(() => { + vi.doUnmock('./request'); + }); + + afterAll(() => { + vi.restoreAllMocks(); + vi.resetModules(); + }); + + test('marble testing', async () => { + const { polling } = await import('./polling'); + + testScheduler.run(({ cold, expectObservable }) => { + const stream = cold('a------------', { a: 'https://example.com/' }).pipe( + polling(2), + concatMap(e => e.arrayBuffer()) + ); + + const unsubA = '^------------!'; + expectObservable(stream, unsubA).toBe('a-----b---c--', { + a: expectedArrayBuffer[0], + b: expectedArrayBuffer[3], + c: expectedArrayBuffer[5] + }); + }); + }); +}); + +describe('classic testing', () => { beforeEach(function () { let counter = 0; fetchMock.mockGlobal().get('https://httpbin.org/my-url-fast', () => { @@ -23,11 +83,12 @@ describe('polling', function () { }); }); - afterEach(function () { + afterEach(() => { fetchMock.unmockGlobal(); }); - test('auto polling', async function () { + test('auto polling', async () => { + const { polling } = await import('./polling'); const expected = [{ hello: 'fast world' }, { hello: 'faster world' }]; return new Promise(done => { diff --git a/packages/operators/src/request/request.test.js b/packages/operators/src/request/request.test.js index 5c7c88f..295d367 100644 --- a/packages/operators/src/request/request.test.js +++ b/packages/operators/src/request/request.test.js @@ -7,7 +7,7 @@ import { log } from '../log.js'; import { request, requestJSON } from './request.js'; import { resolveJSON } from './response.js'; -describe('request observable with default ', function () { +describe('request observable with default ', () => { test('successfull upload', async () => { const formData = new FormData(); formData.set( @@ -42,8 +42,8 @@ describe('request observable with default ', function () { }); }); -describe('request observable with default operators', function () { - beforeEach(function () { +describe('request observable with default operators', () => { + beforeEach(() => { let counter = 0; fetchMock.mockGlobal().get( 'https://httpbin.org/my-url-fast', @@ -68,7 +68,7 @@ describe('request observable with default operators', function () { ); }); - afterEach(function () { + afterEach(() => { fetchMock.unmockGlobal(); }); diff --git a/packages/operators/src/request/response.test.js b/packages/operators/src/request/response.test.js index 54e1176..54e9d26 100644 --- a/packages/operators/src/request/response.test.js +++ b/packages/operators/src/request/response.test.js @@ -2,19 +2,20 @@ import { map, of, tap } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { afterEach, test, describe, beforeEach, expect, vi } from 'vitest'; +import { createResponse } from '../../../test-utils/response'; import { log } from '../log'; import { distinctUntilResponseChanged, resolveJSON, resolveText } from './response'; -describe('response', function () { +describe('response', () => { const testScheduler = new TestScheduler((actual, expected) => { expect(actual).to.eql(expected); }); - beforeEach(function () { + beforeEach(() => { // }); - afterEach(function () { + afterEach(() => { vi.restoreAllMocks(); }); @@ -65,14 +66,14 @@ describe('response', function () { test('marble testing', async () => { const triggerValues = { - a: createResponse('a', 'a'), - b: createResponse('b', 'a'), - c: createResponse('c', 'b'), - d: createResponse('d', 'b'), - e: createResponse('e', 'c'), - f: createResponse('f', 'a'), - g: createResponse('g', 'a'), - h: createResponse('h', 'b') + a: createResponse('/a', 'a'), + b: createResponse('/b', 'a'), + c: createResponse('/c', 'b'), + d: createResponse('/d', 'b'), + e: createResponse('/e', 'c'), + f: createResponse('/f', 'a'), + g: createResponse('/g', 'a'), + h: createResponse('/h', 'b') }; const expectedValues = Object.fromEntries( @@ -104,8 +105,8 @@ describe('response', function () { }); }); -const createResponse = (key, value) => { - const resp = new Response(value); - Object.defineProperty(resp, 'url', { value: `/${key}` }); - return resp; -}; +const pick = (obj, arr) => + arr.reduce( + (acc, record) => (record in obj && (acc[String(record)] = obj[String(record)]), acc), + {} + ); diff --git a/packages/operators/src/request/retry.test.js b/packages/operators/src/request/retry.test.js index b5551e1..4b8dd0d 100644 --- a/packages/operators/src/request/retry.test.js +++ b/packages/operators/src/request/retry.test.js @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test } from 'vitest'; import { networkRetry } from './retry'; -describe('request retry', function () { +describe('request retry', () => { const testScheduler = new TestScheduler((actual, expected) => { expect(actual).deep.equal(expected); }); @@ -27,14 +27,17 @@ describe('request retry', function () { const orderedResponses = [error, error, success]; testScheduler.run(({ cold, expectObservable }) => { - expectObservable( - cold('-a------', { - a: () => orderedResponses.shift() - }).pipe( - map(fn => fn()), - networkRetry({ timeout: () => 5 }) - ) - ).toBe('-------------a', { a: success }, new Error()); + // retry is repeating the sequence + // if you define a delay, you have to add the delay to the subscribe multiple times (num retries) + const stream = cold('a----------', { + a: () => orderedResponses.shift() + }).pipe( + map(fn => fn()), + networkRetry({ timeout: () => 5 }) + ); + + const unsubA = '^----------!'; + expectObservable(stream, unsubA).toBe('----------a', { a: success }, new Error()); }); }); }); diff --git a/packages/test-utils/response.js b/packages/test-utils/response.js new file mode 100644 index 0000000..04d6388 --- /dev/null +++ b/packages/test-utils/response.js @@ -0,0 +1,5 @@ +export const createResponse = (url, content, options = { status: 200 }) => { + const resp = new Response(content, options); + Object.defineProperty(resp, 'url', { value: url }); + return resp; +}; diff --git a/packages/test-utils/utils.js b/packages/test-utils/utils.js new file mode 100644 index 0000000..e69de29