Skip to content

Commit

Permalink
feat: add subscribeAction
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jan 17, 2019
1 parent 93ba5b2 commit c8c8c53
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 20 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"license": "MIT",
"devDependencies": {
"@types/jest": "^23.3.12",
"@types/lodash.clonedeep": "^4.5.4",
"@vue/test-utils": "^1.0.0-beta.28",
"codecov": "^3.1.0",
"eslint": "^5.12.0",
Expand Down
7 changes: 5 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ export class Store<S extends Dict = {}, G extends Dict = {}, Spy = jest.Mock> {
private _initialState: S
private _spy: SpyCreator<Spy>
private _modulesNamespaceMap: any
private _handlers: Function[]
private _mutationsHandlers: Function[]
private _actionsHandlers: Function[]

constructor(options?: StoreConstructorOptions<S, G, Spy>)

reset(): void
private _initialize(): void

subscribe(handler: Function): () => void
private _triggerSubscriptions(type: string, payload: any): void
subscribeAction(handler: Function): () => void
private _triggerMutationSubscriptions(type: string, payload: any): void
private _triggerActionSubscriptions(type: string, payload: any): void
}
64 changes: 52 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,25 @@ exports.Store = class Store {
}
) {
this._spy = spy
this.commit = this._spy.create(this._triggerSubscriptions.bind(this))
this.dispatch = this._spy.create()
this._initialGetters = getters
this._initialState = state
this._initialize()
// next few lines are for the sake of typings
// TODO: find a way of removing them
this.commit = this._spy.create(
this._triggerMutationSubscriptions.bind(this)
)
this.dispatch = this._spy.create(
this._triggerActionSubscriptions.bind(this)
)

this.getters = this._initialGetters = getters
this.state = this._initialState = state

/** @type {Function[]} */
this._mutationsHandlers = []
/** @type {Function[]} */
this._handlers = []
this._actionsHandlers = []

// actually clone the state, reset the getters
this._initialize()

// this is necessary for map* helpers
/** @type {any} */
Expand Down Expand Up @@ -109,14 +121,20 @@ exports.Store = class Store {
// getters is a plain object
this.getters = { ...this._initialGetters }
this.state = clone(this._initialState)
this._mutationsHandlers = []
this._actionsHandlers = []
}

/**
* Resets the store as if it was just created. Should be called before or after each test
*/
reset () {
this._spy.reset(this.dispatch)
this._spy.reset(this.commit)
this.commit = this._spy.create(
this._triggerMutationSubscriptions.bind(this)
)
this.dispatch = this._spy.create(
this._triggerActionSubscriptions.bind(this)
)
this._initialize()
}

Expand All @@ -125,17 +143,39 @@ exports.Store = class Store {
* @returns {() => void} unsubscribe
*/
subscribe (handler) {
this._handlers.push(handler)
this._mutationsHandlers.push(handler)
return () => {
this._mutationsHandlers.splice(
this._mutationsHandlers.indexOf(handler),
1
)
}
}

/**
* @param {Function} handler callback to call when an action is dispatched
* @returns {() => void} unsubscribe
*/
subscribeAction (handler) {
this._actionsHandlers.push(handler)
return () => {
this._handlers.splice(this._handlers.indexOf(handler), 1)
this._actionsHandlers.splice(this._actionsHandlers.indexOf(handler), 1)
}
}

/**
* @param {string} type name of the mutation
* @param {*} [payload] payload passed to the mutation
*/
_triggerSubscriptions (type, payload) {
this._handlers.forEach(fn => fn({ type, payload }, this.state))
_triggerMutationSubscriptions (type, payload) {
this._mutationsHandlers.forEach(fn => fn({ type, payload }, this.state))
}

/**
* @param {string} type name of the mutation
* @param {*} [payload] payload passed to the mutation
*/
_triggerActionSubscriptions (type, payload) {
this._actionsHandlers.forEach(fn => fn({ type, payload }, this.state))
}
}
73 changes: 69 additions & 4 deletions tests/subscriptions.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,44 @@ const { Store } = require('../src')

describe('Store subscriptions', () => {
it('subscribes to mutations', () => {
const state = {}
const store = new Store({ state })
const store = new Store({ state: {} })
const spy = jest.fn()
store.subscribe(spy)
store.commit('mutation', 'payload')

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith(
{ type: 'mutation', payload: 'payload' },
state
{}
)
})

it('subscribes to actions', () => {
const store = new Store({ state: {} })
const spy = jest.fn()
store.subscribeAction(spy)
store.dispatch('action', 'payload')

expect(spy).toHaveBeenCalledTimes(1)
expect(spy).toHaveBeenCalledWith({ type: 'action', payload: 'payload' }, {})
})

it('cleans up handlers upon reset', () => {
const store = new Store()
const spy = jest.fn()
store.subscribe(spy)
store.subscribeAction(spy)
// @ts-ignore
expect(store._mutationsHandlers).toHaveLength(1)
// @ts-ignore
expect(store._actionsHandlers).toHaveLength(1)
store.reset()
// @ts-ignore
expect(store._mutationsHandlers).toHaveLength(0)
// @ts-ignore
expect(store._actionsHandlers).toHaveLength(0)
})

it('resets subscriptions on reset', () => {
const store = new Store()
const spy = jest.fn()
Expand All @@ -25,12 +50,52 @@ describe('Store subscriptions', () => {
expect(spy).not.toHaveBeenCalled()
})

it('returns an unsubscribe callback', () => {
it('can watch again after reset', () => {
const store = new Store({ state: {} })
store.reset()

const mutation = jest.fn()
const action = jest.fn()
store.subscribe(mutation)
store.subscribeAction(action)
store.commit('mutation', 'payload')
store.dispatch('action', 'payload')
expect(mutation).toHaveBeenCalledTimes(1)
expect(action).toHaveBeenCalledTimes(1)
expect(mutation).toHaveBeenCalledWith(
{ type: 'mutation', payload: 'payload' },
{}
)
expect(action).toHaveBeenCalledWith(
{ type: 'action', payload: 'payload' },
{}
)
})

it('returns an unsubscribe callback from subscribe', () => {
const store = new Store()
const spy = jest.fn()
const unsubscribe = store.subscribe(spy)
unsubscribe()
store.commit('mutation', 'payload')
expect(spy).not.toHaveBeenCalled()
})

it('resets actions subscriptions on reset', () => {
const store = new Store()
const spy = jest.fn()
store.subscribeAction(spy)
store.reset()
store.dispatch('action', 'payload')
expect(spy).not.toHaveBeenCalled()
})

it('returns an unsubscribe callback from subscribeAction', () => {
const store = new Store()
const spy = jest.fn()
const unsubscribe = store.subscribeAction(spy)
unsubscribe()
store.dispatch('action', 'payload')
expect(spy).not.toHaveBeenCalled()
})
})
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
"noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
Expand All @@ -39,7 +39,7 @@
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
"types": ["node"] /* Type declaration files to be included in compilation. */,
// "types": ["node"] /* Type declaration files to be included in compilation. */,
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.12.tgz#7e0ced251fa94c3bc2d1023d4b84b2992fa06376"
integrity sha512-/kQvbVzdEpOq4tEWT79yAHSM4nH4xMlhJv2GrLVQt4Qmo8yYsPdioBM1QpN/2GX1wkfMnyXvdoftvLUr0LBj7Q==

"@types/lodash.clonedeep@^4.5.4":
version "4.5.4"
resolved "https://registry.yarnpkg.com/@types/lodash.clonedeep/-/lodash.clonedeep-4.5.4.tgz#2515c5f08bc95afebfb597711871b0497f5d7da7"
integrity sha512-+rCVPIZOJaub++wU/lmyp/SxiKlqXQaXI5LryzjuHBKFj51ApVt38Xxk9psLWNGMuR/obEQNTH0l/yDfG4ANNQ==
dependencies:
"@types/lodash" "*"

"@types/lodash@*":
version "4.14.119"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.119.tgz#be847e5f4bc3e35e46d041c394ead8b603ad8b39"
integrity sha512-Z3TNyBL8Vd/M9D9Ms2S3LmFq2sSMzahodD6rCS9V2N44HUMINb75jNkSuwAx7eo2ufqTdfOdtGQpNbieUjPQmw==

"@vue/test-utils@^1.0.0-beta.28":
version "1.0.0-beta.28"
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5"
Expand Down

0 comments on commit c8c8c53

Please sign in to comment.