mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 10:08:52 +08:00
add sqlEditor test; refactoring
This commit is contained in:
@@ -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
45
src/hint.js
Normal 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)
|
||||
}
|
||||
11
tests/unit/components/SqlEditor.spec.js
Normal file
11
tests/unit/components/SqlEditor.spec.js
Normal 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
205
tests/unit/hint.spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user