feat(UI自动化): 调试支持本地浏览器运行

This commit is contained in:
nathan.liu 2022-05-11 10:13:13 +08:00 committed by zhangdahai112
parent bff34acf8b
commit a91018886f
21 changed files with 310 additions and 17 deletions

View File

@ -20,6 +20,8 @@ public class RunDefinitionRequest {
private boolean saved;
private boolean runLocal;
private String requestId;
private String name;

View File

@ -80,6 +80,9 @@ public class ParameterConfig extends MsParameter {
private String scenarioId;
private String reportType;
private boolean runLocal;
/**
* 排除生成临界控制器的场景
*/

View File

@ -33,5 +33,7 @@ public class User implements Serializable {
private String platformInfo;
private String seleniumServer;
private static final long serialVersionUID = 1L;
}

View File

@ -13,6 +13,7 @@
<result column="last_workspace_id" jdbcType="VARCHAR" property="lastWorkspaceId" />
<result column="phone" jdbcType="VARCHAR" property="phone" />
<result column="source" jdbcType="VARCHAR" property="source" />
<result column="selenium_server" jdbcType="VARCHAR" property="seleniumServer" />
<result column="last_project_id" jdbcType="VARCHAR" property="lastProjectId" />
<result column="create_user" jdbcType="VARCHAR" property="createUser" />
</resultMap>
@ -79,7 +80,7 @@
</sql>
<sql id="Base_Column_List">
id, `name`, email, `password`, `status`, create_time, update_time, `language`, last_workspace_id,
phone, `source`, last_project_id, create_user
phone, `source`, selenium_server, last_project_id, create_user
</sql>
<sql id="Blob_Column_List">
platform_info
@ -372,6 +373,9 @@
<if test="platformInfo != null">
platform_info = #{platformInfo,jdbcType=LONGVARCHAR},
</if>
<if test="seleniumServer != null">
`selenium_server` = #{seleniumServer,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>

View File

@ -9,10 +9,7 @@ import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.request.member.AddMemberRequest;
import io.metersphere.controller.request.member.EditPassWordRequest;
import io.metersphere.controller.request.member.QueryMemberRequest;
import io.metersphere.controller.request.member.UserRequest;
import io.metersphere.controller.request.member.*;
import io.metersphere.controller.request.resourcepool.UserBatchProcessRequest;
import io.metersphere.dto.UserDTO;
import io.metersphere.dto.UserGroupPermissionDTO;
@ -269,4 +266,18 @@ public class UserController {
public Map<Object, Object> getWSAndProjectByUserId(@PathVariable String userId) {
return userService.getWSAndProjectByUserId(userId);
}
/**
* 配置 用户的selenium-server 地址 ip:port
*/
@PostMapping("/update/seleniumServer")
@MsAuditLog(module = OperLogModule.SYSTEM_USER, type = OperLogConstants.UPDATE, title = "selenium-server地址")
public int updateSeleniumServer(@RequestBody EditSeleniumServerRequest request) {
return userService.updateUserSeleniumServer(request);
}
@GetMapping("/verify/seleniumServer")
public String verifySeleniumServer() {
return userService.verifyUserSeleniumServer();
}
}

View File

@ -0,0 +1,10 @@
package io.metersphere.controller.request.member;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EditSeleniumServerRequest {
private String seleniumServer;
}

View File

@ -15,6 +15,7 @@ public class ProjectConfig {
private Boolean caseCustomNum = false;
private Boolean scenarioCustomNum = false;
private String apiQuickMenu;
private String uiQuickMenu;
private Boolean casePublic = false;
private Integer mockTcpPort = 0;
private Boolean mockTcpOpen = false;

View File

@ -19,10 +19,7 @@ import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.ResultHolder;
import io.metersphere.controller.request.LoginRequest;
import io.metersphere.controller.request.WorkspaceRequest;
import io.metersphere.controller.request.member.AddMemberRequest;
import io.metersphere.controller.request.member.EditPassWordRequest;
import io.metersphere.controller.request.member.QueryMemberRequest;
import io.metersphere.controller.request.member.UserRequest;
import io.metersphere.controller.request.member.*;
import io.metersphere.controller.request.resourcepool.UserBatchProcessRequest;
import io.metersphere.dto.GroupResourceDTO;
import io.metersphere.dto.UserDTO;
@ -39,6 +36,7 @@ import io.metersphere.log.vo.OperatingLogDetails;
import io.metersphere.log.vo.system.SystemReference;
import io.metersphere.notice.domain.UserDetail;
import io.metersphere.security.MsUserToken;
import okhttp3.*;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ExecutorType;
@ -59,6 +57,7 @@ import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static io.metersphere.commons.constants.SessionConstants.ATTR_USER;
@ -102,7 +101,7 @@ public class UserService {
}
public Map<String, User> queryNameByIds(List<String> userIds) {
if(userIds.isEmpty()){
if (userIds.isEmpty()) {
return new HashMap<>(0);
}
return extUserMapper.queryNameByIds(userIds);
@ -171,7 +170,7 @@ public class UserService {
private void checkQuota(QuotaService quotaService, String type, List<String> sourceIds, int size) {
if (quotaService != null) {
Map<String, Integer> addMemberMap = sourceIds.stream().collect(Collectors.toMap( id -> id, id -> size));
Map<String, Integer> addMemberMap = sourceIds.stream().collect(Collectors.toMap(id -> id, id -> size));
quotaService.checkMemberCount(addMemberMap, type);
}
}
@ -1346,4 +1345,64 @@ public class UserService {
return false;
}
public int updateUserSeleniumServer(EditSeleniumServerRequest request) {
UserExample userExample = new UserExample();
userExample.createCriteria().andIdEqualTo(SessionUtils.getUser().getId());
List<User> users = userMapper.selectByExample(userExample);
if (!CollectionUtils.isEmpty(users)) {
User user = users.get(0);
String seleniumServer = request.getSeleniumServer();
user.setSeleniumServer(StringUtils.isBlank(seleniumServer) ? "" : seleniumServer.trim());
user.setUpdateTime(System.currentTimeMillis());
//更新session seleniumServer 信息
SessionUser sessionUser = SessionUtils.getUser();
sessionUser.setSeleniumServer(seleniumServer);
SessionUtils.putUser(sessionUser);
return userMapper.updateByPrimaryKeySelective(user);
}
MSException.throwException("更新selenium-server地址失败");
return 0;
}
public String verifyUserSeleniumServer() {
UserExample userExample = new UserExample();
userExample.createCriteria().andIdEqualTo(SessionUtils.getUser().getId());
List<User> users = userMapper.selectByExample(userExample);
if (!CollectionUtils.isEmpty(users)) {
User user = users.get(0);
if (StringUtils.isBlank(user.getSeleniumServer())) {
return "configErr";
}
OkHttpClient client = new OkHttpClient().newBuilder()
.connectTimeout(5, TimeUnit.SECONDS)
.build();
MediaType mediaType = MediaType.parse("application/json");
RequestBody body = RequestBody.create(mediaType,
"{\"operationName\":\"\",\"variables\":{},\"query\":\"query Summary {\\n grid {\\n uri\\n totalSlots\\n nodeCount\\n maxSession\\n sessionCount\\n sessionQueueSize\\n version\\n __typename\\n }\\n}\"}");
Request req = new Request.Builder()
.url(user.getSeleniumServer() + "/graphql")
.method("POST", body)
.addHeader("Content-Type", "application/json")
.build();
Response response = null;
try {
response = client.newCall(req).execute();
if (!response.isSuccessful()) {
return "connectionErr";
}
} catch (Exception e) {
return "connectionErr";
} finally {
try {
if (response != null) {
response.close();
}
} catch (Exception e) {
}
}
}
return "ok";
}
}

@ -1 +1 @@
Subproject commit 04012284cd9cd16dd6270f01e71ec4714d67babf
Subproject commit bdab35b7a74d9a77ca74aa0e41e813d28067afaf

View File

@ -0,0 +1,2 @@
alter table user
add selenium_server varchar(255) default "";

View File

@ -1083,6 +1083,12 @@
"id": "PERSONAL_INFORMATION:READ+THIRD_ACCOUNT",
"name": "permission.personal_information.third_account",
"resourceId": "PERSONAL_INFORMATION"
},
{
"id": "PERSONAL_INFORMATION:READ+UI_SETTING",
"name": "permission.personal_information.ui_setting",
"resourceId": "PERSONAL_INFORMATION",
"license": true
}
],
"resource": [

View File

@ -74,6 +74,8 @@
"eslint-plugin-vue": "^5.0.0",
"file-writer": "^1.0.2",
"html-webpack-inline-source-plugin": "0.0.10",
"less": "^3.9.0",
"less-loader": "^7.3.0",
"vue-template-compiler": "2.6.14"
},
"eslintConfig": {

View File

@ -17,6 +17,7 @@ export default {
debug: Boolean,
reportId: String,
runData: Object,
runLocal: Boolean,
saved: Boolean,
environmentType: String,
environmentGroupId: String
@ -78,6 +79,7 @@ export default {
if (this.runData.variables) {
reqObj.variables = this.runData.variables;
}
reqObj.runLocal = this.runLocal;
this.$emit('runRefresh', {});
let url = '/api/automation/run/debug';

View File

@ -46,6 +46,12 @@ export default {
return getCurrentUser();
},
},
mounted() {
this.$EventBus.$on('showPersonInfo', this.handleCommand)
},
beforeDestroy(){
this.$EventBus.$off("showPersonInfo")
},
methods: {
logout: function () {
logout();

View File

@ -39,6 +39,28 @@
</el-row>
</el-tab-pane>
<!-- UI 测试 -->
<el-tab-pane v-if="isXpack" :label="$t('commons.ui_test')" name="ui_test" >
<el-row style="margin-top: 10px">
<span style="font-weight:bold">{{ $t('commons.view_settings') }}</span>
</el-row>
<el-row style="margin-top: 15px">
<app-manage-item :title="$t('ui.ui_debug_mode')"
:append-span="12" :prepend-span="12" :middle-span="0">
<template #append>
<el-radio-group v-model="config.uiQuickMenu" @change="switchChange('UI_QUICK_MENU', $event)">
<el-radio label="local" value="local">
{{ $t('ui.ui_local_debug') }}
</el-radio>
<el-radio label="server" value="server">
{{ $t('ui.ui_server_debug') }}
</el-radio>
</el-radio-group>
</template>
</app-manage-item>
</el-row>
</el-tab-pane>
<el-tab-pane :label="$t('commons.api')" name="api">
<el-row :gutter="20">
<el-col :span="8">
@ -178,6 +200,7 @@ export default {
caseCustomNum: false,
scenarioCustomNum: false,
apiQuickMenu: "",
uiQuickMenu: "server",
casePublic: false,
mockTcpPort: 0,
mockTcpOpen: false,
@ -224,6 +247,9 @@ export default {
if (res.data) {
this.config = res.data;
this.config.shareReport = true;
if(!this.config.uiQuickMenu){
this.config.uiQuickMenu = "server";
}
}
});
}

View File

@ -5,11 +5,13 @@
<el-tab-pane v-if="hasPermission('PERSONAL_INFORMATION:READ+API_KEYS')" name="commons.api_keys" :label="$t('commons.api_keys')" class="setting-item" ></el-tab-pane>
<el-tab-pane v-if="hasPermission('PERSONAL_INFORMATION:READ+EDIT_PASSWORD')" name="change_password" :label="$t('member.edit_password')" class="setting-item" ></el-tab-pane>
<el-tab-pane v-if="hasPermission('PERSONAL_INFORMATION:READ+THIRD_ACCOUNT')&&(hasJira||hasTapd||hasZentao||hasAzure)&&hasPermission('WORKSPACE_SERVICE:READ')" name="third_account" :label="$t('commons.third_account')" class="setting-item" ></el-tab-pane>
<el-tab-pane v-if="hasPermission('PERSONAL_INFORMATION:READ+UI_SETTING') && isXpack" name="commons.ui_setting" :label="$t('commons.ui_setting')" class="setting-item" ></el-tab-pane>
</el-tabs>
<ms-main-container>
<ms-person-from-setting v-if="activeIndex==='commons.personal_setting'" :form = form @getPlatformInfo = "getPlatformInfo" @cancel = "cancel" />
<ms-api-keys v-if="activeIndex==='commons.api_keys'" />
<password-info v-if="activeIndex==='change_password'" :rule-form = "ruleForm" @cancel="cancel" ></password-info>
<ui-setting v-if="activeIndex==='commons.ui_setting'" @cancel="cancel"></ui-setting>
<el-form v-if="activeIndex==='third_account'" >
<jira-user-info @auth="handleAuth" v-if="hasJira" :data="currentPlatformInfo" />
<tapd-user-info @auth="handleAuth" v-if="hasTapd" :data="currentPlatformInfo" />
@ -32,7 +34,8 @@
import MsApiKeys from "@/business/components/settings/personal/ApiKeys";
import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import PasswordInfo from "@/business/components/settings/personal/PasswordInfo";
import {getCurrentUser, getCurrentWorkspaceId, hasPermission} from "@/common/js/utils";
import UiSetting from "@/business/components/settings/personal/UiSetting";
import {getCurrentUser, getCurrentWorkspaceId, hasPermission, hasLicense} from "@/common/js/utils";
import ZentaoUserInfo from "@/business/components/settings/personal/ZentaoUserInfo";
import TapdUserInfo from "@/business/components/settings/personal/TapdUserInfo";
import JiraUserInfo from "@/business/components/settings/personal/JiraUserInfo";
@ -42,7 +45,7 @@
export default {
name: "MsPersonRouter",
components: {MsMainContainer,MsPersonFromSetting,MsApiKeys,PasswordInfo,ZentaoUserInfo, TapdUserInfo, JiraUserInfo, AzureDevopsUserInfo},
components: {MsMainContainer,MsPersonFromSetting,MsApiKeys,PasswordInfo,ZentaoUserInfo, TapdUserInfo, JiraUserInfo, AzureDevopsUserInfo, UiSetting},
inject: [
'reload',
],
@ -69,6 +72,7 @@
hasTapd: false,
hasZentao: false,
hasAzure: false,
isXpack: false,
updatePath: '/user/update/current',
form: {platformInfo: {}},
currentPlatformInfo: {
@ -84,6 +88,17 @@
projectList:[]
}
},
mounted() {
this.$EventBus.$on('siwtchActive', (item) => {
if(item){
this.activeIndex = item
}
})
this.isXpack = hasLicense();
},
beforeDestroy(){
this.$EventBus.$off("siwtchActive")
},
methods:{
hasPermission,
currentUser: () => {
@ -201,7 +216,7 @@
this.activeIndex = 'third_account';
return;
}
}
},
},
created() {
this.getActiveIndex();

View File

@ -0,0 +1,124 @@
<template>
<div class="setting-container">
<div class="server-setting-row">
<div class="server-label">本地 selenium-server 地址</div>
<div class="server-input">
<el-input
size="small"
type="text"
v-model="seleniumServer"
@input="change()"
/>
</div>
<div class="server-desc">(示例: http://192.168.1.101:4444)</div>
</div>
<div class="setting-opt-row">
<el-button size="small" @click="cancel">{{
$t("commons.cancel")
}}</el-button>
<el-button
size="small"
type="primary"
@click="updateSeleniumServer()"
@keydown.enter.native.prevent
>{{ $t("commons.confirm") }}</el-button
>
</div>
</div>
</template>
<script>
import { getCurrentUserId } from "@/common/js/utils";
export default {
name: "UiSetting",
data() {
return {
updateSeleniumServerPath: "/user/update/seleniumServer",
seleniumServer: "",
};
},
mounted() {
this.getUserSeleniumServer();
setTimeout(() => {
this.change();
}, 10);
},
methods: {
change() {
this.$forceUpdate();
},
cancel() {
this.$emit("cancel");
},
confirm() {
this.$emit("confirm");
},
async getUserSeleniumServer() {
let userId = getCurrentUserId();
let res = await this.$get(`/user/info/${userId}`);
if (res.data) {
this.seleniumServer = res.data.data.seleniumServer || "";
}
},
updateSeleniumServer() {
if (this.seleniumServer) {
if (!this.isURL(this.seleniumServer)) {
this.$error(this.$t("selenium-server 输入不合法!"));
return;
}
}
// seleniumServer
this.result = this.$post(
this.updateSeleniumServerPath,
{ seleniumServer: this.seleniumServer },
(response) => {
if (!response.data) {
this.$error(this.$t("selenium-server 配置失败!"));
} else {
this.$success(this.$t("commons.modify_success"));
}
}
);
},
isURL(url) {
let regEx =
/^(http|https):\/\/(([0-9]{1,3}\.){3}[0-9]{1,3}|([0-9a-z_!~*'()-]+\.)*([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\.[a-z]{2,6})(:[0-9]{1,5})?$/;
if (regEx.test(url)) {
return true;
} else {
return false;
}
},
},
};
</script>
<style scope lang="less">
.setting-container {
padding-left: 50px;
.server-setting-row {
margin: 10px 0px;
.server-label {
font-size: 15px;
color: #606266;
}
.server-input {
margin: 10px 0px 5px 0;
.el-input__inner {
width: 300px !important;
}
}
.server-desc {
color: #909399;
font-size: 11px;
}
}
.setting-opt-row {
margin-top: 18px;
el-button {
}
}
}
</style>

@ -1 +1 @@
Subproject commit e30c5a16d53a4cc551066b22598e43b2f5d3a6ce
Subproject commit 4bfd301399124b83199c4a767f8c7f42a5f6b20a

View File

@ -109,6 +109,7 @@ export default {
personal_setting: 'Personal Setting',
api_keys: 'API Keys',
third_account: 'Third Account',
ui_setting: 'UI Setting',
quota: 'Quota',
test_resource_pool: 'Resource Pool',
system_setting: 'Settings',
@ -118,6 +119,7 @@ export default {
follow_api: 'Follow API definition',
response_time_delay: 'Response delay time',
my_workstation: 'MyWorkstation',
ui_test: 'UiTest',
performance: 'Performance',
enable_settings: 'Enable Settings',
view_settings: 'View Settings',
@ -3119,6 +3121,7 @@ export default {
api_keys: 'API Keys',
edit_password: "EDIT PASSWORD",
third_account: 'Third Account',
ui_setting: 'UI Setting',
},
other: {
track: "Track",
@ -3157,5 +3160,8 @@ export default {
ui_automation: "UI Automation",
ui_element: "UI Element Library",
report: "Test Report",
ui_debug_mode: 'UI debugging mode',
ui_local_debug: 'local',
ui_server_debug: 'server',
}
};

View File

@ -102,6 +102,7 @@ export default {
personal_info: '个人信息',
api_keys: 'API Keys',
third_account: '第三方平台账号',
ui_setting: 'UI设置',
quota: '配额管理',
status: '状态',
show_all: '显示全部',
@ -123,6 +124,7 @@ export default {
view_settings: '显示设置',
functional: '功能测试',
my_workstation: '我的工作台',
ui_test: 'UI测试',
input_content: '请输入内容',
create: '新建',
edit: '编辑',
@ -3124,6 +3126,7 @@ export default {
api_keys: 'API Keys',
edit_password: "修改密码",
third_account: '第三方平台账号',
ui_setting: 'UI设置',
},
other: {
track: "测试跟踪",
@ -3209,6 +3212,9 @@ export default {
cmdExtraction: "数据提取",
cmdExtractWindow: "提取窗口信息",
cmdExtractElement: "提取元素信息",
valiate_fail: "校验失败,请检查必填项"
valiate_fail: "校验失败,请检查必填项",
ui_debug_mode: 'UI自动化调试方式',
ui_local_debug: '本地调试',
ui_server_debug: '后端调试',
}
};

View File

@ -102,6 +102,7 @@ export default {
personal_info: '個人信息',
api_keys: 'API Keys',
third_account: '第三方平臺賬號',
ui_setting: 'UI 設置',
quota: '配額管理',
status: '狀態',
show_all: '顯示全部',
@ -123,6 +124,7 @@ export default {
view_settings: '顯示設置',
functional: '功能測試',
my_workstation: '我的工作臺',
ui_test: 'UI測試',
input_content: '請輸入內容',
create: '新建',
edit: '編輯',
@ -3123,6 +3125,7 @@ export default {
api_keys: 'API Keys',
edit_password: "修改密碼",
third_account: '第三方平臺賬號',
ui_setting: 'UI 設置',
},
other: {
track: "測試跟蹤",
@ -3161,5 +3164,8 @@ export default {
ui_automation: "UI 自動化",
ui_element: "元素庫",
report: "測試報告",
ui_debug_mode: 'UI自動化調試管道',
ui_local_debug: '本地調試',
ui_server_debug: '後端調試',
}
};