mirror of
https://github.com/YMFE/yapi.git
synced 2025-03-07 14:16:52 +08:00
feat: add case report module
This commit is contained in:
parent
608cb970af
commit
f8a1fcfce4
@ -20,23 +20,8 @@ function json_parse(data) {
|
||||
}
|
||||
}
|
||||
|
||||
function isValidJson(json) {
|
||||
if (!json) return false;
|
||||
if (typeof json === 'object') return true;
|
||||
try {
|
||||
if (typeof json === 'string') {
|
||||
json5.parse(json);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isJsonData(headers, res) {
|
||||
if (isValidJson(res)) {
|
||||
return true;
|
||||
}
|
||||
function isJsonData(headers) {
|
||||
if (!headers || typeof headers !== 'object') return false;
|
||||
let isResJson = false;
|
||||
Object.keys(headers).map(key => {
|
||||
@ -524,10 +509,10 @@ export default class Run extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { method, domains, pathParam, pathname, query, headers, bodyForm, caseEnv, bodyType, resHeader, loading, validRes, res } = this.state;
|
||||
const { method, domains, pathParam, pathname, query, headers, bodyForm, caseEnv, bodyType, resHeader, loading, validRes } = this.state;
|
||||
HTTP_METHOD[method] = HTTP_METHOD[method] || {}
|
||||
const hasPlugin = this.state.hasPlugin;
|
||||
let isResJson = isJsonData(resHeader, res);
|
||||
let isResJson = isJsonData(resHeader);
|
||||
let path = pathname;
|
||||
pathParam.forEach(item => {
|
||||
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
|
||||
|
@ -0,0 +1,94 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Row, Col, Tabs } from 'antd'
|
||||
const TabPane = Tabs.TabPane;
|
||||
function json_format(json) {
|
||||
return JSON.stringify(json, null, ' ')
|
||||
}
|
||||
|
||||
const CaseReport = function (props) {
|
||||
let body = json_format(props.body);
|
||||
let headers = json_format(props.headers, null, ' ');
|
||||
let res_header = json_format(props.res_header, null, ' ');
|
||||
let res_body = json_format(props.res_body);
|
||||
|
||||
let validRes = props.validRes.map((item, index) => {
|
||||
return <div key={index}>{item.message}</div>
|
||||
})
|
||||
|
||||
return <div className="report">
|
||||
<Tabs defaultActiveKey="request" >
|
||||
<TabPane className="case-report-pane" tab="Request" key="request">
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">Url</Col>
|
||||
<Col span="18">{props.url}</Col>
|
||||
</Row>
|
||||
{props.query ?
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">Query</Col>
|
||||
<Col span="18">{props.query}</Col>
|
||||
</Row>
|
||||
: null
|
||||
}
|
||||
|
||||
{props.headers ?
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">Headers</Col>
|
||||
<Col span="18"><pre>{headers}</pre></Col>
|
||||
</Row>
|
||||
: null
|
||||
}
|
||||
|
||||
{props.body ?
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">Body</Col>
|
||||
<Col span="18"><pre>{body}</pre></Col>
|
||||
</Row>
|
||||
: null
|
||||
}
|
||||
</TabPane>
|
||||
<TabPane className="case-report-pane" tab="Response" key="response">
|
||||
{props.res_header ?
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">Headers</Col>
|
||||
<Col span="18"><pre>{res_header}</pre></Col>
|
||||
</Row>
|
||||
: null
|
||||
}
|
||||
{props.res_body ?
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">Body</Col>
|
||||
<Col span="18"><pre>{res_body}</pre></Col>
|
||||
</Row>
|
||||
: null
|
||||
}
|
||||
|
||||
</TabPane>
|
||||
<TabPane className="case-report-pane" tab="验证结果" key="valid">
|
||||
{props.validRes ?
|
||||
<Row className="case-report">
|
||||
<Col className="case-report-title" span="6">验证结果</Col>
|
||||
<Col span="18">{validRes}</Col>
|
||||
</Row>
|
||||
: null
|
||||
}
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
CaseReport.propTypes = {
|
||||
url: PropTypes.string,
|
||||
body: PropTypes.any,
|
||||
headers: PropTypes.object,
|
||||
res_header: PropTypes.object,
|
||||
res_body: PropTypes.any,
|
||||
query: PropTypes.string,
|
||||
validRes: PropTypes.array
|
||||
}
|
||||
|
||||
|
||||
export default CaseReport;
|
@ -3,17 +3,29 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types'
|
||||
import { withRouter } from 'react-router'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Tooltip, Icon, Button, Spin, Modal, message } from 'antd'
|
||||
import { fetchInterfaceColList, fetchCaseList, setColData } from '../../../../reducer/modules/interfaceCol'
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
|
||||
import { handleMockWord } from '../../../../common.js'
|
||||
// import { formatTime } from '../../../../common.js'
|
||||
import * as Table from 'reactabular-table';
|
||||
import * as dnd from 'reactabular-dnd';
|
||||
import * as resolve from 'table-resolver';
|
||||
import axios from 'axios'
|
||||
import URL from 'url';
|
||||
import Mock from 'mockjs'
|
||||
import json5 from 'json5'
|
||||
import CaseReport from './CaseReport.js'
|
||||
const MockExtra = require('common/mock-extra.js')
|
||||
|
||||
function json_parse(data) {
|
||||
try {
|
||||
return json5.parse(data)
|
||||
} catch (e) {
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
@connect(
|
||||
state => {
|
||||
@ -22,7 +34,8 @@ import axios from 'axios'
|
||||
currColId: state.interfaceCol.currColId,
|
||||
currCaseId: state.interfaceCol.currCaseId,
|
||||
isShowCol: state.interfaceCol.isShowCol,
|
||||
currCaseList: state.interfaceCol.currCaseList
|
||||
currCaseList: state.interfaceCol.currCaseList,
|
||||
currProject: state.project.currProject
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -45,14 +58,18 @@ class InterfaceColContent extends Component {
|
||||
currCaseList: PropTypes.array,
|
||||
currColId: PropTypes.number,
|
||||
currCaseId: PropTypes.number,
|
||||
isShowCol: PropTypes.bool
|
||||
isShowCol: PropTypes.bool,
|
||||
currProject: PropTypes.object
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.reports = {};
|
||||
this.state = {
|
||||
rows: []
|
||||
rows: [],
|
||||
reports: {},
|
||||
visible: false,
|
||||
curCaseid: null
|
||||
};
|
||||
this.onRow = this.onRow.bind(this);
|
||||
this.onMoveRow = this.onMoveRow.bind(this);
|
||||
@ -76,9 +93,9 @@ class InterfaceColContent extends Component {
|
||||
}
|
||||
|
||||
handleColdata = (rows) => {
|
||||
console.log(rows);
|
||||
rows = rows.map((item) => {
|
||||
item.id = item._id;
|
||||
item._test_status = item.test_status;
|
||||
return item;
|
||||
})
|
||||
rows = rows.sort((n, o) => {
|
||||
@ -89,7 +106,134 @@ class InterfaceColContent extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
executeTests = async () => {
|
||||
for (let i = 0, l = this.state.rows.length, newRows, curitem; i < l; i++) {
|
||||
let { rows } = this.state;
|
||||
curitem = Object.assign({}, rows[i], { test_status: 'loading' });
|
||||
newRows = [].concat([], rows);
|
||||
newRows[i] = curitem;
|
||||
this.setState({
|
||||
rows: newRows
|
||||
})
|
||||
let status = 'error';
|
||||
try {
|
||||
let result = await this.handleTest(curitem);
|
||||
if (result.code === 400) {
|
||||
status = 'error';
|
||||
} else if (result.code === 0) {
|
||||
status = 'ok';
|
||||
} else if (result.code === 1) {
|
||||
status = 'invalid'
|
||||
}
|
||||
this.reports[curitem._id] = result;
|
||||
} catch (e) {
|
||||
status = 'error';
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
curitem = Object.assign({}, rows[i], { test_status: status });
|
||||
newRows = [].concat([], rows);
|
||||
newRows[i] = curitem;
|
||||
this.setState({
|
||||
rows: newRows
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleTest = (interfaceData) => {
|
||||
const { currProject } = this.props;
|
||||
const { case_env } = interfaceData;
|
||||
let path = URL.resolve(currProject.basepath, interfaceData.path);
|
||||
interfaceData.req_params = interfaceData.req_params || [];
|
||||
interfaceData.req_params.forEach(item => {
|
||||
path = path.replace(`:${item.name}`, item.value || `:${item.name}`);
|
||||
});
|
||||
const domains = currProject.env.concat();
|
||||
const urlObj = URL.parse(domains.find(item => item.name === case_env).domain);
|
||||
const href = URL.format({
|
||||
protocol: urlObj.protocol || 'http',
|
||||
host: urlObj.host,
|
||||
pathname: urlObj.pathname ? URL.resolve(urlObj.pathname, path) : path,
|
||||
query: this.getQueryObj(interfaceData.req_query)
|
||||
});
|
||||
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let result = { code: 400, msg: '数据异常', validRes: [] };
|
||||
let that = this;
|
||||
window.crossRequest({
|
||||
url: href,
|
||||
method: interfaceData.method,
|
||||
headers: that.getHeadersObj(interfaceData.req_headers),
|
||||
data: interfaceData.req_body_type === 'form' ? that.arrToObj(interfaceData.req_body_form) : interfaceData.req_body_other,
|
||||
success: (res, header) => {
|
||||
res = json_parse(res);
|
||||
result.url = href;
|
||||
result.method = interfaceData.method;
|
||||
result.headers = that.getHeadersObj(interfaceData.req_headers);
|
||||
result.body = interfaceData.req_body_type === 'form' ? (that.arrToObj(interfaceData.req_body_form), null, ' ') : interfaceData.req_body_other
|
||||
result.res_header = header;
|
||||
result.res_body = res;
|
||||
if (res && typeof res === 'object') {
|
||||
let tpl = MockExtra(json_parse(interfaceData.res_body), {
|
||||
query: interfaceData.req_query,
|
||||
body: interfaceData.req_body_form
|
||||
})
|
||||
let validRes = Mock.valid(tpl, res);
|
||||
|
||||
if (validRes.length === 0) {
|
||||
result.code = 0;
|
||||
result.validRes = [{message: '验证通过'}];
|
||||
resolve(result);
|
||||
} else if (validRes.length > 0) {
|
||||
result.code = 1;
|
||||
result.validRes = validRes;
|
||||
resolve(result)
|
||||
}
|
||||
} else {
|
||||
reject(result)
|
||||
}
|
||||
},
|
||||
error: (res) => {
|
||||
result.code = 400;
|
||||
result.msg = '请求异常'
|
||||
reject(res)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
arrToObj(arr) {
|
||||
arr = arr || [];
|
||||
const obj = {};
|
||||
arr.forEach(item => {
|
||||
if (item.name && item.type !== 'file') {
|
||||
obj[item.name] = handleMockWord(item.value);
|
||||
}
|
||||
})
|
||||
return obj;
|
||||
}
|
||||
|
||||
getQueryObj(query) {
|
||||
query = query || [];
|
||||
const queryObj = {};
|
||||
query.forEach(item => {
|
||||
if (item.name) {
|
||||
queryObj[item.name] = handleMockWord(item.value);
|
||||
}
|
||||
})
|
||||
return queryObj;
|
||||
}
|
||||
getHeadersObj(headers) {
|
||||
headers = headers || [];
|
||||
const headersObj = {};
|
||||
headers.forEach(item => {
|
||||
if (item.name && item.value) {
|
||||
headersObj[item.name] = item.value;
|
||||
}
|
||||
})
|
||||
return headersObj;
|
||||
}
|
||||
|
||||
onRow(row) {
|
||||
return {
|
||||
@ -132,6 +276,23 @@ class InterfaceColContent extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
openReport = (id) => {
|
||||
if (!this.reports[id]) {
|
||||
return message.warn('还没有生成报告')
|
||||
}
|
||||
this.setState({
|
||||
visible: true,
|
||||
curCaseid: id
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
handleCancel = () => {
|
||||
this.setState({
|
||||
visible: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const columns = [{
|
||||
property: 'casename',
|
||||
@ -160,6 +321,27 @@ class InterfaceColContent extends Component {
|
||||
return <span>{rowData._id}</span>
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
property: 'test_status',
|
||||
header: {
|
||||
label: '状态'
|
||||
},
|
||||
cell: {
|
||||
formatters: [(value, { rowData }) => {
|
||||
switch (rowData.test_status) {
|
||||
case 'ok':
|
||||
return <div ><Icon style={{ color: '#00a854' }} type="check-circle" /></div>
|
||||
case 'error':
|
||||
return <div ><Tooltip title="请求异常"><Icon type="info-circle" style={{ color: '#f04134' }} /></Tooltip></div>
|
||||
case 'invalid':
|
||||
return <div ><Tooltip title="返回数据校验未通过"><Icon type="exclamation-circle" style={{ color: '#ffbf00' }} /></Tooltip></div>
|
||||
case 'loading':
|
||||
return <div ><Spin /></div>
|
||||
default:
|
||||
return <div ><Icon style={{ color: '#00a854' }} type="check-circle" /></div>
|
||||
}
|
||||
}]
|
||||
}
|
||||
}, {
|
||||
property: 'path',
|
||||
header: {
|
||||
@ -177,6 +359,16 @@ class InterfaceColContent extends Component {
|
||||
}
|
||||
]
|
||||
}
|
||||
}, {
|
||||
header: {
|
||||
label: '测试报告'
|
||||
|
||||
},
|
||||
cell: {
|
||||
formatters: [(text, { rowData }) => {
|
||||
return <Button onClick={() => this.openReport(rowData.id)}>报告</Button>
|
||||
}]
|
||||
}
|
||||
}
|
||||
];
|
||||
const { rows } = this.state;
|
||||
@ -196,26 +388,35 @@ class InterfaceColContent extends Component {
|
||||
})(rows);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ padding: "16px" }}>
|
||||
<h2 style={{ marginBottom: '10px' }}>测试集合</h2>
|
||||
<Table.Provider
|
||||
components={components}
|
||||
columns={resolvedColumns}
|
||||
style={{ width: '100%', lineHeight: '30px' }}
|
||||
>
|
||||
<Table.Header
|
||||
style={{ textAlign: 'left' }}
|
||||
headerRows={resolve.headerRows({ columns })}
|
||||
/>
|
||||
<div className="interface-col">
|
||||
<h2 style={{ marginBottom: '10px', display: 'inline-block' }}>测试集合</h2>
|
||||
<Button type="primary" style={{ float: 'right' }} onClick={this.executeTests}>开始测试</Button>
|
||||
<Table.Provider
|
||||
components={components}
|
||||
columns={resolvedColumns}
|
||||
style={{ width: '100%', lineHeight: '36px' }}
|
||||
>
|
||||
<Table.Header
|
||||
style={{ textAlign: 'left' }}
|
||||
headerRows={resolve.headerRows({ columns })}
|
||||
/>
|
||||
|
||||
<Table.Body
|
||||
rows={resolvedRows}
|
||||
rowKey="id"
|
||||
onRow={this.onRow}
|
||||
/>
|
||||
</Table.Provider>
|
||||
</div>
|
||||
<Table.Body
|
||||
rows={resolvedRows}
|
||||
rowKey="id"
|
||||
onRow={this.onRow}
|
||||
/>
|
||||
</Table.Provider>
|
||||
<Modal
|
||||
title="测试报告"
|
||||
width="660"
|
||||
style={{ minHeight: '500px' }}
|
||||
visible={this.state.visible}
|
||||
onCancel={this.handleCancel}
|
||||
footer={null}
|
||||
>
|
||||
<CaseReport {...this.reports[this.state.curCaseid]} />
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -77,3 +77,23 @@
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
cursor: move;
|
||||
}
|
||||
.report{
|
||||
min-height: 400px;
|
||||
.case-report-pane{
|
||||
margin-top: 10px;
|
||||
}
|
||||
.case-report{
|
||||
margin: 10px;
|
||||
|
||||
.case-report-title{
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.interface-col{
|
||||
padding: 16px;
|
||||
}
|
@ -10,7 +10,7 @@ class advMockModel extends baseModel {
|
||||
return {
|
||||
interface_id: { type: Number, required: true },
|
||||
project_id: {type: Number, required: true},
|
||||
enable: {type: Boolean, default: false}, //1表示开启,0关闭
|
||||
enable: {type: Boolean, default: false},
|
||||
mock_script: String,
|
||||
uid: String,
|
||||
up_time: Number
|
||||
@ -25,7 +25,6 @@ class advMockModel extends baseModel {
|
||||
}
|
||||
|
||||
delByInterfaceId(interface_id) {
|
||||
console.log(interface_id);
|
||||
return this.model.deleteOne({
|
||||
interface_id: interface_id
|
||||
});
|
||||
|
@ -1,10 +1,19 @@
|
||||
const controller = require('./controller');
|
||||
const advModel = require('./model.js');
|
||||
const yapi = require('yapi.js');
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
|
||||
module.exports = function(){
|
||||
|
||||
yapi.connect.then(function () {
|
||||
let Col = mongoose.connection.db.collection('adv_mock')
|
||||
Col.ensureIndex({
|
||||
interface_id: 1
|
||||
})
|
||||
Col.ensureIndex({
|
||||
project_id: 1
|
||||
})
|
||||
})
|
||||
this.bindHook('add_router', function(addRouter){
|
||||
addRouter({
|
||||
controller: controller,
|
||||
|
@ -5,6 +5,7 @@ const yapi = require('./yapi.js');
|
||||
const commons = require('./utils/commons');
|
||||
yapi.commons = commons;
|
||||
const dbModule = require('./utils/db.js');
|
||||
yapi.connect = dbModule.connect();
|
||||
const mockServer = require('./middleware/mockServer.js');
|
||||
const plugins = require('./plugin.js');
|
||||
const websockify = require('koa-websocket');
|
||||
@ -17,7 +18,7 @@ const router = require('./router.js');
|
||||
|
||||
let indexFile = process.argv[2] === 'dev' ? 'dev.html' : 'index.html';
|
||||
|
||||
yapi.connect = dbModule.connect();
|
||||
|
||||
const app = websockify(new Koa());
|
||||
yapi.app = app;
|
||||
app.use(mockServer);
|
||||
|
@ -119,7 +119,7 @@ class interfaceColController extends baseController{
|
||||
if(!id || id == 0){
|
||||
return ctx.body = yapi.commons.resReturn(null, 407, 'col_id不能为空')
|
||||
}
|
||||
let result = await this.caseModel.list(id, 'all');
|
||||
let resultList = await this.caseModel.list(id, 'all');
|
||||
let colData = await this.colModel.get(id);
|
||||
let project = await this.projectModel.getBaseInfo(colData.project_id);
|
||||
|
||||
@ -129,20 +129,26 @@ class interfaceColController extends baseController{
|
||||
}
|
||||
}
|
||||
|
||||
for(let index=0; index< result.length; index++){
|
||||
|
||||
result[index] = result[index].toObject();
|
||||
let interfaceData = await this.interfaceModel.getBaseinfo(result[index].interface_id);
|
||||
if(!interfaceData){
|
||||
await this.caseModel.del(result[index]._id);
|
||||
result[index] = undefined;
|
||||
for(let index=0; index< resultList.length; index++){
|
||||
let result = resultList[index].toObject();
|
||||
let data = await this.interfaceModel.get(result.interface_id);
|
||||
if(!data){
|
||||
await this.caseModel.del(result._id);
|
||||
continue;
|
||||
}
|
||||
let projectData = await this.projectModel.getBaseInfo(interfaceData.project_id);
|
||||
result[index].path = projectData.basepath + interfaceData.path;
|
||||
result[index].method = interfaceData.method;
|
||||
let projectData = await this.projectModel.getBaseInfo(data.project_id);
|
||||
result.path = projectData.basepath + data.path;
|
||||
result.method = data.method;
|
||||
result.req_body_type = data.req_body_type;
|
||||
result.req_headers = data.req_headers;
|
||||
result.res_body = data.res_body;
|
||||
result.res_body_type = data.res_body_type;
|
||||
result.req_body_form = this.handleParamsValue(data.req_body_form, result.req_body_form)
|
||||
result.req_query = this.handleParamsValue(data.req_query, result.req_query)
|
||||
result.req_params = this.handleParamsValue(data.req_params, result.req_params)
|
||||
resultList[index] = result;
|
||||
}
|
||||
ctx.body = yapi.commons.resReturn(result);
|
||||
ctx.body = yapi.commons.resReturn(resultList);
|
||||
} catch (e) {
|
||||
ctx.body = yapi.commons.resReturn(null, 402, e.message);
|
||||
}
|
||||
@ -316,7 +322,7 @@ class interfaceColController extends baseController{
|
||||
return ctx.body = yapi.commons.resReturn(null, 400, '不存在的case');
|
||||
}
|
||||
result = result.toObject();
|
||||
let data = await this.interfaceModel.get(result.interface_id);
|
||||
let data = await this.interfaceModel.get(result.interface_id);
|
||||
if(!data){
|
||||
return ctx.body = yapi.commons.resReturn(null, 400, '找不到对应的接口,请联系管理员')
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user