Commit fc03d3c2 authored by Paul Slaughter's avatar Paul Slaughter

Add sync router and store utility in IDE

**Why?**
- This will be used to help break the cyclical
  dependency of the IDE router and store.
parent 49441ba4
/* eslint-disable import/prefer-default-export */
/**
* This method adds listeners to the given router and store and syncs their state with eachother
*
* ### Why?
*
* Previously the IDE had a circular dependency between a singleton router and a singleton store.
* This causes some integration testing headaches...
*
* At the time, the most effecient way to break this ciruclar dependency was to:
*
* - Replace the router with a factory function that receives a store reference
* - Have the store write to a certain state that can be watched by the router
*
* Hence... This helper function...
*/
export const syncRouterAndStore = (router, store) => {
const disposables = [];
let currentPath = '';
// sync store to router
disposables.push(
store.watch(
state => state.router.fullPath,
fullPath => {
if (currentPath === fullPath) {
return;
}
currentPath = fullPath;
router.push(fullPath);
},
),
);
// sync router to store
disposables.push(
router.afterEach(to => {
if (currentPath === to.fullPath) {
return;
}
currentPath = to.fullPath;
store.dispatch('router/push', currentPath, { root: true });
}),
);
const unsync = () => {
disposables.forEach(fn => fn());
};
return unsync;
};
import Vue from 'vue';
import VueRouter from 'vue-router';
import Vuex from 'vuex';
import routerModule from '~/ide/stores/modules/router';
import { syncRouterAndStore } from '~/ide/sync_router_and_store';
import waitForPromises from 'helpers/wait_for_promises';
const TEST_ROUTE = '/test/lorem/ipsum';
Vue.use(Vuex);
describe('~/ide/sync_router_and_store', () => {
let unsync;
let router;
let store;
let onRouterChange;
const createSync = () => {
unsync = syncRouterAndStore(router, store);
};
const getRouterCurrentPath = () => router.currentRoute.fullPath;
const getStoreCurrentPath = () => store.state.router.fullPath;
const updateRouter = path => {
router.push(path);
return waitForPromises();
};
const updateStore = path => {
store.dispatch('router/push', path);
return waitForPromises();
};
beforeEach(() => {
router = new VueRouter();
store = new Vuex.Store({
modules: {
router: routerModule,
},
});
jest.spyOn(store, 'dispatch');
onRouterChange = jest.fn();
router.beforeEach((to, from, next) => {
onRouterChange(to, from);
next();
});
});
afterEach(() => {
unsync();
unsync = null;
});
it('keeps store and router in sync', async () => {
createSync();
await updateRouter('/test/test');
await updateRouter('/test/test');
await updateStore('123/abc');
await updateRouter('def');
// Even though we pused relative paths, the store and router kept track of the resulting fullPath
expect(getRouterCurrentPath()).toBe('/test/123/def');
expect(getStoreCurrentPath()).toBe('/test/123/def');
});
describe('default', () => {
beforeEach(() => {
createSync();
});
it('store is default', () => {
expect(store.dispatch).not.toHaveBeenCalled();
expect(getStoreCurrentPath()).toBe('');
});
it('router is default', () => {
expect(onRouterChange).not.toHaveBeenCalled();
expect(getRouterCurrentPath()).toBe('/');
});
describe('when store changes', () => {
beforeEach(() => {
updateStore(TEST_ROUTE);
});
it('store is updated', () => {
// let's make sure the action isn't dispatched more than necessary
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(getStoreCurrentPath()).toBe(TEST_ROUTE);
});
it('router is updated', () => {
expect(onRouterChange).toHaveBeenCalledTimes(1);
expect(getRouterCurrentPath()).toBe(TEST_ROUTE);
});
describe('when store changes again to the same thing', () => {
beforeEach(() => {
onRouterChange.mockClear();
updateStore(TEST_ROUTE);
});
it('doesnt change router again', () => {
expect(onRouterChange).not.toHaveBeenCalled();
});
});
});
describe('when router changes', () => {
beforeEach(() => {
updateRouter(TEST_ROUTE);
});
it('store is updated', () => {
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(getStoreCurrentPath()).toBe(TEST_ROUTE);
});
it('router is updated', () => {
// let's make sure the router change isn't triggered more than necessary
expect(onRouterChange).toHaveBeenCalledTimes(1);
expect(getRouterCurrentPath()).toBe(TEST_ROUTE);
});
describe('when router changes again to the same thing', () => {
beforeEach(() => {
store.dispatch.mockClear();
updateRouter(TEST_ROUTE);
});
it('doesnt change store again', () => {
expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
describe('when disposed', () => {
beforeEach(() => {
unsync();
});
it('a store change does not trigger a router change', () => {
updateStore(TEST_ROUTE);
expect(getRouterCurrentPath()).toBe('/');
expect(onRouterChange).not.toHaveBeenCalled();
});
it('a router change does not trigger a store change', () => {
updateRouter(TEST_ROUTE);
expect(getStoreCurrentPath()).toBe('');
expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment