import React, { PureComponent as Component } from 'react';
import PropTypes from 'prop-types';
import {
Button,
Input,
Checkbox,
Modal,
Select,
Spin,
Icon,
Collapse,
Tooltip,
Tabs,
Switch,
Row,
Col,
Alert
} from 'antd';
import constants from '../../constants/variable.js';
import AceEditor from 'client/components/AceEditor/AceEditor';
import _ from 'underscore';
import { isJson, deepCopyJson, json5_parse } from '../../common.js';
import axios from 'axios';
import ModalPostman from '../ModalPostman/index.js';
import CheckCrossInstall, { initCrossRequest } from './CheckCrossInstall.js';
import './Postman.scss';
import ProjectEnv from '../../containers/Project/Setting/ProjectEnv/index.js';
import json5 from 'json5';
const { handleParamsValue, ArrayToObject, schemaValidator } = require('common/utils.js');
const {
handleParams,
checkRequestBodyIsRaw,
handleContentType,
crossRequest,
checkNameIsExistInArray
} = require('common/postmanLib.js');
const plugin = require('client/plugin.js');
const createContext = require('common/createContext')
const HTTP_METHOD = constants.HTTP_METHOD;
const InputGroup = Input.Group;
const Option = Select.Option;
const Panel = Collapse.Panel;
export const InsertCodeMap = [
{
code: 'assert.equal(status, 200)',
title: '断言 httpCode 等于 200'
},
{
code: 'assert.equal(body.code, 0)',
title: '断言返回数据 code 是 0'
},
{
code: 'assert.notEqual(status, 404)',
title: '断言 httpCode 不是 404'
},
{
code: 'assert.notEqual(body.code, 40000)',
title: '断言返回数据 code 不是 40000'
},
{
code: 'assert.deepEqual(body, {"code": 0})',
title: '断言对象 body 等于 {"code": 0}'
},
{
code: 'assert.notDeepEqual(body, {"code": 0})',
title: '断言对象 body 不等于 {"code": 0}'
}
];
const ParamsNameComponent = props => {
const { example, desc, name } = props;
const isNull = !example && !desc;
const TooltipTitle = () => {
return (
{example && (
示例: {example}
)}
{desc && (
备注: {desc}
)}
);
};
return (
{isNull ? (
) : (
}>
)}
);
};
ParamsNameComponent.propTypes = {
example: PropTypes.string,
desc: PropTypes.string,
name: PropTypes.string
};
export default class Run extends Component {
static propTypes = {
data: PropTypes.object, //接口原有数据
save: PropTypes.func, //保存回调方法
type: PropTypes.string, //enum[case, inter], 判断是在接口页面使用还是在测试集
curUid: PropTypes.number.isRequired,
interfaceId: PropTypes.number.isRequired,
projectId: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.state = {
loading: false,
resStatusCode: null,
test_valid_msg: null,
resStatusText: null,
case_env: '',
mock_verify: false,
enable_script: false,
test_script: '',
hasPlugin: true,
inputValue: '',
cursurPosition: { row: 1, column: -1 },
envModalVisible: false,
test_res_header: null,
test_res_body: null,
autoPreviewHTML: true,
...this.props.data
};
}
get testResponseBodyIsHTML() {
const hd = this.state.test_res_header
return hd != null
&& typeof hd === 'object'
&& String(hd['Content-Type'] || hd['content-type']).indexOf('text/html') !== -1
}
checkInterfaceData(data) {
if (!data || typeof data !== 'object' || !data._id) {
return false;
}
return true;
}
// 整合header信息
handleReqHeader = (value, env) => {
let index = value
? env.findIndex(item => {
return item.name === value;
})
: 0;
index = index === -1 ? 0 : index;
let req_header = [].concat(this.props.data.req_headers || []);
let header = [].concat(env[index].header || []);
header.forEach(item => {
if (!checkNameIsExistInArray(item.name, req_header)) {
item = {
...item,
abled: true
};
req_header.push(item);
}
});
req_header = req_header.filter(item => {
return item && typeof item === 'object';
});
return req_header;
};
selectDomain = value => {
let headers = this.handleReqHeader(value, this.state.env);
this.setState({
case_env: value,
req_headers: headers
});
};
async initState(data) {
if (!this.checkInterfaceData(data)) {
return null;
}
const { req_body_other, req_body_type, req_body_is_json_schema } = data;
let body = req_body_other;
// 运行时才会进行转换
if (
this.props.type === 'inter' &&
req_body_type === 'json' &&
req_body_other &&
req_body_is_json_schema
) {
let schema = {};
try {
schema = json5.parse(req_body_other);
} catch (e) {
console.log('e', e);
return;
}
let result = await axios.post('/api/interface/schema2json', {
schema: schema,
required: true
});
body = JSON.stringify(result.data);
}
let example = {}
if(this.props.type === 'inter'){
example = ['req_headers', 'req_query', 'req_body_form'].reduce(
(res, key) => {
res[key] = (data[key] || []).map(item => {
if (
item.type !== 'file' // 不是文件类型
&& (item.value == null || item.value === '') // 初始值为空
&& item.example != null // 有示例值
) {
item.value = item.example;
}
return item;
})
return res;
},
{}
)
}
this.setState(
{
...this.state,
test_res_header: null,
test_res_body: null,
...data,
...example,
req_body_other: body,
resStatusCode: null,
test_valid_msg: null,
resStatusText: null
},
() => this.props.type === 'inter' && this.initEnvState(data.case_env, data.env)
);
}
initEnvState(case_env, env) {
let headers = this.handleReqHeader(case_env, env);
this.setState(
{
req_headers: headers,
env: env
},
() => {
let s = !_.find(env, item => item.name === this.state.case_env);
if (!this.state.case_env || s) {
this.setState({
case_env: this.state.env[0].name
});
}
}
);
}
UNSAFE_componentWillMount() {
this._crossRequestInterval = initCrossRequest(hasPlugin => {
this.setState({
hasPlugin: hasPlugin
});
});
this.initState(this.props.data);
}
componentWillUnmount() {
clearInterval(this._crossRequestInterval);
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.checkInterfaceData(nextProps.data) && this.checkInterfaceData(this.props.data)) {
if (nextProps.data._id !== this.props.data._id) {
this.initState(nextProps.data);
} else if (nextProps.data.interface_up_time !== this.props.data.interface_up_time) {
this.initState(nextProps.data);
}
if (nextProps.data.env !== this.props.data.env) {
this.initEnvState(this.state.case_env, nextProps.data.env);
}
}
}
handleValue(val, global) {
let globalValue = ArrayToObject(global);
return handleParamsValue(val, {
global: globalValue
});
}
onOpenTest = d => {
this.setState({
test_script: d.text
});
};
handleInsertCode = code => {
this.aceEditor.editor.insertCode(code);
};
handleRequestBody = d => {
this.setState({
req_body_other: d.text
});
};
reqRealInterface = async () => {
if (this.state.loading === true) {
this.setState({
loading: false
});
return null;
}
this.setState({
loading: true
});
let options = handleParams(this.state, this.handleValue),
result;
await plugin.emitHook('before_request', options, {
type: this.props.type,
caseId: options.caseId,
projectId: this.props.projectId,
interfaceId: this.props.interfaceId
});
try {
options.taskId = this.props.curUid;
result = await crossRequest(options, options.pre_script || this.state.pre_script, options.after_script || this.state.after_script, createContext(
this.props.curUid,
this.props.projectId,
this.props.interfaceId
));
await plugin.emitHook('after_request', result, {
type: this.props.type,
caseId: options.caseId,
projectId: this.props.projectId,
interfaceId: this.props.interfaceId
});
result = {
header: result.res.header,
body: result.res.body,
status: result.res.status,
statusText: result.res.statusText,
runTime: result.runTime
};
} catch (data) {
result = {
header: data.header,
body: data.body,
status: null,
statusText: data.message
};
}
if (this.state.loading === true) {
this.setState({
loading: false
});
} else {
return null;
}
let tempJson = result.body;
if (tempJson && typeof tempJson === 'object') {
result.body = JSON.stringify(tempJson, null, ' ');
this.setState({
res_body_type: 'json'
});
} else if (isJson(result.body)) {
this.setState({
res_body_type: 'json'
});
}
// 对 返回值数据结构 和定义的 返回数据结构 进行 格式校验
let validResult = this.resBodyValidator(this.props.data, result.body);
if (!validResult.valid) {
this.setState({ test_valid_msg: `返回参数 ${validResult.message}` });
} else {
this.setState({ test_valid_msg: '' });
}
this.setState({
resStatusCode: result.status,
resStatusText: result.statusText,
test_res_header: result.header,
test_res_body: result.body
});
};
// 返回数据与定义数据的比较判断
resBodyValidator = (interfaceData, test_res_body) => {
const { res_body_type, res_body_is_json_schema, res_body } = interfaceData;
let validResult = { valid: true };
if (res_body_type === 'json' && res_body_is_json_schema) {
const schema = json5_parse(res_body);
const params = json5_parse(test_res_body);
validResult = schemaValidator(schema, params);
}
return validResult;
};
changeParam = (name, v, index, key) => {
key = key || 'value';
const pathParam = deepCopyJson(this.state[name]);
pathParam[index][key] = v;
if (key === 'value') {
pathParam[index].enable = !!v;
}
this.setState({
[name]: pathParam
});
};
changeBody = (v, index, key) => {
const bodyForm = deepCopyJson(this.state.req_body_form);
key = key || 'value';
if (key === 'value') {
bodyForm[index].enable = !!v;
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({ req_body_form: bodyForm });
};
// 模态框的相关操作
showModal = (val, index, type) => {
let inputValue = '';
let cursurPosition;
if (type === 'req_body_other') {
// req_body
let editor = this.aceEditor.editor.editor;
cursurPosition = editor.session.doc.positionToIndex(editor.selection.getCursor());
// 获取选中的数据
inputValue = this.getInstallValue(val || '', cursurPosition).val;
} else {
// 其他input 输入
let oTxt1 = document.getElementById(`${type}_${index}`);
cursurPosition = oTxt1.selectionStart;
inputValue = this.getInstallValue(val || '', cursurPosition).val;
// cursurPosition = {row: 1, column: position}
}
this.setState({
modalVisible: true,
inputIndex: index,
inputValue,
cursurPosition,
modalType: type
});
};
// 点击插入
handleModalOk = val => {
const { inputIndex, modalType } = this.state;
if (modalType === 'req_body_other') {
this.changeInstallBody(modalType, val);
} else {
this.changeInstallParam(modalType, val, inputIndex);
}
this.setState({ modalVisible: false });
};
// 根据鼠标位置往req_body中动态插入数据
changeInstallBody = (type, value) => {
const pathParam = deepCopyJson(this.state[type]);
// console.log(pathParam)
let oldValue = pathParam || '';
let newValue = this.getInstallValue(oldValue, this.state.cursurPosition);
let left = newValue.left;
let right = newValue.right;
this.setState({
[type]: `${left}${value}${right}`
});
};
// 获取截取的字符串
getInstallValue = (oldValue, cursurPosition) => {
let left = oldValue.substr(0, cursurPosition);
let right = oldValue.substr(cursurPosition);
let leftPostion = left.lastIndexOf('{{');
let leftPostion2 = left.lastIndexOf('}}');
let rightPostion = right.indexOf('}}');
// console.log(leftPostion, leftPostion2,rightPostion, rightPostion2);
let val = '';
// 需要切除原来的变量
if (leftPostion !== -1 && rightPostion !== -1 && leftPostion > leftPostion2) {
left = left.substr(0, leftPostion);
right = right.substr(rightPostion + 2);
val = oldValue.substring(leftPostion, cursurPosition + rightPostion + 2);
}
return {
left,
right,
val
};
};
// 根据鼠标位置动态插入数据
changeInstallParam = (name, v, index, key) => {
key = key || 'value';
const pathParam = deepCopyJson(this.state[name]);
let oldValue = pathParam[index][key] || '';
let newValue = this.getInstallValue(oldValue, this.state.cursurPosition);
let left = newValue.left;
let right = newValue.right;
pathParam[index][key] = `${left}${v}${right}`;
this.setState({
[name]: pathParam
});
};
// 取消参数插入
handleModalCancel = () => {
this.setState({ modalVisible: false, cursurPosition: -1 });
};
// 环境变量模态框相关操作
showEnvModal = () => {
this.setState({
envModalVisible: true
});
};
handleEnvOk = (newEnv, index) => {
this.setState({
envModalVisible: false,
case_env: newEnv[index].name
});
};
handleEnvCancel = () => {
this.setState({
envModalVisible: false
});
};
render() {
const {
method,
env,
path,
req_params = [],
req_headers = [],
req_query = [],
req_body_type,
req_body_form = [],
loading,
case_env,
inputValue,
hasPlugin
} = this.state;
// console.log(env);
return (
{this.state.modalVisible && (
)}
{this.state.envModalVisible && (
)}
{
if (hasPlugin) {
return '发送请求';
} else {
return '请安装 cross-request 插件';
}
})()}
>
{
return this.props.type === 'inter' ? '保存到测试集' : '更新该用例';
}}
>
{req_params.map((item, index) => {
return (
);
})}
{req_query.map((item, index) => {
return (
);
})}
{req_headers.map((item, index) => {
return (
);
})}
BODY(F9)
}
key="3"
className={
HTTP_METHOD[method].request_body &&
((req_body_type === 'form' && req_body_form.length > 0) || req_body_type !== 'form')
? 'POST'
: 'hidden'
}
>
{req_body_type === 'json' && (
{' '}
)}
(this.aceEditor = editor)}
data={this.state.req_body_other}
mode={req_body_type === 'json' ? null : 'text'}
onChange={this.handleRequestBody}
fullScreen={true}
/>
{HTTP_METHOD[method].request_body &&
req_body_type === 'form' && (
{req_body_form.map((item, index) => {
return (
);
})}
)}
{HTTP_METHOD[method].request_body &&
req_body_type === 'file' && (
)}
= 200 &&
this.state.resStatusCode < 400 &&
!this.state.loading
? 'success'
: 'fail')
}
>
{this.state.resStatusCode + ' ' + this.state.resStatusText}
{this.state.test_valid_msg && (
Warning
}
type="warning"
showIcon
description={this.state.test_valid_msg}
/>
)}
Headers
{
editor.renderer.setShowGutter(false);
}}
readOnly={true}
className="pretty-editor-header"
data={this.state.test_res_header}
mode="json"
/>
Body
this.setState({ autoPreviewHTML: e.target.checked })}>
自动预览HTML
{
this.state.autoPreviewHTML && this.testResponseBodyIsHTML
?
:
}
{this.props.type === 'case' ? (
Test}
key="test"
>
是否开启:
this.setState({ enable_script: e })}
/>
注:Test 脚本只有做自动化测试才执行
{
this.aceEditor = aceEditor;
}}
/>
{InsertCodeMap.map(item => {
return (
{
this.handleInsertCode('\n' + item.code);
}}
>
{item.title}
);
})}
) : null}
);
}
}