@cycle/isolate
Version:
A utility function to make scoped dataflow components in Cycle.js
650 lines (589 loc) • 17.3 kB
text/typescript
// tslint:disable-next-line
import 'mocha';
import 'symbol-observable'; // tslint:disable-line
import * as assert from 'assert';
import {of, from, Observable} from 'rxjs';
import isolate from '../src/index';
import {setAdapt} from '@cycle/run/lib/adapt';
setAdapt(from as any);
describe('isolate', function() {
beforeEach(function() {
(isolate as any).reset();
});
it('should be a function', function() {
assert.strictEqual(typeof isolate, 'function');
});
it('should throw if first argument is not a function', function() {
assert.throws(() => {
isolate('not a function' as any);
}, /First argument given to isolate\(\) must be a 'dataflowComponent' function/i);
});
it('should throw if second argument is null', function() {
function MyDataflowComponent() {}
assert.throws(() => {
isolate(MyDataflowComponent, null);
}, /Second argument given to isolate\(\) must not be null/i);
});
it('should convert the 2nd argument to string if it is not a string', function() {
function MyDataflowComponent() {}
assert.doesNotThrow(() => {
isolate(MyDataflowComponent, 12);
});
});
it('should return a function', function() {
function MyDataflowComponent() {}
const scopedMyDataflowComponent = isolate(MyDataflowComponent, `myScope`);
assert.strictEqual(typeof scopedMyDataflowComponent, `function`);
});
it('should make a new scope if second argument is undefined', function() {
function MyDataflowComponent() {}
const scopedMyDataflowComponent = isolate(MyDataflowComponent);
assert.strictEqual(typeof scopedMyDataflowComponent, `function`);
});
it('should accept a scopes-per-channel object as the second argument', function(done) {
function Component(_sources: any) {
return {
first: _sources.first.getSink(),
second: _sources.second.getSink(),
};
}
const scopedComponent = isolate(Component, {
first: 'scope1',
second: 'scope2',
});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';
const sources = {
first: {
getSink() {
return of(10);
},
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},
second: {
getSink() {
return of(20);
},
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);
assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, 'scope2');
assert.strictEqual(actual4, 'scope2');
let hasFirst = false;
sinks.first.subscribe((x: any) => {
assert.strictEqual(hasFirst, false);
assert.strictEqual(x, 10);
hasFirst = true;
});
sinks.second.subscribe((x: any) => {
assert.strictEqual(hasFirst, true);
assert.strictEqual(x, 20);
done();
});
});
it('should not isolate a channel given null scope', function(done) {
function Component(_sources: any) {
return {
first: _sources.first.getSink(),
second: _sources.second.getSink(),
};
}
const scopedComponent = isolate(Component, {
first: null,
second: 'scope2',
});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';
const sources = {
first: {
getSink() {
return of(10);
},
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},
second: {
getSink() {
return of(20);
},
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);
assert.strictEqual(actual1, '');
assert.strictEqual(actual2, '');
assert.strictEqual(actual3, 'scope2');
assert.strictEqual(actual4, 'scope2');
let hasFirst = false;
sinks.first.subscribe((x: any) => {
assert.strictEqual(hasFirst, false);
assert.strictEqual(x, 10);
hasFirst = true;
});
sinks.second.subscribe((x: any) => {
assert.strictEqual(hasFirst, true);
assert.strictEqual(x, 20);
done();
});
});
it('should generate a scope if a channel is undefined in scopes-per-channel', function(done) {
function Component(_sources: any) {
return {
first: _sources.first.getSink(),
second: _sources.second.getSink(),
};
}
const scopedComponent = isolate(Component, {first: 'scope1'});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';
const sources = {
first: {
getSink() {
return of(10);
},
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},
second: {
getSink() {
return of(20);
},
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);
assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, 'cycle1');
assert.strictEqual(actual4, 'cycle1');
let hasFirst = false;
sinks.first.subscribe((x: any) => {
assert.strictEqual(hasFirst, false);
assert.strictEqual(x, 10);
hasFirst = true;
});
sinks.second.subscribe((x: any) => {
assert.strictEqual(hasFirst, true);
assert.strictEqual(x, 20);
done();
});
});
it('should accept a wildcard * in the scopes-per-channel object', function(done) {
function Component(_sources: any) {
return {
first: _sources.first.getSink(),
second: _sources.second.getSink(),
};
}
const scopedComponent = isolate(Component, {
first: 'scope1',
'*': 'default',
});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';
const sources = {
first: {
getSink() {
return of(10);
},
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},
second: {
getSink() {
return of(20);
},
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);
assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, 'default');
assert.strictEqual(actual4, 'default');
let hasFirst = false;
sinks.first.subscribe((x: any) => {
assert.strictEqual(hasFirst, false);
assert.strictEqual(x, 10);
hasFirst = true;
});
sinks.second.subscribe((x: any) => {
assert.strictEqual(hasFirst, true);
assert.strictEqual(x, 20);
done();
});
});
it('should not isolate a non-specified channel if wildcard * is null', function(done) {
function Component(_sources: any) {
return {
first: _sources.first.getSink(),
second: _sources.second.getSink(),
};
}
const scopedComponent = isolate(Component, {
first: 'scope1',
'*': null,
});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';
const sources = {
first: {
getSink() {
return of(10);
},
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},
second: {
getSink() {
return of(20);
},
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);
assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, '');
assert.strictEqual(actual4, '');
let hasFirst = false;
sinks.first.subscribe((x: any) => {
assert.strictEqual(hasFirst, false);
assert.strictEqual(x, 10);
hasFirst = true;
});
sinks.second.subscribe((x: any) => {
assert.strictEqual(hasFirst, true);
assert.strictEqual(x, 20);
done();
});
});
it('should not convert to string values in scopes-per-channel object', function(done) {
function Component(_sources: any) {
return {
first: _sources.first.getSink(),
second: _sources.second.getSink(),
};
}
const scopedComponent = isolate(Component, {first: 123, second: 456});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';
const sources = {
first: {
getSink() {
return of(10);
},
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},
second: {
getSink() {
return of(20);
},
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);
assert.strictEqual(actual1, 123);
assert.strictEqual(actual2, 123);
assert.strictEqual(actual3, 456);
assert.strictEqual(actual4, 456);
let hasFirst = false;
sinks.first.subscribe((x: any) => {
assert.strictEqual(hasFirst, false);
assert.strictEqual(x, 10);
hasFirst = true;
});
sinks.second.subscribe((x: any) => {
assert.strictEqual(hasFirst, true);
assert.strictEqual(x, 20);
done();
});
});
describe('scopedDataflowComponent', function() {
it('should return a valid dataflow component', function(done) {
function driver() {
return {};
}
function MyDataflowComponent(
sources: {other: unknown},
foo: string,
bar: string
) {
return {
other: of([foo, bar]),
};
}
const scopedMyDataflowComponent = isolate(MyDataflowComponent);
const scopedSinks = scopedMyDataflowComponent(
{other: driver()},
`foo`,
`bar`
);
assert.strictEqual(typeof scopedSinks, `object`);
scopedSinks.other.subscribe(strings => {
assert.strictEqual(strings.join(), `foo,bar`);
done();
});
});
it('should return correct types when all inputs are typed', function(done) {
class MyTestSource {
constructor() {}
public isolateSource(so: MyTestSource, scope: string) {
return new MyTestSource();
}
public isolateSink(
sink: Observable<Array<string>>,
scope: string
): Observable<Array<string>> {
return sink;
}
}
function MyDataflowComponent(
sources: {other: MyTestSource},
foo: string,
bar: string
) {
return {
other: of([foo, bar]),
};
}
const scopedMyDataflowComponent = isolate(MyDataflowComponent);
const scopedSinks = scopedMyDataflowComponent(
{other: new MyTestSource()},
`foo`,
`bar`
) as {other: Observable<Array<string>>};
assert.strictEqual(typeof scopedSinks, `object`);
scopedSinks.other.subscribe(strings => {
assert.strictEqual(strings.join(), `foo,bar`);
done();
});
});
it('should return correct types when all inputs are typed', function(done) {
class MyTestSource {
constructor() {}
public isolateSource(so: MyTestSource, scope: string) {
return new MyTestSource();
}
public isolateSink(
sink: Observable<Array<string>>,
scope: string
): Observable<Array<number>> {
return of([123, 456]);
}
}
function MyDataflowComponent(
sources: {other: MyTestSource},
foo: string,
bar: string
) {
return {
other: of([foo, bar]),
};
}
const scopedMyDataflowComponent = isolate(MyDataflowComponent);
const scopedSinks = scopedMyDataflowComponent(
{other: new MyTestSource()},
`foo`,
`bar`
) as {other: Observable<Array<number>>};
assert.strictEqual(typeof scopedSinks, `object`);
scopedSinks.other.subscribe(nums => {
assert.strictEqual(nums.join(), `123,456`);
done();
});
});
it('should return correct types when all inputs are typed', function(done) {
function MyDataflowComponent(
sources: {other: Observable<string>},
foo: string,
bar: string
) {
return {
other: of([foo, bar]),
};
}
const scopedMyDataflowComponent = isolate(MyDataflowComponent);
const scopedSinks = scopedMyDataflowComponent(
{other: of<string>('foo')},
`foo`,
`bar`
);
assert.strictEqual(typeof scopedSinks, `object`);
scopedSinks.other.subscribe((x: Array<string>) => {
assert.strictEqual(x.join(), `foo,bar`);
done();
});
});
it('should call `isolateSource` of drivers', function() {
function driver() {
function isolateSource(source: any, scope: string) {
return source.someFunc(scope);
}
function someFunc(this: any, v: string) {
const scope = this.scope;
return {
scope: scope.concat(v),
someFunc,
isolateSource,
};
}
return {
scope: [],
someFunc,
isolateSource,
};
}
function MyDataflowComponent(sources: {other: any}) {
return {
other: sources.other.someFunc('a'),
};
}
const scopedMyDataflowComponent = isolate(MyDataflowComponent, `myScope`);
const scopedSinks = scopedMyDataflowComponent({other: driver()});
assert.strictEqual(scopedSinks.other.scope.length, 2);
assert.strictEqual(scopedSinks.other.scope[0], `myScope`);
assert.strictEqual(scopedSinks.other.scope[1], `a`);
});
it('should not call `isolateSink` for a sink-only driver', function() {
function driver(sink: any) {}
function MyDataflowComponent(sources: {other: any}) {
return {
other: of('a'),
};
}
let scopedMyDataflowComponent;
assert.doesNotThrow(function() {
scopedMyDataflowComponent = isolate(MyDataflowComponent, `myScope`);
});
const scopedSinks = (scopedMyDataflowComponent as any)({
other: driver(null),
});
scopedSinks.other.subscribe((x: any) => assert.strictEqual(x, 'a'));
});
it('should call `isolateSink` of drivers', function(done) {
function driver() {
function isolateSink(sink: any, scope: string) {
return sink.map((v: string) => `${v} ${scope}`);
}
return {
isolateSink,
};
}
function MyDataflowComponent(sources: {other: unknown}) {
return {
other: of('a'),
};
}
const scopedMyDataflowComponent = isolate(MyDataflowComponent, `myScope`);
const scopedSinks = scopedMyDataflowComponent({other: driver()});
const i = 0;
scopedSinks.other.subscribe(x => {
assert.strictEqual(x, 'a myScope');
done();
});
});
it('should handle undefined cases gracefully', function() {
const MyDataflowComponent = () => ({});
const scopedMyDataflowComponent = isolate(MyDataflowComponent, 'myScope');
assert.doesNotThrow(() =>
scopedMyDataflowComponent({noSource: void 0 as any})
);
});
});
});