feat: 增加版本通知和siderbar滚动条

This commit is contained in:
gandchuan 2018-07-10 17:22:35 +08:00
commit 4441f7e797
39 changed files with 1462 additions and 252 deletions

View File

@ -1,3 +1,14 @@
### v1.3.19
* 增加项目文档记录wiki
* 支持swagger URL 导入
* 接口运行和测试集合中加入参数备注信息
#### Bug Fixed
* 修复测试用例名称为空时保存测试用例出现的bug
### v1.3.18
* 增加全局接口搜索功能

View File

@ -1,26 +1,60 @@
<h1 class="curproject-name"> 导入导出测试 </h1>
<h1 class="curproject-name"> 跨项目测试集合 </h1>
# my-test
[TOC]
# %u516C%u5171%u5206%u7C7B
[TOC]
# www
[TOC]
## test%0A%3Ca%20id%3Dtest%3E%20%3C/a%3E
## test_2%0A%3Ca%20id%3Dtest_2%3E%20%3C/a%3E
[TOC]
### 基本信息
**Path** /api/col/list
**Path** /api/test/2
**Method** GET
**Method** POST
**接口描述:**
undefined
### 请求参数
**Headers**
| 参数名称 | 参数值 | 是否必须 | 示例 | 备注 |
| ------------ | ------------ | ------------ | ------------ | ------------ |
| Content-Type | application/json | 是 | | |
**Body**
```javascript
{
"aa": 12
}
```
### 返回数据
<table>
<thead class="ant-table-thead">
<tr>
<th key=name>名称</th><th key=type>类型</th><th key=required>是否必须</th><th key=default>默认值</th><th key=desc>备注</th><th key=sub>其他信息</th>
</tr>
</thead><tbody className="ant-table-tbody"><tr key=0-0><td key=0><span style="padding-left: 0px"><span style="color: #8c8a8a"></span> a</span></td><td key=1><span>object []</span></td><td key=2>必须</td><td key=3></td><td key=4><span></span></td><td key=5><p key=3><span style="font-weight: '700'">item 类型: </span><span>object</span></p></td></tr><tr key=0-0-0><td key=0><span style="padding-left: 20px"><span style="color: #8c8a8a">├─</span> item1</span></td><td key=1><span>string</span></td><td key=2>非必须</td><td key=3></td><td key=4><span></span></td><td key=5></td></tr><tr key=0-0-1><td key=0><span style="padding-left: 20px"><span style="color: #8c8a8a">├─</span> item2</span></td><td key=1><span>string</span></td><td key=2>非必须</td><td key=3></td><td key=4><span></span></td><td key=5></td></tr><tr key=0-1><td key=0><span style="padding-left: 0px"><span style="color: #8c8a8a"></span> b</span></td><td key=1><span>string</span></td><td key=2>必须</td><td key=3></td><td key=4><span></span></td><td key=5></td></tr>
</tbody>
</table>
# %u4EA4%u6613%u4E2D%u53F0
[TOC]
# %u60F9%u6211
[TOC]
# %u6258%u5C14%u65AF%u6CF0
[TOC]

View File

