opti: 重构接口请求和测试部分代码

This commit is contained in:
suxiaoxin 2017-11-29 21:52:39 +08:00
parent 32bd14a2f5
commit dbd56db77b
21 changed files with 1736 additions and 1058 deletions

View File

@ -25,7 +25,7 @@ const roleAction = {
function isJson(json){
if(!json) return false;
try{
json = json5.parse(json);
json = JSON.parse(json);
return json;
}catch(e){
return false;
@ -34,6 +34,24 @@ function isJson(json){
exports.isJson = isJson;
function isJson5(json){
if(!json) return false;
try{
json = json5.parse(json);
return json;
}catch(e){
return false;
}
}
function deepCopyJson(json){
return JSON.parse(JSON.stringify(json));
}
exports.deepCopyJson = deepCopyJson;
exports.isJson5 = isJson5;
exports.checkAuth = (action, role)=>{
return Roles[roleAction[action]] <= Roles[role];
}
@ -305,4 +323,8 @@ exports.joinPath = (domain, joinPath) =>{
joinPath = joinPath.substr(1);
}
return domain + joinPath;
}
exports.safeArray = (arr) => {
return Array.isArray(arr) ? arr : [];
}

View File

@ -0,0 +1,67 @@
import React from 'react'
import mockEditor from './mockEditor'
import PropTypes from 'prop-types'
import './AceEditor.scss'
const ModeMap = {
'javascript' : 'ace/mode/javascript',
'json' : 'ace/mode/json',
'text' : 'ace/mode/text',
'xml' : 'ace/mode/xml',
'html' : 'ace/mode/html'
}
function getMode(mode){
return ModeMap[mode] || ModeMap.text
}
class AceEditor extends React.PureComponent {
constructor(props){
super(props);
}
static propTypes = {
data: PropTypes.any,
onChange: PropTypes.func,
className: PropTypes.string,
mode: PropTypes.string, //enum[json, text, javascript], default is javascript
readOnly: PropTypes.bool,
callback: PropTypes.func,
style: PropTypes.object,
fullScreen: PropTypes.bool
}
componentDidMount(){
this.editor = mockEditor({
container: this.editorElement,
data: this.props.data,
onChange: this.props.onChange,
readOnly: this.props.readOnly,
fullScreen: this.props.fullScreen
})
let mode = this.props.mode || 'javascript';
this.editor.editor.getSession().setMode(getMode(mode));
if(typeof this.props.callback === 'function'){
this.props.callback(this.editor.editor)
}
}
componentWillReceiveProps(nextProps){
if(!this.editor) return;
if(nextProps.data !== this.props.data && this.editor.getValue() !== nextProps.data){
this.editor.setValue(nextProps.data);
let mode = nextProps.mode || 'javascript';
this.editor.editor.getSession().setMode(getMode(mode));
this.editor.editor.clearSelection();
}
}
render() {
return <div className={this.props.className} style={this.props.style || {width: '100%', height: '200px'}} ref={editor=>{
this.editorElement=editor
}} ></div>
}
}
export default AceEditor;

View File

@ -0,0 +1,16 @@
.ace_editor.fullScreen {
height: auto;
width: auto;
border: 0;
margin: 0;
position: fixed !important;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1000008;
}
.fullScreen {
overflow: hidden
}

View File

@ -1,6 +1,9 @@
var ace = require('brace'),
Mock = require('mockjs')
Mock = require('mockjs');
require('brace/mode/javascript');
require('brace/mode/json');
require('brace/mode/xml');
require('brace/mode/html')
require('brace/theme/xcode');
require("brace/ext/language_tools.js");
var json5 = require('json5');
@ -57,6 +60,20 @@ var langTools = ace.acequire("ace/ext/language_tools"),
{ name: '协议', mock: '@protocol' }
];
let dom = ace.acequire("ace/lib/dom");
ace.acequire("ace/commands/default_commands").commands.push({
name: "Toggle Fullscreen",
bindKey: "F9",
exec: function(editor) {
if(editor._fullscreen_yapi){
let fullScreen = dom.toggleCssClass(document.body, "fullScreen")
dom.setCssClass(editor.container, "fullScreen", fullScreen)
editor.setAutoScrollEditorIntoView(!fullScreen)
editor.resize()
}
}
})
function run(options) {
var editor,
mockEditor,
@ -81,6 +98,7 @@ function run(options) {
}
data = options.data || '';
options.readOnly = options.readOnly || false;
options.fullScreen = options.fullScreen || false;
editor = ace.edit(container)
editor.$blockScrolling = Infinity;
@ -96,9 +114,10 @@ function run(options) {
enableLiveAutocompletion: true,
useWorker: true
});
editor._fullscreen_yapi = options.fullScreen;
mockEditor = {
curData: {},
getValue: editor.getValue,
getValue: ()=>mockEditor.curData.text,
setValue: function (data) {
data = data || '';
if (typeof data === 'string') {
@ -107,8 +126,12 @@ function run(options) {
editor.setValue(JSON.stringify(data, null, " "))
}
},
editor: editor
editor: editor,
options: options
}
rhymeCompleter = {
identifierRegexps: [/[@]/],
getCompletions: function (editor, session, pos, prefix, callback) {
@ -133,6 +156,7 @@ function run(options) {
if (typeof options.onChange === 'function') {
options.onChange.call(mockEditor, mockEditor.curData);
}
editor.clearSelection();
});

View File

@ -0,0 +1,59 @@
import React from 'react'
import { Alert } from 'antd'
import PropTypes from 'prop-types'
exports.initCrossRequest = function initCrossRequest(fn){
let startTime = 0;
let _crossRequest = setInterval(() => {
startTime += 500;
if (startTime > 5000) {
clearInterval(_crossRequest);
}
if (window.crossRequest) {
clearInterval(_crossRequest);
fn(true)
} else {
fn(false)
}
}, 500)
return _crossRequest;
}
CheckCrossInstall.propTypes = {
hasPlugin: PropTypes.bool
}
function CheckCrossInstall (props) {
const hasPlugin = props.hasPlugin;
return <div className={hasPlugin ? null : 'has-plugin'} >
{hasPlugin ? '' : <Alert
message={
<div>
重要当前的接口测试服务需安装免费测试增强插件,仅支持 chrome 浏览器选择下面任意一种安装方式
<div>
<a
target="blank"
href="https://chrome.google.com/webstore/detail/cross-request/cmnlfmgbjmaciiopcgodlhpiklaghbok?hl=en-US"
>[Google 商店获取需翻墙]
</a>
</div>
<div>
<a
target="blank"
href="/api/interface/download_crx"
> [手动下载] </a>
<span> zip 文件解压后将 crx 文件拖入到 chrome://extensions/ </span>
<a
target="blank"
href="http://www.jianshu.com/p/12ca04c61fc6"
> [详细安装教程] </a>
</div>
</div>
}
type="warning"
/>
}
</div>
}
export default CheckCrossInstall

View File

@ -0,0 +1,866 @@
import React, { PureComponent as Component } from 'react'
import PropTypes from 'prop-types'
import Mock from 'mockjs'
import { Button, Input, Checkbox, Select, Alert, Spin, Icon, Collapse, Tooltip, message, Switch } from 'antd'
import { autobind } from 'core-decorators';
import constants from '../../constants/variable.js'
import mockEditor from '../../containers/Project/Interface/InterfaceList/mockEditor'
import URL from 'url';
const MockExtra = require('common/mock-extra.js')
import './Postman.scss';
import json5 from 'json5'
import { isJson, handleJson, handleParamsValue,joinPath } from '../../common.js'
import _ from "underscore"
import ModalPostman from '../ModalPostman/index.js'
function json_parse(data) {
try {
return json5.parse(data)
} catch (e) {
return data
}
}
function isJsonData(headers) {
if (!headers || typeof headers !== 'object') return false;
let isResJson = false;
Object.keys(headers).map(key => {
if (/content-type/i.test(key) && /application\/json/i.test(headers[key])) {
isResJson = true;
}
})
return isResJson;
}
const InputGroup = Input.Group;
const Option = Select.Option;
const Panel = Collapse.Panel;
const HTTP_METHOD = constants.HTTP_METHOD;
export default class Run extends Component {
static propTypes = {
data: PropTypes.object,
save: PropTypes.func,
saveTip: PropTypes.string,
type: PropTypes.string
}
state = {
res: null,
resHeader: null,
method: 'GET',
domains: [],
pathname: '',
query: [],
bodyForm: [],
headers: [],
caseEnv: '',
bodyType: '',
bodyOther: '',
loading: false,
validRes: [],
hasPlugin: true,
test_status: null,
resMockTest: true,
resStatusCode: null,
resStatusText: '',
modalVisible: false,
inputIndex: 0,
inputValue: '',
modalType: ''
}
constructor(props) {
super(props)
}
componentWillMount() {
let startTime = 0;
this.interval = setInterval(() => {
startTime += 500;
if (startTime > 5000) {
clearInterval(this.interval);
}
if (window.crossRequest) {
clearInterval(this.interval);
this.setState({
hasPlugin: true
})
} else {
this.setState({
hasPlugin: false
})
}
}, 500)
this.getInterfaceState()
}
componentWillUnmount() {
clearInterval(this.interval)
}
componentWillReceiveProps(nextProps) {
if (nextProps.data._id !== this.props.data._id) {
this.getInterfaceState(nextProps)
}
}
componentDidMount() {
// console.log('router',this.props);
const { bodyType } = this.state;
if (bodyType && bodyType !== 'file' && bodyType !== 'form') {
this.loadBodyEditor()
}
}
handleValue(val) {
return handleParamsValue(val, {});
}
@autobind
getInterfaceState(nextProps) {
const props = nextProps || this.props;
const { data, type } = props;
const {
method = '',
path: url = '',
req_headers = [],
req_body_type,
req_query = [],
req_params = [],
req_body_other = '',
req_body_form = [],
basepath = '',
env = [],
case_env = '',
test_status = '',
test_res_body = '',
test_res_header = '',
mock_verify = false
} = data;
// case 任意编辑 pathname不管项目的 basepath
const pathname = (type === 'inter' ? (basepath + url) : url).replace(/\/+/g, '/');
// let hasContentType = false;
// req_headers.forEach(headerItem => {
// // TODO 'Content-Type' 排除大小写不同格式影响
// if (headerItem.name === 'Content-Type'){
// hasContentType = true;
// headerItem.value = headerItem.value || 'application/x-www-form-urlencoded';
// }
// })
// if (!hasContentType) {
// req_headers.push({name: 'Content-Type', value: 'application/x-www-form-urlencoded'});
// }
// const domains = env.concat();
// if (domain && !env.find(item => item.domain === domain)) {
// domains.push({name: 'default', domain})
// }
this.setState({
method,
domains: env.concat(),
pathParam: req_params.concat(),
pathname,
query: req_query.concat(),
bodyForm: req_body_form.concat(),
headers: req_headers.concat(),
bodyOther: req_body_other,
caseEnv: case_env || (env[0] && env[0].name),
bodyType: req_body_type || 'form',
loading: false,
test_status: test_status,
validRes: [],
res: test_res_body,
resHeader: test_res_header,
resMockTest: mock_verify
}, () => {
if (req_body_type && req_body_type !== 'file' && req_body_type !== 'form') {
this.loadBodyEditor()
}
if (test_res_body) {
this.bindAceEditor();
}
});
}
handleResponse = (res, header, third)=> {
res = third.res.body || third.res.statusText;
try {
this.setState({
loading: false,
resStatusCode: third.res.status,
resStatusText: third.res.statusText
})
res = json_parse(res);
let test_status = 'error';
let validRes = [];
if (isNaN(third.res.status)) {
res = res || '请求异常';
message.error(res);
test_status = 'error';
} else {
const { res_body, res_body_type } = this.props.data;
let query = {};
this.state.query.forEach(item => {
query[item.name] = item.value;
})
let body = {};
if (this.state.bodyType === 'form') {
this.state.bodyForm.forEach(item => {
body[item.name] = item.value;
})
} else if (this.state.bodyType === 'json') {
body = json_parse(this.state.bodyOther);
}
if (res_body && res_body_type === 'json' && typeof res === 'object' && this.state.resMockTest === true) {
let tpl = MockExtra(json_parse(res_body), {
query: query,
body: body
})
validRes = Mock.valid(tpl, res)
}
if (Array.isArray(validRes) && validRes.length > 0) {
message.warn('请求完成, 返回数据跟接口定义不匹配');
validRes = validRes.map(item => {
return item.message
})
test_status = 'invalid';
} else if (Array.isArray(validRes) && validRes.length === 0) {
message.success('请求完成');
test_status = 'ok'
}
}
this.setState({ res, resHeader: header, validRes, test_status }, this.bindAceEditor)
} catch (err) {
console.error(err)
}
}
@autobind
reqRealInterface() {
if (this.state.loading) {
this.setState({ loading: false })
return;
}
const { headers, bodyForm, pathParam, bodyOther, caseEnv, domains, method, pathname, query, bodyType } = this.state;
let path = pathname;
pathParam.forEach(item => {
path = path.replace(`:${item.name}`, this.handleValue(item.value) || `:${item.name}`);
});
let curdomain = _.find(domains, item => item.name === caseEnv).domain;
const urlObj = URL.parse(joinPath(curdomain, path));
let pathQuery = {};
urlObj.query && urlObj.query.split('&').forEach(item => {
if (item) {
item = item.split('=');
pathQuery[item[0]] = item[1];
}
})
const href = URL.format({
protocol: urlObj.protocol || 'http',
host: urlObj.host,
pathname: urlObj.pathname,
query: Object.assign(pathQuery, this.getQueryObj(query))
});
let reqBody;
if (bodyType === 'form') {
reqBody = this.arrToObj(bodyForm)
} else if(bodyType === 'json'){
reqBody = isJson(bodyOther);
if (reqBody === false) {
return message.error('请求 Body 的 json 格式有误')
} else {
reqBody = handleJson(reqBody, this.handleValue)
}
}else{
reqBody = bodyOther;
}
this.setState({ loading: true })
window.crossRequest({
url: href,
method,
headers: this.getHeadersObj(headers),
data: reqBody,
files: bodyType === 'form' ? this.getFiles(bodyForm) : {},
file: bodyType === 'file' ? 'single-file' : null,
timeout: 8240000,
success: this.handleResponse,
error: this.handleResponse
})
}
// @autobind
// changeDomain(value) {
// this.setState({ currDomain: value });
// }
@autobind
selectDomain(value) {
this.setState({ caseEnv: value });
}
@autobind
changeHeader(v, index, isName) {
// v = v.target.value
const headers = json_parse(JSON.stringify(this.state.headers));
if (isName) {
headers[index].name = v;
} else {
headers[index].value = v;
}
this.setState({ headers });
}
@autobind
addHeader() {
const { headers } = this.state;
this.setState({ headers: headers.concat([{ name: '', value: '' }]) })
}
@autobind
deleteHeader(index) {
const { headers } = this.state;
this.setState({ headers: headers.filter((item, i) => +index !== +i) });
}
@autobind
setContentType() {
// const headersObj = this.getHeadersObj(this.state.headers);
// headersObj['Content-Type'] = type;
// this.setState({ headers: this.objToArr(headersObj) })
}
@autobind
changeQuery(v, index, key) {
console.log(v);
key = key || 'value';
// v = v.target.value;
const query = json_parse(JSON.stringify(this.state.query));
if (key == 'enable') {
query[index].enable = v;
} else {
query[index].value = v;
query[index].enable = true;
}
this.setState({ query });
}
@autobind
addQuery() {
const { query } = this.state;
this.setState({ query: query.concat([{ name: '', value: '' }]) })
}
@autobind
deleteQuery(index) {
const { query } = this.state;
this.setState({ query: query.filter((item, i) => +index !== +i) });
}
@autobind
changePathParam(v, index, isKey) {
// v = v.target.value;
const pathParam = JSON.parse(JSON.stringify(this.state.pathParam));
const name = pathParam[index].name;
let newPathname = this.state.pathname;
if (isKey) {
if (!name && v) {
newPathname += `/:${v}`;
} else {
newPathname = newPathname.replace(`/:${name}`, v ? `/:${v}` : '')
}
pathParam[index].name = v;
} else {
pathParam[index].value = v;
}
this.setState({ pathParam, pathname: newPathname });
}
@autobind
addPathParam() {
const { pathParam } = this.state;
this.setState({ pathParam: pathParam.concat([{ name: '', value: '' }]) })
}
@autobind
deletePathParam(index) {
const { pathParam } = this.state;
const name = pathParam[index].name;
const newPathname = this.state.pathname.replace(`/:${name}`, '');
this.setState({ pathParam: pathParam.filter((item, i) => +index !== +i), pathname: newPathname });
}
@autobind
changeBody(v, index, key) {
const bodyForm = json_parse(JSON.stringify(this.state.bodyForm));
key = key || 'value';
if (key === 'value') {
bodyForm[index].enable = true;
if (bodyForm[index].type === 'file') {
bodyForm[index].value = 'file_' + index
} else {
bodyForm[index].value = v
}
} else if (key === 'enable') {
bodyForm[index].enable = v
}
this.setState({ bodyForm });
}
@autobind
addBody() {
const { bodyForm } = this.state;
this.setState({ bodyForm: bodyForm.concat([{ name: '', value: '', type: 'text' }]) })
}
@autobind
deleteBody(index) {
const { bodyForm } = this.state;
this.setState({ bodyForm: bodyForm.filter((item, i) => +index !== +i) });
}
@autobind
changeMethod(value) {
this.setState({ method: value });
}
@autobind
changePath(e) {
const path = e.target.value;
const urlObj = URL.parse(path, true);
this.setState({
query: this.objToArr(urlObj.query),
pathname: urlObj.pathname
})
}
@autobind
changeBodyType(value) {
this.setState({ bodyType: value }, () => {
if (value !== 'file' && value !== 'form') {
this.loadBodyEditor()
}
})
}
// hasCrossRequestPlugin() {
// const dom = document.getElementById('y-request');
// return dom.getAttribute('key') === 'yapi';
// }
objToArr(obj, key, value) {
const keyName = key || 'name';
const valueName = value || 'value';
const arr = []
Object.keys(obj).forEach((_key) => {
if (_key) {
arr.push({ [keyName]: _key, [valueName]: obj[_key] });
}
})
return arr;
}
arrToObj(arr) {
const obj = {};
arr.forEach(item => {
if (item)
if (item.name && item.type !== 'file' && item.enable) {
obj[item.name] = this.handleValue(item.value);
}
})
return obj;
}
getFiles(bodyForm) {
const files = {};
bodyForm.forEach(item => {
if (item.name && item.enable === true && item.type === 'file') {
files[item.name] = item.value
}
})
return files;
}
getQueryObj(query) {
const queryObj = {};
query.forEach(item => {
if (item.name && item.enable) {
queryObj[item.name] = this.handleValue(item.value);
}
})
return queryObj;
}
getHeadersObj(headers) {
const headersObj = {};
headers.forEach(item => {
if (item.name && item.value) {
headersObj[item.name] = this.handleValue(item.value);
}
})
return headersObj;
}
bindAceEditor = () => {
mockEditor({
container: 'res-body-pretty',
data: this.state.res,
readOnly: true,
onChange: function () { }
})
mockEditor({
container: 'res-headers-pretty',
data: this.state.resHeader,
readOnly: true,
onChange: function () { }
})
}
loadBodyEditor = () => {
const that = this;
setTimeout(function () {
mockEditor({
container: 'body-other-edit',
data: that.state.bodyOther,
onChange: function (d) {
that.setState({
bodyOther: d.text
})
}
})
}, 0);
}
@autobind
onTestSwitched(checked) {
this.setState({
resMockTest: checked
});
}
// 模态框的相关操作
showModal = (val, index, type) => {
this.setState({
modalVisible: true,
inputIndex: index,
inputValue: val,
modalType: type
});
}
handleOk = (val) => {
const { inputIndex, modalType } = this.state
switch (modalType) {
case 'query':
this.changeQuery(val, inputIndex);
break;
case 'body':
this.changeBody(val, inputIndex);
break;
case 'header':
this.changeHeader(val, inputIndex);
break;
case 'pathParam':
this.changePathParam(val, inputIndex);
break;
default:
break;
}
this.setState({ modalVisible: false });
}
handleCancel = () => {
this.setState({ modalVisible: false });
}
render() {
const { method, domains, pathParam, pathname, query, inputValue, headers, bodyForm, caseEnv, bodyType, resHeader, loading, validRes } = this.state;
HTTP_METHOD[method] = HTTP_METHOD[method] || {}
const hasPlugin = this.state.hasPlugin;
let isResJson = isJsonData(resHeader);
let path = pathname;
pathParam.forEach(item => {
let val = this.handleValue(item.value);
path = path.replace(`:${item.name}`, val || `:${item.name}`);
});
const pathObj = URL.parse(path);
path = pathObj.pathname;
let pathQuery = {};
pathObj.query && pathObj.query.split('&').forEach(item => {
if (item) {
item = item.split('=');
pathQuery[item[0]] = item[1];
}
})
const search = decodeURIComponent(URL.format({ query: Object.assign(pathQuery, this.getQueryObj(query)) }));
let validResView;
validResView = validRes.map((item, index) => {
return <div key={index}>{item}</div>
})
return (
<div className="interface-test postman">
<ModalPostman
visible={this.state.modalVisible}
handleCancel={this.handleCancel}
handleOk={this.handleOk}
inputValue={inputValue}
envType={this.props.type}
>
</ModalPostman>
<div className={hasPlugin ? null : 'has-plugin'} >
{hasPlugin ? '' : <Alert
message={
<div>
{/* 温馨提示:当前正在使用接口测试服务,请安装我们为您免费提供的测试增强插件&nbsp;(该插件可支持任何 chrome 内核的浏览器) */}
重要当前的接口测试服务需安装免费测试增强插件 支持所有 webkit 内核选择下面任意一种安装方式
<div>
<a
target="blank"
href="https://chrome.google.com/webstore/detail/cross-request/cmnlfmgbjmaciiopcgodlhpiklaghbok?hl=en-US"
> [Google 商店获取需翻墙]</a>
</div>
<div>
<a
target="blank"
href="/api/interface/download_crx"
> [手动下载] </a>
<span> zip 文件解压后将 crx 文件拖入到 chrome://extensions/ </span>
<a
target="blank"
href="http://www.jianshu.com/p/12ca04c61fc6"
> [详细安装教程] </a>
</div>
</div>
}
type="warning"
/>
}
</div>
<h2 className="interface-title" style={{ marginTop: 0 }}>请求部分&nbsp;
<Tooltip placement="top" title="在 '设置->环境配置' 配置 domain"><Icon type="question-circle-o" /></Tooltip>
</h2>
<div className="url">
<InputGroup compact style={{ display: 'flex' }}>
<Select disabled value={method} style={{ flexBasis: 60 }} onChange={this.changeMethod} >
<Option value="GET">GET</Option>
<Option value="POST">POST</Option>
</Select>
<Select value={caseEnv} style={{ flexBasis: 180, flexGrow: 1 }} onSelect={this.selectDomain}>
{
domains.map((item, index) => (<Option value={item.name} key={index}>{item.name + '' + item.domain}</Option>))
}
</Select>
<Input disabled value={path + search} onChange={this.changePath} spellCheck="false" style={{ flexBasis: 180, flexGrow: 1 }} />
</InputGroup>
<Tooltip placement="bottom" title={(() => {
if (hasPlugin) {
return '发送请求'
} else {
return '请安装cross-request插件'
}
})()}>
<Button
disabled={!hasPlugin}
onClick={this.reqRealInterface}
type="primary"
style={{ marginLeft: 10 }}
icon={loading ? 'loading' : ''}
>{loading ? '取消' : '发送'}</Button>
</Tooltip>
<Tooltip placement="bottom" title={this.props.saveTip}>
<Button
onClick={this.props.save}
type="primary"
style={{ marginLeft: 10 }}
>{this.props.type === 'inter' ? '保存' : '保存'}</Button>
</Tooltip>
</div>
<Collapse defaultActiveKey={['0', '1', '2', '3']} bordered={true}>
<Panel header="PATH PARAMETERS" key="0" className={pathParam.length === 0 ? 'hidden' : ''}>
{
pathParam.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} onChange={e => this.changePathParam(e.target.value, index, true)} className="key" />
<span className="eq-symbol">=</span>
<Input
value={item.value}
className="value"
onChange={e => this.changePathParam(e.target.value, index)}
placeholder="参数值"
addonAfter={<Icon type="edit" onClick={() => this.showModal(item.value, index, 'pathParam')} />}
/>
<Icon style={{ display: 'none' }} type="delete" className="icon-btn" onClick={() => this.deletePathParam(index)} />
</div>
)
})
}
<Button style={{ display: 'none' }} type="primary" icon="plus" onClick={this.addPathParam}>添加Path参数</Button>
</Panel>
<Panel header="QUERY PARAMETERS" key="1" className={query.length === 0 ? 'hidden' : ''}>
{
query.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} className="key" />
&nbsp;
{item.required == 1 ?
<Checkbox checked={true} disabled >enable</Checkbox> :
<Checkbox checked={item.enable} onChange={e => this.changeQuery(e.target.checked, index, 'enable')}>enable</Checkbox>
}
<span className="eq-symbol">=</span>
<Input
value={item.value}
className="value"
onChange={e => this.changeQuery(e.target.value, index)}
placeholder="参数值"
addonAfter={<Icon type="edit" onClick={() => this.showModal(item.value, index, 'query')} />}
/>
<Icon style={{ display: 'none' }} type="delete" className="icon-btn" onClick={() => this.deleteQuery(index)} />
</div>
)
})
}
<Button style={{ display: 'none' }} type="primary" icon="plus" onClick={this.addQuery}>添加Query参数</Button>
</Panel>
<Panel header="HEADERS" key="2" className={headers.length === 0 ? 'hidden' : ''}>
{
headers.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} onChange={e => this.changeHeader(e.target.value, index, true)} className="key" />
<span className="eq-symbol">=</span>
<Input
value={item.value}
className="value"
onChange={e => this.changeHeader(e.target.value, index)}
placeholder="参数值"
addonAfter={<Icon type="edit" onClick={() => this.showModal(item.value, index, 'header')} />}
/>
<Icon style={{ display: 'none' }} type="delete" className="icon-btn" onClick={() => this.deleteHeader(index)} />
</div>
)
})
}
<Button style={{ display: 'none' }} type="primary" icon="plus" onClick={this.addHeader}>添加Header</Button>
</Panel>
<Panel
header={
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<div>BODY</div>
</div>
}
key="3"
className={HTTP_METHOD[method].request_body ? 'POST' : 'hidden'}
>
<div style={{ display: HTTP_METHOD[method].request_body && bodyType !== 'form' && bodyType !== 'file' ? 'block' : 'none' }}>
<div id="body-other-edit" style={{ marginTop: 10, minHeight: 150 }} className="pretty-editor"></div>
</div>
{
HTTP_METHOD[method].request_body && bodyType === 'form' &&
<div>
{
bodyForm.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} onChange={e => this.changeBody(e.target.value, index, 'key')} className="key" />
&nbsp;
{item.required == 1 ?
<Checkbox checked={true} disabled >enable</Checkbox> :
<Checkbox checked={item.enable} onChange={e => this.changeBody(e.target.checked, index, 'enable')}>enable</Checkbox>
}
<span className="eq-symbol">=</span>
{item.type === 'file' ?
<Input type="file" id={'file_' + index} onChange={e => this.changeBody(e.target.value, index, 'value')} multiple className="value" /> :
<Input
value={item.value}
className="value"
onChange={e => this.changeBody(e.target.value, index)}
placeholder="参数值"
addonAfter={<Icon type="edit" onClick={() => this.showModal(item.value, index, 'body')} />}
/>
}
<Icon style={{ display: 'none' }} type="delete" className="icon-btn" onClick={() => this.deleteBody(index)} />
</div>
)
})
}
<Button style={{ display: 'none' }} type="primary" icon="plus" onClick={this.addBody}>添加Form参数</Button>
</div>
}
{
HTTP_METHOD[method].request_body && bodyType === 'file' &&
<div>
<Input type="file" id="single-file"></Input>
</div>
}
{/*
method !== 'POST' &&
<div>GET 请求没有 BODY</div>
*/}
</Panel>
</Collapse>
<h2 className="interface-title">返回结果</h2>
<Spin spinning={this.state.loading}>
<h2 style={{ display: this.state.resStatusCode ? '' : 'none' }} className={'res-code ' + ((this.state.resStatusCode >= 200 && this.state.resStatusCode < 400 && !this.state.loading) ? 'success' : 'fail')}>
{this.state.resStatusCode + ' ' + this.state.resStatusText}</h2>
<div style={{ display: this.state.res ? '' : 'none' }} className="container-header-body">
<div className="header">
<div className="container-title">
<h4>Headers</h4>
</div>
<div id="res-headers-pretty" className="pretty-editor-header"></div>
</div>
<div className="resizer">
<div className="container-title">
<h4 style={{ visibility: 'hidden' }}>1</h4>
</div>
</div>
<div className="body">
<div className="container-title">
<h4>Body</h4>
</div>
<div id="res-body-pretty" className="pretty-editor-body" style={{ display: isResJson ? '' : 'none' }}></div>
<div
style={{ display: isResJson ? 'none' : '' }}
className="res-body-text"
>{this.state.res && this.state.res.toString()}</div>
</div>
</div>
</Spin>
<p style={{ display: this.state.resStatusCode === null ? '' : 'none' }}>发送请求后在这里查看返回结果</p>
<h2 className="interface-title">数据结构验证
<Switch style={{ verticalAlign: 'text-bottom', marginLeft: '8px' }} checked={this.state.resMockTest} onChange={this.onTestSwitched} />
</h2>
<div className={(isResJson && this.state.resMockTest) ? '' : 'none'}>
{(isResJson && this.state.resMockTest) ? validResView : <div><p>若开启此功能则发送请求后在这里查看验证结果</p><p>YApi Response body </p></div>}
</div>
</div>
)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,4 @@
@import '../../styles/mixin.scss';
.postman {
.pretty-editor {
border: 1px solid #d9d9d9;
@ -11,42 +12,137 @@
.pretty-editor-header {
border: 1px solid #d9d9d9;
border-radius: 4px;
min-height: 200px;
min-height: 300px;
}
}
.interface-test {
padding: .24rem;
.ant-checkbox-wrapper{
display: flex;
}
}
// .modal-postman {
// .ant-modal-body {
// padding: 0;
// }
// .ant-modal-footer {
// background-color: #f5f5f5;
// }
// .modal-postman-form {
// padding: 16px;
// max-height: 300px;
// overflow-y: scroll;
// .row {
// margin-bottom: 8px;
// }
// }
// .modal-postman-expression, .modal-postman-preview {
// border-top: 1px solid #e9e9e9;
// padding: 8px 16px;
// }
// .modal-postman-expression {
// }
// .modal-postman-preview {
// background-color: #f5f5f5;
// }
// .title {
// border-left: 3px solid #2395f1;
// padding-left: 8px;
// }
// }
.interface-test {
padding: .24rem;
.ant-checkbox-wrapper{
display: flex;
}
}
.response-tab{
margin-top: 20px;
margin-bottom: 20px;
.ant-tabs-nav{
background: #f7f7f7;
border-radius: 0 0 4px 4px;
border: 1px solid #d9d9d9;
width: 100%;
}
.header, .body{
padding-left: 10px;
margin-bottom: 10px;
}
.response-test{
min-height: 400px;
}
}
.ant-spin-blur {
.res-code.success {
background-color: transparent;
}
.res-code.fail {
background-color: transparent;
}
}
.res-code {
padding: .08rem .28rem;
color: #fff;
margin-left: -.1rem;
margin-right: -.28rem;
transition: all .2s;
position: relative;
border-radius: 2px;
}
.res-code.success {
background-color: $color-antd-green;
}
.res-code.fail {
background-color: $color-antd-red;
}
// 容器左侧是header 右侧是body
.container-header-body {
display: flex;
padding-bottom: .36rem;
.header, .body {
flex: 1 0 300px;
.pretty-editor-header, .pretty-editor-body {
height: 100%;
}
.postman .pretty-editor-body {
min-height: 200px;
}
.ace_print-margin {
display: none;
}
}
.header {
max-width: 400px;
}
.container-title {
padding: .08rem 0;
}
.resizer {
flex: 0 0 21px;
position: relative;
&:after {
content: '';
display: block;
width: 1px;
height: 100%;
background-color: #acaaaa;
opacity: .8;
position: absolute;
left: 16px;
}
}
// res body 无返回json时显示text信息
.res-body-text {
height: 100%;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 8px;
}
}
.has-plugin, .req-part, .resp-part {
margin-bottom: 16px;
}
.url {
display: flex;
margin: 24px 10px;
}
.key-value-wrap {
display: flex;
align-items: center;
margin: 0 0 5px 0;
.key {
flex-basis: 220px;
}
.value {
flex-grow: 1;
}
.eq-symbol {
margin: 0 5px;
}
.params-enable{
width: 98px;
}
}
.icon-btn {
cursor: pointer;
margin-left: 6px;
}
.icon-btn:hover {
color: #2395f1;
}
}

