test: add unit tests for the UI (#129)

This commit is contained in:
Rick 2023-07-16 23:02:03 +08:00 committed by GitHub
parent fa024321a1
commit 54edea5882
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 7924 additions and 1030 deletions

1
.github/pre-commit vendored
View File

@ -1,3 +1,4 @@
#!/bin/sh
make test-all
cd console/atest-ui && npm run format

View File

@ -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

View File

@ -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?)$"
}
}

View File

@ -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>

View File

@ -10,7 +10,7 @@ import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus, {
locale: zhCn,
locale: zhCn
})
// app.use(router)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)
})
})

View File

@ -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[]
}

View File

@ -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: {