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 https://spectre.5000 // @version 16.903 // @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 2025 // @license MIT // @match http://62.210.192.50:3000/projects* // @match *://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 ( ` /* ==== Modal Styling ==== */ .modal { display: none; position: fixed; z-index: 1; inset: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.4); } .modal-content { background-color: #fefefe; margin: 3% auto; padding: 20px; border: 1px solid #888; width: 85%; height: 85%; text-align: center; } .imgModal1 img { height: 95%; } .imgModal2 img { max-width: 95%; } #image-compare { width: 100%; height: 95%; } /* ==== Modal Close Button ==== */ .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; } .close:hover, .close:focus { color: black; text-decoration: none; } /* ==== Image Compare Wrapper ==== */ .icv__wrapper, div.icv__fluidwrapper { background-size: contain; background-repeat: no-repeat; } .icv__label { font-size: 1.25rem; } /* ==== Metadata Styling ==== */ .date-time { color: black; font-size: 1.5rem; } .version { font-size: 0.8em; color: #909090; margin: -5px 0 -10px; } .greenBuildDate { color: green; font-weight: bold; } .redBuildDate { color: red; font-weight: bold; } /* ==== ZIP Download Block ==== */ #download-block { display: flex; flex-direction: column; align-items: center; } /* ==== Lightbox Border ==== */ .lightbox { border: 2px dashed red; } /* ==== Tabs: Product ==== */ #product-tabs { margin: 20px 0; border-bottom: 1px solid #ddd; display: flex; flex-wrap: wrap; } .product-tab { padding: 12px 20px !important; margin-right: 8px !important; border: 1px solid #ddd; border-bottom: none; background-color: #f8f8f8; border-radius: 5px 5px 0 0; font-size: 16px !important; font-weight: bold; min-width: 120px; text-align: center; cursor: pointer; transition: all 0.2s ease; } .product-tab:hover { background-color: #e9e9e9 !important; } .product-tab.x-bees-tab { color: #ff8c00 !important; } .product-tab.x-hoppers-tab { color: #006400 !important; } .product-tab.collaboration-tab { color: #00008b !important; } .product-tab.all-tab { color: #333 !important; } .product-tab.website-tab { color: #8b008b !important; } .active-tab { background-color: #f0f0f0 !important; border-bottom: 3px solid currentColor !important; font-weight: bold !important; } .hidden-project { display: none; } /* ==== Tabs: Environments ==== */ #environment-tabs { margin: 0; padding-left: 0; border-bottom: 1px solid #ddd; display: flex; flex-wrap: wrap; } .environment-tab { padding: 8px 15px; margin-right: 6px; border: 1px solid #ddd; border-bottom: none; background-color: #f8f8f8; border-radius: 5px 5px 0 0; font-size: 14px; color: #333; cursor: pointer; transition: all 0.2s ease; } .environment-tab:hover { background-color: #e9e9e9 !important; } .environment-tab.active-env-tab { background-color: #f0f0f0 !important; border-bottom: 3px solid currentColor !important; font-weight: bold !important; color: black !important; } .environment-tab.stage-tab { color: #9c27b0 !important; } .environment-tab.stable-tab { color: #ff9800 !important; } /* ==== Category Filter ==== */ #category-filters { margin: 10px 0; display: flex; flex-wrap: wrap; align-items: center; padding-left: 15px; } .category-label { font-weight: bold; margin-right: 10px; font-size: 14px; } .category-button { padding: 6px 12px !important; margin: 0 8px 5px 0 !important; border: 1px solid #ddd; background-color: #f1f1f1; border-radius: 4px; font-size: 13px !important; color: #333 !important; cursor: pointer; transition: all 0.2s ease; } .category-button:hover { background-color: #e0e0e0; } .category-button.active-category { background-color: #4caf50; color: white !important; font-weight: bold; box-shadow: 0 1px 3px rgba(0,0,0,0.2); } .category-kite { border-left: 4px solid #2196F3; } .category-web { border-left: 4px solid #FF5722; } .category-android { border-left: 4px solid #4CAF50; } .category-ios { border-left: 4px solid #9C27B0; } .category-mobile-kite { border-left: 4px solid #FFC107; } /* ==== Project Rows ==== */ .old-project-row { background-color: #f7e6e6 !important; } .very-old-project-row { background-color: #ffe5b4 !important; font-weight: bold; border-left: 4px solid orange; } /* ==== Test Labels and Stats ==== */ .label--fail, .label--pass, .label--warning, a.total-tests { font-size: 1.65rem !important; font-weight: bold !important; text-align: center !important; vertical-align: middle !important; border-radius: 6px !important; margin: 2px !important; display: inline-block !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } td.text-center { text-align: right !important; /* Изменяет выравнивание с center на left */ padding-left: 15px !important; /* Добавляет отступ слева */ } .label--fail, .label--pass { width: 95px !important; height: 37px !important; line-height: 40px !important; } .label--warning { width: 100px !important; height: 37px !important; line-height: 40px !important; } a.total-tests { width: 95px !important; height: 35px !important; line-height: 40px !important; } /* ==== Row Dividers in Project Table ==== */ tr.project { border-bottom: 2px solid black !important; } tr.project td, tr.project th { padding-top: 8px !important; padding-bottom: 8px !important; } /* ==== Count and Percentage Styling ==== */ #test_count > span > span, .count-number, .count-sup { font-size: 1.5rem !important; } .count-number { vertical-align: middle; font-size: 1.65rem; } .count-sup { vertical-align: super; } .icon-span { font-size: 2.2rem; margin-left: 5px; } .percent-mark { display: inline-block; background-color: #FF8C00; color: white; padding: 0.2rem 0.5rem; border-radius: 0.25rem; font-size: 1rem; } /* ==== Difference Count ==== */ .low-difference-count { font-size: 0.8em; color: #909090; font-weight: bold; margin-top: -20px; } /* ==== Adjust Suite Column Width ==== */ table#projects th:first-child, table#projects td:first-child, #suite th:first-child, #suite td:first-child { max-width: 150px !important; width: 150px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } /* Make sure links in Suite column also respect the width */ table#projects th:first-child a, #suite th:first-child a { display: block !important; max-width: 100% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } /* Ensure text inside cells is trimmed */ table#projects th:first-child *, table#projects td:first-child *, #suite th:first-child *, #suite td:first-child * { white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .download-button { width: 250px; height: 60px; background-color: #28a745; color: white; font-size: 1.75rem; font-weight: 600; border: none; border-radius: 6px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; margin: 4px; transition: background-color 0.2s ease-in-out; } .download-button:hover { background-color: #218838; } td#test_count { padding-left: 8px !important; padding-right: 8px !important; } #environment-tabs { margin: 0; padding-left: 0; border-bottom: none !important; } #category-filters { margin: 0 !important; } .category-label { margin-left: 5px; } /* Ensure proper alignment */ #combined-filters { padding-left: 15px; } ` ); let totalTests = { '------x-bees------': 0, 'monitoring chats list x-bees WEB': 7, 'basic visual regression testing x-bees WEB': 424, 'DMbasic visual regression testing x-bees WEB': 404, 'basic visual regression testing x-bees iOS': 18, 'basic visual regression testing simulator x-bees iOS': 190, 'basic visual regression testing x-bees Android': 234, 'DM basic visual regression testing x-bees Android': 234, 'storybook visual regression testing x-bees Android': 77, 'Kite basic visual regression testing (chrome) x-bees KITE': 96, 'Kite basic visual regression testing (firefox) x-bees KITE': 92, 'Kite basic visual regression testing (safari) x-bees KITE': 92, '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, '------x-hoppers------': 0, 'monitoring chats list x-hoppers WEB': 7, 'basic visual regression testing x-hoppers WEB': 388, 'DMbasic visual regression testing x-hoppers WEB': 369, 'basic visual regression testing x-hoppers iOS': 18, 'basic visual regression testing simulator x-hoppers iOS': 190, 'basic visual regression testing x-hoppers Android': 234, '------collboration------': 0, 'Kite basic visual regression testing (chrome) collaboration KITE': 79, 'DMbasic visual regression testing collaboration WEB': 351, 'basic visual regression testing collaboration WEB': 175, 'basic visual regression testing collaboration Android': 241, 'basic visual regression testing collaboration iOS': 18, 'basic visual regression testing simulator collaboration iOS': 188, 'chrome Kite mobile basic visual regression testing collaboration mobile KITE': 50, 'safari Kite mobile basic visual regression testing collaboration mobile KITE': 33 }; // Tab functionality function addProductTabs() { if (window.location.pathname === '/projects') { console.log('Adding product tabs'); // Create tab container const tabContainer = document.createElement('div'); tabContainer.id = 'product-tabs'; // Define products for tabs const products = [ {name: 'All', class: 'all-tab'}, {name: 'x-bees', class: 'x-bees-tab'}, {name: 'x-hoppers', class: 'x-hoppers-tab'}, {name: 'collaboration', class: 'collaboration-tab'}, {name: 'Website', class: 'website-tab'} ]; // Create tabs products.forEach(product => { const tab = document.createElement('button'); tab.innerText = product.name; tab.dataset.product = product.name.toLowerCase(); tab.classList.add('product-tab', product.class); // Default active tab if (product.name === 'All') { tab.classList.add('active-tab'); } tab.addEventListener('click', function() { // Show environment tabs for all products const envTabs = document.getElementById('environment-tabs'); if (envTabs) { // Always show environment tabs, regardless of the tab envTabs.style.display = 'flex'; // Update active environment tab when changing product const activeEnvTab = document.querySelector('.environment-tab.active-env-tab'); if (!activeEnvTab) { const allEnvTab = document.querySelector('.environment-tab[data-env="all environments"]'); if (allEnvTab) allEnvTab.classList.add('active-env-tab'); } } filterProjectsByProduct(this.dataset.product); // Update active tab styling document.querySelectorAll('.product-tab').forEach(t => { t.classList.remove('active-tab'); }); this.classList.add('active-tab'); }); tabContainer.appendChild(tab); }); // Insert tabs before the projects table const projectsHeader = document.querySelector('h1'); if (projectsHeader) { projectsHeader.parentNode.insertBefore(tabContainer, projectsHeader.nextSibling); // Add environment tabs addEnvironmentTabs(projectsHeader.parentNode, tabContainer); // Add category filters addCategoryFilters(projectsHeader.parentNode); // Make environment tabs visible initially const envTabs = document.getElementById('environment-tabs'); if (envTabs) { envTabs.style.display = 'flex'; } } } } function addRowDividers() { const projectRows = document.querySelectorAll('tr.project'); projectRows.forEach(row => { row.style.borderBottom = '2px solid black'; }); } // Find and hide the Projects header function hideProjectsHeader() { const projectsHeader = document.querySelector('h1'); if (projectsHeader && projectsHeader.textContent.includes('Projects')) { projectsHeader.style.display = 'none'; } } // Add this function to your script function adjustColumnWidths() { // Select the projects table const projectsTable = document.getElementById('projects'); if (!projectsTable) return; // Get all project rows const projectRows = projectsTable.querySelectorAll('tr.project'); if (!projectRows.length) return; console.log('Adjusting column widths for better label fitting'); // Adjust name column width const nameHeaders = projectsTable.querySelectorAll('th:first-child, td:first-child'); nameHeaders.forEach(header => { header.style.maxWidth = '290px'; header.style.width = '220px'; header.style.whiteSpace = 'nowrap'; header.style.overflow = 'hidden'; header.style.textOverflow = 'ellipsis'; }); // Adjust suite column width const suiteHeaders = projectsTable.querySelectorAll('th:nth-child(2), td:nth-child(2)'); suiteHeaders.forEach(header => { header.style.maxWidth = '550px'; header.style.width = '450px'; header.style.whiteSpace = 'normal'; header.style.overflow = 'hidden'; header.style.textOverflow = 'ellipsis'; }); // Make the last column use remaining space const lastRunHeaders = projectsTable.querySelectorAll('th:nth-child(3), td:nth-child(3)'); lastRunHeaders.forEach(header => { header.style.width = 'auto'; }); // Make test result labels more compact const testLabels = projectsTable.querySelectorAll('.label--fail, .label--pass, .label--warning, a.total-tests'); testLabels.forEach(label => { label.style.padding = '1px 1px'; }); // Improve label layout using flexbox const labelContainers = projectsTable.querySelectorAll('td.text-center'); labelContainers.forEach(container => { container.style.whiteSpace = 'nowrap'; }); } // Add environment tabs for all products function addEnvironmentTabs(parentNode, tabContainer) { const envTabContainer = document.createElement('div'); envTabContainer.id = 'environment-tabs'; const environments = [ {name: 'All Environments', class: ''}, {name: '🏗️ Stage', class: 'stage-tab'}, {name: '🏭 Stable', class: 'stable-tab'} ]; environments.forEach(env => { const tab = document.createElement('button'); tab.innerText = env.name; tab.dataset.env = env.name.toLowerCase(); tab.classList.add('environment-tab'); if (env.class) tab.classList.add(env.class); // Default active tab if (env.name === 'All Environments') { tab.classList.add('active-env-tab'); } tab.addEventListener('click', function() { filterProjectsByEnvironment(this.dataset.env); // Update active tab styling document.querySelectorAll('.environment-tab').forEach(t => { t.classList.remove('active-env-tab'); }); this.classList.add('active-env-tab'); }); envTabContainer.appendChild(tab); }); // Insert after product tabs parentNode.insertBefore(envTabContainer, tabContainer.nextSibling); } // Add category filters for platform filtering function addCategoryFilters(parentNode) { const categoryContainer = document.createElement('div'); categoryContainer.id = 'category-filters'; const labelSpan = document.createElement('span'); labelSpan.classList.add('category-label'); labelSpan.innerText = 'Filter by platform:'; categoryContainer.appendChild(labelSpan); const categories = [ {name: 'All', class: ''}, {name: 'WEB', class: 'category-web'}, {name: 'KITE', class: 'category-kite'}, {name: 'Android', class: 'category-android'}, {name: 'iOS', class: 'category-ios'}, {name: 'Mobile KITE', class: 'category-mobile-kite'} ]; categories.forEach(category => { const button = document.createElement('button'); button.innerText = category.name; button.dataset.category = category.name.toLowerCase(); button.classList.add('category-button'); if (category.class) button.classList.add(category.class); // Default active button if (category.name === 'All') { button.classList.add('active-category'); } button.addEventListener('click', function() { filterProjectsByCategory(this.dataset.category); // Update active button styling document.querySelectorAll('.category-button').forEach(b => { b.classList.remove('active-category'); }); this.classList.add('active-category'); }); categoryContainer.appendChild(button); }); // Insert after environment tabs or product tabs const envTabs = document.getElementById('environment-tabs'); if (envTabs) { parentNode.insertBefore(categoryContainer, envTabs.nextSibling); } else { const prodTabs = document.getElementById('product-tabs'); if (prodTabs) { parentNode.insertBefore(categoryContainer, prodTabs.nextSibling); } } } // Filter projects based on selected tab function filterProjectsByProduct(product) { const projects = document.querySelectorAll('tr.project'); const activeEnvTab = document.querySelector('.environment-tab.active-env-tab'); const activeCategoryTab = document.querySelector('.category-button.active-category'); const envFilter = activeEnvTab ? activeEnvTab.dataset.env : 'all environments'; const categoryFilter = activeCategoryTab ? activeCategoryTab.dataset.category : 'all'; projects.forEach(project => { const projectNameElement = project.querySelector('th'); if (!projectNameElement) return; const projectName = projectNameElement.innerText.toLowerCase(); let showProject = true; // Apply product filter if (product !== 'all' && !projectName.includes(product)) { showProject = false; } // Apply environment filter for any product if (product !== 'all' && envFilter !== 'all environments') { if (!projectName.includes(envFilter.toLowerCase())) { showProject = false; } } // Apply category filter if (categoryFilter !== 'all') { if (!projectName.includes(categoryFilter.toLowerCase())) { showProject = false; } } if (showProject) { project.classList.remove('hidden-project'); } else { project.classList.add('hidden-project'); } }); } // Filter projects based on selected environment function filterProjectsByEnvironment(environment) { const activeProductTab = document.querySelector('.product-tab.active-tab'); if (!activeProductTab) return; const activeCategoryTab = document.querySelector('.category-button.active-category'); const product = activeProductTab.dataset.product?.toLowerCase() || 'all'; const categoryFilter = activeCategoryTab?.dataset.category?.toLowerCase() || 'all'; const env = environment.toLowerCase(); document.querySelectorAll('tr.project').forEach(project => { const projectNameElement = project.querySelector('th'); if (!projectNameElement) return; const projectName = projectNameElement.innerText.toLowerCase(); const projectEnv = project.dataset.project?.toLowerCase() || ''; // Product filter if (product !== 'all' && !projectName.includes(product)) { project.classList.add('hidden-project'); return; } // Environment filter if (env !== 'all environments') { const isStage = env.includes('stage'); const isStable = env.includes('stable'); if ((isStage && !projectEnv.includes('stage')) || (isStable && !projectEnv.includes('stable'))) { project.classList.add('hidden-project'); return; } } // Category filter if (categoryFilter !== 'all') { const isMobileKite = categoryFilter === 'mobile kite'; const categoryMatch = isMobileKite ? projectName.includes('mobile') && projectName.includes('kite') : projectName.includes(categoryFilter); if (!categoryMatch) { project.classList.add('hidden-project'); return; } } // If all filters passed project.classList.remove('hidden-project'); }); } // Filter projects based on selected category function filterProjectsByCategory(category) { const activeProductTab = document.querySelector('.product-tab.active-tab'); const activeEnvTab = document.querySelector('.environment-tab.active-env-tab'); if (!activeProductTab) return; const product = activeProductTab.dataset.product; const environment = activeEnvTab ? activeEnvTab.dataset.env : 'all environments'; const projects = document.querySelectorAll('tr.project'); projects.forEach(project => { const projectNameElement = project.querySelector('th'); if (!projectNameElement) return; const projectName = projectNameElement.innerText.toLowerCase(); let showProject = true; // Apply product filter if (product !== 'all' && !projectName.includes(product)) { showProject = false; } // Apply environment filter if not "all environments" if (environment !== 'all environments' && !projectName.includes(environment.toLowerCase())) { showProject = false; } // Apply category filter if (category !== 'all') { // Handle special case for mobile kite if (category === 'mobile kite') { if (!projectName.includes('mobile') || !projectName.includes('kite')) { showProject = false; } } else if (!projectName.includes(category)) { showProject = false; } } if (showProject) { project.classList.remove('hidden-project'); } else { project.classList.add('hidden-project'); } }); } function combineFilterElements() { const envTabs = document.getElementById('environment-tabs'); const categoryFilters = document.getElementById('category-filters'); if (envTabs && categoryFilters) { // Create a container for both elements const combinedContainer = document.createElement('div'); combinedContainer.id = 'combined-filters'; combinedContainer.style.display = 'flex'; combinedContainer.style.flexWrap = 'wrap'; combinedContainer.style.alignItems = 'center'; combinedContainer.style.marginTop = '10px'; combinedContainer.style.marginBottom = '10px'; // Make environment tabs take less space envTabs.style.marginRight = '25px'; envTabs.style.borderBottom = 'none'; // Move elements to the combined container if (envTabs.parentNode) { envTabs.parentNode.insertBefore(combinedContainer, envTabs); } combinedContainer.appendChild(envTabs); combinedContainer.appendChild(categoryFilters); } } // Highlight project age with background colors function highlightProjectAge() { const projectRows = document.querySelectorAll('tr.project'); const now = new Date(); projectRows.forEach(row => { const dateTimeSpan = row.querySelector('td.text-muted span'); if (!dateTimeSpan) return; const dateText = dateTimeSpan.innerText; // Check for old projects (> 1 month) if (dateText.includes('month ago') || dateText.includes('months ago') || dateText.includes('year ago') || dateText.includes('years ago')) { row.classList.add('old-project-row'); } // Check for very old projects (> 3 days) if (dateText.includes('days ago') && parseInt(dateText) > 3) { row.classList.add('very-old-project-row'); } }); } 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'); // Initialize all functions addDifferencesMarks(); footerAndHeader(); addVisualComparisonButtons(); improveDateTime(); lowDiffFilter(); totalTestCount(); sortingProjects(); sortingTests(); envStyle(); productStyle(); addModalsToImgs(testimgs); addModalsToImgs(baselineimgs); addProductTabs(); highlightProjectAge(); addRowDividers(); adjustColumnWidths(); hideProjectsHeader(); combineFilterElements(); // 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++; const testNameElement = testRow.querySelector('.test__name'); const testEnvElement = testRow.querySelector('th p'); if (testNameElement && testEnvElement) { testName = testNameElement.innerText; testEnv = testEnvElement.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'); if (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 containerHeader = $(".container").find("h1"); const archiveName = containerHeader.length ? containerHeader.text() : "spectre-export"; 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 testImgLink = $this.find("td#image-test a.test__image"); if (testImgLink.length) { var url = testImgLink.attr('data-href'); var testNameElement = $this.find("span.test__name"); var testInfoElement = $this.find("small.text-muted"); if (url && testNameElement.length && testInfoElement.length) { var testName = testNameElement.text().replace(/\s\s+/g, ' ').replace(/\s+/g, '-'); var testInfo = testInfoElement.text().replace(/\s\s+/g, ' ').replace(/,/g, '').replace(/\s+/g, '-'); var filename = testName + testInfo; 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! Archive created successfully."); }, function (e) { showError(e); }); return false; }); } } } // Open image in modal window function addModalsToImgs(imgs) { if (!imgs || imgs.length === 0) return; imgs.forEach(img => { if (img && img.href) { img.setAttribute("data-href", img.href); img.removeAttribute('href'); } }); let modalWindow = document.createElement("div"); let imgClass = "imgModal1"; if (window.location.pathname.indexOf('projects/website/suites') !== -1) { imgClass = "imgModal2"; } modalWindow.classList.add("modal", imgClass); modalWindow.innerHTML = '<div class="modal-content"><span class="close">×</span><img class="lightbox" src=""/></div>'; document.body.appendChild(modalWindow); imgs.forEach((element) => { if (element) { element.addEventListener('click', function(event){ const modal = document.querySelector(`div.${imgClass}`); if (modal) { modal.style.display = "block"; const modalimg = modal.querySelector("img"); if (modalimg) { modalimg.src = element.getAttribute('data-href'); } const close = modal.querySelector("span.close"); if (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"); failedTests.forEach(test => { if (!test) return; pics = test.querySelectorAll("a.test__image"); if (!pics || pics.length < 2) return; pics.forEach(pic => { if (pic) pic.classList.add("diffBtn"); }); picReference = pics[0].href; picCurrent = pics[1].href; if (test.id && picReference && picCurrent) { addModalWindows(test.id, picReference, picCurrent, test); } }); const failedButtons = document.querySelectorAll(".diffed .test__diff"); failedButtons.forEach(button => { if (!button || !button.parentElement || !button.parentElement.parentElement) return; let parentId = button.parentElement.parentElement.id; if (parentId) { addElementButton(parentId, button); } }); 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 && newBaselines.length > 0) { let breadcrumb = document.querySelector("ul.breadcrumb"); if (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 => { const link = item.querySelector("th > a"); if (link) link.style.color = 'red'; }); newButton.addEventListener('click', function(event){ console.log('Button show-new-baselines clicked'); oldBaselines.forEach(item => { if (item) item.remove(); }); const filters = document.querySelector("div.filters"); if (filters) { filters.scrollIntoView({ behavior: "instant", block: "start", inline: "start" }); } }); } } } function improveDateTime() { // Improve date/time of test run let dateElements = document.querySelectorAll('td.text-muted span'); dateElements.forEach(el => { if (!el) return; let dateTime = el.getAttribute('Title'); if (dateTime) { addElementPTime(el, dateTime); } }); // Improve date/time of project dateElements = document.querySelectorAll('#test_count span.text-muted span'); let parentCells = document.querySelectorAll('td#test_count'); let parentRows = document.querySelectorAll('tr.project'); dateElements.forEach(function(el, index) { if (!el || !parentCells[index]) return; let dateTime = el.getAttribute('Title'); let dateText = el.innerText; let row = parentRows[index]; // Mark old runs if (dateText) { if (dateText.includes('month ago') || dateText.includes('months ago') || dateText.includes('year ago')) { // Change from deeppink to bold red el.style.color = 'red'; el.style.fontWeight = 'bold'; // Color the entire row light gray for runs older than 1 month if (row) row.style.backgroundColor = '#D3D3D3'; } else if (dateText.includes('days ago')) { // Extract number of days const daysMatch = dateText.match(/(\d+) days ago/); if (daysMatch && daysMatch[1]) { const days = parseInt(daysMatch[1]); if (days > 3) { // Mark text in red for runs older than 3 days el.style.color = 'red'; el.style.fontWeight = 'bold'; // Also color the entire row light red if (row) row.style.backgroundColor = '#FDF5E6'; } } } } if (dateTime) { let newP = document.createElement("p"); newP.classList.add("date-time"); newP.innerHTML = '🕒 ' + dateTime; parentCells[index].appendChild(newP); } }); } // Highlight envs in suit names function envStyle() { let nameElements = document.querySelectorAll('.project a'); nameElements.forEach(el => { if (!el) return; let content = el.outerText ? 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>'; } }); } // Highlight product in suit names function productStyle() { let nameElements = document.querySelectorAll('.project th'); nameElements.forEach(el => { if (!el) return; let text = el.innerText ? el.innerText.toLowerCase() : ''; if (text.includes('x-bees')) { el.innerHTML = '<font color="darkorange">' + el.innerHTML + '</font>'; } else if (text.includes('x-hoppers')) { el.innerHTML = '<font color="darkgreen">' + el.innerHTML + '</font>'; } else if (text.includes('collaboration')) { el.innerHTML = '<font color="darkblue">' + 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'); if (!filtersForm) return; 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 => { if (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.length > 0) { executedTestsElements.forEach(el => { if (!el) return; let newCount; let count = parseInt(el.textContent, 10); let suitName = el.getAttribute('data-suite'); if (!suitName) return; console.log(suitName); suitName = suitName.replace('stable: ', '').replace('stage: ', ''); if (totalTests[suitName]) { // Визуальное отображение числа с индексом степени if (count === totalTests[suitName]) { newCount = `<span class="count-number">${count}</span><sup class="count-sup">${totalTests[suitName]}</sup>`; } else if (count < totalTests[suitName]) { newCount = ` <font color="#FF8C00"> <b class="count-number">${count}</b> </font><sup class="count-sup">${totalTests[suitName]}</sup>`; } else { newCount = `<span class="count-number">${count}</span><sup class="count-sup"><font color="darkorange"><b class="count-number">${totalTests[suitName]}</b></font></sup>`; } // Вставка основного контента (число и степень) el.innerHTML = newCount; // Добавление иконки отдельно const iconSpan = document.createElement('span'); iconSpan.classList.add('icon-span'); // Применение класса для иконки // Определяем иконку в зависимости от условий if (count < totalTests[suitName]) { iconSpan.textContent = '⚠️'; } else if (count > totalTests[suitName]) { iconSpan.textContent = '⬆️'; } // Вставка иконки в элемент, если она не пуста if (iconSpan.textContent) el.appendChild(iconSpan); // % при падении if (count < totalTests[suitName]) { let percentMark = document.createElement("a"); percentMark.classList.add("label", "label--warning", "percent-mark"); percentMark.title = "Test Pass Rate"; percentMark.innerText = `${((count / totalTests[suitName]) * 100).toFixed(1)}%`; insertAfter(percentMark, el); } } }); } } // Re-sort projects function sortingProjects() { let projectsTable = document.querySelector('#projects'); if (!projectsTable) return; let projects = projectsTable.querySelectorAll(".project"); if (!projects || projects.length === 0) return; console.log('-sort: projects-'); let sortedProjectsArr = []; let projectName, projectNameEnv, projectNameNormalized; projects.forEach(project => { if (!project) return; projectName = project.getAttribute('data-project'); if (!projectName) return; 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}); }); if (sortedProjectsArr.length > 0) { sortedProjectsArr.sort((a, b) => b.name.localeCompare(a.name)); // Clear the table before appending sorted elements projectsTable.innerHTML = ''; 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-testing/runs') !== -1) { let testsBlockArr = document.querySelectorAll("tr.test"); if (!testsBlockArr || testsBlockArr.length === 0) return; let parentTests = testsBlockArr[0].parentElement; if (!parentTests) return; let sortedTestsBlockArr = []; console.log('-sort: tests-'); testsBlockArr.forEach(testsBlock => { if (!testsBlock) return; const test = testsBlock.querySelector("th a"); if (!test) return; const testName = test.innerHTML.replace(/\s+/g, '-').toLowerCase(); console.log(testName); sortedTestsBlockArr.push({name: testName, val: testsBlock}); }); if (sortedTestsBlockArr.length > 0) { sortedTestsBlockArr.sort((a, b) => a.name.localeCompare(b.name)); // Clear parent before appending sorted elements parentTests.innerHTML = ''; sortedTestsBlockArr.forEach(testsBlock => { parentTests.appendChild(testsBlock.val); }); console.log('-sort: done-'); } } } // Filter projects based on all criteria function filterByAllCriteria(product, environment, category) { const projects = document.querySelectorAll('tr.project'); projects.forEach(project => { const projectNameElement = project.querySelector('th'); if (!projectNameElement) return; const projectName = projectNameElement.innerText.toLowerCase(); let showProject = true; // Apply product filter if (product !== 'all' && !projectName.includes(product)) { showProject = false; } // Apply environment filter if (environment !== 'all environments' && !projectName.includes(environment.toLowerCase())) { showProject = false; } // Apply category filter if (category !== 'all') { // Handle special case for mobile kite if (category === 'mobile kite') { if (!projectName.includes('mobile') || !projectName.includes('kite')) { showProject = false; } } else if (!projectName.includes(category)) { showProject = false; } } if (showProject) { project.classList.remove('hidden-project'); } else { project.classList.add('hidden-project'); } }); } // Helper functions function addElementButton(parentId, test) { if (!parentId || !test) return; 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) { if (!parentId || !reference || !current) return; 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) { if (!parent || !text) return; const time = text.split(' '); if (time.length < 4) return; const timeParts = time[3].split(':'); if (timeParts.length < 2) return; const hours = timeParts[0]; const minutes = timeParts[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}`); if (!modal) return; modal.style.display = "block"; let close = modal.querySelector("span.close"); if (close) { close.onclick = function() { modal.style.display = "none"; }; } window.onclick = function(event) { if (event.target == modal) { modal.style.display = "none"; } }; }); }); } function insertAfter(newNode, existingNode) { if (!newNode || !existingNode || !existingNode.parentNode) return; 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("download-button"); // заменили класс 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); // Add version info let versionInfo = document.createElement("p"); versionInfo.classList.add("version"); versionInfo.innerHTML = 'Spectre 5000 v.16.901'; downloadBlock.appendChild(versionInfo); 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); } }); }); } })();