cypress tests, lint, feature complete

This commit is contained in:
Martin McKeaveney 2020-08-24 15:48:34 +01:00
parent 40bf90c745
commit 1d56d9a2ce
14 changed files with 197 additions and 148 deletions

View File

@ -1,4 +1,7 @@
context('Create a View', () => { context('Create a View', () => {
const TOTAL_RECORDS = 6
before(() => { before(() => {
cy.visit('localhost:4001/_builder') cy.visit('localhost:4001/_builder')
cy.createApp('View App', 'View App Description') cy.createApp('View App', 'View App Description')
@ -7,6 +10,7 @@ context('Create a View', () => {
cy.addColumn('data', 'age', 'Number') cy.addColumn('data', 'age', 'Number')
cy.addColumn('data', 'rating', 'Number') cy.addColumn('data', 'rating', 'Number')
// 6 Records
cy.addRecord(["Students", 25, 1]) cy.addRecord(["Students", 25, 1])
cy.addRecord(["Students", 20, 3]) cy.addRecord(["Students", 20, 3])
cy.addRecord(["Students", 18, 6]) cy.addRecord(["Students", 18, 6])
@ -15,11 +19,39 @@ context('Create a View', () => {
cy.addRecord(["Teachers", 36, 3]) cy.addRecord(["Teachers", 36, 3])
}) })
it('creates a view', () => {
it('creates a stats view based on age', () => {
cy.contains("Create New View").click() cy.contains("Create New View").click()
cy.get("[placeholder='View Name']").type("Test View") cy.get("[placeholder='View Name']").type("Test View")
cy.contains("Save View").click() cy.contains("Save View").click()
cy.get(".title").contains("Test View")
cy.get("thead th").should(($headers) => {
expect($headers).to.have.length(3)
const headers = $headers.map((i, header) => Cypress.$(header).text())
expect(headers.get()).to.deep.eq([
"group",
"age",
"rating"
])
})
});
it('filters the view by age over 10', () => {
cy.contains("Filter").click()
cy.contains("Add Filter").click()
cy.get(".menu-container").find("select").first().select("age")
cy.get(".menu-container").find("select").eq(1).select("More Than")
cy.get("input[placeholder='age']").type(18)
cy.contains("Save").click()
cy.get("tbody tr").should(($values) => {
expect($values).to.have.length(5)
})
});
it('creates a stats calculation view based on age', () => {
cy.contains("Calculate").click()
cy.get(".menu-container").find("select").first().select("Statistics")
cy.get(".menu-container").find("select").eq(1).select("age")
cy.contains("Save").click()
cy.get("thead th").should(($headers) => { cy.get("thead th").should(($headers) => {
expect($headers).to.have.length(7) expect($headers).to.have.length(7)
const headers = $headers.map((i, header) => Cypress.$(header).text()) const headers = $headers.map((i, header) => Cypress.$(header).text())
@ -28,8 +60,8 @@ context('Create a View', () => {
"sum", "sum",
"min", "min",
"max", "max",
"sumsqr",
"count", "count",
"sumsqr",
"avg", "avg",
]) ])
}) })
@ -37,17 +69,17 @@ context('Create a View', () => {
const values = $values.map((i, value) => Cypress.$(value).text()) const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([ expect(values.get()).to.deep.eq([
"null", "null",
"173", "155",
"18", "20",
"49", "49",
"5671", "5",
"6", "5347",
"28.833333333333332" "31"
]) ])
}) })
}) })
it('groups the stats view by group', () => { it('groups the view by group', () => {
cy.contains("Group By").click() cy.contains("Group By").click()
cy.get("select").select("group") cy.get("select").select("group")
cy.contains("Save").click() cy.contains("Save").click()
@ -58,12 +90,12 @@ context('Create a View', () => {
const values = $values.map((i, value) => Cypress.$(value).text()) const values = $values.map((i, value) => Cypress.$(value).text())
expect(values.get()).to.deep.eq([ expect(values.get()).to.deep.eq([
"Students", "Students",
"88", "70",
"18", "20",
"25", "25",
"1974", "3",
"4", "1650",
"22" "23.333333333333332"
]) ])
}) })
}) })
@ -77,6 +109,7 @@ context('Create a View', () => {
}) })
it('deletes a view', () => { it('deletes a view', () => {
cy.contains("[data-cy=model-nav-item]", "Test View Updated").click()
cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click() cy.contains("[data-cy=model-nav-item]", "Test View Updated").find(".ri-more-line").click()
cy.contains("Delete").click() cy.contains("Delete").click()
cy.get(".content").contains("button", "Delete").click() cy.get(".content").contains("button", "Delete").click()

View File

@ -9,10 +9,6 @@ const INITIAL_BACKEND_UI_STATE = {
selectedDatabase: {}, selectedDatabase: {},
selectedModel: {}, selectedModel: {},
draftModel: {}, draftModel: {},
tabs: {
SETUP_PANEL: "SETUP",
NAVIGATION_PANEL: "NAVIGATE",
},
} }
export const getBackendUiStore = () => { export const getBackendUiStore = () => {
@ -128,7 +124,12 @@ export const getBackendUiStore = () => {
}, },
save: async view => { save: async view => {
const response = await api.post(`/api/views`, view) const response = await api.post(`/api/views`, view)
const viewMeta = await response.json() const json = await response.json()
const viewMeta = {
name: view.name,
...json,
}
store.update(state => { store.update(state => {
const viewModel = state.models.find( const viewModel = state.models.find(
@ -136,13 +137,10 @@ export const getBackendUiStore = () => {
) )
if (view.originalName) delete viewModel.views[view.originalName] if (view.originalName) delete viewModel.views[view.originalName]
viewModel.views[view.name] = viewMeta viewModel.views[view.name] = viewMeta
state.models = state.models state.models = state.models
state.selectedView = { state.selectedView = viewMeta
name: view.name,
...viewMeta
}
return state return state
}) })
}, },
@ -159,11 +157,3 @@ export const getBackendUiStore = () => {
return store return store
} }
// Store Actions
export const createDatabaseForApp = store => appInstance => {
store.update(state => {
state.appInstances.push(appInstance)
return state
})
}

View File

@ -1,7 +1,6 @@
import { values, cloneDeep } from "lodash/fp" import { values, cloneDeep } from "lodash/fp"
import { get_capitalised_name } from "../../helpers" import { get_capitalised_name } from "../../helpers"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend"
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import api from "../api" import api from "../api"
import { DEFAULT_PAGES_OBJECT } from "../../constants" import { DEFAULT_PAGES_OBJECT } from "../../constants"
@ -50,8 +49,6 @@ export const getStore = () => {
store.setPackage = setPackage(store, initial) store.setPackage = setPackage(store, initial)
store.createDatabaseForApp = backendStoreActions.createDatabaseForApp(store)
store.saveScreen = saveScreen(store) store.saveScreen = saveScreen(store)
store.setCurrentScreen = setCurrentScreen(store) store.setCurrentScreen = setCurrentScreen(store)
store.setCurrentPage = setCurrentPage(store) store.setCurrentPage = setCurrentPage(store)

View File

@ -19,19 +19,21 @@
import GroupByPopover from "./popovers/GroupBy.svelte" import GroupByPopover from "./popovers/GroupBy.svelte"
import FilterPopover from "./popovers/Filter.svelte" import FilterPopover from "./popovers/Filter.svelte"
export let view = {} export let view = {}
let data = [] let data = []
$: ({ name, groupBy, filters, field } = view) $: name = view.name
$: filters = view.filters
$: field = view.field
$: groupBy = view.groupBy
$: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy) $: !name.startsWith("all_") && filters && fetchViewData(name, field, groupBy)
async function fetchViewData(name, field, groupBy) { async function fetchViewData(name, field, groupBy) {
const params = new URLSearchParams(); const params = new URLSearchParams()
if (field) params.set("stats", true); if (field) params.set("stats", true)
if (groupBy) params.set("group", groupBy); if (groupBy) params.set("group", groupBy)
let QUERY_VIEW_URL = `/api/views/${name}?${params}` let QUERY_VIEW_URL = `/api/views/${name}?${params}`

View File

@ -1,33 +1,25 @@
<script> <script>
import { Input, Select } from "@budibase/bbui" import { Input, Select } from "@budibase/bbui"
export let value
export let meta export let meta
export let value = meta.type === "boolean" ? false : ""
const isSelect = meta => let isSelect =
meta.type === "string" && meta.type === "string" &&
meta.constraints && meta.constraints &&
meta.constraints.inclusion && meta.constraints.inclusion &&
meta.constraints.inclusion.length > 0 meta.constraints.inclusion.length > 0
let type = determineInputType(meta) let type = determineInputType(meta)
let options = determineOptions(meta)
value = value || type === "checkbox" ? false : ""
function determineInputType(meta) { function determineInputType(meta) {
if (meta.type === "datetime") return "date" if (meta.type === "datetime") return "date"
if (meta.type === "number") return "number" if (meta.type === "number") return "number"
if (meta.type === "boolean") return "checkbox" if (meta.type === "boolean") return "checkbox"
if (isSelect(meta)) return "select" if (isSelect) return "select"
return "text" return "text"
} }
function determineOptions(meta) {
return isSelect(meta) ? meta.constraints.inclusion : []
}
const handleInput = event => { const handleInput = event => {
if (event.target.type === "checkbox") { if (event.target.type === "checkbox") {
value = event.target.checked value = event.target.checked
@ -46,7 +38,7 @@
{#if type === 'select'} {#if type === 'select'}
<Select thin secondary data-cy="{meta.name}-select" bind:value> <Select thin secondary data-cy="{meta.name}-select" bind:value>
<option /> <option />
{#each options as opt} {#each meta.constraints.inclusion as opt}
<option value={opt}>{opt}</option> <option value={opt}>{opt}</option>
{/each} {/each}
</Select> </Select>

View File

@ -15,7 +15,7 @@
{ {
name: "Statistics", name: "Statistics",
key: "stats", key: "stats",
} },
] ]
export let view = {} export let view = {}

View File

@ -73,7 +73,7 @@
.input-group-row { .input-group-row {
display: grid; display: grid;
grid-template-columns: 50px 1fr 20px 1fr; grid-template-columns: 75px 1fr 20px 1fr;
gap: var(--spacing-s); gap: var(--spacing-s);
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
align-items: center; align-items: center;

View File

@ -47,10 +47,10 @@
selected={selectedView === viewName} selected={selectedView === viewName}
title={viewName} title={viewName}
icon="ri-eye-line" icon="ri-eye-line"
on:click={() => selectView({ on:click={() => (selectedView === viewName ? {} : selectView({
name: viewName, name: viewName,
...model.views[viewName], ...model.views[viewName],
})}> }))}>
<EditViewPopover <EditViewPopover
view={{ name: viewName, ...model.views[viewName] }} /> view={{ name: viewName, ...model.views[viewName] }} />
</ListItem> </ListItem>

View File

@ -46,10 +46,10 @@ const controller = {
// add views to model document // add views to model document
const model = await db.get(ctx.request.body.modelId) const model = await db.get(ctx.request.body.modelId)
if (!model.views) model.views = {} if (!model.views) model.views = {}
if (!view.meta.schema) { if (!view.meta.schema) {
view.meta.schema = model.schema view.meta.schema = model.schema
} }
model.views[viewToSave.name] = view.meta model.views[viewToSave.name] = view.meta
if (originalName) { if (originalName) {
delete model.views[originalName] delete model.views[originalName]

View File

@ -1,5 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`viewBuilder Calculate creates a view with the calculation statistics schema 1`] = `
Object {
"map": "function (doc) {
if (doc.modelId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" ) {
emit(doc[\\"_id\\"], doc[\\"myField\\"]);
}
}",
"meta": Object {
"calculation": "stats",
"field": "myField",
"filters": Array [],
"groupBy": undefined,
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"schema": Object {
"avg": Object {
"type": "number",
},
"count": Object {
"type": "number",
},
"group": Object {
"type": "string",
},
"max": Object {
"type": "number",
},
"min": Object {
"type": "number",
},
"sum": Object {
"type": "number",
},
"sumsqr": Object {
"type": "number",
},
},
},
"reduce": "_stats",
}
`;
exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = ` exports[`viewBuilder Filter creates a view with multiple filters and conjunctions 1`] = `
Object { Object {
"map": "function (doc) { "map": "function (doc) {
@ -8,6 +49,7 @@ Object {
} }
}", }",
"meta": Object { "meta": Object {
"calculation": undefined,
"field": undefined, "field": undefined,
"filters": Array [ "filters": Array [
Object { Object {
@ -24,16 +66,8 @@ Object {
], ],
"groupBy": undefined, "groupBy": undefined,
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"schema": Object { "schema": undefined,
"avg": "number",
"count": "number",
"max": "number",
"min": "number",
"sum": "number",
"sumsqr": "number",
},
}, },
"reduce": "_stats",
} }
`; `;
@ -45,18 +79,12 @@ Object {
} }
}", }",
"meta": Object { "meta": Object {
"calculation": undefined,
"field": "score", "field": "score",
"filters": Array [], "filters": Array [],
"groupBy": "age", "groupBy": "age",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"schema": Object { "schema": undefined,
"avg": "number",
"count": "number",
"max": "number",
"min": "number",
"sum": "number",
"sumsqr": "number",
},
}, },
"reduce": "_stats", "reduce": "_stats",
} }

View File

@ -1,10 +1,10 @@
const statsViewTemplate = require("../viewBuilder"); const viewTemplate = require("../viewBuilder");
describe("viewBuilder", () => { describe("viewBuilder", () => {
describe("Filter", () => { describe("Filter", () => {
it("creates a view with multiple filters and conjunctions", () => { it("creates a view with multiple filters and conjunctions", () => {
expect(statsViewTemplate({ expect(viewTemplate({
"name": "Test View", "name": "Test View",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [{ "filters": [{
@ -22,17 +22,20 @@ describe("viewBuilder", () => {
}) })
describe("Calculate", () => { describe("Calculate", () => {
expect(statsViewTemplate({ it("creates a view with the calculation statistics schema", () => {
expect(viewTemplate({
"name": "Calculate View", "name": "Calculate View",
"field": "myField", "field": "myField",
"calculation": "stats",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"filters": [] "filters": []
})).toMatchSnapshot() })).toMatchSnapshot()
})
}) })
describe("Group By", () => { describe("Group By", () => {
it("creates a view emitting the group by field", () => { it("creates a view emitting the group by field", () => {
expect(statsViewTemplate({ expect(viewTemplate({
"name": "Test Scores Grouped By Age", "name": "Test Scores Grouped By Age",
"modelId": "14f1c4e94d6a47b682ce89d35d4c78b0", "modelId": "14f1c4e94d6a47b682ce89d35d4c78b0",
"groupBy": "age", "groupBy": "age",

View File

@ -12,27 +12,27 @@ const TOKEN_MAP = {
const SCHEMA_MAP = { const SCHEMA_MAP = {
stats: { stats: {
group: { group: {
type: "string" type: "string",
}, },
sum: { sum: {
type: "number" type: "number",
}, },
min: { min: {
type: "number" type: "number",
}, },
max: { max: {
type: "number" type: "number",
}, },
count: { count: {
type: "number" type: "number",
}, },
sumsqr: { sumsqr: {
type: "number" type: "number",
}, },
avg: { avg: {
type: "number" type: "number",
} },
} },
} }
/** /**
@ -45,37 +45,46 @@ function parseFilterExpression(filters) {
const expression = [] const expression = []
for (let filter of filters) { for (let filter of filters) {
if (filter.conjunction) expression.push(TOKEN_MAP[filter.conjunction]); if (filter.conjunction) expression.push(TOKEN_MAP[filter.conjunction])
if (filter.condition === "CONTAINS") { if (filter.condition === "CONTAINS") {
expression.push( expression.push(
`doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${ `doc["${filter.key}"].${TOKEN_MAP[filter.condition]}("${filter.value}")`
filter.value )
}")`) } else {
return expression.push(
`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${filter.value}"`
)
} }
expression.push(`doc["${filter.key}"] ${TOKEN_MAP[filter.condition]} "${
filter.value
}"`)
} }
return expression.join(" ") return expression.join(" ")
} }
/**
* Returns a CouchDB compliant emit() expression that is used to emit the
* correct key/value pairs for custom views.
* @param {String?} field - field to use for calculations, if any
* @param {String?} groupBy - field to group calculation results on, if any
*/
function parseEmitExpression(field, groupBy) { function parseEmitExpression(field, groupBy) {
if (field) return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);` if (field) return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
return `emit(doc._id);` return `emit(doc._id);`
} }
function viewTemplate({ /**
field, * Return a fully parsed CouchDB compliant view definition
modelId, * that will be stored in the design document in the database.
groupBy, *
filters = [], * @param {Object} viewDefinition - the JSON definition for a custom view.
calculation * field: field that calculations will be performed on
}) { * modelId: modelId of the model this view was created from
const parsedFilters = parseFilterExpression(filters) * groupBy: field that calculations will be grouped by. Field must be present for this to be useful
* filters: Array of filter objects containing predicates that are parsed into a JS expression
* calculation: an optional calculation to be performed over the view data.
*/
function viewTemplate({ field, modelId, groupBy, filters = [], calculation }) {
const parsedFilters = parseFilterExpression(filters)
const filterExpression = parsedFilters ? `&& ${parsedFilters}` : "" const filterExpression = parsedFilters ? `&& ${parsedFilters}` : ""
const emitExpression = parseEmitExpression(field, groupBy) const emitExpression = parseEmitExpression(field, groupBy)
@ -89,14 +98,14 @@ function viewTemplate({
groupBy, groupBy,
filters, filters,
schema: SCHEMA_MAP[calculation], schema: SCHEMA_MAP[calculation],
calculation calculation,
}, },
map: `function (doc) { map: `function (doc) {
if (doc.modelId === "${modelId}" ${filterExpression}) { if (doc.modelId === "${modelId}" ${filterExpression}) {
${emitExpression} ${emitExpression}
} }
}`, }`,
...reduction ...reduction,
} }
} }

View File

@ -3,15 +3,13 @@
exports[`/views query returns data for the created view 1`] = ` exports[`/views query returns data for the created view 1`] = `
Array [ Array [
Object { Object {
"key": null, "avg": 2333.3333333333335,
"value": Object { "count": 3,
"avg": 2333.3333333333335, "group": null,
"count": 3, "max": 4000,
"max": 4000, "min": 1000,
"min": 1000, "sum": 7000,
"sum": 7000, "sumsqr": 21000000,
"sumsqr": 21000000,
},
}, },
] ]
`; `;
@ -19,26 +17,22 @@ Array [
exports[`/views query returns data for the created view using a group by 1`] = ` exports[`/views query returns data for the created view using a group by 1`] = `
Array [ Array [
Object { Object {
"key": "One", "avg": 1500,
"value": Object { "count": 2,
"avg": 1500, "group": "One",
"count": 2, "max": 2000,
"max": 2000, "min": 1000,
"min": 1000, "sum": 3000,
"sum": 3000, "sumsqr": 5000000,
"sumsqr": 5000000,
},
}, },
Object { Object {
"key": "Two", "avg": 4000,
"value": Object { "count": 1,
"avg": 4000, "group": "Two",
"count": 1, "max": 4000,
"max": 4000, "min": 4000,
"min": 4000, "sum": 4000,
"sum": 4000, "sumsqr": 16000000,
"sumsqr": 16000000,
},
}, },
] ]
`; `;

View File

@ -66,13 +66,14 @@ describe("/views", () => {
TestView: { TestView: {
field: "Price", field: "Price",
modelId: model._id, modelId: model._id,
filters: [],
schema: { schema: {
sum: "number", name: {
min: "number", type: "text",
max: "number", constraints: {
count: "number", type: "string"
sumsqr: "number", }
avg: "number" }
} }
} }
}); });