tcms/testruns/static/testruns/js/get.js
import { fetchBugDetails } from '../../../../static/js/bugs'
import { jsonRPC } from '../../../../static/js/jsonrpc'
import { propertiesCard } from '../../../../static/js/properties'
import { tagsCard } from '../../../../static/js/tags'
import {
advancedSearchAndAddTestCases, animate,
arrayToDict, bindDeleteCommentButton,
changeDropdownSelectedItem, currentTimeWithTimezone,
markdown2HTML, renderCommentsForObject, renderCommentHTML,
quickSearchAndAddTestCase, treeViewBind,
findSelectorsToShowAndHide, findSelectorsToShowAndHideFromAPIData,
showOrHideMultipleRows
} from '../../../../static/js/utils'
import { initSimpleMDE } from '../../../../static/js/simplemde_security_override'
const allExecutionStatuses = {}
const allExecutions = {}
const expandedExecutionIds = []
const permissions = {
removeTag: false,
addComment: false,
removeComment: false
}
const autocompleteCache = {}
export function pageTestrunsGetReadyHandler () {
permissions.removeTag = $('#test_run_pk').data('perm-remove-tag') === 'True'
permissions.addComment = $('#test_run_pk').data('perm-add-comment') === 'True'
permissions.removeComment = $('#test_run_pk').data('perm-remove-comment') === 'True'
const testRunId = $('#test_run_pk').data('pk')
propertiesCard(testRunId, 'run', 'TestRun.properties', undefined, undefined)
$('#start-button').on('click', function () {
const timeZone = $('#clock').data('time-zone')
const now = currentTimeWithTimezone(timeZone)
jsonRPC('TestRun.update', [testRunId, { start_date: now }], testRun => {
const startDate = moment(testRun.start_date).format('DD MMM YYYY, HH:mm a')
$('.start-date').html(startDate)
$(this).hide()
})
})
$('#stop-button').on('click', function () {
const timeZone = $('#clock').data('time-zone')
const now = currentTimeWithTimezone(timeZone)
jsonRPC('TestRun.update', [testRunId, { stop_date: now }], testRun => {
const stopDate = moment(testRun.stop_date).format('DD MMM YYYY, HH:mm a')
$('.stop-date').html(stopDate)
$(this).hide()
$('#test_run_pk').parent('h1').css({ 'text-decoration': 'line-through' })
})
})
$('.js-bulk-create-testrun').click(function () {
$(this).parents('.dropdown').toggleClass('open')
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
const planId = Number($('#test_run_pk').data('plan-pk'))
window.location.assign(`/runs/new?p=${planId}&c=${selected.caseIds.join('&c=')}`)
return false
})
$('.add-comment-bulk').click(function () {
$(this).parents('.dropdown').toggleClass('open')
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
const enterCommentText = $('#test_run_pk').data('trans-comment')
const comment = prompt(enterCommentText)
if (!comment) {
return false
}
selected.executionIds.forEach(executionId => {
jsonRPC('TestExecution.add_comment', [executionId, comment], () => {
reloadRowFor(allExecutions[executionId])
})
})
return false
})
$('.add-hyperlink-bulk').click(function () {
$(this).parents('.dropdown').toggleClass('open')
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
return addLinkToExecutions(selected.executionIds)
})
$('.remove-execution-bulk').click(function () {
$(this).parents('.dropdown').toggleClass('open')
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
const areYouSureText = $('#test_run_pk').data('trans-are-you-sure')
if (confirm(areYouSureText)) {
removeCases(selected.executionIds)
}
return false
})
$('.change-assignee-bulk').click(function () {
$(this).parents('.dropdown').toggleClass('open')
changeAssigneeBulk()
return false
})
$('.update-case-text-bulk').click(function () {
$(this).parents('.dropdown').toggleClass('open')
updateCaseText()
return false
})
$('.bulk-change-status').click(function () {
$(this).parents('.dropdown').toggleClass('open')
// `this` is the clicked link
const statusId = $(this).data('status-id')
changeStatusBulk(statusId)
// so that we don't follow the link
return false
})
// bind everything in tags table
tagsCard('TestRun', testRunId, { run: testRunId }, permissions.removeTag)
jsonRPC('TestExecutionStatus.filter', {}, executionStatuses => {
// convert from list to a dict for easier indexing later
for (let i = 0; i < executionStatuses.length; i++) {
allExecutionStatuses[executionStatuses[i].id] = executionStatuses[i]
}
const rpcQuery = { run_id: testRunId }
// if page has URI params then try filtering, e.g. by status
const filterParams = new URLSearchParams(location.search)
if (filterParams.has('status_id')) {
rpcQuery.status_id__in = filterParams.getAll('status_id')
}
jsonRPC('TestExecution.filter', rpcQuery, testExecutions => {
drawPercentBar(testExecutions, false)
renderTestExecutions(testExecutions)
renderAdditionalInformation(testRunId)
})
})
$('.bulk-select-checkbox').click(event => {
const isChecked = event.target.checked
const testExecutionSelectors = $('#test-executions-container').find('.test-execution-checkbox:visible')
testExecutionSelectors.each((_index, te) => { te.checked = isChecked })
})
quickSearchAndAddTestCase(testRunId, addTestCaseToRun, autocompleteCache, { case_status__is_confirmed: true })
$('#btn-search-cases').click(function () {
return advancedSearchAndAddTestCases(
testRunId, 'TestRun.add_case', $(this).attr('href'),
$('#test_run_pk').data('trans-error-adding-cases')
)
})
$('.js-toolbar-filter-options li').click(function (ev) {
return changeDropdownSelectedItem(
'.js-toolbar-filter-options',
'#input-filter-button',
ev.target,
$('#toolbar-filter')
)
})
$('#toolbar-filter').on('keyup', function () {
const filterValue = $(this).val().toLowerCase()
const filterBy = $('.js-toolbar-filter-options .selected')[0].dataset.filterType
filterTestExecutionsByProperty(
testRunId,
Object.values(allExecutions),
filterBy,
filterValue
)
})
// assigned-to-me button
document.getElementById('id_assigned_to_me').onchange = () => {
const isChecked = $('#id_assigned_to_me').is(':checked')
const filterValue = isChecked ? $('#test_run_pk').data('current-user') : ''
// update the filter widget which will do the actual filtering
changeDropdownSelectedItem(
'.js-toolbar-filter-options',
'#input-filter-button',
$('.js-toolbar-filter-options [data-filter-type="assignee__username"]').find('a')[0],
$('#toolbar-filter')
)
$('#toolbar-filter').val(filterValue)
$('#toolbar-filter').keyup()
}
// email notifications card
$('#add-cc').click(() => {
const username = prompt($('#test_run_pk').data('trans-enter-assignee-name-or-email'))
if (!username) {
return false
}
jsonRPC('TestRun.add_cc', [testRunId, username], result => {
// todo: instead of reloading render this in the form above
window.location.reload(true)
})
})
$('.js-remove-cc').click((event) => {
const uid = $(event.target).parent('[data-uid]').data('uid')
jsonRPC('TestRun.remove_cc', [testRunId, uid], result => {
$(event.target).parents('tr').hide()
})
})
}
function isFiltered () {
const filterParams = new URLSearchParams(location.search)
return $('#toolbar-filter').val() !== '' || filterParams.has('status_id')
}
function filterTestExecutionsByProperty (runId, executions, filterBy, filterValue) {
// no input => show all rows
if (filterValue.trim().length === 0) {
$('.test-execution-element').show()
$('.test-executions-count').text(executions.length)
return
}
if (filterBy === 'is_automated' && filterValue !== '0' && filterValue !== '1') {
alert($('#test_run_pk').data('trans-bool-value-required'))
return
}
$('.test-execution-element').hide()
if (filterBy === 'is_automated' || filterBy === 'priority' || filterBy === 'category') {
const query = { run: runId }
if (filterBy === 'is_automated') {
query.case__is_automated = filterValue
} else if (filterBy === 'priority') {
query.case__priority__value__icontains = filterValue
} else if (filterBy === 'category') {
query.case__category__name__icontains = filterValue
}
// note: querying TEs so that -FromAPIData() can work properly!
jsonRPC('TestExecution.filter', query, function (filtered) {
// hide again if a previous async request showed something else
$('.test-execution-element').hide()
const rows = findSelectorsToShowAndHideFromAPIData(executions, filtered, '.test-execution-{0}')
showOrHideMultipleRows('.test-execution-element', rows)
$('.test-executions-count').text(rows.show.length)
})
} else {
const rows = findSelectorsToShowAndHide(executions, filterBy, filterValue, '.test-execution-{0}')
showOrHideMultipleRows('.test-execution-element', rows)
$('.test-executions-count').text(rows.show.length)
}
}
function addTestCaseToRun (runId) {
const caseName = $('#search-testcase')[0].value
const testCase = autocompleteCache[caseName]
// test case is already present so don't add it
const allCaseIds = Object.values(allExecutions).map(te => te.case)
if (allCaseIds.indexOf(testCase.id) > -1) {
$('#search-testcase').val('')
return false
}
jsonRPC('TestRun.add_case', [runId, testCase.id], function (result) {
// IMPORTANT: the API result includes a 'sortkey' field value!
window.location.reload(true)
// TODO: remove the page reload above and add the new case to the list
$('#search-testcase').val('')
})
}
function selectedCheckboxes () {
const allSelected = $('.test-execution-checkbox:checked')
if (!allSelected.length) {
const warningText = $('#test_run_pk').data('trans-no-executions-selected')
alert(warningText)
return {}
}
const testCaseIds = []
const testExecutionIds = []
allSelected.each((_index, checkbox) => {
checkbox = $(checkbox)
const testExecutionId = checkbox.data('test-execution-id')
testExecutionIds.push(testExecutionId)
const testCaseId = checkbox.data('test-execution-case-id')
testCaseIds.push(testCaseId)
})
return {
caseIds: testCaseIds,
executionIds: testExecutionIds
}
}
function drawPercentBar (testExecutions, updateTestRun = false) {
let positiveCount = 0
let negativeCount = 0
const allCount = testExecutions.length
const statusCount = {}
Object.values(allExecutionStatuses).forEach(s => (statusCount[s.name] = { count: 0, id: s.id }))
testExecutions.forEach(testExecution => {
const executionStatus = allExecutionStatuses[testExecution.status]
if (executionStatus.weight > 0) {
positiveCount++
} else if (executionStatus.weight < 0) {
negativeCount++
}
statusCount[executionStatus.name].count++
})
renderProgressBars(positiveCount, negativeCount, allCount)
renderCountPerStatusList(statusCount)
if (updateTestRun) {
// first non-zero status reported => TR is started
if (positiveCount + negativeCount === 1 && $('.start-date').html().trim().replace('-', '') === '') {
$('#start-button').click()
return
}
// there are no more neutral executions left => TR is finished; update timestamp
if (positiveCount + negativeCount === allCount && $('.stop-date').html().trim().replace('-', '') === '') {
$('#stop-button').click()
}
}
}
function renderProgressBars (positiveCount, negativeCount, allCount) {
const positivePercent = +(positiveCount / allCount * 100).toFixed(2)
const positiveBar = $('.progress > .progress-completed')
if (positivePercent) {
positiveBar.text(`${positivePercent}%`)
}
positiveBar.css('width', `${positivePercent}%`)
positiveBar.attr('aria-valuenow', `${positivePercent}`)
const negativePercent = +(negativeCount / allCount * 100).toFixed(2)
const negativeBar = $('.progress > .progress-failed')
if (negativePercent) {
negativeBar.text(`${negativePercent}%`)
}
negativeBar.css('width', `${negativePercent}%`)
negativeBar.attr('aria-valuenow', `${negativePercent}`)
const neutralPercent = +(100 - (negativePercent + positivePercent)).toFixed(2)
const neutralBar = $('.progress > .progress-bar-remaining')
if (neutralPercent) {
neutralBar.text(`${neutralPercent}%`)
}
neutralBar.css('width', `${neutralPercent}%`)
neutralBar.attr('aria-valuenow', `${neutralPercent}`)
$('.total-execution-count').text(allCount)
}
function renderCountPerStatusList (statusCount) {
for (const status in statusCount) {
const statusId = statusCount[status].id
$(`#count-for-status-${statusId}`).attr('href', `?status_id=${statusId}`).text(statusCount[status].count)
}
}
function renderTestExecutions (testExecutions) {
// sort executions by sortkey
testExecutions.sort(function (te1, te2) {
return te1.sortkey - te2.sortkey
})
const container = $('#test-executions-container')
testExecutions.forEach(testExecution => {
container.append(renderTestExecutionRow(testExecution))
})
bindEvents()
$('.test-executions-count').html(testExecutions.length)
}
function bindEvents (selector) {
treeViewBind(selector)
$('.test-execution-element').click(function (ev) {
// don't trigger row expansion when kebab menu is clicked
if ($(ev.target).is('button, a, input, .fa-ellipsis-v')) {
return
}
const tePK = $(ev.target)
.parents('.test-execution-element')
.find('.test-execution-checkbox')
.data('test-execution-id')
// row was expanded once, dom is ready
if (expandedExecutionIds.indexOf(tePK) > -1) {
return
}
expandedExecutionIds.push(tePK)
getExpandArea(allExecutions[tePK])
})
}
function getExpandArea (testExecution) {
const container = $(`.test-execution-${testExecution.id}`)
container.find('.test-execution-information .run-date').html(testExecution.stop_date || '-')
container.find('.test-execution-information .build').html(testExecution.build__name)
container.find('.test-execution-information .text-version').html(testExecution.case_text_version)
jsonRPC('TestCase.history',
[testExecution.case, {
history_id: testExecution.case_text_version
}], (data) => {
data.forEach((entry) => {
markdown2HTML(entry.text, container.find('.test-execution-text')[0])
container.find('.test-execution-notes').append(entry.notes)
})
})
const commentsRow = container.find('.comments')
const simpleMDEinitialized = container.find('.comment-form').data('simple-mde-initialized')
if (!simpleMDEinitialized) {
const textArea = container.find('textarea')[0]
const fileUpload = container.find('input[type="file"]')
const editor = initSimpleMDE(textArea, $(fileUpload), textArea.id)
container.find('.comment-form').data('simple-mde-initialized', true)
container.find('.post-comment').click(() => {
const input = editor.value().trim()
if (input) {
jsonRPC('TestExecution.add_comment', [testExecution.id, input], comment => {
editor.value('')
commentsRow.append(renderCommentHTML(
1 + container.find('.js-comment-container').length,
comment,
$('template#comment-template')[0],
parentNode => {
bindDeleteCommentButton(
testExecution.id,
'TestExecution.remove_comment',
permissions.removeComment,
parentNode)
}))
})
}
})
container.find('.change-status-button').click(function () {
const statusId = $(this).attr('data-status-id')
const comment = editor.value().trim()
addCommentToExecution(testExecution, comment, () => {
editor.value('')
})
const $this = $(this)
jsonRPC('TestExecution.update', [testExecution.id, testExecutionUpdateArgs(statusId)], execution => {
// update TestRun if not filtered
reloadRowFor(execution, !isFiltered())
$this.parents('.list-group-item-container').addClass('hidden')
// click the .list-group-item-header, not the .test-execution-element itself, because otherwise the handler will fail
$this.parents('.test-execution-element').next().find('.list-group-item-header').click()
})
})
}
renderCommentsForObject(
testExecution.id,
'TestExecution.get_comments',
'TestExecution.remove_comment',
permissions.removeComment,
commentsRow
)
jsonRPC('TestExecution.get_links', { execution_id: testExecution.id }, links => {
const ul = container.find('.test-execution-hyperlinks')
ul.innerHTML = ''
links.forEach(link => ul.append(renderLink(link)))
bindDeleteLinkButton()
})
jsonRPC('TestCase.list_attachments', [testExecution.case], attachments => {
const ul = container.find('.test-case-attachments')
if (!attachments.length) {
ul.find('.hidden').removeClass('hidden')
return
}
const liTemplate = $('#attachments-list-item')[0].content
attachments.forEach(attachment => {
const li = liTemplate.cloneNode(true)
const attachmentLink = $(li).find('a')[0]
attachmentLink.href = attachment.url
attachmentLink.innerText = attachment.url.split('/').slice(-1)[0]
ul.append(li)
})
})
jsonRPC('TestExecution.history', testExecution.id, history => {
const historyContainer = container.find('.history-container')
history.forEach(h => {
historyContainer.append(renderHistoryEntry(h))
})
})
}
function addCommentToExecution (testExecution, input, handler) {
if (!input) {
return
}
jsonRPC('TestExecution.add_comment', [testExecution.id, input], handler)
}
function renderAdditionalInformation (testRunId, execution) {
let linksQuery = { execution__run: testRunId }
let casesQuery = { executions__run: testRunId }
let componentQ = { cases__executions__run: testRunId }
let tagsQ = { case__executions__run: testRunId }
let propertiesQ = { execution__run: testRunId }
const planId = Number($('#test_run_pk').data('plan-pk'))
// if called from reloadRowFor(execution) then filter only for
// that one row
if (execution) {
linksQuery = { execution: execution.id }
casesQuery = { executions: execution.id }
componentQ = { cases__executions: execution.id }
tagsQ = { case__executions: execution.id }
propertiesQ = { execution: execution.id }
}
// update bug icons for all executions
jsonRPC('TestExecution.get_links', linksQuery, (links) => {
const withDefects = new Set()
links.forEach((link) => {
if (link.is_defect) {
withDefects.add(link.execution)
}
})
withDefects.forEach((te) => {
$(`.test-execution-${te}`).find('.js-bugs').removeClass('hidden')
})
})
// update properties display
jsonRPC('TestExecution.properties', propertiesQ, (props) => {
const propsPerTe = props.reduce(function (map, obj) {
if (!(obj.execution in map)) {
map[obj.execution] = {}
}
map[obj.execution][obj.name] = obj.value
return map
}, {})
for (const teId of Object.keys(propsPerTe)) {
const row = $(`.test-execution-${teId}`)
// when loading this page filtered by status some TCs do not exist
// but we don't know about it b/c the above queries are overzealous
if (!row.length) { continue }
let propString = ''
for (const name of Object.keys(propsPerTe[teId])) {
propString += `${name}: ${propsPerTe[teId][name]}; `
}
const propertiesRow = row.find('.js-row-properties')
propertiesRow.toggleClass('hidden')
propertiesRow.html(propertiesRow.html() + propString + '<br>')
}
})
// update priority, category & automation status for all executions
// also tags & components via nested API calls
jsonRPC('Component.filter', componentQ, components => {
jsonRPC('Tag.filter', tagsQ, tags => {
jsonRPC('TestCase.filter', casesQuery, testCases => {
jsonRPC('TestCase.filter', { plan: planId }, function (casesInPlan) {
casesInPlan = arrayToDict(casesInPlan)
casesInPlan = Object.keys(casesInPlan).map(id => parseInt(id))
for (const testCase of testCases) {
let rowSelector = `.test-execution-case-${testCase.id}`
// Preferably operate over the exact execution row to prevent
// appending new HTML onto existing values, e.g. Tags. See #3367
//
// Root cause of the bug in #3367 is that some fields contain icons
// and pre-existing HTML coming from the template and we can't call .empty()
// on them. When such TE is parametrized then there are multiple HTML rows
// matching `rowSelector`/`testCase.id`, therefore the UI is appended to many times!
if (execution) {
rowSelector += `.test-execution-${execution.id}`
}
const row = $(rowSelector)
// when loading this page filtered by status some TCs do not exist
// but we don't know about it b/c the above queries are overzealous
if (!row.length) { continue }
row.find('.test-execution-priority').html(testCase.priority__value)
row.find('.test-execution-category').html(testCase.category__name)
const isAutomatedElement = row.find('.test-execution-automated')
const isAutomatedIcon = testCase.is_automated ? 'fa-cog' : 'fa-hand-paper-o'
const isAutomatedAttr = testCase.is_automated ? isAutomatedElement.data('automated') : isAutomatedElement.data('manual')
isAutomatedElement.addClass(isAutomatedIcon)
isAutomatedElement.attr('title', isAutomatedAttr)
// test case isn't part of the parent test plan
if (casesInPlan.indexOf(testCase.id) === -1) {
row.find('.js-tc-not-in-tp').toggleClass('hidden')
}
// render tags and components if available
testCase.tagNames = []
// todo: this is sub-optimal b/c it searches whether tag is attached
// to the current testCase and does so for every case in the list
for (let i = 0; i < tags.length; i++) {
if (tags[i].case === testCase.id && testCase.tagNames.indexOf(tags[i].name) === -1) {
testCase.tagNames.push(tags[i].name)
}
}
if (testCase.tagNames.length) {
const tagsRow = row.find('.js-row-tags')
tagsRow.toggleClass('hidden')
tagsRow.html(tagsRow.html() + testCase.tagNames.join(', '))
}
testCase.componentNames = []
// todo: this is sub-optimal b/c it searches whether component is attached
// to the current testCase and does so for every case in the list
for (let i = 0; i < components.length; i++) {
if (components[i].cases === testCase.id) {
testCase.componentNames.push(components[i].name)
}
}
if (testCase.componentNames.length) {
const componentsRow = row.find('.js-row-components')
componentsRow.toggleClass('hidden')
componentsRow.html(componentsRow.html() + testCase.componentNames.join(', '))
}
// update internal data structure
const teID = row.find('.test-execution-checkbox').data('test-execution-id')
allExecutions[teID].tags = testCase.tagNames
allExecutions[teID].components = testCase.componentNames
}
})
})
})
})
}
function renderHistoryEntry (historyEntry) {
if (!historyEntry.history_change_reason) {
return ''
}
const template = $($('#history-entry')[0].content.cloneNode(true))
template.find('.history-date').html(historyEntry.history_date)
template.find('.history-user').html(historyEntry.history_user__username)
// convert to markdown code block for the diff language
const changeReason = `\`\`\`diff\n${historyEntry.history_change_reason}\n\`\`\``
markdown2HTML(changeReason, template.find('.history-change-reason')[0])
return template
}
function renderTestExecutionRow (testExecution) {
// refresh the internal data structure b/c some fields are used
// to render the expand area and may have changed via bulk-update meanwhile
testExecution.status__name = $('#test_run_pk').data(`trans-execution-status-${testExecution.status}`)
allExecutions[testExecution.id] = testExecution
const testExecutionRowTemplate = $('#test-execution-row')[0].content
const template = $(testExecutionRowTemplate.cloneNode(true))
template.find('.test-execution-checkbox').data('test-execution-id', testExecution.id)
template.find('.test-execution-checkbox').data('test-execution-case-id', testExecution.case)
template.find('.test-execution-element').attr('id', `test-execution-${testExecution.id}`)
template.find('.test-execution-element').addClass(`test-execution-${testExecution.id}`)
template.find('.test-execution-element').addClass(`test-execution-case-${testExecution.case}`)
template.find('.test-execution-info').html(`TE-${testExecution.id}/TC-${testExecution.case}:`)
template.find('.test-execution-info-link').html(testExecution.case__summary)
template.find('.test-execution-info-link').attr('href', `/case/${testExecution.case}/`)
template.find('.test-execution-tester').html(testExecution.tested_by__username || '-')
template.find('.test-execution-asignee').html(testExecution.assignee__username || '-')
const testExecutionStatus = allExecutionStatuses[testExecution.status]
template.find('.test-execution-status-icon').addClass(testExecutionStatus.icon).css('color', testExecutionStatus.color)
template.find('.test-execution-status-name').html(testExecution.status__name).css('color', testExecutionStatus.color)
template.find('.add-link-button').click(() => addLinkToExecutions([testExecution.id]))
template.find('.one-click-bug-report-button').click(() => fileBugFromExecution(testExecution))
// remove from expanded list b/c data may have changed
expandedExecutionIds.splice(expandedExecutionIds.indexOf(testExecution.id), 1)
// WARNING: only comments related stuff below
if (!permissions.addComment) {
template.find('.comment-form').hide()
return template
}
template.find('textarea')[0].id = `comment-for-testexecution-${testExecution.id}`
template.find('input[type="file"]')[0].id = `file-upload-for-testexecution-${testExecution.id}`
return template
}
function changeStatusBulk (statusId) {
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
const updateArgs = testExecutionUpdateArgs(statusId)
selected.executionIds.forEach(executionId => {
jsonRPC('TestExecution.update', [executionId, updateArgs], execution => {
// update TestRun if not filtered
reloadRowFor(execution, !isFiltered())
})
})
}
function reloadRowFor (execution, updateTestRun = false) {
const testExecutionRow = $(`.test-execution-${execution.id}`)
animate(testExecutionRow, () => {
testExecutionRow.replaceWith(renderTestExecutionRow(execution))
// note: this is here b/c animate() is async and we risk race conditions
// b/c we use global variables for state. The drawback is that progress
// will be updated even if statuses aren't changed !!!
drawPercentBar(Object.values(allExecutions), updateTestRun)
renderAdditionalInformation(execution.run_id, execution)
bindEvents(`.test-execution-${execution.id}`)
})
}
function changeAssigneeBulk () {
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
const enterAssigneeText = $('#test_run_pk').data('trans-enter-assignee-name-or-email')
const assignee = prompt(enterAssigneeText)
if (!assignee) {
return false
}
selected.executionIds.forEach(executionId => {
jsonRPC('TestExecution.update', [executionId, { assignee }], execution => {
reloadRowFor(execution)
})
})
}
function updateCaseText () {
const selected = selectedCheckboxes()
if ($.isEmptyObject(selected)) {
return false
}
selected.executionIds.forEach(executionId =>
jsonRPC('TestExecution.update', [executionId, { case_text_version: 'latest' }], execution => {
reloadRowFor(execution)
})
)
}
function fileBugFromExecution (execution) {
// remove all previous event handlers
$('.one-click-bug-report-form').off('submit')
// this handler must be here, because if we bind it when the page is loaded.
// we have no way of knowing for what execution ID the form is submitted for.
$('.one-click-bug-report-form').submit(() => {
const trackerId = $('.one-click-bug-report-form #id-issue-tracker').val()
jsonRPC('Bug.report', [execution.id, trackerId], result => {
// close the modal
$('#one-click-bug-report-modal button.close').click()
if (result.rc !== 0) {
alert(result.response)
return
}
reloadRowFor(execution)
// unescape b/c Issue #1533
const targetUrl = result.response.replace(/&/g, '&')
window.open(targetUrl, '_blank')
})
return false
})
return true // so that the modal is opened
}
function addLinkToExecutions (testExecutionIDs) {
// remove all previous event handlers
$('.add-hyperlink-form').off('submit')
// this handler must be here, because if we bind it when the page is loaded.
// we have no way of knowing for what execution ID the form is submitted for.
$('.add-hyperlink-form').submit(() => {
const url = $('.add-hyperlink-form #id_url').val()
const name = $('.add-hyperlink-form #id_name').val()
const isDefect = $('.add-hyperlink-form #defectCheckbox').is(':checked')
const updateTracker = true
testExecutionIDs.forEach(testExecutionId => {
jsonRPC('TestExecution.add_link', [{
execution_id: testExecutionId,
url,
name,
is_defect: isDefect
}, updateTracker], link => {
const testExecutionRow = $(`div.list-group-item.test-execution-${testExecutionId}`)
animate(testExecutionRow, () => {
if (link.is_defect) {
testExecutionRow.find('.js-bugs').removeClass('hidden')
}
const ul = testExecutionRow.find('.test-execution-hyperlinks')
ul.append(renderLink(link))
bindDeleteLinkButton()
})
})
})
// clean the values
$('.add-hyperlink-form #id_name').val('')
$('.add-hyperlink-form #id_url').val('')
$('.add-hyperlink-form #defectCheckbox').bootstrapSwitch('state', false)
$('.add-hyperlink-form #autoUpdateCheckbox').bootstrapSwitch('state', false)
// close the modal
$('#add-link-modal button.close').click()
return false
})
return true // so that the modal is opened
}
function renderLink (link) {
const linkEntryTemplate = $('#link-entry')[0].content
const template = $(linkEntryTemplate.cloneNode(true))
if (link.is_defect) {
template.find('.link-icon').addClass('fa fa-bug')
const bugTooltip = template.find('.bug-tooltip')
bugTooltip.css('visibility', 'visible')
template.find('[data-toggle=popover]')
.popovers()
.on('show.bs.popover', () => fetchBugDetails({ href: link.url }, bugTooltip))
}
const linkUrlEl = template.find('.link-url')
linkUrlEl.html(link.name || link.url)
linkUrlEl.attr('href', link.url)
template.find('.js-remove-linkreference').attr('data-link-id', link.id)
template.find('li').attr('data-link-id', link.id)
return template
}
export function bindDeleteLinkButton () {
$('.js-remove-linkreference').click(function (event) {
const row = $(event.target).parents('li')
const linkId = $(event.target).parent('.js-remove-linkreference').data('link-id')
if (linkId) {
jsonRPC('TestExecution.remove_link', { pk: linkId }, () => {
$(row).fadeOut(500)
})
}
return false
})
}
function removeCases (executionIds) {
for (const executionId of executionIds) {
jsonRPC('TestExecution.remove', { id: executionId }, () => {
$(`#test-execution-${executionId}`).remove()
expandedExecutionIds.splice(expandedExecutionIds.indexOf(executionId), 1)
delete allExecutions[executionId]
const testExecutionCountEl = $('.test-executions-count')
const count = parseInt(testExecutionCountEl[0].innerText)
testExecutionCountEl.html(count - 1)
}, true)
}
drawPercentBar(Object.values(allExecutions))
}
function testExecutionUpdateArgs (statusId) {
const statusWeight = allExecutionStatuses[statusId].weight
const updateArgs = { status: statusId, stop_date: '' }
if (statusWeight !== 0) {
const timeZone = $('#clock').data('time-zone')
updateArgs.stop_date = currentTimeWithTimezone(timeZone)
}
return updateArgs
}