@ -66,6 +66,36 @@ const InsertCodeMap = [
}
];
const ParamsNameComponent = props => {
const { example, desc, name } = props;
const isNull = !example && !desc;
const TooltipTitle = () => {
return (
<div>
{example && <div>示例 {example}</div>}
{desc && <div>备注 {desc}</div>}
</div>
);
};
return (
<div>
{isNull ? (
<Input disabled value={name} className="key" />
) : (
<Tooltip placement="topLeft" title={<TooltipTitle />}>
<Input disabled value={name} className="key" />
</Tooltip>
)}
</div>
);
};
ParamsNameComponent.propTypes = {
example: PropTypes.string,
desc: PropTypes.string,
name: PropTypes.string
};
export default class Run extends Component {
static propTypes = {
data: PropTypes.object, //接口原有数据
@ -160,7 +190,6 @@ export default class Run extends Component {
required: true
});
body = JSON.stringify(result.data);
console.log('body', body);
}
this.setState(
@ -291,7 +320,6 @@ export default class Run extends Component {
res_body_type: 'json'
});
}
this.setState({
resStatusCode: result.status,
resStatusText: result.statusText,
@ -559,7 +587,13 @@ export default class Run extends Component {
{req_params.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} className="key" />
{/* <Tooltip
placement="topLeft"
title={<TooltipContent example={item.example} desc={item.desc} />}
>
<Input disabled value={item.name} className="key" />
</Tooltip> */}
<ParamsNameComponent example={item.example} desc={item.desc} name={item.name} />
<span className="eq-symbol">=</span>
<Input
value={item.value}
@ -594,7 +628,13 @@ export default class Run extends Component {
{req_query.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} className="key" />
{/* <Tooltip
placement="topLeft"
title={<TooltipContent example={item.example} desc={item.desc} />}
>
<Input disabled value={item.name} className="key" />
</Tooltip> */}
<ParamsNameComponent example={item.example} desc={item.desc} name={item.name} />
&nbsp;
{item.required == 1 ? (
<Checkbox className="params-enable" checked={true} disabled />
@ -632,7 +672,13 @@ export default class Run extends Component {
{req_headers.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} className="key" />
{/* <Tooltip
placement="topLeft"
title={<TooltipContent example={item.example} desc={item.desc} />}
>
<Input disabled value={item.name} className="key" />
</Tooltip> */}
<ParamsNameComponent example={item.example} desc={item.desc} name={item.name} />
<span className="eq-symbol">=</span>
<Input
value={item.value}
@ -682,7 +728,8 @@ export default class Run extends Component {
高级参数设置
</Button>
<Tooltip title="高级参数设置只在json字段值中生效">
{' '}<Icon type="question-circle-o" />
{' '}
<Icon type="question-circle-o" />
</Tooltip>
</div>
)}
@ -703,7 +750,17 @@ export default class Run extends Component {
{req_body_form.map((item, index) => {
return (
<div key={index} className="key-value-wrap">
<Input disabled value={item.name} className="key" />
{/* <Tooltip
placement="topLeft"
title={<TooltipContent example={item.example} desc={item.desc} />}
>
<Input disabled value={item.name} className="key" />
</Tooltip> */}
<ParamsNameComponent
example={item.example}
desc={item.desc}
name={item.name}
/>
&nbsp;
{item.required == 1 ? (
<Checkbox className="params-enable" checked={true} disabled />

View File

@ -16,13 +16,16 @@ import 'jsondiffpatch/public/formatters-styles/html.css';
import './TimeLine.scss';
const Option = AutoComplete.Option;
// const Option = AutoComplete.Option;
const {Option, OptGroup} = AutoComplete;
const AddDiffView = props => {
const { title, content, className } = props;
if (!content) {
return null;
}
return (
<div className={className}>
<h3 className="title">{title}</h3>
@ -93,8 +96,8 @@ function timeago(timestamp) {
};
},
{
fetchNewsData: fetchNewsData,
fetchMoreNews: fetchMoreNews,
fetchNewsData,
fetchMoreNews,
fetchInterfaceList
}
)
@ -121,7 +124,7 @@ class TimeTree extends Component {
curDiffData: {},
apiList: []
};
this.curInterfaceId = '';
this.curSelectValue = '';
}
getMore() {
@ -135,7 +138,7 @@ class TimeTree extends Component {
this.props.type,
this.props.curpage + 1,
10,
this.curInterfaceId
this.curSelectValue
)
.then(function() {
that.setState({ loading: false });
@ -176,9 +179,9 @@ class TimeTree extends Component {
});
}
handleSelectApi = interfaceId => {
this.curInterfaceId = interfaceId;
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 10, interfaceId);
handleSelectApi = selectValue => {
this.curSelectValue = selectValue;
this.props.fetchNewsData(this.props.typeid, this.props.type, 1, 10, selectValue);
};
render() {
@ -198,7 +201,7 @@ class TimeTree extends Component {
let methodColor = variable.METHOD_COLOR[item.method ? item.method.toLowerCase() : 'get'];
return (
<Option title={item.title} value={item._id + ''} path={item.path} key={item._id}>
{item.path}{' '}
{item.title}{' '}
<Tag
style={{ color: methodColor.color, backgroundColor: methodColor.bac, border: 'unset' }}
>
@ -217,7 +220,8 @@ class TimeTree extends Component {
if (data && data.length) {
data = data.map((item, i) => {
let interfaceDiff = false;
if (item.data && typeof item.data === 'object' && item.data.interface_id) {
// 去掉了 && item.data.interface_id
if (item.data && typeof item.data === 'object') {
interfaceDiff = true;
}
return (
@ -257,7 +261,8 @@ class TimeTree extends Component {
pending = <Spin />;
}
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, curDiffData);
return (
<section className="news-timeline">
<Modal
@ -290,12 +295,9 @@ class TimeTree extends Component {
onSelect={this.handleSelectApi}
style={{ width: '100%' }}
placeholder="Select Api"
optionLabelProp="path"
optionLabelProp="title"
filterOption={(inputValue, options) => {
if (options.props.value == '')
{
return true;
}
if (options.props.value == '') return true;
if (
options.props.path.indexOf(inputValue) !== -1 ||
options.props.title.indexOf(inputValue) !== -1
@ -305,7 +307,14 @@ class TimeTree extends Component {
return false;
}}
>
{children}
{/* {children} */}
<OptGroup label="other">
<Option value="wiki" path="" title="wiki">wiki</Option>
</OptGroup>
<OptGroup label="api">
{children}
</OptGroup>
</AutoComplete>
</Col>
</Row>

View File

@ -218,6 +218,7 @@ class InterfaceColContent extends Component {
result;
try {
result = await this.handleTest(curitem);
if (result.code === 400) {
status = 'error';
} else if (result.code === 0) {
@ -288,6 +289,7 @@ class InterfaceColContent extends Component {
}
);
await this.handleScriptTest(interfaceData, responseData, validRes, requestParams);
if (validRes.length === 0) {
result.code = 0;
result.validRes = [
@ -316,7 +318,9 @@ class InterfaceColContent extends Component {
};
//response, validRes
// 断言测试
handleScriptTest = async (interfaceData, response, validRes, requestParams) => {
// 是否启动断言
if (interfaceData.enable_script !== true) {
return null;
}

View File

@ -110,8 +110,8 @@ class Content extends Component {
document.getElementsByTagName('title')[0].innerText =
this.props.curdata.title + '-' + this.title;
}
let InterfaceTabs = {
let InterfaceTabs = {
view: {
component: View,
name: '预览'

View File

@ -22,7 +22,7 @@ require('tui-editor/dist/tui-editor.css'); // editor ui
require('tui-editor/dist/tui-editor-contents.css'); // editor content
require('highlight.js/styles/github.css'); // code block highlight
require('./editor.css');
var Editor = require('tui-editor');
var Editor = require('tui-editor');
function checkIsJsonSchema(json) {
try {

View File

@ -1,24 +1,24 @@
import React, { PureComponent as Component } from 'react'
import React, { PureComponent as Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types'
import PropTypes from 'prop-types';
import { Route, Switch, Redirect, matchPath } from 'react-router-dom';
import { Subnav } from '../../components/index';
import { fetchGroupMsg } from '../../reducer/modules/group';
import { setBreadcrumb } from '../../reducer/modules/user';
import { getProject } from '../../reducer/modules/project';
import Interface from './Interface/Interface.js'
import Activity from './Activity/Activity.js'
import Setting from './Setting/Setting.js'
import Interface from './Interface/Interface.js';
import Activity from './Activity/Activity.js';
import Setting from './Setting/Setting.js';
import Loading from '../../components/Loading/Loading';
import ProjectMember from './Setting/ProjectMember/ProjectMember.js';
import ProjectData from './Setting/ProjectData/ProjectData.js';
const plugin = require('client/plugin.js');
@connect(
state => {
return {
curProject: state.project.currProject,
currGroup: state.group.currGroup
}
};
},
{
getProject,
@ -26,9 +26,7 @@ import ProjectData from './Setting/ProjectData/ProjectData.js';
setBreadcrumb
}
)
export default class Project extends Component {
static propTypes = {
match: PropTypes.object,
curProject: PropTypes.object,
@ -37,80 +35,108 @@ export default class Project extends Component {
fetchGroupMsg: PropTypes.func,
setBreadcrumb: PropTypes.func,
currGroup: PropTypes.object
}
};
constructor(props) {
super(props)
super(props);
}
async componentWillMount() {
await this.props.getProject(this.props.match.params.id);
await this.props.fetchGroupMsg(this.props.curProject.group_id);
this.props.setBreadcrumb([{
name: this.props.currGroup.group_name,
href: '/group/' + this.props.currGroup._id
}, {
name: this.props.curProject.name
}]);
this.props.setBreadcrumb([
{
name: this.props.currGroup.group_name,
href: '/group/' + this.props.currGroup._id
},
{
name: this.props.curProject.name
}
]);
}
async componentWillReceiveProps(nextProps) {
const currProjectId = this.props.match.params.id;
const nextProjectId = nextProps.match.params.id;
if(currProjectId !== nextProjectId) {
if (currProjectId !== nextProjectId) {
await this.props.getProject(nextProjectId);
await this.props.fetchGroupMsg(this.props.curProject.group_id);
this.props.setBreadcrumb([{
name: this.props.currGroup.group_name,
href: '/group/' + this.props.currGroup._id
}, {
name: this.props.curProject.name
}]);
this.props.setBreadcrumb([
{
name: this.props.currGroup.group_name,
href: '/group/' + this.props.currGroup._id
},
{
name: this.props.curProject.name
}
]);
}
}
render() {
const { match, location } = this.props;
let routers = {
activity: { name: '动态', path: "/project/:id/activity" },
interface: { name: '接口', path: "/project/:id/interface/:action" },
setting: { name: '设置', path: "/project/:id/setting" },
members: { name: '成员管理', path: "/project/:id/members" },
data: { name: '数据管理', path: "/project/:id/data" }
}
interface: { name: '接口', path: '/project/:id/interface/:action', component: Interface },
activity: { name: '动态', path: '/project/:id/activity', component: Activity },
data: { name: '数据管理', path: '/project/:id/data', component: ProjectData },
members: { name: '成员管理', path: '/project/:id/members', component: ProjectMember },
setting: { name: '设置', path: '/project/:id/setting', component: Setting }
};
plugin.emitHook('sub_nav', routers);
let key, defaultName;
for (key in routers) {
if (matchPath(location.pathname, {
path: routers[key].path
}) !== null) {
if (
matchPath(location.pathname, {
path: routers[key].path
}) !== null
) {
defaultName = routers[key].name;
break;
}
}
let subnavData = [{
name: routers.interface.name,
path: `/project/${match.params.id}/interface/api`
}, {
name: routers.activity.name,
path: `/project/${match.params.id}/activity`
}, {
name: routers.data.name,
path: `/project/${match.params.id}/data`
}, {
name: routers.members.name,
path: `/project/${match.params.id}/members`
}, {
name: routers.setting.name,
path: `/project/${match.params.id}/setting`
}];
// let subnavData = [{
// name: routers.interface.name,
// path: `/project/${match.params.id}/interface/api`
// }, {
// name: routers.activity.name,
// path: `/project/${match.params.id}/activity`
// }, {
// name: routers.data.name,
// path: `/project/${match.params.id}/data`
// }, {
// name: routers.members.name,
// path: `/project/${match.params.id}/members`
// }, {
// name: routers.setting.name,
// path: `/project/${match.params.id}/setting`
// }];
let subnavData = [];
Object.keys(routers).forEach(key => {
let item = routers[key];
let value = {};
if (key === 'interface') {
value = {
name: item.name,
path: `/project/${match.params.id}/interface/api`
};
} else {
value = {
name: item.name,
path: item.path.replace(/\:id/gi, match.params.id)
};
}
subnavData.push(value);
});
if (this.props.currGroup.type === 'private') {
subnavData = subnavData.filter(item => {
return item.name != '成员管理'
})
return item.name != '成员管理';
});
}
if (Object.keys(this.props.curProject).length === 0) {
@ -119,22 +145,31 @@ export default class Project extends Component {
return (
<div>
<Subnav
default={defaultName}
data={subnavData} />
<Subnav default={defaultName} data={subnavData} />
<Switch>
<Redirect exact from="/project/:id" to={`/project/${match.params.id}/interface/api`} />
<Route path={routers.activity.path} component={Activity} />
<Route path={routers.interface.path} component={Interface} />
{/* <Route path={routers.activity.path} component={Activity} />
<Route path={routers.setting.path} component={Setting} />
{this.props.currGroup.type !== 'private' ?
<Route path={routers.members.path} component={ProjectMember} />
<Route path={routers.members.path} component={routers.members.component}/>
: null
}
<Route path={routers.data.path} component={ProjectData} />
<Route path={routers.data.path} component={ProjectData} /> */}
{Object.keys(routers).map(key => {
let item = routers[key];
return key === 'members' ? (
this.props.currGroup.type !== 'private' ? (
<Route path={item.path} component={item.component} key={key}/>
) : null
) : (
<Route path={item.path} component={item.component} key={key}/>
);
})}
</Switch>
</div>
)
);
}
}

View File

@ -1,5 +1,5 @@
import React, { PureComponent as Component } from 'react';
import { Upload, Icon, message, Select, Tooltip, Button, Spin, Switch, Modal, Radio } from 'antd';
import { Upload, Icon, message, Select, Tooltip, Button, Spin, Switch, Modal, Radio, Input } from 'antd';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import './ProjectData.scss';
@ -59,7 +59,9 @@ class ProjectData extends Component {
curExportType: null,
showLoading: false,
dataSync: false,
exportContent: 'all'
exportContent: 'all',
isSwaggerUrl: false,
swaggerUrl: ''
};
}
static propTypes = {
@ -116,6 +118,7 @@ class ProjectData extends Component {
);
};
// 本地文件上传
handleFile = info => {
if (!this.state.curImportType) {
return message.error('请选择导入数据的方式');
@ -198,12 +201,61 @@ class ProjectData extends Component {
});
};
// 处理导入信息同步
onChange = checked => {
this.setState({
dataSync: checked
});
};
// 处理swagger URL 导入
handleUrlChange = checked => {
this.setState({
isSwaggerUrl: checked
});
};
// 记录输入的url
swaggerUrlInput = url => {
this.setState({
swaggerUrl: url
});
};
// url导入上传
onUrlUpload = async () => {
if (!this.state.curImportType) {
return message.error('请选择导入数据的方式');
}
if(!this.state.swaggerUrl) {
return message.error('url 不能为空');
}
if (this.state.selectCatid) {
this.setState({ showLoading: true });
try {
let content = await axios(this.state.swaggerUrl);
content = content.data;
let res = await importDataModule[this.state.curImportType].run(content);
if (this.state.dataSync) {
// 开启同步
this.showConfirm(res);
} else {
// 未开启同步
await this.handleAddInterface(res);
}
} catch (e) {
this.setState({ showLoading: false });
message.error(e.message);
}
} else {
message.error('请选择上传的默认分类');
}
};
handleChange = e => {
this.setState({ exportContent: e.target.value });
};
@ -290,27 +342,51 @@ class ProjectData extends Component {
<Switch checked={this.state.dataSync} onChange={this.onChange} />
</div>
<div style={{ marginTop: 16, height: 180 }}>
<Spin spinning={this.state.showLoading} tip="上传中...">
<Dragger {...uploadMess}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">点击或者拖拽文件到上传区域</p>
<p
className="ant-upload-hint"
onClick={e => {
e.stopPropagation();
}}
dangerouslySetInnerHTML={{
__html: this.state.curImportType
? importDataModule[this.state.curImportType].desc
: null
}}
/>
</Dragger>
</Spin>
</div>
{this.state.curImportType === 'swagger' && (
<div className="dataSync">
<span>
开启url导入<Tooltip title="swagger url 导入">
<Icon type="question-circle-o" />
</Tooltip>{' '}
&nbsp;&nbsp;:
</span>
<Switch checked={this.state.isSwaggerUrl} onChange={this.handleUrlChange} />
</div>
)}
{this.state.isSwaggerUrl ? (
<div className="import-content url-import-content">
<Input
placeholder="http://demo.swagger.io/v2/swagger.json"
onChange={e => this.swaggerUrlInput(e.target.value)}
/>
<Button type="primary" className="url-btn" onClick={this.onUrlUpload} loading={this.state.showLoading}>
上传
</Button>
</div>
) : (
<div className="import-content">
<Spin spinning={this.state.showLoading} tip="上传中...">
<Dragger {...uploadMess}>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">点击或者拖拽文件到上传区域</p>
<p
className="ant-upload-hint"
onClick={e => {
e.stopPropagation();
}}
dangerouslySetInnerHTML={{
__html: this.state.curImportType
? importDataModule[this.state.curImportType].desc
: null
}}
/>
</Dragger>
</Spin>
</div>
)}
</div>
<div

View File

@ -35,6 +35,18 @@
}
}
.import-content {
margin-top: 16px;
height: 180px;
}
.url-import-content {
text-align: center;
.url-btn{
margin-top: 16px;
}
}
.export-content{
text-align: center;
}

View File

@ -170,7 +170,27 @@ hooks = {
type: 'listener',
mulit: true,
listener: []
},
/*
* 添加 subnav 钩子
* @param Object reducerModules
*
* let routers = {
interface: { name: '接口', path: "/project/:id/interface/:action", component:Interface },
activity: { name: '动态', path: "/project/:id/activity", component: Activity},
data: { name: '数据管理', path: "/project/:id/data", component: ProjectData},
members: { name: '成员管理', path: "/project/:id/members" , component: ProjectMember},
setting: { name: '设置', path: "/project/:id/setting" , component: Setting}
}
*
*/
sub_nav: {
type: 'listener',
mulit: true,
listener: []
}
};
function bindHook(name, listener) {

View File

@ -53,14 +53,14 @@ export default (state = initialState, action) => {
import axios from 'axios';
import variable from '../../constants/variable';
export function fetchNewsData(typeid, type, page, limit, interfaceId) {
export function fetchNewsData(typeid, type, page, limit, selectValue) {
let param = {
typeid: typeid,
type: type,
page: page,
limit: limit ? limit : variable.PAGE_LIMIT,
interface_id: interfaceId
};
selectValue
}
return {
type: FETCH_NEWS_DATA,
@ -69,14 +69,14 @@ export function fetchNewsData(typeid, type, page, limit, interfaceId) {
})
};
}
export function fetchMoreNews(typeid, type, page, limit, interfaceId) {
export function fetchMoreNews(typeid, type, page, limit, selectValue) {
const param = {
typeid: typeid,
type: type,
page: page,
limit: limit ? limit : variable.PAGE_LIMIT,
interface_id: interfaceId
};
selectValue
}
return {
type: FETCH_MORE_NEWS,
payload: axios.get('/api/log/list', {

View File

@ -13,5 +13,7 @@ module.exports = {
name: 'export-data'
},{
name: 'import-yapi-json'
},{
name: 'wiki'
}]
}

View File

@ -66,7 +66,17 @@ module.exports = function (jsondiffpatch, formattersHtml, curDiffData) {
if (curDiffData && typeof curDiffData === 'object' && curDiffData.current) {
const { current, old } = curDiffData;
const { current, old, type } = curDiffData;
// wiki 信息的diff 输出
if(type === 'wiki') {
if (current != old) {
diffView.push({
title: 'wiki更新',
content: diffText(old, current)
})
}
return diffView = diffView.filter(item => item.content)
}
if (current.path != old.path) {
diffView.push({
title: 'Api 路径',

View File

@ -169,4 +169,5 @@ exports.json_format= function(json){
}catch(e){
return json;
}
}
}

View File

@ -5,7 +5,7 @@
YApi 为了解决这个问题,开发了可视化接口自动化测试功能,只需要配置每个接口的入参和对 RESPONSE 断言,即可实现对接口的自动化测试,大大提升了接口测试的效率。
## 第一步,测试集合
使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。
使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)
![](case-col.png)

View File

@ -43,13 +43,15 @@ module.exports = function(){
let caseInst = yapi.getInst(caseModel);
// let ip = ctx.ip.match(/\d+.\d+.\d+.\d+/)[0];
// request.ip
let ip = yapi.commons.getIp(ctx)
// 数据库信息查询
let listWithIp =await caseInst.model.find({
interface_id: interfaceId,
ip_enable: true,
ip: ip
}).select('_id params');
}).select('_id params');
let matchList = [];
listWithIp.forEach(item=>{
let params = item.params;
@ -158,8 +160,10 @@ module.exports = function(){
this.bindHook('mock_after', async function(context){
let interfaceId = context.interfaceData._id;
let caseData = await checkCase(context.ctx, interfaceId);
if(caseData){
let data = await handleByCase(caseData);
context.mockJson = yapi.commons.json_parse(data.res_body);
try{
context.mockJson = Mock.mock(mockExtra(context.mockJson, {

View File

@ -0,0 +1,12 @@
import WikiPage from './wikiPage/index'
// const WikiPage = require('./wikiPage/index')
module.exports = function(){
this.bindHook('sub_nav', function(app){
app.wiki = {
name: 'Wiki',
path: '/project/:id/wiki',
component: WikiPage
}
})
}

View File

@ -0,0 +1,222 @@
const baseController = require('controllers/base.js');
const wikiModel = require('./wikiModel.js');
const projectModel = require('models/project.js');
const userModel = require('models/user.js');
const jsondiffpatch = require('jsondiffpatch');
const formattersHtml = jsondiffpatch.formatters.html;
const yapi = require('yapi.js');
// const util = require('./util.js');
const fs = require('fs-extra');
const path = require('path');
const showDiffMsg = require('../../common/diff-view.js');
class wikiController extends baseController {
constructor(ctx) {
super(ctx);
this.Model = yapi.getInst(wikiModel);
this.projectModel = yapi.getInst(projectModel);
}
/**
* 获取wiki信息
* @interface wiki_desc/get
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async getWikiDesc(ctx) {
try {
let project_id = ctx.request.query.project_id;
if (!project_id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
}
let result = await this.Model.get(project_id);
return (ctx.body = yapi.commons.resReturn(result));
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
/**
* 保存wiki信息
* @interface wiki_desc/get
* @method get
* @category statistics
* @foldnumber 10
* @returns {Object}
*/
async uplodaWikiDesc(ctx) {
try {
let params = ctx.request.body;
params = yapi.commons.handleParams(params, {
project_id: 'number',
desc: 'string',
markdown: 'string'
});
if (!params.project_id) {
return (ctx.body = yapi.commons.resReturn(null, 400, '项目id不能为空'));
}
if (!this.$tokenAuth) {
let auth = await this.checkAuth(params.project_id, 'project', 'edit');
if (!auth) {
return (ctx.body = yapi.commons.resReturn(null, 400, '没有权限'));
}
}
let notice = params.email_notice;
delete params.email_notice;
const username = this.getUsername();
const uid = this.getUid();
// 如果当前数据库里面没有数据
let result = await this.Model.get(params.project_id);
if (!result) {
let data = Object.assign(params, {
username,
uid,
add_time: yapi.commons.time(),
up_time: yapi.commons.time()
});
let res = await this.Model.save(data);
ctx.body = yapi.commons.resReturn(res);
} else {
let data = Object.assign(params, {
username,
uid,
up_time: yapi.commons.time()
});
let upRes = await this.Model.up(result._id, data);
ctx.body = yapi.commons.resReturn(upRes);
}
let logData = {
type: 'wiki',
project_id: params.project_id,
current: params.desc,
old: result ? result.toObject().desc : ''
};
let wikiUrl = `http://${ctx.request.host}/project/${params.project_id}/wiki`;
if (notice) {
let diffView = showDiffMsg(jsondiffpatch, formattersHtml, logData);
let annotatedCss = fs.readFileSync(
path.resolve(
yapi.WEBROOT,
'node_modules/jsondiffpatch/public/formatters-styles/annotated.css'
),
'utf8'
);
let htmlCss = fs.readFileSync(
path.resolve(
yapi.WEBROOT,
'node_modules/jsondiffpatch/public/formatters-styles/html.css'
),
'utf8'
);
let project = await this.projectModel.getBaseInfo(params.project_id);
yapi.commons.sendNotice(params.project_id, {
title: `${username} 更新了wiki说明`,
content: `<html>
<head>
<meta charset="utf-8" />
<style>
${annotatedCss}
${htmlCss}
</style>
</head>
<body>
<div><h3>${username}更新了wiki说明</h3>
<p>修改用户: ${username}</p>
<p>修改项目: <a href="${wikiUrl}">${project.name}</a></p>
<p>详细改动日志: ${this.diffHTML(diffView)}</p></div>
</body>
</html>`
});
}
// 保存修改日志信息
yapi.commons.saveLog({
content: `<a href="/user/profile/${uid}">${username}</a> 更新了 <a href="${wikiUrl}">wiki</a> 的信息`,
type: 'project',
uid,
username: username,
typeid: params.project_id,
data: logData
});
return 1;
} catch (err) {
ctx.body = yapi.commons.resReturn(null, 400, err.message);
}
}
diffHTML(html) {
if (html.length === 0) {
return `<span style="color: #555">没有改动该操作未改动wiki数据</span>`;
}
return html.map(item => {
return `<div>
<h4 class="title">${item.title}</h4>
<div>${item.content}</div>
</div>`;
});
}
// 处理编辑冲突
async wikiConflict(ctx) {
try {
let result;
ctx.websocket.on('message', async message => {
let id = parseInt(ctx.query.id, 10);
if (!id) {
return ctx.websocket.send('id 参数有误');
}
result = await this.Model.get(id);
console.log(message);
switch (message) {
case 'start':
if (result && result.edit_uid === this.getUid()) {
await this.Model.upEditUid(result._id, 0);
}
break;
case 'editor':
let userInst, userinfo, data;
if (result && result.edit_uid !== 0 && result.edit_uid !== this.getUid()) {
userInst = yapi.getInst(userModel);
userinfo = await userInst.findById(result.edit_uid);
data = {
errno: result.edit_uid,
data: { uid: result.edit_uid, username: userinfo.username }
};
} else {
if (result) {
await this.Model.upEditUid(result._id, this.getUid());
}
data = {
errno: 0,
data: result
};
}
ctx.websocket.send(JSON.stringify(data));
break;
case 'end':
await this.Model.upEditUid(result._id, 0);
break;
default:
break;
}
});
ctx.websocket.on('close', async () => {});
} catch (err) {
yapi.commons.log(err, 'error');
}
}
}
module.exports = wikiController;

View File

@ -0,0 +1,64 @@
module.exports = {
server: true,
client: true,
httpCodes: [
100,
101,
102,
200,
201,
202,
203,
204,
205,
206,
207,
208,
226,
300,
301,
302,
303,
304,
305,
307,
308,
400,
401,
402,
403,
404,
405,
406,
407,
408,
409,
410,
411,
412,
413,
414,
415,
416,
417,
418,
422,
423,
424,
426,
428,
429,
431,
500,
501,
502,
503,
504,
505,
506,
507,
508,
510,
511
]
};

View File

@ -0,0 +1,39 @@
const yapi = require('yapi.js');
const mongoose = require('mongoose');
const controller = require('./controller');
module.exports = function() {
yapi.connect.then(function() {
let Col = mongoose.connection.db.collection('wiki');
Col.createIndex({
project_id: 1
});
});
this.bindHook('add_router', function(addRouter) {
addRouter({
// 获取wiki信息
controller: controller,
method: 'get',
path: 'wiki_desc/get',
action: 'getWikiDesc'
});
addRouter({
// 更新wiki信息
controller: controller,
method: 'post',
path: 'wiki_desc/up',
action: 'uplodaWikiDesc'
});
});
this.bindHook('add_ws_router', function(wsRouter) {
wsRouter({
controller: controller,
method: 'get',
path: 'wiki_desc/solve_conflict',
action: 'wikiConflict'
});
});
};

View File

@ -0,0 +1,74 @@
// 时间
const convert2Decimal = num => (num > 9 ? num : `0${num}`);
/**
* 格式化
* @param val {Object or String or Number} 日期对象 或是可new Date的对象或时间戳
* @return {String} 2017-01-20 20:00:00
*/
exports.formatDate = val => {
let date = val;
if (typeof val !== 'object') {
date = new Date(val);
}
return `${[
date.getFullYear(),
convert2Decimal(date.getMonth() + 1),
convert2Decimal(date.getDate())
].join('-')} ${[
convert2Decimal(date.getHours()),
convert2Decimal(date.getMinutes()),
convert2Decimal(date.getSeconds())
].join(':')}`;
};
// const json5_parse = require('../client/common.js').json5_parse;
exports.timeago = timestamp => {
let minutes, hours, days, seconds, mouth, year;
const timeNow = parseInt(new Date().getTime() / 1000);
seconds = timeNow - timestamp;
if (seconds > 86400 * 30 * 12) {
year = parseInt(seconds / (86400 * 30 * 12));
} else {
year = 0;
}
if (seconds > 86400 * 30) {
mouth = parseInt(seconds / (86400 * 30));
} else {
mouth = 0;
}
if (seconds > 86400) {
days = parseInt(seconds / 86400);
} else {
days = 0;
}
if (seconds > 3600) {
hours = parseInt(seconds / 3600);
} else {
hours = 0;
}
minutes = parseInt(seconds / 60);
if (year > 0) {
return year + '年前';
} else if (mouth > 0 && year <= 0) {
return mouth + '月前';
} else if (days > 0 && mouth <= 0) {
return days + '天前';
} else if (days <= 0 && hours > 0) {
return hours + '小时前';
} else if (hours <= 0 && minutes > 0) {
return minutes + '分钟前';
} else if (minutes <= 0 && seconds > 0) {
if (seconds < 30) {
return '刚刚';
} else {
return seconds + '秒前';
}
} else {
return '刚刚';
}
};

View File

@ -0,0 +1,58 @@
const yapi = require('yapi.js');
const baseModel = require('models/base.js');
class statisMockModel extends baseModel {
getName() {
return 'wiki';
}
getSchema() {
return {
project_id: { type: Number, required: true },
username: String,
uid: { type: Number, required: true },
edit_uid: { type: Number, default: 0 },
desc: String,
markdown: String,
add_time: Number,
up_time: Number
};
}
save(data) {
let m = new this.model(data);
return m.save();
}
get(project_id) {
return this.model.findOne({
project_id: project_id
})
.exec();
}
up(id, data) {
return this.model.update(
{
_id: id
},
data,
{ runValidators: true }
);
}
upEditUid(id, uid) {
return this.model.update({
_id: id
},
{ edit_uid: uid },
{ runValidators: true });
}
}
module.exports = statisMockModel;

View File

@ -0,0 +1,71 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Checkbox } from 'antd';
// deps for editor
require('codemirror/lib/codemirror.css'); // codemirror
require('tui-editor/dist/tui-editor.css'); // editor ui
require('tui-editor/dist/tui-editor-contents.css'); // editor content
require('highlight.js/styles/github.css'); // code block highlight
var Editor = require('tui-editor');
class WikiEditor extends Component {
constructor(props) {
super(props);
}
static propTypes = {
isConflict: PropTypes.bool,
onUpload: PropTypes.func,
onCancel: PropTypes.func,
notice: PropTypes.bool,
onEmailNotice: PropTypes.func,
desc: PropTypes.string
};
componentDidMount() {
this.editor = new Editor({
el: document.querySelector('#desc'),
initialEditType: 'wysiwyg',
height: '500px',
initialValue: this.props.desc
});
}
onUpload = () => {
let desc = this.editor.getHtml();
let markdown = this.editor.getMarkdown();
this.props.onUpload(desc, markdown);
};
render() {
const { isConflict, onCancel, notice, onEmailNotice } = this.props;
return (
<div>
<div
id="desc"
className="wiki-editor"
style={{ display: !isConflict ? 'block' : 'none' }}
/>
<div className="wiki-title wiki-up">
<Button
icon="upload"
type="primary"
className="upload-btn"
disabled={isConflict}
onClick={this.onUpload}
>
更新
</Button>
<Button onClick={onCancel} className="upload-btn">
取消
</Button>
<Checkbox checked={notice} onChange={onEmailNotice}>
通知相关人员
</Checkbox>
</div>
</div>
);
}
}
export default WikiEditor;

View File

@ -0,0 +1,41 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button } from 'antd';
import { Link } from 'react-router-dom';
const WikiView = props => {
const { editorEable, onEditor, uid, username, editorTime, desc } = props;
return (
<div className="wiki-view-content">
<div className="wiki-title">
<Button icon="edit" onClick={onEditor} disabled={!editorEable}>
编辑
</Button>
{username && (
<div className="wiki-user">
{' '}
<Link className="user-name" to={`/user/profile/${uid || 11}`}>
{username}
</Link>{' '}
修改于 {editorTime}
</div>
)}
</div>
<div
className="tui-editor-contents"
dangerouslySetInnerHTML={{ __html: desc }}
/>
</div>
);
};
WikiView.propTypes = {
editorEable: PropTypes.bool,
onEditor: PropTypes.func,
uid: PropTypes.number,
username: PropTypes.string,
editorTime: PropTypes.string,
desc: PropTypes.string
};
export default WikiView;

View File

@ -0,0 +1,255 @@
import React, { Component } from 'react';
import { message } from 'antd';
import { connect } from 'react-redux';
import axios from 'axios';
import PropTypes from 'prop-types';
import './index.scss';
import { timeago } from '../util.js';
import { Link } from 'react-router-dom';
import WikiView from './View.js';
import WikiEditor from './Editor.js';
@connect(
state => {
return {
projectMsg: state.project.currProject
};
},
{}
)
class WikiPage extends Component {
constructor(props) {
super(props);
this.state = {
isEditor: false,
isUpload: true,
desc: '',
markdown: '',
notice: props.projectMsg.switch_notice,
status: 'INIT',
editUid: '',
editName: '',
curdata: null
};
}
static propTypes = {
match: PropTypes.object,
projectMsg: PropTypes.object
};
async componentDidMount() {
const currProjectId = this.props.match.params.id;
await this.handleData({ project_id: currProjectId });
this.handleConflict();
}
componentWillUnmount() {
// willUnmount
try {
if (this.state.status === 'CLOSE') {
this.WebSocket.send('end');
this.WebSocket.close();
}
} catch (e) {
return null;
}
}
// 结束编辑websocket
endWebSocket = () => {
try {
if (this.state.status === 'CLOSE') {
const sendEnd = () => {
this.WebSocket.send('end');
};
this.handleWebsocketAccidentClose(sendEnd);
}
} catch (e) {
return null;
}
};
// 处理多人编辑冲突问题
handleConflict = () => {
// console.log(location)
let domain = location.hostname + (location.port !== '' ? ':' + location.port : '');
let s;
//因后端 node 仅支持 ws 暂不支持 wss
let wsProtocol = location.protocol === 'https' ? 'ws' : 'ws';
s = new WebSocket(
wsProtocol +
'://' +
domain +
'/api/ws_plugin/wiki_desc/solve_conflict?id=' +
this.props.match.params.id
);
s.onopen = () => {
this.WebSocket = s;
s.send('start');
};
s.onmessage = e => {
let result = JSON.parse(e.data);
if (result.errno === 0) {
// 更新
if (result.data) {
this.setState({
// curdata: result.data,
desc: result.data.desc,
username: result.data.username,
uid: result.data.uid,
editorTime: timeago(result.data.up_time)
});
}
// 新建
this.setState({
isEditor: !this.state.isEditor,
status: 'CLOSE'
});
} else {
this.setState({
editUid: result.data.uid,
editName: result.data.username,
status: 'EDITOR'
});
}
};
s.onerror = () => {
this.setState({
status: 'CLOSE'
});
console.warn('websocket 连接失败,将导致多人编辑同一个接口冲突。');
};
};
// 点击编辑按钮 发送 websocket 获取数据
onEditor = () => {
// this.WebSocket.send('editor');
const sendEditor = () => {
this.WebSocket.send('editor');
};
this.handleWebsocketAccidentClose(sendEditor, status => {
// 如果websocket 启动不成功用户依旧可以对wiki 进行编辑
if (!status) {
this.setState({
isEditor: !this.state.isEditor
});
}
});
};
// 处理websocket 意外断开问题
handleWebsocketAccidentClose = (fn, callback) => {
// websocket 是否启动
if (this.WebSocket) {
// websocket 断开
if (this.WebSocket.readyState !== 1) {
message.error('websocket 链接失败,请重新刷新页面');
} else {
fn();
}
callback(true);
} else {
callback(false);
}
};
// 获取数据
handleData = async params => {
let result = await axios.get('/api/plugin/wiki_desc/get', { params });
if (result.data.errcode === 0) {
const data = result.data.data;
if (data) {
this.setState({
desc: data.desc,
markdown: data.markdown,
username: data.username,
uid: data.uid,
editorTime: timeago(data.up_time)
});
}
} else {
message.error(`请求数据失败: ${result.data.errmsg}`);
}
};
// 数据上传
onUpload = async (desc, markdown) => {
const currProjectId = this.props.match.params.id;
let option = {
project_id: currProjectId,
desc,
markdown,
email_notice: this.state.notice
};
let result = await axios.post('/api/plugin/wiki_desc/up', option);
if (result.data.errcode === 0) {
await this.handleData({ project_id: currProjectId });
this.setState({ isEditor: false });
} else {
message.error(`更新失败: ${result.data.errmsg}`);
}
this.endWebSocket();
// this.WebSocket.send('end');
};
// 取消编辑
onCancel = () => {
this.setState({ isEditor: false });
this.endWebSocket();
};
// 邮件通知
onEmailNotice = e => {
this.setState({
notice: e.target.checked
});
};
render() {
const { isEditor, username, editorTime, notice, uid, status, editUid, editName } = this.state;
const editorEable =
this.props.projectMsg.role === 'admin' ||
this.props.projectMsg.role === 'owner' ||
this.props.projectMsg.role === 'dev';
const isConflict = status === 'EDITOR';
return (
<div className="g-row">
<div className="m-panel wiki-content">
<div className="wiki-content">
{isConflict && (
<div className="wiki-conflict">
<Link to={`/user/profile/${editUid || uid}`}>
<b>{editName || username}</b>
</Link>
<span>正在编辑该wiki请稍后再试...</span>
</div>
)}
</div>
{!isEditor ? (
<WikiView
editorEable={editorEable}
onEditor={this.onEditor}
uid={uid}
username={username}
editorTime={editorTime}
desc={this.state.desc}
/>
) : (
<WikiEditor
isConflict={isConflict}
onUpload={this.onUpload}
onCancel={this.onCancel}
notice={notice}
onEmailNotice={this.onEmailNotice}
desc={this.state.desc}
/>
)}
</div>
</div>
);
}
}
export default WikiPage;

View File

@ -0,0 +1,27 @@
.wiki-content {
.wiki-user {
padding-top: 8px;
}
.wiki-editor {
padding-top: 16px;
}
.upload-btn {
margin-right: 16px;
}
.wiki-conflict {
text-align: center;
font-size: 14px;
padding-top: 10px;
}
.wiki-up {
text-align: right;
padding-top: 16px;
}
.wiki-title {
padding-bottom: 16px;
}
}

View File

@ -740,7 +740,7 @@ class interfaceController extends baseController {
ctx.body = yapi.commons.resReturn(null, 402, err.message);
}
}
// 处理编辑冲突
async solveConflict(ctx) {
try {
let id = parseInt(ctx.query.id, 10),
@ -1066,10 +1066,10 @@ class interfaceController extends baseController {
async schema2json(ctx) {
let schema = ctx.request.body.schema;
let required = ctx.request.body.required;
let res = yapi.commons.schemaToJson(schema, {
alwaysFakeOptionals: required ? true : false
});
alwaysFakeOptionals: _.isUndefined(required) ? true : require
})
// console.log('res',res)
return (ctx.body = res);
}

View File

@ -559,6 +559,10 @@ class interfaceColController extends baseController {
return (ctx.body = yapi.commons.resReturn(null, 400, '用例id不能为空'));
}
if (!params.casename) {
return (ctx.body = yapi.commons.resReturn(null, 400, '用例名称不能为空'));
}
let caseData = await this.caseModel.get(params.id);
let auth = await this.checkAuth(caseData.project_id, 'project', 'edit');
if (!auth) {
@ -671,7 +675,7 @@ class interfaceColController extends baseController {
let result = await this.colModel.up(id, params);
let username = this.getUsername();
yapi.commons.saveLog({
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了接口集 <a href="/project/${
content: `<a href="/user/profile/${this.getUid()}">${username}</a> 更新了测试集合 <a href="/project/${
colData.project_id
}/interface/col/${id}">${colData.name}</a> `,
type: 'project',

View File

@ -44,7 +44,7 @@ class logController extends baseController {
page = ctx.request.query.page || 1,
limit = ctx.request.query.limit || 10,
type = ctx.request.query.type,
interfaceId = ctx.request.query.interface_id;
selectValue = ctx.request.query.selectValue;
if (!typeid) {
return (ctx.body = yapi.commons.resReturn(null, 400, 'typeid不能为空'));
}
@ -80,9 +80,9 @@ class logController extends baseController {
list: projectLogList,
total: Math.ceil(total / limit)
});
} else if (type === 'project') {
let result = await this.Model.listWithPaging(typeid, type, page, limit, interfaceId);
let count = await this.Model.listCount(typeid, type, interfaceId);
} else if (type === "project") {
let result = await this.Model.listWithPaging(typeid, type, page, limit, selectValue);
let count = await this.Model.listCount(typeid, type, selectValue);
ctx.body = yapi.commons.resReturn({
total: Math.ceil(count / limit),

View File

@ -936,9 +936,10 @@ class projectController extends baseController {
return (ctx.body = yapi.commons.resReturn(null, 405, '项目id不能为空'));
}
if ((await this.checkAuth(project_id, 'project', 'edit')) !== true) {
return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
}
// 去掉权限判断
// if ((await this.checkAuth(project_id, 'project', 'edit')) !== true) {
// return (ctx.body = yapi.commons.resReturn(null, 405, '没有权限'));
// }
let env = await this.Model.getByEnv(project_id);
// console.log('project', projectData)

View File

@ -18,6 +18,11 @@ class interfaceColController extends baseController {
async testGet(ctx) {
try {
let query = ctx.query;
// cookie 检测
ctx.cookies.set('_uid', 12, {
expires: yapi.commons.expireDate(7),
httpOnly: true
});
ctx.body = yapi.commons.resReturn(query);
} catch (e) {
ctx.body = yapi.commons.resReturn(null, 402, e.message);

View File

@ -218,7 +218,9 @@ module.exports = async (ctx, next) => {
if (interfaceData.res_body_is_json_schema === true) {
//json-schema
const schema = yapi.commons.json_parse(interfaceData.res_body);
res = yapi.commons.schemaToJson(schema);
res = yapi.commons.schemaToJson(schema, {
alwaysFakeOptionals: true
});
} else {
// console.log('header', ctx.request.header['content-type'].indexOf('multipart/form-data'))
// 处理 format-data

View File

@ -4,118 +4,152 @@ var mongoose = require('mongoose');
var Schema = mongoose.Schema;
class logModel extends baseModel {
getName() {
return 'log';
getName() {
return 'log';
}
getSchema() {
return {
uid: { type: Number, required: true },
typeid: { type: Number, required: true },
type: {
type: String,
enum: ['user', 'group', 'interface', 'project', 'other', 'interface_col'],
required: true
},
content: { type: String, required: true },
username: { type: String, required: true },
add_time: Number,
data: Schema.Types.Mixed //用于原始数据存储
};
}
/**
* @param {String} content log内容
* @param {Enum} type log类型 ['user', 'group', 'interface', 'project', 'other']
* @param {Number} uid 用户id
* @param {String} username 用户名
* @param {Number} typeid 类型id
* @param {Number} add_time 时间
*/
save(data) {
let saveData = {
content: data.content,
type: data.type,
uid: data.uid,
username: data.username,
typeid: data.typeid,
add_time: yapi.commons.time(),
data: data.data
};
let log = new this.model(saveData);
return log.save();
}
del(id) {
return this.model.remove({
_id: id
});
}
list(typeid, type) {
return this.model
.find({
typeid: typeid,
type: type
})
.exec();
}
listWithPaging(typeid, type, page, limit, selectValue) {
page = parseInt(page);
limit = parseInt(limit);
const params = {
type: type,
typeid: typeid
};
if (selectValue === 'wiki') {
params['data.type'] = selectValue;
}
getSchema() {
return {
uid: { type: Number, required: true },
typeid: { type: Number, required: true },
type: { type: String,enum:['user', 'group', 'interface','project', 'other', 'interface_col'], required: true },
content: { type: String, required: true },
username: { type: String, required: true },
add_time: Number,
data: Schema.Types.Mixed //用于原始数据存储
};
if (selectValue && !isNaN(selectValue)) {
params['data.interface_id'] = +selectValue;
}
/**
* @param {String} content log内容
* @param {Enum} type log类型 ['user', 'group', 'interface', 'project', 'other']
* @param {Number} uid 用户id
* @param {String} username 用户名
* @param {Number} typeid 类型id
* @param {Number} add_time 时间
*/
save(data) {
let saveData = {
content: data.content,
type: data.type,
uid: data.uid,
username: data.username,
typeid: data.typeid,
add_time: yapi.commons.time(),
data: data.data
};
let log = new this.model(saveData);
return log.save();
}
del(id) {
return this.model.remove({
_id: id
});
}
list(typeid,type) {
return this.model.find({
typeid: typeid,
type: type
})
.exec();
}
listWithPaging(typeid,type, page, limit, interfaceId) {
page = parseInt(page);
limit = parseInt(limit);
const params = {
type: type,
typeid: typeid
}
if(interfaceId && !isNaN(interfaceId)){
params['data.interface_id'] = +interfaceId
}
return this.model.find(params).sort({add_time:-1}).skip((page - 1) * limit).limit(limit).exec();
}
listWithPagingByGroup(typeid, pidList, page, limit) {
page = parseInt(page);
limit = parseInt(limit);
return this.model.find({
"$or":[{
type: "project",
typeid: {"$in": pidList}
},{
type: "group",
typeid: typeid
}]
}).sort({add_time:-1}).skip((page - 1) * limit).limit(limit).exec();
}
listCountByGroup(typeid,pidList) {
return this.model.count({
"$or":[{
type: "project",
typeid: {"$in": pidList}
},{
type: "group",
typeid: typeid
}]
});
}
listCount(typeid,type, interfaceId) {
const params = {
type: type,
return this.model
.find(params)
.sort({ add_time: -1 })
.skip((page - 1) * limit)
.limit(limit)
.exec();
}
listWithPagingByGroup(typeid, pidList, page, limit) {
page = parseInt(page);
limit = parseInt(limit);
return this.model
.find({
$or: [
{
type: 'project',
typeid: { $in: pidList }
},
{
type: 'group',
typeid: typeid
}
]
})
.sort({ add_time: -1 })
.skip((page - 1) * limit)
.limit(limit)
.exec();
}
listCountByGroup(typeid, pidList) {
return this.model.count({
$or: [
{
type: 'project',
typeid: { $in: pidList }
},
{
type: 'group',
typeid: typeid
}
if(interfaceId && !isNaN(interfaceId)){
params['data.interface_id'] = +interfaceId
}
return this.model.count(params);
]
});
}
listCount(typeid, type, selectValue) {
const params = {
type: type,
typeid: typeid
};
if (selectValue === 'wiki') {
params['data.type'] = selectValue;
}
listWithCatid(typeid,type, interfaceId) {
const params = {
type: type,
typeid: typeid
if (selectValue && !isNaN(selectValue)) {
params['data.interface_id'] = +selectValue;
}
if(interfaceId && !isNaN(interfaceId)){
params['data.interface_id'] = +interfaceId
}
return this.model.find(params).sort({add_time:-1}).limit(1).select('uid content type username typeid add_time').exec();
return this.model.count(params);
}
listWithCatid(typeid, type, interfaceId) {
const params = {
type: type,
typeid: typeid
};
if (interfaceId && !isNaN(interfaceId)) {
params['data.interface_id'] = +interfaceId;
}
return this.model
.find(params)
.sort({ add_time: -1 })
.limit(1)
.select('uid content type username typeid add_time')
.exec();
}
}
module.exports = logModel;
module.exports = logModel;

View File

@ -35,6 +35,7 @@ formats.forEach(item => {
exports.schemaToJson = function(schema, options = {}) {
Object.assign(options, defaultOptions);
jsf.option(options);
let result;
try {
@ -274,6 +275,7 @@ exports.sandbox = (sandbox, script) => {
script.runInContext(context, {
timeout: 3000
});
return sandbox;
};
@ -504,6 +506,7 @@ function convertString(variable) {
exports.runCaseScript = async function runCaseScript(params) {
let script = params.script;
// script 是断言
if (!script) {
return yapi.commons.resReturn('ok');
}
@ -523,11 +526,13 @@ exports.runCaseScript = async function runCaseScript(params) {
let result = {};
try {
result = yapi.commons.sandbox(context, script);
result.logs = logs;
return yapi.commons.resReturn(result);
} catch (err) {
logs.push(convertString(err));
result.logs = logs;
return yapi.commons.resReturn(result, 400, err.name + ': ' + err.message);
}
};

View File

@ -20,11 +20,14 @@ function addPluginRouter(config) {
pluginsRouterPath.push(routerPath);
createAction(router, "/api", config.controller, config.action, routerPath, method, true);
}
function websocket(app) {
createAction(router, "/api", interfaceController, "solveConflict", "/interface/solve_conflict", "get")
yapi.emitHookSync('add_ws_router', addPluginRouter);
app.ws.use(router.routes())
app.ws.use(router.allowedMethods());
app.ws.use(function (ctx, next) {

View File

@ -7,7 +7,15 @@
<div class="icon">&#xf0fd;</div>
<input type="text" class="input js-input" placeholder="搜索" />
<div class="m-search-result js-search-result"></div>
</div></div><nav class="m-header-nav js-nav"><ul class="m-header-items"><li class="item active"><a class="href" href="index.html">教程</a></li><li class="item "><a class="href" href="../devops/index.html">内网部署</a></li></ul></nav><div id="js-nav-btn" class="m-header-btn ui-font-ydoc"></div></header><div class="m-content" id="js-content"><div id="markdown-body" class="m-content-container markdown-body"><h3 id="v1.3.18">v1.3.18</h3>
</div></div><nav class="m-header-nav js-nav"><ul class="m-header-items"><li class="item active"><a class="href" href="index.html">教程</a></li><li class="item "><a class="href" href="../devops/index.html">内网部署</a></li></ul></nav><div id="js-nav-btn" class="m-header-btn ui-font-ydoc"></div></header><div class="m-content" id="js-content"><div id="markdown-body" class="m-content-container markdown-body"><h3 id="v1.3.19">v1.3.19</h3>
<ul>
<li>增加项目文档记录wiki</li>
</ul>
<h4>Bug Fixed</h4>
<ul>
<li>修复测试用例名称为空时保存测试用例出现的bug</li>
</ul>
<h3 id="v1.3.18">v1.3.18</h3>
<ul>
<li>增加全局接口搜索功能</li>
<li>邮件通知过滤功能</li>

View File

@ -11,7 +11,7 @@
<p>传统的接口自动化测试成本高,大量的项目没有使用自动化测试保证接口的质量,仅仅依靠手动测试,是非常不可靠和容易出错的。</p>
<p>YApi 为了解决这个问题,开发了可视化接口自动化测试功能,只需要配置每个接口的入参和对 RESPONSE 断言,即可实现对接口的自动化测试,大大提升了接口测试的效率。</p>
<h2 id="第一步,测试集合">第一步,测试集合</h2>
<p>使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。</p>
<p>使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)</p>
<p><img src="case-col.png" alt=""></p>
<p><img src="import-case.png" alt=""></p>
<h2 id="第二步,编辑测试用例">第二步,编辑测试用例</h2>

View File

@ -596,7 +596,7 @@ window.ydoc_plugin_search_json = {
{
"title": "第一步,测试集合",
"url": "/documents/case.html#第一步,测试集合",
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。"
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)。"
},
{
"title": "第二步,编辑测试用例",
@ -653,7 +653,7 @@ window.ydoc_plugin_search_json = {
{
"title": "第一步,测试集合",
"url": "/documents/case.html#第一步,测试集合",
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口。"
"content": "第一步,测试集合使用 YApi 自动化测试,第一步需要做得是创建测试集合和导入接口,点击添加集合创建,创建完成后导入接口(同一个接口可以多次导入)。"
},
{
"title": "第二步,编辑测试用例",
@ -1149,6 +1149,11 @@ window.ydoc_plugin_search_json = {
"content": "",
"url": "/documents/CHANGELOG.html",
"children": [
{
"title": "v1.3.19",
"url": "/documents/CHANGELOG.html#v1.3.19",
"content": "v1.3.19增加项目文档记录wiki\nBug Fixed修复测试用例名称为空时保存测试用例出现的bug\n"
},
{
"title": "v1.3.18",
"url": "/documents/CHANGELOG.html#v1.3.18",
@ -1306,6 +1311,11 @@ window.ydoc_plugin_search_json = {
"content": "",
"url": "/documents/CHANGELOG.html",
"children": [
{
"title": "v1.3.19",
"url": "/documents/CHANGELOG.html#v1.3.19",
"content": "v1.3.19增加项目文档记录wiki\nBug Fixed修复测试用例名称为空时保存测试用例出现的bug\n"
},
{
"title": "v1.3.18",
"url": "/documents/CHANGELOG.html#v1.3.18",