diff --git a/cypress/integration/components-tab.js b/cypress/integration/components-tab.js index 100fe2319..fc66237a4 100644 --- a/cypress/integration/components-tab.js +++ b/cypress/integration/components-tab.js @@ -1,6 +1,6 @@ import { suite } from '../utils/suite' -const baseInstanceCount = 9 +const baseInstanceCount = 10 suite('components tab', () => { it('should detect instances inside shadow DOM', () => { diff --git a/cypress/integration/vuex-edit.js b/cypress/integration/vuex-edit.js index d64a89eae..5f3fd7f06 100644 --- a/cypress/integration/vuex-edit.js +++ b/cypress/integration/vuex-edit.js @@ -3,8 +3,10 @@ import { suite } from '../utils/suite' suite('vuex edit', () => { it('should edit state using the decrease button', () => { cy.get('.vuex-tab').click() + cy.get('[data-id="load-vuex-state"]').click() + // using the decrease button - cy.get('.data-field').eq(0) + cy.get('.state .data-field').eq(0) .find('.actions .vue-ui-button').eq(1) .click({ force: true }) .click({ force: true }) @@ -17,7 +19,7 @@ suite('vuex edit', () => { it('should edit state using the increase button', () => { // using the increase button - cy.get('.data-field').eq(0).click() + cy.get('.state .data-field').eq(0).click() .find('.actions .vue-ui-button').eq(2) .click({ force: true }) .click({ force: true }) @@ -30,7 +32,7 @@ suite('vuex edit', () => { it('should edit state using the edit input', () => { // using the edit input - cy.get('.data-field').eq(0).click() + cy.get('.state .data-field').eq(0).click() .find('.actions .vue-ui-button').eq(0).click({ force: true }) cy.get('.edit-input').type('12') cy.get('.edit-overlay > .actions > :nth-child(2) > .content > .vue-ui-icon').click() @@ -40,8 +42,8 @@ suite('vuex edit', () => { get('#counter p').contains('12') }) - // change count back to 0 - cy.get('.data-field').eq(0).click() + // change count back to 1 + cy.get('.state .data-field').eq(0).click() .find('.actions .vue-ui-button').eq(0).click({ force: true }) cy.get('.edit-input').type('0') cy.get('.edit-overlay > .actions > :nth-child(2) > .content > .vue-ui-icon').click() diff --git a/cypress/integration/vuex-tab.js b/cypress/integration/vuex-tab.js index ee7d2418c..a222fa1a5 100644 --- a/cypress/integration/vuex-tab.js +++ b/cypress/integration/vuex-tab.js @@ -10,7 +10,7 @@ suite('vuex tab', () => { get('#counter p').contains('1') }) cy.get('.vuex-tab').click() - cy.get('.history .entry').should('have.length', 4) + cy.get('.history .entry').should('have.length', 5) cy.get('[data-id="load-vuex-state"]').click() cy.get('.recording-vuex-state').should('not.be.visible') cy.get('.loading-vuex-state').should('not.be.visible') @@ -18,7 +18,7 @@ suite('vuex tab', () => { expect(el.text()).to.include('type:"DECREMENT"') expect(el.text()).to.include('count:1') }) - cy.get('.history .entry').eq(3).should('have.class', 'inspected').should('have.class', 'active') + cy.get('.history .entry').eq(4).should('have.class', 'inspected').should('have.class', 'active') }) it('should filter state & getters', () => { @@ -41,7 +41,7 @@ suite('vuex tab', () => { cy.get('.history .entry[data-active="true"].active').should('have.length', 0) cy.get('.left .search input').clear().type('/dec)/i') - cy.get('.history .entry[data-active="true"]').should('have.length', 3) + cy.get('.history .entry[data-active="true"]').should('have.length', 4) cy.get('.history .entry[data-active="true"].inspected').should('have.length', 0) cy.get('.history .entry[data-active="true"].active').should('have.length', 1) @@ -65,19 +65,19 @@ suite('vuex tab', () => { }) it('should time-travel', () => { - cy.get('.history .entry[data-index="2"] .entry-actions .action-time-travel').click({ force: true }) - cy.get('.history .entry[data-index="2"]') + cy.get('.history .entry[data-index="3"] .entry-actions .action-time-travel').click({ force: true }) + cy.get('.history .entry[data-index="3"]') .should('have.class', 'inspected') .should('have.class', 'active') cy.get('#target').iframe().then(({ get }) => { get('#counter p').contains('2') }) - cy.get('.history .entry[data-index="1"] .mutation-type').click({ force: true }) - cy.get('.history .entry[data-index="1"]') + cy.get('.history .entry[data-index="2"] .mutation-type').click({ force: true }) + cy.get('.history .entry[data-index="2"]') .should('have.class', 'inspected') .should('not.have.class', 'active') - cy.get('.history .entry[data-index="2"]') + cy.get('.history .entry[data-index="3"]') .should('not.have.class', 'inspected') .should('have.class', 'active') cy.get('.recording-vuex-state').should('not.be.visible') @@ -90,11 +90,11 @@ suite('vuex tab', () => { cy.get('#target').iframe().then(({ get }) => { get('#counter p').contains('2') }) - cy.get('.history .entry[data-index="1"] .entry-actions .action-time-travel').click({ force: true }) - cy.get('.history .entry[data-index="1"]') + cy.get('.history .entry[data-index="2"] .entry-actions .action-time-travel').click({ force: true }) + cy.get('.history .entry[data-index="2"]') .should('have.class', 'inspected') .should('have.class', 'active') - cy.get('.history .entry[data-index="2"]') + cy.get('.history .entry[data-index="3"]') .should('not.have.class', 'inspected') .should('not.have.class', 'active') cy.get('#target').iframe().then(({ get }) => { @@ -112,8 +112,8 @@ suite('vuex tab', () => { cy.get('#target').iframe().then(({ get }) => { get('#counter p').contains('1') }) - cy.get('.history .entry[data-index="0"] .entry-actions .action-time-travel').click({ force: true }) - cy.get('.history .entry[data-index="0"]') + cy.get('.history .entry[data-index="1"] .entry-actions .action-time-travel').click({ force: true }) + cy.get('.history .entry[data-index="1"]') .should('have.class', 'inspected') .should('have.class', 'active') cy.get('#target').iframe().then(({ get }) => { @@ -122,10 +122,10 @@ suite('vuex tab', () => { }) it('should revert', () => { - cy.get('.history .entry[data-index="3"] .mutation-type').click({ force: true }) - cy.get('.history .entry[data-index="3"]').find('.action-revert').click({ force: true }) - cy.get('.history .entry[data-active="true"]').should('have.length', 3) - cy.get('.history .entry[data-index="2"]') + cy.get('.history .entry[data-index="4"] .mutation-type').click({ force: true }) + cy.get('.history .entry[data-index="4"]').find('.action-revert').click({ force: true }) + cy.get('.history .entry[data-active="true"]').should('have.length', 4) + cy.get('.history .entry[data-index="3"]') .should('have.class', 'inspected') .should('have.class', 'active') cy.get('.vuex-state-inspector').then(el => { @@ -137,8 +137,7 @@ suite('vuex tab', () => { }) it('should commit', () => { - cy.get('.history .entry[data-index="2"] .mutation-type').click({ force: true }) - cy.get('.history .entry[data-index="2"] .action-commit').click({ force: true }) + cy.get('.history .entry[data-index="3"] .action-commit').click({ force: true }) cy.get('.history .entry[data-active="true"]').should('have.length', 1) cy.get('.history .entry[data-index="0"]') .should('have.class', 'inspected') @@ -152,20 +151,23 @@ suite('vuex tab', () => { }) it('should display getters', () => { - cy.get('.vuex-state-inspector').then(el => { - expect(el.text()).to.include('isPositive:true') + cy.get('.vuex-state-inspector').within(() => { + cy.get('.key').contains('count').parent().contains('2') + cy.get('.key').contains('isPositive').parent().contains('true') }) cy.get('#target').iframe().then(({ get }) => { get('.decrement') .click({ force: true }) .click({ force: true }) .click({ force: true }) + get('#counter p').contains('-1') }) cy.get('.history .entry[data-index="3"]').click({ force: true }) cy.get('.recording-vuex-state').should('not.be.visible') cy.get('.loading-vuex-state').should('not.be.visible') - cy.get('.vuex-state-inspector').then(el => { - expect(el.text()).to.include('isPositive:false') + cy.get('.vuex-state-inspector').within(() => { + cy.get('.key').contains('count').parent().contains('-1') + cy.get('.key').contains('isPositive').parent().contains('false') }) }) @@ -178,7 +180,7 @@ suite('vuex tab', () => { cy.get('#target').iframe().then(({ get }) => { get('.increment').click({ force: true }) }) - cy.get('.history .entry').should('have.length', 4) + cy.get('.history .entry').should('have.length', 5) }) it('should copy vuex state', () => { diff --git a/shells/dev/target/Init.vue b/shells/dev/target/Init.vue new file mode 100644 index 000000000..e4f1a58b1 --- /dev/null +++ b/shells/dev/target/Init.vue @@ -0,0 +1,20 @@ + + + diff --git a/shells/dev/target/index.js b/shells/dev/target/index.js index 71832abba..09e312349 100644 --- a/shells/dev/target/index.js +++ b/shells/dev/target/index.js @@ -2,6 +2,7 @@ import Vue from 'vue' import store from './store' import Target from './Target.vue' import Other from './Other.vue' +import Init from './Init.vue' import Counter from './Counter.vue' import VuexObject from './VuexObject.vue' import NativeTypes from './NativeTypes.vue' @@ -39,7 +40,8 @@ new Vue({ h(Events, { key: 'foo' }), h(NativeTypes, { key: new Date() }), h(Router, { key: [] }), - h(VuexObject) + h(VuexObject), + h(Init) ]) } }).$mount('#app') diff --git a/shells/dev/target/store.js b/shells/dev/target/store.js index f4eab0516..34a68a52f 100644 --- a/shells/dev/target/store.js +++ b/shells/dev/target/store.js @@ -5,6 +5,7 @@ Vue.use(Vuex) export default new Vuex.Store({ state: { + inited: 0, count: 0, date: new Date(), set: new Set(), @@ -21,6 +22,7 @@ export default new Vuex.Store({ } }, mutations: { + TEST_INIT: state => state.inited++, INCREMENT: state => state.count++, DECREMENT: state => state.count--, UPDATE_DATE: state => { diff --git a/src/backend/hook.js b/src/backend/hook.js index d7dc95b0f..6f23ce232 100644 --- a/src/backend/hook.js +++ b/src/backend/hook.js @@ -17,19 +17,36 @@ export function installHook (window) { const hook = { Vue: null, + _buffer: [], + + _replayBuffer (event) { + let buffer = this._buffer + this._buffer = [] + + for (let i = 0, l = buffer.length; i < l; i++) { + let allArgs = buffer[i] + allArgs[0] === event + ? this.emit.apply(this, allArgs) + : this._buffer.push(allArgs) + } + }, + on (event, fn) { - event = '$' + event - ;(listeners[event] || (listeners[event] = [])).push(fn) + const $event = '$' + event + if (listeners[$event]) { + listeners[$event].push(fn) + } else { + listeners[$event] = [fn] + this._replayBuffer(event) + } }, once (event, fn) { - const eventAlias = event - event = '$' + event function on () { - this.off(eventAlias, on) + this.off(event, on) fn.apply(this, arguments) } - ;(listeners[event] || (listeners[event] = [])).push(on) + this.on(event, on) }, off (event, fn) { @@ -55,14 +72,17 @@ export function installHook (window) { }, emit (event) { - event = '$' + event - let cbs = listeners[event] + const $event = '$' + event + let cbs = listeners[$event] if (cbs) { - const args = [].slice.call(arguments, 1) + const eventArgs = [].slice.call(arguments, 1) cbs = cbs.slice() for (let i = 0, l = cbs.length; i < l; i++) { - cbs[i].apply(this, args) + cbs[i].apply(this, eventArgs) } + } else { + const allArgs = [].slice.call(arguments) + this._buffer.push(allArgs) } } } @@ -78,6 +98,7 @@ export function installHook (window) { hook.once('vuex:init', store => { hook.store = store + hook.initialStore = clone(store) }) Object.defineProperty(window, '__VUE_DEVTOOLS_GLOBAL_HOOK__', { @@ -85,4 +106,164 @@ export function installHook (window) { return hook } }) + + // Clone deep utility for cloning initial state of the store + // REFERENCE: https://github.com/buunguyen/node-clone/commit/63afda9de9d94b9332586e34a646a13e8d719244 + + function clone (parent, circular, depth, prototype) { + if (typeof circular === 'object') { + depth = circular.depth + prototype = circular.prototype + circular = circular.circular + } + // maintain two arrays for circular references, where corresponding parents + // and children have the same index + var allParents = [] + var allChildren = [] + + var useBuffer = typeof Buffer !== 'undefined' + + if (typeof circular === 'undefined') { circular = true } + + if (typeof depth === 'undefined') { depth = Infinity } + + // recurse this function so we don't reset allParents and allChildren + function _clone (parent, depth) { + // cloning null always returns null + if (parent === null) { return null } + + if (depth === 0) { return parent } + + var child + var proto + if (typeof parent !== 'object') { + return parent + } + + if (parent instanceof Map) { + child = new Map() + } else if (parent instanceof Set) { + child = new Set() + } else if (parent instanceof Promise) { + child = new Promise(function (resolve, reject) { + parent.then(function (value) { + resolve(_clone(value, depth - 1)) + }, function (err) { + reject(_clone(err, depth - 1)) + }) + }) + } else if (_isArray(parent)) { + child = [] + } else if (_isRegExp(parent)) { + child = new RegExp(parent.source, _getRegExpFlags(parent)) + if (parent.lastIndex) child.lastIndex = parent.lastIndex + } else if (_isDate(parent)) { + child = new Date(parent.getTime()) + } else if (useBuffer && Buffer.isBuffer(parent)) { + child = Buffer.alloc(parent.length) + parent.copy(child) + return child + } else if (parent instanceof Error) { + child = Object.create(parent) + } else { + if (typeof prototype === 'undefined') { + proto = Object.getPrototypeOf(parent) + child = Object.create(proto) + } else { + child = Object.create(prototype) + proto = prototype + } + } + + if (circular) { + var index = allParents.indexOf(parent) + + if (index !== -1) { + return allChildren[index] + } + allParents.push(parent) + allChildren.push(child) + } + + if (parent instanceof Map) { + var keyIterator = parent.keys() + while (true) { + let next = keyIterator.next() + if (next.done) { + break + } + var keyChild = _clone(next.value, depth - 1) + var valueChild = _clone(parent.get(next.value), depth - 1) + child.set(keyChild, valueChild) + } + } + if (parent instanceof Set) { + var iterator = parent.keys() + while (true) { + let next = iterator.next() + if (next.done) { + break + } + var entryChild = _clone(next.value, depth - 1) + child.add(entryChild) + } + } + + for (let i in parent) { + var attrs + if (proto) { + attrs = Object.getOwnPropertyDescriptor(proto, i) + } + + if (attrs && attrs.set == null) { + continue + } + child[i] = _clone(parent[i], depth - 1) + } + + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(parent) + for (let i = 0; i < symbols.length; i++) { + // Don't need to worry about cloning a symbol because it is a primitive, + // like a number or string. + var symbol = symbols[i] + var descriptor = Object.getOwnPropertyDescriptor(parent, symbol) + if (descriptor && !descriptor.enumerable) { + continue + } + child[symbol] = _clone(parent[symbol], depth - 1) + } + } + + return child + } + + return _clone(parent, depth) + } + + // private utility functions + + function _objToStr (o) { + return Object.prototype.toString.call(o) + } + + function _isDate (o) { + return typeof o === 'object' && _objToStr(o) === '[object Date]' + } + + function _isArray (o) { + return typeof o === 'object' && _objToStr(o) === '[object Array]' + } + + function _isRegExp (o) { + return typeof o === 'object' && _objToStr(o) === '[object RegExp]' + } + + function _getRegExpFlags (re) { + var flags = '' + if (re.global) flags += 'g' + if (re.ignoreCase) flags += 'i' + if (re.multiline) flags += 'm' + return flags + } } diff --git a/src/backend/vuex.js b/src/backend/vuex.js index b0ec4e711..08fec18cf 100644 --- a/src/backend/vuex.js +++ b/src/backend/vuex.js @@ -14,15 +14,16 @@ export function initVuexBackend (hook, bridge) { computed: originalVm.$options.computed }) - const getSnapshot = () => stringify({ - state: store.state, - getters: store.getters || {} + const getSnapshot = (_store = store) => stringify({ + state: _store.state, + getters: _store.getters || {} }) let baseSnapshot, snapshots, mutations, lastState function reset () { - baseSnapshot = getSnapshot() + baseSnapshot = getSnapshot(hook.initialStore) + hook.initialStore = undefined mutations = [] resetSnapshotCache() } @@ -72,7 +73,6 @@ export function initVuexBackend (hook, bridge) { snapshot }) if (apply) { - console.log('vuex:travel-to-state', state) hook.emit('vuex:travel-to-state', state) } }) diff --git a/src/devtools/index.js b/src/devtools/index.js index cdd71b573..3d764fbad 100644 --- a/src/devtools/index.js +++ b/src/devtools/index.js @@ -8,7 +8,6 @@ import { parse } from '../util' import { isChrome, initEnv } from './env' import SharedData, { init as initSharedData, destroy as destroySharedData } from 'src/shared-data' import storage from './storage' -import { snapshotsCache } from './views/vuex/cache' import VuexResolve from './views/vuex/resolve' for (const key in filters) { @@ -144,8 +143,7 @@ function initApp (shell) { }) bridge.on('vuex:inspected-state', ({ index, snapshot }) => { - snapshotsCache.set(index, snapshot) - store.commit('vuex/RECEIVE_STATE', snapshot) + store.commit('vuex/RECEIVE_STATE', { index, snapshot }) if (index === -1) { store.commit('vuex/UPDATE_BASE_STATE', snapshot) diff --git a/src/devtools/views/vuex/VuexHistory.vue b/src/devtools/views/vuex/VuexHistory.vue index 9818b6a9c..92ac84fa2 100644 --- a/src/devtools/views/vuex/VuexHistory.vue +++ b/src/devtools/views/vuex/VuexHistory.vue @@ -104,7 +104,7 @@