npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

vuex-snapshot

v0.1.5

Published

Take snapshots of your store and actions for Jest snapshot testing

Downloads

55

Readme

vuex-snapshot • codecov Build Status

Module to snapshot test vuex actions with jest

Table of contents

Why use snapshot tests for actions?

I hope you are familiar with what jest, vuex and snapshot testing are.

Vuex actions are straightforward to read, and writing tests that are more complex and 10 times longer than the code they cover feels really wrong.

Actions fulfill 3 roles:

  1. Representation of app logic (conditions & calls of commits\dispatches)
  2. API for components
  3. Asynchronous layer for store (as mutations must be sync)

As such we unit test them to make sure that:

  1. When we change \ add execution path others don't get broken
  2. Our component API didn't change

vuex-snapshot makes this easy and declarative, even for async actions.

Getting started

Prerequisites

  • :heavy_check_mark: Node 6 stable or later
  • :heavy_check_mark: jest and, babel-jest installed (es6-modules imports would be used in examples, but vuex-snapshot is also output as CommonJS)

Installation

via npm

npm install --save-dev vuex-snapshot

via yarn

yarn add --dev vuex-snapshot

Basic example

Say, you are testing some card game

// @/store/actions.js
export const restartGame = ({commit}) => {
  commit('shuffleDeck')
  commit('setScore', 0)
}


// actions.spec.js
import {snapAction} from 'vuex-snapshot'
import {restartGame} from '@/store/actions'

test('restartGame matches snapshot', () => {
  expect(snapAction(restartGame)).toMatchSnapshot()
})

/*
__snapshots__/actions.spec.js
after running jest
*/

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`play restartGame matches snapshot 1`] = `
Array [
  Object {
    "message": "COMMIT: shuffleDeck",
  },
  Object {
    "message": "COMMIT: setScore",
    "payload": 0,
  },
]
`;

NOTE: by default vuex-snapshot would not use commit & dispatch from your store, but you can pass them via mocks

Usage

Testing async actions

// @/store/actions.js
export const openDashboard = ({commit, dispatch}) => new Promise((resolve, reject) => {
  commit('setRoute', 'loading')
  dispatch('load', 'dashboard')
    .then(() => {
      commit('setRoute', 'dashboard')
      resolve()
    })
    .catch('reject')
})

// actions.spec.js
import {snapAction, MockPromise} from 'vuex-snapshot'
import {openDashboard} from '@/store/actions'

test('openDashboard matches success snapshot', done => {
  // MockPromise can be resolved manually unlike default Promise
  const dispatch = name => new MockPromise(name)

  // order in which promises would be resolved
  const resolutions = ['load']

  snapAction(openDashboard, {dispatch}, resolutions)
    .then(run => {
      expect(run).toMatchSnapshot()
      done()
    })
})

Testing async actions [2]

// @/store/actions.js
export const login = ({commit, dispatch, getters}, creditals) => {
  return new Promise((resolve, reject) => {
    if(!getters.user.loggedIn) {
      fetch('/api/login/', {
        method: 'POST',
        body: JSON.stringify(creditals)
      })
        .then(res => res.json())
        .then(data => {
          commit('setUser', data)
          dispatch('setRoute', 'profile')
          resolve()
        })
        .catch(reject)
    } else {
      resolve()
    }
  })
}


// actions.spec.js
import {snapAction, useMockFetch, MockPromise} from 'vuex-snapshot'
import {login} from '@/store/actions'

test('login matches success snapshot', done => {
  useMockFetch()
  
  const payload = { authCode: 1050 }
  const getters = {
    user: {
      loggedIn: false
    }
  }

  // this is equivalent to calling resolve(payload) inside promise cb
  const resolutions = [{
    name: '/api/login/',
    payload: { json: () => new MockPromise('json') }
  }, {
    name: 'json',
    payload: { name: 'someUser', id: 21 }
  }]

  snapAction(login, {getters, payload}, resolutions)
    .then(run => {
      expect(run).toMatchSnapshot()
      done()
    })
})


// testing error scenarios is just as easy
test('login matches network fail snapshot', done => {
  useMockFetch()
  
  const payload = { authCode: 1050 }
  const getters = {
    user: {
      loggedIn: false
    }
  }

  const resolutions = [{
    name: '/api/login/',
    type: 'reject', // resolve is default value
    payload: new TypeError('Failed to fetch')
  }]

  snapAction(login, {getters, payload}, resolutions)
    .then(run => {
      /* vuex-snapshot would write that action rejected in the snapshot
         so you can test rejections as well */
      expect(run).toMatchSnapshot()
      done()
    })
})

NOTE: promises with same names would be matched to resolutions in order they were created

mocks

