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:
@@ -1,39 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="codemirror-container">
|
<div class="codemirror-container">
|
||||||
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" />
|
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import CM from 'codemirror'
|
import hint from '@/hint'
|
||||||
import { codemirror } from 'vue-codemirror'
|
import { codemirror } from 'vue-codemirror'
|
||||||
import 'codemirror/lib/codemirror.css'
|
import 'codemirror/lib/codemirror.css'
|
||||||
import 'codemirror/mode/sql/sql.js'
|
import 'codemirror/mode/sql/sql.js'
|
||||||
import 'codemirror/theme/neo.css'
|
import 'codemirror/theme/neo.css'
|
||||||
import 'codemirror/addon/hint/show-hint.js'
|
|
||||||
import 'codemirror/addon/hint/show-hint.css'
|
import 'codemirror/addon/hint/show-hint.css'
|
||||||
import 'codemirror/addon/hint/sql-hint.js'
|
|
||||||
import 'codemirror/addon/display/autorefresh.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 {
|
export default {
|
||||||
name: 'SqlEditor',
|
name: 'SqlEditor',
|
||||||
props: ['value'],
|
props: ['value'],
|
||||||
components: {
|
components: { codemirror },
|
||||||
codemirror
|
|
||||||
},
|
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
query: this.value,
|
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: {
|
watch: {
|
||||||
query () {
|
query () {
|
||||||
this.$emit('input', this.query)
|
this.$emit('input', this.query)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onCmChange: debounce(function (editor) {
|
onChange: hint.show
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</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