feat: add case report module

This commit is contained in:
suxiaoxin 2017-09-20 20:21:58 +08:00
parent 608cb970af
commit f8a1fcfce4
8 changed files with 376 additions and 61 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, '找不到对应的接口,请联系管理员')
}