By using mocks object you can pass state, getters, payload(action's second argument) of any type, as well as custom commit and dispatch functions.

NOTE: Make sure your getters are what they return, not how they calculate it

Example

const action = jest.fn()

const mocks = {
  payload: 0,
  state: {
    stateValue: 'smth'
  },
  getters: {
    answer: 42
  },
  commit: console.log
  dispatch: jest.fn()
}

snapAction(action, mocks)

// would call the action like
action({
  state: mocks.state,
  getters: mocks.getters,
  commit: (name, payload) => mocks.commit(name, payload, proxies),
  dispatch: (name, payload) => mocks.dispatch(name, payload, proxies),
}, mocks.payload)

Proxies is an object with commit and dispatch that were actually passed to action (not those from mocks)

Note: state and getters are being reassigned. Like they would pass .toEqual test, but not a .toBe one.

MockPromises

import {MockPromise} from 'vuex-snapshot'

const name = 'some string'
const cb = (resolve, reject) => {}
new MockPromise(cb, name)
new MockPromise(cb) // name will be 'Promise'
new MockPromise(name) //cb will be  () => {}

// some manual control
const toResolve = new MockPromise('some name')
const toReject = new MockPromise('some other name')
const payload = {type: 'any'}

toResolve.resolve(payload)
toReject.reject(payload)

console.log(toReject.name) // some other name

This class extends Promise, so Promise.all and other promise methods work perfectly for it

NOTE: new MockPromise.then(cb) actually creates new MockPromise (that is default Promise behavior). As such there is a risk of resolutions = ['Promise', 'Promise'] matching this one instead of the Promise you've meant. This is just as true for catch, finally, Promise.all and Promise.race

snapAction overloads

import {snapAction, Snapshot} from 'vuex-snapshot'

snapAction(action)
snapAction(action, mocks)
snapAction(action, resolutions)
snapAction(action, mocks, resolutions)
snapAction(action, mocks, resolutions, snapshotToWriteTo)
// where snapshotToWriteTo is instance of Snapshot class 

If action returned a promise snapAction would do the same. That promise will resolve with an Array of Objects that represents action's execution. It could be compared to snapshot, or tested manually.

If vuex-snapshot experienced internal error snapAction test it would reject with an Object of following structure:

{
  err, // Actual error that has been thrown
  run // action's execution up to the error point
}

If action returned anything that is not a promise (including undefined) snapAction would synchronously return an array mentioned above.

Utilities


// all vuex-snapshot Utilities
import {
  reset,
  
  resetTimetable,
  resetConfig,

  useMockPromise,
  useRealPromise,

  useMockFetch,
  useRealFetch,
} from 'vuex-snapshot'

reset

Reset calls all other resets and useReal.

resetTimetable

Makes sure no already created promises could be matched to resolutions.

resetConfig

Resets vuexSnapshot.config to default values.

useMockPromise

Replaces window.Promise (same as global.Promise) with vuexSnapshot.MockPromise that could be named and resolved manually.

useRealPromise

Sets window.Promise to its original value.

useMockFetch

Replaces window.fetch (same as global.fetch) with vuexSnapshot.MockPromise that could be named and resolved manually.

useRealFetch

Sets window.fetch to its original value.

Config

These fit very specific types of tests, so using beforeEach(vuexSnapshot.reset) is highly encouraged.

vuexSnapshot.config.autoResolve

Default

false

Description

Instead of acting according to passed resolutions vuex-snapshot will automatically trigger resolve on each mock promise in order they were created.

vuexSnapshot.config.snapEnv

Default

false

Description

Starts snapshot with 2 entries:

{
  message: 'DATA MOCKS'
  payload: {
    state //value of state
    getters // value of getters
  }
}

{
  message: 'ACTION MOCKS'
  payload // passed action payload if there was one
}

// values of state, gettes and payload are not being copied

vuexSnapshot.config.allowManualActionResolution

Default

false

Description

Allows vuexSnapshot to resolve promise returned by action.

Tips

Mocking timers for vuex-snapshot resolutions

import {snapAction, MockPromise} from 'vuex-snapshot'

test('action snapshot usnig timers', done => {
  const realSetTimeout = setTimeout
  window.setTimeout = (cb, time) => {
    const mock = new MockPromise('Timeout')
    mock.then(cb)
    return realSetTimeout(mock.resolve, time)
  }

  // actual "test"
  const action = () => new Promise(resolve => {
    setTimeout(resolve, 100500)
  })

  snapAction(action, ['Timeout'])
    .then(run => {
      expect(run).toMatchSnapshot()
      done()
    })
    .catch(err => {
      console.error(err)
      console.log(timetable.entries)
      done()
    })

  window.setTimeout = realSetTimeout
})

NOTE: This is not fully accurate simulation because resolving it manually or via resolutions would cause a bit higher priority in event-loop, and resolution on timeout would be 1 tick late Because Promise.then() is not synchronous

Deep testing (execute called actions)

// @/store/actions.js
export const action1 = ({commit, dispatch}) => {
  commit('mutation1')
  dispatch('action2')
}

export const action2 = ({commit, dispatch}) => {
  commit('mutation2')
}


// actions.spec.js
import {snapAction} from 'vuex-snapshot'
import * as actions from '@/store/actions'

test('Many actions', () => {
  const state = {}
  const getters = {}

  const dispatch = (namy, payload, {commit, dispatch}) => {
    return actions[name]({state, getters, commit, dispatch}, payload)
  }

  expect(snapAction(actions.action1, {state, getters, dispatch})).toMatchSnapshot()
})

This should work for async actions too