View File

@ -0,0 +1,163 @@
import { isJson5, handleJson, joinPath, safeArray } from '../../common.js'
import constants from '../../constants/variable.js'
import _ from "underscore"
import URL from 'url';
const HTTP_METHOD = constants.HTTP_METHOD;
exports.checkRequestBodyIsRaw = checkRequestBodyIsRaw;
exports.handleParams = handleParams;
exports.handleContentType = handleContentType;
exports.crossRequest = crossRequest;
const ContentTypeMap = {
'application/json': 'json',
'application/xml': 'xml',
'other': 'text',
'application/html': 'html'
}
function handleContentType(headers) {
if (!headers || typeof headers !== 'object') return ContentTypeMap.other;
let contentTypeItem = 'other';
try {
Object.keys(headers).forEach(key => {
if (/content-type/i.test(key)) {
contentTypeItem = headers[key].split(";")[0].trim().toLowerCase();
}
})
return ContentTypeMap[contentTypeItem] ? ContentTypeMap[contentTypeItem] : ContentTypeMap.other;
} catch (err) {
return ContentTypeMap.other
}
}
function checkRequestBodyIsRaw(method, reqBodyType) {
if (reqBodyType && reqBodyType !== 'file' && reqBodyType !== 'form' && HTTP_METHOD[method].request_body) {
return reqBodyType;
}
return false;
}
function handleCurrDomain(domains, case_env) {
let currDomain = _.find(domains, item => item.name === case_env);
if (!currDomain) {
currDomain = domains[0];
}
return currDomain;
}
function crossRequest(options) {
return new Promise((resolve, reject) => {
options.error = options.success = function (res, header, data) {
if (isNaN(data.res.status)) {
reject({
body: res,
header,
message: '请求异常,请检查 chrome network 错误信息...'
})
}
resolve(data);
}
window.crossRequest(options);
})
}
function handleParams(interfaceData, handleValue, requestParams) {
function paramsToObjectWithEnable(arr) {
const obj = {};
safeArray(arr).forEach(item => {
if (item && item.name && (item.enable || item.required === '1')) {
obj[item.name] = handleValue(item.value);
if (requestParams) {
requestParams[item.name] = obj[item.name];
}
}
})
return obj;
}
function paramsToObjectUnWithEnable(arr) {
const obj = {};
safeArray(arr).forEach(item => {
if (item && item.name) {
obj[item.name] = handleValue(item.value);
if (requestParams) {
requestParams[item.name] = obj[item.name];
}
}
})
return obj;
}
let { case_env, path, env } = interfaceData;
let pathQuery = {}, currDomain, requestBody, requestOptions;
interfaceData.req_params = interfaceData.req_params || [];
interfaceData.req_params.forEach(item => {
let val = handleValue(item.value);
if (requestParams) {
requestParams[item.name] = val;
}
path = path.replace(`:${item.name}`, val || `:${item.name}`);
});
currDomain = handleCurrDomain(env, case_env);
const urlObj = URL.parse(joinPath(currDomain.domain, path));
urlObj.query && urlObj.query.split('&').forEach(item => {
if (item) {
item = item.split('=');
pathQuery[item[0]] = item[1];
}
})
const url = URL.format({
protocol: urlObj.protocol || 'http',
host: urlObj.host,
pathname: urlObj.pathname,
query: Object.assign(pathQuery, paramsToObjectWithEnable(interfaceData.req_query))
});
if (HTTP_METHOD[interfaceData.method].request_body) {
if (interfaceData.req_body_type === 'form') {
requestBody = paramsToObjectWithEnable(safeArray(interfaceData.req_body_form).filter(item => {
return item.type == 'text'
}));
} else {
let reqBody = isJson5(interfaceData.req_body_other);
if (reqBody === false) {
requestBody = handleValue(reqBody);
} else {
if (requestParams) {
requestParams = Object.assign(requestParams, reqBody);
}
requestBody = handleJson(reqBody, handleValue);
}
}
}
requestOptions = {
url,
method: interfaceData.method,
headers: paramsToObjectUnWithEnable(interfaceData.req_headers),
data: requestBody,
timeout: 82400000
}
if (interfaceData.req_body_type === 'form') {
requestOptions.files = paramsToObjectWithEnable(safeArray(interfaceData.req_body_form).filter(item => {
return item.type == 'file'
}))
} else if (interfaceData.req_body_type === 'file') {
requestOptions.file = 'single-file'
}
return requestOptions;
}

