diff --git a/client/common.js b/client/common.js
index b46f1b2b..82f15b70 100755
--- a/client/common.js
+++ b/client/common.js
@@ -1,6 +1,7 @@
import React from 'react';
import moment from 'moment';
import constants from './constants/variable';
+import Mock from 'mockjs'
const Roles = {
0 : 'admin',
@@ -71,6 +72,40 @@ exports.debounce = (func, wait) => {
};
};
+
+
+exports.simpleJsonPathParse = function (key, json){
+ if(!key || typeof key !== 'string' || key.indexOf('$.') !== 0 || key.length <= 2){
+ return null;
+ }
+ let keys = key.substr(2).split(".");
+ keys = keys.filter(item=>{
+ return item;
+ })
+ for(let i=0, l = keys.length; i< l; i++){
+ try{
+ let m = keys[i].match(/(.*?)\[([0-9]+)\]/)
+ if(m){
+ json = json[m[1]][m[2]];
+ }else{
+ json = json[keys[i]];
+ }
+
+
+ }catch(e){
+ json = null;
+ break;
+ }
+ }
+ return json;
+
+}
+
+exports.handleMockWord =(word) =>{
+ if(!word || typeof word !== 'string' || word[0] !== '@') return word;
+ return Mock.mock(word);
+}
+
// 从 Javascript 对象中选取随机属性
exports.pickRandomProperty = (obj) => {
let result;
diff --git a/client/components/Postman/Postman.js b/client/components/Postman/Postman.js
index b1c013c4..be00c59a 100755
--- a/client/components/Postman/Postman.js
+++ b/client/components/Postman/Postman.js
@@ -10,6 +10,7 @@ import URL from 'url';
const MockExtra = require('common/mock-extra.js')
import './Postman.scss';
import json5 from 'json5'
+import { handleMockWord } from '../../common.js'
function json_parse(data) {
try {
@@ -19,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 => {
@@ -75,8 +61,9 @@ export default class Run extends Component {
bodyType: '',
bodyOther: '',
loading: false,
- validRes: null,
- hasPlugin: true
+ validRes: [],
+ hasPlugin: true,
+ test_status: null
}
constructor(props) {
@@ -136,8 +123,13 @@ export default class Run extends Component {
req_body_form = [],
basepath = '',
env = [],
- case_env = ''
+ case_env = '',
+ test_status = '',
+ test_res_body = '',
+ test_report = [],
+ test_res_header=''
} = data;
+
// case 任意编辑 pathname,不管项目的 basepath
const pathname = (type === 'inter' ? (basepath + url) : url).replace(/\/+/g, '/');
@@ -168,12 +160,21 @@ export default class Run extends Component {
bodyOther: req_body_other,
caseEnv: case_env || (env[0] && env[0].name),
bodyType: req_body_type || 'form',
- loading: false
+ loading: false,
+ test_status: test_status,
+ validRes: test_report,
+ res: test_res_body,
+ resHeader: test_res_header
}, () => {
if (req_body_type && req_body_type !== 'file' && req_body_type !== 'form') {
this.loadBodyEditor()
}
+ if(test_res_body){
+ this.bindAceEditor();
+ }
+
});
+
}
@autobind
@@ -192,7 +193,7 @@ export default class Run extends Component {
const href = URL.format({
protocol: urlObj.protocol || 'http',
host: urlObj.host,
- pathname: urlObj.pathname ? urlObj.pathname + path : path,
+ pathname: urlObj.pathname ? URL.resolve(urlObj.pathname, path) : path,
query: this.getQueryObj(query)
});
@@ -212,7 +213,7 @@ export default class Run extends Component {
}
const { res_body, res_body_type } = that.props.data;
- let validRes = '';
+ let validRes = [];
let query = {};
that.state.query.forEach(item => {
query[item.name] = item.value;
@@ -233,8 +234,17 @@ export default class Run extends Component {
validRes = Mock.valid(tpl, res)
}
- message.success('请求完成')
- that.setState({ res, resHeader: header, validRes })
+
+ if (Array.isArray(validRes) && validRes.length > 0) {
+ message.warn('请求完成, 返回数据跟接口定义不匹配');
+ validRes = validRes.map(item => {
+ return item.message
+ })
+ that.setState({ res, resHeader: header, validRes, test_status: 'invalid' })
+ } else if (Array.isArray(validRes) && validRes.length === 0) {
+ message.success('请求完成');
+ that.setState({ res, resHeader: header, validRes: ['验证通过'], test_status: 'ok' })
+ }
that.setState({ loading: false })
that.bindAceEditor()
} catch (e) {
@@ -250,7 +260,7 @@ export default class Run extends Component {
message.error(e.message)
}
message.error('请求异常')
- that.setState({ res: err || '请求失败', resHeader: header, validRes: null })
+ that.setState({ res: err || '请求失败', resHeader: header, validRes: [], test_status: 'error' })
that.setState({ loading: false })
that.bindAceEditor()
}
@@ -428,11 +438,12 @@ export default class Run extends Component {
const obj = {};
arr.forEach(item => {
if (item.name && item.type !== 'file') {
- obj[item.name] = item.value || '';
+ obj[item.name] = handleMockWord(item.value);
}
})
return obj;
}
+
getFiles(bodyForm) {
const files = {};
bodyForm.forEach(item => {
@@ -446,7 +457,7 @@ export default class Run extends Component {
const queryObj = {};
query.forEach(item => {
if (item.name) {
- queryObj[item.name] = item.value || '';
+ queryObj[item.name] = handleMockWord(item.value);
}
})
return queryObj;
@@ -498,11 +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}`);
@@ -510,16 +520,10 @@ export default class Run extends Component {
const search = decodeURIComponent(URL.format({ query: this.getQueryObj(query) }));
let validResView;
- if (!validRes) {
- validResView = '请定义返回json'
- }
- if (Array.isArray(validRes) && validRes.length > 0) {
- validResView = validRes.map((item, index) => {
- return
{item.message}
- })
- } else if (Array.isArray(validRes)) {
- validResView = 验证通过
- }
+ validResView = validRes.map((item, index) => {
+ return {item}
+ })
+
@@ -569,7 +573,7 @@ export default class Run extends Component {
domains.map((item, index) => ())
}
-
+
@@ -587,7 +591,7 @@ export default class Run extends Component {
onClick={this.props.save}
type="primary"
style={{ marginLeft: 10 }}
- >{this.props.type === 'inter' ? '保存' : '更新'}
+ >{this.props.type === 'inter' ? '保存' : '保存'}
diff --git a/client/containers/Group/GroupList/GroupList.scss b/client/containers/Group/GroupList/GroupList.scss
index 48ca0b33..91e3c5b3 100755
--- a/client/containers/Group/GroupList/GroupList.scss
+++ b/client/containers/Group/GroupList/GroupList.scss
@@ -49,8 +49,8 @@
transition: all .2s;
}
.delete-group:hover, .edit-group:hover {
- color: $color-blue;
- background-color: $color-white;
+ background-color: $color-blue;
+ border: 1px solid $color-blue;
}
}
.group-operate {
diff --git a/client/containers/Project/Interface/InterfaceCol/CaseReport.js b/client/containers/Project/Interface/InterfaceCol/CaseReport.js
new file mode 100644
index 00000000..78986d91
--- /dev/null
+++ b/client/containers/Project/Interface/InterfaceCol/CaseReport.js
@@ -0,0 +1,93 @@
+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 {item.message}
+ })
+
+ return
+
+
+
+ Url
+ {props.url}
+
+ {props.query ?
+
+ Query
+ {props.query}
+
+ : null
+ }
+
+ {props.headers ?
+
+ Headers
+ {headers}
+
+ : null
+ }
+
+ {props.body ?
+
+ Body
+ {body}
+
+ : null
+ }
+
+
+ {props.res_header ?
+
+ Headers
+ {res_header}
+
+ : null
+ }
+ {props.res_body ?
+
+ Body
+ {res_body}
+
+ : null
+ }
+
+
+
+ {props.validRes ?
+
+ 验证结果
+ {validRes}
+
+ : null
+ }
+
+
+
+
+
+
+}
+
+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;
\ No newline at end of file
diff --git a/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js b/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js
index 0d5a84a4..9847f3c1 100755
--- a/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js
+++ b/client/containers/Project/Interface/InterfaceCol/InterfaceCaseContent.js
@@ -96,6 +96,7 @@ export default class InterfaceCaseContent extends Component {
}
updateCase = async () => {
+
const {
caseEnv: case_env,
pathname: path,
@@ -107,9 +108,10 @@ export default class InterfaceCaseContent extends Component {
bodyForm: req_body_form,
bodyOther: req_body_other
} = this.postman.state;
+
const {editCasename: casename} = this.state;
const {_id: id} = this.props.currCase;
- const res = await axios.post('/api/col/up_case', {
+ let params = {
id,
casename,
case_env,
@@ -121,7 +123,20 @@ export default class InterfaceCaseContent extends Component {
req_body_type,
req_body_form,
req_body_other
- });
+ };
+ if(this.postman.state.test_status !== 'error'){
+ params.test_res_body = this.postman.state.res;
+ params.test_report = this.postman.state.validRes;
+ params.test_status = this.postman.state.test_status;
+ params.test_res_header = this.postman.state.resHeader;
+ }
+
+
+ if(params.test_res_body && typeof params.test_res_body === 'object'){
+ params.test_res_body = JSON.stringify(params.test_res_body, null, ' ');
+ }
+
+ const res = await axios.post('/api/col/up_case', params);
if (this.props.currCase.casename !== casename) {
this.props.fetchInterfaceColList(this.props.match.params.id);
}
diff --git a/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js b/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js
index 0e56fbd1..e80968e9 100755
--- a/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js
+++ b/client/containers/Project/Interface/InterfaceCol/InterfaceColContent.js
@@ -3,9 +3,31 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types'
import { withRouter } from 'react-router'
import { Link } from 'react-router-dom'
-import { Table, Tooltip } from 'antd'
+import { Tooltip, Icon, Button, Spin, Modal, message } from 'antd'
import { fetchInterfaceColList, fetchCaseList, setColData } from '../../../../reducer/modules/interfaceCol'
-import { formatTime } from '../../../../common.js'
+import HTML5Backend from 'react-dnd-html5-backend';
+import { DragDropContext } from 'react-dnd';
+import { handleMockWord, simpleJsonPathParse } 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 => {
@@ -14,7 +36,8 @@ import { formatTime } from '../../../../common.js'
currColId: state.interfaceCol.currColId,
currCaseId: state.interfaceCol.currCaseId,
isShowCol: state.interfaceCol.isShowCol,
- currCaseList: state.interfaceCol.currCaseList
+ currCaseList: state.interfaceCol.currCaseList,
+ currProject: state.project.currProject
}
},
{
@@ -24,7 +47,8 @@ import { formatTime } from '../../../../common.js'
}
)
@withRouter
-export default class InterfaceColContent extends Component {
+@DragDropContext(HTML5Backend)
+class InterfaceColContent extends Component {
static propTypes = {
match: PropTypes.object,
@@ -36,11 +60,22 @@ export default 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)
+ super(props);
+ this.reports = {};
+ this.records = {};
+ this.state = {
+ rows: [],
+ reports: {},
+ visible: false,
+ curCaseid: null
+ };
+ this.onRow = this.onRow.bind(this);
+ this.onMoveRow = this.onMoveRow.bind(this);
}
async componentWillMount() {
@@ -49,73 +84,362 @@ export default class InterfaceColContent extends Component {
const params = this.props.match.params;
const { actionId } = params;
currColId = +actionId ||
- result.payload.data.data.find(item => +item._id === +currColId) && +currColId ||
- result.payload.data.data[0]._id;
+ result.payload.data.data.find(item => +item._id === +currColId) && +currColId ||
+ result.payload.data.data[0]._id;
this.props.history.push('/project/' + params.id + '/interface/col/' + currColId)
- if(currColId && currColId != 0){
- this.props.fetchCaseList(currColId);
- this.props.setColData({currColId: +currColId, isShowCol: true})
+ if (currColId && currColId != 0) {
+ await this.props.fetchCaseList(currColId);
+ this.props.setColData({ currColId: +currColId, isShowCol: true })
+ this.handleColdata(this.props.currCaseList)
}
-
+
}
- componentWillReceiveProps(nextProps) {
+ handleColdata = (rows) => {
+ rows = rows.map((item) => {
+ item.id = item._id;
+ item._test_status = item.test_status;
+ return item;
+ })
+ rows = rows.sort((n, o) => {
+ return n.index > o.index
+ })
+ this.setState({
+ rows: rows
+ })
+ }
+
+ 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;
+ this.records[curitem._id] = result.res_body;
+ } 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) : 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)
+ }
+ })
+ })
+ }
+
+
+ handleVarWord(val){
+ return simpleJsonPathParse(val, this.records)
+ }
+
+ handleValue(val){
+ if(!val || typeof val !== 'string'){
+ return val;
+ }else if(val[0] === '@'){
+ return handleMockWord(val);
+ }else if(val.indexOf('$.') === 0){
+ return this.handleVarWord(val);
+ }
+ return val;
+ }
+
+ arrToObj =(arr) =>{
+ arr = arr || [];
+ const obj = {};
+ arr.forEach(item => {
+ if (item.name && item.type !== 'file') {
+ obj[item.name] = this.handleValue(item.value);
+ }
+ })
+ return obj;
+ }
+
+ getQueryObj =(query)=> {
+ query = query || [];
+ const queryObj = {};
+ query.forEach(item => {
+ if (item.name) {
+ queryObj[item.name] = this.handleValue(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 {
+ rowId: row.id,
+ onMove: this.onMoveRow
+ };
+ }
+ onMoveRow({ sourceRowId, targetRowId }) {
+ let rows = dnd.moveRows({
+ sourceRowId,
+ targetRowId
+ })(this.state.rows);
+ let changes = [];
+ rows.forEach((item, index) => {
+ changes.push({
+ id: item._id,
+ index: index
+ })
+ })
+
+ axios.post('/api/col/up_col_index', changes).then()
+ if (rows) {
+ this.setState({ rows });
+ }
+ }
+
+ async componentWillReceiveProps(nextProps) {
const { interfaceColList } = nextProps;
const { actionId: oldColId, id } = this.props.match.params
let newColId = nextProps.match.params.actionId
if (!interfaceColList.find(item => +item._id === +newColId)) {
this.props.history.push('/project/' + id + '/interface/col/' + interfaceColList[0]._id)
} else if (oldColId !== newColId) {
- if(newColId && newColId != 0){
- this.props.fetchCaseList(newColId);
- this.props.setColData({currColId: +newColId, isShowCol: true})
+ if (newColId && newColId != 0) {
+ await this.props.fetchCaseList(newColId);
+ this.props.setColData({ currColId: +newColId, isShowCol: true })
+ this.handleColdata(this.props.currCaseList)
}
-
+
}
}
+ openReport = (id) => {
+ if (!this.reports[id]) {
+ return message.warn('还没有生成报告')
+ }
+ this.setState({
+ visible: true,
+ curCaseid: id
+ })
+
+ }
+
+ handleCancel = () => {
+ this.setState({
+ visible: false
+ });
+ }
+
render() {
-
- const { currCaseList } = this.props;
-
const columns = [{
- title: '用例名称',
- dataIndex: 'casename',
- key: 'casename',
- render: (text, item)=>{
- return {text}
+ property: 'casename',
+ header: {
+ label: '用例名称'
+ },
+ cell: {
+ formatters: [
+ (text, { rowData }) => {
+ let record = rowData;
+ return {record.casename}
+ }
+ ]
}
}, {
- title: '接口路径',
- dataIndex: 'path',
- key: 'path',
- render: (path, record) => {
- return (
-
- {path}
-
- )
+ header: {
+ label: 'key',
+ formatters: [() => {
+ return
+ Key
+ }]
+ },
+ cell: {
+ formatters: [
+ (value, { rowData }) => {
+ return {rowData._id}
+ }]
}
}, {
- title: '请求方法',
- dataIndex: 'method',
- key: 'method'
- }, {
- title: '更新时间',
- dataIndex: 'up_time',
- key: 'up_time',
- render: (item) => {
- return {formatTime(item)}
+ property: 'test_status',
+ header: {
+ label: '状态'
+ },
+ cell: {
+ formatters: [(value, { rowData }) => {
+ switch (rowData.test_status) {
+ case 'ok':
+ return
+ case 'error':
+ return
+ case 'invalid':
+ return
+ case 'loading':
+ return
+ default:
+ return
+ }
+ }]
}
- }];
+ }, {
+ property: 'path',
+ header: {
+ label: '接口路径'
+ },
+ cell: {
+ formatters: [
+ (text, { rowData }) => {
+ let record = rowData;
+ return (
+
+ {record.path}
+
+ )
+ }
+ ]
+ }
+ }, {
+ header: {
+ label: '测试报告'
+
+ },
+ cell: {
+ formatters: [(text, { rowData }) => {
+ return
+ }]
+ }
+ }
+ ];
+ const { rows } = this.state;
+ const components = {
+ header: {
+ cell: dnd.Header
+ },
+ body: {
+ row: dnd.Row
+ }
+ };
+
+ const resolvedColumns = resolve.columnChildren({ columns });
+ const resolvedRows = resolve.resolve({
+ columns: resolvedColumns,
+ method: resolve.nested
+ })(rows);
return (
-
-
+
+
测试集合
+
+
+
+
+
+
+
+
+
)
}
}
+
+export default InterfaceColContent
\ No newline at end of file
diff --git a/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss b/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss
index da4ed49f..c84c48f0 100755
--- a/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss
+++ b/client/containers/Project/Interface/InterfaceCol/InterfaceColMenu.scss
@@ -30,3 +30,65 @@
}
}
}
+
+.container {
+ display: block;
+ width: 100%;
+ padding: 10px;
+}
+/*
+ * note that styling gu-mirror directly is a bad practice because it's too generic.
+ * you're better off giving the draggable elements a unique class and styling that directly!
+ */
+.gu-mirror {
+ padding: 10px;
+ background-color: rgba(0, 0, 0, 0.2);
+ transition: opacity 0.4s ease-in-out;
+}
+.container div {
+ cursor: move;
+ cursor: grab;
+ cursor: -moz-grab;
+ cursor: -webkit-grab;
+ margin-bottom: 10px;
+}
+.container div:last-child {
+ margin-bottom: 0;
+}
+.gu-mirror {
+ cursor: grabbing;
+ cursor: -moz-grabbing;
+ cursor: -webkit-grabbing;
+}
+.container .ex-moved {
+ background-color: #e74c3c;
+}
+.container.ex-over {
+ background-color: rgba(255, 255, 255, 0.3);
+}
+.handle {
+ padding: 0 5px;
+ margin-right: 5px;
+ 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;
+}
diff --git a/client/containers/Project/Interface/InterfaceList/Run/Run.js b/client/containers/Project/Interface/InterfaceList/Run/Run.js
index a6eba675..5e852aa6 100755
--- a/client/containers/Project/Interface/InterfaceList/Run/Run.js
+++ b/client/containers/Project/Interface/InterfaceList/Run/Run.js
@@ -59,7 +59,8 @@ export default class Run extends Component {
bodyForm: req_body_form,
bodyOther: req_body_other
} = this.postman.state;
- const res = await axios.post('/api/col/add_case', {
+
+ let params = {
interface_id,
casename: caseName,
col_id: colId,
@@ -73,7 +74,20 @@ export default class Run extends Component {
req_body_type,
req_body_form,
req_body_other
- });
+ };
+
+ if(this.postman.state.test_status !== 'error'){
+ params.test_res_body = this.postman.state.res;
+ params.test_report = this.postman.state.validRes;
+ params.test_status = this.postman.state.test_status;
+ params.test_res_header = this.postman.state.resHeader;
+ }
+
+ if(params.test_res_body && typeof params.test_res_body === 'object'){
+ params.test_res_body = JSON.stringify(params.test_res_body, null, ' ');
+ }
+
+ const res = await axios.post('/api/col/add_case', params);
if (res.data.errcode) {
message.error(res.data.errmsg)
} else {
@@ -85,7 +99,6 @@ export default class Run extends Component {
render () {
const { currInterface, currProject } = this.props;
const data = Object.assign({}, currInterface, currProject, {_id: currInterface._id})
-
return (
this.setState({saveCaseModalVisible: true})} ref={this.savePostmanRef} />
diff --git a/exts/yapi-plugin-advanced-mock/model.js b/exts/yapi-plugin-advanced-mock/model.js
index 6de524f6..d11d7c9f 100644
--- a/exts/yapi-plugin-advanced-mock/model.js
+++ b/exts/yapi-plugin-advanced-mock/model.js
@@ -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
});
diff --git a/exts/yapi-plugin-advanced-mock/server.js b/exts/yapi-plugin-advanced-mock/server.js
index 75af1d1b..e2ace960 100644
--- a/exts/yapi-plugin-advanced-mock/server.js
+++ b/exts/yapi-plugin-advanced-mock/server.js
@@ -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,
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 37106811..ffadc16d 100755
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,6 +1,6 @@
{
"name": "yapi",
- "version": "1.0.0",
+ "version": "1.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3196,6 +3196,22 @@
"randombytes": "2.0.5"
}
},
+ "disposables": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.1.tgz",
+ "integrity": "sha1-BkcnoltU9QK9griaot+4358bOeM="
+ },
+ "dnd-core": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.5.1.tgz",
+ "integrity": "sha1-F49a1lJs4C3VlQjxFVNfe/wM6U4=",
+ "requires": {
+ "asap": "2.0.6",
+ "invariant": "2.2.2",
+ "lodash": "4.17.4",
+ "redux": "3.7.2"
+ }
+ },
"dns-equal": {
"version": "1.0.0",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/dns-equal/-/dns-equal-1.0.0.tgz",
@@ -11438,6 +11454,27 @@
}
}
},
+ "react-dnd": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.5.1.tgz",
+ "integrity": "sha1-7O8dnYR4y3av1Zdc1RI+98O+v+U=",
+ "requires": {
+ "disposables": "1.0.1",
+ "dnd-core": "2.5.1",
+ "hoist-non-react-statics": "2.3.1",
+ "invariant": "2.2.2",
+ "lodash": "4.17.4",
+ "prop-types": "15.5.10"
+ }
+ },
+ "react-dnd-html5-backend": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.1.tgz",
+ "integrity": "sha1-02VuUUsMRpkCpIX/+nX4aE4hx3c=",
+ "requires": {
+ "lodash": "4.17.4"
+ }
+ },
"react-dock": {
"version": "0.2.4",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/react-dock/-/react-dock-0.2.4.tgz",
@@ -12182,6 +12219,19 @@
"slick-carousel": "1.7.1"
}
},
+ "reactabular-dnd": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/reactabular-dnd/-/reactabular-dnd-8.9.0.tgz",
+ "integrity": "sha1-B5LeSto2H5Qlj4pqGg1svYPSpaA="
+ },
+ "reactabular-table": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/reactabular-table/-/reactabular-table-8.9.0.tgz",
+ "integrity": "sha1-RvbO9jnNm9SyuvHxTiaG+ys14oQ=",
+ "requires": {
+ "classnames": "2.2.5"
+ }
+ },
"read-all-stream": {
"version": "3.1.0",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/read-all-stream/-/read-all-stream-3.1.0.tgz",
@@ -13644,6 +13694,11 @@
}
}
},
+ "table-resolver": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/table-resolver/-/table-resolver-3.2.0.tgz",
+ "integrity": "sha512-DQrDHFdJPnvIhyjAcTqF4vhu/Uhp5eNRst9Url9KmBNqxYSMrPXOJoxhU7HPCd3efi1Hua7lMIDnBAphsdhPQw=="
+ },
"tapable": {
"version": "0.2.8",
"resolved": "https://repo.corp.qunar.com/artifactory/api/npm/npm-qunar/tapable/-/tapable-0.2.8.tgz",
diff --git a/package.json b/package.json
index 7a45f5c3..7c2f4fb1 100755
--- a/package.json
+++ b/package.json
@@ -78,10 +78,14 @@
"rc-queue-anim": "^1.2.0",
"rc-scroll-anim": "^1.0.7",
"react": "^15.6.1",
+ "react-dnd": "^2.5.1",
+ "react-dnd-html5-backend": "^2.5.1",
"react-dom": "^15.6.1",
"react-redux": "^5.0.5",
"react-router-dom": "^4.1.1",
"react-scripts": "1.0.10",
+ "reactabular-dnd": "^8.9.0",
+ "reactabular-table": "^8.9.0",
"redux": "^3.7.1",
"redux-promise": "^0.5.3",
"redux-thunk": "^2.2.0",
@@ -89,6 +93,7 @@
"sha1": "^1.1.1",
"string-replace-webpack-plugin": "^0.1.3",
"style-loader": "^0.18.2",
+ "table-resolver": "^3.2.0",
"underscore": "^1.8.3",
"universal-cookie": "^2.0.8",
"url": "^0.11.0",
diff --git a/server/app.js b/server/app.js
index 489761d3..ca6f803c 100755
--- a/server/app.js
+++ b/server/app.js
@@ -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);
@@ -27,6 +28,8 @@ app.use(router.allowedMethods());
websocket(app);
+
+
app.use( async (ctx, next) => {
if( /^\/(?!api)[a-zA-Z0-9\/\-_]*$/.test(ctx.path) ){
ctx.path = "/"
diff --git a/server/controllers/interfaceCol.js b/server/controllers/interfaceCol.js
index 629bc3f6..1acd7d9b 100755
--- a/server/controllers/interfaceCol.js
+++ b/server/controllers/interfaceCol.js
@@ -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,27 @@ 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 +323,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, '找不到对应的接口,请联系管理员')
}
@@ -327,8 +334,7 @@ class interfaceColController extends baseController{
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.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)
@@ -341,6 +347,9 @@ class interfaceColController extends baseController{
handleParamsValue(params, val){
let value = {};
+ try{
+ params = params.toObject();
+ }catch(e){ }
if(params.length === 0 || val.length === 0){
return params;
}
@@ -349,7 +358,7 @@ class interfaceColController extends baseController{
})
params.forEach((item, index)=>{
if(!value[item.name] || typeof value[item.name] !== 'object') return null;
- params[index].value = value[item.name].value;
+ params[index].value = value[item.name].value;
})
return params;
}
@@ -412,9 +421,8 @@ class interfaceColController extends baseController{
if(!params || !Array.isArray(params)){
ctx.body = yapi.commons.resReturn(null, 400, "请求参数必须是数组")
}
- // let caseName = "";
params.forEach((item) => {
- if(item.id && item.index){
+ if(item.id){
this.caseModel.upCaseIndex(item.id, item.index).then((res) => {}, (err) => {
yapi.commons.log(err.message, 'error')
})
@@ -422,15 +430,6 @@ class interfaceColController extends baseController{
});
- // let username = this.getUsername();
- // yapi.commons.saveLog({
- // content: `用户 "${username}" 更新了接口集 "${params.col_name}"`,
- // type: 'project',
- // uid: this.getUid(),
- // username: username,
- // typeid: params.project_id
- // });
-
return ctx.body = yapi.commons.resReturn('成功!')
}catch(e){
ctx.body = yapi.commons.resReturn(null, 400, e.message)
diff --git a/server/controllers/test.js b/server/controllers/test.js
new file mode 100644
index 00000000..0435b59f
--- /dev/null
+++ b/server/controllers/test.js
@@ -0,0 +1,164 @@
+const yapi = require('../yapi.js');
+const baseController = require('./base.js');
+
+class interfaceColController extends baseController{
+ constructor(ctx) {
+ super(ctx);
+ }
+
+ /**
+ * 测试 get
+ * @interface /test/get
+ * @method GET
+ * @returns {Object}
+ * @example
+ */
+ async testGet(ctx){
+ try {
+ let query = ctx.query;
+ ctx.body = yapi.commons.resReturn(query);
+ } catch (e) {
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 post
+ * @interface /test/post
+ * @method POST
+ * @returns {Object}
+ * @example
+ */
+ async testPost(ctx){
+ try{
+ let params = ctx.request.body;
+ ctx.body = yapi.commons.resReturn(params);
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 单文件上传
+ * @interface /test/single/upload
+ * @method POST
+ * @returns {Object}
+ * @example
+ */
+ async testSingleUpload(ctx){
+ try{
+ let params = ctx.request.body;
+ ctx.body = yapi.commons.resReturn({res: '上传成功'});
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 文件上传
+ * @interface /test/files/upload
+ * @method POST
+ * @returns {Object}
+ * @example
+ */
+ async testFilesUpload(ctx){
+ try{
+ let params = ctx.request.body;
+ ctx.body = yapi.commons.resReturn({res: '上传成功'});
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 put
+ * @interface /test/put
+ * @method PUT
+ * @returns {Object}
+ * @example
+ */
+ async testPut(ctx){
+ try{
+ let params = ctx.request.body;
+ ctx.body = yapi.commons.resReturn(params);
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 delete
+ * @interface /test/delete
+ * @method DELETE
+ * @returns {Object}
+ * @example
+ */
+ async testDelete(ctx){
+ try{
+ let params = ctx.request.body;
+ ctx.body = yapi.commons.resReturn(params);
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 head
+ * @interface /test/head
+ * @method HEAD
+ * @returns {Object}
+ * @example
+ */
+ async testHead(ctx){
+ try{
+ let query = ctx.query;
+ ctx.body = yapi.commons.resReturn(query);
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 options
+ * @interface /test/options
+ * @method OPTIONS
+ * @returns {Object}
+ * @example
+ */
+ async testOptions(ctx){
+ try{
+ let query = ctx.query;
+ ctx.body = yapi.commons.resReturn(query);
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+ /**
+ * 测试 patch
+ * @interface /test/patch
+ * @method PATCH
+ * @returns {Object}
+ * @example
+ */
+ async testPatch(ctx){
+ try{
+ let params = ctx.request.body;
+ ctx.body = yapi.commons.resReturn(params);
+
+ }catch(e){
+ ctx.body = yapi.commons.resReturn(null, 402, e.message);
+ }
+ }
+
+
+}
+
+module.exports = interfaceColController
diff --git a/server/models/interfaceCase.js b/server/models/interfaceCase.js
index a12632de..bcc9c967 100755
--- a/server/models/interfaceCase.js
+++ b/server/models/interfaceCase.js
@@ -1,5 +1,7 @@
const yapi = require('../yapi.js');
const baseModel = require('./base.js');
+var mongoose = require('mongoose');
+var Schema = mongoose.Schema;
class interfaceCase extends baseModel {
getName() {
@@ -17,25 +19,21 @@ class interfaceCase extends baseModel {
add_time: Number,
up_time: Number,
case_env: { type: String },
- // path: { type: String },
- // method: { type: String },
req_params: [{
name: String, value: String
}],
req_query: [{
name: String, value: String
}],
- // req_headers: [{
- // name: String, value: String
- // }],
- // req_body_type: {
- // type: String,
- // enum: ['form', 'json', 'text', 'xml']
- // },
+
req_body_form: [{
name: String, value: String
}],
- req_body_other: String
+ req_body_other: String,
+ test_res_body: String,
+ test_status: {type: String, enum: ['ok', 'invalid', 'error', '']},
+ test_report: [],
+ test_res_header: Schema.Types.Mixed
};
}
diff --git a/server/router.js b/server/router.js
index e274b8cf..0050e8c3 100755
--- a/server/router.js
+++ b/server/router.js
@@ -3,6 +3,7 @@ const interfaceController = require('./controllers/interface.js');
const groupController = require('./controllers/group.js');
const userController = require('./controllers/user.js');
const interfaceColController = require('./controllers/interfaceCol.js');
+const testController = require('./controllers/test.js');
const yapi = require('./yapi.js');
const projectController = require('./controllers/project.js');
@@ -47,6 +48,10 @@ let INTERFACE_CONFIG = {
col: {
prefix: '/col/',
controller: interfaceColController
+ },
+ test: {
+ prefix: '/test/',
+ controller: testController
}
};
@@ -347,7 +352,44 @@ let routerConfig = {
path: "del_case",
method: "get"
}
- ]
+ ],
+ "test": [{
+ action: "testPost",
+ path: "post",
+ method: "post"
+ }, {
+ action: "testGet",
+ path: "get",
+ method: "get"
+ }, {
+ action: "testPut",
+ path: "put",
+ method: "put"
+ }, {
+ action: "testDelete",
+ path: "delete",
+ method: "del"
+ }, {
+ action: "testHead",
+ path: "head",
+ method: "head"
+ }, {
+ action: "testOptions",
+ path: "options",
+ method: "options"
+ }, {
+ action: "testPatch",
+ path: "patch",
+ method: "patch"
+ }, {
+ action: "testFilesUpload",
+ path: "files/upload",
+ method: "post"
+ }, {
+ action: "testSingleUpload",
+ path: "single/upload",
+ method: "post"
+ }]
}
let pluginsRouterPath = [];
diff --git a/ykit.js b/ykit.js
index 59ec79bb..e1177ea1 100755
--- a/ykit.js
+++ b/ykit.js
@@ -69,13 +69,18 @@ module.exports = {
'react-router-dom',
'prop-types',
'axios',
- 'moment'
-
+ 'moment',
+ 'react-dnd-html5-backend',
+ 'react-dnd',
+ 'reactabular-table',
+ 'reactabular-dnd',
+ 'table-resolver'
],
lib2: [
'brace',
'mockjs',
- 'json5'
+ 'json5',
+ 'url'
]
}
},