1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-07 02:28:54 +08:00

5 Commits

Author SHA1 Message Date
lana-k
244ba9eb08 #116 add JSON/NDJSON 2024-09-17 11:35:53 +02:00
lana-k
53e5194295 #116 update tests 2024-09-16 23:49:02 +02:00
lana-k
04274ef19a #116 fix lint 2024-09-15 18:08:46 +02:00
lana-k
3893a66f4e Merge branch 'master' of github.com:lana-k/sqliteviz 2024-09-05 22:15:38 +02:00
lana-k
1b6b7c71e9 #116 JSON file import 2024-09-05 22:15:12 +02:00
20 changed files with 1034 additions and 232 deletions

View File

@@ -4,11 +4,12 @@
# sqliteviz
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can:
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
- import a CSV file into a SQLite database and visualize imported data
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file
- manage inquiries and run them against different databases
- import/export inquiries from/to a JSON file

View File

@@ -1,6 +1,6 @@
{
"name": "sqliteviz",
"version": "0.24.1",
"version": "0.25.0",
"license": "Apache-2.0",
"private": true,
"scripts": {

View File

@@ -1,6 +1,6 @@
{
"background_color": "white",
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
"display": "fullscreen",
"icons": [
{

View File

@@ -8,8 +8,8 @@
:clickToClose="false"
>
<div class="dialog-header">
CSV import
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
{{ typeName }} import
<close-icon @click="cancelImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<text-field
@@ -18,15 +18,15 @@
width="484px"
:disabled="disableDialog"
:error-msg="tableNameError"
id="csv-table-name"
id="csv-json-table-name"
/>
<div class="chars">
<div v-if="!isJson && !isNdJson" class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCsv"
:disabled="disableDialog"
@input="preview"
/>
<text-field
label="Quote char"
@@ -36,6 +36,7 @@
:disabled="disableDialog"
class="char-input"
id="quote-char"
@input="preview"
/>
<text-field
label="Escape char"
@@ -49,52 +50,52 @@
:disabled="disableDialog"
class="char-input"
id="escape-char"
@input="preview"
/>
</div>
<check-box
@click="header = $event"
:init="true"
v-if="!isJson && !isNdJson"
:init="header"
label="Use first row as column headers"
:disabled="disableDialog"
@click="changeHeaderDisplaying"
/>
<sql-table
v-if="previewData
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
"
v-if="previewData && previewData.rowCount > 0"
:data-set="previewData"
class="preview-table"
:preview="true"
class="preview-table"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
class="import-errors"
:messages="importMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
@click="cancelImport"
id="import-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
v-show="!importCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
:disabled="disableDialog || disableImport"
@click="loadToDb(file)"
id="import-start"
>
Import
</button>
<button
v-show="importCsvCompleted"
v-show="importCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
id="import-finish"
>
Finish
</button>
@@ -115,7 +116,7 @@ import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default {
name: 'CsvImport',
name: 'CsvJsonImport',
components: {
CloseIcon,
TextField,
@@ -124,33 +125,50 @@ export default {
SqlTable,
Logs
},
props: ['file', 'db', 'dialogName'],
props: {
file: File,
db: Object,
dialogName: String
},
data () {
return {
disableDialog: false,
disableImport: false,
tableName: '',
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
importCsvCompleted: false,
importCsvMessages: [],
importCompleted: false,
importMessages: [],
previewData: null,
addedTable: null,
tableNameError: ''
}
},
computed: {
isJson () {
return fIo.isJSON(this.file)
},
isNdJson () {
return fIo.isNDJSON(this.file)
},
typeName () {
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
}
},
watch: {
quoteChar () {
this.previewCsv()
isJson () {
if (this.isJson) {
this.delimiter = '\u001E'
this.header = false
}
},
escapeChar () {
this.previewCsv()
},
header () {
this.previewCsv()
isNdJson () {
if (this.isNdJson) {
this.delimiter = '\u001E'
this.header = false
}
},
tableName: time.debounce(function () {
this.tableNameError = ''
@@ -164,7 +182,11 @@ export default {
}, 400)
},
methods: {
cancelCsvImport () {
changeHeaderDisplaying (e) {
this.header = e
this.preview()
},
cancelImport () {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
@@ -175,14 +197,15 @@ export default {
}
},
reset () {
this.header = true
this.header = !this.isJson && !this.isNdJson
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
this.tableName = ''
this.disableDialog = false
this.importCsvCompleted = false
this.importCsvMessages = []
this.disableImport = false
this.importCompleted = false
this.importMessages = []
this.previewData = null
this.addedTable = null
this.tableNameError = ''
@@ -191,39 +214,69 @@ export default {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async previewCsv () {
this.importCsvCompleted = false
async preview () {
this.disableImport = false
if (!this.file) {
return
}
this.importCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc']
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const parseResult = this.isJson
? await this.getJsonParseResult(this.file)
: await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.previewData.rowCount = parseResult.rowCount
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
this.importMessages = parseResult.messages || []
if (this.previewData.rowCount === 0) {
this.disableImport = true
this.importMessages.push({
type: 'info',
message: 'No rows to import.'
})
}
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
this.importMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
console.error(err)
this.importMessages = [{
message: err,
type: 'error'
}]
}
},
async loadFromCsv (file) {
async getJsonParseResult (file) {
const jsonContent = await fIo.getFileContent(file)
const isEmpty = !jsonContent.trim()
return {
data: {
columns: ['doc'],
values: { doc: !isEmpty ? [jsonContent] : [] }
},
hasErrors: false,
messages: [],
rowCount: +(!isEmpty)
}
},
async loadToDb (file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
@@ -234,17 +287,18 @@ export default {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc']
}
const parseCsvMsg = {
message: 'Parsing CSV...',
const parsingMsg = {
message: `Parsing ${this.typeName}...`,
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
this.importMessages.push(parsingMsg)
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
message: `Importing ${this.typeName} into a SQLite database...`,
type: 'info'
}
let importLoadingIndicator = null
@@ -256,27 +310,30 @@ export default {
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
const parseResult = this.isJson
? await this.getJsonParseResult(file)
: await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.rowCount
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
parsingMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
this.importMessages = this.importMessages.concat(parseResult.messages)
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
// Inform about parsing success
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Loading indicator for parsing is not needed anymore
clearTimeout(parsingLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
this.importMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
@@ -291,52 +348,108 @@ export default {
this.addedTable = this.tableName
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
importMsg.message = `Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCsvCompleted = true
this.importCompleted = true
} else {
parseCsvMsg.message = 'Parsing ended with errors.'
parseCsvMsg.type = 'info'
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parsingMsg.message = 'Parsing ended with errors.'
parsingMsg.type = 'info'
this.importMessages = this.importMessages.concat(parseResult.messages)
}
} catch (err) {
if (parseCsvMsg.type === 'loading') {
parseCsvMsg.type = 'info'
console.error(err)
if (parsingMsg.type === 'loading') {
parsingMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importCsvMessages.push({
this.importMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parseCsvLoadingIndicator)
clearTimeout(parsingLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish () {
this.$modal.hide(this.dialogName)
const stmt = [
'/*',
const stmt = this.getQueryExample()
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCompleted = false
this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
},
getQueryExample () {
return this.isNdJson ? this.getNdJsonQueryExample()
: this.isJson ? this.getJsonQueryExample()
: [
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
].join('\n')
},
getNdJsonQueryExample () {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
return [
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.',
' */',
`SELECT doc->>'${firstKey}'`,
`FROM "${this.addedTable}"`
].join('\n')
} catch (err) {
console.error(err)
return [
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
' */',
'SELECT *',
`FROM "${this.addedTable}"`
].join('\n')
}
},
getJsonQueryExample () {
try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0]
return [
'/*',
` * Your JSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.',
' */',
'SELECT *',
`FROM "${this.addedTable}"`,
`JOIN json_each(doc, '$.${firstKey}')`
].join('\n')
} catch (err) {
console.error(err)
return [
'/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
' */',
'SELECT *',
`FROM "${this.addedTable}"`
].join('\n')
}
}
}
}
@@ -347,10 +460,14 @@ export default {
padding-bottom: 0;
}
#csv-json-table-name {
margin-bottom: 24px;
}
.chars {
display: flex;
align-items: flex-end;
margin: 24px 0 20px;
margin: 0 0 20px;
}
.char-input {
margin-right: 44px;
@@ -359,7 +476,7 @@ export default {
margin-top: 18px;
}
.import-csv-errors {
.import-errors {
height: 136px;
margin-top: 8px;
}

View File

@@ -10,7 +10,8 @@
@click="browse"
>
<div class="text">
Drop the database or CSV file here or click to choose a file from your computer.
Drop the database, CSV, JSON or NDJSON file here
or click to choose a file from your computer.
</div>
</div>
</div>
@@ -41,13 +42,13 @@
</div>
<div id="error" class="error"></div>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
<!--Parse csv or json dialog -->
<csv-json-import
ref="addCsvJson"
:file="file"
:db="newDb"
dialog-name="importFromCsv"
@cancel="cancelCsvImport"
dialog-name="importFromCsvJson"
@cancel="cancelImport"
@finish="finish"
/>
</div>
@@ -57,7 +58,7 @@
import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database'
import CsvImport from '@/components/CsvImport'
import CsvJsonImport from '@/components/CsvJsonImport'
import events from '@/lib/utils/events'
export default {
@@ -79,7 +80,7 @@ export default {
},
components: {
ChangeDbIcon,
CsvImport
CsvJsonImport
},
data () {
return {
@@ -102,7 +103,7 @@ export default {
}
},
methods: {
cancelCsvImport () {
cancelImport () {
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
@@ -128,21 +129,22 @@ export default {
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
events.send('database.import', file.size, {
from: 'csv',
from: isJson ? 'json' : 'csv',
new_db: true
})
this.file = file
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
return Promise.all([csvImport.previewCsv(), this.animationPromise])
.then(csvImport.open)
const csvJsonImportModal = this.$refs.addCsvJson
csvJsonImportModal.reset()
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
.then(csvJsonImportModal.open)
}
},
browse () {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
.then(this.checkFile)
},

View File

@@ -75,7 +75,7 @@ export default {
components: { Pager },
props: {
dataSet: Object,
time: String,
time: [String, Number],
pageSize: {
type: Number,
default: 20

View File

@@ -34,7 +34,7 @@
</defs>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Add new table from CSV
Add new table from CSV, JSON or NDJSON
</span>
</span>
</template>

View File

@@ -22,7 +22,7 @@
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Load another database or CSV
Load another database, CSV, JSON or NDJSON
</span>
</div>
</template>

View File

@@ -7,9 +7,9 @@ const hintsByCode = {
}
export default {
getResult (source) {
getResult (source, columns) {
const result = {
columns: []
columns: columns || []
}
const values = {}
if (source.meta.fields) {
@@ -24,8 +24,18 @@ export default {
return value
})
})
} else if (columns) {
columns.forEach((col, i) => {
values[col] = source.data.map(row => {
let value = row[i]
if (value instanceof Date) {
value = value.toISOString()
}
return value
})
})
} else {
for (let i = 0; i <= source.data[0].length - 1; i++) {
for (let i = 0; source.data[0] && i <= source.data[0].length - 1; i++) {
const colName = `col${i + 1}`
result.columns.push(colName)
values[colName] = source.data.map(row => {
@@ -76,7 +86,7 @@ export default {
let res
try {
res = {
data: this.getResult(results),
data: this.getResult(results, config.columns),
delimiter: results.meta.delimiter,
hasErrors: false,
rowCount: results.data.length

View File

@@ -1,4 +1,10 @@
export default {
isJSON (file) {
return file && file.type === 'application/json'
},
isNDJSON (file) {
return file && file.name.endsWith('.ndjson')
},
isDatabase (file) {
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
return file.type
@@ -51,19 +57,20 @@ export default {
},
importFile () {
const reader = new FileReader()
return this.getFileFromUser('.json')
.then(file => {
return new Promise((resolve, reject) => {
reader.onload = e => {
resolve(e.target.result)
}
reader.readAsText(file)
})
return this.getFileContent(file)
})
},
getFileContent (file) {
const reader = new FileReader()
return new Promise(resolve => {
reader.onload = e => resolve(e.target.result)
reader.readAsText(file)
})
},
readFile (path) {
return fetch(path)
},

View File

@@ -10,7 +10,7 @@
</div>
<db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile"/>
<add-table-icon @click="addCsv"/>
<add-table-icon @click="addCsvJson"/>
</div>
<div v-show="schemaVisible" class="schema">
<table-description
@@ -21,12 +21,12 @@
/>
</div>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
<!--Parse csv or json dialog -->
<csv-json-import
ref="addCsvJson"
:file="file"
:db="$store.state.db"
dialog-name="addCsv"
dialog-name="addCsvJson"
/>
</div>
</template>
@@ -40,7 +40,7 @@ import TreeChevron from '@/components/svg/treeChevron'
import DbUploader from '@/components/DbUploader'
import ExportIcon from '@/components/svg/export'
import AddTableIcon from '@/components/svg/addTable'
import CsvImport from '@/components/CsvImport'
import CsvJsonImport from '@/components/CsvJsonImport'
export default {
name: 'Schema',
@@ -51,7 +51,7 @@ export default {
DbUploader,
ExportIcon,
AddTableIcon,
CsvImport
CsvJsonImport
},
data () {
return {
@@ -80,16 +80,17 @@ export default {
exportToFile () {
this.$store.state.db.export(`${this.dbName}.sqlite`)
},
async addCsv () {
this.file = await fIo.getFileFromUser('.csv')
async addCsvJson () {
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
await csvImport.previewCsv()
csvImport.open()
const csvJsonImportModal = this.$refs.addCsvJson
csvJsonImportModal.reset()
await csvJsonImportModal.preview()
csvJsonImportModal.open()
const isJson = fIo.isJSON(this.file) || fIo.isNDJSON(this.file)
events.send('database.import', this.file.size, {
from: 'csv',
from: isJson ? 'json' : 'csv',
new_db: false
})
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<main-menu />
<keep-alive include="Workspace">
<keep-alive include="Workspace,Inquiries">
<router-view id="main-view" />
</keep-alive>
</div>

View File

@@ -2,10 +2,10 @@ import { expect } from 'chai'
import sinon from 'sinon'
import Vuex from 'vuex'
import { mount } from '@vue/test-utils'
import CsvImport from '@/components/CsvImport'
import CsvJsonImport from '@/components/CsvJsonImport'
import csv from '@/lib/csv'
describe('CsvImport.vue', () => {
describe('CsvJsonImport.vue', () => {
let state = {}
let actions = {}
let mutations = {}
@@ -13,7 +13,7 @@ describe('CsvImport.vue', () => {
let clock
let wrapper
const newTabId = 1
const file = { name: 'my data.csv' }
const file = new File([], 'my data.csv')
beforeEach(() => {
clock = sinon.useFakeTimers()
@@ -40,11 +40,11 @@ describe('CsvImport.vue', () => {
}
// mount the component
wrapper = mount(CsvImport, {
wrapper = mount(CsvJsonImport, {
store,
propsData: {
file,
dialogName: 'addCsv',
dialogName: 'addCsvJson',
db
}
})
@@ -74,11 +74,12 @@ describe('CsvImport.vue', () => {
}]
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
expect(wrapper.find('.dialog-header').text()).to.equal('CSV import')
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
@@ -93,8 +94,36 @@ describe('CsvImport.vue', () => {
.to.include('Information about row 0. Comma was used as a standart delimiter.')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
expect(wrapper.find('#import-start').attributes().disabled).to.equal(undefined)
})
it('disables import if no rows found', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['col2', 'col1'],
values: {
col1: [],
col2: []
}
},
rowCount: 0,
messages: []
})
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
const rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(0)
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('No rows to import.')
expect(wrapper.find('.no-data').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
expect(wrapper.find('#import-start').attributes().disabled).to.equal('disabled')
})
it('reparses when parameters changes', async () => {
@@ -111,7 +140,7 @@ describe('CsvImport.vue', () => {
rowCount: 1
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await csv.parse.returnValues[0]
await wrapper.vm.$nextTick()
@@ -231,20 +260,32 @@ describe('CsvImport.vue', () => {
col2: ['foo']
}
},
rowCount: 1
rowCount: 1,
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
let resolveParsing
parse.onCall(1).returns(new Promise(resolve => {
resolveParsing = resolve
resolveParsing = () => resolve({
delimiter: '|',
data: {
columns: ['col1', 'col2'],
values: {
col1: [1],
col2: ['foo']
}
},
rowCount: 1,
messages: []
})
}))
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await wrapper.vm.$nextTick()
// "Parsing CSV..." in the logs
@@ -262,11 +303,11 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
await resolveParsing()
await parse.returnValues[1]
@@ -306,7 +347,7 @@ describe('CsvImport.vue', () => {
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
@@ -315,8 +356,8 @@ describe('CsvImport.vue', () => {
resolveImport = resolve
}))
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
@@ -329,11 +370,11 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
await resolveImport()
})
@@ -377,12 +418,12 @@ describe('CsvImport.vue', () => {
resolveImport = resolve
}))
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
@@ -397,11 +438,11 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
await resolveImport()
})
@@ -440,12 +481,12 @@ describe('CsvImport.vue', () => {
}]
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
@@ -460,11 +501,11 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
})
it('has proper state before import is completed', async () => {
@@ -501,12 +542,12 @@ describe('CsvImport.vue', () => {
wrapper.vm.db.addTableFromCsv = sinon.stub()
.resolves(new Promise(resolve => { resolveImport = resolve }))
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
@@ -525,11 +566,11 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
// After resolving - loading indicator is not shown
@@ -570,12 +611,12 @@ describe('CsvImport.vue', () => {
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
@@ -589,10 +630,10 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
})
it('import fails', async () => {
@@ -627,12 +668,12 @@ describe('CsvImport.vue', () => {
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
@@ -647,10 +688,10 @@ describe('CsvImport.vue', () => {
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
})
it('import finish', async () => {
@@ -668,19 +709,19 @@ describe('CsvImport.vue', () => {
messages: []
})
wrapper.vm.previewCsv()
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#import-start').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('#csv-finish').trigger('click')
await wrapper.find('#import-finish').trigger('click')
expect(actions.addTab.calledOnce).to.equal(true)
await actions.addTab.returnValues[0]
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
})
@@ -699,47 +740,525 @@ describe('CsvImport.vue', () => {
messages: []
})
await wrapper.vm.previewCsv()
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-import').trigger('click')
await wrapper.find('#import-start').trigger('click')
await wrapper.vm.$nextTick()
await wrapper.find('#csv-cancel').trigger('click')
await wrapper.find('#import-cancel').trigger('click')
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
})
it('checks table name', async () => {
sinon.stub(csv, 'parse').resolves()
await wrapper.vm.previewCsv()
sinon.stub(csv, 'parse').resolves({
data: {},
hasErrors: false,
messages: []
})
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-table-name input').setValue('foo')
await wrapper.find('#csv-json-table-name input').setValue('foo')
await clock.tick(400)
await wrapper.vm.$nextTick()
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
expect(wrapper.find('#csv-json-table-name .text-field-error').text()).to.equal('')
wrapper.vm.db.validateTableName = sinon.stub().rejects(new Error('this is a bad table name'))
await wrapper.find('#csv-table-name input').setValue('bar')
await wrapper.find('#csv-json-table-name input').setValue('bar')
await clock.tick(400)
await wrapper.vm.$nextTick()
expect(wrapper.find('#csv-table-name .text-field-error').text())
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
.to.equal('this is a bad table name. Try another table name.')
await wrapper.find('#csv-table-name input').setValue('')
await wrapper.find('#csv-json-table-name input').setValue('')
await clock.tick(400)
await wrapper.vm.$nextTick()
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
expect(wrapper.find('#csv-json-table-name .text-field-error').text()).to.equal('')
await wrapper.find('#csv-import').trigger('click')
expect(wrapper.find('#csv-table-name .text-field-error').text())
await wrapper.find('#import-start').trigger('click')
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
.to.equal("Table name can't be empty")
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
})
})
describe('CsvJsonImport.vue - json', () => {
let state = {}
let actions = {}
let mutations = {}
let store = {}
let clock
let wrapper
const newTabId = 1
const file = new File(
[new Blob(
[JSON.stringify({ foo: [1, 2, 3] }, null, 2)],
{ type: 'application/json' }
)],
'my data.json',
{ type: 'application/json' })
beforeEach(() => {
clock = sinon.useFakeTimers()
// mock store state and mutations
state = {}
mutations = {
setDb: sinon.stub(),
setCurrentTabId: sinon.stub()
}
actions = {
addTab: sinon.stub().resolves(newTabId)
}
store = new Vuex.Store({ state, mutations, actions })
const db = {
sanitizeTableName: sinon.stub().returns('my_data'),
addTableFromCsv: sinon.stub().resolves(),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub(),
validateTableName: sinon.stub().resolves(),
execute: sinon.stub().resolves(),
refreshSchema: sinon.stub().resolves()
}
// mount the component
wrapper = mount(CsvJsonImport, {
store,
propsData: {
file,
dialogName: 'addCsvJson',
db
}
})
})
afterEach(() => {
sinon.restore()
})
it('previews', async () => {
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
expect(wrapper.find('.dialog-header').text()).to.equal('JSON import')
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
expect(wrapper.findComponent({ name: 'delimiter-selector' }).exists()).to.equal(false)
expect(wrapper.find('#quote-char input').exists()).to.equal(false)
expect(wrapper.find('#escape-char input').exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).exists()).to.equal(false)
const rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal([
'{',
' "foo": [',
' 1,',
' 2,',
' 3',
' ]',
'}'
].join('\n')
)
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
})
it('has proper state before parsing is complete', async () => {
const getJsonParseResult = sinon.stub(wrapper.vm, 'getJsonParseResult')
getJsonParseResult.onCall(0).returns({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
hasErrors: false,
messages: []
})
let resolveParsing
getJsonParseResult.onCall(1).returns(new Promise(resolve => {
resolveParsing = () => resolve({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
hasErrors: false,
messages: []
})
}))
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await wrapper.vm.$nextTick()
// "Parsing JSON..." in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
.to.equal('Parsing JSON...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
await resolveParsing()
await getJsonParseResult.returnValues[1]
// Loading indicator is not shown when parsing is compete
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('has proper state before import is completed', async () => {
const getJsonParseResult = sinon.spy(wrapper.vm, 'getJsonParseResult')
let resolveImport = sinon.stub()
wrapper.vm.db.addTableFromCsv = sinon.stub()
.resolves(new Promise(resolve => { resolveImport = resolve }))
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await getJsonParseResult.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
.to.equal('Importing JSON into a SQLite database...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
// After resolving - loading indicator is not shown
await resolveImport()
await wrapper.vm.db.addTableFromCsv.returnValues[0]
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('import success', async () => {
const getJsonParseResult = sinon.spy(wrapper.vm, 'getJsonParseResult')
await wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await getJsonParseResult.returnValues[1]
await wrapper.vm.$nextTick()
// Import success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(3)
expect(logs.at(2).text()).to.contain('Importing JSON into a SQLite database is completed in')
// All the dialog controls are enabled
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
})
})
describe('CsvJsonImport.vue - ndjson', () => {
let state = {}
let actions = {}
let mutations = {}
let store = {}
let clock
let wrapper
const newTabId = 1
const file = new File([], 'my data.ndjson')
beforeEach(() => {
clock = sinon.useFakeTimers()
// mock store state and mutations
state = {}
mutations = {
setDb: sinon.stub(),
setCurrentTabId: sinon.stub()
}
actions = {
addTab: sinon.stub().resolves(newTabId)
}
store = new Vuex.Store({ state, mutations, actions })
const db = {
sanitizeTableName: sinon.stub().returns('my_data'),
addTableFromCsv: sinon.stub().resolves(),
createProgressCounter: sinon.stub().returns(1),
deleteProgressCounter: sinon.stub(),
validateTableName: sinon.stub().resolves(),
execute: sinon.stub().resolves(),
refreshSchema: sinon.stub().resolves()
}
// mount the component
wrapper = mount(CsvJsonImport, {
store,
propsData: {
file,
dialogName: 'addCsvJson',
db
}
})
})
afterEach(() => {
sinon.restore()
})
it('previews', async () => {
sinon.stub(csv, 'parse').resolves({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
messages: []
})
wrapper.vm.preview()
await wrapper.vm.open()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
expect(wrapper.find('.dialog-header').text()).to.equal('JSON import')
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
expect(wrapper.findComponent({ name: 'delimiter-selector' }).exists()).to.equal(false)
expect(wrapper.find('#quote-char input').exists()).to.equal(false)
expect(wrapper.find('#escape-char input').exists()).to.equal(false)
expect(wrapper.findComponent({ name: 'check-box' }).exists()).to.equal(false)
const rows = wrapper.findAll('tbody tr')
expect(rows).to.have.lengthOf(1)
expect(rows.at(0).findAll('td').at(0).text()).to.equal('{ "foo": [ 1, 2, 3 ] }')
expect(wrapper.findComponent({ name: 'logs' }).text())
.to.include('Preview parsing is completed in')
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
})
it('has proper state before parsing is complete', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1
})
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
let resolveParsing
parse.onCall(1).returns(new Promise(resolve => {
resolveParsing = () => resolve({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
messages: []
})
}))
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await wrapper.vm.$nextTick()
// "Parsing JSON..." in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
.to.equal('Parsing JSON...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
await resolveParsing()
await parse.returnValues[1]
// Loading indicator is not shown when parsing is compete
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('has proper state before import is completed', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
hasErrors: false,
messages: []
})
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
hasErrors: false,
messages: []
})
let resolveImport = sinon.stub()
wrapper.vm.db.addTableFromCsv = sinon.stub()
.resolves(new Promise(resolve => { resolveImport = resolve }))
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Parsing success in the logs
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
.to.equal('Importing JSON into a SQLite database...')
// After 1 second - loading indicator is shown
await clock.tick(1000)
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(true)
// All the dialog controls are disabled
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
// After resolving - loading indicator is not shown
await resolveImport()
await wrapper.vm.db.addTableFromCsv.returnValues[0]
expect(
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
).to.equal(false)
})
it('import success', async () => {
const parse = sinon.stub(csv, 'parse')
parse.onCall(0).resolves({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 1,
hasErrors: false,
messages: []
})
// we need to separate calles because messages will mutate
parse.onCall(1).resolves({
delimiter: '|',
data: {
columns: ['doc'],
values: {
doc: ['{ "foo": [ 1, 2, 3 ] }']
}
},
rowCount: 2,
hasErrors: false,
messages: []
})
wrapper.vm.preview()
wrapper.vm.open()
await wrapper.vm.$nextTick()
await wrapper.find('#csv-json-table-name input').setValue('foo')
await wrapper.find('#import-start').trigger('click')
await csv.parse.returnValues[1]
await wrapper.vm.$nextTick()
// Import success in the logs
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
expect(logs).to.have.lengthOf(3)
expect(logs.at(2).text()).to.contain('Importing JSON into a SQLite database is completed in')
// All the dialog controls are enabled
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
})
})

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai'
import { mount, shallowMount } from '@vue/test-utils'
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
import DelimiterSelector from '@/components/CsvJsonImport/DelimiterSelector'
describe('DelimiterSelector', async () => {
it('shows the name of value', async () => {

View File

@@ -31,7 +31,7 @@ describe('DbUploader.vue', () => {
it('loads db on click and redirects to /workspace', async () => {
// mock getting a file from user
const file = { name: 'test.db' }
const file = new File([], 'test.db')
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
@@ -85,7 +85,7 @@ describe('DbUploader.vue', () => {
})
// mock a file dropped by a user
const file = { name: 'test.db' }
const file = new File([], 'test.db')
const dropData = { dataTransfer: new DataTransfer() }
Object.defineProperty(dropData.dataTransfer, 'files', {
value: [file],
@@ -103,7 +103,7 @@ describe('DbUploader.vue', () => {
it("doesn't redirect if already on /workspace", async () => {
// mock getting a file from user
const file = { name: 'test.db' }
const file = new File([], 'test.db')
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock db loading
@@ -136,7 +136,7 @@ describe('DbUploader.vue', () => {
it('shows parse dialog if gets csv file', async () => {
// mock getting a file from user
const file = { name: 'test.csv' }
const file = new File([], 'test.csv')
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
@@ -153,24 +153,92 @@ describe('DbUploader.vue', () => {
}
})
const CsvImport = wrapper.vm.$refs.addCsv
const CsvImport = wrapper.vm.$refs.addCsvJson
sinon.stub(CsvImport, 'reset')
sinon.stub(CsvImport, 'previewCsv').resolves()
sinon.stub(CsvImport, 'preview').resolves()
sinon.stub(CsvImport, 'open')
await wrapper.find('.drop-area').trigger('click')
await wrapper.vm.$nextTick()
expect(CsvImport.reset.calledOnce).to.equal(true)
await wrapper.vm.animationPromise
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
expect(CsvImport.preview.calledOnce).to.equal(true)
await wrapper.vm.$nextTick()
expect(CsvImport.open.calledOnce).to.equal(true)
wrapper.destroy()
})
it('deletes temporary db if CSV import is canceled', async () => {
it('shows parse dialog if gets json file', async () => {
// mock getting a file from user
const file = { name: 'test.csv' }
const file = new File([], 'test.json', { type: 'application/json' })
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/workspace' }
// mount the component
const wrapper = mount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
const JsonImport = wrapper.vm.$refs.addCsvJson
sinon.stub(JsonImport, 'reset')
sinon.stub(JsonImport, 'preview').resolves()
sinon.stub(JsonImport, 'open')
await wrapper.find('.drop-area').trigger('click')
await wrapper.vm.$nextTick()
expect(JsonImport.reset.calledOnce).to.equal(true)
await wrapper.vm.animationPromise
expect(JsonImport.preview.calledOnce).to.equal(true)
await wrapper.vm.$nextTick()
expect(JsonImport.open.calledOnce).to.equal(true)
wrapper.destroy()
})
it('shows parse dialog if gets ndjson file', async () => {
// mock getting a file from user
const file = new File([], 'test.ndjson')
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
const $router = { push: sinon.stub() }
const $route = { path: '/workspace' }
// mount the component
const wrapper = mount(DbUploader, {
attachTo: place,
store,
mocks: { $router, $route },
propsData: {
type: 'illustrated'
}
})
const JsonImport = wrapper.vm.$refs.addCsvJson
sinon.stub(JsonImport, 'reset')
sinon.stub(JsonImport, 'preview').resolves()
sinon.stub(JsonImport, 'open')
await wrapper.find('.drop-area').trigger('click')
await wrapper.vm.$nextTick()
expect(JsonImport.reset.calledOnce).to.equal(true)
await wrapper.vm.animationPromise
expect(JsonImport.preview.calledOnce).to.equal(true)
await wrapper.vm.$nextTick()
expect(JsonImport.open.calledOnce).to.equal(true)
wrapper.destroy()
})
it('deletes temporary db if import is canceled', async () => {
// mock getting a file from user
const file = new File([], 'test.csv')
sinon.stub(fu, 'getFileFromUser').resolves(file)
// mock router
@@ -186,9 +254,9 @@ describe('DbUploader.vue', () => {
}
})
const CsvImport = wrapper.vm.$refs.addCsv
const CsvImport = wrapper.vm.$refs.addCsvJson
sinon.stub(CsvImport, 'reset')
sinon.stub(CsvImport, 'previewCsv').resolves()
sinon.stub(CsvImport, 'preview').resolves()
sinon.stub(CsvImport, 'open')
await wrapper.find('.drop-area').trigger('click')

View File

@@ -28,7 +28,26 @@ describe('csv.js', () => {
})
})
it('getResult without fields', () => {
it('getResult without fields but with columns', () => {
const source = {
data: [
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
[2, 'bar', new Date('2021-07-30T14:10:15.717Z')]
],
meta: {}
}
const columns = ['id', 'name', 'date']
expect(csv.getResult(source, columns)).to.eql({
columns: ['id', 'name', 'date'],
values: {
id: [1, 2],
name: ['foo', 'bar'],
date: ['2021-06-30T14:10:24.717Z', '2021-07-30T14:10:15.717Z']
}
})
})
it('getResult without fields and columns', () => {
const source = {
data: [
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],

View File

@@ -106,10 +106,65 @@ describe('fileIo.js', () => {
await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
})
it('isJSON', () => {
let file = { type: 'application/json' }
expect(fIo.isJSON(file)).to.equal(true)
file = { type: 'application/x-sqlite3' }
expect(fIo.isJSON(file)).to.equal(false)
file = { type: '', name: 'test.db' }
expect(fIo.isJSON(file)).to.equal(false)
file = { type: '', name: 'test.sqlite' }
expect(fIo.isJSON(file)).to.equal(false)
file = { type: '', name: 'test.sqlite3' }
expect(fIo.isJSON(file)).to.equal(false)
file = { type: '', name: 'test.csv' }
expect(fIo.isJSON(file)).to.equal(false)
file = { type: '', name: 'test.ndjson' }
expect(fIo.isJSON(file)).to.equal(false)
file = { type: 'text', name: 'test.db' }
expect(fIo.isJSON(file)).to.equal(false)
})
it('isNDJSON', () => {
let file = { type: 'application/json', name: 'test.json' }
expect(fIo.isNDJSON(file)).to.equal(false)
file = { type: 'application/x-sqlite3', name: 'test.sqlite3' }
expect(fIo.isNDJSON(file)).to.equal(false)
file = { type: '', name: 'test.db' }
expect(fIo.isNDJSON(file)).to.equal(false)
file = { type: '', name: 'test.sqlite' }
expect(fIo.isNDJSON(file)).to.equal(false)
file = { type: '', name: 'test.sqlite3' }
expect(fIo.isNDJSON(file)).to.equal(false)
file = { type: '', name: 'test.csv' }
expect(fIo.isNDJSON(file)).to.equal(false)
file = { type: '', name: 'test.ndjson' }
expect(fIo.isNDJSON(file)).to.equal(true)
file = { type: 'text', name: 'test.db' }
expect(fIo.isNDJSON(file)).to.equal(false)
})
it('isDatabase', () => {
let file = { type: 'application/vnd.sqlite3' }
expect(fIo.isDatabase(file)).to.equal(true)
file = { type: 'application/json' }
expect(fIo.isDatabase(file)).to.equal(false)
file = { type: 'application/x-sqlite3' }
expect(fIo.isDatabase(file)).to.equal(true)
@@ -125,6 +180,9 @@ describe('fileIo.js', () => {
file = { type: '', name: 'test.csv' }
expect(fIo.isDatabase(file)).to.equal(false)
file = { type: '', name: 'test.ndjson' }
expect(fIo.isDatabase(file)).to.equal(false)
file = { type: 'text', name: 'test.db' }
expect(fIo.isDatabase(file)).to.equal(false)
})

View File

@@ -125,7 +125,7 @@ describe('Schema.vue', () => {
})
it('adds table', async () => {
const file = { name: 'test.csv' }
const file = new File([], 'test.csv')
sinon.stub(fIo, 'getFileFromUser').resolves(file)
sinon.stub(csv, 'parse').resolves({
@@ -152,20 +152,20 @@ describe('Schema.vue', () => {
const store = new Vuex.Store({ state, actions, mutations })
const wrapper = mount(Schema, { store, localVue })
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
sinon.spy(wrapper.vm, 'addCsv')
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
sinon.spy(wrapper.vm.$refs.addCsvJson, 'preview')
sinon.spy(wrapper.vm, 'addCsvJson')
sinon.spy(wrapper.vm.$refs.addCsvJson, 'loadToDb')
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
await wrapper.vm.addCsv.returnValues[0]
await wrapper.vm.$refs.addCsvJson.preview.returnValues[0]
await wrapper.vm.addCsvJson.returnValues[0]
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
await wrapper.find('#csv-import').trigger('click')
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
await wrapper.find('#csv-finish').trigger('click')
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
await wrapper.find('#import-start').trigger('click')
await wrapper.vm.$refs.addCsvJson.loadToDb.returnValues[0]
await wrapper.find('#import-finish').trigger('click')
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
await state.db.refreshSchema.returnValues[0]
expect(wrapper.vm.$store.state.db.schema).to.eql([