View File

@ -3,7 +3,6 @@ import Footer from './Footer/Footer.js'
import Header from './Header/Header.js'
import Intro from './Intro/Intro.js'
import Loading from './Loading/Loading.js'
import MockDoc from './MockDoc/MockDoc.js'
import ProjectCard from './ProjectCard/ProjectCard.js'
import Subnav from './Subnav/Subnav.js'
import Postman from './Postman/Postman'
@ -14,7 +13,6 @@ export {
Header,
Intro,
Loading,
MockDoc,
ProjectCard,
Subnav,
Postman

View File

@ -99,16 +99,17 @@ export default class InterfaceCaseContent extends Component {
updateCase = async () => {
const {
caseEnv: case_env,
pathname: path,
method,
pathParam: req_params,
query: req_query,
headers: req_headers,
bodyType: req_body_type,
bodyForm: req_body_form,
bodyOther: req_body_other,
resMockTest: mock_verify
case_env,
req_params,
req_query,
req_headers,
req_body_type,
req_body_form,
req_body_other,
test_script,
enable_script,
test_res_body,
test_res_header
} = this.postman.state;
const { editCasename: casename } = this.state;
@ -116,27 +117,19 @@ export default class InterfaceCaseContent extends Component {
let params = {
id,
casename,
case_env,
path,
method,
case_env,
req_params,
req_query,
req_headers,
req_body_type,
req_body_form,
req_body_other,
mock_verify
test_script,
enable_script,
test_res_body,
test_res_header
};
if (this.postman.state.test_status !== 'error') {
params.test_res_body = this.postman.state.res;
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) {
@ -166,7 +159,7 @@ export default class InterfaceCaseContent extends Component {
render() {
const { currCase, currProject } = this.props;
const { isEditingCasename, editCasename } = this.state;
const data = Object.assign({}, currCase, currProject, { _id: currCase._id });
const data = Object.assign({}, currCase, {env: currProject.env}, { _id: currCase._id });
return (
<div style={{ padding: '6px 0' }} className="case-content">
<div className="case-title">
@ -182,7 +175,9 @@ export default class InterfaceCaseContent extends Component {
</span>
</div>
<div>
<Postman data={data} type="case" saveTip="更新保存修改" save={this.updateCase} ref={this.savePostmanRef} />
{Object.keys(currCase).length > 0 &&
<Postman data={data} type="case" saveTip="更新保存修改" save={this.updateCase} ref={this.savePostmanRef} />
}
</div>
</div>
)

View File

@ -8,27 +8,19 @@ import { Tooltip, Icon, Button, Spin, Modal, message, Select, Switch } from 'ant
import { fetchInterfaceColList, fetchCaseList, setColData } from '../../../../reducer/modules/interfaceCol'
import HTML5Backend from 'react-dnd-html5-backend';
import { DragDropContext } from 'react-dnd';
import { isJson, handleJson, handleParamsValue, joinPath } from '../../../../common.js'
import mockEditor from '../InterfaceList/mockEditor';
import { isJson, handleParamsValue } from '../../../../common.js'
import AceEditor from 'client/components/AceEditor/AceEditor';
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'
import _ from 'underscore'
import { handleParams, crossRequest } from 'client/components/Postman/postmanLib.js'
import { initCrossRequest } from 'client/components/Postman/CheckCrossInstall.js'
const MockExtra = require('common/mock-extra.js')
const Option = Select.Option;
function json_parse(data) {
try {
return json5.parse(data)
} catch (e) {
return data
}
}
function handleReport(json) {
try {
@ -113,28 +105,16 @@ class InterfaceColContent extends Component {
this.handleColdata(this.props.currCaseList)
}
let startTime = 0;
this.interval = setInterval(() => {
startTime += 500;
if (startTime > 5000) {
clearInterval(this.interval);
}
if (window.crossRequest) {
clearInterval(this.interval);
this.setState({
hasPlugin: true
})
} else {
this.setState({
hasPlugin: false
})
}
}, 500)
this._crossRequestInterval = initCrossRequest((hasPlugin) => {
this.setState({
hasPlugin: hasPlugin
})
});
}
componentWillUnmount() {
clearInterval(this.interval)
clearInterval(this._crossRequestInterval)
}
handleColdata = (rows) => {
@ -154,7 +134,7 @@ 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' });
curitem = Object.assign({}, rows[i],{env: this.props.currProject.env}, { test_status: 'loading' });
newRows = [].concat([], rows);
newRows[i] = curitem;
this.setState({
@ -193,92 +173,44 @@ class InterfaceColContent extends Component {
}
handleTest = async (interfaceData) => {
const { currProject } = this.props;
let requestParams = {};
let { case_env } = interfaceData;
let path = interfaceData.path;
interfaceData.req_params = interfaceData.req_params || [];
interfaceData.req_params.forEach(item => {
let val = this.handleValue(item.value);
requestParams[item.name] = val;
path = path.replace(`:${item.name}`, val || `:${item.name}`);
});
const domains = currProject.env.concat();
let options = handleParams(interfaceData, this.handleValue, requestParams)
case_env = this.state.currColEnv ? this.state.currColEnv : case_env;
let result = { code: 400,
msg: '数据异常',
validRes: [],
...options
};
let pathQuery = {};
let currDomain = _.find(domains, item => item.name === case_env);
if (!currDomain) {
currDomain = domains[0];
}
const urlObj = URL.parse(joinPath(currDomain.domain, path));
urlObj.query && urlObj.query.split('&').forEach(item => {
if (item) {
item = item.split('=');
pathQuery[item[0]] = item[1];
}
})
const href = URL.format({
protocol: urlObj.protocol || 'http',
host: urlObj.host,
pathname: urlObj.pathname,
query: Object.assign(pathQuery, this.getQueryObj(interfaceData.req_query, requestParams))
});
let result = { code: 400, msg: '数据异常', validRes: [] };
let that = this;
result.url = href;
result.method = interfaceData.method;
result.headers = that.getHeadersObj(interfaceData.req_headers);
if (interfaceData.req_body_type === 'form') {
result.body = that.arrToObj(interfaceData.req_body_form, requestParams)
} else {
let reqBody = isJson(interfaceData.req_body_other);
if (reqBody === false) {
result.body = this.handleValue(interfaceData.req_body_other)
} else {
reqBody = handleJson(reqBody, this.handleValue)
requestParams = Object.assign(requestParams, reqBody);
result.body = JSON.stringify(reqBody)
}
}
try {
let data = await this.crossRequest({
url: href,
method: interfaceData.method,
headers: that.getHeadersObj(interfaceData.req_headers),
data: result.body,
timeout: 8240000
})
let res = data.res.body = json_parse(data.res.body);
let header = data.res.header;
result.res_header = header;
result.res_body = res;
result.params = requestParams;
let validRes = [];
if (res && typeof res === 'object') {
if (interfaceData.mock_verify) {
let tpl = MockExtra(json_parse(interfaceData.res_body), {
query: interfaceData.req_query,
body: interfaceData.req_body_form
})
validRes = Mock.valid(tpl, res);
}
let data = await crossRequest(options)
let res = data.res.body = isJson(data.res.body);
result = {
...result,
res_header: data.res.header,
res_body: res
}
let validRes = [];
// 弃用 mock 字段验证功能
// if (res && typeof res === 'object') {
// if (interfaceData.mock_verify) {
// let tpl = MockExtra(json_parse(interfaceData.res_body), {
// query: interfaceData.req_query,
// body: interfaceData.req_body_form
// })
// validRes = Mock.valid(tpl, res);
// }
// }
let responseData = Object.assign({}, {
status: data.res.status,
body: res,
header: data.res.header,
statusText: data.res.statusText
})
await that.handleScriptTest(interfaceData, responseData, validRes, requestParams);
await this.handleScriptTest(interfaceData, responseData, validRes, requestParams);
if (validRes.length === 0) {
result.code = 0;
result.validRes = [{ message: '验证通过' }];
@ -286,41 +218,23 @@ class InterfaceColContent extends Component {
result.code = 1;
result.validRes = validRes;
}
return result;
} catch (data) {
if (data.err) {
data.err = data.err || '请求异常';
try {
data.err = json_parse(data.err);
} catch (e) {
console.log(e)
}
result.res_body = data.err;
result.res_header = data.header;
} else {
result.res_body = data.message;
result = {
...result,
res_header: data.header,
res_body: data.err || data.message,
status: null,
statusText: data.message,
code: 400
}
result.code = 400;
return result;
}
}
crossRequest = (options) => {
return new Promise((resolve, reject) => {
options.error = options.success = function (res, header, data) {
if(isNaN(data.res.status)){
reject({
err: res,
header
})
}
resolve(data);
}
window.crossRequest(options);
})
result.params = requestParams;
return result;
}
//response, validRes
@ -450,21 +364,12 @@ class InterfaceColContent extends Component {
curScript: findCase.test_script,
advVisible: true,
curCaseid: id
}, () => {
let that = this;
if (that.Editor) {
that.Editor.setValue(this.state.curScript);
} else {
that.Editor = mockEditor({
container: 'case-script',
data: this.state.curScript,
onChange: function (d) {
that.setState({
curScript: d.text
})
}
})
}
})
}
handleScriptChange = (d)=>{
this.setState({
curScript: d.text
})
}
@ -700,7 +605,8 @@ class InterfaceColContent extends Component {
是否开启:&nbsp;
<Switch checked={this.state.enableScript} onChange={e => this.setState({ enableScript: e })} />
</h3>
<div className="case-script" id="case-script" style={{ minHeight: 500 }}></div>
<AceEditor className="case-script" data={this.state.curScript} onChange={this.handleScriptChange} />
</Modal>
</div>
)

View File

@ -124,5 +124,6 @@
}
.case-script{
min-height: 500px;
margin: 10px
}

View File

@ -88,70 +88,3 @@
word-break: break-all;
}
// 容器左侧是header 右侧是body
.container-header-body {
display: flex;
padding-bottom: .36rem;
.header, .body {
flex: 1 0 300px;
.pretty-editor-header, .pretty-editor-body {
height: 100%;
}
.postman .pretty-editor-body {
min-height: 200px;
}
.ace_print-margin {
display: none;
}
}
.header {
max-width: 400px;
}
.container-title {
padding: .08rem 0;
}
.resizer {
flex: 0 0 21px;
position: relative;
&:after {
content: '';
display: block;
width: 1px;
height: 100%;
background-color: $color-text-dark;
opacity: .8;
position: absolute;
left: 10px;
}
}
// res body 无返回json时显示text信息
.res-body-text {
height: 100%;
border: 1px solid #d9d9d9;
border-radius: 4px;
padding: 8px;
}
}
.ant-spin-blur {
.res-code.success {
background-color: transparent;
}
.res-code.fail {
background-color: transparent;
}
}
.res-code {
padding: .08rem .28rem;
color: #fff;
margin-left: -.28rem;
margin-right: -.28rem;
transition: all .2s;
position: relative;
border-radius: 2px;
}
.res-code.success {
background-color: $color-antd-green;
}
.res-code.fail {
background-color: $color-antd-red;
}

View File

@ -9,6 +9,7 @@ import json5 from 'json5'
import { message, Tabs, Affix } from 'antd'
import Editor from 'wangeditor'
import EasyDragSort from '../../../../components/EasyDragSort/EasyDragSort.js'
import mockEditor from 'client/components/AceEditor/mockEditor';
const TabPane = Tabs.TabPane;
let EditFormContext;
@ -50,7 +51,7 @@ const dataTpl = {
req_body_form: { name: "", type: "text", required: "1", desc: "", example: "" }
}
const mockEditor = require('./mockEditor.js');
const HTTP_METHOD = constants.HTTP_METHOD;
const HTTP_METHOD_KEYS = Object.keys(HTTP_METHOD);
const HTTP_REQUEST_HEADER = constants.HTTP_REQUEST_HEADER;
@ -249,7 +250,8 @@ class InterfaceEditForm extends Component {
req_body_other: d.text
})
EditFormContext.props.changeEditStatus(initReqBody !== d.text);
}
},
fullScreen: true
})
this.resBodyEditor = mockEditor({
@ -260,7 +262,8 @@ class InterfaceEditForm extends Component {
res_body: d.text
});
EditFormContext.props.changeEditStatus(initResBody !== d.text);
}
},
fullScreen: true
})
this.mockPreview = mockEditor({
@ -771,11 +774,12 @@ class InterfaceEditForm extends Component {
<Row className={'interface-edit-item ' + (this.props.form.getFieldValue('req_body_type') === 'json' ? this.state.hideTabs.req.body : 'hide')}>
<Col className="interface-edit-json-info">
基于 Json5, 使用注释方式写参数说明 <Tooltip title={<pre>
基于 Json5, 参数描述信息用注释的方式实现 <Tooltip title={<pre>
{Json5Example}
</pre>}>
<Icon type="question-circle-o" style={{ color: "#086dbf" }} />
</Tooltip>
全局编辑 退出全屏 请按 F9
</Col>
<Col id="req_body_json" style={{ minHeight: "300px" }}>
</Col>
@ -836,7 +840,9 @@ class InterfaceEditForm extends Component {
{Json5Example}
</pre>}>
<Icon type="question-circle-o" style={{ color: "#086dbf" }} />
</Tooltip> ,具体使用方法请 <span className="href" onClick={() => window.open('http://yapi.qunar.com/mock.html', '_blank')}>查看文档</span></h3>
</Tooltip> ,具体使用方法请 <span className="href" onClick={() => window.open('http://yapi.qunar.com/mock.html', '_blank')}>查看文档</span>
全局编辑 退出全屏 请按 F9
</h3>
<div id="res_body_json" style={{ minHeight: "300px", display: this.state.jsonType === 'tpl' ? 'block' : 'none' }} ></div>
<div id="mock-preview" style={{ backgroundColor: "#eee", lineHeight: "20px", minHeight: "300px", display: this.state.jsonType === 'preview' ? 'block' : 'none' }}></div>
</div>

View File

@ -49,17 +49,13 @@ export default class Run extends Component {
const project_id = this.props.match.params.id;
const interface_id = this.props.currInterface._id;
const {
caseEnv: case_env,
pathname: path,
method,
pathParam: req_params,
query: req_query,
headers: req_headers,
bodyType: req_body_type,
bodyForm: req_body_form,
bodyOther: req_body_other,
resMockTest: mock_verify
case_env,
req_params,
req_query,
req_headers,
req_body_type,
req_body_form,
req_body_other
} = this.postman.state;
let params = {
@ -68,24 +64,14 @@ export default class Run extends Component {
col_id: colId,
project_id,
case_env,
path,
method,
req_params,
req_query,
req_headers,
req_body_type,
req_body_form,
req_body_other,
mock_verify
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, ' ');
}
@ -101,7 +87,10 @@ export default class Run extends Component {
render () {
const { currInterface, currProject } = this.props;
const data = Object.assign({}, currInterface, currProject, {_id: currInterface._id})
const data = Object.assign({}, currInterface, {
env: currProject.env
})
data.path = currProject.basepath + currInterface.path;
return (
<div>
<Postman data={data} type="inter" saveTip="保存到集合" save={() => this.setState({saveCaseModalVisible: true})} ref={this.savePostmanRef} />

View File

@ -1,33 +1,4 @@
.interface-test {
.has-plugin, .req-part, .resp-part {
margin-bottom: 16px;
}
.url {
display: flex;
margin-bottom: 24px;
}
.key-value-wrap {
display: flex;
align-items: center;
margin: 0 0 5px 0;
.key {
flex-basis: 220px;
}
.value {
flex-grow: 1;
}
.eq-symbol {
margin: 0 5px;
}
}
.icon-btn {
cursor: pointer;
margin-left: 6px;
}
.icon-btn:hover {
color: #2395f1;
}
}
.add-col-modal {
.col-list {
height: 200px;
@ -45,4 +16,4 @@
color: rgba(255, 255, 255, 1);
}
}
}
}

View File

@ -4,15 +4,14 @@ import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import { Table, Icon, Row, Col } from 'antd'
import { Link } from 'react-router-dom'
const mockEditor = require('./mockEditor.js')
import AceEditor from 'client/components/AceEditor/AceEditor';
import { formatTime } from '../../../../common.js';
import ErrMsg from '../../../../components/ErrMsg/ErrMsg.js';
import variable from '../../../../constants/variable';
import constants from '../../../../constants/variable.js'
const HTTP_METHOD = constants.HTTP_METHOD;
// import { Card } from 'antd'
// import { getMockUrl } from '../../reducer/modules/news.js'
@connect(state => {
return {
@ -89,34 +88,21 @@ class View extends Component {
}
return <div style={{ display: dataSource.length ? "" : "none" }} className="colBody">
<h3 className="col-title">Body</h3>
<Table bordered size="small" pagination={false} columns={columns} dataSource={dataSource} />
</div>
} else if (req_body_type === 'file') {
return <div style={{ display: this.props.curData.req_body_other ? "" : "none" }} className="colBody">
<h3 className="col-title">Body</h3>
<div>{this.props.curData.req_body_other}</div>
</div>
} else if (req_body_type === 'raw') {
return <div style={{ display: this.props.curData.req_body_other ? "" : "none" }} className="colBody">
<h3 className="col-title">Body</h3>
<div>{this.props.curData.req_body_other}</div>
</div>
}
}
res_body(res_body_type, res_body) {
if (res_body_type === 'json') {
let h = this.countEnter(this.props.curData.res_body);
let h = this.countEnter(res_body);
return <div className="colBody">
<div id="vres_body_json" style={{ minHeight: h * 16 + 100 }}></div>
{/* <div id="vres_body_json" style={{ minHeight: h * 16 + 100 }}></div> */}
<AceEditor data={res_body} readOnly={true} style={{ minHeight: h * 16 + 100 }} />
</div>
} else if (res_body_type === 'raw') {
return <div className="colBody">
<div>{res_body}</div>
<AceEditor data={res_body} readOnly={true} mode="text" />
</div>
}
}
@ -174,37 +160,13 @@ class View extends Component {
}
return c;
}
bindAceEditor() {
if (this.props.curData.req_body_type === "json" && this.props.curData.title) {
mockEditor({
container: 'vreq_body_json',
data: this.props.curData.req_body_other,
readOnly: true,
onChange: function () { }
})
}
if (this.props.curData.title && this.props.curData.res_body_type === "json") {
let content = this.props.curData.res_body ? this.props.curData.res_body: '没有定义';
mockEditor({
container: 'vres_body_json',
data: content,
readOnly: true,
onChange: function () { }
})
}
}
componentDidMount() {
if (this.props.curData.title) {
this.bindAceEditor.bind(this)();
}
componentDidMount() {
if (!this.props.curData.title && this.state.init) {
this.setState({ init: false });
}
}
componentDidUpdate() {
this.bindAceEditor.bind(this)();
}
componentWillUpdate() {
if (!this.props.curData.title && this.state.init) {
this.setState({ init: false });
@ -296,9 +258,8 @@ class View extends Component {
let methodColor = variable.METHOD_COLOR[this.props.curData.method ? this.props.curData.method.toLowerCase() : "get"];
// statusColor = statusColor[this.props.curData.status?this.props.curData.status.toLowerCase():"undone"];
let h = this.countEnter(this.props.curData.req_body_other);
const aceEditor = <div style={{ display: this.props.curData.req_body_other && this.props.curData.req_body_type === "json" ? "block" : "none" }} className="colBody">
<span className="colKey">请求Body</span>
<div id="vreq_body_json" style={{ minHeight: h * 16 + 20 }}></div>
const aceEditor = <div style={{ display: this.props.curData.req_body_other && (this.props.curData.req_body_type !== "form" ) ? "block" : "none" }} className="colBody">
<AceEditor data={this.props.curData.req_body_other} style={{ minHeight: h * 16 + 20 }} mode={this.props.curData.req_body_type === 'json' ? 'javascript' : 'text'} />
</div>
if (!methodColor) methodColor = "get";
let res = <div className="caseContainer">
@ -352,20 +313,15 @@ class View extends Component {
<h3 className="col-title">Query</h3>
{this.req_query(this.props.curData.req_query)}
</div> : ""}
{/*<div className="colreqBodyType">
<span className="colKey">请求Body类型</span>
<span className="colValue">{this.props.curData.req_body_type}</span>
</div>*/}
<div style={{display: this.props.curData.method && HTTP_METHOD[this.props.curData.method.toUpperCase()].request_body ? '' : 'none'}}>
<h3 className="col-title">Body:</h3>
{ aceEditor }
{
this.req_body_form(this.props.curData.req_body_type, this.props.curData.req_body_form)
}
</div>
{/*<div className="colreqBodyType">
<span className="colKey">返回Body类型</span>
<span className="colValue">{this.props.curData.res_body_type}</span>
</div>*/}
<h2 className="interface-title">Response</h2>
{this.res_body(this.props.curData.res_body_type, this.props.curData.res_body)}
</div>;

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types'
import { withRouter } from 'react-router-dom';
import { Form, Switch, Button, message, Icon, Tooltip, Radio } from 'antd';
import MockCol from './MockCol/MockCol.js'
import mockEditor from 'client/containers/Project/Interface/InterfaceList/mockEditor';
import mockEditor from 'client/components/AceEditor/mockEditor';
import constants from '../../client/constants/variable.js'
const FormItem = Form.Item;

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { Button, Form, Input, Switch, Select, Icon,message, Modal, Col, Row, InputNumber, AutoComplete } from 'antd';
import { safeAssign } from '../../../client/common.js';
import mockEditor from '../../../client/containers/Project/Interface/InterfaceList/mockEditor';
import constants from '../../../client/constants/variable.js'
import { safeAssign } from 'client/common.js';
import mockEditor from 'client/components/AceEditor/mockEditor';
import constants from 'client/constants/variable.js'
import { httpCodes } from '../index.js'
import { connect } from 'react-redux'

View File

@ -24,7 +24,7 @@ class interfaceModel extends baseModel {
path: String,
params: [{
name: String, value: String
}],
}]
},
req_query: [{
name: String, value: String, example: String, desc: String, required: {