diff --git a/history/package.json b/history/package.json index ac7fdaafb..5cb15ab2b 100644 --- a/history/package.json +++ b/history/package.json @@ -17,35 +17,27 @@ "history": "^4.3.0" }, "devDependencies": { - "@cycle/base": "^4.1.1", - "@cycle/dom": "^13.0.0", - "@cycle/most-adapter": "^4.0.1", - "@cycle/most-run": "^4.1.3", - "@cycle/rxjs-adapter": "^3.0.3", - "@cycle/rxjs-run": "^3.0.3", - "@cycle/xstream-adapter": "^3.0.4", - "@cycle/xstream-run": "^3.1.0", + "@cycle/dom": "15.0.0-rc.1", + "@cycle/rxjs-run": "4.0.0-rc.3", + "@cycle/run": "1.0.0-rc.7", "@types/history": "^4.5.0", "@types/mocha": "^2.2.32", "@types/node": "^6.0.46", - "most": "^1.0.4", "rxjs": "5.0.1", "saucie": "^1.4.1", - "ts-node": "^1.6.1", - "xstream": "9.x.x" + "xstream": "10.x.x" }, "scripts": { "lint": "../node_modules/.bin/tslint -c ../tslint.json src/**/*.ts", - "test-node": "../node_modules/.bin/mocha -r ts-node/register test/node/*.ts", - "build-browser-tests": "../node_modules/.bin/browserify -p tsify test/browser/index.ts -o test/browser/bundle.js", - "test-browser": "../node_modules/.bin/testem ci -l Chrome,Firefox", + "prelib": "rm -rf lib/ && mkdir -p lib", + "lib": "../node_modules/.bin/tsc", + "test-node": "../node_modules/.bin/mocha -r ts-node/register test/node/index.ts", + "test-browser": "../node_modules/.bin/testem -l Chrome", "test-browser-ci": "../node_modules/.bin/testem ci", "test": "npm run lint && npm run lib && npm run test-node && npm run test-browser", "test-ci": "npm run lint && npm run lib && npm run test-node && npm run test-browser-ci", "browserify": "../node_modules/.bin/browserify lib/index.js --standalone CycleHistory --outfile dist/cycle-history.js", "uglify": "../node_modules/.bin/uglifyjs dist/cycle-history.js -o dist/cycle-history.min.js", - "prelib": "rm -rf lib/ && mkdir -p lib", - "lib": "../node_modules/.bin/tsc", "predist": "rm -rf dist/ && mkdir -p dist/", "dist": "npm run lib && npm run browserify && npm run uglify", "readme": "node ../.scripts/make-api-docs.js ${PWD##*/} && cat ./.scripts/template-readme.md ./generated-api.md > README.md && rm ./generated-api.md", diff --git a/history/src/captureClicks.ts b/history/src/captureClicks.ts index 4cce31cd5..90d353c42 100644 --- a/history/src/captureClicks.ts +++ b/history/src/captureClicks.ts @@ -1,7 +1,17 @@ -import { StreamAdapter } from '@cycle/base'; - -const clickEvent = 'undefined' !== typeof document && document.ontouchstart ? - 'touchstart' : 'click'; +import xs, {Stream, MemoryStream} from 'xstream'; +import { + HistoryInput, + HistoryDriver, + GoBackHistoryInput, + GoForwardHistoryInput, + GoHistoryInput, + PushHistoryInput, + ReplaceHistoryInput, +} from './types'; + +const CLICK_EVENT = typeof document !== 'undefined' && document.ontouchstart ? + 'touchstart' : + 'click'; function which(ev: any) { if (typeof window === 'undefined') { @@ -19,8 +29,8 @@ function sameOrigin(href: string) { return href && href.indexOf(window.location.origin) === 0; } -function makeClickListener(push: Function) { - return function clickListener(event: any) { +function makeClickListener(push: (p: string) => void) { + return function clickListener(event: MouseEvent) { if (which(event) !== 1) { return; } @@ -33,7 +43,7 @@ function makeClickListener(push: Function) { return; } - let element = event.target; + let element: any = event.target; while (element && element.nodeName !== 'A') { element = element.parentNode; } @@ -61,23 +71,20 @@ function makeClickListener(push: Function) { }; } -function captureAnchorClicks(push: Function) { +function captureAnchorClicks(push: (p: string) => void) { const listener = makeClickListener(push); if (typeof window !== 'undefined') { - document.addEventListener(clickEvent, listener, false); + document.addEventListener(CLICK_EVENT, listener, false); } } -export function captureClicks(historyDriver: (sink$: any, runStreamAdapter: StreamAdapter) => any) { - return function historyDriverWithClickCapture(sink$: any, runStreamAdapter: StreamAdapter): any { - const { observer, stream } = runStreamAdapter.makeSubject(); - +export function captureClicks(historyDriver: HistoryDriver): HistoryDriver { + return function historyDriverWithClickCapture(sink$: Stream) { + const internalSink$ = xs.create(); captureAnchorClicks((pathname: string) => { - observer.next({ type: 'push', pathname }); + internalSink$._n({type: 'push', pathname}); }); - - runStreamAdapter.streamSubscribe(sink$, observer); - - return historyDriver(stream, runStreamAdapter); + sink$._add(internalSink$); + return historyDriver(internalSink$); }; } diff --git a/history/src/createHistory$.ts b/history/src/createHistory$.ts index 91964a74f..cae73e233 100644 --- a/history/src/createHistory$.ts +++ b/history/src/createHistory$.ts @@ -1,27 +1,19 @@ -import { StreamAdapter } from '@cycle/base'; -import { Location, History, UnregisterCallback } from 'history'; -import { HistoryInput } from './types'; - -export function createHistory$ (history: History, sink$: any, - runStreamAdapter: StreamAdapter): any { - const push = makePushState(history); - - const { observer, stream } = runStreamAdapter.makeSubject(); - - const history$ = runStreamAdapter.remember(stream); - - const unlisten = history.listen((loc: Location) => { - observer.next(loc); - }); - - (history$ as any).dispose = - runStreamAdapter.streamSubscribe(sink$, createObserver(push, unlisten)); - +import xs, {Stream, MemoryStream, Listener} from 'xstream'; +import {Location, History, UnregisterCallback} from 'history'; +import {HistoryInput} from './types'; + +export function createHistory$(history: History, + sink$: Stream): MemoryStream { + const history$ = xs.createWithMemory(); + const call = makeCallOnHistory(history); + const unlisten = history.listen((loc: Location) => { history$._n(loc); }); + const sub = sink$.subscribe(createObserver(call, unlisten)); + (history$ as any).dispose = () => { sub.unsubscribe(); unlisten(); }; return history$; }; -function makePushState (history: History) { - return function pushState (input: HistoryInput): void { +function makeCallOnHistory(history: History) { + return function call(input: HistoryInput): void { if (input.type === 'push') { history.push(input.pathname, input.state); } @@ -44,17 +36,17 @@ function makePushState (history: History) { }; } -function createObserver (push: (input: HistoryInput) => any, - unlisten: UnregisterCallback) { +function createObserver(call: (input: HistoryInput) => void, + unlisten: UnregisterCallback): Listener { return { - next (input: HistoryInput | String) { + next (input: HistoryInput | string) { if (typeof input === 'string') { - push({ type: 'push', pathname: input }); + call({type: 'push', pathname: input}); } else { - push(input as HistoryInput); + call(input); } }, - error: unlisten, - complete: unlisten, + error: (err) => { unlisten(); }, + complete: () => { setTimeout(unlisten); }, }; } diff --git a/history/src/drivers.ts b/history/src/drivers.ts new file mode 100644 index 000000000..b2ca9e477 --- /dev/null +++ b/history/src/drivers.ts @@ -0,0 +1,51 @@ +import {Stream, MemoryStream} from 'xstream'; +import { + createBrowserHistory, + createMemoryHistory, + createHashHistory, + BrowserHistoryBuildOptions, + MemoryHistoryBuildOptions, + HashHistoryBuildOptions, + Location, +} from 'history'; +import {createHistory$} from './createHistory$'; +import { + HistoryInput, + HistoryDriver, + GoBackHistoryInput, + GoForwardHistoryInput, + GoHistoryInput, + PushHistoryInput, + ReplaceHistoryInput, +} from './types'; + +/** + * Create a History Driver to be used in the browser. + */ +export function makeHistoryDriver(options?: BrowserHistoryBuildOptions): HistoryDriver { + const history = createBrowserHistory(options); + return function historyDriver(sink$: Stream) { + return createHistory$(history, sink$); + }; +} + +/** + * Create a History Driver to be used in non-browser enviroments + * such as server-side node.js. + */ +export function makeServerHistoryDriver(options?: MemoryHistoryBuildOptions): HistoryDriver { + const history = createMemoryHistory(options); + return function serverHistoryDriver(sink$: Stream) { + return createHistory$(history, sink$); + }; +} + +/** + * Create a History Driver for older browsers using hash routing + */ +export function makeHashHistoryDriver(options?: HashHistoryBuildOptions): HistoryDriver { + const history = createHashHistory(options); + return function hashHistoryDriver(sink$: Stream) { + return createHistory$(history, sink$); + }; +} diff --git a/history/src/historyDriver.ts b/history/src/historyDriver.ts deleted file mode 100644 index 326610177..000000000 --- a/history/src/historyDriver.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { StreamAdapter } from '@cycle/base'; -import { - createBrowserHistory, - createMemoryHistory, - createHashHistory, - BrowserHistoryBuildOptions, - MemoryHistoryBuildOptions, - HashHistoryBuildOptions, -} from 'history'; -import { createHistory$ } from './createHistory$'; - -/** - * Create a History Driver to be used in the browser. - */ -export function makeHistoryDriver (options?: BrowserHistoryBuildOptions) { - const history = createBrowserHistory(options); - return function historyDriver (sink$: any, runStreamAdapter: StreamAdapter): any { - return createHistory$(history, sink$, runStreamAdapter); - }; -} - -/** - * Create a History Driver to be used in non-browser enviroments - * such as server-side node.js. - */ -export function makeServerHistoryDriver (options?: MemoryHistoryBuildOptions) { - const history = createMemoryHistory(options); - return function serverHistoryDriver (sink$: any, runStreamAdapter: StreamAdapter) { - return createHistory$(history, sink$, runStreamAdapter); - }; -} - -/** - * Create a History Driver for older browsers using hash routing - */ -export function makeHashHistoryDriver (options?: HashHistoryBuildOptions) { - const history = createHashHistory(options); - return function hashHistoryDriver (sink$: any, runStreamAdapter: StreamAdapter) { - return createHistory$(history, sink$, runStreamAdapter); - }; -} diff --git a/history/src/index.ts b/history/src/index.ts index 67eb1575a..3b5990edb 100644 --- a/history/src/index.ts +++ b/history/src/index.ts @@ -1,4 +1,4 @@ -export { Location } from 'history'; +export {Location} from 'history'; export * from './types'; -export * from './historyDriver'; +export * from './drivers'; export * from './captureClicks'; diff --git a/history/src/types.ts b/history/src/types.ts index e9bacc19c..468966b65 100644 --- a/history/src/types.ts +++ b/history/src/types.ts @@ -1,29 +1,33 @@ +import {Stream, MemoryStream} from 'xstream'; +import {Location} from 'history'; export type Pathname = string; +export type HistoryDriver = (sink$: Stream) => MemoryStream; + export interface PushHistoryInput { type: 'push'; pathname: Pathname; state?: any; -}; +} export interface ReplaceHistoryInput { type: 'replace'; pathname: Pathname; state?: any; -}; +} export interface GoHistoryInput { type: 'go'; amount: number; -}; +} export interface GoBackHistoryInput { type: 'goBack'; -}; +} export interface GoForwardHistoryInput { type: 'goForward'; -}; +} export type HistoryInput = PushHistoryInput diff --git a/history/test/browser/captureClicks.ts b/history/test/browser/captureClicks.ts new file mode 100644 index 000000000..12c60b4d4 --- /dev/null +++ b/history/test/browser/captureClicks.ts @@ -0,0 +1,34 @@ +/// +/// +import * as assert from 'assert'; +import xs from 'xstream'; +import {makeHashHistoryDriver, captureClicks, Location} from '../../src'; + +describe('captureClicks', () => { + beforeEach(() => { + window.location.hash = ''; + }); + + it('should allow listening to link clicks and change route', function (done) { + const historyDriver = makeHashHistoryDriver(); + const history$ = captureClicks(historyDriver)(xs.never()); + + const sub = history$.subscribe({ + next: (location: Location) => { + assert.strictEqual(location.pathname, '/test'); + sub.unsubscribe(); + done(); + }, + error: (err) => {}, + complete: () => {}, + }); + + const a = document.createElement('a'); + a.href = '/test'; + document.body.appendChild(a); + + setTimeout(() => { + a.click(); + }); + }); +}); diff --git a/history/test/browser/common.ts b/history/test/browser/common.ts new file mode 100644 index 000000000..b2c5516a6 --- /dev/null +++ b/history/test/browser/common.ts @@ -0,0 +1,24 @@ +/// +/// +import * as assert from 'assert'; +import {makeHashHistoryDriver, makeHistoryDriver} from '../../src'; + +describe('makeHistoryDriver', () => { + it('should be a function', () => { + assert.strictEqual(typeof makeHistoryDriver, 'function'); + }); + + it('should return a function' , () => { + assert.strictEqual(typeof makeHistoryDriver(), 'function'); + }); +}); + +describe('makeHashHistoryDriver', () => { + it('should be a function', () => { + assert.strictEqual(typeof makeHashHistoryDriver, 'function'); + }); + + it('should return a function' , () => { + assert.strictEqual(typeof makeHashHistoryDriver(), 'function'); + }); +}); diff --git a/history/test/browser/index.ts b/history/test/browser/index.ts index 317a60f5a..5ab2d4eac 100644 --- a/history/test/browser/index.ts +++ b/history/test/browser/index.ts @@ -1,258 +1,4 @@ -/// -/// - -import * as assert from 'assert'; -import { StreamAdapter, Observer } from '@cycle/base'; -import XStreamAdapter from '@cycle/xstream-adapter'; -import MostAdapter from '@cycle/most-adapter'; -import RxJSAdapter from '@cycle/rxjs-adapter'; -import xs from 'xstream'; -import { makeHashHistoryDriver, captureClicks, Location } from '../../src'; - -describe('makeServerHistoryDriver', () => { - it('should be a function', () => { - assert.strictEqual(typeof makeHashHistoryDriver, 'function'); - }); - - it('should return a function' , () => { - assert.strictEqual(typeof makeHashHistoryDriver(), 'function'); - }); -}); - -describe('captureClicks', () => { - beforeEach(() => { - window.location.hash = ''; - }); - - it('should listen to link clicks and change route', (done) => { - const historyDriver = makeHashHistoryDriver(); - - const history$ = captureClicks(historyDriver)(xs.never(), XStreamAdapter); - - (history$ as any).addListener({ - next: (location: Location) => { - assert.strictEqual(location.pathname, url('/test')); - done(); - }, - }); - - let a = document.createElement('a'); - a.href = url('/test'); - document.body.appendChild(a); - - setTimeout(() => { - a.click(); - }); - }); -}); - -runTests(XStreamAdapter, 'xstream'); -runTests(MostAdapter, 'most'); -runTests(RxJSAdapter, 'RxJS'); - -function runTests (adapter: StreamAdapter, streamLibrary: string) { - describe(`historyDriver - ${streamLibrary}`, () => { - // beforeEach(() => { - // window.location.hash = ''; - // }); - - it('should return a stream', () => { - const stream = adapter.makeSubject().stream; - const historyDriver = makeHashHistoryDriver(); - - assert(adapter.isValidStream(historyDriver(stream, adapter))); - }); - - it('should create a location from pathname', (done) => { - const { next, listen } = buildTest(adapter); - - const unlisten = listen({ - next (location: Location) { - assert.strictEqual(location.pathname, url('/test')); - done(); - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next(url('/test')); - return unlisten && unlisten(); - }, 0); - }); - - it('should create a location from PushHistoryInput', (done) => { - const { next, listen } = buildTest(adapter); - - const unlisten = listen({ - next (location: Location) { - assert.strictEqual(location.pathname, url('/test')); - done(); - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next({ type: 'push', pathname: url('/test') }); - return unlisten && unlisten(); - }, 0); - }); - - it('should create a location from ReplaceHistoryInput', (done) => { - const { next, listen } = buildTest(adapter); - - const unlisten = listen({ - next (location: Location) { - assert.strictEqual(location.pathname, url('/test')); - done(); - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next({ type: 'replace', pathname: url('/test') }); - return unlisten && unlisten(); - }, 0); - }); - - // going back and forth is unreliable in tests for browser - // because they cause refresh? - it.only('should allow going back a route with type `go`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - url('/test'), - url('/other'), - url('/test'), - ]; - - const unlisten = listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next(url('/test')); - next(url('/other')); - next({ type: 'go', amount: -3 }); - return unlisten && unlisten(); - }, 0); - }); - - it.skip('should allow going back a route with type `goBack`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - url('/test'), - url('/other'), - url('/test'), - ]; - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next(url('/test')); - next(url('/other')); - next({ type: 'goBack' }); - }, 0); - }); - - it.skip('should allow going forward a route with type `go`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - '/test', - '/other', - '/test', - '/other', - ]; - - const unlisten = listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - next('/test'); - next('/other'); - next({ type: 'goBack' }); - next({ type: 'go', amount: 1 }); - return unlisten && unlisten(); - }); - - it.skip('should allow going forward a route with type `goForward`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - url('/test'), - url('/other'), - url('/test'), - url('/other'), - ]; - - const unlisten = listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next(url('/test')); - next(url('/other')); - next({ type: 'goBack' }); - next({ type: 'goForward' }); - return unlisten && unlisten(); - }, 0); - }); - }); -} - -function url (path: string) { - return path; -} - -function buildTest (adapter: StreamAdapter) { - const { observer: { next }, stream } = adapter.makeSubject(); - const historyDriver = makeHashHistoryDriver(); - - const history$ = historyDriver(stream, adapter); - - function listen (observer: Observer) { - const noopObserver = { - next: () => void 0, - error: () => void 0, - complete: () => void 0, - }; - - return adapter.streamSubscribe(history$, (Object as any).assign({}, noopObserver, observer)); - } - - return { next, listen }; -} +import './common'; +import './captureClicks'; +import './rxjs'; +import './xstream'; diff --git a/history/test/browser/rxjs.ts b/history/test/browser/rxjs.ts new file mode 100644 index 000000000..27e9e49f7 --- /dev/null +++ b/history/test/browser/rxjs.ts @@ -0,0 +1,124 @@ +/// +/// +import * as assert from 'assert'; +import {Observable} from 'rxjs'; +import {setup, run} from '@cycle/rxjs-run'; +import {makeHashHistoryDriver, captureClicks, Location, HistoryInput} from '../../src'; + +let dispose = () => {}; + +describe('historyDriver - RxJS', () => { + beforeEach(function () { + dispose(); + }); + + it('should return a stream', () => { + function main(sources: {history: Observable}) { + assert.strictEqual(typeof sources.history.switchMap, 'function'); + return { + history: Observable.never(), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + assert.strictEqual(typeof sources.history.switchMap, 'function'); + }); + + it('should create a location from pathname', function (done) { + function main(sources: {history: Observable}) { + return { + history: Observable.of('/test'), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); + + it('should create a location from PushHistoryInput', function (done) { + function main(sources: {history: Observable}) { + return { + history: Observable.of({type: 'push', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); + + it('should create a location from ReplaceHistoryInput', function (done) { + function main(sources: {history: Observable}) { + return { + history: Observable.of({type: 'replace', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); + + it('should allow going back/forwards with `go`, `goBack`, `goForward`', function (done) { + function main(sources: {history: Observable}) { + return { + history: Observable.interval(100).take(6).map(i => [ + '/test', + '/other', + {type: 'go', amount: -1}, + {type: 'go', amount: +1}, + {type: 'goBack'}, + {type: 'goForward'}, + ][i]), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + '/other', + '/test', + '/other', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); +}); diff --git a/history/test/browser/xstream.ts b/history/test/browser/xstream.ts new file mode 100644 index 000000000..bc2a03c18 --- /dev/null +++ b/history/test/browser/xstream.ts @@ -0,0 +1,126 @@ +/// +/// +import * as assert from 'assert'; +import xs, {Stream} from 'xstream'; +import {setup, run} from '@cycle/run'; +import {setAdapt} from '@cycle/run/lib/adapt'; +import {makeHashHistoryDriver, captureClicks, Location, HistoryInput} from '../../src'; + +let dispose = () => {}; + +describe('historyDriver - xstream', () => { + beforeEach(function () { + setAdapt(x => x); + dispose(); + }); + + it('should return a stream', () => { + function main(sources: {history: Stream}) { + assert.strictEqual(typeof sources.history.remember, 'function'); + return { + history: xs.never(), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + assert.strictEqual(typeof sources.history.remember, 'function'); + }); + + it('should create a location from pathname', function (done) { + function main(sources: {history: Stream}) { + return { + history: xs.of('/test'), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); + + it('should create a location from PushHistoryInput', function (done) { + function main(sources: {history: Stream}) { + return { + history: xs.of({type: 'push', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); + + it('should create a location from ReplaceHistoryInput', function (done) { + function main(sources: {history: Stream}) { + return { + history: xs.of({type: 'replace', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); + + it('should allow going back/forwards with `go`, `goBack`, `goForward`', function (done) { + function main(sources: {history: Stream}) { + return { + history: xs.periodic(100).take(6).map(i => [ + '/test', + '/other', + {type: 'go', amount: -1}, + {type: 'go', amount: +1}, + {type: 'goBack'}, + {type: 'goForward'}, + ][i]), + }; + } + + const {sources, run} = setup(main, { history: makeHashHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + '/other', + '/test', + '/other', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + dispose = run(); + }); +}); diff --git a/history/test/node/common.ts b/history/test/node/common.ts new file mode 100644 index 000000000..4add2a181 --- /dev/null +++ b/history/test/node/common.ts @@ -0,0 +1,14 @@ +/// +/// +import * as assert from 'assert'; +import {makeServerHistoryDriver} from '../../src'; + +describe('makeServerHistoryDriver', () => { + it('should be a function', () => { + assert.strictEqual(typeof makeServerHistoryDriver, 'function'); + }); + + it('should return a function' , () => { + assert.strictEqual(typeof makeServerHistoryDriver(), 'function'); + }); +}); diff --git a/history/test/node/index.ts b/history/test/node/index.ts index 9892263c7..8c4e1b629 100644 --- a/history/test/node/index.ts +++ b/history/test/node/index.ts @@ -1,213 +1,3 @@ -/// -/// - -import * as assert from 'assert'; -import { StreamAdapter, Observer } from '@cycle/base'; -import XStreamAdapter from '@cycle/xstream-adapter'; -import MostAdapter from '@cycle/most-adapter'; -import RxJSAdapter from '@cycle/rxjs-adapter'; -import { makeServerHistoryDriver, Location } from '../../src'; - -describe('makeServerHistoryDriver', () => { - it('should be a function', () => { - assert.strictEqual(typeof makeServerHistoryDriver, 'function'); - }); - - it('should return a function' , () => { - assert.strictEqual(typeof makeServerHistoryDriver(), 'function'); - }); -}); - -runTests(XStreamAdapter, 'xstream'); -runTests(MostAdapter, 'most'); -runTests(RxJSAdapter, 'RxJS'); - -function runTests (adapter: StreamAdapter, streamLibrary: string) { - describe(`serverHistoryDriver - ${streamLibrary}`, () => { - it('should return a stream', () => { - const stream = adapter.makeSubject().stream; - const historyDriver = makeServerHistoryDriver(); - - assert(adapter.isValidStream(historyDriver(stream, adapter))); - }); - - it('should create a location from pathname', (done) => { - const { next, listen } = buildTest(adapter); - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, '/test'); - done(); - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { next('/test'); }, 0); - }); - - it('should create a location from PushHistoryInput', (done) => { - const { next, listen } = buildTest(adapter); - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, '/test'); - done(); - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next({ type: 'push', pathname: '/test' }); - }, 0); - }); - - it('should create a location from ReplaceHistoryInput', (done) => { - const { next, listen } = buildTest(adapter); - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, '/test'); - done(); - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { next({ type: 'replace', pathname: '/test' }); }, 0); - }); - - it('should allow going back a route with type `go`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - '/test', - '/other', - '/test', - ]; - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next('/test'); - next('/other'); - next({ type: 'go', amount: -1 }); - }, 0); - }); - - it('should allow going back a route with type `goBack`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - '/test', - '/other', - '/test', - ]; - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next('/test'); - next('/other'); - next({ type: 'goBack' }); - }, 0); - }); - - it('should allow going forward a route with type `go`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - '/test', - '/other', - '/test', - '/other', - ]; - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next('/test'); - next('/other'); - next({ type: 'go', amount: -1 }); - next({ type: 'go', amount: 1 }); - }, 0); - }); - - it('should allow going forward a route with type `goForward`', (done) => { - const { next, listen } = buildTest(adapter); - - const expected = [ - '/test', - '/other', - '/test', - '/other', - ]; - - listen({ - next (location: Location) { - assert.strictEqual(location.pathname, expected.shift()); - if (expected.length === 0) { - done(); - } - }, - error: done, - complete: () => void 0, - }); - - setTimeout(() => { - next('/test'); - next('/other'); - next({ type: 'go', amount: -1 }); - next({ type: 'goForward' }); - }, 0); - }); - }); -} - - -function buildTest (adapter: StreamAdapter) { - const { observer: { next }, stream } = adapter.makeSubject(); - const historyDriver = makeServerHistoryDriver(); - - const history$ = historyDriver(stream, adapter); - - function listen (observer: Observer) { - const noopObserver = { - next: () => void 0, - error: () => void 0, - complete: () => void 0, - }; - - adapter.streamSubscribe(history$, (Object as any).assign({}, noopObserver, observer)); - } - - return { next, listen }; -} +import './common'; +import './rxjs'; +import './xstream'; \ No newline at end of file diff --git a/history/test/node/rxjs.ts b/history/test/node/rxjs.ts new file mode 100644 index 000000000..931cac4a5 --- /dev/null +++ b/history/test/node/rxjs.ts @@ -0,0 +1,212 @@ +/// +/// +import * as assert from 'assert'; +import {Observable} from 'rxjs'; +import {setup, run} from '@cycle/rxjs-run'; +import {makeServerHistoryDriver, Location, HistoryInput} from '../../src'; + +describe('serverHistoryDriver - RxJS', function () { + it('should return an Rx Observable as source', function () { + function main(sources: {history: Observable}) { + assert.strictEqual(typeof sources.history.switchMap, 'function'); + return { + history: Observable.never(), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + assert.strictEqual(typeof sources.history.switchMap, 'function'); + }); + + it('should create a location from pathname', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of('/test'), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should create a location from PushHistoryInput', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of({type: 'push', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should create a location from ReplaceHistoryInput', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of({type: 'replace', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going back a route with type `go`', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of( + '/test', + '/other', + { type: 'go', amount: -1 }, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going back a route with type `goBack`', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of( + '/test', + '/other', + {type: 'goBack'}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going forward a route with type `go`', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of( + '/test', + '/other', + {type: 'go', amount: -1}, + {type: 'go', amount: 1}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + '/other', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going forward a route with type `goForward`', (done) => { + function main(sources: {history: Observable}) { + return { + history: Observable.of( + '/test', + '/other', + {type: 'go', amount: -1}, + {type: 'goForward'}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + '/other', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); +}); diff --git a/history/test/node/xstream.ts b/history/test/node/xstream.ts new file mode 100644 index 000000000..9e3de2248 --- /dev/null +++ b/history/test/node/xstream.ts @@ -0,0 +1,217 @@ +/// +/// +import * as assert from 'assert'; +import xs, {Stream} from 'xstream'; +import {setup, run} from '@cycle/run'; +import {setAdapt} from '@cycle/run/lib/adapt'; +import {makeServerHistoryDriver, Location, HistoryInput} from '../../src'; + +describe('serverHistoryDriver - xstream', function () { + beforeEach(function () { + setAdapt(x => x); + }); + + it('should return a stream', function () { + function main(sources: {history: Stream}) { + assert.strictEqual(typeof sources.history.remember, 'function'); + return { + history: xs.never(), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + assert.strictEqual(typeof sources.history.remember, 'function'); + }); + + it('should create a location from pathname', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of('/test'), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should create a location from PushHistoryInput', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of({type: 'push', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should create a location from ReplaceHistoryInput', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of({type: 'replace', pathname: '/test'}), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, '/test'); + done(); + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going back a route with type `go`', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of( + '/test', + '/other', + {type: 'go', amount: -1}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going back a route with type `goBack`', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of( + '/test', + '/other', + {type: 'goBack'}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going forward a route with type `go`', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of( + '/test', + '/other', + {type: 'go', amount: -1}, + {type: 'go', amount: 1}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + '/other', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); + + it('should allow going forward a route with type `goForward`', (done) => { + function main(sources: {history: Stream}) { + return { + history: xs.of( + '/test', + '/other', + {type: 'go', amount: -1}, + {type: 'goForward'}, + ), + }; + } + + const {sources, run} = setup(main, { history: makeServerHistoryDriver() }); + + const expected = [ + '/test', + '/other', + '/test', + '/other', + ]; + + sources.history.subscribe({ + next (location: Location) { + assert.strictEqual(location.pathname, expected.shift()); + if (expected.length === 0) { + done(); + } + }, + error: done, + complete: () => { done('complete should not be called'); }, + }); + run(); + }); +});