test: add unit tests for the UI (#129)
This commit is contained in:
parent
fa024321a1
commit
54edea5882
|
@ -1,3 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
make test-all
|
||||
cd console/atest-ui && npm run format
|
||||
|
|
|
@ -3,6 +3,5 @@
|
|||
describe('My First Test', () => {
|
||||
it('visits the app root url', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('h1', 'You did it!')
|
||||
})
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,7 +12,8 @@
|
|||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"test": "vitest --dom"
|
||||
},
|
||||
"dependencies": {
|
||||
"element-plus": "^2.3.7",
|
||||
|
@ -25,13 +26,14 @@
|
|||
"@tsconfig/node18": "^2.0.1",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/google-protobuf": "^3.15.6",
|
||||
"@types/jest": "^29.5.3",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/node": "^18.16.18",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"@vitejs/plugin-vue-jsx": "^3.0.1",
|
||||
"@vue/eslint-config-prettier": "^7.1.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/test-utils": "^2.3.2",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^12.14.0",
|
||||
"eslint": "^8.39.0",
|
||||
|
@ -39,13 +41,21 @@
|
|||
"eslint-plugin-vue": "^9.11.0",
|
||||
"grpc_tools_node_protoc_ts": "^5.3.3",
|
||||
"grpc-tools": "^1.12.4",
|
||||
"jest": "^29.6.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.8",
|
||||
"start-server-and-test": "^2.0.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "~5.0.4",
|
||||
"vite": "^4.3.9",
|
||||
"vitest": "^0.32.0",
|
||||
"vitest": "^0.32.4",
|
||||
"vue-tsc": "^1.6.5"
|
||||
},
|
||||
"jest": {
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": "ts-jest"
|
||||
},
|
||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import TestCase from './views/TestCase.vue'
|
||||
import TestSuite from './views/TestSuite.vue'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ElTree } from "element-plus"
|
||||
import ElTree from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { Edit } from '@element-plus/icons-vue'
|
||||
|
||||
|
@ -17,25 +17,25 @@ const testCaseName = ref('')
|
|||
const testSuite = ref('')
|
||||
const handleNodeClick = (data: Tree) => {
|
||||
if (data.children) {
|
||||
viewName.value = "testsuite"
|
||||
viewName.value = 'testsuite'
|
||||
testSuite.value = data.label
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: data.label,
|
||||
name: data.label
|
||||
})
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/ListTestCase', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(d => {
|
||||
.then((response) => response.json())
|
||||
.then((d) => {
|
||||
if (d.items && d.items.length > 0) {
|
||||
data.children = []
|
||||
d.items.forEach((item: any) => {
|
||||
data.children?.push({
|
||||
id: data.label + item.name,
|
||||
label: item.name,
|
||||
parent: data.label,
|
||||
parent: data.label
|
||||
} as Tree)
|
||||
})
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ const handleNodeClick = (data: Tree) => {
|
|||
} else {
|
||||
testCaseName.value = data.label
|
||||
testSuite.value = data.parent
|
||||
viewName.value = "testcase"
|
||||
viewName.value = 'testcase'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,26 +54,26 @@ const currentNodekey = ref('')
|
|||
function loadTestSuites() {
|
||||
const requestOptions = {
|
||||
method: 'POST'
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/GetSuites', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(d => {
|
||||
.then((response) => response.json())
|
||||
.then((d) => {
|
||||
data.value = [] as Tree[]
|
||||
if (!d.data) {
|
||||
return
|
||||
}
|
||||
Object.keys(d.data).map(k => {
|
||||
Object.keys(d.data).map((k) => {
|
||||
let suite = {
|
||||
id: k,
|
||||
label: k,
|
||||
children: [] as Tree[],
|
||||
children: [] as Tree[]
|
||||
} as Tree
|
||||
|
||||
d.data[k].data.forEach((item: any) => {
|
||||
suite.children?.push({
|
||||
id: k + item,
|
||||
label: item,
|
||||
parent: k,
|
||||
parent: k
|
||||
} as Tree)
|
||||
})
|
||||
data.value.push(suite)
|
||||
|
@ -89,10 +89,10 @@ function loadTestSuites() {
|
|||
treeRef.value!.setCheckedKeys([child], false)
|
||||
}
|
||||
|
||||
viewName.value = "testsuite"
|
||||
viewName.value = 'testsuite'
|
||||
testSuite.value = firstItem.label
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
loadTestSuites()
|
||||
|
||||
|
@ -100,8 +100,8 @@ const dialogVisible = ref(false)
|
|||
const suiteCreatingLoading = ref(false)
|
||||
const suiteFormRef = ref<FormInstance>()
|
||||
const testSuiteForm = reactive({
|
||||
name: "",
|
||||
api: "",
|
||||
name: '',
|
||||
api: ''
|
||||
})
|
||||
|
||||
function openTestSuiteCreateDialog() {
|
||||
|
@ -116,16 +116,16 @@ const submitForm = (formEl: FormInstance | undefined) => {
|
|||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: testSuiteForm.name,
|
||||
api: testSuiteForm.api,
|
||||
api: testSuiteForm.api
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
fetch('/server.Runner/CreateTestSuite', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then((response) => response.json())
|
||||
.then(() => {
|
||||
suiteCreatingLoading.value = false
|
||||
loadTestSuites()
|
||||
});
|
||||
})
|
||||
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
@ -149,20 +149,31 @@ const viewName = ref('testcase')
|
|||
<el-button type="primary" @click="openTestSuiteCreateDialog" :icon="Edit">New</el-button>
|
||||
<el-input v-model="filterText" placeholder="Filter keyword" />
|
||||
|
||||
<el-tree :data="data"
|
||||
<el-tree
|
||||
:data="data"
|
||||
highlight-current
|
||||
:check-on-click-node=true
|
||||
:expand-on-click-node=false
|
||||
:check-on-click-node="true"
|
||||
:expand-on-click-node="false"
|
||||
:current-node-key="currentNodekey"
|
||||
ref="treeRef"
|
||||
node-key="id"
|
||||
:filter-node-method="filterTestCases"
|
||||
@node-click="handleNodeClick" />
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</el-aside>
|
||||
|
||||
<el-main>
|
||||
<TestCase v-if="viewName === 'testcase'" :suite="testSuite" :name="testCaseName" @updated="loadTestSuites"/>
|
||||
<TestSuite v-else-if="viewName === 'testsuite'" :name="testSuite" @updated="loadTestSuites"/>
|
||||
<TestCase
|
||||
v-if="viewName === 'testcase'"
|
||||
:suite="testSuite"
|
||||
:name="testCaseName"
|
||||
@updated="loadTestSuites"
|
||||
/>
|
||||
<TestSuite
|
||||
v-else-if="viewName === 'testsuite'"
|
||||
:name="testSuite"
|
||||
@updated="loadTestSuites"
|
||||
/>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
|
@ -170,12 +181,7 @@ const viewName = ref('testcase')
|
|||
<el-dialog v-model="dialogVisible" title="Create Test Suite" width="30%" draggable>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-form
|
||||
ref="suiteFormRef"
|
||||
status-icon
|
||||
label-width="120px"
|
||||
class="demo-ruleForm"
|
||||
>
|
||||
<el-form ref="suiteFormRef" status-icon label-width="120px" class="demo-ruleForm">
|
||||
<el-form-item label="Name" prop="name">
|
||||
<el-input v-model="testSuiteForm.name" />
|
||||
</el-form-item>
|
||||
|
@ -183,7 +189,12 @@ const viewName = ref('testcase')
|
|||
<el-input v-model="testSuiteForm.api" placeholder="http://foo" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm(suiteFormRef)" :loading="suiteCreatingLoading">Submit</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(suiteFormRef)"
|
||||
:loading="suiteCreatingLoading"
|
||||
>Submit</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</span>
|
||||
|
|
|
@ -10,7 +10,7 @@ import 'element-plus/dist/index.css'
|
|||
const app = createApp(App)
|
||||
|
||||
app.use(ElementPlus, {
|
||||
locale: zhCn,
|
||||
locale: zhCn
|
||||
})
|
||||
// app.use(router)
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const props = defineProps({
|
|||
suite: String,
|
||||
address: String,
|
||||
method: String,
|
||||
operationId: String,
|
||||
operationId: String
|
||||
})
|
||||
// const emit = defineEmits(['update:address', 'update:method', 'update:operationId'])
|
||||
const querySuggestedAPIs = NewSuggestedAPIsQuery(props.suite!)
|
||||
|
@ -16,7 +16,6 @@ const handleSelect = (item: TestCase) => {
|
|||
// emit('update:method', item.request.method)
|
||||
// emit('update:operationId', item.name)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -26,7 +25,7 @@ const handleSelect = (item: TestCase) => {
|
|||
:fetch-suggestions="querySuggestedAPIs"
|
||||
@select="handleSelect"
|
||||
placeholder="API Address"
|
||||
style="width: 70%; margin-left: 5px; margin-right: 5px;"
|
||||
style="width: 70%; margin-left: 5px; margin-right: 5px"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="value">{{ item.request.method }}</div>
|
||||
|
|
|
@ -4,13 +4,13 @@ import type { TabsPaneContext } from 'element-plus'
|
|||
import { ElMessage } from 'element-plus'
|
||||
import { Edit, Delete } from '@element-plus/icons-vue'
|
||||
import JsonViewer from 'vue-json-viewer'
|
||||
import _ from 'lodash';
|
||||
import _ from 'lodash'
|
||||
import type { Pair, TestResult, TestCaseWithSuite } from './types'
|
||||
import { NewSuggestedAPIsQuery } from './types'
|
||||
import { NewSuggestedAPIsQuery, CreateFilter, GetHTTPMethods } from './types'
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
suite: String,
|
||||
suite: String
|
||||
})
|
||||
const emit = defineEmits(['updated'])
|
||||
|
||||
|
@ -26,16 +26,16 @@ function sendRequest() {
|
|||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
suite: suite,
|
||||
testcase: name,
|
||||
testcase: name
|
||||
})
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/RunTestCase', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
testResult.value = e
|
||||
requestLoading.value = false
|
||||
|
||||
if (e.error !== "") {
|
||||
if (e.error !== '') {
|
||||
ElMessage({
|
||||
message: e.error,
|
||||
type: 'error'
|
||||
|
@ -43,20 +43,18 @@ function sendRequest() {
|
|||
} else {
|
||||
ElMessage({
|
||||
message: 'Pass!',
|
||||
type: 'success',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
if (e.body !== '') {
|
||||
testResult.value.bodyObject = JSON.parse(e.body)
|
||||
|
||||
console.log(flattenObject(testResult.value.bodyObject))
|
||||
console.log(Object.getOwnPropertyNames(flattenObject(testResult.value.bodyObject)))
|
||||
}
|
||||
}).catch(e => {
|
||||
})
|
||||
.catch((e) => {
|
||||
requestLoading.value = false
|
||||
ElMessage.error('Oops, ' + e)
|
||||
testResult.value.bodyObject = JSON.parse(e.body)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const queryBodyFields = (queryString: string, cb: any) => {
|
||||
|
@ -71,40 +69,38 @@ const queryBodyFields = (queryString: string, cb: any) => {
|
|||
}
|
||||
|
||||
const pairs = [] as Pair[]
|
||||
keys.forEach(e => {
|
||||
keys.forEach((e) => {
|
||||
pairs.push({
|
||||
key: e,
|
||||
value: e,
|
||||
value: e
|
||||
} as Pair)
|
||||
});
|
||||
})
|
||||
|
||||
const results = queryString
|
||||
? pairs.filter(createFilter(queryString))
|
||||
: pairs
|
||||
const results = queryString ? pairs.filter(createFilter(queryString)) : pairs
|
||||
// call callback function to return suggestions
|
||||
cb(results)
|
||||
}
|
||||
|
||||
const emptyTestCaseWithSuite: TestCaseWithSuite = {
|
||||
suiteName: "",
|
||||
suiteName: '',
|
||||
data: {
|
||||
name: "",
|
||||
name: '',
|
||||
request: {
|
||||
api: "",
|
||||
method: "",
|
||||
api: '',
|
||||
method: '',
|
||||
header: [],
|
||||
query: [],
|
||||
form: [],
|
||||
body: "",
|
||||
body: ''
|
||||
},
|
||||
response: {
|
||||
statusCode: 0,
|
||||
body: "",
|
||||
body: '',
|
||||
header: [],
|
||||
bodyFieldsExpect: [],
|
||||
verify: [],
|
||||
schema: "",
|
||||
},
|
||||
schema: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -113,7 +109,7 @@ const testCaseWithSuite = ref(emptyTestCaseWithSuite)
|
|||
function load() {
|
||||
const name = props.name
|
||||
const suite = props.suite
|
||||
if (name === "" || suite === "") {
|
||||
if (name === '' || suite === '') {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -121,14 +117,14 @@ function load() {
|
|||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
suite: suite,
|
||||
testcase: name,
|
||||
testcase: name
|
||||
})
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/GetTestCase', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
if (e.request.method === "") {
|
||||
e.request.method = "GET"
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
if (e.request.method === '') {
|
||||
e.request.method = 'GET'
|
||||
}
|
||||
|
||||
e.request.header.push({
|
||||
|
@ -159,8 +155,8 @@ function load() {
|
|||
testCaseWithSuite.value = {
|
||||
suiteName: suite,
|
||||
data: e
|
||||
} as TestCaseWithSuite;
|
||||
});
|
||||
} as TestCaseWithSuite
|
||||
})
|
||||
}
|
||||
load()
|
||||
watch(props, () => {
|
||||
|
@ -172,23 +168,31 @@ function saveTestCase() {
|
|||
saveLoading.value = true
|
||||
|
||||
// remove empty pair
|
||||
testCaseWithSuite.value.data.request.header = testCaseWithSuite.value.data.request.header.filter(e => e.key !== '')
|
||||
testCaseWithSuite.value.data.request.query = testCaseWithSuite.value.data.request.query.filter(e => e.key !== '')
|
||||
testCaseWithSuite.value.data.request.form = testCaseWithSuite.value.data.request.form.filter(e => e.key !== '')
|
||||
testCaseWithSuite.value.data.response.header = testCaseWithSuite.value.data.response.header.filter(e => e.key !== '')
|
||||
testCaseWithSuite.value.data.response.bodyFieldsExpect = testCaseWithSuite.value.data.response.bodyFieldsExpect.filter(e => e.key !== '')
|
||||
testCaseWithSuite.value.data.response.verify = testCaseWithSuite.value.data.response.verify.filter(e => e !== '')
|
||||
testCaseWithSuite.value.data.request.header = testCaseWithSuite.value.data.request.header.filter(
|
||||
(e) => e.key !== ''
|
||||
)
|
||||
testCaseWithSuite.value.data.request.query = testCaseWithSuite.value.data.request.query.filter(
|
||||
(e) => e.key !== ''
|
||||
)
|
||||
testCaseWithSuite.value.data.request.form = testCaseWithSuite.value.data.request.form.filter(
|
||||
(e) => e.key !== ''
|
||||
)
|
||||
testCaseWithSuite.value.data.response.header =
|
||||
testCaseWithSuite.value.data.response.header.filter((e) => e.key !== '')
|
||||
testCaseWithSuite.value.data.response.bodyFieldsExpect =
|
||||
testCaseWithSuite.value.data.response.bodyFieldsExpect.filter((e) => e.key !== '')
|
||||
testCaseWithSuite.value.data.response.verify =
|
||||
testCaseWithSuite.value.data.response.verify.filter((e) => e !== '')
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(testCaseWithSuite.value)
|
||||
};
|
||||
fetch('/server.Runner/UpdateTestCase', requestOptions)
|
||||
.then(e => {
|
||||
}
|
||||
fetch('/server.Runner/UpdateTestCase', requestOptions).then((e) => {
|
||||
if (e.ok) {
|
||||
ElMessage({
|
||||
message: 'Saved.',
|
||||
type: 'success',
|
||||
type: 'success'
|
||||
})
|
||||
} else {
|
||||
ElMessage.error('Oops, ' + e.statusText)
|
||||
|
@ -204,17 +208,16 @@ function deleteTestCase() {
|
|||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
suite: suite,
|
||||
testcase: name,
|
||||
testcase: name
|
||||
})
|
||||
};
|
||||
fetch('/server.Runner/DeleteTestCase', requestOptions)
|
||||
.then(e => {
|
||||
}
|
||||
fetch('/server.Runner/DeleteTestCase', requestOptions).then((e) => {
|
||||
if (e.ok) {
|
||||
emit('updated', 'hello from child')
|
||||
|
||||
ElMessage({
|
||||
message: 'Delete.',
|
||||
type: 'success',
|
||||
type: 'success'
|
||||
})
|
||||
|
||||
// clean all the values
|
||||
|
@ -225,24 +228,7 @@ function deleteTestCase() {
|
|||
})
|
||||
}
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: 'GET',
|
||||
label: 'GET',
|
||||
},
|
||||
{
|
||||
value: 'POST',
|
||||
label: 'POST',
|
||||
},
|
||||
{
|
||||
value: 'DELETE',
|
||||
label: 'DELETE',
|
||||
},
|
||||
{
|
||||
value: 'PUT',
|
||||
label: 'PUT',
|
||||
},
|
||||
]
|
||||
const options = GetHTTPMethods()
|
||||
|
||||
const activeName = ref('second')
|
||||
|
||||
|
@ -286,82 +272,90 @@ const radio1 = ref('1')
|
|||
const pupularHeaders = ref([] as Pair[])
|
||||
const requestOptions = {
|
||||
method: 'POST'
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/PopularHeaders', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
pupularHeaders.value = e.data
|
||||
})
|
||||
const queryPupularHeaders = (queryString: string, cb: (arg: any) => void) => {
|
||||
const results = queryString
|
||||
? pupularHeaders.value.filter(createFilter(queryString))
|
||||
? pupularHeaders.value.filter(CreateFilter(queryString))
|
||||
: pupularHeaders.value
|
||||
|
||||
results.forEach(e => {
|
||||
results.forEach((e) => {
|
||||
e.value = e.key
|
||||
})
|
||||
cb(results)
|
||||
}
|
||||
const createFilter = (queryString: string) => {
|
||||
return (v: Pair) => {
|
||||
return (
|
||||
v.key.toLowerCase().indexOf(queryString.toLowerCase()) !== -1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function flattenObject(obj: any): any {
|
||||
function _flattenPairs(obj: any, prefix: string): [string, any][] {
|
||||
if (!_.isObject(obj)) {
|
||||
return [prefix, obj];
|
||||
return [prefix, obj]
|
||||
}
|
||||
|
||||
return _.toPairs(obj).reduce((final: [string, any][], nPair: [string, any]) => {
|
||||
const flattened = _flattenPairs(nPair[1], `${prefix}.${nPair[0]}`);
|
||||
const flattened = _flattenPairs(nPair[1], `${prefix}.${nPair[0]}`)
|
||||
if (flattened.length === 2 && !_.isObject(flattened[0]) && !_.isObject(flattened[1])) {
|
||||
return final.concat([flattened as [string, any]]);
|
||||
return final.concat([flattened as [string, any]])
|
||||
} else {
|
||||
return final.concat(flattened);
|
||||
return final.concat(flattened)
|
||||
}
|
||||
}, []);
|
||||
}, [])
|
||||
}
|
||||
|
||||
if (!_.isObject(obj)) {
|
||||
return JSON.stringify(obj);
|
||||
return JSON.stringify(obj)
|
||||
}
|
||||
|
||||
const pairs: [string, any][] = _.toPairs(obj).reduce((final: [string, any][], pair: [string, any]) => {
|
||||
const flattened = _flattenPairs(pair[1], pair[0]);
|
||||
const pairs: [string, any][] = _.toPairs(obj).reduce(
|
||||
(final: [string, any][], pair: [string, any]) => {
|
||||
const flattened = _flattenPairs(pair[1], pair[0])
|
||||
if (flattened.length === 2 && !_.isObject(flattened[0]) && !_.isObject(flattened[1])) {
|
||||
return final.concat([flattened as [string, any]]);
|
||||
return final.concat([flattened as [string, any]])
|
||||
} else {
|
||||
return final.concat(flattened);
|
||||
return final.concat(flattened)
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return pairs.reduce((acc: any, pair: [string, any]) => {
|
||||
acc[pair[0]] = pair[1];
|
||||
return acc;
|
||||
}, {});
|
||||
acc[pair[0]] = pair[1]
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="common-layout">
|
||||
<el-container style="height: 60vh">
|
||||
<el-header style="padding-left: 5px;">
|
||||
<div style="margin-bottom: 5px;">
|
||||
<el-button type="primary" @click="saveTestCase" :icon="Edit" :loading="saveLoading">Save</el-button>
|
||||
<el-header style="padding-left: 5px">
|
||||
<div style="margin-bottom: 5px">
|
||||
<el-button type="primary" @click="saveTestCase" :icon="Edit" :loading="saveLoading"
|
||||
>Save</el-button
|
||||
>
|
||||
<el-button type="primary" @click="deleteTestCase" :icon="Delete">Delete</el-button>
|
||||
</div>
|
||||
<el-select v-model="testCaseWithSuite.data.request.method" class="m-2" placeholder="Method" size="middle">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
<el-select
|
||||
v-model="testCaseWithSuite.data.request.method"
|
||||
class="m-2"
|
||||
placeholder="Method"
|
||||
size="middle"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-autocomplete
|
||||
v-model="testCaseWithSuite.data.request.api"
|
||||
:fetch-suggestions="querySuggestedAPIs"
|
||||
placeholder="API Address"
|
||||
style="width: 70%; margin-left: 5px; margin-right: 5px;"
|
||||
style="width: 70%; margin-left: 5px; margin-right: 5px"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="value">{{ item.request.method }}</div>
|
||||
|
@ -404,25 +398,45 @@ function flattenObject(obj: any): any {
|
|||
<el-radio :label="4">x-www-form-urlencoded</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<el-input v-model="testCaseWithSuite.data.request.body" :autosize="{ minRows: 4, maxRows: 8 }" type="textarea"
|
||||
placeholder="Please input" />
|
||||
<el-input
|
||||
v-model="testCaseWithSuite.data.request.body"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
type="textarea"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Expected" name="expected">
|
||||
<el-row :gutter="20">
|
||||
<span class="ml-3 w-50 text-gray-600 inline-flex items-center" style="margin-left: 15px; margin-right: 15px">Status Code:</span>
|
||||
<el-input v-model="testCaseWithSuite.data.response.statusCode" class="w-50 m-2"
|
||||
placeholder="Please input" style="width: 200px" />
|
||||
<span
|
||||
class="ml-3 w-50 text-gray-600 inline-flex items-center"
|
||||
style="margin-left: 15px; margin-right: 15px"
|
||||
>Status Code:</span
|
||||
>
|
||||
<el-input
|
||||
v-model="testCaseWithSuite.data.response.statusCode"
|
||||
class="w-50 m-2"
|
||||
placeholder="Please input"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-row>
|
||||
<el-input v-model="testCaseWithSuite.data.response.body" :autosize="{ minRows: 4, maxRows: 8 }" type="textarea"
|
||||
placeholder="Expected Body" />
|
||||
<el-input
|
||||
v-model="testCaseWithSuite.data.response.body"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
type="textarea"
|
||||
placeholder="Expected Body"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Expected Headers" name="expected-headers">
|
||||
<el-table :data="testCaseWithSuite.data.response.header" style="width: 100%">
|
||||
<el-table-column label="Key" width="180">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.key" placeholder="Key" @change="expectedHeaderChange" />
|
||||
<el-input
|
||||
v-model="scope.row.key"
|
||||
placeholder="Key"
|
||||
@change="expectedHeaderChange"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Value">
|
||||
|
@ -465,8 +479,11 @@ function flattenObject(obj: any): any {
|
|||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="Schema" name="schema">
|
||||
<el-input v-model="testCaseWithSuite.data.response.schema"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }" type="textarea" />
|
||||
<el-input
|
||||
v-model="testCaseWithSuite.data.response.schema"
|
||||
:autosize="{ minRows: 4, maxRows: 8 }"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-main>
|
||||
|
@ -477,35 +494,28 @@ function flattenObject(obj: any): any {
|
|||
<el-input
|
||||
v-model="testResult.output"
|
||||
:autosize="{ minRows: 4, maxRows: 6 }"
|
||||
readonly=true
|
||||
readonly="true"
|
||||
type="textarea"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Body" name="body">
|
||||
<JsonViewer :value="testResult.bodyObject"
|
||||
:expand-depth=5
|
||||
copyable
|
||||
boxed
|
||||
sort
|
||||
/>
|
||||
<JsonViewer :value="testResult.bodyObject" :expand-depth="5" copyable boxed sort />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane name="response-header">
|
||||
<template #label>
|
||||
<el-badge :value="testResult.header.length" class="item">
|
||||
Header
|
||||
</el-badge>
|
||||
<el-badge :value="testResult.header.length" class="item"> Header </el-badge>
|
||||
</template>
|
||||
<el-table :data="testResult.header" style="width: 100%">
|
||||
<el-table-column label="Key" width="200">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.key" placeholder="Key" readonly=true />
|
||||
<el-input v-model="scope.row.key" placeholder="Key" readonly="true" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Value">
|
||||
<template #default="scope">
|
||||
<div style="display: flex; align-items: center">
|
||||
<el-input v-model="scope.row.value" placeholder="Value" readonly=true />
|
||||
<el-input v-model="scope.row.value" placeholder="Value" readonly="true" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
|
@ -7,33 +7,34 @@ import type { Suite, TestCase } from './types'
|
|||
import { NewSuggestedAPIsQuery } from './types'
|
||||
|
||||
const props = defineProps({
|
||||
name: String,
|
||||
name: String
|
||||
})
|
||||
const emit = defineEmits(['updated'])
|
||||
let querySuggestedAPIs = NewSuggestedAPIsQuery(props.name!)
|
||||
|
||||
const suite = ref({
|
||||
name: "",
|
||||
api: "",
|
||||
name: '',
|
||||
api: '',
|
||||
spec: {
|
||||
kind: "",
|
||||
url: "",
|
||||
kind: '',
|
||||
url: ''
|
||||
}
|
||||
} as Suite)
|
||||
function load() {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: props.name,
|
||||
name: props.name
|
||||
})
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/GetTestSuite', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
suite.value = e
|
||||
}).catch(e => {
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
});
|
||||
})
|
||||
}
|
||||
load()
|
||||
watch(props, () => {
|
||||
|
@ -43,32 +44,31 @@ watch(props, () => {
|
|||
function save() {
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(suite.value),
|
||||
};
|
||||
body: JSON.stringify(suite.value)
|
||||
}
|
||||
fetch('/server.Runner/UpdateTestSuite', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
ElMessage({
|
||||
message: 'Updated.',
|
||||
type: 'success',
|
||||
type: 'success'
|
||||
})
|
||||
}).catch(e => {
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const testcaseFormRef = ref<FormInstance>()
|
||||
const testCaseForm = reactive({
|
||||
suiteName: "",
|
||||
name: "",
|
||||
api: "",
|
||||
method: "GET",
|
||||
suiteName: '',
|
||||
name: '',
|
||||
api: '',
|
||||
method: 'GET'
|
||||
})
|
||||
const rules = reactive<FormRules<Suite>>({
|
||||
name: [
|
||||
{ required: true, message: 'Please input TestCase name', trigger: 'blur' },
|
||||
]
|
||||
name: [{ required: true, message: 'Please input TestCase name', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
function openNewTestCaseDialog() {
|
||||
|
@ -90,18 +90,18 @@ const submitForm = async (formEl: FormInstance | undefined) => {
|
|||
name: testCaseForm.name,
|
||||
request: {
|
||||
api: testCaseForm.api,
|
||||
method: "GET",
|
||||
method: 'GET'
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
fetch('/server.Runner/CreateTestCase', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then((response) => response.json())
|
||||
.then(() => {
|
||||
suiteCreatingLoading.value = false
|
||||
emit('updated', 'hello from child')
|
||||
});
|
||||
})
|
||||
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
@ -112,20 +112,21 @@ function del() {
|
|||
const requestOptions = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: props.name,
|
||||
name: props.name
|
||||
})
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/DeleteTestSuite', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
ElMessage({
|
||||
message: 'Deleted.',
|
||||
type: 'success',
|
||||
type: 'success'
|
||||
})
|
||||
emit('updated')
|
||||
}).catch(e => {
|
||||
})
|
||||
.catch((e) => {
|
||||
ElMessage.error('Oops, ' + e)
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
const suiteCreatingLoading = ref(false)
|
||||
|
@ -133,19 +134,19 @@ const suiteCreatingLoading = ref(false)
|
|||
const apiSpecKinds = [
|
||||
{
|
||||
value: 'swagger',
|
||||
label: 'Swagger',
|
||||
label: 'Swagger'
|
||||
},
|
||||
{
|
||||
value: 'openapi',
|
||||
label: 'OpenAPI',
|
||||
label: 'OpenAPI'
|
||||
}
|
||||
]
|
||||
|
||||
const handleAPISelect = (item: TestCase) => {
|
||||
if (testCaseForm.method === "") {
|
||||
if (testCaseForm.method === '') {
|
||||
testCaseForm.method = item.request.method
|
||||
}
|
||||
if (testCaseForm.name === "") {
|
||||
if (testCaseForm.name === '') {
|
||||
testCaseForm.name = item.name
|
||||
}
|
||||
}
|
||||
|
@ -156,7 +157,12 @@ const handleAPISelect = (item: TestCase) => {
|
|||
<el-text class="mx-1" type="primary">{{ suite.name }}</el-text>
|
||||
<el-input class="mx-1" v-model="suite.api" placeholder="API"></el-input>
|
||||
<el-select v-model="suite.spec.kind" class="m-2" placeholder="API Spec Kind" size="middle">
|
||||
<el-option v-for="item in apiSpecKinds" :key="item.value" :label="item.label" :value="item.value" />
|
||||
<el-option
|
||||
v-for="item in apiSpecKinds"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input class="mx-1" v-model="suite.spec.url" placeholder="API Spec URL"></el-input>
|
||||
|
||||
|
@ -166,7 +172,7 @@ const handleAPISelect = (item: TestCase) => {
|
|||
<el-button type="primary" @click="openNewTestCaseDialog" :icon="Edit">New TestCase</el-button>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="Create Test Case" width="30%" draggable>
|
||||
<el-dialog v-model="dialogVisible" title="Create Test Case" width="40%" draggable>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-form
|
||||
|
@ -174,7 +180,7 @@ const handleAPISelect = (item: TestCase) => {
|
|||
:model="testCaseForm"
|
||||
ref="testcaseFormRef"
|
||||
status-icon
|
||||
label-width="120px"
|
||||
label-width="60px"
|
||||
class="demo-ruleForm"
|
||||
>
|
||||
<el-form-item label="Name" prop="name">
|
||||
|
@ -189,7 +195,7 @@ const handleAPISelect = (item: TestCase) => {
|
|||
:fetch-suggestions="querySuggestedAPIs"
|
||||
@select="handleAPISelect"
|
||||
placeholder="API Address"
|
||||
style="width: 70%; margin-left: 5px; margin-right: 5px;"
|
||||
style="width: 100%; margin-left: 5px; margin-right: 5px"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div class="value">{{ item.request.method }}</div>
|
||||
|
@ -198,7 +204,12 @@ const handleAPISelect = (item: TestCase) => {
|
|||
</el-autocomplete>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm(testcaseFormRef)" :loading="suiteCreatingLoading">Submit</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitForm(testcaseFormRef)"
|
||||
:loading="suiteCreatingLoading"
|
||||
>Submit</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</span>
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import { describe } from 'node:test'
|
||||
import { NewSuggestedAPIsQuery, CreateFilter, GetHTTPMethods } from '../types'
|
||||
import type { Pair } from '../types'
|
||||
|
||||
describe('NewSuggestedAPIsQuery', () => {
|
||||
test('function is not null', () => {
|
||||
const func = NewSuggestedAPIsQuery('suite')
|
||||
expect(func).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CreateFilter', () => {
|
||||
const filter = CreateFilter('suite')
|
||||
|
||||
test('ignore letter case', () => {
|
||||
expect(filter({ value: 'Suite' } as Pair)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('not match', () => {
|
||||
expect(filter({ value: 'not match' } as Pair)).not.toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('GetHTTPMethods', () => {
|
||||
test('HTTP methods', () => {
|
||||
const options = GetHTTPMethods()
|
||||
expect(options).toHaveLength(4)
|
||||
})
|
||||
})
|
|
@ -1,55 +1,55 @@
|
|||
import { ref } from 'vue'
|
||||
|
||||
export interface Suite {
|
||||
name: string;
|
||||
api: string;
|
||||
name: string
|
||||
api: string
|
||||
spec: {
|
||||
kind: string;
|
||||
url: string;
|
||||
kind: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface TestResult {
|
||||
body: string,
|
||||
bodyObject: {},
|
||||
output: string,
|
||||
error: string,
|
||||
statusCode: number,
|
||||
header: Pair[],
|
||||
body: string
|
||||
bodyObject: {}
|
||||
output: string
|
||||
error: string
|
||||
statusCode: number
|
||||
header: Pair[]
|
||||
}
|
||||
|
||||
export interface Pair {
|
||||
key: string,
|
||||
key: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface TestCaseWithSuite {
|
||||
suiteName: string,
|
||||
suiteName: string
|
||||
data: TestCase
|
||||
}
|
||||
|
||||
export interface TestCase {
|
||||
name: string,
|
||||
request: TestCaseRequest,
|
||||
response: TestCaseResponse,
|
||||
name: string
|
||||
request: TestCaseRequest
|
||||
response: TestCaseResponse
|
||||
}
|
||||
|
||||
export interface TestCaseRequest {
|
||||
api: string,
|
||||
method: string,
|
||||
header: Pair[],
|
||||
query: Pair[],
|
||||
form: Pair[],
|
||||
body: string,
|
||||
api: string
|
||||
method: string
|
||||
header: Pair[]
|
||||
query: Pair[]
|
||||
form: Pair[]
|
||||
body: string
|
||||
}
|
||||
|
||||
export interface TestCaseResponse {
|
||||
statusCode: number,
|
||||
body: string,
|
||||
header: Pair[],
|
||||
bodyFieldsExpect: Pair[],
|
||||
verify: string[],
|
||||
schema: string,
|
||||
statusCode: number
|
||||
body: string
|
||||
header: Pair[]
|
||||
bodyFieldsExpect: Pair[]
|
||||
verify: string[]
|
||||
schema: string
|
||||
}
|
||||
|
||||
// Suggested APIs query
|
||||
|
@ -58,18 +58,16 @@ export function NewSuggestedAPIsQuery(suite: string) {
|
|||
return function (queryString: string, cb: (arg: any) => void) {
|
||||
loadCache(suite, function () {
|
||||
const results = queryString
|
||||
? localCache.value.filter(createFilter(queryString))
|
||||
? localCache.value.filter(CreateFilter(queryString))
|
||||
: localCache.value
|
||||
|
||||
cb(results.slice(0, 10))
|
||||
})
|
||||
}
|
||||
}
|
||||
const createFilter = (queryString: string) => {
|
||||
return (v: TestCaseWithValue) => {
|
||||
return (
|
||||
v.request.api.toLowerCase().indexOf(queryString.toLowerCase()) !== -1
|
||||
)
|
||||
export function CreateFilter(queryString: string) {
|
||||
return (v: Pair) => {
|
||||
return v.value.toLowerCase().indexOf(queryString.toLowerCase()) !== -1
|
||||
}
|
||||
}
|
||||
function loadCache(suite: string, callback: Function) {
|
||||
|
@ -78,7 +76,7 @@ function loadCache(suite: string, callback : Function) {
|
|||
return
|
||||
}
|
||||
|
||||
if (suite === "") {
|
||||
if (suite === '') {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -87,10 +85,10 @@ function loadCache(suite: string, callback : Function) {
|
|||
body: JSON.stringify({
|
||||
name: suite
|
||||
})
|
||||
};
|
||||
}
|
||||
fetch('/server.Runner/GetSuggestedAPIs', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(e => {
|
||||
.then((response) => response.json())
|
||||
.then((e) => {
|
||||
localCache.value = e.data
|
||||
localCache.value.forEach((v: TestCaseWithValue) => {
|
||||
v.value = v.request.api
|
||||
|
@ -99,6 +97,25 @@ function loadCache(suite: string, callback : Function) {
|
|||
})
|
||||
}
|
||||
|
||||
interface TestCaseWithValue extends TestCase {
|
||||
value: string
|
||||
interface TestCaseWithValue extends TestCase, Pair {}
|
||||
|
||||
export function GetHTTPMethods() {
|
||||
return [
|
||||
{
|
||||
value: 'GET',
|
||||
key: 'GET'
|
||||
},
|
||||
{
|
||||
key: 'POST',
|
||||
label: 'POST'
|
||||
},
|
||||
{
|
||||
value: 'DELETE',
|
||||
key: 'DELETE'
|
||||
},
|
||||
{
|
||||
value: 'PUT',
|
||||
key: 'PUT'
|
||||
}
|
||||
] as Pair[]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ export default mergeConfig(
|
|||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals:true,
|
||||
// environment: 'jsdom',
|
||||
environment: 'happy-dom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/*'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
transformMode: {
|
||||
|
|
Loading…
Reference in New Issue