diff --git a/src/lib/storedInquiries/index.js b/src/lib/storedInquiries/index.js index a030a54..2d0e5df 100644 --- a/src/lib/storedInquiries/index.js +++ b/src/lib/storedInquiries/index.js @@ -33,17 +33,16 @@ export default { }, isTabNeedName (inquiryTab) { - const isFromScratch = !inquiryTab.initName - return inquiryTab.isPredefined || isFromScratch + return inquiryTab.isPredefined || !inquiryTab.name }, save (inquiryTab, newName) { const value = { id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id, query: inquiryTab.query, - viewType: inquiryTab.$refs.dataView.mode, - viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(), - name: newName || inquiryTab.initName + viewType: inquiryTab.dataView.mode, + viewOptions: inquiryTab.dataView.getOptionsForSave(), + name: newName || inquiryTab.name } // Get inquiries from local storage diff --git a/src/lib/tab.js b/src/lib/tab.js new file mode 100644 index 0000000..f1ab8ee --- /dev/null +++ b/src/lib/tab.js @@ -0,0 +1,58 @@ +import { nanoid } from 'nanoid' +import time from '@/lib/utils/time' +import events from '@/lib/utils/events' + +export default class Tab { + constructor (state, inquiry = {}) { + this.id = inquiry.id || nanoid() + this.name = inquiry.id ? inquiry.name : null + this.tempName = inquiry.name || (state.untitledLastIndex + ? `Untitled ${state.untitledLastIndex}` + : 'Untitled') + this.query = inquiry.query + this.viewOptions = inquiry.viewOptions || undefined + this.isPredefined = inquiry.isPredefined + this.viewType = inquiry.viewType || 'chart' + this.result = null + this.isGettingResults = false + this.error = null + this.time = 0 + this.layout = inquiry.layout || { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + } + + this.isSaved = !!inquiry.id + this.state = state + } + + async execute () { + this.isGettingResults = true + this.result = null + this.error = null + const db = this.state.db + try { + const start = new Date() + this.result = await db.execute(this.query + ';') + this.time = time.getPeriod(start, new Date()) + + if (this.result && this.result.values) { + events.send('resultset.create', + this.result.values[this.result.columns[0]].length + ) + } + + events.send('query.run', parseFloat(this.time), { status: 'success' }) + } catch (err) { + this.error = { + type: 'error', + message: err + } + + events.send('query.run', 0, { status: 'error' }) + } + db.refreshSchema() + this.isGettingResults = false + } +} diff --git a/src/store/actions.js b/src/store/actions.js index 7096951..4f22216 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -1,32 +1,17 @@ -import { nanoid } from 'nanoid' +import Tab from '@/lib/tab' export default { - async addTab ({ state }, data) { - const tab = data ? JSON.parse(JSON.stringify(data)) : {} - // If no data then create a new blank one... - // No data.id means to create new tab, but not blank, - // e.g. with 'select * from csv_import' inquiry after csv import - if (!data || !data.id) { - tab.id = nanoid() - tab.name = null - tab.tempName = state.untitledLastIndex - ? `Untitled ${state.untitledLastIndex}` - : 'Untitled' - tab.viewType = 'chart' - tab.viewOptions = undefined - tab.isSaved = false - } else { - tab.isSaved = true - } - - // add new tab only if was not already opened - if (!state.tabs.some(openedTab => openedTab.id === tab.id)) { + async addTab ({ state }, inquiry = {}) { + // add new tab only if it was not already opened + if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) { + const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry))) state.tabs.push(tab) if (!tab.name) { state.untitledLastIndex += 1 } + return tab.id } - return tab.id + return inquiry.id } } diff --git a/src/store/mutations.js b/src/store/mutations.js index 4ac4e0f..b31f07c 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -1,5 +1,3 @@ -import Vue from 'vue' - export default { setDb (state, db) { if (state.db) { @@ -8,8 +6,8 @@ export default { state.db = db }, - updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) { - const tab = state.tabs[index] + updateTab (state, { tab, newValues }) { + const { name, id, query, viewType, viewOptions, isSaved } = newValues const oldId = tab.id if (id && state.currentTabId === oldId) { @@ -26,13 +24,12 @@ export default { // Saved inquiry is not predefined delete tab.isPredefined } - - Vue.set(state.tabs, index, tab) }, - deleteTab (state, index) { + deleteTab (state, tab) { + const index = state.tabs.indexOf(tab) // If closing tab is the current opened - if (state.tabs[index].id === state.currentTabId) { + if (tab.id === state.currentTabId) { if (index < state.tabs.length - 1) { state.currentTabId = state.tabs[index + 1].id } else if (index > 0) { @@ -46,10 +43,12 @@ export default { state.tabs.splice(index, 1) }, setCurrentTabId (state, id) { - state.currentTabId = id - }, - setCurrentTab (state, tab) { - state.currentTab = tab + try { + state.currentTabId = id + state.currentTab = state.tabs.find(tab => tab.id === id) + } catch (e) { + console.error('Can\'t open a tab id:' + id) + } }, updatePredefinedInquiries (state, inquiries) { state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries] diff --git a/src/views/Main/Inquiries/index.vue b/src/views/Main/Inquiries/index.vue index 9612c19..2f0f328 100644 --- a/src/views/Main/Inquiries/index.vue +++ b/src/views/Main/Inquiries/index.vue @@ -338,12 +338,14 @@ export default { storedInquiries.updateStorage(this.inquiries) // update tab, if renamed inquiry is opened - const tabIndex = this.findTabIndex(processedInquiry.id) - if (tabIndex >= 0) { + const tab = this.$store.state.tabs + .find(tab => tab.id === processedInquiry.id) + if (tab) { this.$store.commit('updateTab', { - index: tabIndex, - name: this.newName, - id: processedInquiry.id + tab, + newValues: { + name: this.newName + } }) } // hide dialog @@ -367,9 +369,10 @@ export default { this.inquiries.splice(this.processedInquiryIndex, 1) // Close deleted inquiry tab if it was opened - const tabIndex = this.findTabIndex(this.processedInquiryId) - if (tabIndex >= 0) { - this.$store.commit('deleteTab', tabIndex) + const tab = this.$store.state.tabs + .find(tab => tab.id === this.processedInquiryId) + if (tab) { + this.$store.commit('deleteTab', tab) } // Clear checkbox @@ -383,10 +386,12 @@ export default { // Close deleted inquiries if it was opened const tabs = this.$store.state.tabs - for (let i = tabs.length - 1; i >= 0; i--) { + let i = tabs.length - 1 + while (i > -1) { if (this.selectedInquiriesIds.has(tabs[i].id)) { - this.$store.commit('deleteTab', i) + this.$store.commit('deleteTab', tabs[i]) } + i-- } // Clear checkboxes @@ -395,9 +400,6 @@ export default { this.selectedInquiriesCount = this.selectedInquiriesIds.size storedInquiries.updateStorage(this.inquiries) }, - findTabIndex (id) { - return this.$store.state.tabs.findIndex(tab => tab.id === id) - }, exportToFile (inquiryList, fileName) { storedInquiries.export(inquiryList, fileName) }, diff --git a/src/views/Main/MainMenu.vue b/src/views/Main/MainMenu.vue index fa97664..1e8213a 100644 --- a/src/views/Main/MainMenu.vue +++ b/src/views/Main/MainMenu.vue @@ -80,19 +80,10 @@ export default { return this.$store.state.currentTab }, isSaved () { - if (!this.currentInquiry) { - return false - } - const tabIndex = this.currentInquiry.tabIndex - const tab = this.$store.state.tabs[tabIndex] - return tab && tab.isSaved + return this.currentInquiry && this.currentInquiry.isSaved }, isPredefined () { - if (this.currentInquiry) { - return this.currentInquiry.isPredefined - } else { - return false - } + return this.currentInquiry && this.currentInquiry.isPredefined }, runDisabled () { return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query) @@ -145,13 +136,15 @@ export default { // Update tab in store this.$store.commit('updateTab', { - index: this.currentInquiry.tabIndex, - name: value.name, - id: value.id, - query: value.query, - viewType: value.viewType, - viewOptions: value.viewOptions, - isSaved: true + tab: this.currentInquiry, + newValues: { + name: value.name, + id: value.id, + query: value.query, + viewType: value.viewType, + viewOptions: value.viewOptions, + isSaved: true + } }) // Restore data: diff --git a/src/views/Main/Workspace/Tabs/Tab/index.vue b/src/views/Main/Workspace/Tabs/Tab/index.vue index 4e4c71f..333e469 100644 --- a/src/views/Main/Workspace/Tabs/Tab/index.vue +++ b/src/views/Main/Workspace/Tabs/Tab/index.vue @@ -7,40 +7,40 @@ :after="{ size: 50, max: 100 }" > -
+
- + - + - +
* @@ -13,20 +13,14 @@ {{ tab.tempName }}
- +
Create @@ -38,25 +32,25 @@
Close tab {{ - closingTabIndex !== null - ? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`) + closingTab !== null + ? (closingTab.name || `[${closingTab.tempName}]`) : '' }}
You have unsaved changes. Save changes in {{ - closingTabIndex !== null - ? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`) + closingTab !== null + ? (closingTab.name || `[${closingTab.tempName}]`) : '' }} before closing?
- - +
@@ -73,14 +67,14 @@ export default { }, data () { return { - closingTabIndex: null + closingTab: null } }, computed: { tabs () { return this.$store.state.tabs }, - selectedIndex () { + selectedTabId () { return this.$store.state.currentTabId } }, @@ -97,25 +91,24 @@ export default { selectTab (id) { this.$store.commit('setCurrentTabId', id) }, - beforeCloseTab (index) { - this.closingTabIndex = index - if (!this.tabs[index].isSaved) { + beforeCloseTab (tab) { + this.closingTab = tab + if (!tab.isSaved) { this.$modal.show('close-warn') } else { - this.closeTab(index) + this.closeTab(tab) } }, - closeTab (index) { + closeTab (tab) { this.$modal.hide('close-warn') - this.closingTabIndex = null - this.$store.commit('deleteTab', index) + this.$store.commit('deleteTab', tab) }, - saveAndClose (index) { + saveAndClose (tab) { this.$root.$on('inquirySaved', () => { - this.closeTab(index) + this.closeTab(tab) this.$root.$off('inquirySaved') }) - this.selectTab(this.tabs[index].id) + this.selectTab(tab.id) this.$modal.hide('close-warn') this.$nextTick(() => { this.$root.$emit('saveInquiry') diff --git a/tests/lib/storedInquiries/storedInquiries.spec.js b/tests/lib/storedInquiries/storedInquiries.spec.js index 75f3a62..cc3f398 100644 --- a/tests/lib/storedInquiries/storedInquiries.spec.js +++ b/tests/lib/storedInquiries/storedInquiries.spec.js @@ -87,14 +87,14 @@ describe('storedInquiries.js', () => { it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => { const tab = { - initName: 'foo' + name: 'foo' } expect(storedInquiries.isTabNeedName(tab)).to.equal(false) }) it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => { const tab = { - initName: null, + name: null, tempName: 'Untitled' } expect(storedInquiries.isTabNeedName(tab)).to.equal(true) @@ -102,7 +102,7 @@ describe('storedInquiries.js', () => { it('isTabNeedName returns true when the inquiry is predefined', () => { const tab = { - initName: 'foo', + name: 'foo', isPredefined: true } @@ -351,14 +351,13 @@ describe('storedInquiries.js', () => { query: 'select * from foo', viewType: 'chart', viewOptions: [], - initName: null, - $refs: { - dataView: { - getOptionsForSave () { - return ['chart'] - } + name: null, + dataView: { + getOptionsForSave () { + return ['chart'] } } + } const value = storedInquiries.save(tab, 'foo') expect(value.id).to.equal(tab.id) @@ -376,19 +375,18 @@ describe('storedInquiries.js', () => { query: 'select * from foo', viewType: 'chart', viewOptions: [], - initName: null, - $refs: { - dataView: { - getOptionsForSave () { - return ['chart'] - } + name: null, + dataView: { + getOptionsForSave () { + return ['chart'] } } + } const first = storedInquiries.save(tab, 'foo') - tab.initName = 'foo' + tab.name = 'foo' tab.query = 'select * from foo' storedInquiries.save(tab) const inquiries = storedInquiries.getStoredInquiries() @@ -409,12 +407,10 @@ describe('storedInquiries.js', () => { query: 'select * from foo', viewType: 'chart', viewOptions: [], - initName: 'foo predefined', - $refs: { - dataView: { - getOptionsForSave () { - return ['chart'] - } + name: 'foo predefined', + dataView: { + getOptionsForSave () { + return ['chart'] } }, isPredefined: true diff --git a/tests/lib/tab.spec.js b/tests/lib/tab.spec.js new file mode 100644 index 0000000..d45bfc1 --- /dev/null +++ b/tests/lib/tab.spec.js @@ -0,0 +1,189 @@ +import { expect } from 'chai' +import sinon from 'sinon' +import Tab from '@/lib/tab.js' + +describe('tab.js', () => { + it('Creates a tab for new inquiry', () => { + const state = { + untitledLastIndex: 5 + } + + const newTab = new Tab(state) + expect(newTab).to.include({ + name: null, + tempName: 'Untitled 5', + query: undefined, + viewOptions: undefined, + isPredefined: undefined, + viewType: 'chart', + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: false, + state: state + }) + expect(newTab.layout).to.include({ + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }) + expect(newTab.id).to.have.lengthOf(21) + }) + + it('Creates a tab for existing inquiry', () => { + const state = { + untitledLastIndex: 5 + } + + const inquiry = { + id: 'qwerty', + query: 'SELECT * from foo', + viewType: 'pivot', + viewOptions: 'this is view options object', + name: 'Foo inquiry', + createdAt: '2022-12-05T18:30:30' + } + + const newTab = new Tab(state, inquiry) + expect(newTab).to.include({ + id: 'qwerty', + name: 'Foo inquiry', + tempName: 'Foo inquiry', + query: 'SELECT * from foo', + viewOptions: 'this is view options object', + isPredefined: undefined, + viewType: 'pivot', + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true, + state: state + }) + expect(newTab.layout).to.include({ + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }) + }) + + it('Set isGettingResults true when execute', async () => { + let resolveQuering + // mock store state + const state = { + currentTabId: 1, + dbName: 'fooDb', + db: { + execute: sinon.stub().returns(new Promise(resolve => { + resolveQuering = resolve + })), + refreshSchema: sinon.stub().resolves() + } + } + + const newTab = new Tab(state, { + id: 'qwerty', + query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', + viewType: 'cart', + viewOptions: 'this is view options object', + name: 'Foo inquiry', + createdAt: '2022-12-05T18:30:30' + }) + + expect(newTab.isGettingResults).to.equal(false) + newTab.execute() + expect(newTab.isGettingResults).to.equal(true) + resolveQuering() + }) + + it('Updates result with query execution result', async () => { + const result = { + columns: ['id', 'name'], + values: { + id: [1, 2], + name: ['Harry', 'Drako'] + } + } + + // mock store state + const state = { + currentTabId: 1, + dbName: 'fooDb', + db: { + execute: sinon.stub().resolves(result), + refreshSchema: sinon.stub().resolves() + } + } + + const newTab = new Tab(state, { + id: 'qwerty', + query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', + viewType: 'cart', + viewOptions: 'this is view options object', + name: 'Foo inquiry', + createdAt: '2022-12-05T18:30:30' + }) + + await newTab.execute() + expect(newTab.isGettingResults).to.equal(false) + expect(newTab.result).to.eql(result) + }) + + it('Updates error with query execution error', async () => { + // mock store state + const state = { + currentTabId: 1, + dbName: 'fooDb', + db: { + execute: sinon.stub().rejects(new Error('No such table')), + refreshSchema: sinon.stub().resolves() + } + } + + const newTab = new Tab(state, { + id: 'qwerty', + query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', + viewType: 'cart', + viewOptions: 'this is view options object', + name: 'Foo inquiry', + createdAt: '2022-12-05T18:30:30' + }) + + await newTab.execute() + expect(newTab.error.type).to.eql('error') + expect(newTab.error.message.toString()).to.equal('Error: No such table') + }) + + it('Updates schema after query execution', async () => { + const result = { + columns: ['id', 'name'], + values: { + id: [], + name: [] + } + } + + // mock store state + const state = { + currentTabId: 1, + dbName: 'fooDb', + db: { + execute: sinon.stub().resolves(result), + refreshSchema: sinon.stub().resolves() + } + } + + const newTab = new Tab(state, { + id: 'qwerty', + query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', + viewType: 'cart', + viewOptions: 'this is view options object', + name: 'Foo inquiry', + createdAt: '2022-12-05T18:30:30' + }) + + await newTab.execute() + expect(state.db.refreshSchema.calledOnce).to.equal(true) + }) +}) diff --git a/tests/store/actions.spec.js b/tests/store/actions.spec.js index dc23d2d..45ea799 100644 --- a/tests/store/actions.spec.js +++ b/tests/store/actions.spec.js @@ -11,7 +11,7 @@ describe('actions', () => { } let id = await addTab({ state }) - expect(state.tabs[0]).to.eql({ + expect(state.tabs[0]).to.include({ id: id, name: null, tempName: 'Untitled', @@ -22,7 +22,7 @@ describe('actions', () => { expect(state.untitledLastIndex).to.equal(1) id = await addTab({ state }) - expect(state.tabs[1]).to.eql({ + expect(state.tabs[1]).to.include({ id: id, name: null, tempName: 'Untitled 1', @@ -41,14 +41,13 @@ describe('actions', () => { const tab = { id: 1, name: 'test', - tempName: null, query: 'SELECT * from foo', viewType: 'chart', - viewOptions: {}, + viewOptions: 'an object with view options', isSaved: true } await addTab({ state }, tab) - expect(state.tabs[0]).to.eql(tab) + expect(state.tabs[0]).to.include(tab) expect(state.untitledLastIndex).to.equal(0) }) diff --git a/tests/store/mutations.spec.js b/tests/store/mutations.spec.js index 12dffbd..4300730 100644 --- a/tests/store/mutations.spec.js +++ b/tests/store/mutations.spec.js @@ -5,7 +5,6 @@ const { updateTab, deleteTab, setCurrentTabId, - setCurrentTab, updatePredefinedInquiries, setDb, setLoadingPredefinedInquiries, @@ -37,8 +36,7 @@ describe('mutations', () => { isPredefined: false } - const newTab = { - index: 0, + const newValues = { id: 1, name: 'new test', query: 'SELECT * from bar', @@ -51,7 +49,7 @@ describe('mutations', () => { tabs: [tab] } - updateTab(state, newTab) + updateTab(state, { tab, newValues }) expect(state.tabs[0]).to.eql({ id: 1, name: 'new test', @@ -75,8 +73,7 @@ describe('mutations', () => { isPredefined: true } - const newTab = { - index: 0, + const newValues = { id: 2, name: 'new test', query: 'SELECT * from bar', @@ -90,7 +87,7 @@ describe('mutations', () => { currentTabId: 1 } - updateTab(state, newTab) + updateTab(state, { tab, newValues }) expect(state.tabs).to.have.lengthOf(1) expect(state.currentTabId).to.equal(2) expect(state.tabs[0].id).to.equal(2) @@ -111,8 +108,7 @@ describe('mutations', () => { isSaved: false } - const newTab = { - index: 0, + const newValues = { id: 1, name: 'new test' } @@ -121,7 +117,7 @@ describe('mutations', () => { tabs: [tab] } - updateTab(state, newTab) + updateTab(state, { tab, newValues }) expect(state.tabs).to.have.lengthOf(1) expect(state.tabs[0].id).to.equal(1) expect(state.tabs[0].name).to.equal('new test') @@ -141,8 +137,7 @@ describe('mutations', () => { isPredefined: true } - const newTab = { - index: 0, + const newValues = { isSaved: false } @@ -150,7 +145,7 @@ describe('mutations', () => { tabs: [tab] } - updateTab(state, newTab) + updateTab(state, { tab, newValues }) expect(state.tabs).to.have.lengthOf(1) expect(state.tabs[0].id).to.equal(1) expect(state.tabs[0].name).to.equal('test') @@ -184,7 +179,7 @@ describe('mutations', () => { currentTabId: 1 } - deleteTab(state, 0) + deleteTab(state, tab1) expect(state.tabs).to.have.lengthOf(1) expect(state.tabs[0].id).to.equal(2) expect(state.currentTabId).to.equal(2) @@ -216,7 +211,7 @@ describe('mutations', () => { currentTabId: 2 } - deleteTab(state, 1) + deleteTab(state, tab2) expect(state.tabs).to.have.lengthOf(1) expect(state.tabs[0].id).to.equal(1) expect(state.currentTabId).to.equal(1) @@ -258,7 +253,7 @@ describe('mutations', () => { currentTabId: 2 } - deleteTab(state, 1) + deleteTab(state, tab2) expect(state.tabs).to.have.lengthOf(2) expect(state.tabs[0].id).to.equal(1) expect(state.tabs[1].id).to.equal(3) @@ -281,43 +276,11 @@ describe('mutations', () => { currentTabId: 1 } - deleteTab(state, 0) + deleteTab(state, tab1) expect(state.tabs).to.have.lengthOf(0) expect(state.currentTabId).to.equal(null) }) - it('deleteTab - not opened', () => { - const tab1 = { - id: 1, - name: 'foo', - tempName: null, - query: 'SELECT * from foo', - viewType: 'chart', - viewOptions: {}, - isSaved: true - } - - const tab2 = { - id: 2, - name: 'bar', - tempName: null, - query: 'SELECT * from bar', - viewType: 'chart', - viewOptions: {}, - isSaved: true - } - - const state = { - tabs: [tab1, tab2], - currentTabId: 1 - } - - deleteTab(state, 1) - expect(state.tabs).to.have.lengthOf(1) - expect(state.tabs[0].id).to.equal(1) - expect(state.currentTabId).to.equal(1) - }) - it('setCurrentTabId', () => { const state = { currentTabId: 1 @@ -327,15 +290,6 @@ describe('mutations', () => { expect(state.currentTabId).to.equal(2) }) - it('setCurrentTab', () => { - const state = { - currentTab: { id: 1 } - } - - setCurrentTab(state, { id: 2 }) - expect(state.currentTab).to.eql({ id: 2 }) - }) - it('updatePredefinedInquiries - single', () => { const inquiry = { id: 1, diff --git a/tests/views/Main/MainMenu.spec.js b/tests/views/Main/MainMenu.spec.js index 4fb9393..a3f7448 100644 --- a/tests/views/Main/MainMenu.spec.js +++ b/tests/views/Main/MainMenu.spec.js @@ -45,7 +45,7 @@ describe('MainMenu.vue', () => { it('Save is not visible if there is no tabs', () => { const state = { currentTab: null, - tabs: [{}], + tabs: [], db: {} } const store = new Vuex.Store({ state }) @@ -62,13 +62,15 @@ describe('MainMenu.vue', () => { }) it('Save is disabled if current tab.isSaved is true', async () => { + const tab = { + id: 1, + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const store = new Vuex.Store({ state }) @@ -83,17 +85,19 @@ describe('MainMenu.vue', () => { expect(wrapper.find('#save-btn').element.disabled).to.equal(false) await vm.$set(state.tabs[0], 'isSaved', true) + await vm.$nextTick() expect(wrapper.find('#save-btn').element.disabled).to.equal(true) }) it('Creates a tab', async () => { + const tab = { + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const newInquiryId = 1 @@ -121,13 +125,14 @@ describe('MainMenu.vue', () => { }) it('Creates a tab and redirects to workspace', async () => { + const tab = { + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const newInquiryId = 1 @@ -156,13 +161,14 @@ describe('MainMenu.vue', () => { it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"', async () => { + const tab = { + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const store = new Vuex.Store({ state }) @@ -201,13 +207,14 @@ describe('MainMenu.vue', () => { it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"', async () => { + const tab = { + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const store = new Vuex.Store({ state }) @@ -245,13 +252,14 @@ describe('MainMenu.vue', () => { }) it('Ctrl B calls createNewInquiry', async () => { + const tab = { + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const store = new Vuex.Store({ state }) @@ -280,13 +288,14 @@ describe('MainMenu.vue', () => { it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace', async () => { + const tab = { + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const store = new Vuex.Store({ state }) @@ -325,13 +334,16 @@ describe('MainMenu.vue', () => { it('Saves the inquiry when no need the new name', async () => { + const tab = { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ id: 1, name: 'foo', isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const mutations = { @@ -364,13 +376,15 @@ describe('MainMenu.vue', () => { // check that the tab was updated expect(mutations.updateTab.calledOnceWith(state, sinon.match({ - index: 0, - name: 'foo', - id: 1, - query: 'SELECT * FROM foo', - viewType: 'chart', - viewOptions: [], - isSaved: true + tab, + newValues: { + name: 'foo', + id: 1, + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: [], + isSaved: true + } }))).to.equal(true) // check that 'inquirySaved' event was triggered on $root @@ -378,13 +392,17 @@ describe('MainMenu.vue', () => { }) it('Shows en error when the new name is needed but not specifyied', async () => { + const tab = { + id: 1, + name: null, + tempName: 'Untitled', + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const mutations = { @@ -424,13 +442,17 @@ describe('MainMenu.vue', () => { }) it('Saves the inquiry with a new name', async () => { + const tab = { + id: 1, + name: null, + tempName: 'Untitled', + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const mutations = { @@ -475,13 +497,15 @@ describe('MainMenu.vue', () => { // check that the tab was updated expect(mutations.updateTab.calledOnceWith(state, sinon.match({ - index: 0, - name: 'foo', - id: 1, - query: 'SELECT * FROM foo', - viewType: 'chart', - viewOptions: [], - isSaved: true + tab: tab, + newValues: { + name: 'foo', + id: 1, + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: [], + isSaved: true + } }))).to.equal(true) // check that 'inquirySaved' event was triggered on $root @@ -489,23 +513,26 @@ describe('MainMenu.vue', () => { }) it('Saves a predefined inquiry with a new name', async () => { - const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0, - isPredefined: true, - result: { - columns: ['id', 'name'], - values: [ - [1, 'Harry Potter'], - [2, 'Drako Malfoy'] - ] - }, - viewType: 'chart', - viewOptions: [] + const tab = { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isPredefined: true, + result: { + columns: ['id', 'name'], + values: [ + [1, 'Harry Potter'], + [2, 'Drako Malfoy'] + ] }, - tabs: [{ id: 1, name: 'foo', isSaved: false, isPredefined: true }], + viewType: 'chart', + viewOptions: [], + isSaved: false + } + const state = { + currentTab: tab, + tabs: [tab], db: {} } const mutations = { @@ -553,13 +580,15 @@ describe('MainMenu.vue', () => { // check that the tab was updated expect(mutations.updateTab.calledOnceWith(state, sinon.match({ - index: 0, - name: 'bar', - id: 2, - query: 'SELECT * FROM foo', - viewType: 'chart', - viewOptions: [], - isSaved: true + tab, + newValues: { + name: 'bar', + id: 2, + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: [], + isSaved: true + } }))).to.equal(true) // check that 'inquirySaved' event was triggered on $root @@ -580,13 +609,17 @@ describe('MainMenu.vue', () => { }) it('Cancel saving', async () => { + const tab = { + id: 1, + name: null, + tempName: 'Untitled', + query: 'SELECT * FROM foo', + execute: sinon.stub(), + isSaved: false + } const state = { - currentTab: { - query: 'SELECT * FROM foo', - execute: sinon.stub(), - tabIndex: 0 - }, - tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }], + currentTab: tab, + tabs: [tab], db: {} } const mutations = { diff --git a/tests/views/Main/Workspace/Tabs/Tab/Tab.spec.js b/tests/views/Main/Workspace/Tabs/Tab/Tab.spec.js index 3cf2201..d533031 100644 --- a/tests/views/Main/Workspace/Tabs/Tab/Tab.spec.js +++ b/tests/views/Main/Workspace/Tabs/Tab/Tab.spec.js @@ -31,13 +31,23 @@ describe('Tab.vue', () => { store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo', - initViewType: 'chart', - initViewOptions: [], - tabIndex: 0, - isPredefined: false + tab: { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0 + } } }) @@ -60,7 +70,23 @@ describe('Tab.vue', () => { store, stubs: ['chart'], propsData: { - id: 1 + tab: { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0 + } } }) expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false) @@ -79,40 +105,51 @@ describe('Tab.vue', () => { store, stubs: ['chart'], propsData: { - id: 1 + tab: { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0 + } } }) expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false) }) - it('Calls setCurrentTab when becomes active', async () => { - // mock store state - const state = { - currentTabId: 0 - } - sinon.spy(mutations, 'setCurrentTab') - const store = new Vuex.Store({ state, mutations }) - - // mount the component - const wrapper = mount(Tab, { - store, - stubs: ['chart'], - propsData: { - id: 1 - } - }) - - state.currentTabId = 1 - await wrapper.vm.$nextTick() - expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true) - }) - it('Update tab state when a query is changed', async () => { // mock store state const state = { tabs: [ - { id: 1, name: 'foo', query: 'SELECT * FROM foo', chart: [], isSaved: true } + { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true + } ], currentTabId: 1 } @@ -124,13 +161,7 @@ describe('Tab.vue', () => { store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false + tab: state.tabs[0] } }) await wrapper.findComponent({ name: 'SqlEditor' }).vm.$emit('input', ' limit 100') @@ -141,7 +172,24 @@ describe('Tab.vue', () => { // mock store state const state = { tabs: [ - { id: 1, name: 'foo', query: 'SELECT * FROM foo', chart: [], isSaved: true } + { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true + } ], currentTabId: 1 } @@ -153,13 +201,7 @@ describe('Tab.vue', () => { store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false + tab: state.tabs[0] } }) await wrapper.findComponent({ name: 'DataView' }).vm.$emit('update') @@ -169,29 +211,38 @@ describe('Tab.vue', () => { it('Shows .result-in-progress message when executing query', async () => { // mock store state const state = { - currentTabId: 1, - db: { - execute () { return new Promise(() => {}) } - } + currentTabId: 1 } const store = new Vuex.Store({ state, mutations }) // mount the component + const tab = { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true + } const wrapper = mount(Tab, { store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false + tab } }) - wrapper.vm.execute() + tab.isGettingResults = true await wrapper.vm.$nextTick() expect(wrapper.find('.run-result-panel .result-in-progress').isVisible()).to.equal(true) }) @@ -199,30 +250,42 @@ describe('Tab.vue', () => { it('Shows error when executing query ends with error', async () => { // mock store state const state = { - currentTabId: 1, - db: { - execute: sinon.stub().rejects(new Error('There is no table foo')), - refreshSchema: sinon.stub().resolves() - } + currentTabId: 1 } const store = new Vuex.Store({ state, mutations }) + const tab = { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true + } // mount the component const wrapper = mount(Tab, { store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false + tab } }) - await wrapper.vm.execute() + tab.error = { + type: 'error', + message: 'There is no table foo' + } + await wrapper.vm.$nextTick() expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false) expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false) expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true) @@ -239,11 +302,26 @@ describe('Tab.vue', () => { } // mock store state const state = { - currentTabId: 1, - db: { - execute: sinon.stub().resolves(result), - refreshSchema: sinon.stub().resolves() - } + currentTabId: 1 + } + + const tab = { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true } const store = new Vuex.Store({ state, mutations }) @@ -253,83 +331,50 @@ describe('Tab.vue', () => { store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false + tab } }) - await wrapper.vm.execute() + tab.result = result + await wrapper.vm.$nextTick() expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false) expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false) expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false) expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result) }) - it('Updates schema after query execution', async () => { - const result = { - columns: ['id', 'name'], - values: { - id: [], - name: [] - } - } - - // mock store state - const state = { - currentTabId: 1, - dbName: 'fooDb', - db: { - execute: sinon.stub().resolves(result), - refreshSchema: sinon.stub().resolves() - } - } - - const store = new Vuex.Store({ state, mutations }) - - // mount the component - const wrapper = mount(Tab, { - store, - stubs: ['chart'], - propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false - } - }) - - await wrapper.vm.execute() - expect(state.db.refreshSchema.calledOnce).to.equal(true) - }) - it('Switches views', async () => { const state = { - currentTabId: 1, - db: {} + currentTabId: 1 } const store = new Vuex.Store({ state, mutations }) + const tab = { + id: 1, + name: 'foo', + query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isPredefined: false, + result: null, + isGettingResults: false, + error: null, + time: 0, + isSaved: true + } + const wrapper = mount(Tab, { attachTo: place, store, stubs: ['chart'], propsData: { - id: 1, - initName: 'foo', - initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);', - initViewOptions: [], - initViewType: 'chart', - tabIndex: 0, - isPredefined: false + tab } }) diff --git a/tests/views/Main/Workspace/Tabs/Tabs.spec.js b/tests/views/Main/Workspace/Tabs/Tabs.spec.js index edc1522..8822dbb 100644 --- a/tests/views/Main/Workspace/Tabs/Tabs.spec.js +++ b/tests/views/Main/Workspace/Tabs/Tabs.spec.js @@ -94,8 +94,33 @@ describe('Tabs.vue', () => { // mock store state const state = { tabs: [ - { id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true }, - { id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false } + { + id: 1, + name: 'foo', + query: 'select * from foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: true + }, + { + id: 2, + name: null, + tempName: 'Untitled', + query: '', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: false + } ], currentTabId: 2 } @@ -125,8 +150,33 @@ describe('Tabs.vue', () => { // mock store state const state = { tabs: [ - { id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true }, - { id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false } + { + id: 1, + name: 'foo', + query: 'select * from foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: true + }, + { + id: 2, + name: null, + tempName: 'Untitled', + query: '', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: false + } ], currentTabId: 2 } @@ -166,8 +216,33 @@ describe('Tabs.vue', () => { // mock store state const state = { tabs: [ - { id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true }, - { id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false } + { + id: 1, + name: 'foo', + query: 'select * from foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: true + }, + { + id: 2, + name: null, + tempName: 'Untitled', + query: '', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: false + } ], currentTabId: 2 } @@ -211,8 +286,33 @@ describe('Tabs.vue', () => { // mock store state const state = { tabs: [ - { id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true }, - { id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false } + { + id: 1, + name: 'foo', + query: 'select * from foo', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: true + }, + { + id: 2, + name: null, + tempName: 'Untitled', + query: '', + viewType: 'chart', + viewOptions: {}, + layout: { + sqlEditor: 'above', + table: 'bottom', + dataView: 'hidden' + }, + isSaved: false + } ], currentTabId: 2 }