1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

add sqlEditor test; refactoring

This commit is contained in:
lana-k
2021-02-11 16:45:18 +01:00
parent d68616b543
commit 840f4fa450
4 changed files with 265 additions and 48 deletions

View File

@@ -1,39 +1,22 @@
<template>
<div class="codemirror-container">
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" />
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
</div>
</template>
<script>
import CM from 'codemirror'
import hint from '@/hint'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/hint/sql-hint.js'
import 'codemirror/addon/display/autorefresh.js'
import { debounce } from 'debounce'
const sqlHint = CM.hint.sql
CM.hint.sql = (cm, options) => {
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
const result = sqlHint(cm, options)
// Don't show the hint if there is only one option
// and the token is already completed with this option
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) {
result.list = []
}
return result
}
export default {
name: 'SqlEditor',
props: ['value'],
components: {
codemirror
},
components: { codemirror },
data () {
return {
query: this.value,
@@ -49,40 +32,13 @@ export default {
}
}
},
computed: {
tables () {
const tables = {}
if (this.$store.state.schema) {
this.$store.state.schema.forEach(table => {
tables[table.name] = table.columns.map(column => column.name)
})
}
return tables
}
},
watch: {
query () {
this.$emit('input', this.query)
}
},
methods: {
onCmChange: debounce(function (editor) {
// Don't show autocomplete after a space or semicolon or in string literals
const ch = editor.getTokenAt(editor.getCursor()).string.slice(-1)
const tokenType = editor.getTokenAt(editor.getCursor()).type
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
return
}
const hintOptions = {
tables: this.tables,
completeSingle: false,
completeOnSingleClick: true,
alignWithWord: false
}
CM.showHint(editor, CM.hint.sql, hintOptions)
}, 400)
onChange: hint.show
}
}
</script>

45
src/hint.js Normal file
View File

@@ -0,0 +1,45 @@
import CM from 'codemirror'
import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/sql-hint.js'
import store from '@/store'
import { debounce } from 'debounce'
export function getHints (cm, options) {
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
const result = CM.hint.sql(cm, options)
// Don't show the hint if there is only one option
// and the token is already completed with this option
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) {
result.list = []
}
return result
}
const hintOptions = {
get tables () {
const tables = {}
if (store.state.schema) {
store.state.schema.forEach(table => {
tables[table.name] = table.columns.map(column => column.name)
})
}
return tables
},
completeSingle: false,
completeOnSingleClick: true,
alignWithWord: false
}
export default {
show: debounce(function (editor) {
// Don't show autocomplete after a space or semicolon or in string literals
const token = editor.getTokenAt(editor.getCursor())
const ch = token.string.slice(-1)
const tokenType = token.type
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
return
}
CM.showHint(editor, getHints, hintOptions)
}, 400)
}

View File

@@ -0,0 +1,11 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import SqlEditor from '@/components/SqlEditor.vue'
describe('SqlEditor.vue', () => {
it('Emits input event when a query is changed', async() => {
const wrapper = mount(SqlEditor)
await wrapper.findComponent({ name: 'codemirror'}).vm.$emit('input', 'SELECT * FROM foo')
expect(wrapper.emitted('input')[0]).to.eql(['SELECT * FROM foo'])
})
})

205
tests/unit/hint.spec.js Normal file
View File

@@ -0,0 +1,205 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { state } from '@/store'
import { default as hint, getHints } from '@/hint'
import CM from 'codemirror'
describe('hint.js', () => {
it('Calculates table list for hint', () => {
// mock store state
const schema = [
{
name: 'foo',
columns: [
{ name: 'fooId', type: 'INTEGER' },
{ name: 'name', type: 'NVARCHAR(20)' }
]
},
{
name: 'bar',
columns: [
{ name: 'barId', type: 'INTEGER' }
]
}
]
sinon.stub(state, 'schema').value(schema)
// mock showHint and editor
sinon.stub(CM, 'showHint')
const editor = {
getTokenAt() {
return {
string: 'SELECT',
type: 'keyword'
}
},
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers();
hint.show(editor)
clock.tick(500)
expect(CM.showHint.called).to.equal(true)
expect(CM.showHint.firstCall.args[2].tables).to.eql({
foo: ['fooId', 'name'],
bar: ['barId']
})
sinon.restore()
})
it("Doesn't show hint when in string or space, or ';'", () => {
// mock showHint and editor
sinon.stub(CM, 'showHint')
const editor = {
getTokenAt() {
return {
string: 'foo',
type: 'string'
}
},
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
expect(CM.showHint.called).to.equal(false)
sinon.restore()
})
it("Doesn't show hint after space", () => {
// mock showHint and editor
sinon.stub(CM, 'showHint')
const editor = {
getTokenAt() {
return {
string: ' ',
type: null
}
},
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
expect(CM.showHint.called).to.equal(false)
sinon.restore()
})
it("Doesn't show hint after ';'", () => {
// mock showHint and editor
sinon.stub(CM, 'showHint')
const editor = {
getTokenAt() {
return {
string: ';',
type: 'punctuation'
}
},
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers()
hint.show(editor)
clock.tick(500)
expect(CM.showHint.called).to.equal(false)
sinon.restore()
})
it("getHints returns [ ] if there is only one option and the token is already completed with this option", () => {
// mock CM.hint.sql and editor
sinon.stub(CM.hint, 'sql').returns({ list: [{ text: 'SELECT' }] })
const editor = {
getTokenAt() {
return {
string: 'select',
type: 'keyword'
}
},
getCursor: sinon.stub()
}
const hints = getHints(editor, {})
expect(hints.list).to.eql([])
sinon.restore()
})
it("getHints returns hints as is when there are more than one option", () => {
// mock CM.hint.sql and editor
let list = [
{ text: 'SELECT' },
{ text: 'ST' }
]
sinon.stub(CM.hint, 'sql').returns({ list })
const editor = {
getTokenAt() {
return {
string: 'se',
type: 'keyword'
}
},
getCursor: sinon.stub()
}
const hints = getHints(editor, {})
expect(hints.list).to.eql(list)
sinon.restore()
})
it("getHints returns hints as is when there only one option but the token is not cpmpleted", () => {
// mock CM.hint.sql and editor
let list = [{ text: 'SELECT' }]
sinon.stub(CM.hint, 'sql').returns({ list })
const editor = {
getTokenAt() {
return {
string: 'sele',
type: 'keyword'
}
},
getCursor: sinon.stub()
}
const hints = getHints(editor, {})
expect(hints.list).to.eql(list)
sinon.restore()
})
it('tables is empty object when schema is null', () => {
// mock store state
sinon.stub(state, 'schema').value(null)
// mock showHint and editor
sinon.stub(CM, 'showHint')
const editor = {
getTokenAt() {
return {
string: 'SELECT',
type: 'keyword'
}
},
getCursor: sinon.stub()
}
const clock = sinon.useFakeTimers();
hint.show(editor)
clock.tick(500)
expect(CM.showHint.called).to.equal(true)
expect(CM.showHint.firstCall.args[2].tables).to.eql({})
sinon.restore()
})
})