feat(测试跟踪): UI支持环境

--story=1010692 --user=张大海 【UI测试】支持与环境管理打通 https://www.tapd.cn/55049933/s/1315234
This commit is contained in:
zhangdahai112 2022-12-13 13:48:14 +08:00 committed by zhangdahai112
parent d0cf2b0b33
commit de8d6ff963
21 changed files with 6391 additions and 76 deletions

View File

@ -18,12 +18,21 @@
:content="$t('commons.import')"
@click="importJSON"
/>
<ms-table-button
<el-popover
v-permission="['PROJECT_ENVIRONMENT:READ+EXPORT']"
icon="el-icon-box"
:content="$t('commons.export')"
@click="exportJSON"
/>
placement="bottom"
trigger="hover"
:content="$t('envrionment.export_variable_tip')"
width="300">
<ms-table-button
style="margin-left: 10px"
slot="reference"
v-permission="['PROJECT_ENVIRONMENT:READ+EXPORT']"
icon="el-icon-box"
:content="$t('commons.export')"
@click="exportJSON"
/>
</el-popover>
<el-link
style="margin-left: 10px"
@click="batchAdd"
@ -58,6 +67,31 @@
>
<ms-table-column prop="num" sortable label="ID" min-width="60">
</ms-table-column>
<ms-table-column
prop="scope"
sortable
:label="$t('commons.scope')"
:filters="scopeTypeFilters"
:filter-method="filterScope"
min-width="120">
<template slot-scope="scope">
<el-select
v-model="scope.row.scope"
:placeholder="$t('commons.please_select')"
size="mini"
@change="changeType(scope.row)"
>
<el-option
v-for="item in scopeTypeFilters"
:key="item.value"
:label="item.text"
:value="item.value"
/>
</el-select>
</template>
</ms-table-column>
<ms-table-column
prop="name"
:label="$t('api_test.variable_name')"
@ -128,14 +162,14 @@
sortable
>
<template slot-scope="scope">
<el-input v-model="scope.row.description" size="mini" />
<el-input v-model="scope.row.description" size="mini"/>
</template>
</ms-table-column>
<ms-table-column :label="$t('commons.operating')" width="150">
<template v-slot:default="scope">
<span>
<el-switch v-model="scope.row.enable" size="mini" />
<el-switch v-model="scope.row.enable" size="mini"/>
<el-tooltip
effect="dark"
:content="$t('commons.remove')"
@ -171,7 +205,7 @@
</ms-table-column>
</ms-table>
</div>
<batch-add-parameter @batchSave="batchSave" ref="batchAdd" />
<batch-add-parameter @batchSave="batchSave" ref="batchAdd"/>
<api-variable-setting ref="apiVariableSetting"></api-variable-setting>
<variable-import
ref="variableImport"
@ -181,7 +215,7 @@
</template>
<script>
import { KeyValue } from "../../../model/EnvTestModel";
import {KeyValue} from "../../../model/EnvTestModel";
import MsApiVariableInput from "./ApiVariableInput";
import BatchAddParameter from "./BatchAddParameter";
import MsTableButton from "../../MsTableButton";
@ -189,8 +223,9 @@ import MsTable from "../../table/MsTable";
import MsTableColumn from "../../table/MsTableColumn";
import ApiVariableSetting from "./ApiVariableSetting";
import CsvFileUpload from "./variable/CsvFileUpload";
import { downloadFile, getUUID, operationConfirm } from "../../../utils";
import {downloadFile, getUUID, operationConfirm} from "../../../utils";
import VariableImport from "./variable/VariableImport";
import _ from "lodash";
export default {
name: "MsApiScenarioVariables",
@ -230,15 +265,19 @@ export default {
},
],
typeSelectOptions: [
{ value: "CONSTANT", label: this.$t("api_test.automation.constant") },
{ value: "LIST", label: this.$t("test_track.case.list") },
{ value: "CSV", label: "CSV" },
{ value: "COUNTER", label: this.$t("api_test.automation.counter") },
{ value: "RANDOM", label: this.$t("api_test.automation.random") },
{value: "CONSTANT", label: this.$t("api_test.automation.constant")},
{value: "LIST", label: this.$t("test_track.case.list")},
{value: "CSV", label: "CSV"},
{value: "COUNTER", label: this.$t("api_test.automation.counter")},
{value: "RANDOM", label: this.$t("api_test.automation.random")},
],
variables: {},
selectVariable: "",
editData: {},
scopeTypeFilters: [
{text: this.$t("commons.api"), value: "api"},
{text: this.$t("commons.ui_test"), value: "ui"},
]
};
},
watch: {
@ -279,15 +318,15 @@ export default {
if (repeatKey !== "") {
this.$warning(
this.$t("api_test.environment.common_config") +
"【" +
repeatKey +
"】" +
this.$t("load_test.param_is_duplicate")
"【" +
repeatKey +
"】" +
this.$t("load_test.param_is_duplicate")
);
}
if (isNeedCreate) {
this.variables.push(
new KeyValue({ enable: true, id: getUUID(), type: "CONSTANT" })
new KeyValue({enable: true, id: getUUID(), type: "CONSTANT", scope: "api"})
);
}
this.$emit("change", this.variables);
@ -320,11 +359,11 @@ export default {
},
querySearch(queryString, cb) {
let restaurants = [
{ value: "UTF-8" },
{ value: "UTF-16" },
{ value: "GB2312" },
{ value: "ISO-8859-15" },
{ value: "US-ASCll" },
{value: "UTF-8"},
{value: "UTF-16"},
{value: "GB2312"},
{value: "ISO-8859-15"},
{value: "US-ASCll"},
];
let results = queryString
? restaurants.filter(this.createFilter(queryString))
@ -346,6 +385,9 @@ export default {
this.$set(item, "description", item.remark);
item.remark = undefined;
}
if (!item.scope) {
this.$set(item, "scope", "api");
}
index++;
});
},
@ -369,7 +411,7 @@ export default {
}
);
},
filter() {
filter(scope) {
let datas = [];
this.variables.forEach((item) => {
if (this.selectVariable && this.selectVariable != "" && item.name) {
@ -389,6 +431,29 @@ export default {
});
this.variables = datas;
},
filterScope(scope) {
let datas = [];
let variables = _.cloneDeep(this.variables);
variables.forEach((item) => {
if (scope == "api") {
if (
item.scope && item.scope != "api"
) {
item.hidden = true;
} else {
item.hidden = undefined;
}
} else {
if (item.scope == scope) {
item.hidden = undefined;
} else {
item.hidden = true;
}
}
datas.push(item);
});
this.variables = datas;
},
openSetting(data) {
this.$refs.apiVariableSetting.open(data);
},
@ -449,8 +514,15 @@ export default {
this.sortParameters();
},
exportJSON() {
if (this.$refs.variableTable.selectIds.length < 1) {
this.$warning(this.$t("api_test.environment.select_variable"));
let apiVariable = [];
this.$refs.variableTable.selectRows.forEach((r) => {
if (!r.scope || r.scope != "ui") {
apiVariable.push(r);
}
});
if (apiVariable.length < 1) {
this.$warning(this.$t("api_test.environment.select_api_variable"));
return;
}
let variablesJson = [];
@ -460,7 +532,7 @@ export default {
if (row.type === "CSV") {
messages = this.$t("variables.csv_download");
}
if (row.name) {
if (row.name && (!row.scope || row.scope == "api")) {
variablesJson.push(row);
}
});
@ -496,7 +568,15 @@ export default {
},
created() {
if (this.items.length === 0) {
this.items.push(new KeyValue({ enable: true }));
this.items.push(new KeyValue({enable: true, scope: "api"}));
} else {
// api
_.forEach(this.items, item => {
if (!item.scope) {
this.$set(item, "scope", "api");
}
})
this.variables = this.items;
}
},
};

View File

@ -0,0 +1,140 @@
<template>
<el-row>
<el-col :span="16">
<el-select v-model="selfQuantity" placeholder=" " size="mini" filterable default-first-option
allow-create
class="timing_select" :disabled="selfChoose">
<el-option
v-for="item in quantityOptions"
:key="item"
:label="item"
:value="item">
</el-option>
</el-select>
<el-select v-model="selfUnit" placeholder=" " size="mini"
class="timing_select" :disabled="selfChoose">
<el-option
v-for="item in unitOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</el-col>
</el-row>
</template>
<script>
export default {
name: "MiniTimingItem",
components: {},
props: {
choose: {
type: Boolean,
default() {
return false;
},
},
expr: {
type: String,
default() {
return "";
}
},
title: {
type: String,
default() {
return "";
}
},
shareLink: {
type: Boolean,
default() {
return false;
},
},
unitOptions: {
type: Array,
default() {
return [
{value: "D", label: this.$t('commons.date_unit.day')},
{value: "M", label: this.$t('commons.date_unit.month')},
{value: "Y", label: this.$t('commons.date_unit.year')},
];
},
}
},
watch: {
expr(val) {
this.parseExpr(val);
},
choose(val) {
this.selfChoose = val;
}
},
data() {
return {
selfQuantity: "",
selfUnit: "",
selfChoose: this.choose,
selfExpr: this.expr,
quantityOptions: [
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
"11", "12", "13", "14", "15", "16", "17", "18", "19", "20",
"21", "22", "23", "24", "25", "26", "27", "28", "29", "30",
"31"
],
}
},
methods: {
chooseChange(val) {
if (val && (!this.selfQuantity || !this.selfUnit)) {
this.$warning(this.$t('project.please_select_cleaning_time'));
this.selfChoose = false;
return false;
}
if (val && this.selfQuantity) {
if (typeof this.selfQuantity !== 'number' && isNaN(parseInt(this.selfQuantity))) {
this.$warning(this.$t('api_test.request.time') + this.$t('commons.type_of_integer'));
this.selfChoose = false;
return false;
}
if (this.selfQuantity <= 0 || parseInt(this.selfQuantity) <= 0) {
this.$warning(this.$t('commons.adv_search.operators.gt') + "0");
this.selfChoose = false;
return false;
}
if (Number(this.selfQuantity) > parseInt(this.selfQuantity)) {
this.$warning(this.$t('api_test.request.time') + this.$t('commons.type_of_integer'));
this.selfChoose = false;
return false;
}
}
this.$emit("update:choose", val);
this.$emit("update:expr", parseInt(this.selfQuantity) + this.selfUnit);
this.$emit("chooseChange");
},
parseExpr(expr) {
if (!expr) {
return;
}
// 1D 1M 1Y
this.selfUnit = expr.substring(expr.length - 1);
this.selfQuantity = expr.substring(0, expr.length - 1);
}
}
}
</script>
<style scoped>
.timing_name {
color: var(--primary_color);
}
.timing_select {
width: 80px;
margin-left: 2px;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="form-section">
<div>
<div class="title">{{ title }}</div>
<el-tooltip class="item" effect="dark" :content="content" placement="top-start">
<span
:class="{ 'el-icon-arrow-left pointer' : !active, 'el-icon-arrow-down pointer' : active}"
@click="active=!active"></span>
</el-tooltip>
<template v-if="active">
<slot></slot>
</template>
</div>
</div>
</template>
<script>
export default {
name: "FormSection",
props: {
title: {
type: String,
default: ""
},
initActive: {
type: Boolean,
default: false
}
},
data() {
return {
active: false,
content: this.$t('api_test.definition.document.open')
}
},
watch: {
initActive: {
handler(val) {
this.active = val;
},
immediate: true
},
active: {
handler(val) {
if (val) {
this.content = this.$t('api_test.definition.document.close');
} else {
this.content = this.$t('api_test.definition.document.open');
}
},
immediate: true
},
}
}
</script>
<style scoped>
.form-section {
padding-top: 10px;
}
.title {
margin-right: 15px;
display: inline-block;
}
.pointer {
cursor: pointer;
}
</style>

View File

@ -1578,6 +1578,7 @@ const message = {
environment_group_id: "Environment Group ID",
select_environment: "Please select environment",
select_variable: "Please select variable",
select_api_variable: "Please select api variable",
please_save_test: "Please Save Test First",
common_config: "Common Config",
http_config: "HTTP Config",
@ -2927,56 +2928,162 @@ const message = {
step_results: "Step results",
treatment_method: "Treatment method",
scenario_steps: "Scenario steps",
basic_information: "Basic information",
check_element: "Please select the elements",
selenium_tip: "Support Selenium-IDE format",
selenium_export_tip: "Export side format",
elementObject: "Element Object",
elementLocator: "Element Locator",
elementType: "Element Type",
not_selected: "(Not Selected)",
not_selected_location: "(Not Selected Location)",
location: "Location",
run: "Run",
locate_type: "Locate Type",
coord: "Coord",
enable_or_not: "Enable/Disable",
enable: "Enable",
disable: "Disable",
resolution: "resolution",
ignore_fail: "ignore fail",
not_ignore_fail: "not ignore fail",
cmdValidation: "Assert",
cmdValidateValue: "Assert Value",
cmdValidateText: "Assert Text",
cmdValidateDropdown: "Assert Dropdown",
cmdValidateElement: "Assert Element",
cmdValidateTitle: "Assert Title",
cmdValidation: "Validation",
cmdValidateValue: "ValidateValue",
cmdValidateText: "ValidateText",
cmdValidateDropdown: "ValidateDropdown",
cmdValidateElement: "ValidateElement",
cmdValidateTitle: "ValidateTitle",
cmdOpen: "Open",
cmdSelectWindow: "Select Window",
cmdSetWindowSize: "Set Window Size",
cmdSelectFrame: "Select Frame",
cmdDialog: "Dialog Operation",
cmdDropdownBox: "Dropdown Operation",
submit: "submit",
cmdSetItem: "Set Item",
cmdWaitElement: "Wait Element",
cmdSelectWindow: "SelectWindow",
cmdSetWindowSize: "SetWindowSize",
cmdSelectFrame: "SelectFrame",
cmdDialog: "DialogOperation",
cmdDropdownBox: "DropdownBox",
submit: "Submit",
cmdSetItem: "SetItem",
cmdWaitElement: "WaitElement",
cmdInput: "Input",
cmdMouseClick: "Mouse Click",
cmdMouseMove: "Mouse Move",
cmdMouseDrag: "Mouse Drag",
cmdMouseClick: "MouseClick",
cmdMouseMove: "MouseMove",
cmdMouseDrag: "MouseDrag",
cmdTimes: "Times",
cmdForEach: "ForEach",
cmdWhile: "While",
cmdIf: "If",
cmdElse: "Else",
cmdElseIf: "ElseIf",
close: "close",
close: "Close",
cmdExtraction: "Extraction",
cmdExtractWindow: "Extract window",
cmdExtractElement: "Extract Element",
cmdExtractWindow: "ExtractWindow",
cmdExtractElement: "ExtractElement",
valiate_fail: "Validate fail",
check_subitem: 'check subitem',
basic_information: "Basic information",
step_type: "Step type",
selenium_tip: "Support Selenium-IDE plugin format import",
selenium_export_tip: "Export side file via MeterSphere",
elementObject: "Element Object",
elementLocator: "Element Locator",
elementType: "Category",
not_selected: "(No element selected)",
not_selected_location: "(No selected element location)",
location: "Location",
run: "Run",
locate_type: "Location method",
coord: "coordinate",
enable_or_not: "Enable/Disable",
enable: "Enable",
disable: "Disable",
resolution: "resolution",
ignore_fail: "Ignore exception and continue execution",
not_ignore_fail: "Abort process",
input_or_not: "input",
input_content: "Input content",
insert_content: "Type content",
append_content: "Append input",
append_tip: "Check, append the input after the existing content;<br/>Uncheck, clear the existing content and then input",
pls_input: "Please input content",
opt_type: "mode:",
yes: "Yes",
no: "No",
confirm: "OK",
cancel: "Cancel",
press_button: "Click the popup OK button or Cancel button",
param_null: "Parameter cannot be null",
operation_object: "Operation object",
sub_item: "Sub-item",
value: "value",
select: "Select",
option: "Option ( Option )",
index: "Index ( Index )",
s_value: "Value ( Value )",
text: "Text ( Text )",
set_itera: "Set traversal",
foreach_tip: "Set loop iteration, support array row data, for example: [1,2,3]; you can also enter variables",
intervals: "Interval time",
condition_type: "Condition Type",
please_select: "Please select",
condition_list: "Condition list: set multiple conditions by list",
condition_list_: "Condition List",
condition_exp: "Conditional expression: If the expression is true, then execute the steps inside",
condition_exp_: "Condition expression",
expression: "expression",
if_tip: "Please use ${var} for variables, and single quotes for strings, such as: ${name} === 'Zhangsan'",
input_c_tip: "'The contenteditable attribute of an editable paragraph element must be true to enable input; for example: &lt;p contenteditable=&quot;true&quot;&gt;This is an editable paragraph. Please try editing the text.&lt; /p&gt;'",
input: "input box",
editable_p: "Editable paragraph",
click_type: "Click method",
set_click_point: "Set the mouse click position",
click_tip_1: "Check to control the click position of the mouse on the element",
element_location: "Element Location",
click_point: "Click position",
x: "Abscissa",
y: "ordinate",
click_tip_2: "The upper left corner of the default element is 0, 0; by setting the relative position, control the click position of the mouse on the element",
click: "click",
dclick: "Double click",
press: "press",
standup: "Bounce up",
mouse_start: "Mouse start position",
drag_start: "The position of the starting point of the dragged element",
mouse_end: "Mouse end position",
drag_end: "The final position of the dragged element",
move_type: "Move Type",
mouse_location: "Mouse location",
relative_location: "relative coordinate location",
move_tip: "Relative position, the current position coordinate of the element is 0, 0",
mouse_out_e: "Mouse out of element",
mouse_in_e: "Mouse in element",
mouse_e_to_c: "Mouse mouse from element to coordinate position",
url: "Webpage address",
sf_tip: "If you are switching frames, you need to pass in the index or element positioning before switching",
sf_index: "frame index number",
select_index: "Select the frame of the current page;",
select_f_tip: "Example: For example, if the index value is entered as 1, the effect will switch to the second frame of the current page (the index value starts from 0)",
exit_frame: "Exit the current frame (back to the main page)",
select_frame_index: "Switch to the specified frame according to the frame index",
select_by_location: "Switch frame according to the positioning method",
sw_tip1: "If you switch to the specified window, you need to pass in the handle",
handle_id: "Handle ID",
window_handle: "Window handle ID",
frame_index: "Webpage index number",
window_index: "Window web page index number",
select_open_window: "Select the number of pages that have been opened;",
s_w_t1: "Example: For example, if the index value is entered as 3, then the effect will switch to the third window that has been opened (the index value starts from 1)",
switch_by_id: "Switch to the specified window according to the handle ID",
switch_by_index: "Switch to the specified window according to the page index number",
switch_to_default: "Switch to initial window",
ws_tip1: "Specify the size, set the size of the window according to the input width and height",
size: "Size:",
by_pixel: "in pixels",
width: "width",
height: "Height",
times: "Number of cycles",
set_times: "Set the number of times of the loop, you can enter a variable",
wait_text: "Wait for text",
wait_timeout: "Wait Timeout",
wait_for_text: "Wait for the element to be equal to the given value (Text)",
wait_for_ele_pre: "Wait for element to exist",
wait_for_ele_visible: "Wait for element to show",
wait_for_ele_not_visible: "Wait for element not visible",
wait_for_ele_not_pre: "Wait for element not present",
wait_for_ele_edi: "Wait for element to be editable",
wait_for_ele_not_edi: "Wait for element not editable",
wait_tip: "For the Text attribute of the element, it refers to the text content displayed on the page, and the waiting timeout time is 15000ms",
exe_first: "Execute first and then judge",
while_t_1: "Execute first and then judge similar to doWhile , execute the loop body once and then judge the condition",
while_t_2: "Please use ${var} for variables and single quotes for strings, such as: ${name} === 'Zhangsan'",
loop_time_out: "Loop timeout",
operation: "Operation",
use_pixel: 'use pixel',
fullscreen: 'maximum',
swicth_to_default: "switch to origin window",
program_controller: 'Process control',
input_operation: 'input operation',
mouse_operation: 'Mouse operation',
element_operation: 'Element operation',
dialog_operation: 'Pop-up operation',
browser_operation: 'Browser operation',
pause: 'Pause',
browser: "Browser",
inner_drag: "Drag in element",
@ -2993,7 +3100,7 @@ const message = {
custom_command_title: "Command",
custom_command_label: "Custom command",
automation_list: "Automation list",
create_custom_command: "Create command",
create_custom_command: "Add command",
create_custom_command_label: "Create custom command",
import_by_list_label: "UI list import",
open_custom_command_label: "Open command",
@ -3004,14 +3111,161 @@ const message = {
delete_scenario_lable: "Delete scenario",
delete_command_lable: "Delete command",
command_name_label: "Command name",
unplanned_module: "Unplanned module",
default_module: "Default module",
executing: "Executing...",
unexecute: "PENDING",
check_command: "Please tick the instruction",
ip: "ip address",
cword: "Word",
csentence: "Sentence",
cparagraph: "Paragraph",
loading: "Loading...",
close_dialog: "close",
unknown_scenario: "Unknown Scenario",
unknown_instruction: "Unknown Instruction",
unknown_element: "Unknown Element",
scenario_ref_add_warning: "No other steps can be added to the referenced scene/instruction steps and sub steps!",
batch_editing_steps: "Batch editing steps",
wait_time_config: "Timeout setting",
wait_element_timeout: "Wait element timeout",
more_config_options: "More advanced settings options",
updated_config_info: "The updated options are",
config_success: "Config success",
cmdFileUpload: "File upload",
relevant_file: "Relevant File",
validate_tips: "To judge whether the actual result is consistent with the expected one, you can add multiple assertions",
instruction: "instruction",
screen_tip: "If the scene step triggers a native popup (alert or prompt), or if there is no page, the screenshot will not take effect;",
ele_css_attr: "Element CSS attribute",
ele_css_tip1: "Such as element CSS properties, color properties, font-size properties, etc.",
store_css_attr: "CSS attribute (storeCssAttribute)",
validate_type: "Please select an assertion method",
expect_value: "Expected value",
expect_title: "Please enter the desired page title",
title_tip: "Assert whether the title of the current window is consistent with the expected value, if it matches exactly, the assertion succeeds, otherwise it fails",
input_var: "Please enter a variable",
input_expect: "Please enter the expected value",
var_tip: "Assert whether the variable matches the expected value",
confirm_after_success: "Whether to click the confirm button after success",
input_expect_text: "Please enter the expected popup text",
input_window_tip: "Only supports the assertion of pop-up text. If yes, the confirmation button on the pop-up will be clicked after the assertion is successful. If no, no button will be clicked after the assertion is successful",
select_value: "The value of the selected element is equal to the desired (SelectedValue)",
select_label: "The text displayed by the drop-down box option is equal to the expected (SelectedLabel) ",
not_select_value: "The value of the selected element is not equal to the expected (NotSelectedValue)",
assert_check: "The element is checked (Checked)",
assert_editable: "Element is editable (Editable)",
assert_element_present: "Element Present (ElementPresent)",
assert_element_not_present: "ElementNotPresent",
assert_element_not_checked: "Element is not checked (NotChecked)",
assert_element_not_editable: "Element is not editable (NotEditable)",
assert_element_not_text: "Element text is not equal to expected (NotText)",
assert_text: "Element text equals expected(Text)",
assert_value: "The element value is equal to the expected (Value)",
script_tip: "Only js script is supported, the set script will be executed in the browser",
script_type: "Script Type",
set_var: "Set variable",
async: "async",
sync: "Sync",
return: "There is a return value",
no_return: "No return value",
sample_obj: "Ordinary Object",
is_string: "Is it a string type",
like_string_tip: "Such as strings, numbers, json objects, arrays, etc.;",
like_string_tip2: "Note: If it is not a valid js object type when stored as an object type (such as illegal special characters, space effects), it may generate a report failure.",
ele_pro: "Element Properties",
like_ele_tip: "such as the element's name attribute, id attribute, value attribute, etc.",
xpath_locator: "xpath path",
xpath_tip: "Only supports element positioning in xpath mode, and returns a value",
store: "Ordinary object (store)",
store_text: "Element text (storeText)",
store_value: "Element value (storeValue)",
store_attr: "Element attribute (storeAttribute)",
store_xpath_count: "Number of elements matching xpath (storeXpathCount)",
store_window_handle: "Window Handle(storeWindowHandle)",
store_title: "Web page title (storeTitle)",
wait_time: "Wait time",
per_tip: "After enabling the performance mode, the memory and cpu usage will be reduced, and the running results will not show step screenshots",
fail_over: "Failed to terminate",
validate_tip: "Check means a hard assertion (assert), if the assertion fails, the program will terminate. Unchecked means a soft assertion (verify), if the assertion fails, the program will not terminate.",
scenario: "Scenario",
extract_type: "Please select the extraction information type",
input_handle_name: "Please enter the storage window handle variable name",
extract_tip: "Save the extracted content to a variable",
input_window_title: "Please enter the variable name to store the title of the webpage",
revoke: "Revoke",
is_required: "Required",
remark: "Remark",
result: "Result",
var_step: "Variable generation steps",
param_required_tip: "After debugging and saving, the usage of variables will be automatically verified. The parameters used in user-defined steps must be filled in; Not required is the redundant parameter not used in the custom step.",
name: "Name",
parameter_configuration: "Parameter configuration",
enter_parameters: "Enter parameters",
out_parameters: "Outer parameters",
opt_ele: "Operation element",
dst_ele: "Destination element",
drag_type: "Drag and drop method",
drag_end_point: "The final position of the dragged element",
add_file: "Add file",
file: "File: 【",
been_deleted: "】 has been deleted! Please select the file again!",
click_force: "Force click",
click_tip_3: "Checked, the element is blocked and can be forced to click",
pls_sel_loctype: "Please select a location type",
pls_input_locator: "Please fill in the location",
import_template: "Import Template",
download_template: "Download Template",
import_desc: "Import Description",
el_import_tip_1: "1. If the imported ID already exists, update the element;",
el_import_tip_2: "2. If the imported ID is empty or the ID does not exist, add an element;",
el_import: "Element import",
empty_text: "No data yet",
confirm_del_el: "Confirm delete element",
confirm_del: "Confirm delete",
confirm_del_in : "Confirm delete command",
deng: "wait",
ge_instruction: "Instructions?",
ge_el: "elements?",
ge_scenario: "Scenarios?",
view_ref: "View Reference",
unplanned_element: "Unplanned element",
scenario_instruction: "Scenario/Instruction",
element_beening: "The element under the module is being",
element_beening_desc: "element used be scenario",
reference: "reference",
continue_or_not: "Whether to continue",
continue_or_not_delete: "Whether to continue delete",
unplanned_scenario: "Unplanned Scenario",
unplanned_module: "Unplanned Module",
confrim_del_node: "OK to delete node",
and_all_sub_node: "All resources under its subnodes?",
instruction_is_referenced: "Instruction is referenced:",
scenario_is_referenced: "Scenario is referenced:",
confirm_del_ins: "Confirm Delete Instruction",
confirm_del_scen: "Confirm delete scene",
check_grid: "Connection failed, please check selenium-grid service status",
check_grid_ip: "Connection failed, please check selenium-grid address information",
view_config: "View configuration information",
config_ip: "The local ip and port information are not detected, please check",
personal_info: "Personal Information",
in_config: "In Settings",
},
command_steps_label: "Instruction Steps",
assert_in_text: "Element text contains expectations (InText)",
assert_not_in_text: "Element text does not contain expectations (NotInText)",
equal: "equal",
not_equal: "not equal to",
contain: "contains",
not_contain: "Does not contain",
greater: "greater than",
greater_equal: "greater than or equal to",
lower: "less than",
lower_equal: "Less than or equal to",
null: "empty",
not_null: "not null",
assertion_configuration: "Assertion Configuration",
smart_variable_enable: "Use the current scene variables first",
use_origin_variable_scene: "Use original scene variables",
use_origin_env_run: "Use original scene environment to execute"},
project_application: {
workstation: {
time_tip: 'Off, no time range is set; On, according to the set time range, enter the list to be updated, if the time range is exceeded, it will be automatically cleared from the list;',

View File

@ -490,7 +490,8 @@ const message = {
ui_scenario: '未规划场景',
ui_module: "未规划模块",
},
template_delete: "模版删除"
template_delete: "模版删除",
scope: "应用场景",
},
login: {
normal_Login: "普通登录",
@ -1587,6 +1588,7 @@ const message = {
environment_group_id: "环境组ID",
select_environment: "请选择环境",
select_variable: "请选择变量",
select_api_variable: "请选择接口测试变量",
please_save_test: "请先保存测试",
common_config: "通用配置",
list_info: "列表数据用,分隔",
@ -2963,7 +2965,7 @@ const message = {
cmdElse: "Else",
cmdElseIf: "ElseIf",
close: "关闭网页",
cmdExtraction: "提取参数",
cmdExtraction: "数据提取",
cmdExtractWindow: "提取窗口信息",
cmdExtractElement: "提取元素信息",
valiate_fail: "校验失败,请检查必填项",
@ -2986,6 +2988,113 @@ const message = {
treatment_method: "处理方式",
scenario_steps: "场景步骤",
basic_information: "基础信息",
step_type: "步骤类型",
input_or_not: "是否输入",
input_content: "输入内容",
insert_content: "键入内容",
append_content: "追加输入",
append_tip: "勾选,在现有内容后面追加输入;<br/>不勾选,清空现有内容后再进行输入",
pls_input: "请输入内容",
opt_type: "操作方式:",
yes: "是",
no: "否",
confirm: "确定",
cancel: "取消",
press_button: "点击弹窗确定按钮或取消按钮",
param_null: "参数不能为空",
operation_object: "操作对象",
sub_item: "子选项",
value: "值",
select: "选择",
option: "选项( Option )",
index: "索引( Index )",
s_value: "值( Value )",
text: "文本( Text )",
set_itera: "设置遍历",
foreach_tip: "设置循环迭代,支持数组行数据,例如: [1,2,3];也可输入变量",
intervals: "间隔时间",
condition_type: "条件类型",
please_select: "请选择",
condition_list: "条件列表:通过列表的方式设置多个条件",
condition_list_: "条件列表",
condition_exp: "条件表达式:表达式判断为真,则执行里面的步骤",
condition_exp_: "条件表达式",
expression: "表达式",
if_tip: "变量请使用${var},字符串请加单引号,如:${name} === '张三'",
input_c_tip: "'可编辑段落的元素 contenteditable 属性必须为 true, 才可实现输入;例:&lt;p contenteditable=&quot;true&quot;&gt;这是一段可编辑的段落。请试着编辑该文本。&lt;/p&gt;'",
input: "输入框",
editable_p: "可编辑段落",
click_type: "点击方式",
set_click_point: "设置鼠标点击位置",
click_tip_1: "勾选,可控制鼠标在元素上的点击位置",
element_location: "元素位置",
click_point: "点击位置",
x: "横坐标",
y: "纵坐标",
click_tip_2: "默认元素的左上角为00通过设置相对位置控制鼠标在元素上的点击位置",
click: "单击",
dclick: "双击",
press: "按下",
standup: "弹起",
mouse_start: "鼠标起始位置",
drag_start: "被拖拽的元素起点的位置",
mouse_end: "鼠标终点位置",
drag_end: "被拖拽的元素最终的位置",
move_type: "移动方式",
mouse_location: "鼠标位置",
relative_location: "相对坐标位置",
move_tip: "相对位置元素当前的位置坐标为00",
mouse_out_e: "鼠标移出元素",
mouse_in_e: "鼠标移入元素",
mouse_e_to_c: "鼠标从元素移到坐标位置",
url: "网页地址",
sf_tip: "如果是切换 frame需要传入索引或者元素定位后再切换",
sf_index: "frame 索引号",
select_index: "选择当前页面的第几个 frame",
select_f_tip: "例:比如索引值输入 1那么效果会切换到当前页面的第 2 个 frame(索引值从 0 开始计算)",
exit_frame: "退出当前 frame(回到主页面)",
select_frame_index: "根据 frame 索引号切换到指定 frame",
select_by_location: "根据定位方式切换 frame",
sw_tip1: "如果是切换到指定窗口,需要传入句柄",
handle_id: "句柄 ID",
window_handle: "窗口句柄 ID",
frame_index: "网页索引号",
window_index: "窗口网页索引号",
select_open_window: "选择打开过的第几个网页;",
s_w_t1: "例:比如索引值输入 3那么效果会切换到已经打开过的第 3 个窗口(索引值从 1 开始计算)",
switch_by_id: "根据句柄 ID 切换到指定窗口",
switch_by_index: "根据网页索引号切换到指定窗口",
swicth_to_default: "切换到初始窗口",
ws_tip1: "指定尺寸,根据输入的宽度和高度,设置窗口的大小",
size: "尺寸:",
by_pixel: "以像素为单位",
width: "宽度",
height: "高度",
times: "循环次数",
set_times: "设置循环的次数,可输入变量",
wait_text: "等待文本",
wait_timeout: "等待超时",
wait_for_text: "等待元素等于给定的定值(Text)",
wait_for_ele_pre: "等待元素存在",
wait_for_ele_visible: "等待元素显示",
wait_for_ele_not_visible: "等待元素不显示",
wait_for_ele_not_pre: "等待元素不存在",
wait_for_ele_edi: "等待元素可编辑",
wait_for_ele_not_edi: "等待元素不可编辑",
wait_tip: "针对元素的Text属性指页面展示出来的文本内容等待超时时间为15000ms",
exe_first: "先执行后判断",
while_t_1: "先执行后判断类似 doWhile ,先执行一次循环体再判断条件",
while_t_2: "变量请使用${var},字符串请加单引号,如:${name} === '张三'",
loop_time_out: "循环超时时间",
operation: '操作',
use_pixel: '指定尺寸(像素为单位)',
fullscreen: '窗口最大化',
program_controller: '流程控制',
input_operation: '输入操作',
mouse_operation: '鼠标操作',
element_operation: '元素操作',
dialog_operation: '弹窗操作',
browser_operation: '浏览器操作',
check_element: "请勾选元素",
check_subitem: '请选择子分类',
pause: '等待时间',
@ -3017,13 +3126,157 @@ const message = {
command_name_label: "指令名称",
command_steps_label: "指令步骤",
command_step_info: "在右侧添加指令步骤",
default_module: "默认模块",
executing: "正在执行...",
unexecute: "未执行",
check_command: "请勾选指令",
ip: "ip地址",
cword: "词语",
csentence: "句子",
cparagraph: "段落",
loading: "加载中...",
close_dialog: "关闭",
unknown_scenario: "创建场景",
unknown_instruction: "创建指令",
unknown_element: "创建元素",
scenario_ref_add_warning: "引用的场景/指令步骤及子步骤都无法添加其他步骤!",
batch_editing_steps: "批量编辑步骤",
wait_time_config: "超时时间设置",
wait_element_timeout: "等待元素超时时间",
more_config_options: "更多高级设置选项",
updated_config_info: "更新后选项为",
config_success: "配置成功",
cmdFileUpload: "文件上传",
relevant_file: "关联需要上传的文件",
validate_tips: "判断实际的结果是否与期望的一致,可添加多条断言",
instruction: "指令",
screen_tip: "场景步骤如果触发原生弹窗alert或prompt或不存在页面时截图不生效",
ele_css_attr: "元素CSS属性",
ele_css_tip1: "如元素的 CSS 属性color 属性font-size 属性等",
store_css_attr: "CSS属性(storeCssAttribute)",
validate_type: "请选择断言方式",
expect_value: "期望值",
expect_title: "请输入期望的网页标题",
title_tip: "断言当前窗口的标题是否跟期望值一致,完全匹配则断言成功,否则失败",
input_var: "请输入变量",
input_expect: "请输入期望值",
var_tip: "断言变量与期望值是否匹配",
confirm_after_success: "成功后是否点击确认按钮",
input_expect_text: "请输入期望的弹窗文本",
input_window_tip: "仅支持弹窗文本的断言,选择是,则断言成功后会点击弹窗上的确认按钮,选择否,则断言成功后不点击任何按钮",
select_value: "所选元素的值等于期望(SelectedValue)",
select_label: "下拉框选项显示的文本等于期望(SelectedLabel) ",
not_select_value: "所选元素的值不等于期望(NotSelectedValue) ",
assert_check: "元素被选中(Checked)",
assert_editable: "元素可编辑(Editable)",
assert_element_present: "元素存在(ElementPresent)",
assert_element_not_present: "元素不存在(ElementNotPresent)",
assert_element_not_checked: "元素未被选中(NotChecked)",
assert_element_not_editable: "元素不可编辑(NotEditable)",
assert_element_not_text: "元素文本不等于期望(NotText)",
assert_text: "元素文本等于期望(Text)",
assert_value: "元素值等于期望(Value)",
script_tip: "仅支持js脚本设置的脚本将在浏览器执行",
script_type: "脚本类型",
set_var: "设置变量",
async: "异步",
sync: "同步",
return: "有返回值",
no_return: "无返回值",
sample_obj: "普通对象",
is_string: "是否为字符串类型",
like_string_tip: "如字符串、数字、json对象、数组等",
like_string_tip2: "注意:作为对象类型存储时如果不是一个合法的 js 对象类型(如非法特殊字符、空格影响),可能会生成报告失败。",
ele_pro: "元素属性",
like_ele_tip: "如元素的 name 属性id 属性value 属性等",
xpath_locator: "xpath 路径",
xpath_tip: "只支持 xpath 方式的元素定位,返回的是一个数值",
store: "普通对象(store)",
store_text: "元素文本(storeText)",
store_value: "元素值(storeValue)",
store_attr: "元素属性(storeAttribute)",
store_xpath_count: "匹配 xpath 的元素数量(storeXpathCount)",
store_window_handle: "窗口 Handle(storeWindowHandle)",
store_title: "网页标题(storeTitle)",
wait_time: "等待时间",
per_tip: "启用性能模式后将减少内存和cpu的占用运行结果不展示步骤截图",
fail_over: "失败终止",
validate_tip: "勾选表示为硬断言assert如果断言失败程序会终止。不勾选表示为软断言verify如果断言失败程序不会终止。",
scenario: "场景",
extract_type: "请选择提取信息类型",
input_handle_name: "请输入存储窗口 Handle 变量名",
extract_tip: "将提取的内容保存到变量中",
input_window_title: "请输入存储网页标题变量名",
opt_ele: "操作元素",
dst_ele: "目标元素",
drag_type: "拖拽方式",
drag_end_point: "被拖拽的元素最终的位置",
revoke: "撤回",
is_required: "是否必填",
remark: "备注",
result: "执行结果",
var_step: "变量产生步骤",
param_required_tip: "调试保存后,自动校验变量使用情况, 必填为自定义步骤内使用到的参数;非必填为自定义步骤内未使用到的冗余参数",
name: "名称",
parameter_configuration: "参数配置",
enter_parameters: "入参",
out_parameters: "出参",
add_file: "添加文件",
file: "文件: 【",
been_deleted: "】 已被删除!请重新选择文件!",
click_force: "强制点击",
click_tip_3: "勾选,元素被遮挡,可强制点击",
pls_sel_loctype: "请选择定位类型",
pls_input_locator: "请填写定位",
import_template: "导入模板",
download_template: "下载模板",
import_desc: "导入说明",
el_import_tip_1: "1.如果导入的ID已存在,则更新元素;",
el_import_tip_2: "2.如果导入的ID为空或ID不存在则新增元素;",
el_import: "元素导入",
empty_text: "暂无数据",
confirm_del_el: "确认删除元素 ",
confirm_del: "确认删除 ",
confirm_del_in : "确认删除指令 ",
deng: "等 ",
ge_instruction: "个指令 ",
ge_el: "个元素 ",
ge_scenario: " 个场景 ",
view_ref: "查看引用",
unplanned_element: "未规划元素",
scenario_instruction: "场景/指令",
element_beening: "模块下的元素被",
element_beening_desc: "元素被场景",
reference: "引用",
continue_or_not: "是否继续",
continue_or_not_delete: "是否继续删除",
unplanned_scenario: "未规划场景",
unplanned_module: "未规划模块",
confrim_del_node: "确定删除节点 ",
and_all_sub_node: " 及其子节点下所有资源?",
instruction_is_referenced: "指令被引用:",
scenario_is_referenced: "场景被引用:",
confirm_del_ins: "确认删除指令",
confirm_del_scen: "确认删除场景",
check_grid: "连接失败,请检查 selenium-grid 服务状态",
check_grid_ip: "连接失败,请检查 selenium-grid 地址信息",
view_config: "查看配置信息",
config_ip: "没有检测到本地ip和端口信息请在",
personal_info: "个人信息",
in_config: "中设置",
assert_in_text: "元素文本包含期望(InText)",
assert_not_in_text: "元素文本不包含期望(NotInText)",
equal: "等于",
not_equal: "不等于",
contain: "包含",
not_contain: "不包含",
greater: "大于",
greater_equal: "大于等于",
lower: "小于",
lower_equal: "小于等于",
null: "空",
not_null: "非空",
assertion_configuration: "断言配置",
},
project_application: {
workstation: {
@ -3046,6 +3299,9 @@ const message = {
scenario_title: "场景测试任务",
ui_title: "UI测试任务",
perf_title: "性能测试任务"
},
envrionment: {
export_variable_tip: "导出接口测试变量"
}
}

View File

@ -1584,6 +1584,7 @@ const message = {
environment_group_id: "環境組ID",
select_environment: "請選擇環境",
select_variable: "請選擇变量",
select_api_variable: "請選擇接口測試变量",
please_save_test: "請先保存測試",
common_config: "通用配置",
http_config: "HTTP配置",

View File

@ -0,0 +1,29 @@
package io.metersphere.controller.remote;
import io.metersphere.service.remote.UiTestService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping(path = {
"/test/plan/uiScenario/case",
"/ui/scenario/module",
"/share/test/plan/uiScenario/case",
"/ui/automation"
})
public class UiTestController {
@Resource
UiTestService uiTestService;
@PostMapping("/**")
public Object list(HttpServletRequest request, @RequestBody Object param) {
return uiTestService.post(request.getRequestURI(), param);
}
@GetMapping("/**")
public Object get(HttpServletRequest request) {
return uiTestService.get(request.getRequestURI());
}
}

View File

@ -0,0 +1,13 @@
package io.metersphere.service.remote;
import io.metersphere.commons.constants.MicroServiceName;
import io.metersphere.service.RemoteService;
import org.springframework.stereotype.Service;
@Service
public class UiTestService extends RemoteService {
public UiTestService() {
super(MicroServiceName.UI_TEST);
}
}

View File

@ -137,7 +137,7 @@ import MsTableOperatorButton from "metersphere-frontend/src/components/MsTableOp
import MsTablePagination from "metersphere-frontend/src/components/pagination/TablePagination";
import ApiEnvironmentConfig from "metersphere-frontend/src/components/environment/ApiEnvironmentConfig";
import {Environment, parseEnvironment} from "metersphere-frontend/src/model/EnvironmentModel";
import EnvironmentEdit from "metersphere-frontend/src/components/environment/EnvironmentEdit";
import EnvironmentEdit from "./components/EnvironmentEdit";
import MsAsideItem from "metersphere-frontend/src/components/MsAsideItem";
import MsAsideContainer from "metersphere-frontend/src/components/MsAsideContainer";
import ProjectSwitch from "metersphere-frontend/src/components/head/ProjectSwitch";

View File

@ -0,0 +1,553 @@
<template>
<el-main class="environment-edit" style="margin-left: 0px">
<el-form :model="environment" :rules="rules" ref="environment" label-width="80px">
<el-row>
<el-col :span="10" v-if="!isProject">
<el-form-item class="project-item" prop="currentProjectId" :label="$t('project.select')">
<el-select v-model="environment.currentProjectId" filterable clearable
size="small" :disabled="!ifCreate">
<el-option v-for="item in projectList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item prop="name" :label="$t('api_test.environment.name')">
<el-input v-model="environment.name" :disabled="isReadOnly" :placeholder="this.$t('commons.input_name')"
clearable size="small"/>
</el-form-item>
</el-col>
<el-col :span="4" v-if="!hideButton" :offset="isProject ? 10 : 0">
<div style="float: right;width: fit-content;">
<div style="float: left; margin-right: 8px;">
<slot name="other"></slot>
</div>
<div class="ms_btn">
<el-button type="primary" @click="confirm" @keydown.enter.native.prevent size="small">
{{ $t('commons.confirm') }}
</el-button>
</div>
</div>
</el-col>
</el-row>
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.environment.common_config')" name="common">
<ms-environment-common-config :common-config="environment.config.commonConfig" ref="commonConfig"
:is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.http_config')" name="http">
<ms-environment-http-config :project-id="environment.projectId" :http-config="environment.config.httpConfig"
ref="httpConfig" :is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.database_config')" name="sql">
<ms-database-config :configs="environment.config.databaseConfigs" :is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.environment.tcp_config')" name="tcp">
<ms-tcp-config :config="environment.config.tcpConfig" :is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('commons.ssl.config')" name="ssl">
<ms-environment-s-s-l-config :project-id="environment.projectId" :ssl-config="environment.config.sslConfig"
:is-read-only="isReadOnly"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.all_pre_script')" name="prescript">
<div style="padding-bottom: 20px;" v-if="!ifCreate">
<el-link style="float: right;" type="primary" @click="openHis('preScript')">
{{ $t('operating_log.change_history') }}
</el-link>
</div>
<environment-global-script
v-if="isRefresh && environment.config.globalScriptConfig && environment.config.preProcessor && environment.config.preStepProcessor"
:filter-request.sync="environment.config.globalScriptConfig.filterRequestPreScript"
:exec-after-private-script.sync="environment.config.globalScriptConfig.isPreScriptExecAfterPrivateScript"
:conn-scenario.sync="environment.config.globalScriptConfig.connScenarioPreScript"
:script-processor="environment.config.preProcessor"
:scrpit-step-processor="environment.config.preStepProcessor"
:is-pre-processor="true"
:is-read-only="isReadOnly"
@updateGlobalScript="updateGlobalScript"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.all_post_script')" name="postscript">
<div style="padding-bottom: 20px;" v-if="!ifCreate">
<el-link style="float: right;" type="primary" @click="openHis('postScript')">
{{ $t('operating_log.change_history') }}
</el-link>
</div>
<environment-global-script
v-if="isRefresh && environment.config.globalScriptConfig && environment.config.postProcessor && environment.config.postStepProcessor"
:filter-request.sync="environment.config.globalScriptConfig.filterRequestPostScript"
:exec-after-private-script.sync="environment.config.globalScriptConfig.isPostScriptExecAfterPrivateScript"
:conn-scenario.sync="environment.config.globalScriptConfig.connScenarioPostScript"
:script-processor="environment.config.postProcessor"
:scrpit-step-processor="environment.config.postStepProcessor"
:is-pre-processor="false"
:is-read-only="isReadOnly"
@updateGlobalScript="updateGlobalScript"/>
</el-tab-pane>
<!-- 认证配置 -->
<el-tab-pane :label="$t('api_test.definition.request.all_auth_config')" name="authConfig" v-if="isRefresh">
<el-tooltip class="item-tabs" effect="dark" :content="$t('api_test.definition.request.auth_config_info')"
placement="top-start" slot="label">
<span>{{ $t('api_test.definition.request.all_auth_config') }}</span>
</el-tooltip>
<ms-api-auth-config :is-read-only="isReadOnly" :request="environment.config.authManager"/>
</el-tab-pane>
<!--全局断言-->
<el-tab-pane :label="$t('env_options.all_assertions')" name="assertions">
<el-tooltip class="item-tabs" effect="dark" :content="$t('env_options.all_assertions')"
placement="top-start" slot="label">
<span>{{ $t('env_options.all_assertions') }}</span>
</el-tooltip>
<div v-if="hasLicense" style="margin-bottom: 15px">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item
:label="$t('error_report_library.use_error_report')"
prop="status" style="margin-bottom: 0px;">
<el-switch :disabled="isReadOnly"
v-model="environment.config.useErrorCode" style="margin-right: 10px"/>
</el-form-item>
</el-col>
</el-row>
<el-row v-show="environment.config.useErrorCode" :gutter="20">
<el-col style="margin-left: 30px">
{{ $t('error_report_library.conflict_with_success') }}
<el-switch
class="errorReportConfigSwitch"
v-model="environment.config.higherThanSuccess"
:active-text="$t('error_report_library.option.name')"
:inactive-text="$t('api_test.automation.request_success')">
</el-switch>
</el-col>
</el-row>
<el-row v-show="environment.config.useErrorCode" :gutter="20">
<el-col style="margin-left: 30px">
{{ $t('error_report_library.conflict_with_error') }}
<el-switch
class="errorReportConfigSwitch"
v-model="environment.config.higherThanError"
:active-text="$t('error_report_library.option.name')"
:inactive-text="$t('api_test.automation.request_error')">
</el-switch>
</el-col>
</el-row>
</div>
<global-assertions :is-read-only="isReadOnly" :assertions="environment.config.assertions"
:is-show-json-path-suggest="false"/>
</el-tab-pane>
</el-tabs>
</el-form>
<ms-change-history ref="changeHistory"/>
</el-main>
</template>
<script>
import {editEnv} from "metersphere-frontend/src/api/environment";
import MsApiScenarioVariables from "metersphere-frontend/src/components/environment/commons/ApiScenarioVariables";
import MsApiKeyValue from "metersphere-frontend/src/components/environment/commons/ApiKeyValue";
import MsDialogFooter from "metersphere-frontend/src/components/MsDialogFooter";
import {REQUEST_HEADERS} from "metersphere-frontend/src/utils/constants";
import {CommonConfig, Environment} from "metersphere-frontend/src/model/EnvironmentModel";
import MsApiHostTable from "metersphere-frontend/src/components/environment/commons/ApiHostTable";
import MsDatabaseConfig from "metersphere-frontend/src/components/environment/database/DatabaseConfig";
import MsEnvironmentHttpConfig from "./EnvironmentHttpConfig";
import MsEnvironmentCommonConfig from "metersphere-frontend/src/components/environment/EnvironmentCommonConfig";
import MsEnvironmentSSLConfig from "metersphere-frontend/src/components/environment/EnvironmentSSLConfig";
import MsApiAuthConfig from "metersphere-frontend/src/components/environment/auth/ApiAuthConfig";
import MsTcpConfig from "metersphere-frontend/src/components/environment/tcp/TcpConfig";
import {getUUID} from "metersphere-frontend/src/utils";
import {hasLicense} from "metersphere-frontend/src/utils/permission";
import MsChangeHistory from "metersphere-frontend/src/components/environment/history/EnvHistory";
import MsDialogHeader from "metersphere-frontend/src/components/MsDialogHeader";
import GlobalAssertions from "metersphere-frontend/src/components/environment/assertion/GlobalAssertions";
import EnvironmentGlobalScript from "metersphere-frontend/src/components/environment/EnvironmentGlobalScript";
export default {
name: "EnvironmentEdit",
components: {
MsTcpConfig,
MsApiAuthConfig,
MsEnvironmentCommonConfig,
MsEnvironmentHttpConfig,
MsEnvironmentSSLConfig,
MsDatabaseConfig, MsApiHostTable, MsDialogFooter, MsApiKeyValue, MsApiScenarioVariables, MsChangeHistory,
MsDialogHeader, GlobalAssertions, EnvironmentGlobalScript
},
props: {
environment: new Environment(),
projectId: String,
isReadOnly: {
type: Boolean,
default: false
},
hideButton: Boolean,
ifCreate: {
type: Boolean,
default: false
},
projectList: {
type: Array,
default() {
return [];
}
},
isProject: Boolean
},
data() {
return {
result: false,
envEnable: false,
isRefresh: true,
rules: {
name: [
{required: true, message: this.$t('commons.input_name'), trigger: 'blur'},
{max: 64, message: this.$t('commons.input_limit', [1, 64]), trigger: 'blur'}
],
currentProjectId: [
{required: true, message: "", trigger: 'blur'},
],
},
headerSuggestions: REQUEST_HEADERS,
activeName: 'common'
}
},
created() {
if (!this.environment.config.preProcessor) {
this.environment.config.preProcessor = {};
}
if (!this.environment.config.postProcessor) {
this.environment.config.postProcessor = {};
}
if (!this.environment.config.preStepProcessor) {
this.environment.config.preStepProcessor = {};
}
if (!this.environment.config.postStepProcessor) {
this.environment.config.postStepProcessor = {};
}
if (!this.environment.config.globalScriptConfig) {
this.environment.config.globalScriptConfig = {
filterRequestPreScript: [],
filterRequestPostScript: [],
isPreScriptExecAfterPrivateScript: false,
isPostScriptExecAfterPrivateScript: false,
connScenarioPreScript: false,
connScenarioPostScript: false,
};
}
if (!this.environment.config.authManager) {
this.environment.config.authManager = {'hashTree': []};
}
if (!this.environment.config.authManager.hashTree) {
this.environment.config.authManager.hashTree = [];
}
if (!this.environment.config.assertions) {
this.$set(this.environment.config, 'assertions', {
duration: {duration: 0},
regex: [],
jsonPath: [],
xpath2: [],
jsr223: [],
document: {type: "JSON", data: {json: [], xml: []}},
});
}
},
watch: {
environment: function (o) {
if (!this.environment.config.preProcessor) {
this.environment.config.preProcessor = {};
if (!this.environment.config.preProcessor.script) {
this.environment.config.preProcessor.script = "";
}
}
if (!this.environment.config.postProcessor) {
this.environment.config.postProcessor = {};
if (!this.environment.config.postProcessor.script) {
this.environment.config.postProcessor.script = "";
}
}
if (!this.environment.config.preStepProcessor) {
this.environment.config.preStepProcessor = {};
}
if (!this.environment.config.postStepProcessor) {
this.environment.config.postStepProcessor = {};
}
if (!this.environment.config.globalScriptConfig) {
this.environment.config.globalScriptConfig = {
filterRequestPreScript: [],
filterRequestPostScript: [],
isPreScriptExecAfterPrivateScript: false,
isPostScriptExecAfterPrivateScript: false,
connScenarioPreScript: false,
connScenarioPostScript: false,
};
}
if (!this.environment.config.authManager) {
this.environment.config.authManager = {'hashTree': []};
}
if (!this.environment.config.authManager.hashTree) {
this.environment.config.authManager.hashTree = [];
}
if (!this.environment.config.assertions) {
this.$set(this.environment.config, 'assertions', {
duration: {duration: 0},
regex: [],
jsonPath: [],
xpath2: [],
jsr223: [],
document: {type: "JSON", data: {json: [], xml: []}},
});
}
this.isRefresh = false;
this.$nextTick(() => {
this.isRefresh = true;
});
this.envEnable = o.enable;
},
//projectId
'environment.currentProjectId'() {
// el-select''''projectId使
if (!this.environment.currentProjectId) {
this.environment.projectId = null;
} else {
this.environment.projectId = this.environment.currentProjectId;
}
}
},
computed: {
hasLicense() {
let license = hasLicense();
return license;
},
},
methods: {
updateGlobalScript(isPreScript, filedName, value) {
if (isPreScript) {
if (filedName === "connScenario") {
this.environment.config.globalScriptConfig.connScenarioPreScript = value;
} else if (filedName === "execAfterPrivateScript") {
this.environment.config.globalScriptConfig.isPreScriptExecAfterPrivateScript = value;
} else if (filedName === "filterRequest") {
this.environment.config.globalScriptConfig.filterRequestPreScript = value;
}
} else {
if (filedName === "connScenario") {
this.environment.config.globalScriptConfig.connScenarioPostScript = value;
} else if (filedName === "execAfterPrivateScript") {
this.environment.config.globalScriptConfig.isPostScriptExecAfterPrivateScript = value;
} else if (filedName === "filterRequest") {
this.environment.config.globalScriptConfig.filterRequestPostScript = value;
}
}
},
save() {
this.$refs['environment'].validate((valid) => {
if (valid && this.$refs.commonConfig.validate() && this.$refs.httpConfig.validate()) {
this._save(this.environment);
}
});
},
openHis(logType) {
this.$refs.changeHistory.open(this.environment.id, ["项目-环境设置", "項目-環境設置", "Project environment setting"], logType);
},
validate() {
let isValidate = false;
this.$refs['environment'].validate((valid) => {
if (valid && this.$refs.commonConfig.validate() && this.$refs.httpConfig.validate()) {
isValidate = true;
} else {
isValidate = false;
}
});
return isValidate;
},
geFiles(obj) {
let uploadFiles = [];
obj.uploadIds = [];
if (obj.config && obj.config.sslConfig && obj.config.sslConfig.files) {
obj.config.sslConfig.files.forEach(item => {
if (item.file && item.file.size > 0) {
if (!item.id) {
item.name = item.file.name;
item.id = getUUID();
}
obj.uploadIds.push(item.id);
uploadFiles.push(item.file);
}
})
}
return uploadFiles;
},
getVariablesFiles(obj) {
let variablesFiles = [];
obj.variablesFilesIds = [];
// csv
if (obj.config.commonConfig.variables) {
obj.config.commonConfig.variables.forEach(param => {
if (param.type === 'CSV' && param.files) {
param.files.forEach(item => {
if (item.file && item.file.name) {
if (!item.id) {
let fileId = getUUID().substring(0, 12);
item.name = item.file.name;
item.id = fileId;
}
obj.variablesFilesIds.push(item.id);
variablesFiles.push(item.file);
}
})
}
});
}
return variablesFiles;
},
check(items) {
let repeatKey = "";
items.forEach((item, index) => {
items.forEach((row, rowIndex) => {
if (item.name === row.name && index !== rowIndex) {
repeatKey = item.name;
}
});
});
return repeatKey;
},
_save(environment) {
if (!environment.projectId) {
this.$warning(this.$t('api_test.select_project'));
return;
}
if (environment && environment.config && environment.config.commonConfig && environment.config.commonConfig.variables) {
let repeatKey = this.check(environment.config.commonConfig && environment.config.commonConfig.variables);
if (repeatKey !== "") {
this.$warning(this.$t('api_test.environment.common_config') + "【" + repeatKey + "】" + this.$t('load_test.param_is_duplicate'));
return;
}
}
let message = '';
if (environment && environment.config && environment.config.httpConfig && environment.config.httpConfig.conditions) {
environment.config.httpConfig.conditions.forEach(env => {
if (env.type === "MODULE" && env.details.length === 0) {
message += this.$t('load_test.domain') + ":" + env.socket + ":" + this.$t('api_test.environment.module_warning');
return;
}
if (env.type === "PATH" && env.details) {
env.details.forEach(item => {
if (!item.name) {
message += this.$t('load_test.domain') + ":" + env.socket + ":" + this.$t('api_test.environment.path_warning');
return;
}
})
}
})
}
environment.config.commonConfig.variables.forEach(variable => {
if (variable.type === 'CSV' && variable.files.length === 0) {
message = this.$t('api_test.automation.csv_warning');
return;
}
})
if (message) {
this.$warning(message);
return;
}
let bodyFiles = this.geFiles(environment);
let variablesFiles = this.getVariablesFiles(environment);
let formData = new FormData();
if (bodyFiles) {
bodyFiles.forEach(f => {
formData.append("files", f);
})
}
if (variablesFiles) {
variablesFiles.forEach(f => {
formData.append("variablesFiles", f);
})
}
let param = this.buildParam(environment);
formData.append('request', new Blob([JSON.stringify(param)], {type: "application/json"}));
editEnv(formData, param).then((response) => {
this.$success(this.$t('commons.save_success'));
this.$emit('refreshAfterSave'); //EnvironmentList.vue使
this.cancel()
}, error => {
this.$emit('errorRefresh', error);
});
},
buildParam: function (environment) {
let param = {};
Object.assign(param, environment);
let hosts = param.config.commonConfig.hosts;
if (hosts != undefined) {
let validHosts = [];
// host
hosts.forEach(host => {
if (host.status === '') {
validHosts.push(host);
}
});
param.config.commonConfig.hosts = validHosts;
}
param.config = JSON.stringify(param.config);
return param;
},
cancel() {
this.$emit('close');
},
confirm() {
this.$emit("confirm");
},
clearValidate() {
this.$refs["environment"].clearValidate();
},
initVariables() {
if (!this.environment.config.commonConfig) {
this.$set(this.environment.config, 'commonConfig', new CommonConfig());
} else {
if (this.environment.config.commonConfig.variables) {
this.environment.config.commonConfig.variables.forEach(v => {
if (!v.scope) {
this.$set(v, "scope", "api");
}
})
}
}
}
},
}
</script>
<style scoped>
.ms-opt-btn {
position: absolute;
z-index: 10;
margin-top: 28px;
}
span {
display: block;
margin-bottom: 15px;
}
span:not(:first-child) {
margin-top: 15px;
}
.errorReportConfigSwitch :deep(.el-switch__label) {
color: #D8DAE2;
}
.errorReportConfigSwitch :deep(.is-active) {
color: var(--count_number);
}
</style>

View File

@ -0,0 +1,583 @@
<template>
<el-form :model="condition" :rules="rules" ref="httpConfig" class="ms-el-form-item__content" :disabled="isReadOnly">
<div class="ms-border">
<el-form-item prop="socket">
<el-row type="flex" justify="space-between">
<el-col :span="14">
<span class="ms-env-span" style="line-height: 30px;">{{ $t('api_test.environment.socket') }}</span>
<el-input v-model="condition.socket" style="width: 85%"
:placeholder="$t('api_test.request.url_description')" clearable size="small">
<template slot="prepend">
<el-select v-model="condition.protocol" class="request-protocol-select" size="small">
<el-option label="http://" value="http"/>
<el-option label="https://" value="https"/>
</el-select>
</template>
</el-input>
</el-col>
<el-col :span="10">
<span style="margin-right: 12px; line-height: 30px;">{{ $t('commons.description') }}</span>
<el-input v-model="condition.description" maxlength="200" :show-word-limit="true" size="small"
style="width: 70%;"/>
</el-col>
</el-row>
</el-form-item>
<el-form-item prop="enable">
<span class="ms-env-span">{{ $t('api_test.environment.condition_enable') }}</span>
<el-radio-group v-model="condition.type" @change="typeChange">
<el-radio label="NONE">{{ $t('api_test.definition.document.data_set.none') }}</el-radio>
<el-radio label="MODULE">{{ $t('test_track.module.module') }}</el-radio>
<el-radio label="PATH">{{ $t('api_test.definition.api_path') }}</el-radio>
</el-radio-group>
<div v-if="condition.type === 'MODULE'" style="margin-top: 6px">
<ms-select-tree size="small" :data="moduleOptions" :default-key="condition.ids" @getValue="setModule"
:obj="moduleObj" clearable :checkStrictly="true" multiple v-if="!loading"/>
</div>
<div v-if="condition.type === 'PATH'" style="margin-top: 6px">
<el-input v-model="pathDetails.name" :placeholder="$t('api_test.value')" clearable size="small">
<template v-slot:prepend>
<el-select v-model="pathDetails.value" class="request-protocol-select" size="small">
<el-option :label="$t('api_test.request.assertions.contains')" value="contains"/>
<el-option :label="$t('commons.adv_search.operators.equals')" value="equals"/>
</el-select>
</template>
</el-input>
</div>
<!-- 接口测试配置 -->
<form-section :title="$t('commons.api')" :init-active=true>
<p>{{ $t('api_test.request.headers') }}</p>
<el-row>
<el-link class="ms-el-link" @click="batchAdd" style="color: #783887"> {{
$t("commons.batch_add")
}}
</el-link>
</el-row>
<ms-api-key-value :items="condition.headers" :isShowEnable="true" :suggestions="headerSuggestions"/>
<div style="margin-top: 20px">
<el-button v-if="!condition.id" type="primary" style="float: right" size="mini" @click="add">
{{ $t('commons.add') }}
</el-button>
<div v-else>
<el-button type="primary" style="float: right;margin-left: 10px" size="mini" @click="clear">
{{ $t('commons.clear') }}
</el-button>
<el-button type="primary" style="float: right" size="mini" @click="update(condition)">{{
$t('commons.update')
}}
</el-button>
</div>
</div>
</form-section>
<!-- UI 配置 -->
<form-section :title="$t('commons.ui_test')" :init-active="false">
<el-row :gutter="10" style="padding-top: 10px;">
<el-col :span="6">
<!-- 浏览器驱动 -->
<span style="margin-right: 10px;">{{$t("ui.browser")}}</span>
<el-select
size="mini"
v-model="httpConfig.browser"
style="width: 100px"
>
<el-option
v-for="b in browsers"
:key="b.value"
:value="b.value"
:label="b.label"
></el-option>
</el-select>
</el-col>
<el-col :span="6">
<!-- 性能模式 -->
<el-checkbox
v-model="httpConfig.headlessEnabled"
>
<span> {{ $t("ui.performance_mode") }}</span>
</el-checkbox>
<ms-instructions-icon size="10" :content="$t('ui.per_tip')"/>
</el-col>
</el-row>
<el-row :gutter="10">
<el-col :span="24">
<ms-ui-scenario-cookie-table :items="httpConfig.cookie"/>
</el-col>
</el-row>
</form-section>
</el-form-item>
</div>
<div class="ms-border">
<el-table :data="httpConfig.conditions" highlight-current-row @current-change="selectRow" ref="envTable">
<el-table-column prop="socket" :label="$t('load_test.domain')" show-overflow-tooltip width="180">
<template v-slot:default="{row}">
{{ getUrl(row) }}
</template>
</el-table-column>
<el-table-column prop="type" :label="$t('api_test.environment.condition_enable')" show-overflow-tooltip
min-width="100px">
<template v-slot:default="{row}">
{{ getName(row) }}
</template>
</el-table-column>
<el-table-column prop="details" show-overflow-tooltip min-width="120px" :label="$t('api_test.value')">
<template v-slot:default="{row}">
{{ getDetails(row) }}
</template>
</el-table-column>
<el-table-column prop="createTime" show-overflow-tooltip min-width="120px" :label="$t('commons.create_time')">
<template v-slot:default="{row}">
<span>{{ row.time | datetimeFormat }}</span>
</template>
</el-table-column>
<el-table-column prop="description" show-overflow-tooltip min-width="120px" :label="$t('commons.description')">
<template v-slot:default="{row}">
{{ row.description }}
</template>
</el-table-column>
<el-table-column :label="$t('commons.operating')" width="100px">
<template v-slot:default="{row}">
<div>
<ms-table-operator-button :tip="$t('api_test.automation.copy')"
icon="el-icon-document-copy" @exec="copy(row)"/>
<ms-table-operator-button :tip="$t('api_test.automation.remove')"
icon="el-icon-delete" @exec="remove(row)" type="danger"/>
</div>
</template>
</el-table-column>
</el-table>
<batch-add-parameter @batchSave="batchSave" ref="batchAdd"/>
</div>
</el-form>
</template>
<script>
import {HttpConfig} from "metersphere-frontend/src/model/EnvironmentModel";
import {getApiModuleByProjectIdAndProtocol} from "metersphere-frontend/src/api/environment";
import MsApiKeyValue from "metersphere-frontend/src/components/environment/commons/ApiKeyValue";
import {REQUEST_HEADERS} from "metersphere-frontend/src/utils/constants";
import MsSelectTree from "metersphere-frontend/src/components/select-tree/SelectTree";
import MsTableOperatorButton from "metersphere-frontend/src/components/MsTableOperatorButton";
import {getUUID} from "metersphere-frontend/src/utils";
import {KeyValue} from "metersphere-frontend/src/model/EnvTestModel";
import Vue from "vue";
import BatchAddParameter from "metersphere-frontend/src/components/environment/commons/BatchAddParameter";
import FormSection from "metersphere-frontend/src/components/form/FormSection";
import MsInstructionsIcon from 'metersphere-frontend/src/components/MsInstructionsIcon';
import MsUiScenarioCookieTable from "./ui-related/UiScenarioCookieTable";
export default {
name: "MsEnvironmentHttpConfig",
components: {
MsUiScenarioCookieTable,
FormSection, MsApiKeyValue, MsSelectTree, MsTableOperatorButton, BatchAddParameter, MsInstructionsIcon},
props: {
httpConfig: new HttpConfig({cookie: []}),
projectId: String,
isReadOnly: {
type: Boolean,
default: false
},
},
created() {
this.list();
if (this.httpConfig && !this.httpConfig.cookie) {
this.$set(this.httpConfig, "cookie", []);
}
},
data() {
let socketValidator = (rule, value, callback) => {
if (!this.validateSocket(value)) {
callback(new Error(this.$t("commons.formatErr")));
return false;
} else {
callback();
return true;
}
};
return {
headerSuggestions: REQUEST_HEADERS,
rules: {
socket: [{required: false, validator: socketValidator, trigger: "blur"}],
},
moduleOptions: [],
moduleObj: {
id: "id",
label: "name",
},
loading: false,
pathDetails: new KeyValue({name: "", value: "contains"}),
condition: {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
port: 0,
headers: [new KeyValue()],
headlessEnabled: true,
browser : 'CHROME'
},
beforeCondition: {},
browsers: [
{
label: this.$t("chrome"),
value: "CHROME",
},
{
label: this.$t("firefox"),
value: "FIREFOX",
},
],
};
},
watch: {
projectId() {
this.list();
},
httpConfig: function (o) {
//
if (this.httpConfig && this.httpConfig.socket && this.httpConfig.conditions && this.httpConfig.conditions.length === 0) {
this.condition.type = "NONE";
this.condition.socket = this.httpConfig.socket;
this.condition.protocol = this.httpConfig.protocol;
this.condition.port = this.httpConfig.port;
this.condition.domain = this.httpConfig.domain;
this.condition.time = new Date().getTime();
this.condition.headers = this.httpConfig.headers;
this.condition.description = this.httpConfig.description;
this.add();
}
this.condition = {
id: undefined,
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
port: 0,
headers: [new KeyValue()]
};
},
},
methods: {
getUrl(row) {
return row.protocol + "://" + row.socket;
},
getName(row) {
switch (row.type) {
case "NONE":
return this.$t("api_test.definition.document.data_set.none");
case "MODULE":
return this.$t("test_track.module.module");
case "PATH":
return this.$t("api_test.definition.api_path");
}
},
clearHisData() {
this.httpConfig.socket = undefined;
this.httpConfig.protocol = undefined;
this.httpConfig.port = undefined;
this.httpConfig.domain = undefined;
},
getDetails(row) {
if (row && row.type === "MODULE") {
if (row.details && row.details instanceof Array) {
let value = "";
row.details.forEach((item) => {
value += item.name + ",";
});
if (value.endsWith(",")) {
value = value.substr(0, value.length - 1);
}
return value;
}
} else if (row && row.type === "PATH" && row.details.length > 0 && row.details[0].name) {
return row.details[0].value === "equals" ? this.$t("commons.adv_search.operators.equals") + row.details[0].name : this.$t("api_test.request.assertions.contains") + row.details[0].name;
} else {
return "";
}
},
selectRow(row) {
this.condition = {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
port: 0,
headers: [new KeyValue()]
};
if (row) {
this.httpConfig.socket = row.socket;
this.httpConfig.protocol = row.protocol;
this.httpConfig.port = row.port;
this.httpConfig.description = row.description;
this.condition = row;
if (!this.condition.headers) {
this.condition.headers = [new KeyValue()];
}
if (row.type === "PATH" && row.details.length > 0) {
this.pathDetails = JSON.parse(JSON.stringify(row.details[0]));
} else if (row.type === "MODULE" && row.details.length > 0) {
this.condition.ids = [];
row.details.forEach((item) => {
this.condition.ids.push(item.value);
});
}
}
this.beforeCondition = JSON.parse(JSON.stringify(this.condition));
this.reload();
},
typeChange() {
if (this.condition.type === "NONE" && this.condition.id && this.checkNode(this.condition.id)) {
this.condition.type = this.beforeCondition.type;
this.$warning(this.$t('api_test.environment.repeat_warning'));
return;
}
switch (this.condition.type) {
case "NONE":
this.condition.details = [];
break;
case "MODULE":
this.condition.details = [];
break;
case "PATH":
this.pathDetails = new KeyValue({name: "", value: "contains"});
break;
}
},
list() {
if (this.projectId) {
this.result = getApiModuleByProjectIdAndProtocol(this.projectId, "HTTP").then((response) => {
if (response.data && response.data !== null) {
this.moduleOptions = response.data;
}
});
} else { //projectId
this.moduleOptions = [];
}
},
setModule(id, data) {
if (data && data.length > 0) {
this.condition.details = [];
data.forEach((item) => {
this.condition.details.push(new KeyValue({name: item.name, value: item.id}));
});
}
},
update() {
const index = this.httpConfig.conditions.findIndex((d) => d.id === this.condition.id);
this.validateSocket(this.condition.socket);
let obj = {
id: this.condition.id,
type: this.condition.type,
domain: this.condition.domain,
socket: this.condition.socket,
headers: this.condition.headers,
protocol: this.condition.protocol,
port: this.condition.port,
time: this.condition.time
};
if (obj.type === "PATH") {
this.httpConfig.conditions[index].details = [this.pathDetails];
} else {
obj.details = this.condition.details ? JSON.parse(JSON.stringify(this.condition.details)) : this.condition.details;
}
if (index !== -1) {
Vue.set(this.httpConfig.conditions[index], obj, 1);
this.condition = {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
headers: [new KeyValue()]
};
this.reload();
}
this.$refs.envTable.setCurrentRow(0);
},
clear() {
this.condition = {
type: "NONE",
details: [new KeyValue({name: "", value: "contains"})],
protocol: "http",
socket: "",
domain: "",
headers: [new KeyValue()]
};
this.$refs.envTable.setCurrentRow(0);
},
reload() {
this.loading = true
this.$nextTick(() => {
this.loading = false
});
},
checkNode(id) {
let index = 1;
this.httpConfig.conditions.forEach(item => {
if (item.type === "NONE") {
if (!id || id !== item.id) {
index++;
}
}
})
return index > 1;
},
add() {
if (this.condition.type === "NONE" && this.checkNode()) {
this.$warning(this.$t('api_test.environment.repeat_warning'));
return;
}
this.validateSocket();
let obj = {
id: getUUID(),
type: this.condition.type,
socket: this.condition.socket,
protocol: this.condition.protocol,
headers: this.condition.headers,
domain: this.condition.domain,
port: this.condition.port,
time: new Date().getTime(),
description: this.condition.description
};
if (this.condition.type === "PATH") {
obj.details = [JSON.parse(JSON.stringify(this.pathDetails))];
} else {
obj.details = this.condition.details ? JSON.parse(JSON.stringify(this.condition.details)) : this.condition.details;
}
this.httpConfig.conditions.unshift(obj);
this.clearHisData();
},
remove(row) {
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
this.httpConfig.conditions.splice(index, 1);
this.clearHisData();
},
copy(row) {
if (row.type === "NONE") {
this.$warning(this.$t('api_test.environment.copy_warning'));
return;
}
const index = this.httpConfig.conditions.findIndex((d) => d.id === row.id);
let obj = {
id: getUUID(),
type: row.type,
socket: row.socket,
details: row.details,
protocol: row.protocol,
headers: JSON.parse(JSON.stringify(row.headers)),
domain: row.domain,
time: new Date().getTime()
};
if (index != -1) {
this.httpConfig.conditions.splice(index, 0, obj);
} else {
this.httpConfig.conditions.push(obj);
}
},
validateSocket(socket) {
if (!socket) return true;
let urlStr = this.condition.protocol + "://" + socket;
let url = {};
try {
url = new URL(urlStr);
} catch (e) {
return false;
}
this.condition.domain = decodeURIComponent(url.hostname);
this.condition.port = url.port;
let path = url.pathname === "/" ? "" : url.pathname;
if (url.port) {
this.condition.socket = this.condition.domain + ":" + url.port + path;
} else {
this.condition.socket = this.condition.domain + path;
}
return true;
},
validate() {
let isValidate = false;
this.$refs["httpConfig"].validate((valid) => {
isValidate = valid;
});
return isValidate;
},
batchAdd() {
this.$refs.batchAdd.open();
},
_handleBatchVars(data) {
let params = data.split("\n");
let keyValues = [];
params.forEach(item => {
if (item) {
let line = item.split(/|:/);
let values = item.split(line[0] + ":");
let required = false;
keyValues.push(new KeyValue({
name: line[0],
required: required,
value: values[1],
type: "text",
valid: false,
file: false,
encode: true,
enable: true,
contentType: "text/plain"
}));
}
});
return keyValues;
},
batchSave(data) {
if (data) {
let keyValues = this._handleBatchVars(data);
keyValues.forEach(keyValue => {
let isAdd = true;
for (let i in this.condition.headers) {
let item = this.condition.headers[i];
if (item.name === keyValue.name) {
item.value = keyValue.value;
isAdd = false;
}
}
if (isAdd) {
this.condition.headers.splice(this.condition.headers.indexOf(h => !h.name), 0, keyValue);
}
})
}
},
},
};
</script>
<style scoped>
.request-protocol-select {
width: 90px;
}
.ms-env-span {
margin-right: 10px;
}
:deep(.el-form-item) {
margin-bottom: 15px;
}
.ms-el-form-item__content :deep(.el-form-item__content) {
line-height: 20px;
}
.ms-el-link {
float: right;
margin-right: 45px;
}
</style>

View File

@ -0,0 +1,28 @@
<template>
<div>
<ms-tag v-if="value == 'Prepare'" type="info" :content="$t('test_track.plan.plan_status_prepare')"/>
<ms-tag v-if="value == 'Underway'" type="primary" :content="$t('test_track.plan.plan_status_running')"/>
<ms-tag v-if="value == 'Finished'" type="warning" :content="$t('test_track.plan.plan_status_finished')"/>
<ms-tag v-if="value == 'Completed'" type="success" :content="$t('test_track.plan.plan_status_completed')"/>
<ms-tag v-if="value === 'Trash'" type="danger" effect="plain" :content="$t('test_track.plan.plan_status_trash')"/>
<ms-tag v-if="value == 'Archived'" type="danger" :content="$t('test_track.plan.plan_status_archived')"/>
</div>
</template>
<script>
import MsTag from "metersphere-frontend/src/components/MsTag";
export default {
name: "PlanStatusTableItem",
components: {MsTag},
props: {
value: {
type: String
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,71 @@
<template>
<el-button-group>
<el-tooltip
v-for="item in tabList"
:key="item.domKey"
class="item"
effect="dark"
:content="item.tip"
:placement="item.placement"
>
<el-button
plain
:class="{ active: currentActiveDom === item.domKey }"
@click="changeTab(item.domKey)"
>{{ item.content }}</el-button
>
</el-tooltip>
</el-button-group>
</template>
<script>
export default {
name: "ToggleTabs",
props: {
activeDom: String,
tabList: {
type: Array,
default() {
return [
{
domKey: "default",
tip: "default",
content: "default",
placement: "top",
enable: true,
},
];
},
},
},
computed: {
currentActiveDom() {
return this.activeDom;
},
},
methods: {
changeTab(domKey) {
this.$emit("update:activeDom", domKey);
this.$emit("toggleTab", domKey)
},
},
};
</script>
<style scoped>
.active {
border: solid 1px #6d317c !important;
background-color: var(--primary_color) !important;
color: #ffffff !important;
}
.case-button {
border-left: solid 1px var(--primary_color);
}
.item {
height: 32px;
padding: 5px 8px;
border: solid 1px var(--primary_color);
}
</style>

View File

@ -0,0 +1,328 @@
<template>
<div>
<slot name="header"></slot>
<ms-node-tree
:is-display="getIsRelevance"
v-loading="result.loading"
:tree-nodes="data"
:allLabel="$t('ui.custom_command_label')"
:type="isReadOnly ? 'view' : 'edit'"
:delete-permission="['PROJECT_UI_SCENARIO:READ+DELETE']"
:add-permission="['PROJECT_UI_SCENARIO:READ+CREATE']"
:update-permission="['PROJECT_UI_SCENARIO:READ+EDIT']"
:show-case-num="showCaseNum"
:hide-opretor="isTrashData"
local-suffix="ui_module"
:default-label="'未规划模块'"
@add="add"
@edit="edit"
@drag="drag"
@remove="remove"
@refresh="list"
@filter="filter"
@nodeSelectEvent="nodeChange"
class="element-node-tree"
ref="nodeTree">
<template v-slot:header>
<ms-search-bar
:show-operator="showOperator && !isTrashData"
:condition="condition"
:commands="operators"/>
<module-trash-button v-if="!isReadOnly && !isTrashData" :condition="condition" :exe="enableTrash"
:total='total'/>
</template>
</ms-node-tree>
<ms-add-basis-scenario
@saveAsEdit="saveAsEdit"
@refresh="refresh"
ref="basisScenario"/>
</div>
</template>
<script>
import SelectMenu from "metersphere-frontend/src/components/environment/snippet/ext/SelectMenu";
import MsNodeTree from "metersphere-frontend/src/components/environment/snippet/ext/NodeTree";
import {buildTree} from "metersphere-frontend/src/model/NodeTree";
import MsSearchBar from "metersphere-frontend/src/components/search/MsSearchBar";
import {getCurrentProjectID} from "metersphere-frontend/src/utils/token";
import {
addScenarioModule,
deleteScenarioModule,
dragScenarioModule,
editScenarioModule,
getScenarioModules,
posScenarioModule
} from "./ui-scenario";
export default {
name: 'UiCustomCommandModule',
components: {
MsSearchBar,
MsNodeTree,
SelectMenu,
},
props: {
currentType: String,
isReadOnly: {
type: Boolean,
default() {
return false;
}
},
showOperator: Boolean,
relevanceProjectId: String,
pageSource: String,
total: Number,
isTrashData: Boolean,
planId: String,
showCaseNum: {
type: Boolean,
default: true
}
},
computed: {
isPlanModel() {
return this.planId ? true : false;
},
isRelevanceModel() {
return this.relevanceProjectId ? true : false;
},
projectId() {
return getCurrentProjectID();
},
getIsRelevance() {
if (this.pageSource !== 'scenario') {
return this.openType;
} else {
return "scenario";
}
}
},
data() {
return {
openType: 'relevance',
result: {},
condition: {
filterText: "",
trashEnable: false
},
data: [],
currentModule: undefined,
operators: [
{
label: this.$t('api_test.api_import.label'),
callback: this.handleImport,
permissions: ['PROJECT_UI_SCENARIO:READ+IMPORT_SCENARIO']
},
// Sence v2.4 custom command is no longer supported export IDE,
// MS export is supported in subsequent versions
// {
// label: this.$t('report.export'),
// callback: this.exportSide,
// permissions: ['PROJECT_UI_SCENARIO:READ+EXPORT_SCENARIO']
// }
]
}
},
mounted() {
this.list();
},
watch: {
'condition.filterText'() {
this.filter();
},
'condition.trashEnable'() {
this.$emit('enableCustomTrash', this.condition.trashEnable);
},
relevanceProjectId() {
this.list(this.relevanceProjectId);
},
isTrashData() {
this.condition.trashEnable = this.isTrashData;
this.list();
}
},
methods: {
saveAsEdit(data) {
data.type = "add";
data.scenarioType = this.currentType;
this.$emit('saveAsEdit', data);
},
refresh() {
this.$emit("refreshTable");
},
handleImport() {
if (this.projectId) {
this.result = this.$get("/ui/scenario/module/list/" + this.projectId + "?type=" + this.currentType).then(response => {
if (response.data != undefined && response.data != null) {
this.data = response.data;
this.data.forEach(node => {
buildTree(node, {path: ''});
});
}
});
this.$refs.apiImport.open(this.currentModule);
}
},
filter() {
this.$refs.nodeTree.filter(this.condition.filterText);
},
list(projectId) {
if (this.isPlanModel) {
let url = '/ui/scenario/module/list/plan/' + this.planId;
this.$get(url).then(response => {
if (response.data != undefined && response.data != null) {
this.data = response.data;
this.data.forEach(node => {
buildTree(node, {path: ''});
});
}
})
} else {
getScenarioModules(projectId ? projectId : this.projectId, this.isTrashData, this.currentType).then((data) => {
if (data) {
this.data = data.data;
this.data.forEach(node => {
buildTree(node, {path: ''});
});
this.$emit('setCustomModuleOptions', this.data);
this.$emit('setCustomNodeTree', this.data);
if (this.$refs.nodeTree) {
this.$refs.nodeTree.filter(this.condition.filterText);
}
}
});
}
},
edit(param) {
param.projectId = this.projectId;
param.scenarioType = this.currentType;
editScenarioModule(param).then(() => {
this.$success(this.$t('commons.save_success'));
this.list();
});
this.refresh()
},
add(param) {
param.projectId = this.projectId;
param.scenarioType = this.currentType;
if (param && param.level >= 9) {
this.list();
this.$error("模块树最大深度为8层");
return;
}
addScenarioModule(param).then(() => {
this.$success(this.$t('commons.save_success'));
this.list();
});
//
this.refresh();
},
remove(nodeIds) {
deleteScenarioModule(nodeIds).then(() => {
this.list();
this.refresh();
this.$emit("customNodeChange")
});
},
drag(param, list) {
param.scenarioType = this.currentType;
dragScenarioModule(param).then(() => {
posScenarioModule(list).then(() => {
this.list();
});
});
this.refresh();
},
nodeChange(node, nodeIds, pNodes) {
this.currentModule = node.data;
if (node.data.id === 'root') {
this.$emit("customNodeChange", node, [], pNodes);
} else {
this.$emit("customNodeChange", node, nodeIds, pNodes);
}
},
addScenario() {
if (!this.projectId) {
this.$warning(this.$t('commons.check_project_tip'));
return;
}
this.$refs.basisScenario.open(this.currentModule);
},
enableTrash() {
this.condition.trashEnable = true;
this.$emit('enableCustomTrash', this.condition.trashEnable);
},
exportSide() {
this.$emit('exportSide', this.data);
}
}
}
</script>
<style scoped>
.node-tree {
margin-top: 15px;
margin-bottom: 15px;
}
.ms-el-input {
height: 25px;
line-height: 25px;
}
.custom-tree-node {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
width: 100%;
}
.father .child {
display: none;
}
.father:hover .child {
display: block;
}
.node-title {
width: 0;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0 5px;
overflow: hidden;
}
.node-operate > i {
color: #409eff;
margin: 0 5px;
}
:deep(.el-tree-node__content) {
height: 33px;
}
.ms-api-buttion {
width: 30px;
}
.element-node-tree {
width: 100%;
/* min-width: 290px; */
}
:deep(.element-node-tree .recycle .el-col.el-col-3) {
text-align: center;
}
</style>

View File

@ -0,0 +1,569 @@
<template>
<div>
<div
style="
border: 1px #dcdfe6 solid;
min-height: 50px;
border-radius: 4px;
width: 99%;
margin-top: 10px;
clear: both;
"
>
<ms-table
v-loading="loading"
row-key="id"
:data="variables"
:total="items.length"
:screen-height="'100px'"
:batch-operators="batchButtons"
:remember-order="true"
:highlightCurrentRow="true"
@refresh="onChange"
ref="variableTable"
>
<ms-table-column prop="cookie" label="cookie" min-width="160">
<template slot-scope="scope">
<el-input
v-model="scope.row.cookie"
size="mini"
:placeholder="$t('cookie')"
@change="change"
/>
</template>
</ms-table-column>
<ms-table-column
prop="userName"
:label="$t('api_test.request.sql.username')"
min-width="200"
>
<template slot-scope="scope">
<el-input
v-model="scope.row.userName"
size="mini"
maxlength="200"
:placeholder="$t('api_test.request.sql.username')"
show-word-limit
@change="change"
/>
</template>
</ms-table-column>
<ms-table-column
prop="password"
:label="$t('api_test.request.tcp.password')"
min-width="140"
>
<template slot-scope="scope">
<el-input
v-model="scope.row.password"
size="mini"
maxlength="200"
show-password
:placeholder="$t('api_test.request.tcp.password')"
@change="change"
/>
</template>
</ms-table-column>
<ms-table-column
prop="description"
:label="$t('commons.validity_period')"
min-width="200"
:editContent="'aaa'"
>
<template slot-scope="scope">
<mini-timing-item :expr="scope.row.expireTime"></mini-timing-item>
</template>
</ms-table-column>
<ms-table-column
prop="updateTime"
:label="$t('commons.update_time')"
min-width="160"
>
<template slot-scope="scope">
{{
scope.row.updateTime | datetimeFormat
}}
</template>
</ms-table-column>
<ms-table-column :label="$t('commons.operating')" width="150">
<template v-slot:default="scope">
<el-switch v-model="scope.row.enable" size="mini"></el-switch>
<el-tooltip
effect="dark"
:content="$t('关联登录场景/指令')"
placement="top-start"
>
<el-button
icon="el-icon-setting"
circle
size="mini"
@click="openRelevance"
v-if="!existCookieConfig"
style="margin-left: 10px"
/>
<el-dropdown @command="handleCommand" v-if="existCookieConfig">
<el-button
icon="el-icon-paperclip"
circle
size="mini"
style="margin-left: 10px"
/>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="view">查看关联</el-dropdown-item>
<el-dropdown-item command="cancelRelevance">取消关联</el-dropdown-item>
<el-dropdown-item command="relevance">重新关联</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-tooltip>
</template>
</ms-table-column>
</ms-table>
</div>
<batch-add-parameter @batchSave="batchSave" ref="batchAdd"/>
<api-variable-setting ref="apiVariableSetting"></api-variable-setting>
<!-- 关联登录获取cookie的场景 -->
<ui-scenario-edit-relevance
ref="relevanceUiDialog"
@reference="reference"
:scenarioType="currentRelevanceType"
/>
<variable-import
ref="variableImport"
@mergeData="mergeData"
></variable-import>
</div>
</template>
<script>
import {KeyValue} from "metersphere-frontend/src/model/EnvTestModel";
import MsApiVariableInput from "metersphere-frontend/src/components/environment/commons/ApiVariableInput";
import BatchAddParameter from "metersphere-frontend/src/components/environment/commons/BatchAddParameter";
import MsTableButton from "metersphere-frontend/src/components/MsTableButton";
import MsTable from "metersphere-frontend/src/components/table/MsTable";
import MsTableColumn from "metersphere-frontend/src/components/table/MsTableColumn";
import ApiVariableSetting from "metersphere-frontend/src/components/environment/commons/ApiVariableSetting";
import CsvFileUpload from "metersphere-frontend/src/components/environment/commons/variable/CsvFileUpload";
import {downloadFile, getUUID, operationConfirm} from "metersphere-frontend/src/utils";
import VariableImport from "metersphere-frontend/src/components/environment/VariableImport";
import _ from "lodash";
import MiniTimingItem from "metersphere-frontend/src/components/environment/commons/MiniTimingItem";
import UiScenarioEditRelevance from "@/business/menu/environment/components/ui-related/UiScenarioEditRelevance";
export default {
name: "MsUiScenarioCookieTable",
components: {
BatchAddParameter,
MsApiVariableInput,
MsTableButton,
MsTable,
MsTableColumn,
ApiVariableSetting,
CsvFileUpload,
VariableImport,
MiniTimingItem,
UiScenarioEditRelevance,
},
props: {
items: Array,
isReadOnly: {
type: Boolean,
default: false,
},
showVariable: {
type: Boolean,
default: true,
},
showCopy: {
type: Boolean,
default: true,
},
},
data() {
return {
loading: false,
screenHeight: "400px",
batchButtons: [
{
name: this.$t("api_test.definition.request.batch_delete"),
handleClick: this.handleDeleteBatch,
},
],
typeSelectOptions: [
{value: "CONSTANT", label: this.$t("api_test.automation.constant")},
{value: "LIST", label: this.$t("test_track.case.list")},
{value: "CSV", label: "CSV"},
{value: "COUNTER", label: this.$t("api_test.automation.counter")},
{value: "RANDOM", label: this.$t("api_test.automation.random")},
],
variables: {},
selectVariable: "",
editData: {},
scopeTypeFilters: [
{text: this.$t("commons.api"), value: "api"},
{text: this.$t("commons.ui_test"), value: "ui"},
],
currentRelevanceType: 'scenario',
existCookieConfig: false
};
},
watch: {
items: {
handler(v) {
this.variables = v;
this.sortParameters();
},
immediate: true,
deep: true,
},
variables: {
handler(v) {
if (this.variables && this.variables.length && this.variables[0].relevanceId) {
this.existCookieConfig = true;
} else {
this.existCookieConfig = false;
}
},
immediate: true,
deep: true,
}
},
methods: {
remove: function (index) {
const dataIndex = this.variables.findIndex((d) => d.name === index.name);
this.variables.splice(dataIndex, 1);
this.$emit("change", this.variables);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
let repeatKey = "";
this.variables.forEach((item, index) => {
this.variables.forEach((row, rowIndex) => {
if (item.name === row.name && index !== rowIndex) {
repeatKey = item.name;
}
});
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (repeatKey !== "") {
this.$warning(
this.$t("api_test.environment.common_config") +
"【" +
repeatKey +
"】" +
this.$t("load_test.param_is_duplicate")
);
}
this.$emit("change", this.variables);
},
changeType(data) {
data.value = "";
if (
!data.delimiter ||
(!data.files && data.files.length === 0) ||
!data.quotedData
) {
data.delimiter = ",";
data.files = [];
data.quotedData = "false";
}
},
valueText(data) {
switch (data.type) {
case "LIST":
return this.$t("api_test.environment.list_info");
case "CONSTANT":
return this.$t("api_test.value");
case "COUNTER":
case "RANDOM":
return this.$t("api_test.environment.advanced_setting");
default:
return this.$t("api_test.value");
}
},
querySearch(queryString, cb) {
let restaurants = [
{value: "UTF-8"},
{value: "UTF-16"},
{value: "GB2312"},
{value: "ISO-8859-15"},
{value: "US-ASCll"},
];
let results = queryString
? restaurants.filter(this.createFilter(queryString))
: restaurants;
// callback
cb(results);
},
sortParameters() {
let index = 1;
this.variables.forEach((item) => {
item.num = index;
if (!item.type || item.type === "text") {
item.type = "CONSTANT";
}
if (!item.id) {
item.id = getUUID();
}
if (item.remark) {
this.$set(item, "description", item.remark);
item.remark = undefined;
}
if (!item.scope) {
this.$set(item, "scope", "api");
}
index++;
});
},
handleDeleteBatch() {
operationConfirm(
this,
this.$t("api_test.environment.variables_delete_info") + " ",
() => {
let ids = this.$refs.variableTable.selectRows;
ids.forEach((row) => {
if (row.id) {
const index = this.variables.findIndex((d) => d.id === row.id);
if (index !== this.variables.length - 1) {
this.variables.splice(index, 1);
}
}
});
this.sortParameters();
this.$refs.variableTable.cancelCurrentRow();
this.$refs.variableTable.clear();
}
);
},
filter(scope) {
let datas = [];
this.variables.forEach((item) => {
if (this.selectVariable && this.selectVariable != "" && item.name) {
if (
item.name
.toLowerCase()
.indexOf(this.selectVariable.toLowerCase()) == -1
) {
item.hidden = true;
} else {
item.hidden = undefined;
}
} else {
item.hidden = undefined;
}
datas.push(item);
});
this.variables = datas;
},
filterScope(scope) {
let datas = [];
let variables = _.cloneDeep(this.variables);
variables.forEach((item) => {
if (scope == "api") {
if (
item.scope && item.scope != "api"
) {
item.hidden = true;
} else {
item.hidden = undefined;
}
} else {
if (item.scope == scope) {
item.hidden = undefined;
} else {
item.hidden = true;
}
}
datas.push(item);
});
this.variables = datas;
},
openSetting(data) {
this.$refs.apiVariableSetting.open(data);
},
isDisable: function (row) {
const index = this.variables.findIndex((d) => d.name === row.name);
return this.variables.length - 1 !== index;
},
_handleBatchVars(data) {
let params = data.split("\n");
let keyValues = [];
params.forEach((item) => {
if (item) {
let line = item.split(/|:/);
let values = item.split(line[0] + ":");
let required = false;
keyValues.push(
new KeyValue({
name: line[0],
required: required,
value: values[1],
type: "CONSTANT",
valid: false,
file: false,
encode: true,
enable: true,
description: undefined,
})
);
}
});
return keyValues;
},
batchAdd() {
this.$refs.batchAdd.open();
},
batchSave(data) {
if (data) {
let keyValues = this._handleBatchVars(data);
keyValues.forEach((keyValue) => {
let isAdd = true;
this.variables.forEach((item) => {
if (item.name === keyValue.name) {
item.value = keyValue.value;
isAdd = false;
}
});
if (isAdd) {
this.variables.splice(
this.variables.indexOf((i) => !i.name),
0,
keyValue
);
}
});
}
},
onChange() {
this.sortParameters();
},
exportJSON() {
let apiVariable = [];
this.$refs.variableTable.selectRows.forEach((r) => {
if (!r.scope || r.scope != "ui") {
apiVariable.push(r);
}
});
if (apiVariable.length < 1) {
this.$warning(this.$t("api_test.environment.select_api_variable"));
return;
}
let variablesJson = [];
let messages = "";
let rows = this.$refs.variableTable.selectRows;
rows.forEach((row) => {
if (row.type === "CSV") {
messages = this.$t("variables.csv_download");
}
if (row.name && (!row.scope || row.scope == "api")) {
variablesJson.push(row);
}
});
if (messages !== "") {
this.$warning(messages);
return;
}
downloadFile(
"MS_" + variablesJson.length + "_Environments_variables.json",
JSON.stringify(variablesJson)
);
},
importJSON() {
this.$refs.variableImport.open();
},
mergeData(data, modeId) {
JSON.parse(data).map((importData) => {
importData.id = getUUID();
importData.enable = true;
importData.showMore = false;
let sameNameIndex = this.variables.findIndex(
(d) => d.name === importData.name
);
if (sameNameIndex !== -1) {
if (modeId === "fullCoverage") {
this.variables.splice(sameNameIndex, 1, importData);
}
} else {
this.variables.splice(this.variables.length - 1, 0, importData);
}
});
},
handleCommand(c) {
switch (c) {
case "view":
break;
case "cancelRelevance":
this.variables[0].relevanceId = null;
break;
case "relevance":
this.openRelevance();
break;
default:
break;
}
},
openRelevance() {
this.$refs.relevanceUiDialog.open();
},
reference(id) {
this.variables[0].relevanceId = id.keys().next().value.id;
this.$refs.relevanceUiDialog.close();
this.$success(this.$t('commons.save_success'));
}
},
created() {
if (this.items.length === 0) {
this.items.push(new KeyValue({enable: true, expireTime: '1Y'}));
} else {
// api
_.forEach(this.items, item => {
if (!item.scope) {
this.$set(item, "scope", "api");
}
})
this.variables = this.items;
}
},
};
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
:deep(.table-select-icon) {
display: none !important;
}
</style>

View File

@ -0,0 +1,440 @@
<template>
<test-case-relevance-base
:dialog-title="$t('ui.import_by_list_label')"
@setProject="setProject"
ref="baseRelevance"
>
<template v-slot:aside>
<ui-scenario-module
v-if="currentType === 'scenario'"
currentType="scenario"
style="margin-top: 5px"
@nodeSelectEvent="nodeChange"
@refreshTable="refresh"
@setModuleOptions="setModuleOptions"
@enableTrash="false"
:is-read-only="true"
ref="nodeTree"
/>
<ui-custom-command-module
v-if="currentType === 'customCommand'"
currentType="customCommand"
style="margin-top: 5px"
@customNodeChange="customNodeChange"
@refreshTable="refresh"
@setCustomModuleOptions="setCustomModuleOptions"
@enableTrash="false"
:is-read-only="true"
ref="customNodeTree"
/>
</template>
<ui-scenario-list
v-if="currentType === 'scenario'"
currentType="scenario"
:module-options="moduleOptions"
:select-node-ids="selectNodeIds"
:select-project-id="projectId"
:trash-enable="false"
:batch-operators="[]"
:is-reference-table="true"
@selection="setData"
:is-relate="true"
:init-ui-table-opretion="'init'"
:custom-num="customNum"
:showDrag="false"
:mode="'import'"
ref="uiScenarioList"
>
<toggle-tabs
slot="tabChange"
:activeDom.sync="currentType"
:tabList="tabList"
></toggle-tabs>
</ui-scenario-list>
<ui-custom-command-list
v-if="currentType === 'customCommand'"
currentType="customCommand"
:module-options="customModuleOptions"
:select-node-ids="selectCustomNodeIds"
:select-project-id="projectId"
:trash-enable="false"
:batch-operators="[]"
:is-reference-table="true"
@selection="setData"
:is-relate="true"
:init-ui-table-opretion="'init'"
:custom-num="customNum"
:showDrag="false"
:mode="'import'"
ref="uiCustomScenarioList"
>
<toggle-tabs
slot="tabChange"
:activeDom.sync="currentType"
:tabList="tabList"
></toggle-tabs>
</ui-custom-command-list>
<template v-slot:headerBtn>
<!-- todo 场景引用 -->
<el-button
type="primary"
@click="reference"
:loading="buttonIsWorking"
@keydown.enter.native.prevent
size="mini"
>
{{ $t("api_test.scenario.reference") }}
</el-button>
</template>
</test-case-relevance-base>
</template>
<script>
import MsContainer from "metersphere-frontend/src/components/MsContainer";
import MsAsideContainer from "metersphere-frontend/src/components/MsAsideContainer";
import MsMainContainer from "metersphere-frontend/src/components/MsMainContainer";
import {hasLicense} from "metersphere-frontend/src/utils/permission.js";
import {getUUID} from 'metersphere-frontend/src/utils';
import RelevanceDialog from "metersphere-frontend/src/components/environment/snippet/ext/RelevanceDialog";
import TestCaseRelevanceBase from "metersphere-frontend/src/components/environment/snippet/ext/TestCaseRelevanceBase";
import UiScenarioModule from "./UiScenarioModule";
import UiScenarioList from "./UiScenarioList";
import ToggleTabs from "./ToggleTabs";
import UiCustomCommandList from "./UiCustomCommandList";
import UiCustomCommandModule from "./UiCustomCommandModule";
import {TYPE_TO_C} from "metersphere-frontend/src/model/Setting";
const VersionSelect = {};
export default {
name: "UiScenarioEditRelevance",
components: {
UiScenarioModule,
VersionSelect: VersionSelect.default,
TestCaseRelevanceBase,
RelevanceDialog,
UiScenarioList,
MsMainContainer,
MsAsideContainer,
MsContainer,
ToggleTabs,
UiCustomCommandList,
UiCustomCommandModule,
},
props: {
scenarioType: String,
},
data() {
return {
currentType: "scenario",
buttonIsWorking: false,
result: {},
currentProtocol: null,
selectNodeIds: [],
selectCustomNodeIds: [],
moduleOptions: [],
customModuleOptions: [],
isApiListEnable: true,
currentScenario: [],
currentScenarioIds: [],
projectId: "",
customNum: false,
versionOptions: [],
currentVersion: "",
versionEnable: true,
tabList: [
{
domKey: "scenario",
tip: this.$t("ui.scenario_title"),
content: this.$t("ui.scenario_title"),
placement: "left",
enable: true,
},
{
domKey: "customCommand",
tip: this.$t("ui.custom_command_title"),
content: this.$t("ui.custom_command_title"),
placement: "right",
enable: true,
},
],
};
},
watch: {
projectId(val) {
this.listByType(val);
},
scenarioType(val) {
this.currentType = this.scenarioType;
},
currentType(val) {
this.listByType(this.projectId);
this.searchByType();
},
},
computed: {
isScenario() {
return this.currentType === "scenario";
},
},
methods: {
listByType(projectId) {
this.$nextTick(() => {
this.isScenario
? this.$refs.nodeTree.list(this.projectId)
: this.$refs.customNodeTree.list(this.projectId);
});
},
changeButtonLoadingType() {
this.buttonIsWorking = false;
},
createScenarioDefinition(scenarios, data, referenced) {
let errArr = [];
for (let item of data) {
let scenarioDefinition = JSON.parse(item.scenarioDefinition);
if (!scenarioDefinition) {
continue;
}
if (
!scenarioDefinition.hashTree ||
scenarioDefinition.hashTree.length <= 0
) {
errArr.push(scenarioDefinition.name);
continue;
}
if (scenarioDefinition && scenarioDefinition.hashTree) {
//scenarioDefinition
this.handleScenarioDefinition(scenarioDefinition);
let variables = scenarioDefinition.variables
? scenarioDefinition.variables.filter((v) => {
return v.name;
})
: [];
if (item.scenarioType === "customCommand" && item.commandViewStruct) {
let innerVariables = this.handleInnerVariables(
item.commandViewStruct
);
if (innerVariables) {
variables = innerVariables;
}
}
let outputVariables = this.handleOutputVariables(
item.commandViewStruct
);
let obj = {
//id resourceId el-tree key
id: getUUID(),
name: item.name,
type: this.currentType,
variables: this.resetInnerVariable(variables),
outputVariables: this.resetOutVariable(outputVariables),
environmentMap: scenarioDefinition.environmentMap,
referenced: referenced,
resourceId: item.id,
hashTree: scenarioDefinition.hashTree,
projectId: item.projectId,
num: item.num,
versionName: item.versionName,
versionEnable: item.versionEnable,
description: item.description,
clazzName: this.isScenario
? TYPE_TO_C.get("UiScenario")
: TYPE_TO_C.get("customCommand"),
};
scenarios.push(obj);
}
}
if (errArr.length > 0) {
let msg = errArr.join("、");
if (msg && msg.length > 30) {
msg = msg.substring(0, 30) + "...";
}
let str = this.isScenario
? this.$t("ui.scenario")
: this.$t("ui.instruction");
this.$error(msg + " " + str + "为空,导入失败");
}
if (referenced === "REF") {
//
if (this.isScenario) {
//
this.$EventBus.$emit("handleScenarioREFEvent")
}
//
else {
this.$EventBus.$emit("handleCustomCommandREFEvent")
}
}
},
resetInnerVariable(vars) {
if (vars && Array.isArray(vars)) {
vars.forEach((v) => {
v.id = getUUID();
});
}
return vars;
},
resetOutVariable(vars) {
if (vars && Array.isArray(vars)) {
vars.forEach((v) => {
v.id = getUUID();
});
}
return vars;
},
handleInnerVariables(commandViewStruct) {
if (!commandViewStruct) {
return [];
}
let struct = JSON.parse(commandViewStruct);
if (struct && struct.length > 1) {
return struct[1].variables;
}
return [];
},
handleOutputVariables(commandViewStruct) {
if (!commandViewStruct) {
return [];
}
let struct = JSON.parse(commandViewStruct);
if (struct && struct.length > 1) {
let cur = struct[1].outputVariables;
if (cur) {
return cur;
}
}
return [];
},
handleScenarioDefinition(scenarioDefinition) {
if (!scenarioDefinition || !scenarioDefinition.hashTree || scenarioDefinition.hashTree.length <= 0) {
return;
}
scenarioDefinition.resourceId = scenarioDefinition.id || getUUID();
scenarioDefinition.id = getUUID();
//
if (scenarioDefinition.outputVariables) {
scenarioDefinition.outputVariables = "";
}
for (let item of scenarioDefinition.hashTree) {
item.id = getUUID();
//
if (item.outputVariables) {
item.outputVariables = "";
}
if (item.hashTree) {
this.handleScenarioDefinition(item);
}
}
},
reference() {
this.buttonIsWorking = true;
let selectIds = this.scenarioType == "scenario" ? this.$refs.uiScenarioList.getSelectRows() : this.$refs.uiCustomScenarioList.getSelectRows();
if (!selectIds || selectIds.size == 0 || selectIds.size > 1) {
this.$warning('请(只)选择一个场景/指令作为获取 cookie 的流程!');
return;
}
this.$emit("reference", selectIds);
this.buttonIsWorking = false;
},
close() {
this.$refs.baseRelevance.close();
},
open() {
this.buttonIsWorking = false;
this.$refs.baseRelevance.open();
this.searchByType();
},
/**
* common opt
*/
searchByType() {
this.$nextTick(() => {
this.isScenario
? this.$refs.uiScenarioList.search(this.projectId)
: this.$refs.uiCustomScenarioList.search(this.projectId);
});
},
nodeChange(node, nodeIds, pNodes) {
this.selectNodeIds = nodeIds;
},
customNodeChange(node, nodeIds, pNodes) {
this.selectCustomNodeIds = nodeIds;
},
handleProtocolChange(protocol) {
this.currentProtocol = protocol;
},
setModuleOptions(data) {
this.moduleOptions = data;
},
setCustomModuleOptions(data) {
this.customModuleOptions = data;
},
refresh() {
this.searchByType();
},
setData(data) {
this.currentScenario = Array.from(data).map((row) => row);
this.currentScenarioIds = Array.from(data).map((row) => row.id);
},
setProject(projectId) {
this.projectId = projectId;
this.selectNodeIds = [];
this.selectCustomNodeIds = [];
},
getConditions() {
return this.getConditionsByType();
},
getConditionsByType() {
try {
if (this.isScenario) {
return this.$refs.uiScenarioList.getConditions();
}
return this.$refs.uiCustomScenarioList.getConditions();
} catch (e) {
return {};
}
},
getVersionOptionList(projectId) {
if (hasLicense()) {
this.$get("/project/version/get-project-versions/" + projectId).then(
(response) => {
this.versionOptions = response.data;
}
);
}
},
changeVersion(currentVersion) {
if (this.$refs.uiScenarioList) {
this.$refs.uiScenarioList.condition.versionId = currentVersion || null;
}
this.refresh();
},
checkVersionEnable(projectId) {
if (!projectId) {
return;
}
if (hasLicense()) {
this.$get("/project/version/enable/" + projectId).then((response) => {
this.versionEnable = false;
this.$nextTick(() => {
this.versionEnable = true;
});
});
}
},
},
};
</script>
<style scoped></style>

View File

@ -0,0 +1,289 @@
<template>
<div>
<slot name="header"></slot>
<ms-node-tree
:is-display="getIsRelevance"
v-loading="result.loading"
:tree-nodes="data"
:allLabel="$t('ui.all_scenario')"
:type="isReadOnly ? 'view' : 'edit'"
:delete-permission="['PROJECT_UI_SCENARIO:READ+DELETE']"
:add-permission="['PROJECT_UI_SCENARIO:READ+CREATE']"
:update-permission="['PROJECT_UI_SCENARIO:READ+EDIT']"
:default-label="'未规划场景'"
:show-case-num="showCaseNum"
:hide-opretor="isTrashData"
local-suffix="ui_scenario"
@add="add"
@edit="edit"
@drag="drag"
@remove="remove"
@refresh="list"
@filter="filter"
@nodeSelectEvent="nodeChange"
class="element-node-tree"
ref="nodeTree">
<template v-slot:header>
<ms-search-bar
:show-operator="showOperator && !isTrashData"
:condition="condition"
:commands="operators"/>
</template>
</ms-node-tree>
</div>
</template>
<script>
import SelectMenu from "metersphere-frontend/src/components/environment/snippet/ext/SelectMenu";
import MsNodeTree from "metersphere-frontend/src/components/environment/snippet/ext/NodeTree";
import {buildTree} from "metersphere-frontend/src/model/NodeTree";
import MsSearchBar from "metersphere-frontend/src/components/search/MsSearchBar";
import {getCurrentProjectID} from "metersphere-frontend/src/utils/token";
import {
addScenarioModule,
deleteScenarioModule,
dragScenarioModule,
editScenarioModule,
getScenarioModules,
posScenarioModule
} from "./ui-scenario";
export default {
name: 'UiScenarioModule',
components: {
MsSearchBar,
MsNodeTree,
SelectMenu,
},
props: {
isReadOnly: {
type: Boolean,
default() {
return false;
}
},
showOperator: Boolean,
relevanceProjectId: String,
pageSource: String,
total: Number,
isTrashData: Boolean,
planId: String,
showCaseNum: {
type: Boolean,
default: true
}
},
computed: {
isPlanModel() {
return this.planId ? true : false;
},
isRelevanceModel() {
return this.relevanceProjectId ? true : false;
},
projectId() {
return getCurrentProjectID();
},
getIsRelevance() {
if (this.pageSource !== 'scenario') {
return this.openType;
} else {
return "scenario";
}
}
},
data() {
return {
openType: 'relevance',
result: {},
condition: {
filterText: "",
trashEnable: false
},
data: [],
currentModule: undefined,
operators: [
{
label: this.$t('api_test.api_import.label'),
callback: this.handleImport,
permissions: ['PROJECT_UI_SCENARIO:READ+IMPORT_SCENARIO']
},
{
label: this.$t('report.export'),
callback: this.exportSide,
permissions: ['PROJECT_UI_SCENARIO:READ+EXPORT_SCENARIO']
}
]
}
},
mounted() {
this.list();
},
watch: {
'condition.filterText'() {
this.filter();
},
'condition.trashEnable'() {
this.$emit('enableTrash', this.condition.trashEnable);
},
relevanceProjectId() {
this.list(this.relevanceProjectId);
},
isTrashData() {
this.condition.trashEnable = this.isTrashData;
this.list();
}
},
methods: {
saveAsEdit(data) {
data.type = "add";
this.$emit('saveAsEdit', data);
},
refresh() {
this.$emit("refreshTable");
},
filter() {
this.$refs.nodeTree.filter(this.condition.filterText);
},
list(projectId) {
getScenarioModules(projectId ? projectId : this.projectId, this.isTrashData).then(data => {
if (data) {
this.data = data.data;
this.data.forEach(node => {
buildTree(node, {path: ''});
});
this.$emit('setModuleOptions', this.data);
this.$emit('setNodeTree', this.data);
if (this.$refs.nodeTree) {
this.$refs.nodeTree.filter(this.condition.filterText);
}
}
});
},
edit(param) {
param.projectId = this.projectId;
editScenarioModule(param).then(() => {
this.$success(this.$t('commons.save_success'));
this.list();
});
this.refresh()
},
add(param) {
param.projectId = this.projectId;
if (param && param.level >= 9) {
this.list();
this.$error("模块树最大深度为8层");
return;
}
addScenarioModule(param).then(() => {
this.$success(this.$t('commons.save_success'));
this.list();
});
//
this.refresh();
},
remove(nodeIds) {
deleteScenarioModule(nodeIds).then(() => {
this.list();
this.refresh();
this.$emit("nodeSelectEvent")
});
},
drag(param, list) {
dragScenarioModule(param).then(() => {
posScenarioModule(list).then(() => {
this.list();
});
});
this.refresh();
},
nodeChange(node, nodeIds, pNodes) {
this.currentModule = node.data;
if (node.data.id === 'root') {
this.$emit("nodeSelectEvent", node, [], pNodes);
} else {
this.$emit("nodeSelectEvent", node, nodeIds, pNodes);
}
},
addScenario() {
if (!this.projectId) {
this.$warning(this.$t('commons.check_project_tip'));
return;
}
this.$refs.basisScenario.open(this.currentModule);
},
enableTrash() {
this.condition.trashEnable = true;
this.$emit('enableTrash', this.condition.trashEnable);
},
exportSide() {
this.$emit('exportSide', this.data);
}
}
}
</script>
<style scoped>
.node-tree {
margin-top: 15px;
margin-bottom: 15px;
}
.ms-el-input {
height: 25px;
line-height: 25px;
}
.custom-tree-node {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
width: 100%;
}
.father .child {
display: none;
}
.father:hover .child {
display: block;
}
.node-title {
width: 0;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0 5px;
overflow: hidden;
}
.node-operate > i {
color: #409eff;
margin: 0 5px;
}
:deep(.el-tree-node__content) {
height: 33px;
}
.ms-api-buttion {
width: 30px;
}
.element-node-tree {
width: 100%;
/* min-width: 290px; */
}
:deep(.element-node-tree .recycle .el-col.el-col-3) {
text-align: center;
}
</style>

View File

@ -0,0 +1,38 @@
import {get, post} from "metersphere-frontend/src/plugins/request"
let baseUrl = '/ui/scenario/module/';
let trashUrl = '/ui/scenario/module/trash/';
export function getScenarioModules(projectId, isTrashData, type) {
let url = isTrashData ? trashUrl : baseUrl;
url = url + 'list/' + projectId
if(type){
url = `${url}?type=${type}`;
}
return get(url);
}
export function addScenarioModule(param) {
return post(baseUrl + 'add', param);
}
export function editScenarioModule(param) {
return post(baseUrl + 'edit', param);
}
export function dragScenarioModule(param) {
return post(baseUrl + 'drag', param);
}
export function posScenarioModule(param, callback) {
return post(baseUrl + 'pos', param, callback);
}
export function deleteScenarioModule(nodeIds, callback) {
return post(baseUrl + 'delete', nodeIds, callback);
}
export function getUiAutomationList(currentPage, pageSize, param) {
return post("/ui/automation/list/" + currentPage + "/" + pageSize, param);
}

View File

@ -35,6 +35,9 @@ const message = {
},
project_version: {
version_time: '版本周期',
},
environment: {
export_variable_tip : "导出接口测试变量"
}
}