NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Spectre 5000 // @namespace http://10.100.5.78:3000/ // @version 16.705 // @description The feeling of not wanting to lose, everyone has it. If you find it unfair, you’ll have to work harder yourself. // @author 3rdUnknown // @copyright 2024 // @license MIT // @match http://62.210.192.50:3000/projects // @match http://62.210.192.50:3000/projects/* // @match http://spectre.autotests-xbees.wildix.com/projects // @match http://spectre.autotests-xbees.wildix.com/projects/* // @match https://spectre.autotests-xbees.wildix.com/projects // @match https://spectre.autotests-xbees.wildix.com/projects/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js // @require https://unpkg.com/image-compare-viewer/dist/image-compare-viewer.min.js // @resource IMPORTED_CSS https://unpkg.com/image-compare-viewer/dist/image-compare-viewer.min.css // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_getResourceText // @grant GM_addStyle // @run-at document-idle // @updateURL https://openuserjs.org/meta/3rdUnknown/Spectre_5000.meta.js // ==/UserScript== (function() { 'use strict'; const my_css = GM_getResourceText("IMPORTED_CSS"); GM_addStyle(my_css); GM_addStyle ( ` /* The Modal (background) */ .modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1; /* Sit on top */ left: 0; top: 0; width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgb(0,0,0); /* Fallback color */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ } /* Modal Content/Box */ .modal-content { background-color: #fefefe; margin: 3% auto; /* 3% from the top and centered */ padding: 20px; border: 1px solid #888; width: 80%; /* Could be more or less, depending on screen size */ height: 80%; } .imgModal img { height: 95%; } #image-compare { width: 100%; height: 95%; } div.icv__fluidwrapper { background-size: contain; background-repeat: no-repeat; } .icv__wrapper { background-size: contain; background-repeat: no-repeat; } .icv__label { font-size: 1.25rem; } /* The Close Button */ .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; } .close:hover, .close:focus { color: black; text-decoration: none; cursor: pointer; } /* Date and time */ .date-time { color: black; font-size: 1.5rem; } /* Version */ .version { font-size: 0.8em; color: #909090; margin: -5px 0px -10px 0px; } .greenBuildDate { color:green; font-weight: bold; } .redBuildDate { color:red; font-weight: bold; } .low-difference-count { font-size: 0.8em; color: #909090; font-weight: bold; margin: -20px 0px 0px 0px; } /* ZIP dl */ div#download-block { display: flex; align-items: center; flex-direction: column; } /* Lightbox image border */ .lightbox { border-color: red; border-width: 2px; border-style: dashed; } ` ); // Test data 07.2024 let totalTests = { 'basic visual regression testing x-bees WEB': 414, 'DMbasic visual regression testing x-bees WEB': 414, 'monitoring chats list x-bees WEB': 7, 'basic visual regression testing x-bees iOS': 15, 'basic visual regression testing simulator x-bees iOS': 180, 'basic visual regression testing x-bees Android': 216, 'DM basic visual regression testing x-bees Android': 216, 'storybook visual regression testing x-bees Android': 81, 'Kite basic visual regression testing (chrome) x-bees KITE': 91, 'Kite basic visual regression testing (firefox) x-bees KITE': 90, 'Kite basic visual regression testing (safari) x-bees KITE': 90, 'chrome Kite mobile basic visual regression testing x-bees mobile KITE': 56, 'safari Kite mobile basic visual regression testing x-bees mobile KITE': 35, 'widget: visual regression testing WEBSITE' : 48, 'website: visual regression testing WEBSITE' : 54, 'expand widget: visual regression testing WEBSITE': 11, }; let text, score, lowDiffCount = 0; let lowDiffCountUniq = 0; let testLowDiffTitle = ''; let testNamePrev = ''; //selectors const differenceMarkElements = document.querySelectorAll('span.test__diff.text-muted'); const testimgs = document.querySelectorAll('a.test__image'); const baselineimgs = document.querySelectorAll('a.baseline__image'); addDifferencesMarks(); footerAndHeader(); addVisualComparisonButtons(); improveDateTime(); lowDiffFilter(); totalTestCount(); sortingProjects(); sortingTests(); titleStyle(); addModalsToImgs(testimgs); addModalsToImgs(baselineimgs); //Improve visibility of difference marks function addDifferencesMarks() { let testRow, testName, testEnv; const negative = '<img src="https://github.githubassets.com/images/icons/emoji/unicode/2716.png" width="32" height="32">'; const question = '<img src="https://github.githubassets.com/images/icons/emoji/unicode/2754.png" width="32" height="32">'; const positive = '<img src="https://github.githubassets.com/images/icons/emoji/unicode/2714.png" width="32" height="32">'; for (let elem of differenceMarkElements) { text = elem.innerHTML; testRow = elem.parentElement.parentElement; if (elem.innerHTML !== 'No difference') { console.log(elem.innerHTML); score = parseFloat(elem.innerHTML); if (score <= 0.05) { elem.innerHTML = question + '<font color="orange" size="+2">' + score + '%</font>' + '<br />' + text; } else if (score > 0.05) { elem.innerHTML = negative + '<font color="red" size="+2">' + score + '%</font>' + '<br />' + text; } if (score < 0.10) { lowDiffCount++; testName = testRow.querySelector('.test__name').innerText; testEnv = testRow.querySelector('th p').innerText; if (testName !== testNamePrev) { testLowDiffTitle = `${testLowDiffTitle} ${testName} - ${testEnv}\n`; testNamePrev = testName; lowDiffCountUniq++; } } testRow.classList.add("diffed"); } else { elem.innerHTML = positive + text; testRow.classList.add("pass"); } } } // Improve footer and header: added download all button, added count low diff screenshots function footerAndHeader() { if (window.location.pathname.indexOf('/runs/') !== -1) { const h1 = document.querySelector('div h1'); const lowDiffP = document.createElement("p"); lowDiffP.classList.add("low-difference-count"); lowDiffP.innerText = `Low diff tests: ${lowDiffCountUniq} / Low diff screenshots: ${lowDiffCount}`; lowDiffP.title = testLowDiffTitle; insertAfter(lowDiffP, h1) // DL IMAGES addDownloadBlock(); const archiveName = $(".container").find("h1").text(); const imageExtension = '.png'; $("#download-images").on("click", function () { resetMessage(); //JSZIP.js var zip = new JSZip(); // find every checked item $("tr.test").each(function () { var $this = $(this); var url = $this.find("td#image-test a.test__image").attr('data-href'); var testName= $this.find("span.test__name").text().replace(/\s\s+/g, ' ').replace(/\s+/g, '-'); var testInfo= $this.find("small.text-muted").text().replace(/\s\s+/g, ' ').replace(/,/g, '').replace(/\s+/g, '-'); var filename = testName+testInfo; //var filename = url.replace(/.*\//g, ""); zip.file(filename+imageExtension, urlToPromise(url), {binary:true}); }); // when everything has been downloaded, we can trigger the dl zip.generateAsync({type:"blob"}, function updateCallback(metadata) { var msg = "progression : " + metadata.percent.toFixed(2) + " %"; if(metadata.currentFile) { msg += ", current file = " + metadata.currentFile; } showMessage(msg); updatePercent(metadata.percent|0); }) .then(function callback(blob) { //FileSaver.js saveAs(blob, archiveName+".zip"); showMessage("done !"); }, function (e) { showError(e); }); return false; }); } } // Open image in modal window function addModalsToImgs(imgs) { imgs.forEach(img => { img.setAttribute("data-href", img.href); img.removeAttribute('href'); }); let modalWindow = document.createElement("div"); modalWindow.classList.add("modal","imgModal"); modalWindow.innerHTML = '<div class="modal-content"><span class="close">×</span><img class="lightbox" src=""/></div>'; document.body.appendChild(modalWindow); imgs.forEach((element) => { element.addEventListener('click', function(event){ const modal = document.querySelector(`div.imgModal`); modal.style.display = "block"; const modalimg = modal.querySelector("img"); modalimg.src = element.getAttribute('data-href'); const close = modal.querySelector("span.close"); close.onclick = function() { modal.style.display = "none"; } window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } } }); }); } // Add visual comparison button function addVisualComparisonButtons() { let pics, picReference, picCurrent; let failedTests = document.querySelectorAll(".diffed"); let failedButtons = document.querySelectorAll(".diffed .test__diff"); failedTests.forEach(test => { pics = test.querySelectorAll("a.test__image"); pics.forEach(pic => { pic.classList.add("diffBtn"); }); picReference = pics[0].href; picCurrent= pics[1].href; addModalWindows(test.id, picReference, picCurrent, test); }); failedButtons.forEach(test => { let parentId = test.parentElement.parentElement.id; addElementButton(parentId, test); }); setListener(); setViewers(); } // baseline highlight if ((window.location.pathname.includes('/projects/')) && (window.location.pathname.includes('/suites/')) && (window.location.pathname.indexOf('/runs/') === -1)) { let newBaselines = document.querySelectorAll("tr.new-baseline"); let oldBaselines = document.querySelectorAll("tr.old-baseline"); if (newBaselines) { let breadcrumb = document.querySelector("ul.breadcrumb"); let newButton = document.createElement("button"); newButton.id = 'show-new-baselines'; newButton.classList.add("label--all", "label"); newButton.innerText = `New baselines: ${newBaselines.length}`; breadcrumb.appendChild(newButton); newBaselines.forEach(item => { item.querySelector("th > a").style.color = 'red'; }); newButton.addEventListener('click', function(event){ console.log('Button show-new-baselines clicked'); oldBaselines.forEach(item => { item.remove(); }); var filters = document.querySelector("div.filters"); filters.scrollIntoView({ behavior: "instant", block: "start", inline: "start" }); }); } } function improveDateTime() { //Impove date/time of test run let dateElements = document.querySelectorAll('td.text-muted span'); dateElements.forEach(el => { let dateTime = el.getAttribute('Title'); addElementPTime(el, dateTime); }); // Impove date/time of project dateElements = document.querySelectorAll('#test_count span.text-muted span'); let parentCells = document.querySelectorAll('td#test_count'); dateElements.forEach(function(el, index, arr) { let dateTime = el.getAttribute('Title'); let dateText = el.innerText; if (dateText.includes('days ago')||dateText.includes('month ago')||dateText.includes('months ago')) { el.style.color = 'deeppink'; } let newP = document.createElement("p"); newP.classList.add("date-time"); newP.innerHTML = '🕒 '+dateTime; parentCells[index].appendChild(newP); }); } //Highlight envs in suit names function titleStyle() { let nameElements = document.querySelectorAll('.project a'); nameElements.forEach(el => { let content = el.outerText.toLowerCase(); if (content.includes('stage')) { el.innerHTML = '🏗️ ' + '<font color="darkviolet">' + el.innerHTML + '<font>'; } else if (content.includes('prod')) { el.innerHTML = '🏢 ' + el.innerHTML; } else if (content.includes('stable')) { el.innerHTML = '🏭 ' + '<font color="darkorange">' + el.innerHTML + '<font>'; } }); } //low diff filter options function lowDiffFilter() { if ((window.location.pathname.indexOf('/suites/') !== -1) && (window.location.pathname.indexOf('/runs/') !== -1)) { let filtersForm = document.querySelector('div.filters form'); let selectDiff = document.createElement('select'); selectDiff.name = 'diff'; selectDiff.id = 'diff'; selectDiff.classList.add('form-control'); let optionAll = document.createElement('option'); optionAll.value = 'All'; optionAll.innerText = "All"; let optionLowDiff = document.createElement('option'); optionLowDiff.value = 'low-diff'; optionLowDiff.innerText = "Low diff"; selectDiff.appendChild(optionAll); selectDiff.appendChild(optionLowDiff); selectDiff.addEventListener('change', function() { let tests = document.querySelectorAll(".pass, .test--fail"); if (this.value === 'low-diff') { console.log('low-diff filter ON') tests.forEach(test => { test.remove(); }); } else { console.log('low-diff filter OFF') location.reload(true); } }); filtersForm.appendChild(selectDiff); } } //highlight total test count function totalTestCount() { let executedTestsElements = document.querySelectorAll('a.total-tests'); if (executedTestsElements) { executedTestsElements.forEach(el => { let newCount; let count = el.textContent; let suitName = el.getAttribute('data-suite'); console.log(suitName); suitName = suitName.replace('stable: ','').replace('stage: ', ''); if (totalTests[suitName]) { if (count == totalTests[suitName]) { newCount = `${count}<sup style=" font-size: smaller;">${totalTests[suitName]}</sup>`; } else if (count < totalTests[suitName]) { newCount = `<font color="darkorange"><b>${count}</b></font><sup style=" font-size: smaller;">${totalTests[suitName]}</sup> ⚠️`; let percentMark = document.createElement("a"); percentMark.classList.add("label", "label--warning"); percentMark.title = "Test Pass Rate"; percentMark.innerText = `${((count/totalTests[suitName])*100).toFixed(1)}%`; insertAfter(percentMark, el); } else { newCount = `${count}<sup style=" font-size: smaller;"><font color="darkorange"><b>${totalTests[suitName]}</b></font></sup> ⬆️`; } el.innerHTML = newCount; } }); } } //re-sort projects function sortingProjects() { let projectsTable = document.querySelector('#projects'); let sortedProjectsArr = new Array(); let projectName, projectNameEnv, projectNameNormalized; console.log('-sort: projects-'); if (projectsTable) { let projects = projectsTable.querySelectorAll(".project"); projectsTable.innerHTML = ''; projects.forEach(project => { projectName = project.getAttribute('data-project'); projectNameEnv = projectName.split(":")[0]; // Set priority for suites if (projectName.includes('monitoring chats list')) { projectNameNormalized = projectNameEnv+'9'; } else if (projectName.includes('widget:')||projectName.includes('website:')) { projectNameNormalized = '1website'; } else { projectNameNormalized = projectNameEnv; } console.log(projectNameNormalized) sortedProjectsArr.push({name: projectNameNormalized, val: project}); }); sortedProjectsArr.sort((a, b) => b.name.localeCompare(a.name)); sortedProjectsArr.forEach(project => { projectsTable.appendChild(project.val); }); console.log('-sort: done-'); } } //re-sort tests function sortingTests() { if (window.location.pathname.indexOf('projects/website/suites/website-visual-regression-testingOFF/runs') !== -1) { let testsBlockArr = document.querySelectorAll("tr.test"); let parentTests = testsBlockArr[0].parentElement; let sortedTestsBlockArr = new Array(); console.log('-sort: tests-'); let test, testName; if (testsBlockArr) { parentTests.innerHTML = ''; testsBlockArr.forEach(testsBlock => { test = testsBlock.querySelector("th a"); testName = test.innerHTML.replace(/\s+/g, '-').toLowerCase(); console.log(testName) sortedTestsBlockArr.push({name: testName, val: testsBlock}); testsBlock.remove(); }); sortedTestsBlockArr.sort((a, b) => a.name.localeCompare(b.name)); sortedTestsBlockArr.forEach(testsBlock => { parentTests.appendChild(testsBlock.val); }); } console.log('-sort: done-'); } } })(); ////helpers//// function addElementButton(parentId, test) { let newButton = document.createElement("button"); newButton.id = parentId; newButton.classList.add("label--warning", "label", "diffBtn"); newButton.innerText = "Compare"; insertAfter(newButton, test) } function addModalWindows(parentId, reference, current) { let modalWindow = document.createElement("div"); modalWindow.id = parentId; modalWindow.classList.add("modal"); modalWindow.innerHTML = '<div class="modal-content"><span class="close">×</span><div id="image-compare"><img src="' + reference + '" alt="reference" /><img src="' + current + '" alt="current" /></div></div>'; document.body.appendChild(modalWindow); } function addElementPTime(parent, text) { const time = text.split(' '); const hours = time[3].split(':')[0]; const minutes = time[3].split(':')[1]; let newP = document.createElement("p"); newP.classList.add("date-time"); newP.innerHTML = '🕒 '+text; insertAfter(newP, parent) } function setViewers() { const options = { showLabels: true, labelOptions: { before: 'Baseline', after: 'Comparison', onHover: false }, smoothing: false, startingPoint: 50, fluidMode: true, controlColor: "#FF0000", controlShadow: false, addCircle: false, addCircleBlur: false, }; const viewers = document.querySelectorAll("#image-compare"); viewers.forEach((element) => new ImageCompare(element, options).mount()); } function setListener() { const btn = document.querySelectorAll('button.diffBtn'); btn.forEach((element) => { element.addEventListener('click', function(event){ console.log('Button Clicked'); let modal = document.querySelector(`div#${event.target.id}`); modal.style.display = "block"; let close = modal.querySelector("span.close"); close.onclick = function() { modal.style.display = "none"; } window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } } }); }); } function insertAfter(newNode, existingNode) { existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling); } // DL IMAGES function addDownloadBlock() { let downloadBlock = document.createElement("div"); downloadBlock.id = "download-block"; downloadBlock.classList.add("container"); let downloadButton = document.createElement("button"); downloadButton.id = "download-images"; downloadButton.classList.add("label--fail"); downloadButton.innerHTML = 'Download images'; downloadBlock.appendChild(downloadButton); let progressBlock = document.createElement("div"); progressBlock.id = "progress_bar"; progressBlock.classList.add("progress","hide"); progressBlock.innerHTML = '<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div>'; downloadBlock.appendChild(progressBlock); let progressStatus = document.createElement("p"); progressStatus.id = "result"; progressStatus.classList.add("hide"); progressStatus.innerHTML = ''; downloadBlock.appendChild(progressStatus); let footer = document.createElement("p"); footer.innerHTML = 'All images from the current run will be archived'; downloadBlock.appendChild(footer); document.body.appendChild(downloadBlock); } /** * Reset the message. */ function resetMessage () { $("#result") .removeClass() .text(""); } /** * show a successful message. * @param {String} text the text to show. */ function showMessage(text) { resetMessage(); $("#result") .addClass("alert alert-success") .text(text); } /** * show an error message. * @param {String} text the text to show. */ function showError(text) { resetMessage(); $("#result") .addClass("alert alert-danger") .text(text); } /** * Update the progress bar. * @param {Integer} percent the current percent */ function updatePercent(percent) { $("#progress_bar").removeClass("hide") .find(".progress-bar") .attr("aria-valuenow", percent) .css({ width : percent + "%" }); } var Promise = window.Promise; if (!Promise) { Promise = JSZip.external.Promise; } /** * Fetch the content and return the associated promise. * @param {String} url the url of the content to fetch. * @return {Promise} the promise containing the data. */ function urlToPromise(url) { return new Promise(function(resolve, reject) { //JSZipUtils.js JSZipUtils.getBinaryContent(url, function (err, data) { if(err) { reject(err); } else { resolve(data); } }); }); }