Raw Source
egor00 / Spectre 5000

// ==UserScript==
// @name         Spectre 5000
// @namespace    http://10.100.5.78:3000/
// @version      16.599
// @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 01.2024
    let totalTests = {
        'basic visual regression testing x-bees WEB': 368,
        'DMbasic visual regression testing x-bees WEB': 322,
        'monitoring chats list x-bees WEB': 7,
        'basic visual regression testing x-bees iOS': 18,
        'basic visual regression testing simulator x-bees iOS': 150,
        'basic visual regression testing x-bees Android': 187,
        'DM basic visual regression testing x-bees Android': 187,
        'storybook visual regression testing x-bees Android': 81,
        'Kite basic visual regression testing (chrome) x-bees KITE': 87,
        'Kite basic visual regression testing (firefox) x-bees KITE': 86,
        'Kite basic visual regression testing (safari) x-bees KITE': 86,
        'chrome Kite mobile basic visual regression testing x-bees mobile KITE': 60,
        '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">&times;</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">&times;</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);
            }
        });
    });
}