chore: init repository
This commit is contained in:
commit
697b09b8d4
77
.drone.yml
Normal file
77
.drone.yml
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: default
|
||||
|
||||
trigger:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
steps:
|
||||
- name: install_and_build
|
||||
image: node:18
|
||||
commands:
|
||||
- npm install
|
||||
- npm run build
|
||||
|
||||
- name: lint
|
||||
image: node:18
|
||||
commands:
|
||||
- npm run lint
|
||||
depends_on:
|
||||
- install_and_build
|
||||
|
||||
- name: prettier
|
||||
image: node:18
|
||||
commands:
|
||||
- npm run prettier:check
|
||||
depends_on:
|
||||
- install_and_build
|
||||
|
||||
- name: jest-unit-test
|
||||
image: node:18
|
||||
commands:
|
||||
- npm run test
|
||||
depends_on:
|
||||
- install_and_build
|
||||
|
||||
- name: cucumber-functional-test
|
||||
image: node:18
|
||||
commands:
|
||||
- npm run cucumber:prepare
|
||||
- npm run cucumber:run
|
||||
depends_on:
|
||||
- install_and_build
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: release
|
||||
|
||||
depends_on:
|
||||
- default
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- master
|
||||
- next
|
||||
|
||||
steps:
|
||||
- name: install_and_build
|
||||
image: node:18
|
||||
commands:
|
||||
- npm install
|
||||
- npm run build
|
||||
- name: semantic_release
|
||||
image: node:18
|
||||
environment:
|
||||
NPM_TOKEN:
|
||||
from_secret: npm_token
|
||||
GITEA_TOKEN:
|
||||
from_secret: gitea_token
|
||||
GITEA_URL: https://git.trapcodien.com
|
||||
commands:
|
||||
- npm run release
|
||||
depends_on:
|
||||
- install_and_build
|
||||
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@ -0,0 +1 @@
|
||||
/dist/
|
||||
107
.gitignore
vendored
Normal file
107
.gitignore
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Parcel
|
||||
/.parcel-cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
1
.npmrc
Normal file
1
.npmrc
Normal file
@ -0,0 +1 @@
|
||||
@garm:registry=https://git.trapcodien.com/api/packages/garm/npm/
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@ -0,0 +1 @@
|
||||
/dist/
|
||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
||||
"@trapcodien/prettier-config"
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 garm
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
14
cucumber.js
Normal file
14
cucumber.js
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
default: [
|
||||
// `--format-options '{"snippetInterface": "synchronous"}'`,
|
||||
'--parallel 1',
|
||||
// '--format progress-bar',
|
||||
// '--format usage',
|
||||
// '--format snippets',
|
||||
'--format summary',
|
||||
'--publish-quiet',
|
||||
// '--require-module ts-node/register',
|
||||
// '--require ./src/features/**/*.ts',
|
||||
// '--require ./src/features/*.ts',
|
||||
].join(' '),
|
||||
};
|
||||
7
jest.config.js
Normal file
7
jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
modulePathIgnorePatterns: ['dist'],
|
||||
setupFilesAfterEnv: ['jest-extended/all'],
|
||||
};
|
||||
59
package.json
Normal file
59
package.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "@garm/rxjs-reboot",
|
||||
"version": "0.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"step1": "npm run test:watch Subscription",
|
||||
"step2": "npm run test:watch Subscriber",
|
||||
"step3": "npm run test:watch Observable",
|
||||
"step4": "npm run test:watch Subject",
|
||||
"step5": "npm run test:watch map",
|
||||
"step6": "npm run test:watch scan",
|
||||
"step7": "npm run test:watch retry",
|
||||
"step8": "npm run test:watch finalize",
|
||||
"step9": "npm run test:watch switchMap",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"check:all": "npm run build && npm run lint:all",
|
||||
"tsc": "tsc --build src",
|
||||
"tsc:watch": "tsc --build src --watch",
|
||||
"tsc:check": "tsc --build src --noEmit",
|
||||
"build": "npm run tsc",
|
||||
"build:watch": "npm run tsc:watch",
|
||||
"build:check": "npm run tsc:check",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"lint": "npx eslint src --ext .ts,.tsx,.json --max-warnings=0",
|
||||
"lint:all": "npm run lint && npm run prettier:check",
|
||||
"lint:fix": "npm run lint --fix",
|
||||
"prettier": "prettier --write .",
|
||||
"prettier:check": "prettier --check .",
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"clean:all": "npm run clean",
|
||||
"test": "NODE_ENV=test jest --verbose --passWithNoTests",
|
||||
"test:watch": "NODE_ENV=test jest --verbose --passWithNoTests --watch",
|
||||
"test:all": "npm run clean:all && npm run build && npm run lint:all && npm run test"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Garm",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@cucumber/cucumber": "8.11.0",
|
||||
"@trapcodien/eslint-config": "1.0.4",
|
||||
"@trapcodien/prettier-config": "1.0.4",
|
||||
"@trapcodien/tsconfig": "1.0.6",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/node": "18.13.0",
|
||||
"copyfiles": "2.4.1",
|
||||
"eslint": "8.34.0",
|
||||
"jest": "29.4.2",
|
||||
"jest-extended": "3.2.4",
|
||||
"prettier": "2.8.4",
|
||||
"ts-jest": "29.0.5",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": "7.8.1"
|
||||
}
|
||||
}
|
||||
33
src/Observable.ts
Normal file
33
src/Observable.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Typings
|
||||
*/
|
||||
|
||||
import { Observer } from './Observer';
|
||||
import { Subscription, TeardownLogic } from './Subscription';
|
||||
|
||||
type SubscribeFunction<T> = (obs: Partial<Observer<T>>) => Subscription;
|
||||
|
||||
type SubscriptionFactory<T> = (observer: Observer<T>) => TeardownLogic;
|
||||
|
||||
export interface IObservable<T> {
|
||||
subscribe: SubscribeFunction<T>;
|
||||
}
|
||||
|
||||
export interface ObservableConstructor {
|
||||
new <T>(subscriptionFactory: SubscriptionFactory<T>): IObservable<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
export class Observable<T> implements IObservable<T> {
|
||||
constructor(subscriptionFactory: SubscriptionFactory<T>) {
|
||||
void subscriptionFactory;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
subscribe(obs: Partial<Observer<T>>): Subscription {
|
||||
void obs;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
}
|
||||
9
src/Observer.ts
Normal file
9
src/Observer.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export type OnNext<T> = (value: T) => void;
|
||||
export type OnError = (error: unknown) => void;
|
||||
export type OnComplete = () => void;
|
||||
|
||||
export type Observer<T> = {
|
||||
next: OnNext<T>;
|
||||
error: OnError;
|
||||
complete: OnComplete;
|
||||
};
|
||||
39
src/Subject.ts
Normal file
39
src/Subject.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { IObservable } from './Observable';
|
||||
import { Observer } from './Observer';
|
||||
import { Subscription } from './Subscription';
|
||||
|
||||
/**
|
||||
* Typings
|
||||
*/
|
||||
|
||||
export interface ISubject<T> extends IObservable<T>, Observer<T> {}
|
||||
|
||||
export interface SubjectConstructor {
|
||||
new <T>(): ISubject<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
export class Subject<T> implements IObservable<T>, Observer<T> {
|
||||
public constructor() {}
|
||||
|
||||
public next(value: T): void {
|
||||
void value;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public error(err: unknown): void {
|
||||
void err;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public complete(): void {
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public subscribe(obs: Partial<Observer<T>>): Subscription {
|
||||
void obs;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
}
|
||||
39
src/Subscriber.ts
Normal file
39
src/Subscriber.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Observer } from './Observer';
|
||||
import { Subscription, ISubscription } from './Subscription';
|
||||
|
||||
/**
|
||||
* Typings
|
||||
*/
|
||||
|
||||
export interface ISubscriber<T> extends ISubscription, Observer<T> {}
|
||||
|
||||
export interface SubscriberConstructor {
|
||||
new <T>(obs: Observer<T>): ISubscriber<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
export class Subscriber<T> extends Subscription implements Observer<T> {
|
||||
public constructor(obs: Partial<Observer<T>>) {
|
||||
super();
|
||||
|
||||
void obs;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
/* Observer implementation */
|
||||
public next(value: T): void {
|
||||
void value;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public error(err: unknown): void {
|
||||
void err;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public complete(): void {
|
||||
throw new Error('TODO');
|
||||
}
|
||||
}
|
||||
45
src/Subscription.ts
Normal file
45
src/Subscription.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { Unsubscribable } from 'rxjs/internal/types';
|
||||
|
||||
/**
|
||||
* Typings
|
||||
*/
|
||||
type Teardown = () => void;
|
||||
export interface ISubscription {
|
||||
readonly closed?: boolean;
|
||||
unsubscribe: Teardown;
|
||||
add: (t: TeardownLogic) => void;
|
||||
remove: (s: TeardownLogic) => void;
|
||||
}
|
||||
|
||||
export interface SubscriptionConstructor {
|
||||
new (t?: Teardown): Subscription;
|
||||
}
|
||||
|
||||
export type TeardownLogic = Subscription | Unsubscribable | (() => void) | void;
|
||||
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
|
||||
export class Subscription implements ISubscription {
|
||||
public closed = false;
|
||||
|
||||
public constructor(action?: Teardown) {
|
||||
void action;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public unsubscribe(): void {
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public add(tl: TeardownLogic): void {
|
||||
void tl;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
|
||||
public remove(s: TeardownLogic): void {
|
||||
void s;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
}
|
||||
311
src/__tests__/Observable.spec.ts
Normal file
311
src/__tests__/Observable.spec.ts
Normal file
@ -0,0 +1,311 @@
|
||||
import 'jest-extended';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { Observable, ObservableConstructor } from '../Observable';
|
||||
import { Observer } from '../Observer';
|
||||
|
||||
const testObservable = (ObservableClass: ObservableConstructor) => () => {
|
||||
it('[constructor] an error should be emitted on throw', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(() => {
|
||||
throw 'ERROR';
|
||||
});
|
||||
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onError).toBeCalledWith('ERROR');
|
||||
expect(onNext).not.toBeCalled();
|
||||
expect(onComplete).not.toBeCalled();
|
||||
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[constructor] subscription should be closed on error', () => {
|
||||
const obs: Partial<Observer<unknown>> = { error: jest.fn() };
|
||||
|
||||
const o$ = new ObservableClass(() => {
|
||||
throw 'ERROR';
|
||||
});
|
||||
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[next]', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(42);
|
||||
});
|
||||
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onNext).toBeCalledWith(42);
|
||||
expect(onNext).toBeCalledTimes(1);
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(onError).not.toBeCalled();
|
||||
|
||||
expect(sub.closed).toBe(false);
|
||||
});
|
||||
|
||||
it('[next/complete]', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(42);
|
||||
observer.complete();
|
||||
});
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onNext).toBeCalledWith(42);
|
||||
expect(onNext).toBeCalledTimes(1);
|
||||
expect(onComplete).toBeCalledTimes(1);
|
||||
expect(onError).not.toBeCalled();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[next/error]', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(42);
|
||||
observer.error('dummy error');
|
||||
});
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onNext).toBeCalledWith(42);
|
||||
expect(onNext).toBeCalledTimes(1);
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(onError).toHaveBeenCalledWith('dummy error');
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[next] several values', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(-84);
|
||||
observer.next(-42);
|
||||
observer.next(42);
|
||||
observer.next(84);
|
||||
});
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onNext).toHaveBeenNthCalledWith(1, -84);
|
||||
expect(onNext).toHaveBeenNthCalledWith(2, -42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(3, 42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(4, 84);
|
||||
expect(onNext).toBeCalledTimes(4);
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(onError).not.toBeCalled();
|
||||
expect(sub.closed).toBe(false);
|
||||
});
|
||||
|
||||
it('[next/complete] several values', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(-84);
|
||||
observer.next(-42);
|
||||
observer.next(42);
|
||||
observer.next(84);
|
||||
observer.complete();
|
||||
});
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onNext).toHaveBeenNthCalledWith(1, -84);
|
||||
expect(onNext).toHaveBeenNthCalledWith(2, -42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(3, 42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(4, 84);
|
||||
expect(onNext).toBeCalledTimes(4);
|
||||
expect(onComplete).toBeCalledTimes(1);
|
||||
expect(onError).not.toBeCalled();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[next/error] several values', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(-84);
|
||||
observer.next(-42);
|
||||
observer.next(42);
|
||||
observer.next(84);
|
||||
observer.error('dummy error');
|
||||
});
|
||||
const sub = o$.subscribe(obs);
|
||||
|
||||
expect(onNext).toHaveBeenNthCalledWith(1, -84);
|
||||
expect(onNext).toHaveBeenNthCalledWith(2, -42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(3, 42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(4, 84);
|
||||
expect(onNext).toBeCalledTimes(4);
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(onError).toBeCalledTimes(1);
|
||||
expect(onError).toBeCalledWith('dummy error');
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[next] with 2 subscriptions', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.next(42);
|
||||
});
|
||||
|
||||
const subA = o$.subscribe(obs);
|
||||
expect(onNext).toBeCalledTimes(1);
|
||||
|
||||
const subB = o$.subscribe(obs);
|
||||
expect(subA.closed).toBe(false);
|
||||
expect(subB.closed).toBe(false);
|
||||
|
||||
expect(onNext).toHaveBeenNthCalledWith(1, 42);
|
||||
expect(onNext).toHaveBeenNthCalledWith(2, 42);
|
||||
expect(onNext).toBeCalledTimes(2);
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(onError).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('[complete] with 2 subscriptions', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.complete();
|
||||
});
|
||||
|
||||
const subA = o$.subscribe(obs);
|
||||
expect(subA.closed).toBe(true);
|
||||
expect(onComplete).toBeCalledTimes(1);
|
||||
|
||||
const subB = o$.subscribe(obs);
|
||||
expect(subB.closed).toBe(true);
|
||||
|
||||
expect(onComplete).toBeCalledTimes(2);
|
||||
expect(onNext).not.toBeCalled();
|
||||
expect(onError).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('[error] with 2 subscriptions', () => {
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const obs: Observer<unknown> = { next: onNext, error: onError, complete: onComplete };
|
||||
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.error('dummy error');
|
||||
});
|
||||
|
||||
const subA = o$.subscribe(obs);
|
||||
expect(subA.closed).toBe(true);
|
||||
expect(onError).toBeCalledTimes(1);
|
||||
expect(onError).toBeCalledWith('dummy error');
|
||||
|
||||
const subB = o$.subscribe(obs);
|
||||
expect(subB.closed).toBe(true);
|
||||
expect(onError).toBeCalledTimes(2);
|
||||
expect(onError).toHaveBeenNthCalledWith(2, 'dummy error');
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(onNext).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('[unsubscribe] should pass the closed boolen to true', () => {
|
||||
const o$ = new ObservableClass(() => {});
|
||||
const sub = o$.subscribe({});
|
||||
|
||||
expect(sub.closed).toBe(false);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[unsubscribe] teardown should be called on unsubscribe', () => {
|
||||
const teardown = jest.fn();
|
||||
const o$ = new ObservableClass(() => teardown);
|
||||
|
||||
expect(teardown).not.toBeCalled();
|
||||
|
||||
const sub = o$.subscribe({});
|
||||
expect(teardown).not.toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
sub.unsubscribe();
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
|
||||
o$.subscribe({}).unsubscribe();
|
||||
expect(teardown).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('[complete] teardown should be called on complete', () => {
|
||||
const teardown = jest.fn();
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.complete();
|
||||
return teardown;
|
||||
});
|
||||
|
||||
const sub = o$.subscribe({});
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
|
||||
o$.subscribe({}).unsubscribe();
|
||||
expect(teardown).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('[error] teardown should be called on error', () => {
|
||||
const teardown = jest.fn();
|
||||
const o$ = new ObservableClass(observer => {
|
||||
observer.error('dummy error');
|
||||
return teardown;
|
||||
});
|
||||
|
||||
const sub = o$.subscribe({ error: jest.fn() });
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
|
||||
o$.subscribe({ error: jest.fn() }).unsubscribe();
|
||||
expect(teardown).toBeCalledTimes(2);
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('Observable', testObservable(Rx.Observable as unknown as ObservableConstructor));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('Observable', testObservable(Observable));
|
||||
});
|
||||
224
src/__tests__/Subject.spec.ts
Normal file
224
src/__tests__/Subject.spec.ts
Normal file
@ -0,0 +1,224 @@
|
||||
import 'jest-extended';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { Subject, SubjectConstructor } from '../Subject';
|
||||
|
||||
const testSubject = (SubjectClass: SubjectConstructor) => () => {
|
||||
it('[next] several values', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onNext = jest.fn();
|
||||
const sub = subject.subscribe({ next: onNext });
|
||||
|
||||
subject.next(10);
|
||||
subject.next(20);
|
||||
subject.next(30);
|
||||
|
||||
expect(onNext.mock.calls[0]).toEqual([10]);
|
||||
expect(onNext.mock.calls[1]).toEqual([20]);
|
||||
expect(onNext.mock.calls[2]).toEqual([30]);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('[next] several values for several subscriber', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onNext1 = jest.fn();
|
||||
const onNext2 = jest.fn();
|
||||
const sub1 = subject.subscribe({ next: onNext1 });
|
||||
const sub2 = subject.subscribe({ next: onNext2 });
|
||||
|
||||
subject.next(10);
|
||||
subject.next(20);
|
||||
subject.next(30);
|
||||
|
||||
expect(onNext1.mock.calls[0]).toEqual([10]);
|
||||
expect(onNext1.mock.calls[1]).toEqual([20]);
|
||||
expect(onNext1.mock.calls[2]).toEqual([30]);
|
||||
|
||||
expect(onNext2.mock.calls[0]).toEqual([10]);
|
||||
expect(onNext2.mock.calls[1]).toEqual([20]);
|
||||
expect(onNext2.mock.calls[2]).toEqual([30]);
|
||||
|
||||
sub1.unsubscribe();
|
||||
sub2.unsubscribe();
|
||||
});
|
||||
|
||||
it('[complete] should block next and error', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
const sub = subject.subscribe({ next: onNext, error: onError });
|
||||
|
||||
subject.complete();
|
||||
|
||||
subject.next(1);
|
||||
subject.error('test');
|
||||
|
||||
expect(onNext).not.toBeCalled();
|
||||
expect(onError).not.toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('[complete] should complete', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = subject.subscribe({ complete: onComplete });
|
||||
|
||||
subject.complete();
|
||||
|
||||
expect(onComplete).toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('[complete] should complete once', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = subject.subscribe({ complete: onComplete });
|
||||
|
||||
subject.complete();
|
||||
subject.complete();
|
||||
subject.complete();
|
||||
|
||||
expect(onComplete).toBeCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('[complete] should unsubscribe all listeners', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const teardown1 = jest.fn();
|
||||
const teardown2 = jest.fn();
|
||||
const teardown3 = jest.fn();
|
||||
|
||||
const sub1 = subject.subscribe({});
|
||||
const sub2 = subject.subscribe({});
|
||||
const sub3 = subject.subscribe({});
|
||||
|
||||
sub1.add(teardown1);
|
||||
sub2.add(teardown2);
|
||||
sub3.add(teardown3);
|
||||
|
||||
subject.complete();
|
||||
|
||||
expect(teardown1).toHaveBeenCalledTimes(1);
|
||||
expect(teardown2).toHaveBeenCalledTimes(1);
|
||||
expect(teardown3).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(sub1.closed).toBe(true);
|
||||
expect(sub2.closed).toBe(true);
|
||||
expect(sub3.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[complete] subscribe on a completed subject should complete immediatly', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
subject.complete();
|
||||
|
||||
const sub = subject.subscribe({ complete: onComplete });
|
||||
|
||||
expect(onComplete).toBeCalled();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[error] should block next and complete', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onNext = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
const sub = subject.subscribe({ next: onNext, complete: onComplete, error: onError });
|
||||
|
||||
subject.error('test');
|
||||
subject.next(1);
|
||||
subject.complete();
|
||||
|
||||
expect(onNext).not.toBeCalled();
|
||||
expect(onComplete).not.toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('[error] should emit error once', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onError = jest.fn();
|
||||
|
||||
const sub = subject.subscribe({ error: onError });
|
||||
|
||||
subject.error('test');
|
||||
subject.error('test');
|
||||
subject.error('test');
|
||||
|
||||
expect(onError).toBeCalledTimes(1);
|
||||
expect(onError).toBeCalledWith('test');
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('[error] should unsubscribe all listeners', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const teardown1 = jest.fn();
|
||||
const teardown2 = jest.fn();
|
||||
const teardown3 = jest.fn();
|
||||
const onError = jest.fn();
|
||||
|
||||
const sub1 = subject.subscribe({ error: onError });
|
||||
const sub2 = subject.subscribe({ error: onError });
|
||||
const sub3 = subject.subscribe({ error: onError });
|
||||
|
||||
sub1.add(teardown1);
|
||||
sub2.add(teardown2);
|
||||
sub3.add(teardown3);
|
||||
|
||||
subject.error('test');
|
||||
|
||||
expect(teardown1).toHaveBeenCalledTimes(1);
|
||||
expect(teardown2).toHaveBeenCalledTimes(1);
|
||||
expect(teardown3).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(sub1.closed).toBe(true);
|
||||
expect(sub2.closed).toBe(true);
|
||||
expect(sub3.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[error] subscribe on a completed subject caused by an error should replay this error', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
subject.error('test');
|
||||
|
||||
const sub = subject.subscribe({ error: onError, complete: onComplete });
|
||||
|
||||
expect(onError).toBeCalledWith('test');
|
||||
expect(onComplete).not.toBeCalled();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[subscription] added teardown should be executed once on complete', () => {
|
||||
const subject = new SubjectClass<number>();
|
||||
const teardown = jest.fn();
|
||||
|
||||
const sub = subject.subscribe({});
|
||||
sub.add(teardown);
|
||||
|
||||
subject.complete();
|
||||
expect(teardown).toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
expect(teardown).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('Subject', testSubject(Rx.Subject as unknown as SubjectConstructor));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('Subject', testSubject(Subject));
|
||||
});
|
||||
279
src/__tests__/Subscriber.spec.ts
Normal file
279
src/__tests__/Subscriber.spec.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import 'jest-extended';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { Subscriber, SubscriberConstructor } from '../Subscriber';
|
||||
import { Observer } from '../Observer';
|
||||
|
||||
const getObserver = <T>(): Observer<T> => {
|
||||
return { next: jest.fn(), error: jest.fn(), complete: jest.fn() };
|
||||
};
|
||||
|
||||
const testSubscriber = (SubscriberClass: SubscriberConstructor) => () => {
|
||||
it('[closed] should be false before then true on unsubscribe', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
expect(subscriber.closed).toBe(false);
|
||||
subscriber.unsubscribe();
|
||||
expect(subscriber.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[closed] should be false before then true on error', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
expect(subscriber.closed).toBe(false);
|
||||
subscriber.error('dummy error');
|
||||
expect(subscriber.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[closed] should be false before then true on complete', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
expect(subscriber.closed).toBe(false);
|
||||
subscriber.complete();
|
||||
expect(subscriber.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[next] should call onNext with given values', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.next('a');
|
||||
subscriber.next('b');
|
||||
subscriber.next('c');
|
||||
|
||||
expect(obs.next).toHaveBeenNthCalledWith(1, 'a');
|
||||
expect(obs.next).toHaveBeenNthCalledWith(2, 'b');
|
||||
expect(obs.next).toHaveBeenNthCalledWith(3, 'c');
|
||||
});
|
||||
|
||||
it('[next] cannot call next after error', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.next('a');
|
||||
|
||||
subscriber.error('dummy error');
|
||||
|
||||
subscriber.next('b');
|
||||
subscriber.next('c');
|
||||
|
||||
expect(obs.next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[next] cannot call next after complete', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.next('a');
|
||||
|
||||
subscriber.complete();
|
||||
|
||||
subscriber.next('b');
|
||||
subscriber.next('c');
|
||||
|
||||
expect(obs.next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[next] cannot call next after unsubscribe', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.next('a');
|
||||
|
||||
subscriber.unsubscribe();
|
||||
|
||||
subscriber.next('b');
|
||||
subscriber.next('c');
|
||||
|
||||
expect(obs.next).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[error] should call onError (once) with given error', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.error('dummy error');
|
||||
subscriber.error('dummy error 2 (should be ignored');
|
||||
|
||||
expect(obs.error).toHaveBeenCalledTimes(1);
|
||||
expect(obs.error).toHaveBeenCalledWith('dummy error');
|
||||
});
|
||||
|
||||
it('[error] should call onError before unsubscribe', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
const onUnsubscribe = jest.fn();
|
||||
subscriber.add(onUnsubscribe);
|
||||
|
||||
subscriber.error('dummy error');
|
||||
subscriber.error('dummy error 2 (should be ignored');
|
||||
|
||||
expect(obs.error).toHaveBeenCalledTimes(1);
|
||||
expect(obs.error).toHaveBeenCalledWith('dummy error');
|
||||
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(obs.error).toHaveBeenCalledBefore(onUnsubscribe);
|
||||
expect(obs.error).toHaveBeenCalledBefore(onUnsubscribe);
|
||||
});
|
||||
|
||||
it('[error] should ignore errors when completed', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.complete();
|
||||
subscriber.error('dummy error (should be ignored)');
|
||||
subscriber.error('dummy error 2 (should be ignored');
|
||||
|
||||
expect(obs.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('[error] should ignore errors after unsubscribe', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.unsubscribe();
|
||||
subscriber.error('dummy error (should be ignored)');
|
||||
subscriber.error('dummy error 2 (should be ignored');
|
||||
|
||||
expect(obs.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('[complete] should call onComplete (once)', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.complete();
|
||||
subscriber.complete();
|
||||
|
||||
expect(obs.complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[complete] should call onComplete before unsubscribe', () => {
|
||||
const onUnsubscribe = jest.fn();
|
||||
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.add(onUnsubscribe);
|
||||
|
||||
subscriber.complete();
|
||||
|
||||
expect(obs.complete).toHaveBeenCalledTimes(1);
|
||||
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(obs.complete).toHaveBeenCalledBefore(onUnsubscribe);
|
||||
});
|
||||
|
||||
it('[complete] should not call onComplete after error', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.error('dummy error');
|
||||
subscriber.complete();
|
||||
|
||||
expect(obs.complete).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('[complete] should not call onComplete after unsubscribe', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.unsubscribe();
|
||||
subscriber.complete();
|
||||
|
||||
expect(obs.complete).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('[unsubscribe] should not call onNext/onError/onComplete', () => {
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.unsubscribe();
|
||||
|
||||
expect(obs.next).not.toBeCalled();
|
||||
expect(obs.error).not.toBeCalled();
|
||||
expect(obs.complete).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('[add] added teardowns are called once on unsubscribe', () => {
|
||||
const teardownA = jest.fn();
|
||||
const teardownB = jest.fn();
|
||||
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.add(teardownA);
|
||||
subscriber.add(teardownB);
|
||||
|
||||
subscriber.unsubscribe();
|
||||
subscriber.unsubscribe();
|
||||
|
||||
expect(teardownA).toBeCalledTimes(1);
|
||||
expect(teardownB).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] added teardowns are called once on complete', () => {
|
||||
const teardownA = jest.fn();
|
||||
const teardownB = jest.fn();
|
||||
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.add(teardownA);
|
||||
subscriber.add(teardownB);
|
||||
|
||||
subscriber.complete();
|
||||
subscriber.complete();
|
||||
|
||||
expect(teardownA).toBeCalledTimes(1);
|
||||
expect(teardownB).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] added teardowns are called once on error', () => {
|
||||
const teardownA = jest.fn();
|
||||
const teardownB = jest.fn();
|
||||
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.add(teardownA);
|
||||
subscriber.add(teardownB);
|
||||
|
||||
subscriber.error('dummy error');
|
||||
subscriber.error('dummy error 2');
|
||||
|
||||
expect(teardownA).toBeCalledTimes(1);
|
||||
expect(teardownB).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[remove] removed teardowns are not called on unsubscribe', () => {
|
||||
const subA = { unsubscribe: jest.fn() };
|
||||
const subB = { unsubscribe: jest.fn() };
|
||||
|
||||
const obs = getObserver();
|
||||
const subscriber = new SubscriberClass(obs);
|
||||
|
||||
subscriber.add(subA);
|
||||
subscriber.add(subB);
|
||||
|
||||
subscriber.remove(subA);
|
||||
subscriber.remove(subB);
|
||||
|
||||
subscriber.unsubscribe();
|
||||
|
||||
expect(subA.unsubscribe).not.toBeCalled();
|
||||
expect(subB.unsubscribe).not.toBeCalled();
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('Subscriber', testSubscriber(Rx.Subscriber as SubscriberConstructor));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('Subscriber', testSubscriber(Subscriber));
|
||||
});
|
||||
167
src/__tests__/Subscription.spec.ts
Normal file
167
src/__tests__/Subscription.spec.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import 'jest-extended';
|
||||
import * as Rx from 'rxjs';
|
||||
|
||||
import { SubscriptionConstructor, Subscription } from '../Subscription';
|
||||
|
||||
const testSubscription = (SubscriptionClass: SubscriptionConstructor) => () => {
|
||||
it('[constructor] should not throw when created', () => {
|
||||
expect(() => new SubscriptionClass()).not.toThrow();
|
||||
});
|
||||
|
||||
it('[closed] should be false on creation', () => {
|
||||
expect(new SubscriptionClass().closed).toBe(false);
|
||||
});
|
||||
|
||||
it('[closed] should be true after unsubscribe', () => {
|
||||
const sub = new SubscriptionClass();
|
||||
sub.unsubscribe();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[unsubscribe] should call the given teardown', () => {
|
||||
const teardown = jest.fn();
|
||||
const sub = new SubscriptionClass(teardown);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('[unsubscribe] should call the given teardonwn once', () => {
|
||||
const teardown = jest.fn();
|
||||
const sub = new SubscriptionClass(teardown);
|
||||
|
||||
sub.unsubscribe();
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] should execute added teardowns when subscription is closed', () => {
|
||||
const initialTeardown = jest.fn();
|
||||
const additionalTeardown = jest.fn();
|
||||
|
||||
const sub = new SubscriptionClass(initialTeardown);
|
||||
sub.add(additionalTeardown);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(initialTeardown).toBeCalledTimes(1);
|
||||
expect(additionalTeardown).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] should return a new subscription when adding a teardown', () => {
|
||||
const parentSub = new SubscriptionClass();
|
||||
const childSub = parentSub.add(Rx.noop);
|
||||
|
||||
expect(parentSub).not.toBe(childSub);
|
||||
});
|
||||
|
||||
it('[add] should be able to unsubscribe partially using child subscription', () => {
|
||||
const parentTeardown = jest.fn();
|
||||
const childTeardown = jest.fn();
|
||||
const childSub = new SubscriptionClass(childTeardown);
|
||||
|
||||
const parentSub = new SubscriptionClass(parentTeardown);
|
||||
parentSub.add(childSub);
|
||||
|
||||
childSub.unsubscribe();
|
||||
|
||||
// childSub.unsubscribe();
|
||||
expect(childTeardown).toBeCalledTimes(1);
|
||||
expect(parentTeardown).not.toBeCalled();
|
||||
|
||||
parentSub.unsubscribe();
|
||||
expect(childTeardown).toBeCalledTimes(1);
|
||||
expect(parentTeardown).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] should add several teardowns', () => {
|
||||
const teardownA = jest.fn();
|
||||
const teardownB = jest.fn();
|
||||
const teardownC = jest.fn();
|
||||
|
||||
const sub = new SubscriptionClass();
|
||||
|
||||
sub.add(teardownA);
|
||||
sub.add(teardownB);
|
||||
sub.add(teardownC);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(teardownA).toBeCalledTimes(1);
|
||||
expect(teardownB).toBeCalledTimes(1);
|
||||
expect(teardownC).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] should call a child teardown twice', () => {
|
||||
const teardown = jest.fn();
|
||||
|
||||
const subA = new SubscriptionClass();
|
||||
const subB = new SubscriptionClass();
|
||||
|
||||
subA.add(teardown);
|
||||
subB.add(teardown);
|
||||
|
||||
subA.unsubscribe();
|
||||
subB.unsubscribe();
|
||||
|
||||
expect(teardown).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('[add] should call a child subscription once', () => {
|
||||
const teardown = jest.fn();
|
||||
const teardownSub = new SubscriptionClass(teardown);
|
||||
|
||||
const subA = new SubscriptionClass();
|
||||
const subB = new SubscriptionClass();
|
||||
|
||||
subA.add(teardownSub);
|
||||
subB.add(teardownSub);
|
||||
|
||||
subA.unsubscribe();
|
||||
subB.unsubscribe();
|
||||
|
||||
expect(teardown).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[add] should call a new added teardown when already unsubscribed', () => {
|
||||
const initialTeardown = jest.fn();
|
||||
const sub = new SubscriptionClass(initialTeardown);
|
||||
|
||||
sub.unsubscribe();
|
||||
expect(initialTeardown).toBeCalledTimes(1);
|
||||
|
||||
const additionalTeardown = jest.fn();
|
||||
sub.add(additionalTeardown);
|
||||
expect(additionalTeardown).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('[remove] should be able to remove a child subscription', () => {
|
||||
const parentTeardown = jest.fn();
|
||||
const childTeardown = jest.fn();
|
||||
const childSub = new SubscriptionClass(childTeardown);
|
||||
|
||||
const parentSub = new SubscriptionClass(parentTeardown);
|
||||
parentSub.add(childSub);
|
||||
|
||||
parentSub.remove(childSub);
|
||||
parentSub.unsubscribe();
|
||||
|
||||
expect(childTeardown).not.toBeCalled();
|
||||
expect(parentTeardown).toBeCalledTimes(1);
|
||||
|
||||
childSub.unsubscribe();
|
||||
expect(childTeardown).toBeCalledTimes(1);
|
||||
expect(parentTeardown).toBeCalledTimes(1);
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('Subscription', testSubscription(Rx.Subscription as unknown as SubscriptionConstructor));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('Subscription', testSubscription(Subscription));
|
||||
});
|
||||
63
src/__tests__/operators/finalize.spec.ts
Normal file
63
src/__tests__/operators/finalize.spec.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import * as Rx from 'rxjs';
|
||||
import * as RxOps from 'rxjs/operators';
|
||||
|
||||
import { finalize } from '../../operators/finalize';
|
||||
import { dummyErrorObservable$, dummyObservable$ } from '../../helpers/test-helpers';
|
||||
import { Observable } from '../../Observable';
|
||||
|
||||
type finalizeOperator = typeof finalize;
|
||||
|
||||
const NEVER = new Rx.Observable(() => {}) as unknown as Observable<never>;
|
||||
|
||||
const testFinalize = (finalizeOp: finalizeOperator) => () => {
|
||||
it('finalize on completed', () => {
|
||||
const finalized = jest.fn();
|
||||
|
||||
const result$ = finalizeOp(finalized)(dummyObservable$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = result$.subscribe({ next: onNext, complete: onComplete });
|
||||
|
||||
expect(finalized).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('finalize on error', async () => {
|
||||
const finalized = jest.fn();
|
||||
|
||||
const result$ = finalizeOp(finalized)(dummyErrorObservable$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = result$.subscribe({ next: onNext, error: onError, complete: onComplete });
|
||||
|
||||
expect(finalized).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('finalize when unsubscribed', () => {
|
||||
const finalized = jest.fn();
|
||||
|
||||
const result$ = finalizeOp(finalized)(NEVER);
|
||||
|
||||
const sub = result$.subscribe({});
|
||||
|
||||
expect(finalized).toHaveBeenCalledTimes(0);
|
||||
sub.unsubscribe();
|
||||
expect(finalized).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('finalize', testFinalize(RxOps.finalize as unknown as finalizeOperator));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('finalize', testFinalize(finalize));
|
||||
});
|
||||
75
src/__tests__/operators/map.spec.ts
Normal file
75
src/__tests__/operators/map.spec.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import * as RxOps from 'rxjs/operators';
|
||||
|
||||
import { map } from '../../operators/map';
|
||||
import { dummyErrorObservable$, dummyObservable$ } from '../../helpers/test-helpers';
|
||||
|
||||
type MapOperator = typeof map;
|
||||
|
||||
const testMap = (mapOp: MapOperator) => () => {
|
||||
it('should map values then complete', () => {
|
||||
const result$ = mapOp<number, number>(x => x * 2)(dummyObservable$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const sub = result$.subscribe({ next: onNext, complete: onComplete });
|
||||
|
||||
expect(onNext.mock.calls[0]).toEqual([2]);
|
||||
expect(onNext.mock.calls[1]).toEqual([4]);
|
||||
expect(onNext.mock.calls[2]).toEqual([8]);
|
||||
|
||||
expect(onNext).toHaveBeenCalledTimes(3);
|
||||
expect(onComplete).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should forward error from the source', () => {
|
||||
const result$ = mapOp<number, number>(x => x * 2)(dummyErrorObservable$);
|
||||
|
||||
const onError = jest.fn();
|
||||
const sub = result$.subscribe({ error: onError });
|
||||
|
||||
expect(onError).toHaveBeenCalledWith('test');
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should forward error from the map project function', () => {
|
||||
const result$ = mapOp<number, number>(() => {
|
||||
throw 'woops';
|
||||
})(dummyObservable$);
|
||||
|
||||
const onError = jest.fn();
|
||||
const sub = result$.subscribe({ error: onError });
|
||||
|
||||
expect(onError).toHaveBeenCalledWith('woops');
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should have a cold behavior', () => {
|
||||
const onNext = jest.fn();
|
||||
const mockedProjectFn = jest.fn((x: number) => x * 2);
|
||||
|
||||
const result$ = mapOp<number, number>(mockedProjectFn)(dummyObservable$);
|
||||
|
||||
const sub1 = result$.subscribe({ next: onNext });
|
||||
const sub2 = result$.subscribe({ next: onNext });
|
||||
|
||||
expect(mockedProjectFn).toBeCalledTimes(6);
|
||||
expect(onNext.mock.calls).toEqual([[2], [4], [8], [2], [4], [8]]);
|
||||
|
||||
sub1.unsubscribe();
|
||||
sub2.unsubscribe();
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('map', testMap(RxOps.map as unknown as MapOperator));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('map', testMap(map));
|
||||
});
|
||||
98
src/__tests__/operators/retry.spec.ts
Normal file
98
src/__tests__/operators/retry.spec.ts
Normal file
@ -0,0 +1,98 @@
|
||||
import * as Rx from 'rxjs';
|
||||
import * as RxOps from 'rxjs/operators';
|
||||
import { Observable } from '../../Observable';
|
||||
|
||||
import { retry } from '../../operators/retry';
|
||||
import { dummyObservable$ } from '../../helpers/test-helpers';
|
||||
|
||||
type RetryOperator = typeof retry;
|
||||
|
||||
const testRetry = (retryOp: RetryOperator) => () => {
|
||||
it('should next values then complete', () => {
|
||||
const final$ = retryOp()(dummyObservable$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = final$.subscribe({
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
});
|
||||
|
||||
expect(onNext.mock.calls[0]).toEqual([1]);
|
||||
expect(onNext.mock.calls[1]).toEqual([2]);
|
||||
expect(onNext.mock.calls[2]).toEqual([4]);
|
||||
|
||||
expect(onNext).toHaveBeenCalledTimes(3);
|
||||
expect(onComplete).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should retry the observable on error indefinitely until complete', () => {
|
||||
let i = 0;
|
||||
const main$ = new Rx.Observable<number>(obs => {
|
||||
i = i + 1;
|
||||
if (i <= 3) {
|
||||
obs.next(i);
|
||||
obs.error('test');
|
||||
}
|
||||
}) as unknown as Observable<number>;
|
||||
|
||||
const final$ = retryOp()(main$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = final$.subscribe({
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
});
|
||||
|
||||
expect(onNext.mock.calls).toEqual([[1], [2], [3]]);
|
||||
expect(onError).not.toBeCalled();
|
||||
expect(onComplete).not.toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should retry the observable on error three times then emit the error', () => {
|
||||
const main$ = new Rx.Observable<number>(obs => {
|
||||
obs.next(42);
|
||||
obs.error('test');
|
||||
}) as unknown as Observable<number>;
|
||||
|
||||
const final$ = retryOp(3)(main$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = final$.subscribe({
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
});
|
||||
|
||||
// initial value + 3 replayed values
|
||||
expect(onNext.mock.calls).toEqual([[42], [42], [42], [42]]);
|
||||
expect(onError).toBeCalledTimes(1);
|
||||
expect(onError).toBeCalledWith('test');
|
||||
|
||||
expect(onComplete).not.toBeCalled();
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('map', testRetry(RxOps.retry as unknown as RetryOperator));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('map', testRetry(retry));
|
||||
});
|
||||
88
src/__tests__/operators/scan.spec.ts
Normal file
88
src/__tests__/operators/scan.spec.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import * as RxOps from 'rxjs/operators';
|
||||
|
||||
import { scan } from '../../operators/scan';
|
||||
import { dummyErrorObservable$, dummyObservable$ } from '../../helpers/test-helpers';
|
||||
|
||||
type ScanOperator = typeof scan;
|
||||
|
||||
/**
|
||||
* Tests
|
||||
*/
|
||||
const testScan = (scanOp: ScanOperator) => () => {
|
||||
it('should scan values with a sum function then complete', () => {
|
||||
const result$ = scanOp<number, number>((acc, value) => acc + value, 0)(dummyObservable$);
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
const sub = result$.subscribe({
|
||||
next: onNext,
|
||||
complete: onComplete,
|
||||
});
|
||||
|
||||
expect(onNext.mock.calls[0]).toEqual([1]);
|
||||
expect(onNext.mock.calls[1]).toEqual([3]);
|
||||
expect(onNext.mock.calls[2]).toEqual([7]);
|
||||
|
||||
expect(onNext).toHaveBeenCalledTimes(3);
|
||||
expect(onComplete).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should forward error from the source', () => {
|
||||
const result$ = scanOp<number, number>((acc, value) => acc + value, 0)(dummyErrorObservable$);
|
||||
|
||||
const onError = jest.fn();
|
||||
const sub = result$.subscribe({
|
||||
error: onError,
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledWith('test');
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should forward error from the map project function', () => {
|
||||
const result$ = scanOp<number, number>(() => {
|
||||
throw 'woops';
|
||||
}, 0)(dummyObservable$);
|
||||
|
||||
const onError = jest.fn();
|
||||
const sub = result$.subscribe({
|
||||
error: onError,
|
||||
});
|
||||
|
||||
expect(onError).toHaveBeenCalledWith('woops');
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('should have a cold behavior', () => {
|
||||
const onNext = jest.fn();
|
||||
const mockedFn = jest.fn();
|
||||
|
||||
const result$ = scanOp<number, number>((acc, value) => {
|
||||
mockedFn();
|
||||
return acc + value;
|
||||
}, 0)(dummyObservable$);
|
||||
|
||||
const sub1 = result$.subscribe({ next: onNext });
|
||||
const sub2 = result$.subscribe({ next: onNext });
|
||||
|
||||
expect(mockedFn).toBeCalledTimes(6);
|
||||
expect(onNext.mock.calls).toEqual([[1], [3], [7], [1], [3], [7]]);
|
||||
|
||||
sub1.unsubscribe();
|
||||
sub2.unsubscribe();
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('scan', testScan(RxOps.scan as unknown as ScanOperator));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('scan', testScan(scan));
|
||||
});
|
||||
237
src/__tests__/operators/switchMap.spec.ts
Normal file
237
src/__tests__/operators/switchMap.spec.ts
Normal file
@ -0,0 +1,237 @@
|
||||
import * as Rx from 'rxjs';
|
||||
import * as RxOps from 'rxjs/operators';
|
||||
import { Observable } from '../../Observable';
|
||||
|
||||
import { switchMap } from '../../operators/switchMap';
|
||||
import { createSubject, dummyObservable$, of, toObservable } from '../../helpers/test-helpers';
|
||||
|
||||
type SwitchMapOperator = typeof switchMap;
|
||||
|
||||
/**
|
||||
* Tests
|
||||
*/
|
||||
const testSwitchMap = (switchMapOp: SwitchMapOperator) => () => {
|
||||
it('emit values through the inner observable then complete', () => {
|
||||
const final$ = switchMapOp<Observable<number>, number>(x => x)(of(dummyObservable$));
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onError = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = final$.subscribe({
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
});
|
||||
|
||||
expect(onNext.mock.calls[0]).toEqual([1]);
|
||||
expect(onNext.mock.calls[1]).toEqual([2]);
|
||||
expect(onNext.mock.calls[2]).toEqual([4]);
|
||||
|
||||
expect(onNext).toBeCalledTimes(3);
|
||||
expect(onError).not.toHaveBeenCalled();
|
||||
expect(onComplete).toBeCalledTimes(1);
|
||||
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('should continue emit values through the inner observable even when source is completed', () => {
|
||||
const inner$ = createSubject<number>();
|
||||
const final$ = switchMapOp<Observable<number>, number>(x => x)(of(toObservable(inner$)));
|
||||
|
||||
const onNext = jest.fn();
|
||||
|
||||
const sub = final$.subscribe({
|
||||
next: onNext,
|
||||
});
|
||||
|
||||
expect(onNext).not.toBeCalled();
|
||||
|
||||
inner$.next(1);
|
||||
inner$.next(2);
|
||||
inner$.next(3);
|
||||
|
||||
expect(onNext).toBeCalledTimes(3);
|
||||
expect(onNext.mock.calls).toEqual([[1], [2], [3]]);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('emit values through multiple inner observable', () => {
|
||||
const main$ = createSubject<number>();
|
||||
const final$ = switchMapOp<number, number>(x => {
|
||||
return new Rx.Observable(obs => {
|
||||
obs.next(x);
|
||||
obs.complete();
|
||||
}) as unknown as Observable<number>;
|
||||
})(toObservable(main$));
|
||||
|
||||
const onNext = jest.fn();
|
||||
const onComplete = jest.fn();
|
||||
|
||||
const sub = final$.subscribe({
|
||||
next: onNext,
|
||||
complete: onComplete,
|
||||
});
|
||||
|
||||
expect(onNext).not.toBeCalled();
|
||||
expect(onComplete).not.toBeCalled();
|
||||
|
||||
main$.next(1);
|
||||
main$.next(2);
|
||||
main$.next(3);
|
||||
|
||||
expect(onNext.mock.calls).toEqual([[1], [2], [3]]);
|
||||
expect(onComplete).not.toBeCalled();
|
||||
|
||||
main$.complete();
|
||||
expect(onComplete).toBeCalled();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('unsubscribe previous inner observable when a new value is emitted', () => {
|
||||
const main$ = createSubject<number>();
|
||||
|
||||
const onSubscribe = jest.fn();
|
||||
const onUnsubscribe = jest.fn();
|
||||
|
||||
const final$ = switchMapOp<number, number>(() => {
|
||||
return new Rx.Observable(() => {
|
||||
onSubscribe();
|
||||
return onUnsubscribe;
|
||||
}) as unknown as Observable<number>;
|
||||
})(toObservable(main$));
|
||||
|
||||
const sub = final$.subscribe({});
|
||||
|
||||
expect(onSubscribe).not.toBeCalled();
|
||||
expect(onUnsubscribe).not.toBeCalled();
|
||||
|
||||
main$.next(42);
|
||||
|
||||
expect(onSubscribe).toHaveBeenCalledTimes(1);
|
||||
expect(onUnsubscribe).not.toBeCalled();
|
||||
|
||||
main$.next(42);
|
||||
expect(onSubscribe).toHaveBeenCalledTimes(2);
|
||||
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
});
|
||||
|
||||
it('unsubscribe inner observable when source is unsubscribed', () => {
|
||||
const main$ = createSubject<number>();
|
||||
|
||||
const onSubscribe = jest.fn();
|
||||
const onUnsubscribe = jest.fn();
|
||||
|
||||
const final$ = switchMapOp<number, number>(() => {
|
||||
return new Rx.Observable(() => {
|
||||
onSubscribe();
|
||||
return onUnsubscribe;
|
||||
}) as unknown as Observable<number>;
|
||||
})(toObservable(main$));
|
||||
|
||||
const sub = final$.subscribe({});
|
||||
|
||||
main$.next(42);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
expect(onSubscribe).toHaveBeenCalledTimes(1);
|
||||
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should unsubscribe only when source complete AND inner observable complete', () => {
|
||||
const inner$ = createSubject<number>();
|
||||
const main$ = createSubject<number>();
|
||||
|
||||
const final$ = switchMapOp<number, number>(() => {
|
||||
return toObservable(inner$);
|
||||
})(toObservable(main$));
|
||||
|
||||
const sub = final$.subscribe({});
|
||||
|
||||
main$.next(42);
|
||||
main$.complete();
|
||||
|
||||
expect(sub.closed).toBe(false);
|
||||
|
||||
inner$.complete();
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('should unsubscribe only when source complete AND inner observable emit an error', () => {
|
||||
const inner$ = createSubject<number>();
|
||||
const main$ = createSubject<number>();
|
||||
|
||||
const onError = jest.fn();
|
||||
|
||||
const final$ = switchMapOp<number, number>(() => {
|
||||
return toObservable(inner$);
|
||||
})(toObservable(main$));
|
||||
|
||||
const sub = final$.subscribe({ error: onError });
|
||||
|
||||
main$.next(42);
|
||||
main$.complete();
|
||||
|
||||
expect(sub.closed).toBe(false);
|
||||
|
||||
inner$.error('test');
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('unsubscribe inner observable when source emit an error', () => {
|
||||
const main$ = createSubject<number>();
|
||||
|
||||
const onError = jest.fn();
|
||||
const onSubscribe = jest.fn();
|
||||
const onUnsubscribe = jest.fn();
|
||||
|
||||
const final$ = switchMapOp<number, number>(() => {
|
||||
return new Rx.Observable(() => {
|
||||
onSubscribe();
|
||||
return onUnsubscribe;
|
||||
}) as unknown as Observable<number>;
|
||||
})(toObservable(main$));
|
||||
|
||||
const sub = final$.subscribe({ error: onError });
|
||||
|
||||
main$.next(42);
|
||||
main$.error('test');
|
||||
|
||||
expect(onSubscribe).toHaveBeenCalledTimes(1);
|
||||
expect(onUnsubscribe).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('emit an error when throw an error from the project function', () => {
|
||||
const main$ = createSubject<number>();
|
||||
const onError = jest.fn();
|
||||
|
||||
const final$ = switchMapOp<number, number>(() => {
|
||||
throw 'test';
|
||||
})(toObservable(main$));
|
||||
|
||||
const sub = final$.subscribe({
|
||||
error: onError,
|
||||
});
|
||||
|
||||
main$.next(42);
|
||||
|
||||
expect(onError).toHaveBeenCalledTimes(1);
|
||||
expect(onError).toHaveBeenCalledWith('test');
|
||||
|
||||
expect(sub.closed).toBe(true);
|
||||
});
|
||||
};
|
||||
|
||||
describe('RX', () => {
|
||||
describe('switchMap', testSwitchMap(RxOps.switchMap as unknown as SwitchMapOperator));
|
||||
});
|
||||
|
||||
describe('Custom implementation', () => {
|
||||
describe('switchMap', testSwitchMap(switchMap));
|
||||
});
|
||||
30
src/helpers/test-helpers.ts
Normal file
30
src/helpers/test-helpers.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as Rx from 'rxjs';
|
||||
import { Observable } from '../Observable';
|
||||
import { Subject } from '../Subject';
|
||||
|
||||
/**
|
||||
* Mocks
|
||||
*/
|
||||
export const dummyObservable$ = new Rx.Observable<number>(obs => {
|
||||
obs.next(1);
|
||||
obs.next(2);
|
||||
obs.next(4);
|
||||
obs.complete();
|
||||
}) as unknown as Observable<number>;
|
||||
|
||||
export const dummyErrorObservable$ = new Rx.Observable<number>(obs => {
|
||||
obs.error('test');
|
||||
}) as unknown as Observable<number>;
|
||||
|
||||
/**
|
||||
* Additional helpers (for unit tests)
|
||||
*/
|
||||
export const of = <T>(value: T): Observable<T> => Rx.of(value) as unknown as Observable<T>;
|
||||
|
||||
export const createSubject = <T>(): Subject<T> => {
|
||||
return new Rx.Subject<T>() as unknown as Subject<T>;
|
||||
};
|
||||
|
||||
export const toObservable = <T>(s: Subject<T>): Observable<T> => {
|
||||
return s as unknown as Observable<T>;
|
||||
};
|
||||
8
src/operators/finalize.ts
Normal file
8
src/operators/finalize.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Observable } from '../Observable';
|
||||
import { OperatorFunction } from './types';
|
||||
|
||||
export function finalize<T>(callback: () => void): OperatorFunction<T, T> {
|
||||
void Observable;
|
||||
void callback;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
8
src/operators/map.ts
Normal file
8
src/operators/map.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Observable } from '../Observable';
|
||||
import { OperatorFunction } from './types';
|
||||
|
||||
export function map<T, R>(project: (value: T) => R): OperatorFunction<T, R> {
|
||||
void Observable;
|
||||
void project;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
8
src/operators/retry.ts
Normal file
8
src/operators/retry.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Observable } from '../Observable';
|
||||
import { OperatorFunction } from './types';
|
||||
|
||||
export function retry<T>(limit?: number): OperatorFunction<T, T> {
|
||||
void Observable;
|
||||
void limit;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
8
src/operators/scan.ts
Normal file
8
src/operators/scan.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Observable } from '../Observable';
|
||||
import { OperatorFunction } from './types';
|
||||
|
||||
export function scan<T, R>(accumulator: (acc: R, value: T) => R, seed: R): OperatorFunction<T, R> {
|
||||
void Observable;
|
||||
void accumulator, seed;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
7
src/operators/switchMap.ts
Normal file
7
src/operators/switchMap.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { Observable } from '../Observable';
|
||||
import { OperatorFunction } from './types';
|
||||
|
||||
export function switchMap<T, R>(project: (value: T) => Observable<R>): OperatorFunction<T, R> {
|
||||
void project;
|
||||
throw new Error('TODO');
|
||||
}
|
||||
3
src/operators/types.ts
Normal file
3
src/operators/types.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { Observable } from '../Observable';
|
||||
|
||||
export type OperatorFunction<T, R> = (observable: Observable<T>) => Observable<R>;
|
||||
8
src/tsconfig.json
Normal file
8
src/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@trapcodien/tsconfig/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": "../dist"
|
||||
},
|
||||
"references": [{ "path": "../" }]
|
||||
}
|
||||
10
tsconfig.json
Normal file
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"rootDir": ".",
|
||||
"outDir": ".",
|
||||
"resolveJsonModule": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"files": ["package.json"]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user