From 62bc8663a098fd9e77cfa8fc54c74a83e423f015 Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Mon, 17 Feb 2020 14:06:04 -0500 Subject: [PATCH] Private view (#41) * Add file structure * Add basics of private view * Add composition api in every test * Install nanoid * Add request queue * Register all global components * Make bunny run on api queue * Use private route for debug now * Move request queue to store * Remove unused sleep function in hover test * Use new request queue in private view * Remove jest pre-test config * Finish logo + tests * Add tests for private view * Fix unhandled promise in api test --- package.json | 2 + src/api.test.ts | 48 ++++++- src/api.ts | 40 +++++- src/components/register.ts | 31 +++++ src/components/v-hover/v-hover.test.ts | 6 - src/main.ts | 6 +- src/routes/debug.vue | 8 +- src/stores/projects.test.ts | 1 - src/stores/requests.test.ts | 31 +++++ src/stores/requests.ts | 22 ++++ src/styles/mixins/breakpoint.scss | 4 +- src/views/private/_module-bar-logo.test.ts | 104 +++++++++++++++ src/views/private/_module-bar-logo.vue | 89 +++++++++++++ src/views/private/_module-bar.vue | 27 ++++ src/views/private/index.ts | 4 + src/views/private/private-view.readme.md | 1 + src/views/private/private-view.story.ts | 19 +++ src/views/private/private-view.test.ts | 62 +++++++++ src/views/private/private-view.vue | 145 +++++++++++++++++++++ src/views/public/public-view.test.ts | 5 +- yarn.lock | 12 ++ 21 files changed, 649 insertions(+), 18 deletions(-) create mode 100644 src/components/register.ts create mode 100644 src/stores/requests.test.ts create mode 100644 src/stores/requests.ts create mode 100644 src/views/private/_module-bar-logo.test.ts create mode 100644 src/views/private/_module-bar-logo.vue create mode 100644 src/views/private/_module-bar.vue create mode 100644 src/views/private/index.ts create mode 100644 src/views/private/private-view.readme.md create mode 100644 src/views/private/private-view.story.ts create mode 100644 src/views/private/private-view.test.ts create mode 100644 src/views/private/private-view.vue diff --git a/package.json b/package.json index 5f54f64291..ea443c3c92 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,13 @@ "dependencies": { "@types/debug": "^4.1.5", "@types/lodash": "^4.14.149", + "@types/nanoid": "^2.1.0", "@vue/composition-api": "^0.3.4", "axios": "^0.19.2", "date-fns": "^2.9.0", "debug": "^4.1.1", "lodash": "^4.17.15", + "nanoid": "^2.1.11", "pinia": "0.0.5", "v-tooltip": "^2.0.3", "vue": "^2.6.11", diff --git a/src/api.test.ts b/src/api.test.ts index 912d4f3fa0..9237de3d22 100644 --- a/src/api.test.ts +++ b/src/api.test.ts @@ -1,8 +1,12 @@ -import { getRootPath } from './api'; +import Vue from 'vue'; +import VueCompositionAPI from '@vue/composition-api'; +import { onRequest, onResponse, onError, getRootPath } from './api'; +import { useRequestsStore } from '@/stores/requests'; describe('API', () => { beforeAll(() => { globalThis.window = Object.create(window); + Vue.use(VueCompositionAPI); }); it('Calculates the correct API root URL based on window', () => { @@ -16,4 +20,46 @@ describe('API', () => { const result = getRootPath(); expect(result).toBe('/api/nested/'); }); + + it('Calls startRequest on the store on any request', () => { + const store = useRequestsStore({}); + const spy = jest.spyOn(store, 'startRequest'); + spy.mockImplementation(() => 'abc'); + const newRequest = onRequest({}); + expect(spy).toHaveBeenCalled(); + expect(newRequest.id).toBe('abc'); + }); + + it('Calls endRequest on responses', () => { + const store = useRequestsStore({}); + const spy = jest.spyOn(store, 'endRequest'); + onResponse({ + data: null, + status: 200, + statusText: 'OK', + headers: {}, + config: { + id: 'abc' + } + }); + expect(spy).toHaveBeenCalledWith('abc'); + }); + + it('Calls endRequest on errors', async () => { + const store = useRequestsStore({}); + const spy = jest.spyOn(store, 'endRequest'); + try { + await onError({ + response: { + config: { + id: 'abc' + } + } + }); + } catch (error) { + expect(error).toEqual({ response: { config: { id: 'abc' } } }); + } + + expect(spy).toHaveBeenCalledWith('abc'); + }); }); diff --git a/src/api.ts b/src/api.ts index a99565b3de..7764ac93db 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,9 +1,47 @@ -import axios from 'axios'; +import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { useRequestsStore } from '@/stores/requests'; const api = axios.create({ baseURL: getRootPath() }); +interface RequestConfig extends AxiosRequestConfig { + id: string; +} + +interface Response extends AxiosResponse { + config: RequestConfig; +} + +export const onRequest = (config: AxiosRequestConfig) => { + const requestsStore = useRequestsStore(); + const id = requestsStore.startRequest(); + + const requestConfig: RequestConfig = { + id: id, + ...config + }; + + return requestConfig; +}; + +export const onResponse = (response: AxiosResponse | Response) => { + const requestsStore = useRequestsStore(); + const id = (response.config as RequestConfig).id; + requestsStore.endRequest(id); + return response; +}; + +export const onError = (error: any) => { + const requestsStore = useRequestsStore(); + const id = (error.response.config as RequestConfig).id; + requestsStore.endRequest(id); + return Promise.reject(error); +}; + +api.interceptors.request.use(onRequest); +api.interceptors.response.use(onResponse, onError); + export function getRootPath(): string { const path = window.location.pathname; const parts = path.split('/'); diff --git a/src/components/register.ts b/src/components/register.ts new file mode 100644 index 0000000000..a0449afa45 --- /dev/null +++ b/src/components/register.ts @@ -0,0 +1,31 @@ +import Vue from 'vue'; + +import VAvatar from './v-avatar/'; +import VButton from './v-button/'; +import VCheckbox from './v-checkbox/'; +import VChip from './v-chip/'; +import VHover from './v-hover/'; +import VIcon from './v-icon/'; +import VInput from './v-input/'; +import VOverlay from './v-overlay/'; +import VProgressLinear from './v-progress/linear/'; +import VSheet from './v-sheet/'; +import VSlider from './v-slider/'; +import VSpinner from './v-spinner/'; +import VSwitch from './v-switch/'; +import VTable from './v-table/'; + +Vue.component('v-avatar', VAvatar); +Vue.component('v-button', VButton); +Vue.component('v-checkbox', VCheckbox); +Vue.component('v-chip', VChip); +Vue.component('v-hover', VHover); +Vue.component('v-icon', VIcon); +Vue.component('v-input', VInput); +Vue.component('v-overlay', VOverlay); +Vue.component('v-progress-linear', VProgressLinear); +Vue.component('v-sheet', VSheet); +Vue.component('v-slider', VSlider); +Vue.component('v-spinner', VSpinner); +Vue.component('v-switch', VSwitch); +Vue.component('v-table', VTable); diff --git a/src/components/v-hover/v-hover.test.ts b/src/components/v-hover/v-hover.test.ts index eec55c2b7f..d57d4d9679 100644 --- a/src/components/v-hover/v-hover.test.ts +++ b/src/components/v-hover/v-hover.test.ts @@ -5,12 +5,6 @@ import VHover from './v-hover.vue'; const localVue = createLocalVue(); localVue.use(VueCompositionAPI); -function sleep(ms: number): Promise { - return new Promise(resolve => { - setTimeout(resolve, ms); - }); -} - describe('Hover', () => { let component: Wrapper; diff --git a/src/main.ts b/src/main.ts index f6e0e03989..f13d73858e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,9 +1,11 @@ import Vue from 'vue'; -import router from './router'; -import i18n from './lang/'; import './styles/main.scss'; import './plugins'; +import './components/register'; + +import router from './router'; +import i18n from './lang/'; Vue.config.productionTip = false; diff --git a/src/routes/debug.vue b/src/routes/debug.vue index dd3a7362d3..e6ad009602 100644 --- a/src/routes/debug.vue +++ b/src/routes/debug.vue @@ -1,16 +1,16 @@ + + diff --git a/src/views/private/_module-bar.vue b/src/views/private/_module-bar.vue new file mode 100644 index 0000000000..f1641f6471 --- /dev/null +++ b/src/views/private/_module-bar.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/src/views/private/index.ts b/src/views/private/index.ts new file mode 100644 index 0000000000..5c3d8c7e16 --- /dev/null +++ b/src/views/private/index.ts @@ -0,0 +1,4 @@ +import PrivateView from './private-view.vue'; + +export { PrivateView }; +export default PrivateView; diff --git a/src/views/private/private-view.readme.md b/src/views/private/private-view.readme.md new file mode 100644 index 0000000000..16785aaca3 --- /dev/null +++ b/src/views/private/private-view.readme.md @@ -0,0 +1 @@ +# Private View diff --git a/src/views/private/private-view.story.ts b/src/views/private/private-view.story.ts new file mode 100644 index 0000000000..fd9fb786d3 --- /dev/null +++ b/src/views/private/private-view.story.ts @@ -0,0 +1,19 @@ +import Vue from 'vue'; +import PrivateView from './private-view.vue'; +import markdown from './private-view.readme.md'; + +Vue.component('private-view', PrivateView); + +export default { + title: 'Views / Private', + component: PrivateView, + parameters: { + notes: markdown + } +}; + +export const basic = () => ({ + template: ` + +` +}); diff --git a/src/views/private/private-view.test.ts b/src/views/private/private-view.test.ts new file mode 100644 index 0000000000..7b723cf6e5 --- /dev/null +++ b/src/views/private/private-view.test.ts @@ -0,0 +1,62 @@ +import { shallowMount, createLocalVue, Wrapper } from '@vue/test-utils'; +import VueCompositionAPI from '@vue/composition-api'; +import PrivateView from './private-view.vue'; +import VOverlay from '@/components/v-overlay'; +import useWindowSize from '@/compositions/window-size'; + +let mockWidth = 50; + +jest.mock('@/compositions/window-size', () => + jest.fn().mockImplementation(() => { + return { + width: { + value: mockWidth + }, + height: { + value: mockWidth + } + }; + }) +); + +const localVue = createLocalVue(); +localVue.use(VueCompositionAPI); +localVue.component('v-overlay', VOverlay); + +describe('Views / Private', () => { + beforeEach(() => { + (useWindowSize as jest.Mock).mockClear(); + }); + + it('Shows nav with overlay if screen is < 960px', async () => { + mockWidth = 600; + const component = shallowMount(PrivateView, { localVue }); + expect((component.vm as any).navWithOverlay).toBe(true); + }); + + it('Does not render overlay for nav if screen is >= 960px', async () => { + mockWidth = 960; + let component = shallowMount(PrivateView, { localVue }); + expect((component.vm as any).navWithOverlay).toBe(false); + + mockWidth = 1000; + component = shallowMount(PrivateView, { localVue }); + expect((component.vm as any).navWithOverlay).toBe(false); + }); + + it('Shows drawer with overlay if screen is < 1260px', async () => { + mockWidth = 600; + const component = shallowMount(PrivateView, { localVue }); + expect((component.vm as any).drawerWithOverlay).toBe(true); + }); + + it('Does not render overlay for drawer if screen is >= 1260px', async () => { + mockWidth = 1260; + let component = shallowMount(PrivateView, { localVue }); + expect((component.vm as any).drawerWithOverlay).toBe(false); + + mockWidth = 1300; + component = shallowMount(PrivateView, { localVue }); + expect((component.vm as any).drawerWithOverlay).toBe(false); + }); +}); diff --git a/src/views/private/private-view.vue b/src/views/private/private-view.vue new file mode 100644 index 0000000000..a9fb9eea15 --- /dev/null +++ b/src/views/private/private-view.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/src/views/public/public-view.test.ts b/src/views/public/public-view.test.ts index edb7b8fe19..0349806a58 100644 --- a/src/views/public/public-view.test.ts +++ b/src/views/public/public-view.test.ts @@ -1,3 +1,4 @@ +import Vue from 'vue'; import VueCompositionAPI from '@vue/composition-api'; import { mount, createLocalVue, Wrapper } from '@vue/test-utils'; import { VTooltip } from 'v-tooltip'; @@ -64,7 +65,7 @@ describe('Views / Public', () => { }); }); - it('Uses the project color when the current project key is set, but background image is not', () => { + it('Uses the project color when the current project key is set, but background image is not', async () => { store.state.projects = [ { ...mockProject, @@ -76,6 +77,8 @@ describe('Views / Public', () => { ]; store.state.currentProjectKey = 'my-project'; + await component.vm.$nextTick(); + expect((component.vm as any).artStyles).toEqual({ background: '#4CAF50' }); diff --git a/yarn.lock b/yarn.lock index 840c1049d4..3680b11443 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1757,6 +1757,13 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= +"@types/nanoid@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/nanoid/-/nanoid-2.1.0.tgz#41edfda78986e9127d0dc14de982de766f994020" + integrity sha512-xdkn/oRTA0GSNPLIKZgHWqDTWZsVrieKomxJBOQUK9YDD+zfSgmwD5t4WJYra5S7XyhTw7tfvwznW+pFexaepQ== + dependencies: + "@types/node" "*" + "@types/node@*": version "13.7.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" @@ -9780,6 +9787,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== +nanoid@^2.1.11: + version "2.1.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" + integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"