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 17.050
// @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
// @homepageURL https://openuserjs.org/scripts/3rdUnknown/Spectre_5000
// @supportURL https://openuserjs.org/scripts/3rdUnknown/Spectre_5000
// @match *://62.210.170.212: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
// @downloadURL https://openuserjs.org/install/3rdUnknown/Spectre_5000.user.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%;
}
/* Compare lightbox: no page/inner scroll (website keeps scroll for tall pages) */
body:has(.modal--compare.modal-show:not(.modal--website)) {
overflow: hidden !important;
}
.modal--compare:not(.modal--website) {
overflow: hidden !important;
}
.modal--compare:not(.modal--website) .modal-content {
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 94vh;
max-height: 94vh;
margin: 3vh auto;
overflow: hidden;
position: relative;
}
.modal--compare:not(.modal--website) .compare-viewport {
display: flex;
flex: 1 1 auto;
min-height: 0;
width: 100%;
align-items: stretch;
}
.modal--compare:not(.modal--website) .compare-indicator-gutter {
flex: 0 0 14px;
width: 14px;
position: relative;
box-sizing: border-box;
}
.modal--compare:not(.modal--website) .close {
float: none;
position: absolute;
top: 8px;
right: 12px;
z-index: 10;
}
.modal--compare:not(.modal--website) #image-compare {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
height: auto;
overflow: hidden;
}
.modal--compare:not(.modal--website) #image-compare .icv {
height: 100%;
max-height: 100%;
}
/* ==== 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;
}
/* Image Compare Labels Styling */
.icv__label {
font-size: 0.9rem !important;
font-weight: 600 !important;
padding: 4px 8px !important;
border-radius: 4px !important;
position: absolute !important;
bottom: 10px !important;
z-index: 1000 !important;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5) !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.2) !important;
}
/* Baseline label - зеленый, левый угол */
.icv__label-before,
.icv__label.icv__label-before {
background-color: #28a745 !important;
color: white !important;
border: 1px solid #1e7e34 !important;
left: 10px !important;
}
/* CSS переменные для динамической ширины линий */
:root {
--before-line-width: 0px;
--after-line-width: 0px;
}
.icv__label-before::before,
.icv__label.icv__label-before::before {
content: '' !important;
position: absolute !important;
top: 50% !important;
left: 100% !important;
transform: translateY(-50%) !important;
width: var(--before-line-width) !important;
height: 2px !important;
background-color: #28a745 !important;
border-radius: 1px !important;
z-index: 999 !important;
transition: width 0.2s ease !important;
}
/* Comparison label - оранжевый, правый угол */
.icv__label-after,
.icv__label.icv__label-after {
background-color: #ff8c00 !important;
color: white !important;
border: 1px solid #e07600 !important;
right: 10px !important;
}
.icv__label-after::before,
.icv__label.icv__label-after::before {
content: '' !important;
position: absolute !important;
top: 50% !important;
right: 100% !important;
transform: translateY(-50%) !important;
width: var(--after-line-width) !important;
height: 2px !important;
background-color: #ff8c00 !important;
border-radius: 1px !important;
z-index: 999 !important;
transition: width 0.2s ease !important;
}
/* ==== Metadata Styling ==== */
.date-time {
color: black;
font-size: 1.5rem;
}
.version {
font-size: 0.8em;
color: #909090;
margin: -5px 0 -10px;
}
/* ==== 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 5px 0;
padding-left: 0;
padding-bottom: 10px;
border-bottom: 2px solid #666;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
}
.product-tab {
padding: 12px 20px !important;
margin-right: 8px !important;
margin-bottom: 0 !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: none;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
}
.environment-tab {
padding: 8px 15px;
margin-right: 6px;
margin-bottom: 0;
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.stage-plus-tab {
color: #6a5acd !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);
}
.report-button {
margin-left: 48px !important;
border-left: 4px solid rgb(250, 35, 100);
color:rgb(200, 35, 100) !important;
}
.report-button:hover {
background-color: #f3e5f5 !important;
}
.report-button:active,
.report-button:focus {
background-color: #e1bee7 !important;
color: #6a1b9a !important;
outline: none !important;
box-shadow: none !important;
}
.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: #ffe5b4 !important;
font-weight: bold;
}
.very-old-project-row {
background-color: #f7e6e6 !important;
font-weight: bold;
border-left: 4px solid red;
}
/* ==== 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;
height: 40px !important;
line-height: 40px !important;
padding: 0 8px !important;
border: 1px solid transparent !important;
box-sizing: border-box !important;
}
td.text-center {
text-align: right !important;
padding-left: 15px !important;
}
.label--fail,
.label--pass {
width: 95px !important;
}
.label--warning {
width: 70px !important;
}
a.total-tests {
width: 105px !important;
height: 40px !important;
line-height: 40px !important;
}
/* ==== Custom Blue Fail Button ==== */
.fail-button-blue {
width: 140px !important;
height: 37px !important;
line-height: 40px !important;
background-color: #17a2b8 !important;
color: white !important;
border-color: #17a2b8 !important;
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;
border: 1px solid #17a2b8 !important;
box-sizing: border-box !important;
}
.fail-button-blue:hover {
background-color: #138496 !important;
border-color: #117a8b !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.4rem !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.1rem 0.3rem;
border-radius: 0.25rem;
font-size: 0.8rem;
min-width: 45px;
text-align: center;
}
/* ==== 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;
}
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;
}
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;
}
#combined-filters {
padding-left: 0;
margin-bottom: 10px;
border-bottom: 1px solid #ddd;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 20px;
}
#environment-tabs {
margin: 0;
padding-left: 0;
border-bottom: none !important;
display: flex;
flex-wrap: wrap;
align-items: flex-end;
}
#category-filters {
margin: 0 !important;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.category-label {
margin-left: 0;
margin-right: 10px;
}
/* ==== Table Header Color Override ==== */
table#projects thead th,
table#projects tr:first-child th,
.table-header,
thead th {
background-color: #008080 !important;
color: white !important;
}
/* ==== Set as Baseline Button Styling ==== */
.baseline-button-success,
button.baseline-button-success,
.btn.baseline-button-success,
a.baseline-button-success {
background-color: #28a745 !important;
color: white !important;
border-color: #28a745 !important;
border: 2px solid #28a745 !important;
}
.baseline-button-success:hover,
button.baseline-button-success:hover,
.btn.baseline-button-success:hover,
a.baseline-button-success:hover {
background-color: #218838 !important;
border-color: #1e7e34 !important;
}
.baseline-notification {
position: fixed;
top: 20px;
right: 20px;
background-color: #28a745;
color: white;
padding: 15px 20px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
z-index: 10000;
font-size: 14px;
font-weight: bold;
opacity: 0;
transform: translateX(100%);
transition: all 0.3s ease;
}
.baseline-notification.show {
opacity: 1;
transform: translateX(0);
}
/* ==== Difference Score Styling ==== */
.diff-score-low {
color: #ff8c00 !important;
font-size: 1.8rem !important;
font-weight: bold !important;
}
.diff-score-high {
color: #dc3545 !important;
font-size: 2.0rem !important;
font-weight: bold !important;
}
.diff-icon {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 5px;
}
/* ==== Environment and Product Colors ==== */
.env-stage {
color: #9932cc !important; /* darkviolet */
}
.env-stable {
color: #ff8c00 !important; /* darkorange */
}
.product-x-bees {
color: #ff8c00 !important; /* darkorange */
}
.product-x-hoppers {
color: #006400 !important; /* darkgreen */
}
.product-collaboration {
color: #00008b !important; /* darkblue */
}
/* ==== Test Count Colors ==== */
.count-warning {
color: #ff8c00 !important;
font-weight: bold !important;
}
.count-excess {
color: #ff8c00 !important;
font-weight: bold !important;
}
/* ==== Date and Project Age Styling ==== */
.date-old {
color: red !important;
font-weight: bold !important;
}
.project-old {
background-color: #FDF5E6 !important;
}
.project-very-old {
background-color: #D3D3D3 !important;
}
/* ==== New Baseline Styling ==== */
.new-baseline-link {
color: red !important;
}
/* ==== Modal Display States ==== */
.modal-show {
display: block !important;
}
.modal-hide {
display: none !important;
}
/* ==== Utility Classes ==== */
.flex-container {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
margin-top: 10px !important;
margin-bottom: 10px !important;
}
.hidden-element {
display: none !important;
}
.column-name-width {
max-width: 250px !important;
width: 250px !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.column-suite-width {
max-width: 350px !important;
width: 350px !important;
white-space: normal !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
}
.column-last-run-width {
width: 600px !important;
min-width: 550px !important;
}
/* ==== Split Cell Styling ==== */
.split-cell-container {
display: flex !important;
width: 100% !important;
min-height: 50px !important;
}
.split-cell-container > div:first-child {
flex: 0 0 200px !important;
border-right: 1px solid #ddd !important;
padding-right: 8px !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
}
.split-cell-container > div:last-child {
flex: 1 !important;
padding-left: 8px !important;
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
gap: 2px !important;
}
/* ==== Date Info Header Styling ==== */
.date-info-header {
background-color: #008080 !important;
color: white !important;
padding: 8px !important;
width: 600px !important;
min-width: 550px !important;
}
.date-info-header > div {
display: flex !important;
width: 100% !important;
}
.label-minimal-padding {
padding: 1px 1px !important;
}
/* ==== Compare Button Styling ==== */
.compare-button {
background-color: #ffc107 !important;
color: #212529 !important;
border-color: #ffc107 !important;
min-width: 140px !important;
width: 140px !important;
height: 37px !important;
line-height: 37px !important;
padding: 0 8px !important;
margin: 2px !important;
border-radius: 4px !important;
font-size: 16px !important;
font-weight: bold !important;
text-align: center !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
display: inline-block !important;
text-decoration: none !important;
border: 1px solid #ffc107 !important;
box-sizing: border-box !important;
}
.compare-button:hover {
background-color: #e0a800 !important;
border-color: #d39e00 !important;
color: #212529 !important;
}
/* ==== Baseline Button Styling ==== */
button[value*="baseline"],
input[value*="baseline"],
.test__set-baseline {
background-color: #6c757d !important;
color: white !important;
border-color: #6c757d !important;
min-width: 140px !important;
width: 140px !important;
height: 37px !important;
line-height: 37px !important;
padding: 0 8px !important;
margin: 2px !important;
border-radius: 4px !important;
font-size: 16px !important;
font-weight: bold !important;
text-align: center !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
display: inline-block !important;
text-decoration: none !important;
border: 1px solid #6c757d !important;
box-sizing: border-box !important;
}
button[value*="baseline"]:hover,
input[value*="baseline"]:hover,
.test__set-baseline:hover {
background-color: #5a6268 !important;
border-color: #545b62 !important;
}
/* ==== Unified Button Classes for Test Results ==== */
.test-compare-button {
background-color: #ffc107 !important;
color: #212529 !important;
border-color: #ffc107 !important;
width: 120px !important;
height: 37px !important;
line-height: 37px !important;
padding: 0 8px !important;
margin: 2px !important;
border-radius: 4px !important;
font-size: 16px !important;
font-weight: bold !important;
text-align: center !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
display: inline-block !important;
text-decoration: none !important;
border: 1px solid #ffc107 !important;
box-sizing: border-box !important;
}
.test-compare-button:hover {
background-color: #e0a800 !important;
border-color: #d39e00 !important;
}
.test-fail-button {
background-color: #dc3545 !important;
color: white !important;
border-color: #dc3545 !important;
width: 140px !important;
height: 37px !important;
line-height: 37px !important;
padding: 0 8px !important;
margin: 2px !important;
border-radius: 4px !important;
font-size: 16px !important;
font-weight: bold !important;
text-align: center !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
display: inline-block !important;
text-decoration: none !important;
border: 1px solid #dc3545 !important;
box-sizing: border-box !important;
}
.test-fail-button:hover {
background-color: #c82333 !important;
border-color: #bd2130 !important;
}
.test-baseline-button {
background-color: #6c757d !important;
color: white !important;
border-color: #6c757d !important;
width: 140px !important;
height: 37px !important;
line-height: 37px !important;
padding: 0 8px !important;
margin: 2px !important;
border-radius: 4px !important;
font-size: 16px !important;
font-weight: bold !important;
text-align: center !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
display: inline-block !important;
text-decoration: none !important;
border: 1px solid #6c757d !important;
box-sizing: border-box !important;
}
.test-baseline-button:hover {
background-color: #5a6268 !important;
border-color: #545b62 !important;
}
/* Low diff button styling */
.low-diff-button {
background-color: #C8A2C8 !important;
border: 1px solid #C8A2C8 !important;
color: white !important;
padding: 0 8px !important;
border-radius: 6px !important;
text-decoration: none !important;
font-size: 1.65rem !important;
font-weight: bold !important;
display: inline-block !important;
margin: 2px !important;
transition: all 0.2s ease !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
width: 50px !important;
height: 40px !important;
line-height: 40px !important;
text-align: center !important;
vertical-align: middle !important;
box-sizing: border-box !important;
}
.low-diff-button:hover {
background-color: #C8A2C8 !important;
border-color: #C8A2C8 !important;
color: white !important;
text-decoration: none !important;
}
/* ==== Dynamic Styles for JavaScript Created Elements ==== */
.dynamic-split-container {
display: flex !important;
width: 100% !important;
min-height: 50px !important;
}
.dynamic-date-section {
flex: 0 0 200px !important;
border-right: 1px solid #ddd !important;
padding-right: 8px !important;
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
}
.dynamic-buttons-section {
flex: 1 !important;
padding-left: 8px !important;
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
gap: 2px !important;
}
.dynamic-last-run-cell {
width: 600px !important;
min-width: 550px !important;
padding: 4px !important;
}
.dynamic-header-style {
background-color: #008080 !important;
color: white !important;
padding: 8px !important;
width: 600px !important;
min-width: 550px !important;
}
.dynamic-header-content {
display: flex !important;
width: 100% !important;
}
.dynamic-header-date {
flex: 0 0 200px !important;
text-align: center !important;
border-right: 1px solid #ddd !important;
padding-right: 8px !important;
}
.dynamic-header-results {
flex: 1 !important;
text-align: center !important;
padding-left: 8px !important;
}
.dynamic-notification {
position: fixed !important;
top: 20px !important;
right: 20px !important;
background-color: #28a745 !important;
color: white !important;
padding: 15px 20px !important;
border-radius: 5px !important;
box-shadow: 0 2px 10px rgba(0,0,0,0.2) !important;
z-index: 10000 !important;
font-size: 14px !important;
font-weight: bold !important;
opacity: 0 !important;
transform: translateX(100%) !important;
transition: all 0.3s ease !important;
}
.dynamic-notification.show {
opacity: 1 !important;
transform: translateX(0) !important;
}
.dynamic-baseline-button {
background-color: #28a745 !important;
border-color: #28a745 !important;
color: white !important;
}
.flex-display {
display: flex !important;
}
.border-bottom-black {
border-bottom: 2px solid black !important;
}
.progress-bar {
width: 0% !important;
}
/* ==== Mass baseline editing (runs page) ==== */
.filters.mass-editing-filters {
display: flex !important;
flex-wrap: wrap !important;
align-items: center !important;
}
.filters.mass-editing-filters form {
margin-right: auto !important; /* форма слева */
}
.mass-editing-controls {
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
margin-left: auto !important; /* кнопки справа */
}
/* hide helper: beats existing display:... !important */
.mass-editing-controls [hidden] {
display: none !important;
}
/* кнопки в фильтрах: не переносить текст и не фиксировать узкую ширину */
.mass-editing-controls button.test-compare-button,
.mass-editing-controls button.test-fail-button,
.mass-editing-controls button.test-baseline-button {
white-space: nowrap !important;
width: auto !important;
padding: 0 12px !important;
}
.mass-editing-controls button.test-compare-button {
min-width: 140px !important;
}
.mass-editing-controls button.test-fail-button {
min-width: 160px !important;
}
.mass-editing-controls button.test-baseline-button {
min-width: 120px !important;
}
.lightbox-shortcuts-toggle {
position: relative !important;
display: inline-flex !important;
align-items: center !important;
gap: 4px !important;
margin-right: 4px !important;
padding: 4px 6px !important;
border: 1px solid #cfd8dc !important;
border-radius: 4px !important;
background: #f5f7f8 !important;
cursor: pointer !important;
user-select: none !important;
line-height: 1 !important;
}
.lightbox-shortcuts-toggle__icon {
font-size: 32px !important;
line-height: 1 !important;
opacity: 0.85 !important;
}
.lightbox-shortcuts-toggle__input {
margin: 0 !important;
width: 14px !important;
height: 14px !important;
cursor: pointer !important;
}
.lightbox-shortcuts-toggle__tooltip {
visibility: hidden !important;
opacity: 0 !important;
position: absolute !important;
right: 0 !important;
bottom: calc(100% + 8px) !important;
width: 240px !important;
padding: 8px 10px !important;
border-radius: 6px !important;
background: rgba(20, 28, 34, 0.96) !important;
color: #fff !important;
font-size: 12px !important;
font-weight: 500 !important;
line-height: 1.45 !important;
text-align: left !important;
white-space: normal !important;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25) !important;
pointer-events: none !important;
z-index: 10010 !important;
transition: opacity 0.15s ease !important;
}
.lightbox-shortcuts-toggle:hover .lightbox-shortcuts-toggle__tooltip,
.lightbox-shortcuts-toggle:focus-within .lightbox-shortcuts-toggle__tooltip {
visibility: visible !important;
opacity: 1 !important;
}
.lightbox-shortcuts-toggle__tooltip strong {
display: block !important;
margin-bottom: 4px !important;
font-size: 12px !important;
}
.mass-baseline-label {
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
margin-left: 10px !important;
font-size: 13px !important;
font-weight: 600 !important;
cursor: pointer !important;
user-select: none !important;
}
.mass-baseline-label input[type="checkbox"] {
transform: scale(1.05);
}
.mass-editing-hud {
position: fixed !important;
left: 16px !important;
bottom: 16px !important;
padding: 8px 10px !important;
border-radius: 6px !important;
background: rgba(0, 0, 0, 0.78) !important;
color: #fff !important;
font-size: 14px !important;
font-weight: 700 !important;
z-index: 10005 !important;
pointer-events: none !important;
}
.compare-nav-hud {
position: fixed !important;
left: 16px !important;
top: 16px !important;
padding: 8px 10px !important;
border-radius: 6px !important;
background: rgba(0, 0, 0, 0.78) !important;
color: #fff !important;
font-size: 14px !important;
font-weight: 700 !important;
z-index: 10005 !important;
pointer-events: none !important;
text-align: left !important;
line-height: 1.35 !important;
white-space: nowrap !important;
}
.compare-nav-hud[hidden] {
display: none !important;
}
.auto-diff-indicator {
position: fixed !important;
right: 16px !important;
top: 16px !important;
padding: 4px 8px !important;
border-radius: 4px !important;
background: rgba(255, 34, 85, 0.92) !important;
color: #fff !important;
font-size: 11px !important;
font-weight: 700 !important;
letter-spacing: 0.04em !important;
z-index: 10005 !important;
pointer-events: none !important;
}
.auto-diff-indicator[hidden] {
display: none !important;
}
.image-lightbox-hud {
position: fixed !important;
left: 16px !important;
top: 16px !important;
padding: 8px 10px !important;
border-radius: 6px !important;
background: rgba(0, 0, 0, 0.78) !important;
color: #fff !important;
font-size: 14px !important;
font-weight: 700 !important;
z-index: 10005 !important;
pointer-events: none !important;
text-align: left !important;
line-height: 1.35 !important;
white-space: nowrap !important;
}
.image-lightbox-hud[hidden] {
display: none !important;
}
.modal--baseline-lightbox img.lightbox {
max-width: 95%;
max-height: 95%;
height: 95%;
}
/* ==== WEBSITE: scrollable tall-image compare modal ==== */
.modal--website .modal-content {
height: auto !important;
}
.modal--website .compare-viewport {
display: flex;
width: 100%;
align-items: flex-start;
}
.modal--website .compare-indicator-gutter {
flex: 0 0 14px;
width: 14px;
position: sticky;
top: 0;
height: 100vh;
max-height: 100vh;
box-sizing: border-box;
z-index: 2;
}
.modal--website #image-compare {
flex: 1 1 auto;
min-width: 0;
/* height set by JS after image loads (naturalWidth/Height ratio × container width) */
min-height: 200px !important;
}
/* ==== Diff Highlight Circles (H key) ==== */
.diff-highlight-overlay circle {
animation: diff-circle-appear 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes diff-circle-appear {
0% { r: 0; opacity: 0; stroke-width: 0; }
55% { opacity: 1; stroke-width: 3; }
100% { opacity: 0.88; stroke-width: 2.5; }
}
/* ==== Diff Indicator Bar (vertical minimap, stays after H toggle) ==== */
.modal--compare #image-compare {
position: relative;
}
.compare-indicator-gutter .diff-indicator-bar {
position: absolute;
inset: 0;
z-index: 1;
pointer-events: none;
}
.compare-indicator-gutter .diff-indicator-track {
position: absolute;
top: 1px;
bottom: 1px;
left: 4px;
right: 4px;
width: auto;
background-color: #e8e8e8;
border-radius: 3px;
overflow: hidden;
}
.diff-indicator-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.diff-indicator-markers {
position: absolute;
inset: 0;
}
.diff-indicator-marker {
position: absolute;
left: 0;
right: 0;
background-color: #ff2255;
min-height: 1px;
}
`);
const totalTests = {
//Updated 21.8.26
'------XBS-----': 0,
'monitoring chats list x-bees WEB': 7,
'basic visual regression testing x-bees WEB': 549,
'DMbasic visual regression testing x-bees WEB': 549,
'pseudo localization basic visual regression testing x-bees WEB': 549,
'basic visual regression testing x-bees iOS': 258,
'DM basic visual regression testing x-bees iOS': 258,
'basic visual regression testing simulator x-bees iOS': 243,
'DM basic visual regression testing simulator x-bees iOS': 243,
'big font size basic visual regression testing simulator x-bees iOS': 243,
'pseudo localization basic visual regression testing simulator x-bees iOS': 243,
'basic visual regression testing x-bees Android': 265,
'DM basic visual regression testing x-bees Android': 265,
'big font size basic visual regression testing x-bees Android': 265,
'pseudo localization basic visual regression testing x-bees Android': 265,
'storybook visual regression testing x-bees Android': 73,
'Kite basic visual regression testing (chrome) x-bees KITE': 76,
'Kite basic visual regression testing (firefox) x-bees KITE': 75,
'Kite basic visual regression testing (safari) x-bees KITE': 75,
'chrome Kite mobile basic visual regression testing x-bees mobile KITE': 44,
'safari Kite mobile basic visual regression testing x-bees mobile KITE': 30,
'locales test suite x-bees WEB': 110,
'locales test suite x-bees Android': 77,
'embedded visual regression testing x-bees EMBEDDED': 20,
'------XHOPS------': 0,
'monitoring chats list x-hoppers WEB': 6,
'basic visual regression testing x-hoppers WEB': 526,
'DMbasic visual regression testing x-hoppers WEB': 526,
'calls test suite x-hoppers iOS': 20,
'basic visual regression testing simulator x-hoppers iOS': 240,
'basic visual regression testing x-hoppers Android': 272,
'Kite basic visual regression testing (chrome) x-hoppers KITE': 75,
'chrome Kite mobile basic visual regression testing x-hoppers mobile KITE': 44,
'safari Kite mobile basic visual regression testing x-hoppers mobile KITE': 30,
'------C7------': 0,
'basic visual regression testing collaboration WEB': 415,
'DMbasic visual regression testing collaboration WEB': 415,
'calls test suite collaboration iOS': 20,
'basic visual regression testing simulator collaboration iOS': 229,
'basic visual regression testing collaboration Android': 259,
'Kite basic visual regression testing (chrome) collaboration KITE': 63,
'chrome Kite mobile basic visual regression testing collaboration mobile KITE': 40,
'safari Kite mobile basic visual regression testing collaboration mobile KITE': 30,
'------WEBSITE-----': 0,
'widget: visual regression testing WEBSITE' : 24,
'website: visual regression testing WEBSITE' : 66,
'expand widget: visual regression testing WEBSITE': 11,
'link-preview: visual regression testing WEBSITE': 15
};
const stagePlusProjects = [
'DM basic',
'DMbasic',
'pseudo localization',
'big font size',
'Kite basic visual regression testing (firefox)',
'Kite basic visual regression testing (safari)'
];
// Global variables
let text, score,
lowDiffCount = 0;
let lowDiffCountUniq = 0;
let testLowDiffTitle = '';
let testNamePrev = '';
// Tab functionality
function addProductTabs() {
if (window.location.pathname === '/projects') {
const tabContainer = document.createElement('div');
tabContainer.id = 'product-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' }
];
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);
if (product.name === 'All') {
tab.classList.add('active-tab');
}
tab.addEventListener('click', function() {
const envTabs = document.getElementById('environment-tabs');
if (envTabs) {
envTabs.classList.add('flex-display');
const activeEnvTab = document.querySelector('.environment-tab.active-env-tab');
if (!activeEnvTab) {
const allEnvTab = document.querySelector('.environment-tab[data-env="all"]');
if (allEnvTab) allEnvTab.classList.add('active-env-tab');
}
}
filterProjectsByProduct(this.dataset.product);
document.querySelectorAll('.product-tab').forEach(t => {
t.classList.remove('active-tab');
});
this.classList.add('active-tab');
});
tabContainer.appendChild(tab);
});
const projectsHeader = document.querySelector('h1');
if (projectsHeader) {
projectsHeader.parentNode.insertBefore(tabContainer, projectsHeader.nextSibling);
addEnvironmentTabs(projectsHeader.parentNode, tabContainer);
addCategoryFilters(projectsHeader.parentNode);
const envTabs = document.getElementById('environment-tabs');
if (envTabs) {
envTabs.classList.add('flex-display');
}
}
}
}
function addEnvironmentTabs(parentNode, tabContainer) {
const envTabContainer = document.createElement('div');
envTabContainer.id = 'environment-tabs';
const environments = [
{ name: 'All Environments', value: 'all', class: '' },
{ name: '🏗️ Stage', value: 'stage', class: 'stage-tab' },
{ name: '🧬 Stage Plus', value: 'stage plus', class: 'stage-plus-tab' },
{ name: '🏭 Stable', value: 'stable', class: 'stable-tab' }
];
environments.forEach(env => {
const tab = document.createElement('button');
tab.innerText = env.name;
tab.dataset.env = env.value;
tab.classList.add('environment-tab');
if (env.class) tab.classList.add(env.class);
if (env.name === 'All Environments') {
tab.classList.add('active-env-tab');
}
tab.addEventListener('click', function() {
filterProjectsByEnvironment(this.dataset.env);
document.querySelectorAll('.environment-tab').forEach(t => {
t.classList.remove('active-env-tab');
});
this.classList.add('active-env-tab');
});
envTabContainer.appendChild(tab);
});
document.body.appendChild(envTabContainer);
}
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);
if (category.name === 'All') {
button.classList.add('active-category');
}
button.addEventListener('click', function() {
filterProjectsByCategory(this.dataset.category);
document.querySelectorAll('.category-button').forEach(b => {
b.classList.remove('active-category');
});
this.classList.add('active-category');
});
categoryContainer.appendChild(button);
});
const reportButton = document.createElement('button');
reportButton.innerText = 'Create Report';
reportButton.classList.add('category-button', 'report-button');
reportButton.addEventListener('click', function() {
const visibleProjects = Array.from(document.querySelectorAll('tr.project:not(.hidden-project)'));
const entries = [];
visibleProjects.forEach(tr => {
const platformEl = tr.querySelector('th span');
const platform = platformEl ? platformEl.innerText.trim() : '';
const suiteEl = tr.querySelector('td a span');
const suite = suiteEl ? suiteEl.innerText.trim() : '';
const dateEl = tr.querySelector('td p.date-time');
const date = dateEl ? dateEl.innerText.trim() : '';
const countEl = tr.querySelector('span.count-number');
const countRaw = countEl ? countEl.innerText.trim() : '0';
const totalTestsEl = tr.querySelector('a.total-tests');
let suitKey = totalTestsEl ? (totalTestsEl.getAttribute('data-suite') || '') : '';
suitKey = suitKey.replace('stable: ', '').replace('stage: ', '');
const totalFromMap = totalTests[suitKey] || 0;
const count = parseInt(countRaw, 10) || 0;
const total = totalFromMap;
const percent = total > 0 ? (count / total * 100).toFixed(1) : '0.0';
const linkEl = tr.querySelector('a.label--pass');
let link = '';
if (linkEl) {
const href = linkEl.getAttribute('href') || '';
const cleanPath = href.split('?')[0];
link = window.location.origin + cleanPath;
}
let statusEmoji = '';
if (total > 0) {
if (count < total) statusEmoji = '⚠️ ';
else if (count > total) statusEmoji = '⬆️ ';
else statusEmoji = '✅ ';
}
entries.push({ platform, suite, date, count: countRaw, total, percent, statusEmoji, link });
});
entries.sort((a, b) => b.platform.localeCompare(a.platform));
const blocks = entries.map(e =>
`${e.statusEmoji} ${e.platform} ${e.suite} 📊 ${e.count}/${e.total} (${e.percent}%)\n${e.date} 🔗 ${e.link}`
);
const platformGroups = { WEB: { count: 0, total: 0 }, KITE: { count: 0, total: 0 }, ANDROID: { count: 0, total: 0 }, IOS: { count: 0, total: 0 } };
entries.forEach(e => {
const p = e.platform.toUpperCase();
let group = null;
if (p.includes('WEB')) group = 'WEB';
else if (p.includes('KITE')) group = 'KITE';
else if (p.includes('ANDROID')) group = 'ANDROID';
else if (p.includes('IOS') || p.includes('IOS')) group = 'IOS';
if (group && e.total > 0) {
platformGroups[group].count += parseInt(e.count, 10) || 0;
platformGroups[group].total += e.total;
}
});
const summaryLines = ['VRT Autotests pass rate:'];
['WEB', 'KITE', 'ANDROID', 'IOS'].forEach(g => {
const { count, total } = platformGroups[g];
const rate = total > 0 ? (count / total * 100).toFixed(1) + '%' : 'n/a';
summaryLines.push(`- ${g}: ${rate}`);
});
const text = summaryLines.join('\n') + '\n____________________\n' + blocks.join('\n\n');
navigator.clipboard.writeText(text).catch(() => {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
reportButton.innerText = 'Copied!';
setTimeout(() => { reportButton.innerText = 'Report'; }, 1500);
});
categoryContainer.appendChild(reportButton);
document.body.appendChild(categoryContainer);
}
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';
const categoryFilter = activeCategoryTab ? activeCategoryTab.dataset.category : 'all';
projects.forEach(project => {
const projectNameElement = project.querySelector('th');
if (!projectNameElement) return;
const projectName = projectNameElement.innerText.toLowerCase();
const projectEnv = project.dataset.project?.toLowerCase() || '';
let showProject = true;
if (product !== 'all' && !projectName.includes(product)) {
showProject = false;
}
if (product !== 'all' && envFilter !== 'all') {
if (!matchesEnvironmentFilter(projectEnv, envFilter)) {
showProject = false;
}
}
if (categoryFilter !== 'all') {
if (!projectName.includes(categoryFilter.toLowerCase())) {
showProject = false;
}
}
if (showProject) {
project.classList.remove('hidden-project');
} else {
project.classList.add('hidden-project');
}
});
}
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() || '';
console.log(projectEnv);
if (product !== 'all' && !projectName.includes(product)) {
project.classList.add('hidden-project');
return;
}
if (env !== 'all' && !matchesEnvironmentFilter(projectEnv, env)) {
project.classList.add('hidden-project');
return;
}
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;
}
}
project.classList.remove('hidden-project');
});
}
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';
const projects = document.querySelectorAll('tr.project');
projects.forEach(project => {
const projectNameElement = project.querySelector('th');
if (!projectNameElement) return;
const projectName = projectNameElement.innerText.toLowerCase();
const projectEnv = project.dataset.project?.toLowerCase() || '';
let showProject = true;
console.log(projectName);
if (product !== 'all' && !projectName.includes(product)) {
showProject = false;
}
if (environment !== 'all' && !matchesEnvironmentFilter(projectEnv, environment)) {
showProject = false;
}
if (category !== 'all') {
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 hasStagePlusEntry(projectEnv) {
return stagePlusProjects.some(project => projectEnv.includes(project.toLowerCase()));
}
function matchesEnvironmentFilter(projectEnv, env) {
if (env === 'all') return true;
if (env === 'stage') {
return projectEnv.includes('stage') && !hasStagePlusEntry(projectEnv);
}
if (env === 'stage plus') {
return projectEnv.includes('stage') && hasStagePlusEntry(projectEnv);
}
if (env === 'stable') {
return projectEnv.includes('stable');
}
return projectEnv.includes(env);
}
function combineFilterElements() {
const productTabs = document.getElementById('product-tabs');
const envTabs = document.getElementById('environment-tabs');
const categoryFilters = document.getElementById('category-filters');
if (productTabs && envTabs && categoryFilters) {
const combinedContainer = document.createElement('div');
combinedContainer.id = 'combined-filters';
if (productTabs.parentNode) {
productTabs.parentNode.insertBefore(combinedContainer, productTabs.nextSibling);
}
combinedContainer.appendChild(envTabs);
combinedContainer.appendChild(categoryFilters);
}
}
function addRowDividers() {
const projectRows = document.querySelectorAll('tr.project');
projectRows.forEach(row => {
row.classList.add('border-bottom-black');
});
}
function hideProjectsHeader() {
const projectsHeader = document.querySelector('h1');
if (projectsHeader && projectsHeader.textContent.includes('Projects')) {
projectsHeader.classList.add('hidden-element');
}
}
function adjustColumnWidths() {
const projectsTable = document.getElementById('projects');
if (!projectsTable) return;
const projectRows = projectsTable.querySelectorAll('tr.project');
if (!projectRows.length) return;
const headerRow = projectsTable.querySelector('thead tr');
if (headerRow && !headerRow.querySelector('.date-info-header')) {
const lastRunHeader = headerRow.querySelector('th:nth-child(3)');
if (lastRunHeader) {
lastRunHeader.innerHTML = `
<div class="dynamic-header-content">
<div class="dynamic-header-date">Дата запуска</div>
<div class="dynamic-header-results">Результаты тестов</div>
</div>
`;
lastRunHeader.classList.add('date-info-header', 'dynamic-header-style');
}
}
projectRows.forEach(row => {
const lastRunCell = row.querySelector('td:nth-child(3)');
if (lastRunCell && !lastRunCell.querySelector('.split-cell-container')) {
const dateInfo = lastRunCell.querySelector('span.text-muted');
const dateTimeElement = lastRunCell.querySelector('.date-time');
const buttons = lastRunCell.querySelectorAll('.label--fail, .label--pass, .label--warning, a.total-tests, .low-diff-button');
const splitContainer = document.createElement('div');
splitContainer.classList.add('split-cell-container', 'dynamic-split-container');
const dateSection = document.createElement('div');
dateSection.classList.add('dynamic-date-section');
if (dateInfo) {
dateSection.appendChild(dateInfo.cloneNode(true));
}
if (dateTimeElement) {
dateSection.appendChild(dateTimeElement.cloneNode(true));
}
const buttonsSection = document.createElement('div');
buttonsSection.classList.add('dynamic-buttons-section');
buttons.forEach(button => {
button.classList.add('label-minimal-padding');
buttonsSection.appendChild(button);
});
splitContainer.appendChild(dateSection);
splitContainer.appendChild(buttonsSection);
lastRunCell.innerHTML = '';
lastRunCell.appendChild(splitContainer);
lastRunCell.classList.add('dynamic-last-run-cell');
}
});
const nameHeaders = projectsTable.querySelectorAll('th:first-child, td:first-child');
nameHeaders.forEach(header => {
header.classList.add('column-name-width');
});
const suiteHeaders = projectsTable.querySelectorAll('th:nth-child(2), td:nth-child(2)');
suiteHeaders.forEach(header => {
header.classList.add('column-suite-width');
});
}
function highlightProjectAge() {
const projectRows = document.querySelectorAll('tr.project');
projectRows.forEach(row => {
const dateTimeSpan = row.querySelector('td span.text-muted');
if (!dateTimeSpan) return;
const dateText = dateTimeSpan.innerText;
if ((dateText.includes('days ago') && parseInt(dateText, 10) > 1) || dateText.includes('month ago') || dateText.includes('months ago') || dateText.includes('year ago') || dateText.includes('years ago')) {
row.classList.add('old-project-row');
}
if (dateText.includes('days ago') && parseInt(dateText, 10) > 3) {
row.classList.add('very-old-project-row');
}
});
}
function addLowDiffButtons() {
// Работаем на странице /projects и на страницах с таблицами Latest runs
if (window.location.pathname === '/projects' ||
window.location.pathname.includes('/suites/') ||
document.querySelector('table') ||
document.querySelector('tr.project')) {
// Ищем строки проектов в разных таблицах
const projectRows = document.querySelectorAll('tr.project, tr[data-project], tbody tr');
projectRows.forEach((row, index) => {
// Пропускаем строки заголовков
if (row.querySelector('th') && !row.querySelector('td')) {
return;
}
let buttonsSection = row.querySelector('.split-cell-container > div:last-child');
if (!buttonsSection) {
// Ищем последнюю ячейку с кнопками
const cells = row.querySelectorAll('td');
if (cells.length > 0) {
buttonsSection = cells[cells.length - 1];
}
}
if (!buttonsSection || buttonsSection.querySelector('.low-diff-button')) {
return;
}
let allLinks = buttonsSection.querySelectorAll('a');
let failedLink = null;
for (let link of allLinks) {
const text = link.textContent.toLowerCase().trim();
const computedStyle = window.getComputedStyle(link);
const backgroundColor = computedStyle.backgroundColor;
if (text.includes('failed') ||
backgroundColor.includes('rgb(220, 53, 69)') ||
backgroundColor.includes('rgb(217, 83, 79)') ||
link.classList.contains('label--fail')) {
failedLink = link;
break;
}
}
if (!failedLink && allLinks.length > 0) {
failedLink = allLinks[allLinks.length - 1];
}
if (failedLink && failedLink.href) {
const url = new URL(failedLink.href);
url.searchParams.set('status', 'pass');
url.searchParams.set('diff', 'low-diff');
const lowDiffButton = document.createElement('a');
lowDiffButton.href = url.toString();
lowDiffButton.textContent = 'LD';
lowDiffButton.className = 'low-diff-button label-minimal-padding';
lowDiffButton.title = 'Показать тесты с низкими различиями (Low diff)';
buttonsSection.appendChild(lowDiffButton);
}
});
}
}
function setupBaselineButtons() {
function isMassEditingControl(el) {
if (!el) return false;
try {
return el.hasAttribute('data-mass-editing-control') || !!el.closest('.mass-editing-controls');
} catch (e) {
return false;
}
}
function handleBaselineClick(event) {
// Не трогаем элементы управления массовым режимом
if (isMassEditingControl(this)) return;
this.classList.add('baseline-button-success', 'dynamic-baseline-button');
showBaselineNotification();
event.stopPropagation();
}
function findAndSetupBaselineButtons() {
const selectors = [
'button',
'.btn',
'input[type="button"]',
'input[type="submit"]',
'a[role="button"]',
'a.btn',
'[onclick*="baseline"]',
'[onclick*="Baseline"]'
];
selectors.forEach(selector => {
const buttons = document.querySelectorAll(selector);
buttons.forEach(button => {
if (isMassEditingControl(button)) return;
const text = (button.textContent || button.value || button.title || button.getAttribute('onclick') || '').toLowerCase();
if ((text.includes('baseline') || text.includes('set as baseline')) && !button.hasAttribute('data-baseline-listener')) {
button.addEventListener('click', handleBaselineClick);
button.setAttribute('data-baseline-listener', 'true');
}
});
});
}
findAndSetupBaselineButtons();
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
if (isMassEditingControl(node)) return;
const text = (node.textContent || node.value || node.title || '').toLowerCase();
if ((node.tagName === 'BUTTON' || node.classList.contains('btn') || node.tagName === 'A') &&
(text.includes('baseline') || text.includes('set as baseline')) && !node.hasAttribute('data-baseline-listener')) {
node.addEventListener('click', handleBaselineClick);
node.setAttribute('data-baseline-listener', 'true');
}
if (node.querySelectorAll) {
findAndSetupBaselineButtons();
}
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
setInterval(findAndSetupBaselineButtons, 2000);
}
function showBaselineNotification() {
const existingNotifications = document.querySelectorAll('.baseline-notification');
existingNotifications.forEach(notif => notif.remove());
const notification = document.createElement('div');
notification.classList.add('baseline-notification', 'dynamic-notification');
notification.innerHTML = '✅ Baseline успешно применен!';
document.body.appendChild(notification);
setTimeout(() => notification.classList.add('show'), 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
// Selectors
const differenceMarkElements = document.querySelectorAll('span.test__diff.text-muted');
function addDifferencesMarks() {
let testRow, testName, testEnv;
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') {
elem.innerHTML = positive + text;
testRow.classList.add('pass');
} else {
score = parseFloat(elem.innerHTML);
if (score <= 0.05) {
elem.innerHTML = `<img src="https://github.githubassets.com/images/icons/emoji/unicode/2754.png" class="diff-icon"><span class="diff-score-low">${score}%</span><br />${text}`;
} else if (score > 0.05) {
elem.innerHTML = `<img src="https://github.githubassets.com/images/icons/emoji/unicode/2716.png" class="diff-icon"><span class="diff-score-high">${score}%</span><br />${text}`;
}
if (score < 0.10) {
lowDiffCount += 1;
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 += 1;
}
}
}
testRow.classList.add('diffed');
}
}
}
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);
addDownloadBlock();
bindRunImagesDownload();
}
}
}
function sanitizeDownloadFilenamePart(value) {
return String(value || '')
.replace(/\s\s+/g, ' ')
.replace(/,/g, '')
.replace(/\s+/g, '-')
.replace(/[\\/:*?"<>|]+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
function getArchiveNameFromPage() {
const containerHeader = $('.container').find('h1');
const name = containerHeader.length ? containerHeader.text().trim() : '';
return sanitizeDownloadFilenamePart(name) || 'spectre-export';
}
function generateImagesZip(zip, archiveName) {
return zip.generateAsync({ type: 'blob' }, function updateCallback(metadata) {
let msg = 'progression : ' + metadata.percent.toFixed(2) + ' %';
if (metadata.currentFile) {
msg += ', current file = ' + metadata.currentFile;
}
showMessage(msg);
updatePercent(Math.floor(metadata.percent));
}).then(function callback(blob) {
saveAs(blob, archiveName + '.zip');
showMessage('Done! Archive created successfully.');
}, function (e) {
showError(e);
});
}
function bindRunImagesDownload() {
const archiveName = getArchiveNameFromPage();
const imageExtension = '.png';
$('#download-images').off('click.spectreRunDownload').on('click.spectreRunDownload', function () {
resetMessage();
const zip = new JSZip();
let fileCount = 0;
$('tr.test').each(function () {
const $this = $(this);
const testImgLink = $this.find('td#image-test a.test__image');
if (!testImgLink.length) return;
const url = testImgLink.attr('data-href') || testImgLink.attr('href');
const testNameElement = $this.find('span.test__name');
const testInfoElement = $this.find('small.text-muted');
if (!url || !testNameElement.length || !testInfoElement.length) return;
const testName = sanitizeDownloadFilenamePart(testNameElement.text());
const testInfo = sanitizeDownloadFilenamePart(testInfoElement.text());
const filename = testName + testInfo;
zip.file(filename + imageExtension, urlToPromise(url), { binary: true });
fileCount += 1;
});
if (!fileCount) {
showError('No images found to download.');
return false;
}
generateImagesZip(zip, archiveName);
return false;
});
}
function getBaselineDownloadFilename(row, link, usedNames) {
const href = link.getAttribute('data-href') || link.getAttribute('href') || '';
let filename = '';
try {
const path = new URL(href, window.location.origin).pathname;
filename = path.split('/').pop() || '';
} catch (e) {
filename = '';
}
if (!filename || filename.indexOf('.') === -1) {
const name = sanitizeDownloadFilenamePart(getBaselineRowName(row));
const cells = row ? row.querySelectorAll('td') : [];
const browser = cells.length > 0 ? sanitizeDownloadFilenamePart(cells[0].textContent) : '';
const size = cells.length > 1 ? sanitizeDownloadFilenamePart(cells[1].textContent) : '';
filename = [name, browser, size].filter(Boolean).join('-') + '.png';
}
filename = sanitizeDownloadFilenamePart(filename.replace(/\.png$/i, '')) + '.png';
let unique = filename;
let n = 2;
while (usedNames[unique]) {
unique = filename.replace(/\.png$/i, '') + '-' + n + '.png';
n += 1;
}
usedNames[unique] = true;
return unique;
}
function setupBaselineImageDownload() {
if (!isSuiteBaselinesPage()) return;
if (document.getElementById('download-block')) return;
addDownloadBlock({
footerText: 'All baseline images from the current suite will be archived',
});
const archiveName = getArchiveNameFromPage();
$('#download-images').off('click.spectreBaselineDownload').on('click.spectreBaselineDownload', function () {
resetMessage();
const zip = new JSZip();
const usedNames = {};
let fileCount = 0;
document.querySelectorAll('tr.baseline a.baseline__image').forEach(function(link) {
const url = link.getAttribute('data-href') || link.getAttribute('href');
if (!url) return;
const row = link.closest('tr');
const filename = getBaselineDownloadFilename(row, link, usedNames);
zip.file(filename, urlToPromise(url), { binary: true });
fileCount += 1;
});
if (!fileCount) {
showError('No baseline images found to download.');
return false;
}
generateImagesZip(zip, archiveName);
return false;
});
}
function setupSimpleImageLightboxes() {
const imgClass = getSimpleLightboxImgClass();
const modal = getOrCreateSimpleLightboxModal(imgClass);
['image-baseline', 'image-test', 'image-diff'].forEach(function(columnId) {
document.querySelectorAll('td[id="' + columnId + '"]').forEach(function(cell) {
const link = findSimpleLightboxLink(cell);
if (!prepareSimpleLightboxLink(link)) return;
if (link.hasAttribute('data-simple-lightbox-bound')) return;
link.setAttribute('data-simple-lightbox-bound', 'true');
link.addEventListener('click', function() {
_imageLightboxModalClass = imgClass;
activateSimpleImageLightboxNav(link, columnId);
openSimpleLightboxModal(modal, link.getAttribute('data-href'));
bindSimpleLightboxCloseHandlers(modal);
});
});
});
}
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();
}
function improveDateTime() {
let dateElements = document.querySelectorAll('td.text-muted span');
dateElements.forEach(el => {
if (!el) return;
let dateTime = el.getAttribute('Title');
if (dateTime) {
addElementPTime(el, dateTime);
}
});
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];
if (dateText) {
if (dateText.includes('month ago') || dateText.includes('months ago') || dateText.includes('year ago')) {
el.classList.add('date-old');
if (row) row.classList.add('project-old');
} else if (dateText.includes('days ago')) {
const daysMatch = dateText.match(/(\d+) days ago/);
if (daysMatch && daysMatch[1]) {
const days = parseInt(daysMatch[1], 10);
if (days > 3) {
el.classList.add('date-old');
if (row) row.classList.add('project-very-old');
}
}
}
}
if (dateTime) {
let newP = document.createElement('p');
newP.classList.add('date-time');
newP.innerHTML = '🕒 ' + dateTime;
parentCells[index].appendChild(newP);
}
});
}
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 = '🏗️ <span class="env-stage">' + el.innerHTML + '</span>';
} else if (content.includes('prod')) {
el.innerHTML = '🏢 ' + el.innerHTML;
} else if (content.includes('stable')) {
el.innerHTML = '🏭 <span class="env-stable">' + el.innerHTML + '</span>';
}
});
}
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 = '<span class="product-x-bees">' + el.innerHTML + '</span>';
} else if (text.includes('x-hoppers')) {
el.innerHTML = '<span class="product-x-hoppers">' + el.innerHTML + '</span>';
} else if (text.includes('collaboration')) {
el.innerHTML = '<span class="product-collaboration">' + el.innerHTML + '</span>';
}
});
}
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);
// Проверяем URL-параметры при загрузке страницы
const urlParams = new URLSearchParams(window.location.search);
const diffParam = urlParams.get('diff');
if (diffParam === 'low-diff') {
selectDiff.value = 'low-diff';
// Автоматически применяем фильтр
setTimeout(() => {
let tests = document.querySelectorAll('.pass, .test--fail');
tests.forEach(test => {
if (test) test.remove();
});
}, 1000); // Небольшая задержка для загрузки элементов
}
selectDiff.addEventListener('change', function() {
let tests = document.querySelectorAll('.pass, .test--fail');
if (this.value === 'low-diff') {
tests.forEach(test => {
if (test) test.remove();
});
// Обновляем URL с параметром
const newUrl = new URL(window.location);
newUrl.searchParams.set('diff', 'low-diff');
window.history.pushState({}, '', newUrl);
} else {
// Удаляем параметр из URL и перезагружаем
const newUrl = new URL(window.location);
newUrl.searchParams.delete('diff');
window.location.href = newUrl.toString();
}
});
filtersForm.appendChild(selectDiff);
}
}
function initMassBaselineEditing() {
const isRunsInSuite =
window.location.pathname.includes('/suites/') &&
window.location.pathname.includes('/runs/');
if (!isRunsInSuite) return;
const STATE = {
injected: false,
mode: 'idle', // idle | scanned
baselineInputs: [],
checkboxRecords: [], // { checkbox, label, onChange }
hudEl: null,
filtersEl: null,
controlsEl: null,
btnMass: null,
btnSet: null,
btnCancel: null,
onMassClick: null,
onSetClick: null,
onCancelClick: null,
};
function ensureInjected() {
if (STATE.injected) return true;
const filters = document.querySelector('div.filters');
const form = document.querySelector('div.filters form');
if (!filters || !form) return false;
STATE.filtersEl = filters;
filters.classList.add('mass-editing-filters');
const controls = document.createElement('div');
controls.className = 'mass-editing-controls';
const btnMass = document.createElement('button');
btnMass.type = 'button';
btnMass.className = 'test-compare-button';
btnMass.textContent = 'Mass editing';
btnMass.setAttribute('data-mass-editing-control', 'true');
const btnSet = document.createElement('button');
btnSet.type = 'button';
btnSet.className = 'test-fail-button';
btnSet.textContent = 'Set baselines';
btnSet.hidden = true;
btnSet.setAttribute('data-mass-editing-control', 'true');
const btnCancel = document.createElement('button');
btnCancel.type = 'button';
btnCancel.className = 'test-baseline-button';
btnCancel.textContent = 'Cancel';
btnCancel.hidden = true;
btnCancel.setAttribute('data-mass-editing-control', 'true');
const shortcutsToggle = document.createElement('label');
shortcutsToggle.className = 'lightbox-shortcuts-toggle';
shortcutsToggle.setAttribute('data-mass-editing-control', 'true');
shortcutsToggle.title = '';
const shortcutsIcon = document.createElement('span');
shortcutsIcon.className = 'lightbox-shortcuts-toggle__icon';
shortcutsIcon.setAttribute('aria-hidden', 'true');
shortcutsIcon.textContent = '⌨';
const shortcutsCheckbox = document.createElement('input');
shortcutsCheckbox.type = 'checkbox';
shortcutsCheckbox.checked = false;
shortcutsCheckbox.className = 'lightbox-shortcuts-toggle__input';
shortcutsCheckbox.setAttribute('data-lightbox-shortcuts-toggle', 'true');
const shortcutsTooltip = document.createElement('span');
shortcutsTooltip.className = 'lightbox-shortcuts-toggle__tooltip';
shortcutsTooltip.innerHTML =
'<strong>Shortcuts (only in compare mode)</strong>' +
'↓ / → - Next<br>' +
'↑ / ← - Previous<br>' +
'A - Accept as baseline, next<br>' +
'S - Skip, next<br>' +
'Y - Auto diff highlights<br>';
shortcutsToggle.appendChild(shortcutsCheckbox);
shortcutsToggle.appendChild(shortcutsIcon);
shortcutsToggle.appendChild(shortcutsTooltip);
shortcutsCheckbox.addEventListener('change', function() {
_lightboxShortcutsEnabled = shortcutsCheckbox.checked;
if (!_lightboxShortcutsEnabled && _autoDiffHighlightsEnabled) {
setAutoDiffHighlightsEnabled(false);
}
});
controls.appendChild(shortcutsToggle);
controls.appendChild(btnMass);
controls.appendChild(btnSet);
controls.appendChild(btnCancel);
form.insertAdjacentElement('afterend', controls);
STATE.controlsEl = controls;
STATE.btnMass = btnMass;
STATE.btnSet = btnSet;
STATE.btnCancel = btnCancel;
STATE.onMassClick = () => startMassEditing();
STATE.onSetClick = () => handleSetBaselines();
STATE.onCancelClick = () => cleanupToIdle();
btnMass.addEventListener('click', STATE.onMassClick);
btnSet.addEventListener('click', STATE.onSetClick);
btnCancel.addEventListener('click', STATE.onCancelClick);
STATE.injected = true;
return true;
}
function startMassEditing() {
if (!ensureInjected()) return;
if (STATE.mode !== 'idle') return;
// Standard browser confirm: OK = "Go fast", Cancel = "Cancel"
const ok = confirm("We can do this fast or slow, it's your call.\nMass editing can permanently change multiple screenshots.");
if (!ok) return;
STATE.btnMass.hidden = true;
STATE.btnSet.hidden = false;
STATE.btnCancel.hidden = false;
// По требованию: сканируем сразу при нажатии Mass editing
scanAndRender();
}
function createHudIfNeeded() {
if (STATE.hudEl) return;
const hud = document.createElement('div');
hud.className = 'mass-editing-hud';
hud.textContent = 'SELECTED DIFFS: 0/0';
document.body.appendChild(hud);
STATE.hudEl = hud;
}
function updateHud() {
if (!STATE.hudEl) return;
const found = STATE.baselineInputs.length;
const selected = STATE.checkboxRecords.reduce((acc, r) => acc + (r.checkbox.checked ? 1 : 0), 0);
STATE.hudEl.textContent = `SELECTED DIFFS: ${selected}/${found}`;
}
function scanAndRender() {
// Очищаем возможные хвосты предыдущего скана, если что-то пошло не так
removeCheckboxesAndHud();
const inputs = Array.from(document.querySelectorAll('input.test__set-baseline'));
STATE.baselineInputs = inputs;
if (!inputs.length) {
// Standard browser confirm: OK = "I know what I am doing", Cancel = "Cancel"
confirm("Have you tried turning it off and on again?\nNo changes were detected.");
cleanupToIdle();
return false;
}
createHudIfNeeded();
inputs.forEach((baselineInput, idx) => {
if (!baselineInput || !baselineInput.parentElement) return;
if (baselineInput.hasAttribute('data-mass-baseline-processed')) return;
baselineInput.setAttribute('data-mass-baseline-processed', 'true');
const label = document.createElement('label');
label.className = 'mass-baseline-label';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = true;
checkbox.setAttribute('data-mass-baseline-index', String(idx));
const textNode = document.createTextNode('Set as a baseline');
label.appendChild(checkbox);
label.appendChild(textNode);
baselineInput.insertAdjacentElement('afterend', label);
const onChange = () => updateHud();
checkbox.addEventListener('change', onChange);
STATE.checkboxRecords.push({ checkbox, label, onChange });
});
updateHud();
STATE.mode = 'scanned';
return true;
}
async function applySelectedBaselines() {
const selectedIndices = STATE.checkboxRecords
.filter(r => r.checkbox.checked)
.map(r => parseInt(r.checkbox.getAttribute('data-mass-baseline-index') || '', 10))
.filter(n => Number.isFinite(n));
const selectedInputs = selectedIndices
.map(i => STATE.baselineInputs[i])
.filter(Boolean);
const selectedCount = selectedInputs.length;
// Standard browser confirm: OK/Cancel
const ok = confirm("WARNING: That's one way to get your attention…\nProceed only if you’re confident.");
if (!ok) return;
// Standard browser input (prompt): require safety phrase
const phrase = prompt('Type "THEBEAST" for confirmation:', '');
if (phrase !== 'THEBEAST') {
alert('Aborted.');
return;
}
// Блокируем кнопки на время выполнения
STATE.btnSet.disabled = true;
STATE.btnCancel.disabled = true;
for (let i = 0; i < selectedInputs.length; i++) {
try {
selectedInputs[i].click();
} catch (e) {
// игнорируем единичные ошибки клика
}
await new Promise(r => setTimeout(r, 250));
}
cleanupToIdle();
// Standard browser confirm: OK = refresh, Cancel = stay
const refresh = confirm(`UPDATED ${selectedCount} BASELINES\n\nREFRESH THE PAGE?`);
if (refresh) {
window.location.reload();
}
}
function handleSetBaselines() {
if (!ensureInjected()) return;
if (STATE.mode !== 'scanned') {
const ok = scanAndRender();
if (!ok) return;
}
applySelectedBaselines();
}
function removeCheckboxesAndHud() {
STATE.checkboxRecords.forEach(r => {
try {
r.checkbox.removeEventListener('change', r.onChange);
} catch (e) {}
try {
if (r.label) r.label.remove();
} catch (e) {}
});
STATE.checkboxRecords = [];
// Снимаем метки обработанности, чтобы повторный вход работал корректно
try {
document.querySelectorAll('input.test__set-baseline[data-mass-baseline-processed="true"]').forEach(el => {
el.removeAttribute('data-mass-baseline-processed');
});
} catch (e) {}
if (STATE.hudEl) {
try { STATE.hudEl.remove(); } catch (e) {}
STATE.hudEl = null;
}
STATE.baselineInputs = [];
}
function cleanupToIdle() {
if (!ensureInjected()) return;
removeCheckboxesAndHud();
STATE.mode = 'idle';
STATE.btnSet.disabled = false;
STATE.btnCancel.disabled = false;
// На всякий случай убираем "успешные" классы, если кто-то их навесил
STATE.btnMass.classList.remove('baseline-button-success', 'dynamic-baseline-button');
STATE.btnSet.classList.remove('baseline-button-success', 'dynamic-baseline-button');
STATE.btnCancel.classList.remove('baseline-button-success', 'dynamic-baseline-button');
STATE.btnMass.hidden = false;
STATE.btnSet.hidden = true;
STATE.btnCancel.hidden = true;
}
// Пытаемся сразу, и на случай поздней отрисовки фильтров — наблюдатель
if (!ensureInjected()) {
const obs = new MutationObserver(() => {
if (ensureInjected()) obs.disconnect();
});
obs.observe(document.body, { childList: true, subtree: true });
}
}
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;
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 = `<span class="count-warning count-number">${count}</span><sup class="count-sup">${totalTests[suitName]}</sup>`;
} else {
newCount = `<span class="count-number">${count}</span><sup class="count-sup"><span class="count-excess count-number">${totalTests[suitName]}</span></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);
}
}
});
}
}
function sortingProjects() {
let projectsTable = document.querySelector('#projects');
if (!projectsTable) return;
let projects = projectsTable.querySelectorAll('.project');
if (!projects || projects.length === 0) return;
let sortedProjectsArr = [];
let projectName, projectNameEnv, projectNameNormalized;
projects.forEach(project => {
if (!project) return;
projectName = project.getAttribute('data-project');
if (!projectName) return;
projectNameEnv = projectName.split(':')[0];
if (projectName.includes('monitoring chats list')) {
projectNameNormalized = projectNameEnv + '9';
} else if (projectName.includes('widget:') || projectName.includes('website:')) {
projectNameNormalized = '1website';
} else {
projectNameNormalized = projectNameEnv;
}
sortedProjectsArr.push({ name: projectNameNormalized, val: project });
});
if (sortedProjectsArr.length > 0) {
sortedProjectsArr.sort((a, b) => b.name.localeCompare(a.name));
projectsTable.innerHTML = '';
sortedProjectsArr.forEach(project => {
projectsTable.appendChild(project.val);
});
}
}
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 = [];
testsBlockArr.forEach(testsBlock => {
if (!testsBlock) return;
const test = testsBlock.querySelector('th a');
if (!test) return;
const testName = test.innerHTML.replace(/\s+/g, '-').toLowerCase();
sortedTestsBlockArr.push({ name: testName, val: testsBlock });
});
if (sortedTestsBlockArr.length > 0) {
sortedTestsBlockArr.sort((a, b) => a.name.localeCompare(b.name));
parentTests.innerHTML = '';
sortedTestsBlockArr.forEach(testsBlock => {
parentTests.appendChild(testsBlock.val);
});
}
}
}
// 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.classList.add('new-baseline-link');
});
newButton.addEventListener('click', function() {
oldBaselines.forEach(item => {
if (item) item.remove();
});
const filters = document.querySelector('div.filters');
if (filters) {
filters.scrollIntoView({ behavior: 'instant', block: 'start', inline: 'start' });
}
});
}
}
}
// Helper functions
function addElementButton(parentId, test) {
if (!parentId || !test) return;
let newButton = document.createElement('button');
newButton.id = parentId;
newButton.classList.add('test-compare-button', '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', 'modal--compare');
if (window.location.pathname.indexOf('projects/website') !== -1) {
modalWindow.classList.add('modal--website');
}
modalWindow.innerHTML = '<div class="modal-content"><span class="close">×</span><div class="compare-viewport"><div class="compare-indicator-gutter" aria-hidden="true"></div><div id="image-compare"><img src="' + reference + '" alt="reference" /><img src="' + current + '" alt="current" /></div></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;
let newP = document.createElement('p');
newP.classList.add('date-time');
newP.innerHTML = '🕒 ' + text;
insertAfter(newP, parent);
}
const _icvOptions = {
showLabels: true,
labelOptions: {
before: 'Baseline',
after: 'Comparison',
onHover: false
},
smoothing: false,
startingPoint: 50,
fluidMode: true,
controlColor: '#FF0000',
controlShadow: false,
addCircle: false,
addCircleBlur: false,
};
function mountIcvOnElement(element) {
new ImageCompare(element, _icvOptions).mount();
const observer = new MutationObserver(function() {
updateHorizontalLines();
});
observer.observe(element, { attributes: true, childList: true, subtree: true });
element.addEventListener('mousemove', updateHorizontalLines);
element.addEventListener('click', updateHorizontalLines);
}
function setViewers() {
const viewers = document.querySelectorAll('#image-compare');
viewers.forEach(element => {
// Website modals are mounted on-open after images load (see mountWebsiteViewer)
if (element.closest('.modal--website')) return;
mountIcvOnElement(element);
});
}
function mountWebsiteViewer(modal) {
const container = modal.querySelector('#image-compare');
if (!container || container.dataset.icvMounted) return;
container.dataset.icvMounted = '1';
const firstImg = container.querySelector('img');
if (!firstImg) return;
function doMount() {
if (!firstImg.naturalWidth) return;
const w = container.clientWidth;
const h = Math.round(w * firstImg.naturalHeight / firstImg.naturalWidth);
container.style.height = h + 'px';
mountIcvOnElement(container);
}
if (firstImg.complete && firstImg.naturalWidth) {
doMount();
} else {
firstImg.addEventListener('load', doMount, { once: true });
}
}
function updateHorizontalLines() {
const control = document.querySelector('.icv__control');
const container = document.querySelector('#image-compare');
const beforeLabel = document.querySelector('.icv__label-before');
const afterLabel = document.querySelector('.icv__label-after');
if (!control || !container || !beforeLabel || !afterLabel) return;
const controlRect = control.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const beforeLabelRect = beforeLabel.getBoundingClientRect();
const afterLabelRect = afterLabel.getBoundingClientRect();
const controlCenterX = controlRect.left + (controlRect.width / 2);
const beforeLineWidth = controlCenterX - (beforeLabelRect.right);
const afterLineWidth = (afterLabelRect.left) - controlCenterX;
document.documentElement.style.setProperty('--before-line-width', `${Math.max(0, beforeLineWidth)}px`);
document.documentElement.style.setProperty('--after-line-width', `${Math.max(0, afterLineWidth)}px`);
}
let _lightboxNavEnabled = false;
let _lightboxNavIndex = -1;
let _lightboxShortcutsEnabled = false;
let _compareNavHudEl = null;
let _imageLightboxNavEnabled = false;
let _imageLightboxNavIndex = -1;
let _imageLightboxColumnId = null;
let _imageLightboxModalClass = null;
let _imageLightboxHudEl = null;
function getSimpleLightboxImgClass() {
return window.location.pathname.indexOf('projects/website/suites') !== -1
? 'imgModal2'
: 'imgModal1';
}
function getOrCreateSimpleLightboxModal(imgClass) {
let modal = document.querySelector('div.modal.' + imgClass + ':not(.modal--compare)');
if (modal) return modal;
modal = document.createElement('div');
modal.classList.add('modal', imgClass);
modal.innerHTML = '<div class="modal-content"><span class="close">×</span><img class="lightbox" src=""/></div>';
document.body.appendChild(modal);
return modal;
}
function getOpenSimpleLightboxModal() {
return document.querySelector('.modal.imgModal1.modal-show:not(.modal--baseline-lightbox), .modal.imgModal2.modal-show:not(.modal--baseline-lightbox)');
}
function findSimpleLightboxLink(cell) {
if (!cell) return null;
return cell.querySelector('a.baseline__image, a.test__image, a.diff__image, a[data-href], a[href]');
}
function prepareSimpleLightboxLink(link) {
if (!link) return false;
const href = link.getAttribute('data-href') || link.getAttribute('href');
if (!href) return false;
if (!link.getAttribute('data-href')) {
link.setAttribute('data-href', href);
}
if (link.hasAttribute('href')) {
link.removeAttribute('href');
}
return true;
}
function collectSimpleLightboxItems(columnId) {
const items = [];
document.querySelectorAll('td[id="' + columnId + '"]').forEach(function(cell) {
const link = findSimpleLightboxLink(cell);
if (!prepareSimpleLightboxLink(link)) return;
items.push({
link: link,
row: cell.closest('tr'),
columnId: columnId,
href: link.getAttribute('data-href'),
});
});
return items;
}
function getCurrentSimpleLightboxItems() {
if (!_imageLightboxColumnId) return [];
return collectSimpleLightboxItems(_imageLightboxColumnId);
}
function getTestRowName(row) {
if (!row) return 'Unknown test';
const nameEl = row.querySelector('.test__name');
return nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : 'Unknown test';
}
function ensureImageLightboxHud() {
if (_imageLightboxHudEl) return _imageLightboxHudEl;
const hud = document.createElement('div');
hud.className = 'image-lightbox-hud';
hud.hidden = true;
document.body.appendChild(hud);
_imageLightboxHudEl = hud;
return hud;
}
function hideImageLightboxHud() {
if (!_imageLightboxHudEl) return;
_imageLightboxHudEl.hidden = true;
}
function updateImageLightboxHud() {
const openModal = getOpenSimpleLightboxModal();
if (!openModal || !_imageLightboxNavEnabled || _imageLightboxNavIndex < 0) {
hideImageLightboxHud();
return;
}
const items = getCurrentSimpleLightboxItems();
const item = items[_imageLightboxNavIndex];
if (!item) {
hideImageLightboxHud();
return;
}
hideCompareNavHud();
const hud = ensureImageLightboxHud();
hud.textContent = (_imageLightboxNavIndex + 1) + '/' + items.length + ' ' + getTestRowName(item.row);
hud.hidden = false;
}
function resetImageLightboxNav() {
_imageLightboxNavEnabled = false;
_imageLightboxNavIndex = -1;
_imageLightboxColumnId = null;
hideImageLightboxHud();
}
function openSimpleLightboxModal(modal, href) {
if (!modal || !href) return;
modal.classList.add('modal-show');
modal.classList.remove('modal-hide');
const modalImg = modal.querySelector('img.lightbox');
if (modalImg) {
modalImg.src = href;
}
}
function bindSimpleLightboxCloseHandlers(modal) {
const close = modal.querySelector('span.close');
if (close) {
close.onclick = function() {
closeSimpleLightboxModal(modal);
};
}
window.onclick = function(event) {
if (event.target === modal) {
closeSimpleLightboxModal(modal);
}
};
}
function activateSimpleImageLightboxNav(link, columnId) {
const items = collectSimpleLightboxItems(columnId);
const idx = items.findIndex(function(item) { return item.link === link; });
if (idx === -1) return;
_imageLightboxNavEnabled = true;
_imageLightboxNavIndex = idx;
_imageLightboxColumnId = columnId;
updateImageLightboxHud();
}
function goToSimpleLightboxIndex(index) {
const items = getCurrentSimpleLightboxItems();
if (!items.length) {
resetImageLightboxNav();
return;
}
if (index < 0 || index >= items.length) return;
_imageLightboxNavIndex = index;
const modal = getOrCreateSimpleLightboxModal(_imageLightboxModalClass || getSimpleLightboxImgClass());
openSimpleLightboxModal(modal, items[index].href);
if (items[index].row) {
items[index].row.scrollIntoView({ behavior: 'instant', block: 'center' });
}
updateImageLightboxHud();
}
function goSimpleLightboxNext() {
goToSimpleLightboxIndex(_imageLightboxNavIndex + 1);
}
function goSimpleLightboxPrev() {
goToSimpleLightboxIndex(_imageLightboxNavIndex - 1);
}
function closeSimpleLightboxModal(modal) {
if (!modal) return;
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
resetImageLightboxNav();
}
function closeOpenLightbox(reason) {
const compareModal = document.querySelector('.modal.modal--compare.modal-show');
if (compareModal) {
closeCompareModal(compareModal, reason || 'Escape');
resetLightboxNav(reason || 'Escape');
return true;
}
const simpleModal = getOpenSimpleLightboxModal();
if (simpleModal) {
closeSimpleLightboxModal(simpleModal);
return true;
}
const baselineModal = getOpenBaselineLightboxModal();
if (baselineModal) {
closeBaselineLightboxModal(baselineModal);
return true;
}
return false;
}
function setupLightboxEscapeClose() {
document.addEventListener('keydown', function(e) {
if (e.key !== 'Escape') return;
const compareOpen = document.querySelector('.modal.modal--compare.modal-show');
const simpleOpen = getOpenSimpleLightboxModal();
const baselineOpen = getOpenBaselineLightboxModal();
if (!compareOpen && !simpleOpen && !baselineOpen) return;
e.preventDefault();
closeOpenLightbox('Escape');
});
}
function setupSimpleImageLightboxNav() {
document.addEventListener('keydown', function(e) {
const isNextKey = e.key === 'ArrowDown' || e.key === 'ArrowRight';
const isPrevKey = e.key === 'ArrowUp' || e.key === 'ArrowLeft';
if (!isNextKey && !isPrevKey) return;
if (!_imageLightboxNavEnabled) return;
if (!getOpenSimpleLightboxModal()) {
resetImageLightboxNav();
return;
}
if (document.querySelector('.modal.modal--compare.modal-show')) return;
if (_lightboxShortcutsEnabled && _lightboxNavEnabled) return;
if (e.target.matches('input, textarea, select, [contenteditable="true"]')) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
e.preventDefault();
if (isNextKey) {
goSimpleLightboxNext();
} else {
goSimpleLightboxPrev();
}
});
}
// ── Baseline suite lightbox (suite list page only; isolated from run lightboxes) ──
let _baselineLightboxNavEnabled = false;
let _baselineLightboxNavIndex = -1;
let _baselineLightboxHudEl = null;
let _baselineLightboxCloseBound = false;
function isSuiteBaselinesPage() {
const path = window.location.pathname;
return path.indexOf('/projects/') !== -1
&& path.indexOf('/suites/') !== -1
&& path.indexOf('/runs/') === -1;
}
function getOrCreateBaselineLightboxModal() {
let modal = document.querySelector('div.modal.modal--baseline-lightbox');
if (modal) return modal;
modal = document.createElement('div');
modal.classList.add('modal', 'modal--baseline-lightbox');
modal.innerHTML = '<div class="modal-content"><span class="close">×</span><img class="lightbox" src=""/></div>';
document.body.appendChild(modal);
return modal;
}
function getOpenBaselineLightboxModal() {
return document.querySelector('.modal.modal--baseline-lightbox.modal-show');
}
function collectBaselineLightboxItems() {
const items = [];
document.querySelectorAll('tr.baseline a.baseline__image').forEach(function(link) {
const href = link.getAttribute('data-href') || link.getAttribute('href');
if (!href) return;
if (!link.getAttribute('data-href')) {
link.setAttribute('data-href', href);
}
if (link.hasAttribute('href')) {
link.removeAttribute('href');
}
items.push({
link: link,
row: link.closest('tr'),
href: link.getAttribute('data-href'),
});
});
return items;
}
function getBaselineRowName(row) {
if (!row) return 'Unknown baseline';
const nameEl = row.querySelector('.baseline__name');
return nameEl ? nameEl.textContent.replace(/\s+/g, ' ').trim() : 'Unknown baseline';
}
function ensureBaselineLightboxHud() {
if (_baselineLightboxHudEl) return _baselineLightboxHudEl;
const hud = document.createElement('div');
hud.className = 'image-lightbox-hud';
hud.hidden = true;
document.body.appendChild(hud);
_baselineLightboxHudEl = hud;
return hud;
}
function hideBaselineLightboxHud() {
if (!_baselineLightboxHudEl) return;
_baselineLightboxHudEl.hidden = true;
}
function updateBaselineLightboxHud() {
if (!getOpenBaselineLightboxModal() || !_baselineLightboxNavEnabled || _baselineLightboxNavIndex < 0) {
hideBaselineLightboxHud();
return;
}
const items = collectBaselineLightboxItems();
const item = items[_baselineLightboxNavIndex];
if (!item) {
hideBaselineLightboxHud();
return;
}
hideCompareNavHud();
hideImageLightboxHud();
const hud = ensureBaselineLightboxHud();
hud.textContent = (_baselineLightboxNavIndex + 1) + '/' + items.length + ' ' + getBaselineRowName(item.row);
hud.hidden = false;
}
function resetBaselineLightboxNav() {
_baselineLightboxNavEnabled = false;
_baselineLightboxNavIndex = -1;
hideBaselineLightboxHud();
}
function openBaselineLightboxModal(modal, href) {
if (!modal || !href) return;
modal.classList.add('modal-show');
modal.classList.remove('modal-hide');
const modalImg = modal.querySelector('img.lightbox');
if (modalImg) {
modalImg.src = href;
}
}
function closeBaselineLightboxModal(modal) {
if (!modal) return;
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
resetBaselineLightboxNav();
}
function bindBaselineLightboxCloseHandlers(modal) {
if (!modal || _baselineLightboxCloseBound) return;
_baselineLightboxCloseBound = true;
const close = modal.querySelector('span.close');
if (close) {
close.addEventListener('click', function() {
closeBaselineLightboxModal(modal);
});
}
modal.addEventListener('click', function(event) {
if (event.target === modal) {
closeBaselineLightboxModal(modal);
}
});
}
function activateBaselineLightboxNav(link) {
const items = collectBaselineLightboxItems();
const idx = items.findIndex(function(item) { return item.link === link; });
if (idx === -1) return;
_baselineLightboxNavEnabled = true;
_baselineLightboxNavIndex = idx;
updateBaselineLightboxHud();
}
function goToBaselineLightboxIndex(index) {
const items = collectBaselineLightboxItems();
if (!items.length) {
resetBaselineLightboxNav();
return;
}
if (index < 0 || index >= items.length) return;
_baselineLightboxNavIndex = index;
const modal = getOrCreateBaselineLightboxModal();
openBaselineLightboxModal(modal, items[index].href);
if (items[index].row) {
items[index].row.scrollIntoView({ behavior: 'instant', block: 'center' });
}
updateBaselineLightboxHud();
}
function goBaselineLightboxNext() {
goToBaselineLightboxIndex(_baselineLightboxNavIndex + 1);
}
function goBaselineLightboxPrev() {
goToBaselineLightboxIndex(_baselineLightboxNavIndex - 1);
}
function setupBaselineImageLightbox() {
if (!isSuiteBaselinesPage()) return;
const modal = getOrCreateBaselineLightboxModal();
bindBaselineLightboxCloseHandlers(modal);
document.querySelectorAll('tr.baseline a.baseline__image').forEach(function(link) {
if (link.hasAttribute('data-baseline-lightbox-bound')) return;
const href = link.getAttribute('data-href') || link.getAttribute('href');
if (!href) return;
if (!link.getAttribute('data-href')) {
link.setAttribute('data-href', href);
}
if (link.hasAttribute('href')) {
link.removeAttribute('href');
}
link.setAttribute('data-baseline-lightbox-bound', 'true');
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
activateBaselineLightboxNav(link);
openBaselineLightboxModal(modal, link.getAttribute('data-href'));
});
});
}
function setupBaselineImageLightboxNav() {
document.addEventListener('keydown', function(e) {
const isNextKey = e.key === 'ArrowDown' || e.key === 'ArrowRight';
const isPrevKey = e.key === 'ArrowUp' || e.key === 'ArrowLeft';
if (!isNextKey && !isPrevKey) return;
if (!_baselineLightboxNavEnabled) return;
if (!getOpenBaselineLightboxModal()) {
resetBaselineLightboxNav();
return;
}
if (document.querySelector('.modal.modal--compare.modal-show')) return;
if (getOpenSimpleLightboxModal()) return;
if (e.target.matches('input, textarea, select, [contenteditable="true"]')) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
e.preventDefault();
if (isNextKey) {
goBaselineLightboxNext();
} else {
goBaselineLightboxPrev();
}
});
}
function collectNavigableCompareRows() {
const diffedRows = Array.from(document.querySelectorAll('tr.diffed'));
return diffedRows.filter(function(row) {
return row.id && getCompareModalByTestId(row.id);
});
}
function ensureCompareNavHud() {
if (_compareNavHudEl) return _compareNavHudEl;
const hud = document.createElement('div');
hud.className = 'compare-nav-hud';
hud.hidden = true;
document.body.appendChild(hud);
_compareNavHudEl = hud;
return hud;
}
function hideCompareNavHud() {
if (!_compareNavHudEl) return;
_compareNavHudEl.hidden = true;
}
function updateCompareNavHud() {
const openModal = document.querySelector('.modal.modal--compare.modal-show');
if (!openModal || !_lightboxNavEnabled || _lightboxNavIndex < 0) {
hideCompareNavHud();
return;
}
hideImageLightboxHud();
const rows = collectNavigableCompareRows();
if (!rows[_lightboxNavIndex]) {
hideCompareNavHud();
return;
}
const row = rows[_lightboxNavIndex];
const hud = ensureCompareNavHud();
hud.textContent = (_lightboxNavIndex + 1) + '/' + rows.length + ' ' + getTestRowName(row);
hud.hidden = false;
}
function lbLog(step, data) {
if (data !== undefined) {
console.log('[Spectre5000][LightboxNav] ' + step, data);
} else {
console.log('[Spectre5000][LightboxNav] ' + step);
}
}
function getCompareModalByTestId(testId) {
if (!testId) return null;
return document.querySelector('div.modal.modal--compare#' + CSS.escape(String(testId)));
}
function describeCompareRow(row) {
if (!row) return null;
const modal = row.id ? getCompareModalByTestId(row.id) : null;
return {
rowId: row.id || null,
hasModal: !!modal,
modalIsCompare: !!modal,
hasBaselineBtn: !!row.querySelector('input.test__set-baseline'),
testName: (row.querySelector('.test__name') || {}).textContent || null,
};
}
function getNavigableCompareRows() {
const diffedRows = Array.from(document.querySelectorAll('tr.diffed'));
const rows = collectNavigableCompareRows();
lbLog('getNavigableCompareRows', {
diffedCount: diffedRows.length,
navigableCount: rows.length,
diffedWithoutModal: diffedRows.filter(function(row) {
return !row.id || !getCompareModalByTestId(row.id);
}).length,
diffedWithNonCompareModal: 0,
rows: rows.map(describeCompareRow),
});
return rows;
}
function resetLightboxNav(reason) {
lbLog('resetLightboxNav', {
reason: reason || 'unknown',
wasEnabled: _lightboxNavEnabled,
wasIndex: _lightboxNavIndex,
});
_lightboxNavEnabled = false;
_lightboxNavIndex = -1;
hideCompareNavHud();
}
function closeCompareModal(modal, reason) {
if (!modal) {
lbLog('closeCompareModal skipped — no modal', { reason: reason || 'unknown' });
return;
}
lbLog('closeCompareModal', {
reason: reason || 'unknown',
modalId: modal.id || null,
hadModalShow: modal.classList.contains('modal-show'),
});
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
}
function bindCompareModalCloseHandlers(modal) {
const close = modal.querySelector('span.close');
if (close) {
close.onclick = function() {
closeCompareModal(modal, 'close button click');
resetLightboxNav('close button click');
};
} else {
lbLog('bindCompareModalCloseHandlers — close button not found', {
modalId: modal.id || null,
});
}
window.onclick = function(event) {
if (event.target === modal) {
closeCompareModal(modal, 'backdrop click');
resetLightboxNav('backdrop click');
}
};
}
function openCompareModal(modal, reason) {
if (!modal) {
lbLog('openCompareModal skipped — no modal', { reason: reason || 'unknown' });
return;
}
lbLog('openCompareModal', {
reason: reason || 'unknown',
modalId: modal.id || null,
isWebsite: modal.classList.contains('modal--website'),
});
document.querySelectorAll('.modal.modal--compare.modal-show').forEach(function(m) {
closeCompareModal(m, 'switching to another compare modal');
});
modal.classList.add('modal-show');
modal.classList.remove('modal-hide');
if (modal.classList.contains('modal--website')) {
mountWebsiteViewer(modal);
}
bindCompareModalCloseHandlers(modal);
maybeAutoApplyDiffHighlights(modal);
}
function activateLightboxNav(testId, source) {
const rows = getNavigableCompareRows();
const idx = rows.findIndex(function(row) { return row.id === testId; });
lbLog('activateLightboxNav', {
source: source || 'unknown',
testId: testId,
idx: idx,
enabled: idx !== -1,
row: idx !== -1 ? describeCompareRow(rows[idx]) : null,
});
if (idx === -1) return;
_lightboxNavEnabled = true;
_lightboxNavIndex = idx;
updateCompareNavHud();
}
function setListener() {
const btn = document.querySelectorAll('button.diffBtn');
lbLog('setListener init', {
diffBtnCount: btn.length,
buttons: Array.from(btn).map(function(element) {
return {
id: element.id || null,
text: (element.textContent || '').trim(),
className: element.className,
};
}),
});
btn.forEach(element => {
element.addEventListener('click', function(event) {
const triggerId = event.currentTarget.id || event.target.id || null;
const modal = getCompareModalByTestId(triggerId);
lbLog('Compare button click', {
triggerId: triggerId,
targetTag: event.target && event.target.tagName,
targetId: event.target && event.target.id,
currentTargetId: event.currentTarget && event.currentTarget.id,
modalFound: !!modal,
modalIsCompare: !!modal,
modalClasses: modal ? modal.className : null,
wrongElementByGetElementById: triggerId
? (document.getElementById(triggerId) || {}).tagName || null
: null,
});
if (!modal) return;
openCompareModal(modal, 'Compare button click');
activateLightboxNav(triggerId, 'Compare button click');
});
});
}
function setupLightboxKeyboardNav() {
lbLog('setupLightboxKeyboardNav init', {
pathname: window.location.pathname,
compareModalCount: document.querySelectorAll('.modal.modal--compare').length,
openCompareModalCount: document.querySelectorAll('.modal.modal--compare.modal-show').length,
});
function goToIndex(index, source) {
const rows = getNavigableCompareRows();
lbLog('goToIndex', {
source: source || 'unknown',
requestedIndex: index,
currentIndex: _lightboxNavIndex,
rowCount: rows.length,
});
if (!rows.length) {
closeCompareModal(document.querySelector('.modal.modal--compare.modal-show'), 'goToIndex — no rows');
resetLightboxNav('goToIndex — no rows');
return;
}
if (index >= rows.length) {
closeCompareModal(document.querySelector('.modal.modal--compare.modal-show'), 'goToIndex — past last');
resetLightboxNav('goToIndex — past last');
return;
}
if (index < 0) {
index = 0;
}
_lightboxNavIndex = index;
const targetRow = rows[index];
const targetModal = getCompareModalByTestId(targetRow.id);
lbLog('goToIndex opening', {
source: source || 'unknown',
index: index,
row: describeCompareRow(targetRow),
modalFound: !!targetModal,
});
openCompareModal(targetModal, source || 'goToIndex');
targetRow.scrollIntoView({ behavior: 'instant', block: 'center' });
updateCompareNavHud();
}
function goNext(source) {
goToIndex(_lightboxNavIndex + 1, source || 'goNext');
}
function goPrev(source) {
lbLog('goPrev', {
source: source || 'unknown',
currentIndex: _lightboxNavIndex,
});
if (_lightboxNavIndex > 0) {
goToIndex(_lightboxNavIndex - 1, source || 'goPrev');
} else {
lbLog('goPrev skipped — already at first item');
}
}
function acceptBaselineAndNext(source) {
const rows = getNavigableCompareRows();
const row = rows[_lightboxNavIndex];
const baselineBtn = row ? row.querySelector('input.test__set-baseline') : null;
lbLog('acceptBaselineAndNext', {
source: source || 'unknown',
currentIndex: _lightboxNavIndex,
row: describeCompareRow(row),
baselineBtnFound: !!baselineBtn,
baselineBtnDisabled: baselineBtn ? baselineBtn.disabled : null,
});
if (baselineBtn) {
baselineBtn.click();
}
goNext(source || 'acceptBaselineAndNext');
}
document.addEventListener('keydown', function(e) {
if (e.key === 'y' || e.key === 'Y') {
if (!_lightboxShortcutsEnabled) return;
if (e.target.matches('input, textarea, select, [contenteditable="true"]')) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
e.preventDefault();
toggleAutoDiffHighlights();
return;
}
const openModal = document.querySelector('.modal.modal--compare.modal-show');
const isNavKey = ['ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'a', 'A', 's', 'S'].indexOf(e.key) !== -1;
if (!isNavKey) return;
if (!_lightboxShortcutsEnabled) {
lbLog('keydown ignored — shortcuts toggle off', {
key: e.key,
openCompareModal: !!openModal,
});
return;
}
if (!_lightboxNavEnabled) {
lbLog('keydown ignored — nav not enabled', {
key: e.key,
openCompareModal: !!openModal,
openModalId: openModal ? openModal.id : null,
});
return;
}
if (!openModal) {
lbLog('keydown ignored — no open compare modal, resetting nav', { key: e.key });
resetLightboxNav('keydown — no open compare modal');
return;
}
if (e.target.matches('input, textarea, select, [contenteditable="true"]')) {
lbLog('keydown ignored — focus in editable field', {
key: e.key,
targetTag: e.target.tagName,
targetType: e.target.type || null,
});
return;
}
if (e.metaKey || e.ctrlKey || e.altKey) {
lbLog('keydown ignored — modifier pressed', {
key: e.key,
metaKey: e.metaKey,
ctrlKey: e.ctrlKey,
altKey: e.altKey,
});
return;
}
lbLog('keydown accepted', {
key: e.key,
currentIndex: _lightboxNavIndex,
openModalId: openModal.id,
});
switch (e.key) {
case 'ArrowDown':
case 'ArrowRight':
e.preventDefault();
goNext(e.key);
break;
case 'ArrowUp':
case 'ArrowLeft':
e.preventDefault();
goPrev(e.key);
break;
case 'a':
case 'A':
e.preventDefault();
acceptBaselineAndNext('A');
break;
case 's':
case 'S':
e.preventDefault();
goNext('S');
break;
}
});
}
function insertAfter(newNode, existingNode) {
if (!newNode || !existingNode || !existingNode.parentNode) return;
existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}
// ── Diff Highlight: press H while Compare lightbox is open ──────────────
const _diffCache = new Map(); // testId -> { circleRects, indicatorRects, imgW, imgH }
const _diffLoading = new Set(); // testIds currently being fetched
let _autoDiffHighlightsEnabled = false;
let _autoDiffIndicatorEl = null;
// Pixel-analysis worker (runs off main thread so the slider stays responsive)
const _diffWorkerUrl = URL.createObjectURL(new Blob([`
function computeRects(data, width, height, blockSize) {
var cols = Math.ceil(width / blockSize);
var rows = Math.ceil(height / blockSize);
var grid = new Uint8Array(cols * rows);
for (var y = 0; y < height; y++) {
for (var x = 0; x < width; x++) {
var i = (y * width + x) * 4;
var r = data[i], g = data[i+1], b = data[i+2], a = data[i+3];
if (a < 128) continue;
var maxC = r > g ? (r > b ? r : b) : (g > b ? g : b);
var minC = r < g ? (r < b ? r : b) : (g < b ? g : b);
if (maxC > 40 && (maxC - minC) / maxC > 0.25) {
grid[Math.floor(y / blockSize) * cols + Math.floor(x / blockSize)] = 1;
}
}
}
var visited = new Uint8Array(cols * rows);
var rects = [];
for (var i = 0; i < grid.length; i++) {
if (!grid[i] || visited[i]) continue;
var queue = [i];
visited[i] = 1;
var minBx = Infinity, maxBx = -Infinity, minBy = Infinity, maxBy = -Infinity;
while (queue.length) {
var idx = queue.shift();
var bx = idx % cols;
var by = Math.floor(idx / cols);
if (bx < minBx) minBx = bx;
if (bx > maxBx) maxBx = bx;
if (by < minBy) minBy = by;
if (by > maxBy) maxBy = by;
for (var dy = -1; dy <= 1; dy++) {
for (var dx = -1; dx <= 1; dx++) {
if (dx === 0 && dy === 0) continue;
var nx = bx + dx, ny = by + dy;
if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
var ni = ny * cols + nx;
if (!grid[ni] || visited[ni]) continue;
visited[ni] = 1;
queue.push(ni);
}
}
}
rects.push({
x: minBx * blockSize, y: minBy * blockSize,
width: (maxBx - minBx + 1) * blockSize,
height: (maxBy - minBy + 1) * blockSize,
});
}
return rects;
}
self.onmessage = function(e) {
var buffer = e.data.buffer, width = e.data.width, height = e.data.height;
var data = new Uint8ClampedArray(buffer);
self.postMessage({
circleRects: computeRects(data, width, height, 24),
indicatorRects: computeRects(data, width, height, 5),
});
};
`], { type: 'application/javascript' }));
function isWebsiteCompareModal(modal) {
return modal.classList.contains('modal--website');
}
function getImageCompareLayout(container, imgW, imgH, modal) {
const cRect = container.getBoundingClientRect();
if (modal && isWebsiteCompareModal(modal)) {
const scale = cRect.width / imgW;
return {
scale: scale,
offsetX: 0,
offsetY: 0,
containerWidth: cRect.width,
containerHeight: cRect.height,
};
}
const scale = Math.min(cRect.width / imgW, cRect.height / imgH);
return {
scale: scale,
offsetX: (cRect.width - imgW * scale) / 2,
offsetY: (cRect.height - imgH * scale) / 2,
containerWidth: cRect.width,
containerHeight: cRect.height,
};
}
function getWebsiteIndicatorScrollOffset(modal, container, fullHeight, trackHeight) {
const modalRect = modal.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
const offset = Math.max(0, modalRect.top - containerRect.top);
const maxOffset = Math.max(0, fullHeight - trackHeight);
return Math.min(offset, maxOffset);
}
function ensureCompareContainerPosition(container) {
if (container.style.position !== 'relative') {
container.style.position = 'relative';
}
}
function removeDiffCircles(modal) {
const existing = modal.querySelector('.diff-highlight-overlay');
if (existing) {
existing.remove();
}
}
function getCompareIndicatorGutter(modal) {
return modal.querySelector('.compare-indicator-gutter');
}
function removeDiffIndicator(modal) {
const gutter = getCompareIndicatorGutter(modal);
if (!gutter) return;
const bar = gutter.querySelector('.diff-indicator-bar');
if (!bar) return;
if (bar._resizeObserver) {
bar._resizeObserver.disconnect();
}
if (bar._scrollRoot && bar._scrollListener) {
bar._scrollRoot.removeEventListener('scroll', bar._scrollListener);
window.removeEventListener('scroll', bar._scrollListener);
}
bar.remove();
}
function renderDiffIndicator(modal, rects, imgW, imgH) {
const container = modal.querySelector('#image-compare');
const gutter = getCompareIndicatorGutter(modal);
if (!container || !gutter) return;
let bar = gutter.querySelector('.diff-indicator-bar');
if (!bar) {
bar = document.createElement('div');
bar.classList.add('diff-indicator-bar');
const track = document.createElement('div');
track.classList.add('diff-indicator-track');
const inner = document.createElement('div');
inner.classList.add('diff-indicator-inner');
const markers = document.createElement('div');
markers.classList.add('diff-indicator-markers');
inner.appendChild(markers);
track.appendChild(inner);
bar.appendChild(track);
gutter.appendChild(bar);
bar._markersEl = markers;
bar._innerEl = inner;
bar._trackEl = track;
const updateLayout = function() {
const layout = getImageCompareLayout(container, imgW, imgH, modal);
const fullHeight = imgH * layout.scale;
bar._innerEl.style.height = fullHeight + 'px';
if (isWebsiteCompareModal(modal)) {
const trackHeight = bar._trackEl.clientHeight || gutter.clientHeight;
const scrollOffset = getWebsiteIndicatorScrollOffset(
modal, container, fullHeight, trackHeight
);
bar._innerEl.style.transform = 'translateY(' + (-scrollOffset) + 'px)';
} else {
bar._innerEl.style.transform = 'translateY(' + layout.offsetY + 'px)';
}
bar._markersEl.innerHTML = '';
rects.forEach(function(rect) {
const marker = document.createElement('div');
marker.classList.add('diff-indicator-marker');
marker.style.top = (rect.y * layout.scale) + 'px';
marker.style.height = Math.max(rect.height * layout.scale, 1) + 'px';
bar._markersEl.appendChild(marker);
});
};
updateLayout();
bar._resizeObserver = new ResizeObserver(updateLayout);
bar._resizeObserver.observe(container);
if (isWebsiteCompareModal(modal)) {
bar._scrollRoot = modal;
bar._scrollListener = updateLayout;
modal.addEventListener('scroll', updateLayout, { passive: true });
window.addEventListener('scroll', updateLayout, { passive: true });
}
}
}
function showDiffHighlights(modal, cached) {
renderDiffIndicator(modal, cached.indicatorRects, cached.imgW, cached.imgH);
renderDiffCircles(modal, cached.circleRects, cached.imgW, cached.imgH);
}
function getDiffUrlForCompareModal(modal) {
const testId = modal && modal.id;
if (!testId) return null;
const testRow = document.querySelector('tr[id="' + testId + '"]');
if (!testRow) return null;
const diffTd = testRow.querySelector('[id="image-diff"]');
if (!diffTd) return null;
const diffLink = diffTd.querySelector('a');
if (!diffLink) return null;
return diffLink.getAttribute('data-href') || diffLink.href || null;
}
function requestDiffHighlightsForModal(modal) {
if (!modal || !modal.classList.contains('modal--compare')) return;
const testId = modal.id;
if (!testId) return;
if (_diffCache.has(testId)) {
showDiffHighlights(modal, _diffCache.get(testId));
return;
}
if (_diffLoading.has(testId)) return;
const diffUrl = getDiffUrlForCompareModal(modal);
if (!diffUrl) return;
_diffLoading.add(testId);
loadAndAnalyzeDiffImage(diffUrl, function(circleRects, indicatorRects, imgW, imgH) {
_diffLoading.delete(testId);
const cached = {
circleRects: circleRects,
indicatorRects: indicatorRects,
imgW: imgW,
imgH: imgH,
};
_diffCache.set(testId, cached);
watchModalClose(modal, testId);
if (modal.classList.contains('modal-show')) {
showDiffHighlights(modal, cached);
}
});
}
function clearDiffHighlightsFromModal(modal) {
if (!modal) return;
removeDiffCircles(modal);
removeDiffIndicator(modal);
}
function ensureAutoDiffIndicator() {
if (_autoDiffIndicatorEl) return _autoDiffIndicatorEl;
const indicator = document.createElement('div');
indicator.className = 'auto-diff-indicator';
indicator.textContent = 'Y · DIFF';
indicator.hidden = true;
document.body.appendChild(indicator);
_autoDiffIndicatorEl = indicator;
return indicator;
}
function updateAutoDiffIndicator() {
const indicator = ensureAutoDiffIndicator();
indicator.hidden = !_autoDiffHighlightsEnabled;
}
function setAutoDiffHighlightsEnabled(enabled) {
_autoDiffHighlightsEnabled = enabled;
updateAutoDiffIndicator();
const modal = document.querySelector('.modal.modal--compare.modal-show');
if (enabled && modal) {
maybeAutoApplyDiffHighlights(modal);
} else if (!enabled && modal) {
clearDiffHighlightsFromModal(modal);
}
}
function toggleAutoDiffHighlights() {
setAutoDiffHighlightsEnabled(!_autoDiffHighlightsEnabled);
}
function maybeAutoApplyDiffHighlights(modal) {
if (!_autoDiffHighlightsEnabled || !modal) return;
if (modal.classList.contains('modal--website')) {
setTimeout(function() {
if (modal.classList.contains('modal-show')) {
requestDiffHighlightsForModal(modal);
}
}, 200);
return;
}
requestDiffHighlightsForModal(modal);
}
function setupDiffHighlighter() {
document.addEventListener('keydown', function(e) {
if (e.key !== 'h' && e.key !== 'H') return;
const modal = document.querySelector('.modal.modal--compare.modal-show');
if (!modal) return;
if (modal.querySelector('.diff-highlight-overlay')) {
removeDiffCircles(modal);
return;
}
requestDiffHighlightsForModal(modal);
});
}
function watchModalClose(modal, testId) {
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'class' && !modal.classList.contains('modal-show')) {
_diffCache.delete(testId);
removeDiffCircles(modal);
removeDiffIndicator(modal);
observer.disconnect();
console.log('[Spectre5000] Cache cleared for ' + testId);
}
});
});
observer.observe(modal, { attributes: true });
}
function loadAndAnalyzeDiffImage(url, callback) {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function() {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (ex) {
console.warn('[Spectre5000] Cannot read diff pixels (CORS):', ex);
return;
}
const worker = new Worker(_diffWorkerUrl);
const buffer = imageData.data.buffer;
worker.postMessage(
{ buffer: buffer, width: canvas.width, height: canvas.height },
[buffer]
);
worker.onmessage = function(e) {
worker.terminate();
callback(
e.data.circleRects,
e.data.indicatorRects,
canvas.width,
canvas.height
);
};
worker.onerror = function(err) {
worker.terminate();
console.warn('[Spectre5000] Worker error:', err);
};
};
img.onerror = function() {
console.warn('[Spectre5000] Could not load diff image:', url);
};
img.src = url;
}
function renderDiffCircles(modal, rects, imgW, imgH) {
const container = modal.querySelector('#image-compare');
if (!container) return;
removeDiffCircles(modal);
ensureCompareContainerPosition(container);
const layout = getImageCompareLayout(container, imgW, imgH, modal);
const scale = layout.scale;
const offsetX = layout.offsetX;
const offsetY = layout.offsetY;
const cRect = container.getBoundingClientRect();
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
svg.classList.add('diff-highlight-overlay');
svg.setAttribute('width', cRect.width);
svg.setAttribute('height', cRect.height);
svg.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:9999;overflow:visible;';
const minRadius = 30;
const finalCircleScale = 0.90;
rects.forEach(function(rect) {
const cx = offsetX + (rect.x + rect.width / 2) * scale;
const cy = offsetY + (rect.y + rect.height / 2) * scale;
const r = Math.max(Math.max(rect.width, rect.height) / 2 * scale, minRadius) * finalCircleScale;
const circle = document.createElementNS(svgNS, 'circle');
circle.setAttribute('cx', cx);
circle.setAttribute('cy', cy);
circle.setAttribute('r', r);
circle.setAttribute('fill', 'none');
circle.setAttribute('stroke', '#ff2255');
circle.setAttribute('stroke-width', '2.5');
svg.appendChild(circle);
});
container.appendChild(svg);
}
// ────────────────────────────────────────────────────────────────────────
function addDownloadBlock(options) {
options = options || {};
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"></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 = options.footerText || 'All images from the current run will be archived';
downloadBlock.appendChild(footer);
let versionInfo = document.createElement('p');
versionInfo.classList.add('version');
versionInfo.innerHTML = 'Spectre 5000 v.17.050';
downloadBlock.appendChild(versionInfo);
document.body.appendChild(downloadBlock);
}
function resetMessage() {
$('#result')
.removeClass()
.text('');
}
function showMessage(text) {
resetMessage();
$('#result')
.addClass('alert alert-success')
.text(text);
}
function showError(text) {
resetMessage();
$('#result')
.addClass('alert alert-danger')
.text(text);
}
function updatePercent(percent) {
$('#progress_bar').removeClass('hide')
.find('.progress-bar')
.attr('aria-valuenow', percent)
.css({
width: percent + '%'
});
}
const Promise = window.Promise || JSZip.external.Promise;
function urlToPromise(url) {
return new Promise(function(resolve, reject) {
JSZipUtils.getBinaryContent(url, function(err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
// Initialize all functions
addDifferencesMarks();
footerAndHeader();
addVisualComparisonButtons();
improveDateTime();
lowDiffFilter();
totalTestCount();
sortingProjects();
sortingTests();
envStyle();
productStyle();
setupSimpleImageLightboxes();
setupBaselineImageLightbox();
setupBaselineImageDownload();
addProductTabs();
highlightProjectAge();
addRowDividers();
adjustColumnWidths();
hideProjectsHeader();
combineFilterElements();
setupBaselineButtons();
addLowDiffButtons();
initMassBaselineEditing();
setupDiffHighlighter();
setupLightboxKeyboardNav();
setupSimpleImageLightboxNav();
setupBaselineImageLightboxNav();
setupLightboxEscapeClose();
setTimeout(() => {
addLowDiffButtons();
}, 2000);
})()