Commit 530c0f65 authored by 李光宇's avatar 李光宇
Browse files

Merge branch 'master' into 'main'

代码归档只main分支

See merge request !1
parents 13b3da0e 2b18a6d3
!function(t, i) {
"function" == typeof define && define.amd ? define(["jquery"], i) : "object" == typeof exports ? module.exports = i() : t.Query = i(window.Zepto || window.jQuery || $)
}(this, function(t) {
return {
getQuery: function(t, e, s) {
new RegExp("(^|&|#)" + t + "=([^&]*)(&|$|#)","i");
s = s || window;
var a, n, r = s.location.href, l = "";
if (a = "#" == e ? r.split("#") : r.split("?"),
"" != (n = 1 == a.length ? "" : a[1])) {
gg = n.split(/&|#/);
var h = gg.length;
for (str = arguments[0] + "=",
i = 0; i < h; i++)
if (0 == gg[i].indexOf(str)) {
l = gg[i].replace(str, "");
break
}
}
return decodeURI(l)
},
getForm: function(i) {
var e = {}
, s = {};
t(i).find("*[name]").each(function(i, a) {
var n, r = t(a).attr("name"), l = t.trim(t(a).val()), h = [];
if ("" != r && !t(a).hasClass("getvalued")) {
if ("money" == t(a).data("type") && (l = l.replace(/\,/gi, "")),
"radio" == t(a).attr("type")) {
var c = null;
t("input[name='" + r + "']:radio").each(function() {
t(this).is(":checked") && (c = t.trim(t(this).val()))
}),
l = c || ""
}
if ("checkbox" == t(a).attr("type")) {
var c = [];
t("input[name='" + r + "']:checkbox").each(function() {
t(this).is(":checked") && c.push(t.trim(t(this).val()))
}),
l = c.length ? c.join(",") : ""
}
if (t(a).attr("listvalue") && (e[t(a).attr("listvalue")] || (e[t(a).attr("listvalue")] = [],
t("input[listvalue='" + t(a).attr("listvalue") + "']").each(function() {
if ("" != t(this).val()) {
var i = t(this).attr("name")
, s = {};
if ("json" == t(this).data("type") ? s[i] = JSON.parse(t(this).val()) : s[i] = t.trim(t(this).val()),
t(this).attr("paramquest")) {
var n = JSON.parse(t(this).attr("paramquest"));
s = t.extend(s, n)
}
e[t(a).attr("listvalue")].push(s),
t(this).addClass("getvalued")
}
}))),
t(a).attr("arrayvalue") && (e[t(a).attr("arrayvalue")] || (e[t(a).attr("arrayvalue")] = [],
t("input[arrayvalue='" + t(a).attr("arrayvalue") + "']").each(function() {
if ("" != t(this).val()) {
var i = {};
if (i = "json" == t(this).data("type") ? JSON.parse(t(this).val()) : t.trim(t(this).val()),
t(this).attr("paramquest")) {
var s = JSON.parse(t(this).attr("paramquest"));
i = t.extend(i, s)
}
e[t(a).attr("arrayvalue")].push(i)
}
}))),
"" != r && !t(a).hasClass("getvalued"))
if (r.match(/\./)) {
if (h = r.split("."),
n = h[0],
3 == h.length)
s[h[1]] = s[h[1]] || {},
s[h[1]][h[2]] = l;
else if ("json" == t(a).data("type")) {
if (s[h[1]] = JSON.parse(l),
t(a).attr("paramquest")) {
var p = JSON.parse(t(a).attr("paramquest"));
s[h[1]] = t.extend(s[h[1]], p)
}
} else
s[h[1]] = l;
e[n] ? e[n] = t.extend({}, e[n], s) : e[n] = s
} else
e[r] = l
}
});
var a = {};
for (var n in e) {
var r = e[n];
a[n] = "object" == typeof r ? JSON.stringify(r) : e[n]
}
return t(".getvalued").removeClass("getvalued"),
a
},
setHash: function(i) {
var e = "";
i = t.extend(this.getHash(), i);
var s = [];
for (var a in i)
"" != i[a] && s.push(a + "=" + encodeURIComponent(i[a]));
return e += s.join("&"),
location.hash = e,
this
},
getHash: function(t) {
if ("string" == typeof t)
return this.getQuery(t, "#");
var i = {}
, e = location.hash;
if (e.length > 0) {
e = e.substr(1);
for (var s = e.split("&"), a = 0, n = s.length; a < n; a++) {
var r = s[a].split("=");
r.length > 0 && (i[r[0]] = decodeURI(r[1]) || "")
}
}
return i
}
}
}),
function(t, i) {
"function" == typeof define && define.amd ? define(["jquery", "query"], i) : "object" == typeof exports ? module.exports = i() : t.Paging = i(window.Zepto || window.jQuery || $, Query)
}(this, function(t, i) {
function e() {
var t = Math.random().toString().replace(".", "");
this.id = "Paging_" + t
}
return t.fn.Paging = function(i) {
var s = [];
return t(this).each(function() {
var a = t.extend({
target: t(this)
}, i)
, n = new e;
n.init(a),
s.push(n)
}),
s
}
,
e.prototype = {
init: function(i) {
this.settings = t.extend({
callback: null,
pagesize: 10,
current: 1,
prevTpl: "Prev",
nextTpl: "Next",
firstTpl: "First",
lastTpl: "Last",
ellipseTpl: "...",
toolbar: !1,
hash: !1,
pageSizeList: [5, 10, 15, 20]
}, i),
this.target = t(this.settings.target),
this.container = t('<div id="' + this.id + '" class="ui-paging-container"/>'),
this.target.append(this.container),
this.render(this.settings),
this.format(),
this.bindEvent()
},
render: function(t) {
void 0 !== t.count ? this.count = t.count : this.count = this.settings.count,
void 0 !== t.pagesize ? this.pagesize = t.pagesize : this.pagesize = this.settings.pagesize,
void 0 !== t.current ? this.current = t.current : this.current = this.settings.current,
this.pagecount = Math.ceil(this.count / this.pagesize),
this.format()
},
bindEvent: function() {
var i = this;
this.container.on("click", "li.js-page-action,li.ui-pager", function(e) {
if (t(this).hasClass("ui-pager-disabled") || t(this).hasClass("focus"))
return !1;
t(this).hasClass("js-page-action") ? (t(this).hasClass("js-page-first") && (i.current = 1),
t(this).hasClass("js-page-prev") && (i.current = Math.max(1, i.current - 1)),
t(this).hasClass("js-page-next") && (i.current = Math.min(i.pagecount, i.current + 1)),
t(this).hasClass("js-page-last") && (i.current = i.pagecount)) : t(this).data("page") && (i.current = parseInt(t(this).data("page"))),
i.go()
})
},
go: function(t) {
var e = this;
this.current = t || this.current,
this.current = Math.max(1, e.current),
this.current = Math.min(this.current, e.pagecount),
this.format(),
this.settings.hash && i.setHash({
page: this.current
}),
this.settings.callback && this.settings.callback(this.current, this.pagesize, this.pagecount)
},
changePagesize: function(t) {
this.render({
pagesize: t
}),
this.settings.changePagesize && this.settings.changePagesize.call(this, this.pagesize, this.current, this.pagecount)
},
format: function() {
var i = "<ul>";
if (i += '<li class="js-page-first js-page-action ui-pager" >' + this.settings.firstTpl + "</li>",
i += '<li class="js-page-prev js-page-action ui-pager">' + this.settings.prevTpl + "</li>",
this.pagecount > 6) {
if (i += '<li data-page="1" class="ui-pager">1</li>',
this.current <= 2)
i += '<li data-page="2" class="ui-pager">2</li>',
i += '<li data-page="3" class="ui-pager">3</li>',
i += '<li class="ui-paging-ellipse">' + this.settings.ellipseTpl + "</li>";
else if (this.current > 2 && this.current <= this.pagecount - 2)
this.current > 3 && (i += "<li>" + this.settings.ellipseTpl + "</li>"),
i += '<li data-page="' + (this.current - 1) + '" class="ui-pager">' + (this.current - 1) + "</li>",
i += '<li data-page="' + this.current + '" class="ui-pager">' + this.current + "</li>",
i += '<li data-page="' + (this.current + 1) + '" class="ui-pager">' + (this.current + 1) + "</li>",
this.current < this.pagecount - 2 && (i += '<li class="ui-paging-ellipse" class="ui-pager">' + this.settings.ellipseTpl + "</li>");
else {
i += '<li class="ui-paging-ellipse" >' + this.settings.ellipseTpl + "</li>";
for (var e = this.pagecount - 2; e < this.pagecount; e++)
i += '<li data-page="' + e + '" class="ui-pager">' + e + "</li>"
}
i += '<li data-page="' + this.pagecount + '" class="ui-pager">' + this.pagecount + "</li>"
} else
for (var e = 1; e <= this.pagecount; e++)
i += '<li data-page="' + e + '" class="ui-pager">' + e + "</li>";
i += '<li class="js-page-next js-page-action ui-pager">' + this.settings.nextTpl + "</li>",
i += '<li class="js-page-last js-page-action ui-pager">' + this.settings.lastTpl + "</li>",
i += "</ul>",
this.container.html(i),
1 == this.current && (t(".js-page-prev", this.container).addClass("ui-pager-disabled"),
t(".js-page-first", this.container).addClass("ui-pager-disabled")),
this.current == this.pagecount && (t(".js-page-next", this.container).addClass("ui-pager-disabled"),
t(".js-page-last", this.container).addClass("ui-pager-disabled")),
this.container.find('li[data-page="' + this.current + '"]').addClass("focus").siblings().removeClass("focus"),
this.settings.toolbar && this.bindToolbar()
},
bindToolbar: function() {
for (var i = this, e = t('<li class="ui-paging-toolbar"><select class="ui-select-pagesize"></select><input type="text" class="ui-paging-count"/></li>'), s = t(".ui-select-pagesize", e), a = "", n = 0, r = this.settings.pageSizeList.length; n < r; n++)
a += '<option value="' + this.settings.pageSizeList[n] + '">' + this.settings.pageSizeList[n] + "</option>";
s.html(a),
s.val(this.pagesize),
t("input", e).val(this.current),
t("input", e).click(function() {
t(this).select()
}).keydown(function(e) {
if (13 == e.keyCode) {
var s = parseInt(t(this).val()) || 1;
i.go(s)
}
}),
t("a", e).click(function() {
var e = parseInt(t(this).prev().val()) || 1;
i.go(e)
}),
s.change(function() {
i.changePagesize(t(this).val())
}),
this.container.children("ul").append(e)
}
},
e
});
/*
* @Description:
* @Author: Era Chen
* @Email: chenjiyun@corp.netease.com
* @Date: 2019-08-08 17:41:44
* @LastEditors : Era Chen
* @LastEditTime : 2020-01-09 16:53:46
*/
function StepPannel(data, root){
this.data = data
this.original_steps = data.steps
this.steps = [].concat(data.steps)
this.static = data.static_root
this.currentStep = 0
this.currentWrong = -1
this.pagesize = 20
this.currentPage = 1
this.stepLeft = $('#step-left .step-list')
this.stepRight = $('#step-right')
this.magnifyContainer = $('#magnify .content')
this.magnifyPic = $('#magnify')
this.scale = 0
this.order = 'acc' // or dec
this.duration = 'acc' // or dec
this.status = 'acc' // or dec
this.init = function(){
// 初始化
this.initStepData()
this.bindEvents()
this.init_gallery()
this.init_pagenation()
var steps = [].concat(this.original_steps)
if(steps.length >0){
this.steps = steps
this.filterSteps($('.filter#all'))
} else{
this.setSteps()
}
this.init_video()
if ($('#console pre.trace').length > 0) {
setTimeout(function(){
// 当log超过2w行,转换成高亮模式会导致卡顿
if ($('#console pre.trace').text().length < 20000) {
hljs.highlightBlock($('#console pre.trace')[0], null, false)
}
}, 0)
}
this.highlightBlock()
}
this.bindEvents = function(){
// 绑定事件
var that = this
this.stepLeft.delegate('.step', 'click',function(e){
if(e.target.className.indexOf("step-context") >=0 )
that.jumpToCurrStep(Number(e.target.getAttribute('index')))
else
that.setStepRight(e.currentTarget.getAttribute('index'))
})
$('.gallery .content').delegate('.thumbnail', 'click', function(e){
that.jumpToCurrStep(Number(this.getAttribute('index')))
})
this.stepRight.delegate('.fancybox', "click", function(e) {
that.showMagnifyPic(this.outerHTML)
})
this.stepRight.delegate('.crop_image', "click", function(e) {
that.showMagnifyPic(this.outerHTML)
})
this.magnifyPic.click(function(e) {
if (e.target.tagName.toLowerCase() != 'img'){
that.hideMagnifyPic()
}
})
$('.filter#all').click(function(){
that.steps = [].concat(that.original_steps)
that.filterSteps(this)
})
$('.filter#success').click(function(){
that.steps = that.filterSuccessSteps()
that.filterSteps(this)
})
$('.filter#fail').click(function(){
that.steps = that.filterFailSteps()
that.filterSteps(this)
})
$('.filter#assert').click(function(){
that.steps = that.filterAssertSteps()
that.filterSteps(this)
})
$('#jump-wrong').click(function(){
that.steps = [].concat(that.original_steps)
that.currentWrong = that.findCurrentWrongStep()
if(that.currentWrong>=0){
that.currentStep = that.currentWrong
that.currentPage = Math.ceil(that.currentStep / that.pagesize)
that.setSteps(that.currentStep)
}
})
$('.order#order').click(function(){
that.order = that.order == 'acc' ? 'dec' : 'acc'
that.steps.sort(that.sortSteps('index', that.order == 'acc'))
that.currentPage = 1
that.setSteps()
})
$('.order#duration').click(function(){
that.steps.sort(that.sortSteps('duration_ms', that.duration == 'acc'))
that.duration = that.duration == 'acc' ? 'dec' : 'acc'
that.currentPage = 1
that.setSteps()
})
$('.order#status').click(function(){
that.steps.sort(that.sortSteps('status', that.status == 'acc'))
that.status = that.status == 'acc' ? 'dec' : 'acc'
that.currentPage = 1
that.setSteps()
})
$("#close-console").click(function(){
$('#console').fadeOut(300)
})
$("#show-console").click(function(){
$('#console').fadeIn(300)
})
}
this.sortSteps = function(attr, rev){
//第二个参数没有传递 默认升序排列
if(rev == undefined){
rev = 1;
}else{
rev = (rev) ? 1 : -1;
}
return function(a,b){
a = a[attr];
b = b[attr];
if(a < b){
return rev * -1;
}
if(a > b){
return rev * 1;
}
return 0;
}
}
this.initStepRight = function(){
// 设置高亮
this.highlightBlock()
var that = this
if($(".step-args .fancybox").length>0){
$('#step-right .fancybox .screen').load(function(e){
// 存在截屏,并加载成功
that.resetScale(this)
that.convertSize($('.step-args .crop_image'), 80, 35)
that.resetScreenshot($('#step-right .fancybox'))
})
}
}
this.highlightBlock = function(){
if($('#step-right pre.trace').length>0){
hljs.highlightBlock($('#step-right pre.trace')[0], null, false);
}
}
this.filterSteps = function(dom){
$('.steps .filter').removeClass('active')
$(dom).addClass('active')
this.currentPage = 1
this.setSteps()
}
this.setSteps = function(step){
// 重设步骤页面内容
step = step || (this.steps.length > 0 ? this.steps[0].index : 0)
this.setPagenation()
this.setStepRight(step)
}
this.initStepData = function(){
for(var i = 0; i< this.steps.length; i++){
step = this.steps[i]
if(i == 0){
step.duration_ms = getDelta(step.time, this.data.run_start)
} else{
step.duration_ms = getDelta(step.time, this.steps[i-1].time)
}
step.duration = getFormatDuration(step.duration_ms)
step.index = i
step.status = step.traceback ? 'fail' : 'success'
}
}
this.init_gallery = function(){
var that = this
var fragment = this.original_steps.map(function(step){
if(step.screen && step.screen.thumbnail) {
return '<div class="thumbnail" index="%s">'.format(step.index) +
'<img src="%s" alt="%s"/>'.format(step.screen.thumbnail, step.screen.thumbnail) +
'<div class="time">%s</div>'.format(getFormatDuration2(getDelta(step.time, that.data.run_start))) +
'</div>'
} else{
return ""
}
})
fragment = fragment.join('')
if(fragment == ''){
$('.gallery').hide()
}else{
$('.gallery .content').html(fragment)
}
}
this.jumpToCurrStep = function(step) {
// 跳至指定步骤
step = step || (this.steps.length > 0 ? this.steps[0].index : 0)
this.steps = [].concat(this.original_steps)
this.currentPage = Math.floor(step / this.pagesize) +1
this.setPagenation()
this.setStepRight(step)
$('.steps .filter').removeClass('active')
}
this.showMagnifyPic = function(fragment) {
this.magnifyContainer.html(fragment)
this.magnifyContainer.children().removeAttr('style')
var fancybox = this.magnifyContainer.find('.fancybox')
if (fancybox.length > 0){
var that = this
$('#magnify .fancybox .screen').load(function(e){
// 存在截屏,并加载成功
if (this.height > this.parentNode.offsetHeight){
this.style.height = this.parentNode.offsetHeight + 'px'
}
that.resetScale(this)
that.resetScreenshot($('#magnify .fancybox'))
})
}
this.magnifyPic.fadeIn(300)
}
this.hideMagnifyPic = function() {
this.magnifyPic.fadeOut(300)
}
this.setStepsLeft = function(){
html = this.steps.length>0 ? '' : '<h4 class="no-steps"><span lang="en">Warning: No steps</span></h3>'
var start = (this.currentPage-1)* this.pagesize
start = start < 0 ? 0 : start
var end = (this.currentPage)*this.pagesize
end = end>this.steps.length ? this.steps.length : end
for(var i = start; i< end; i++){
var step = this.steps[i]
var title = step.assert ? '<span lang="en">Assert: </span>' + step.assert : step.title
html += step.scene_name ? '<div class="step-scenario"> '+'<span class="step_title_scenario" lang="en">%s</span>'.format(step.scene_name) : '' +'</div>' +
'<div class="step" index="%s">'.format(step.index) +
'<img src="%simage/step_%s.svg" alt="%s.svg"/>'.format(this.static, step.status, step.status) +
'<span class="order"># %s</span>'.format(step.index +1) +
'<span class="step_title" lang="en">%s</span>'.format(title) +
'<span class="step-time">%s</span>'.format(step.duration) +
'<img class="step-context" src="%simage/eye.svg" alt="eye.svg" index="%s"/>'.format(this.static, step.index) +
'</div>'
}
this.stepLeft.html(html)
}
this.setStepRight = function(index){
index = parseInt(index)
if(!isNaN(index) && index>= 0 && index<this.original_steps.length){
$('.gallery .thumbnail.active').removeClass('active')
$('.gallery .thumbnail[index="%s"]'.format(index)).addClass('active')
this.setStepRightHtml(index)
this.initStepRight()
}
}
this.setStepRightHtml = function(index){
this.stepLeft.find('.step.active').removeClass('active')
this.stepLeft.find(".step[index='%s']".format(index)).addClass('active')
step = this.original_steps[index]
this.currentStep = index
success = step.traceback ? "fail" : "success"
pass = step.traceback ? "Failed" : "Passed"
title = step.code ? step.desc||step.code.name : step.desc
title = title || step.title
var head = "<div class='step-head'><span class='step-status %s'>%s</span><span>Step %s: %s</span></div>"
.format(success, pass , step.index+1, title)
var infos = this.getStepRightInfo(step)
var args = this.getStepRightArgs(step)
this.stepRight.html(head + infos + args)
}
this.getStepRightInfo = function(step){
// HTML 本步骤成功与否、耗时
try{
return ("<div class='step-infos'>"+
"<div class='infos-li'>" +
"<span lang='en'>Status: </span>" +
"<span class='content-val %s'>%s</span>" +
"<img src='%simage/step_%s.svg'>" +
"</div>" +
"<div class='infos-li'>" +
"<span lang='en'>Start: </span>" +
"<span class='content-val'>%s</span>" +
"</div>" +
"<div class='infos-li'>" +
"<span lang='en'>Duration: </span>" +
"<img src='%simage/time.svg'>" +
"<span class='content-val'>%s</span>" +
"</div>" +
"<div class='infos-li step-behavior'>" +
"<span lang='en'>Behavior: </span>" +
"<span class='content-val bold'>%s</span>" +
"</div>" +
"</div>").format(success, pass, this.static, success,
step.time ? getFormatDateTime(step.time): '--',
this.static, step.duration,
step.code ? step.code.name:"null")
} catch (err) {
console.log(err)
return ""
}
}
this.getStepRightArgs = function(step){
// 操作的参数
try{
argHtml = ''
if(step.code){
for(var i=0; i < step.code.args.length; i++){
arg = step.code.args[i]
if(arg.image){
argHtml += ('<img class="crop_image desc" data-width="%s" data-height="%s" src="%s" title="%s">' +
'<p class="desc">resolution: %s</p>')
.format(arg.resolution[0], arg.resolution[1], arg.image, arg.image, arg.value.resolution)
}else{
val = typeof arg.value == 'object' ? JSON.stringify(arg.value) : arg.value
argHtml += '<p class="desc">%s: %s</p>'.format(arg.key,val)
}
}
}
} catch(e) {
console.error(e)
}
// 相似度
if(step.screen && step.screen.confidence){
argHtml += '<p class="desc"><span class="point glyphicon glyphicon-play"></span><span lang="en">Confidence: </span>%s</p>'.format(step.screen.confidence)
}
argHtml = argHtml || '<p class="desc">None</p>'
argHtml = "<div class='fluid infos'>" + argHtml + "</div>"
argHtml += "<div class='fluid screens'>" + this.getStepRightScrren(step) + "</div>"
argHtml += "<div class='fluid traces'>" + this.getStepRightTrace(step) + "</div>"
return "<div class='step-args'><div class='bold'>Args:</div>" + argHtml + "</div>"
}
this.getStepRightScrren = function(step){
if(step.screen && step.screen.src){
var src = step.screen.src
// 截屏
var img = '<img class="screen" data-src="%s" src="%s" title="%s">'.format(src, src, src)
// 点击位置
var targets = ''
for(var i=0; i < step.screen.pos.length; i++){
var pos = step.screen.pos[i]
var rect = JSON.stringify({'left': pos[0], "top": pos[1]})
targets += "<img class='target' src='%simage/target.png' rect=%s>"
.format(this.static, rect)
}
// 线
var vectors = ''
for(var i=0; i < step.screen.vector.length; i++){
var v = step.screen.vector[i]
var rect = JSON.stringify({'left': v[0], "top": v[1]})
vectors += ("<div class='arrow' data-index='%s' rect=%s>" +
'<div class="start"></div>' +
'<div class="line"></div>' +
'<div class="end"></div>' +
'</div>').format(this.currentStep, rect)
}
// 还有个rect <!-- rect area -->
var rectors = ''
for(var i=0;i<step.screen.rect.length; i++){
var rect = step.screen.rect[i]
rectors += "<div class='rect' rect='%s' ></div>"
.format(JSON.stringify(rect))
}
var res = step.screen.resolution
res = res ? 'w=%s h=%s'.format(res[0], res[1]): ""
return '<div class="fancybox" %s >%s</div>'.format(res, img + targets + vectors + rectors)
} else{
return ""
}
}
this.getStepRightTrace = function(step){
if(step.traceback){
return '<div class="desc"><pre class="trace"><code class="python">%s</code></pre></div>'.format(step.traceback)
} else{
return ""
}
}
this.resetScale = function(dom){
/**
* @description: 重新计算截屏缩放的比例
* @param {dom} dom img对象
*/
imgWidth = dom.parentNode.getAttribute('w') || dom.naturalWidth
dwidth = dom.width
this.scale = dwidth / imgWidth
}
this.resetScreenshot = function(fancybox){
// 重新设置targt、方框、连接线位置
var screen = fancybox.find('.screen')
this.convertPos(fancybox.find('.target'), screen, true)
this.convertSize(fancybox.find('.rect'))
this.convertPos(fancybox.find('.rect'), screen)
this.showArrow(fancybox.find(".arrow"), screen)
fancybox.css({
'width': screen.width()
})
}
this.convertPos = function(domList, screen, withSize){
for(var i=0; i<domList.length; i++){
var rect = JSON.parse(domList[i].getAttribute('rect'))
x = rect.left * this.scale
y = rect.top * this.scale
if(withSize){
x -= domList[i].offsetWidth/2
y -= domList[i].offsetHeight/2
}
domList[i].style.left = this.convertPosPersentage(x, screen , 'horizontal')
domList[i].style.top = this.convertPosPersentage(y, screen, 'vertical')
}
}
this.convertSize = function(domList, minWidth, minHeight) {
for(var i=0;i<domList.length; i++){
if (domList[i].tagName.toLowerCase() == 'img'){
w = domList[i].clientWidth
h = domList[i].clientHeight
} else{
var rect = JSON.parse(domList[i].getAttribute('rect'))
w = rect.width
h = rect.height
}
var scale = Math.max(this.scale, (minWidth || 0)/w, (minHeight || 0)/h)
domList[i].style.width = (w * scale) + 'px'
domList[i].style.height = (h * scale) + 'px'
}
}
this.showArrow = function(dom, screen){
var start = this.original_steps[this.currentStep].screen.pos[0]
var vector = this.original_steps[this.currentStep].screen.vector[0]
if(vector && start){
var vt_x = vector[0] * this.scale;
var vt_y = - vector[1] * this.scale;
var vt_width = Math.sqrt(vt_x * vt_x + vt_y * vt_y)
var rotation = 360*Math.atan2(vt_y, vt_x)/(2*Math.PI)
var rt = 'rotate(' + -rotation + 'deg)';
var rotate_css = {
'-ms-transform': rt,
'-webkit-transform': rt,
'-moz-transform': rt,
'transform': rt,
'transform-origin': '6px 15px',
};
dom.css(rotate_css);
dom.css({
'top': this.convertPosPersentage(start[1]* this.scale, screen, 'vertical'),
'left': this.convertPosPersentage(start[0]*this.scale, screen, 'horizontal'),
'width': vt_width
});
}
}
this.filterSuccessSteps = function(){
// 筛选成功步骤
arr = []
for(var i=0; i<this.original_steps.length; i++){
step = this.original_steps[i]
if(step.traceback)
continue
else
arr.push(step)
}
return arr
}
this.filterFailSteps = function(){
// 筛选失败步骤
arr = []
for(var i=0; i<this.original_steps.length; i++){
step = this.original_steps[i]
if(step.traceback)
arr.push(step)
else
continue
}
return arr
}
this.filterAssertSteps = function(){
// 筛选断言步骤
arr = []
for(var i=0; i<this.original_steps.length; i++){
step = this.original_steps[i]
if(step.assert)
arr.push(step)
else
continue
}
return arr
}
this.findCurrentWrongStep = function(){
// 跳至错误步骤
arr = this.filterFailSteps()
if(arr.length>0){
if(this.currentWrong == arr[arr.length-1].index)
return arr[0].index
for(var i=0; i<arr.length; i++){
if(arr[i].index > this.currentWrong)
return arr[i].index
}
}
return -1
}
this.init_pagenation = function(){
//生成分页控件
this.paging = new Paging();
var that = this
var list_len = this.steps.length
this.paging.init({
target:'#pageTool',
pagesize: this.pagesize,
count: this.steps.length,
prevTpl: "<",
nextTpl: ">",
toolbar:true,
pageSizeList: list_len>100 ? [10, 20, 50, 100, list_len] : [10, 20, 50, 100],
changePagesize:function(ps){
that.pagesize = parseInt(ps)
that.currentPage = 1
that.setStepsLeft()
},
callback:function(p){
that.currentPage = parseInt(p)
that.setStepsLeft()
}
});
$('#pageTool').prepend('<span class="steps-total"><span lang="en">Total </span><span class="steps-account"></span></span>')
}
this.setPagenation = function(){
if(this.steps.length > this.pagesize)
$('#pageTool').show()
else
$('#pageTool').hide()
this.paging.render({
'count': this.steps.length
})
$('#pageTool .steps-account').html(this.steps.length)
this.paging.go(this.currentPage)
}
this.init_video = function(){
var container = $('.gif-wrap')
if($('.gif-wrap .embed-responsive').length>0) {
$('.gif-wrap .minimize').click(function(){
container.removeClass('show')
})
$('.gif-wrap .maximize').click(function(){
container.addClass('show')
})
$('.gif-wrap .close').click(function(){
container.hide()
})
}else {
container.hide()
}
}
this.convertPosPersentage = function(pixcel, screen, key){
ret = ''
if(key == 'horizontal'){
ret = pixcel / screen.width() * 100 + '%'
}
else if (key == 'vertical'){
ret = pixcel / screen.height() * 100 + '%'
}
return ret
}
}
String.prototype.format= function(){
var args = Array.prototype.slice.call(arguments);
var count=0;
return this.replace(/%s/g,function(s,i){
return args[count++];
});
}
Date.prototype.Format = function (fmt) { //author: meizz
var o = {
"M+": this.getMonth() + 1, //月份
"d+": this.getDate(), //日
"h+": this.getHours(), //小时
"m+": this.getMinutes(), //分
"s+": this.getSeconds(), //秒
"q+": Math.floor((this.getMonth() + 3) / 3), //季度
"S": this.getMilliseconds() //毫秒
};
if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}
function getFormatDateTime(timestamp){
timestamp = getTimestamp(timestamp)
return (new Date(timestamp)).Format("yyyy-MM-dd hh:mm:ss")
}
function getFormatDate(timestamp){
timestamp = getTimestamp(timestamp)
return (new Date(timestamp)).Format("yyyy / MM / dd")
}
function getFormatTime(timestamp){
timestamp = getTimestamp(timestamp)
return (new Date(timestamp)).Format("hh:mm:ss")
}
function getFormatDuration(delta) {
// 格式化耗时, 格式为 0hr1min6s22ms
console.log(delta)
ms = parseInt(delta % 1000)
delta = parseInt(delta / 1000)
s = delta % 60
delta = parseInt(delta/ 60)
m = delta % 60
h = parseInt(delta/ 60)
if(h == 0)
if(m == 0)
if(s==0)
msg = ms + "ms"
else
msg = s + "s " + ms + "ms"
else
msg = m + 'min ' + s + "s " + ms + "ms"
else
msg = h + 'hr ' + m + 'min ' + s + "s " + ms + "ms"
return msg
}
function getFormatDuration2(delta) {
// 返回耗时,格式为 00:00:19
var midnight = (new Date(new Date().setHours(0, 0, 0, 0))).getTime()
return (new Date(midnight + delta)).Format("hh:mm:ss")
}
function getDelta(end, start) {
// 返回耗时 单位为毫秒, end 和 start 可能是timestamp,也可能是化数据
return getTimestamp(end) - getTimestamp(start)
}
function getTimestamp(time) {
// time有可能是时间戳,也可能是格式化的,返回为毫秒
if(Number(time)){
return Number(time) * 1000
} else{
return (new Date(time).getTime())
}
}
function toggleCollapse(dom){
if(dom.hasClass('collapse')){
dom.removeClass('collapse')
} else{
dom.addClass('collapse')
}
}
function urlArgs(){
var args = {};
var query = location.search.substring(1);
var pairs = query.split(/&|\?/);
for(var i = 0;i < pairs.length; i++){
var pos = pairs[i].indexOf("=");
if(pos == -1) continue;
var name = pairs[i].substring(0, pos);
var value = pairs[i].substring(pos + 1);
value = decodeURIComponent(value);
args[name] = value;
}
// Connect :Android:///04157df490cb0b3f?cap_method=JAVACAP&&ori_method=ADBORI&&touch_method=ADBTOUCH
// && 也会被分割
if(args['connect']){
var methods = []
if(args['cap_method'])
methods.push("cap_method=JAVACAP")
if(args['ori_method'])
methods.push("ori_method=ADBORI")
if(args['touch_method'])
methods.push("touch_method=ADBTOUCH")
if(methods.length>0)
args['connect'] = args['connect'] + '?' + methods.join('&&')
}
return args;
}
var formatStr = function(str) {
return (str.charAt(0).toUpperCase()+str.slice(1)).replace(/_/g, ' ') + ':'
};
function loadUrlInfo(){
// 根据search信息,在summary下面插入设备信息,仅限多机运行的时候使用
args = urlArgs()
result = data.test_result ? 'Passed' : 'Failed'
if(args.type) {
var container = $('#device')
container.addClass('show')
var keys = ["device", "connect", "accomplished", "rate", "succeed", 'failed', "no_of_device", "no_of_script", "type"]
args.rate = (args.succeed / args.accomplished * 100).toFixed(2) + '%'
args.failed = args.accomplished - args.succeed
args['no_of_device'] = args.device_no
args['no_of_script'] = args.script_no
var fragment = keys.map(function(k){
return '<div class="info %s" title="%s"><span lang="en">%s</span>%s</div>'.format(k,args[k], formatStr(k), args[k])
})
back = '<a href="%s#detail" class="back" title="Back to multi-device report"><img src="%simage/back.svg"></a>'.format(args.back, data.static_root)
$('#back_multi').html(back)
container.html(fragment)
result = args.status ? args.status : result
$(".footer").hide()
}
set_task_status(result)
$('.info.connect').append("<div class='copy_device'></div>")
$(".info .copy_device").click(function(){
copyToClipboard(this.parentNode.getAttribute('title'))
})
}
function copyToClipboard(msg){
const input = document.createElement('input')
input.setAttribute('readonly', 'readonly');
input.setAttribute('value', msg);
document.body.appendChild(input);
if (document.execCommand('copy')) {
input.select();
document.execCommand('copy');
console.log('复制成功');
} else{
alert('Copy is not supported by the current browser, please change to chrome')
}
document.body.removeChild(input);
}
function set_task_status(result){
src = "%simage/%s.svg".format(data.static_root, result=='Passed' ? 'success' : 'fail')
$('.summary #result-img').attr('src', src)
$('.summary #result-img').attr('alt', result)
$('.summary #result-desc').addClass(result=='Passed' ? 'green' : 'red')
$('.summary #result-desc').html("[%s]".format(result))
}
function init_page(){
$('.summary .info-sub.start').html(getFormatDate(data.run_start))
$('.summary .info-sub.time').html(getFormatTime(data.run_start) + '-' + getFormatTime(data.run_end))
$('.summary .info-value.duration').html(getFormatDuration(getDelta(data.run_end, data.run_start)))
}
$(function(){
init_page()
stepPanel = new StepPannel(data)
stepPanel.init()
$("img").error(function () {
var orsrc = $(this).attr("src")
if(!orsrc){ return }
if(orsrc.indexOf("report.gif") > -1){
setTimeout(function(){
$(this).attr("src", 'report.gif?timestamp=' + new Date().getTime());
}.bind(this), 5000)
return
}
$(this).unbind("error")
.addClass('error-img')
.attr("src", data.static_root + "image/broken.png")
.attr("orgin-src", orsrc);
});
// 延迟加载图片
lazyload();
// 自动收缩过长的脚本描述
var descHeight = 100;
descWrap = $('.summary .airdesc')
if($('.summary .desc-content').height()>descHeight) {
descWrap.addClass('long collapse')
$(".summary .show-more").click(function(){
toggleCollapse(descWrap)
})
}
// 复制脚本地址到粘贴版
$('#copy_path').click(function(){
copyToClipboard(this.getAttribute('path'))
})
// 从地址search部分加载设备信息等
loadUrlInfo()
})
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Airtest Report {{info.title}}</title>
<!--[if lt IE 9]>
<script src="https://css3-mediaqueries-js.googlecode.com/svn/trunk/css3-mediaqueries.js"></script>
<script src="http://html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link href="{{static_root}}css/report.css" rel="stylesheet">
<script src="{{static_root}}js/jquery-1.10.2.min.js"></script>
<script src="{{ static_root }}js/jquery-lang.js" charset="utf-8" type="text/javascript"></script>
<script src="{{ static_root }}js/langpack/zh_CN.js" charset="utf-8" type="text/javascript"></script>
<script src="{{ static_root }}js/lazyload.js" charset="utf-8" type="text/javascript"></script>
<script type="text/javascript">
data = {{data|safe}}
lang = new Lang();
lang.init({
defaultLang: 'en',
currentLang: '{{ lang }}'
});
</script>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div id="main" class="main col-md-12">
<div id="back_multi"></div>
<h1 class="title">Airtest Report {{info.title}}</h1>
{% if not steps %}
<h2 lang="en" class="empty-report">I am sorry, this log file is empty! </h2>
{% endif %}
<div class="summary" >
<div class="show-{{'vertical' if info.desc else 'horizontal'}}">
<div class="info info1">
<div class="info-left">
<img id='result-img' />
</div>
<div class="info-right">
<div class="info-title"><span lang='en'>Airtest Report</span>
<label id='result-desc' lang="en"></label>
</div>
<div class="info-content">
<div class='info-sub start'></div>
<div class='info-sub time'></div>
</div>
<div class="info-toal">
<div class="info-step">
<span class="info-name" lang="en">Steps: </span>
<span class="info-value">{{steps|length}}</span>
</div>
<div class="info-time">
<span class="info-name" lang="en">Time: </span>
<span class="info-value duration">xxx</span>
</div>
{% if console %}
<div class="info-console">
<span class="info-name" lang="en">Console: </span>
<img id='show-console' src="{{static_root}}image/console_normal.svg" /></a>
<div id='console' class="mask hide">
<div class="hljs content">
<img id='close-console' src="{{static_root}}image/close.svg" /></a>
<div class="console-content"><pre class="trace">{{console}}</pre></div>
</div>
</div>
</div>
{% endif %}
{% if log %}
<div class="info-log">
<span class="info-name" lang="en">Log: </span>
<span class="info-value log">
<a href="{{log}}" target="_blank" download="log.txt">log.txt <img src="{{static_root}}image/download_log.svg" /></a>
</span>
</div>
{% endif %}
</div>
</div>
</div>
<div class="info info2">
<div class="info-left"></div>
<div class="info-right">
<div class="info-title"><span lang='en'>Executors</span></div>
<div class="info-content">
<div class="info-execute">
<div class="circle-img"></div>
{% if info.author %}
<div class="info-name"><span lang="en">Author:</span> {{info.author}}</div>
{% else %}
<div class="info-name" lang="en">Author: Anonymous</div>
{% endif %}
</div>
<div class="info-execute">
<div class="circle-img"></div>
<div class="info-file" title="{{info.path}}">{{info.name}}
<img id="copy_path" path="{{info.path}}" title="copy file path to clipboard" src="{{static_root}}image/copy.svg" />
</div>
</div>
</div>
</div>
</div>
</div>
{% if info.desc %}
<div class="info info3">
<div class="info-title" lang="en">Description:</div>
<div class="airdesc-wrap">
<div class="airdesc">
<div class="desc-content">{{ info.desc|safe }}</div>
<div class="show-more"></div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="device" id='device'>
</div>
<!-- 可以额外显示自定义模块,支持插入Html内容 -->
{{ extra_block|safe }}
<div class="gallery">
<div class="info-title"><span lang="en">Quick view</span></div>
<div class="content">
</div>
</div>
<!--单步-->
{% if steps|length >0 %}
<div class="steps">
<div class="content">
<div class="steps-head">
<div class="head-left">
<div class="order" id='order'><span lang="en">order</span></div>
<div class="order" id='duration'><span lang="en">duration</span></div>
<div class="order" id='status'><span lang="en">status</span></div>
</div>
<div class="head-right">
<span lang="en" class="jump-wrong" id='jump-wrong'>Jump to wrong step</span>
<span lang="en">Filter by:</span>
<span class="filters">
<span lang="en" class="filter" id="all" alt="show all steps">All</span>
<span lang="en" class="filter" id='success' alt="show success steps only">Success</span>
<span lang="en" class="filter" id='fail' alt="show failed steps only">Failed</span>
<span lang="en" class="filter" id="assert" alt="show steps with assertion only">Assert</span>
</span>
</div>
</div>
<div class="steps-content">
<div class="step-left" id='step-left'>
<div class="step-list"></div>
<div id="pageTool"></div>
</div>
<div class="step-right" id='step-right'></div>
</div>
</div>
{% endif %}
</div>
</div>
{% block footer %}
<div class="footer">
<div class="footer-content">
<div class="foo">
<div class="interfaces">
<a class="icon" href="https://github.com/AirtestProject/Airtest" target="_blank">
<img src="{{static_root}}image/Airtest.png" alt="airtest">
</a>
<a class="icon" href="https://github.com/AirtestProject/poco" target="_blank">
<img src="{{static_root}}image/poco.png" alt="Poco">
</a>
</div>
</div>
<div class="foo">
<div class="apps">
<a class="icon ide" href="http://airtest.netease.com/" target="_blank">
<img src="{{static_root}}image/AirtestIDE.png" alt="AirtestIDE">
</a>
<a class="icon" href="https://airlab.163.com/" target="_blank">
<img src="{{static_root}}image/AirLab.png" alt="AirLab">
</a>
</div>
</div>
<div class="foo">
<div class="corp">
<img src="{{static_root}}image/netease.png" alt="NetEase">
<span lang="en">© 1997 - 2019 NetEase, Inc. All Rights Reserved.</span>
</div>
</div>
</div>
</div>
{% endblock %}
</div>
<!-- 录屏 -->
<div class="row gif-wrap show">
<div class="menu">
<div class="pattern pattern1">
<div class="minimize">
<img title="minimize" src="{{static_root}}image/minimize.svg" />
</div>
<div class="close">
<img title="close" src="{{static_root}}image/close.svg" />
</div>
</div>
<div class="pattern pattern2">
<div class="maximize">
<img title="maximize" src="{{static_root}}image/maximize.svg" />
</div>
</div>
</div>
<div class="col-md-6">
{% if records %}
{% for r in records %}
<div align="center" class="embed-responsive embed-responsive-16by9">
<a href="{{ r }}" target="_blank" class="open_in_new_tab">
<img title="open in new tab" src="{{static_root}}image/open_in_new_tab.svg" />
</a>
<video class="embed-responsive-item" controls>
<source src="{{ r }}" type="video/mp4">
</video>
</div>
{% endfor %}
{% endif %}
</div>
</div>
<!-- max pic -->
<div id='magnify' class="mask hide">
<div class="content">
</div>
</div>
</div>
<link href="{{static_root}}css/monokai_sublime.min.css" rel="stylesheet">
<script src="{{static_root}}js/highlight.min.js"></script>
<script type="text/javascript" src="{{static_root}}js/paging.js"></script>
<script src="{{static_root}}js/report.js"></script>
</body>
</html>
#!/usr/bin/env python
# -*- coding:utf8 -*-
import json
import os
import io
import re
import six
import sys
from PIL import Image
import shutil
import jinja2
import traceback
from copy import deepcopy
from datetime import datetime
from jinja2 import evalcontextfilter, Markup, escape
from airtest.aircv import imread, get_resolution
from airtest.core.settings import Settings as ST
from airtest.aircv.utils import compress_image
from airtest.utils.compat import decode_path, script_dir_name
from airtest.cli.info import get_script_info
from six import PY3
LOGDIR = "log"
LOGFILE = "log.txt"
HTML_TPL = "log_template.html"
HTML_FILE = "log.html"
STATIC_DIR = os.path.dirname(__file__)
_paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}')
@evalcontextfilter
def nl2br(eval_ctx, value):
result = u'\n\n'.join(u'<p>%s</p>' % p.replace('\n', '<br>\n')
for p in _paragraph_re.split(escape(value)))
if eval_ctx.autoescape:
result = Markup(result)
return result
def timefmt(timestamp):
"""
Formatting of timestamp in Jinja2 templates
:param timestamp: timestamp of steps
:return: "%Y-%m-%d %H:%M:%S"
"""
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
class LogToHtml(object):
"""Convert log to html display """
scale = 0.5
def __init__(self, script_root, log_root="", static_root="", export_dir=None, script_name="", logfile=LOGFILE, lang="en", plugins=None):
self.log = []
self.script_root = script_root
self.script_name = script_name
self.log_root = log_root
self.static_root = static_root or STATIC_DIR
self.test_result = True
self.run_start = None
self.run_end = None
self.export_dir = export_dir
self.logfile = os.path.join(log_root, logfile)
self.lang = lang
self.init_plugin_modules(plugins)
@staticmethod
def init_plugin_modules(plugins):
if not plugins:
return
for plugin_name in plugins:
print("try loading plugin: %s" % plugin_name)
try:
__import__(plugin_name)
except:
traceback.print_exc()
def _load(self):
logfile = self.logfile.encode(sys.getfilesystemencoding()) if not PY3 else self.logfile
with io.open(logfile, encoding="utf-8") as f:
for line in f.readlines():
self.log.append(json.loads(line))
def _analyse(self, case_img_dir):
""" 解析log成可渲染的dict """
steps = []
children_steps = []
for log in self.log:
depth = log['depth']
if not self.run_start:
self.run_start = log.get('data', {}).get('start_time', '') or log["time"]
self.run_end = log["time"]
if depth == 0:
# single log line, not in stack
steps.append(log)
elif depth == 1:
step = deepcopy(log)
step["__children__"] = children_steps
steps.append(step)
children_steps = []
else:
children_steps.insert(0, log)
# pprint(steps)
translated_steps = [self._translate_step(s, case_img_dir) for s in steps]
return translated_steps
def _translate_step(self, step, case_img_dir):
"""translate single step"""
scene_name = ""
if 'scene_name' in step:
scene_name = step["scene_name"]
name = step["data"]["name"]
title = self._translate_title(name, step)
code = self._translate_code(step, case_img_dir)
desc = self._translate_desc(step, code)
screen = self._translate_screen(step, code)
traceback = self._translate_traceback(step)
assertion = self._translate_assertion(step)
time = step["time"]
# set test failed if any traceback exists
if traceback:
self.test_result = False
translated = {
"title": title,
"time": time,
"code": code,
"screen": screen,
"desc": desc,
"traceback": traceback,
"assert": assertion,
"scene_name": scene_name,
}
return translated
def _translate_assertion(self, step):
if "assert" in step["data"]["name"] and "msg" in step["data"]["call_args"]:
return step["data"]["call_args"]["msg"]
def _translate_screen(self, step, code):
if step['tag'] != "function":
return None
screen = {
"src": None,
"rect": [],
"pos": [],
"vector": [],
"confidence": None,
}
for item in step["__children__"]:
if item["data"]["name"] == "try_log_screen":
snapshot = item["data"].get("ret", None)
if isinstance(snapshot, six.text_type):
src = snapshot
elif isinstance(snapshot, dict):
src = snapshot['screen']
screen['resolution'] = snapshot['resolution']
else:
continue
if self.export_dir: # all relative path
screen['_filepath'] = os.path.join(LOGDIR, src)
else:
screen['_filepath'] = os.path.abspath(os.path.join(self.log_root, src))
screen['_filepath'] = './' + os.path.basename(screen['_filepath'])
screen['src'] = screen['_filepath']
self.get_thumbnail(os.path.join(self.log_root, src))
screen['thumbnail'] = self.get_small_name(screen['src'])
break
display_pos = None
for item in step["__children__"]:
if item["data"]["name"] == "_cv_match" and isinstance(item["data"].get("ret"), dict):
cv_result = item["data"]["ret"]
pos = cv_result['result']
if self.is_pos(pos):
display_pos = [round(pos[0]), round(pos[1])]
rect = self.div_rect(cv_result['rectangle'])
screen['rect'].append(rect)
screen['confidence'] = cv_result['confidence']
break
if step["data"]["name"] in ["touch", "assert_exists", "wait", "exists"]:
# 将图像匹配得到的pos修正为最终pos
if self.is_pos(step["data"].get("ret")):
display_pos = step["data"]["ret"]
elif self.is_pos(step["data"]["call_args"].get("v")):
display_pos = step["data"]["call_args"]["v"]
elif step["data"]["name"] == "swipe":
if "ret" in step["data"]:
screen["pos"].append(step["data"]["ret"][0])
target_pos = step["data"]["ret"][1]
origin_pos = step["data"]["ret"][0]
screen["vector"].append([target_pos[0] - origin_pos[0], target_pos[1] - origin_pos[1]])
if display_pos:
screen["pos"].append(display_pos)
return screen
@classmethod
def get_thumbnail(cls, path):
"""compress screenshot"""
new_path = cls.get_small_name(path)
if not os.path.isfile(new_path):
try:
img = Image.open(path)
compress_image(img, new_path, ST.SNAPSHOT_QUALITY)
except Exception:
traceback.print_exc()
return new_path
else:
return None
@classmethod
def get_small_name(cls, filename):
name, ext = os.path.splitext(filename)
return "%s_small%s" % (name, ext)
def _translate_traceback(self, step):
if "traceback" in step["data"]:
return step["data"]["traceback"]
def _translate_code(self, step, case_img_dir):
if step["tag"] != "function":
return None
step_data = step["data"]
args = []
code = {
"name": step_data["name"],
"args": args,
}
for key, value in step_data["call_args"].items():
args.append({
"key": key,
"value": value,
})
for k, arg in enumerate(args):
value = arg["value"]
if isinstance(value, dict) and value.get("__class__") == "Template":
if self.export_dir: # all relative path
image_path = value['filename']
if not os.path.isfile(os.path.join(self.script_root, image_path)) and value['_filepath']:
# copy image used by using statement
shutil.copyfile(value['_filepath'], os.path.join(self.script_root, value['filename']))
else:
image_path = os.path.abspath(value['_filepath'] or value['filename'])
#arg["image"] = image_path
arg["image"] = case_img_dir + image_path.split("air_case")[1].replace("\\", "/")
if not value['_filepath'] and not os.path.exists(value['filename']):
crop_img = imread(os.path.join(self.script_root, value['filename']))
else:
crop_img = imread(value['_filepath'] or value['filename'])
arg["resolution"] = get_resolution(crop_img)
return code
@staticmethod
def div_rect(r):
"""count rect for js use"""
xs = [p[0] for p in r]
ys = [p[1] for p in r]
left = min(xs)
top = min(ys)
w = max(xs) - left
h = max(ys) - top
return {'left': left, 'top': top, 'width': w, 'height': h}
def _translate_desc(self, step, code):
""" 函数描述 """
if step['tag'] != "function":
return None
name = step['data']['name']
res = step['data'].get('ret')
args = {i["key"]: i["value"] for i in code["args"]}
desc = {
"snapshot": lambda: u"Screenshot description: %s" % args.get("msg"),
"touch": lambda: u"Touch %s" % ("target image" if isinstance(args['v'], dict) else "coordinates %s" % args['v']),
"swipe": u"Swipe on screen",
"wait": u"Wait for target image to appear",
"exists": lambda: u"Image %s exists" % ("" if res else "not"),
"text": lambda: u"Input text:%s" % args.get('text'),
"keyevent": lambda: u"Click [%s] button" % args.get('keyname'),
"sleep": lambda: u"Wait for %s seconds" % args.get('secs'),
"assert_exists": u"Assert target image exists",
"assert_not_exists": u"Assert target image does not exists",
}
# todo: 最好用js里的多语言实现
desc_zh = {
"snapshot": lambda: u"截图描述: %s" % args.get("msg"),
"touch": lambda: u"点击 %s" % (u"目标图片" if isinstance(args['v'], dict) else u"屏幕坐标 %s" % args['v']),
"swipe": u"滑动操作",
"wait": u"等待目标图片出现",
"exists": lambda: u"图片%s存在" % ("" if res else u"不"),
"text": lambda: u"输入文字:%s" % args.get('text'),
"keyevent": lambda: u"点击[%s]按键" % args.get('keyname'),
"sleep": lambda: u"等待%s秒" % args.get('secs'),
"assert_exists": u"断言目标图片存在",
"assert_not_exists": u"断言目标图片不存在",
}
if self.lang == "zh":
desc = desc_zh
ret = desc.get(name)
if callable(ret):
ret = ret()
return ret
def _translate_title(self, name, step):
title = {
"touch": u"Touch",
"swipe": u"Swipe",
"wait": u"Wait",
"exists": u"Exists",
"text": u"Text",
"keyevent": u"Keyevent",
"sleep": u"Sleep",
"assert_exists": u"Assert exists",
"assert_not_exists": u"Assert not exists",
"snapshot": u"Snapshot",
"assert_equal": u"Assert equal",
"assert_not_equal": u"Assert not equal",
}
return title.get(name, name)
@staticmethod
def _render(template_name, output_file=None, **template_vars):
""" 用jinja2渲染html"""
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(STATIC_DIR),
extensions=(),
autoescape=True
)
env.filters['nl2br'] = nl2br
env.filters['datetime'] = timefmt
template = env.get_template(template_name)
html = template.render(**template_vars)
if output_file:
with io.open(output_file, 'w', encoding="utf-8") as f:
f.write(html)
print(output_file)
return html
def is_pos(self, v):
return isinstance(v, (list, tuple))
def copy_tree(self, src, dst, ignore=None):
try:
shutil.copytree(src, dst, ignore=ignore)
except Exception as e:
print(e)
def _make_export_dir(self):
"""mkdir & copy /staticfiles/screenshots"""
# let dirname = <script name>.log
dirname = self.script_name.replace(os.path.splitext(self.script_name)[1], ".log")
# mkdir
dirpath = os.path.join(self.export_dir, dirname)
if os.path.isdir(dirpath):
shutil.rmtree(dirpath, ignore_errors=True)
# copy script
def ignore_export_dir(dirname, filenames):
# 忽略当前导出的目录,防止递归导出
if os.path.commonprefix([dirpath, dirname]) == dirpath:
return filenames
return []
self.copy_tree(self.script_root, dirpath, ignore=ignore_export_dir)
# copy log
logpath = os.path.join(dirpath, LOGDIR)
if os.path.normpath(logpath) != os.path.normpath(self.log_root):
if os.path.isdir(logpath):
shutil.rmtree(logpath, ignore_errors=True)
self.copy_tree(self.log_root, logpath, ignore=shutil.ignore_patterns(dirname))
# if self.static_root is not a http server address, copy static files from local directory
if not self.static_root.startswith("http"):
for subdir in ["css", "fonts", "image", "js"]:
self.copy_tree(os.path.join(self.static_root, subdir), os.path.join(dirpath, "static", subdir))
return dirpath, logpath
def get_relative_log(self, output_file):
try:
html_dir = os.path.dirname(output_file)
return os.path.relpath(os.path.join(self.log_root, 'log.txt') ,html_dir)
except Exception:
traceback.print_exc()
return ""
def get_console(self, output_file):
html_dir = os.path.dirname(output_file)
file = os.path.join(html_dir, 'console.txt')
content = ""
if os.path.isfile(file):
try:
content = self.readFile(file)
except Exception:
try:
content = self.readFile(file, "gbk")
except Exception:
content = traceback.format_exc() + content
content = content + "Can not read console.txt. Please check file in:\n" + file
return content
def readFile(self, filename, code='utf-8'):
content = ""
with io.open(filename, encoding=code) as f:
for line in f.readlines():
content = content + line
return content
def report_data(self, output_file=None, record_list=None, report_dir=None, case_img_dir=None):
"""
Generate data for the report page
:param output_file: The file name or full path of the output file, default HTML_FILE
:param record_list: List of screen recording files
:return:
"""
self._load()
steps = self._analyse(case_img_dir)
script_path = os.path.join(self.script_root, self.script_name)
info = json.loads(get_script_info(script_path))
records = [os.path.join(LOGDIR, f) if self.export_dir
else os.path.abspath(os.path.join(self.log_root, f)) for f in record_list]
if not self.static_root.endswith(os.path.sep):
self.static_root = self.static_root.replace("\\", "/")
self.static_root += "/"
data = {}
data['steps'] = steps
data['name'] = self.script_root
data['scale'] = self.scale
data['test_result'] = self.test_result
data['run_end'] = self.run_end
data['run_start'] = self.run_start
data['static_root'] = report_dir
data['lang'] = self.lang
data['records'] = records
basename = os.path.basename(info['path'])
info['path'] = case_img_dir + basename
data['info'] = info
data['log'] = self.get_relative_log(output_file)
data['console'] = self.get_console(output_file)
data['data'] = json.dumps(data)
return data
def report(self, template_name=HTML_TPL, report_dir=None, case_img_dir=None,output_file=None, record_list=None):
"""
Generate the report page, you can add custom data and overload it if needed
:param template_name: default is HTML_TPL
:param output_file: The file name or full path of the output file, default HTML_FILE
:param record_list: List of screen recording files
:return:
"""
if not self.script_name:
path, self.script_name = script_dir_name(self.script_root)
if self.export_dir:
self.script_root, self.log_root = self._make_export_dir()
# output_file可传入文件名,或绝对路径
output_file = output_file if output_file and os.path.isabs(output_file) \
else os.path.join(self.script_root, output_file or HTML_FILE)
if not self.static_root.startswith("http"):
self.static_root = "static/"
if not record_list:
record_list = [f for f in os.listdir(self.log_root) if f.endswith(".mp4")]
data = self.report_data(output_file=output_file, record_list=record_list, report_dir=report_dir, case_img_dir=case_img_dir)
return self._render(template_name, output_file, **data)
def simple_report(filepath, logpath=True, logfile=LOGFILE, output=HTML_FILE):
path, name = script_dir_name(filepath)
if logpath is True:
logpath = os.path.join(path, LOGDIR)
rpt = LogToHtml(path, logpath, logfile=logfile, script_name=name)
rpt.report(HTML_TPL, output_file=output)
def get_parger(ap):
ap.add_argument("script", help="script filepath")
ap.add_argument("--outfile", help="output html filepath, default to be log.html", default=HTML_FILE)
ap.add_argument("--static_root", help="static files root dir")
ap.add_argument("--log_root", help="log & screen data root dir, logfile should be log_root/log.txt")
ap.add_argument("--record", help="custom screen record file path", nargs="+")
ap.add_argument("--export", help="export a portable report dir containing all resources")
ap.add_argument("--lang", help="report language", default="en")
ap.add_argument("--plugins", help="load reporter plugins", nargs="+")
ap.add_argument("--report", help="placeholder for report cmd", default=True, nargs="?")
return ap
"""
def main(args):
# script filepath
path, name = script_dir_name(args.script)
record_list = args.record or []
log_root = decode_path(args.log_root) or decode_path(os.path.join(path, LOGDIR))
static_root = args.static_root or STATIC_DIR
static_root = decode_path(static_root)
export = decode_path(args.export) if args.export else None
lang = args.lang if args.lang in ['zh', 'en'] else 'en'
plugins = args.plugins
# gen html report
rpt = LogToHtml(path, log_root, static_root, export_dir=export, script_name=name, lang=lang, plugins=plugins)
rpt.report(HTML_TPL, output_file=args.outfile, record_list=record_list)
"""
def main(log_root, static_root, script, lang, plugins, report_dir, case_image_dir, output_file):
# script filepath
#ap = get_parser()
path, name = script_dir_name(script)
record_list = []
log_root = decode_path(log_root) or decode_path(os.path.join(path, LOGDIR))
#static_root = args.static_root or STATIC_DIR
static_root = decode_path(static_root)
export = None
# gen html report
rpt = LogToHtml(path, log_root, static_root, export_dir=export, script_name=name, lang=lang, plugins=plugins)
rpt.report("log_template.html", report_dir, case_image_dir, output_file=output_file)
return rpt
if __name__ == "__main__":
import argparse
ap = argparse.ArgumentParser()
args = get_parger(ap).parse_args()
main(args)
<!DOCTYPE html>
<html>
<head>
<title>测试结果汇总</title>
<style>
table,table tr th,table tr td {
border:1px solid #ccc;
border-collapse: collapse;
color: #669;
padding: 6px 8px;
font-size: 14px;
}
table caption {
font-size: 14px;
color: #039;
text-align: left;
margin-top: 10px;
margin-bottom: -10px;
margin-left: -10px;
}
.fail {
color: red;
width: 7em;
text-align: center;
}
.success {
color: green;
width: 7em;
text-align: center;
}
.details-col-elapsed {
width: 7em;
text-align: center;
}
.details-col-msg {
width: 7em;
text-align: center;
background-color:#ccc;
}
</style>
</head>
<body>
<div>
<!--<div><h2>Test Statistics</h2></div>-->
<table width="1000">
<caption><ul><li>测试概要</li></ul></caption>
<tr width="600">
<th width="300" class='details-col-msg'>用例总数</th>
<th class='details-col-msg'>成功数</th>
<th class='details-col-msg'>运行用时</th>
<th class='details-col-msg'>成功率</th>
</tr>
<tr width="600">
<td class='details-col-elapsed'>1</td>
<td class='details-col-elapsed'>0</td>
<td class='details-col-elapsed'>0分0秒</td>
<td class="details-col-elapsed">0.0%</td>
</tr>
</table>
<!--<div><h2>Test detail</h2></div>-->
<table width="1000">
<caption><ul><li>测试列表</li></ul></caption>
<tr width="600">
<th width="400" class='details-col-msg'>用例名称</th>
<th class='details-col-msg'>执行结果</th>
<th class='details-col-msg'>执行时间(秒)</th>
<th class='details-col-msg'>用例作者</th>
</tr>
<tr width="600">
<td class="details-col-elapsed"><a href='../log/新增已下架商品至购物车列表/log.html' target='_blank'>新增已下架商品至购物车列表</a></td>
<td class="fail">失败</td>
<td class="details-col-elapsed">0.16</td>
<td class="details-col-elapsed">liguangyu</td>
</tr>
</table>
<div><h2></h2></div>
</div>
</body>
</html>
\ No newline at end of file
airtest==1.1.3
airtest-selenium==1.0.3
attrs==19.3.0
bcrypt==3.1.7
beautifulsoup4==4.9.1
bs4==0.0.1
cached-property==1.5.1
certifi==2019.11.28
cffi==1.13.2
chardet==3.0.4
colorama==0.4.3
comtypes==1.1.7
configparser==4.0.2
cryptography==2.8
Deprecated==1.2.7
docker==4.1.0
docker-compose==1.25.1
dockerpty==0.4.1
docopt==0.6.2
facebook-wda==0.2.1
gitdb2==2.0.6
GitPython==3.0.5
hrpc==1.0.8
idna==2.8
importlib-metadata==1.4.0
Jinja2==2.10.3
jsonschema==3.2.0
MarkupSafe==1.1.1
more-itertools==8.1.0
mss==4.0.3
nose==1.3.7
numpy==1.18.1
opencv-contrib-python==3.4.2.17
pandas==0.25.3
paramiko==2.7.1
Pillow==7.0.0
pip==20.0.2
poco==0.96.9
pocoui==1.0.79
pyaml==19.12.0
pycparser==2.19
PyGithub==1.45
PyJWT==1.7.1
pymongo==3.10.1
PyMySQL==0.9.3
PyNaCl==1.3.0
pynput==1.6.5
pypinyin==0.37.0
pyrsistent==0.15.7
python-dateutil==2.8.1
python-gitlab==1.15.0
pytz==2019.3
pywinauto==0.6.3
PyYAML==5.3.1
redis==3.3.11
requests==2.22.0
ruamel.yaml==0.16.10
ruamel.yaml.clib==0.2.0
selenium==3.141.0
setuptools==41.2.0
six==1.13.0
smmap2==2.0.5
soupsieve==2.0.1
svn==0.3.46
texttable==1.6.2
urllib3==1.25.7
websocket-client==0.48.0
wrapt==1.11.2
xlrd==1.2.0
xlutils==2.0.0
xlwt==1.3.0
zipp==1.0.0
pyautogui==0.9.52
openpyxl==3.1.2
\ No newline at end of file
# -*- encoding=utf8 -*-
import os
import sys
import platform
import shutil
import common.run_case_conditions as run
import common.case_tag_get as case_tag_get
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)
if __name__ == '__main__':
# 第一个参数 case 执行类型,'all', 'sc', 'tag'
caseType = sys.argv[1]
# 第2个参数要执行的caseName, 多个文件逗号分割
caseName = sys.argv[2]
# 第3个参数,设置日志等级
log_level = sys.argv[3]
log_level = log_level.lower()
#获取运行环境,写入环境变量
env = sys.argv[4]
os.environ['ENV'] = env.lower()
job_url = ""
#判断传入过来的参数数量是否>5个,如果>5,则传过来的第5个参数为job链接
if len(sys.argv) > 5:
job_url = sys.argv[5]
#复制hosts文件
if platform.system() == 'Windows':
cmd = "xcopy /s /y data\hosts C:\Windows\System32\drivers\etc\ "
else:
cmd = 'mv data/hosts /etc/'
os.system(cmd)
workspace = os.path.abspath(".")
case_path = os.path.join(workspace, 'air_case')
log_path = os.path.join(workspace, 'log')
runtime_log = os.path.join(log_path, 'runtime.txt')
if not os.path.exists(log_path):
os.makedirs('log')
if not os.path.exists(runtime_log):
os.system('type nul >log/run_time.txt')
print("workspace: " + workspace)
print("air_case_path: " + case_path)
print("air_log_path: " + log_path)
print("caseType:" + caseType)
test = run.CustomAirTestCase(workspace, case_path, log_path, runtime_log, log_level)
try:
if caseType == 'sc':
moduleName = sys.argv[2]
caseName = sys.argv[3]
sceName = sys.argv[4]
log_level = sys.argv[5]
log_level = log_level.lower()
# 获取运行环境,写入环境变量
env = sys.argv[6]
os.environ['ENV'] = env.lower()
test.run_by_scenario(moduleName, caseName, sceName)
else:
case_list = caseName.split(',')
if caseType == 'tag':
tag = sys.argv[2]
case_list = case_tag_get.get_case_tag_list(case_path, tag)
run_case, case_list, case_scenario_name = test.merge_new_file(case_list)
test.run_case(run_case, case_list, job_url, case_scenario_name)
except Exception as e:
print("Exception:", e)
finally:
"""
检查air_case中是否有多余的.py文件和场景文件,有就删除
"""
dir_list = os.listdir(case_path)
for cur_file in dir_list:
path = os.path.join(case_path, cur_file)
if os.path.isdir(path):
file_list = os.listdir(path)
for file in file_list:
file_path = os.path.join(path, file)
if os.path.isfile(file_path):
os.remove(file_path)
print('删了什么文件',file_path)
else:
if str(file).startswith("场景"):
name = str(file).replace(".air", "")
del_file = os.path.join(file_path, name + ".py")
if os.path.exists(del_file):
os.remove(del_file)
shutil.rmtree(file_path)
print('删了什么文件', del_file)
print('结束了')
\ No newline at end of file
# -*- encoding=utf8 -*-
import os
import sys
import common.run_case_conditions as run
import common.case_tag_get as case_tag_get
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)
if __name__ == '__main__':
# 第一个参数设备ID
devices = sys.argv[1]
devices_list = devices.split(',')
# 第二个参数 选择单机或多机或多机分布式跑
runType = sys.argv[2]
# 第三个参数 case 执行类型,'all', 'choose', 'tag'
caseType = sys.argv[3]
# 第四个参数要执行的caseName, 多个文件逗号分割
caseName = sys.argv[4]
# 第五个参数,设置日志等级
log_level = sys.argv[5]
log_level = log_level.lower()
workspace = os.path.abspath(".")
case_path = os.path.join(workspace, 'air_case')
log_path = os.path.join(workspace, 'log')
runtime_log = os.path.join(log_path, 'runtime.txt')
print("workspace: " + workspace)
print("air_case_path: " + case_path)
print("air_log_path: " + log_path)
print("runType:" + runType)
print("caseType:" + caseType)
test = run.CustomAirTestCase(workspace, case_path, log_path, runtime_log, log_level)
case_list = caseName.split(',')
if caseType == 'tag':
tag = sys.argv[4]
case_list = case_tag_get.get_case_tag_list_2(case_path, tag)
if runType == 's':
test.run_case_single(devices_list[0], case_list)
os._exit(0)
elif runType == 'd':
test.run_case_by_distri(devices_list, case_list)
os._exit(0)
else:
test.run_case_by_Multi(devices_list, case_list)
os._exit(0)
# -*- encoding=utf8 -*-
import os
import sys
import platform
import shutil
import common.run_case_conditions as run
import common.case_tag_get as case_tag_get
curPath = os.path.abspath(os.path.dirname(__file__))
rootPath = os.path.split(curPath)[0]
sys.path.append(rootPath)
if __name__ == '__main__':
# 第一个参数 case 执行类型,'all', 'sc', 'tag'
caseType = sys.argv[1]
# caseType="tag"
# 第2个参数要执行的caseName, 多个文件逗号分割
caseName = sys.argv[2]
# caseName="登录主数据系统"
# 第3个参数,设置日志等级
log_level = sys.argv[3]
# log_level = "DEBUG"
log_level = log_level.lower()
#获取运行环境,写入环境变量
env = sys.argv[4]
# env="sit"
os.environ['ENV'] = env.lower()
job_url = ""
#判断传入过来的参数数量是否>5个,如果>5,则传过来的第5个参数为job链接
if len(sys.argv) > 5:
job_url = sys.argv[5]
#复制hosts文件
if platform.system() == 'Windows':
cmd = "xcopy /s /y data\hosts C:\Windows\System32\drivers\etc\ "
else:
cmd = 'mv data/hosts /etc/'
os.system(cmd)
workspace = os.path.abspath(".")
case_path = os.path.join(workspace, 'air_case')
log_path = os.path.join(workspace, 'log')
runtime_log = os.path.join(log_path, 'runtime.txt')
if not os.path.exists(log_path):
os.makedirs('log')
if not os.path.exists(runtime_log):
os.system('type nul >log/run_time.txt')
print("workspace: " + workspace)
print("air_case_path: " + case_path)
print("air_log_path: " + log_path)
print("caseType:" + caseType)
test = run.CustomAirTestCase(workspace, case_path, log_path, runtime_log, log_level)
try:
if caseType == 'sc':
moduleName = sys.argv[2]
# moduleName = "spd3.0"
caseName = sys.argv[3]
# caseName = "登录主数据系统"
sceName = sys.argv[4] #场景名称
# sceName = ""
log_level = sys.argv[5]
# log_level = "debug"
log_level = log_level.lower()
# 获取运行环境,写入环境变量
env = sys.argv[6]
os.environ['ENV'] = env.lower()
test.run_by_scenario(moduleName, caseName, sceName)
else:
case_list = caseName.split(',')
if caseType == 'tag':
tag = sys.argv[2]
# tag ="登录主数据系统"
case_list = case_tag_get.get_case_tag_list(case_path, tag)
run_case, case_list, case_scenario_name = test.merge_new_file(case_list)
test.run_case(run_case, case_list, job_url, case_scenario_name)
except Exception as e:
print("Exception:", e)
finally:
"""
检查air_case中是否有多余的.py文件和场景文件,有就删除
"""
dir_list = os.listdir(case_path)
for cur_file in dir_list:
path = os.path.join(case_path, cur_file)
file_list = os.listdir(path)
for file in file_list:
file_path = os.path.join(path, file)
if os.path.isfile(file_path):
os.remove(file_path)
else:
if str(file).startswith("场景"):
name = str(file).replace(".air", "")
del_file = os.path.join(file_path, name + ".py")
if os.path.exists(del_file):
os.remove(del_file)
shutil.rmtree(file_path)
<!DOCTYPE html>
<html>
<head>
<title>测试结果汇总</title>
<style>
table,table tr th,table tr td {
border:1px solid #ccc;
border-collapse: collapse;
color: #669;
padding: 6px 8px;
font-size: 14px;
}
table caption {
font-size: 14px;
color: #039;
text-align: left;
margin-top: 10px;
margin-bottom: -10px;
margin-left: -10px;
}
.fail {
color: red;
width: 7em;
text-align: center;
}
.success {
color: green;
width: 7em;
text-align: center;
}
.details-col-elapsed {
width: 7em;
text-align: center;
}
.details-col-msg {
width: 7em;
text-align: center;
background-color:#ccc;
}
</style>
</head>
<body>
<div>
<!--<div><h2>Test Statistics</h2></div>-->
<table width="1000">
<caption><ul><li>测试概要</li></ul></caption>
<tr width="600">
<th width="300" class='details-col-msg'>用例总数</th>
<th class='details-col-msg'>成功数</th>
<th class='details-col-msg'>运行用时</th>
<th class='details-col-msg'>成功率</th>
</tr>
<tr width="600">
<td class='details-col-elapsed'>{{summary_message.caseNum}}</td>
<td class='details-col-elapsed'>{{summary_message.passNum}}</td>
<td class='details-col-elapsed'>{{summary_message.time}}</td>
<td class="details-col-elapsed">{{summary_message.passRate}}%</td>
</tr>
</table>
<!--<div><h2>Test detail</h2></div>-->
<table width="1000">
<caption><ul><li>测试列表</li></ul></caption>
<tr width="600">
<th width="400" class='details-col-msg'>用例名称</th>
<th class='details-col-msg'>执行结果</th>
<th class='details-col-msg'>执行时间(秒)</th>
<th class='details-col-msg'>用例作者</th>
</tr>
{% for r in case_results %}
<tr width="600">
<td class="details-col-elapsed"><a href='{{log_path}}/{{r.name}}/log.html' target='_blank'>{{r.name}}</a></td>
<td class="{{'success' if r.result else 'fail'}}">{{"成功" if r.result else "失败"}}</td>
<td class="details-col-elapsed">{{r.time}}</td>
<td class="details-col-elapsed">{{r.author}}</td>
</tr>
{% endfor %}
</table>
<div><h2></h2></div>
</div>
</body>
</html>
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment