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.001
// @description The feeling of not wanting to lose, everyone has it. If you find it unfair, you'll have to work harder yourself.
// @author 3rdUnknown
// @copyright 2025
// @license MIT
// @match *://62.210.192.50:3000/projects*
// @match *://spectre.autotests-xbees.wildix.com/projects*
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip-utils/0.1.0/jszip-utils.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.0/FileSaver.min.js
// @require https://unpkg.com/image-compare-viewer/dist/image-compare-viewer.min.js
// @resource IMPORTED_CSS https://unpkg.com/image-compare-viewer/dist/image-compare-viewer.min.css
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @grant GM_openInTab
// @grant GM_getResourceText
// @grant GM_addStyle
// @run-at document-idle
// @updateURL https://openuserjs.org/meta/3rdUnknown/Spectre_5000.meta.js
// ==/UserScript==
(function() {
'use strict';
const my_css = GM_getResourceText('IMPORTED_CSS');
GM_addStyle(my_css);
GM_addStyle(`
/* ==== Modal Styling ==== */
.modal {
display: none;
position: fixed;
z-index: 1;
inset: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: #fefefe;
margin: 3% auto;
padding: 20px;
border: 1px solid #888;
width: 85%;
height: 85%;
text-align: center;
}
.imgModal1 img {
height: 95%;
}
.imgModal2 img {
max-width: 95%;
}
#image-compare {
width: 100%;
height: 95%;
}
/* ==== Modal Close Button ==== */
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
}
/* ==== Image Compare Wrapper ==== */
.icv__wrapper,
div.icv__fluidwrapper {
background-size: contain;
background-repeat: no-repeat;
}
/* 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.stable-tab {
color: #ff9800 !important;
}
/* ==== Category Filter ==== */
#category-filters {
margin: 10px 0;
display: flex;
flex-wrap: wrap;
align-items: center;
padding-left: 15px;
}
.category-label {
font-weight: bold;
margin-right: 10px;
font-size: 14px;
}
.category-button {
padding: 6px 12px !important;
margin: 0 8px 5px 0 !important;
border: 1px solid #ddd;
background-color: #f1f1f1;
border-radius: 4px;
font-size: 13px !important;
color: #333 !important;
cursor: pointer;
transition: all 0.2s ease;
}
.category-button:hover {
background-color: #e0e0e0;
}
.category-button.active-category {
background-color: #4caf50;
color: white !important;
font-weight: bold;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.category-kite {
border-left: 4px solid #2196F3;
}
.category-web {
border-left: 4px solid #FF5722;
}
.category-android {
border-left: 4px solid #4CAF50;
}
.category-ios {
border-left: 4px solid #9C27B0;
}
.category-mobile-kite {
border-left: 4px solid #FFC107;
}
/* ==== Project Rows ==== */
.old-project-row {
background-color: #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;
}
.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;
}
`);
let totalTests = {
//Updated 1.1.26
'------XBS-----': 0,
'monitoring chats list x-bees WEB': 7,
'basic visual regression testing x-bees WEB': 453,
'DMbasic visual regression testing x-bees WEB': 453,
'pseudo localization basic visual regression testing x-bees WEB': 453,
'basic visual regression testing x-bees iOS': 18,
'DM basic visual regression testing x-bees iOS': 18,
'basic visual regression testing simulator x-bees iOS': 185,
'DM basic visual regression testing simulator x-bees iOS': 185,
'big font size basic visual regression testing simulator x-bees iOS': 185,
'pseudo localization basic visual regression testing x-bees iOS': 185,
'basic visual regression testing x-bees Android': 245,
'DM basic visual regression testing x-bees Android': 245,
'big font size basic visual regression testing x-bees Android': 245,
'pseudo localization basic visual regression testing x-bees Android': 245,
'storybook visual regression testing x-bees Android': 77,
'Kite basic visual regression testing (chrome) x-bees KITE': 73,
'Kite basic visual regression testing (firefox) x-bees KITE': 72,
'Kite basic visual regression testing (safari) x-bees KITE': 72,
'chrome Kite mobile basic visual regression testing x-bees mobile KITE': 41,
'safari Kite mobile basic visual regression testing x-bees mobile KITE': 27,
'locales test suite x-bees WEB': 120,
'locales test suite x-bees Android': 70,
'------XHOPS------': 0,
'monitoring chats list x-hoppers WEB': 7,
'basic visual regression testing x-hoppers WEB': 453,
'DMbasic visual regression testing x-hoppers WEB': 453,
'basic visual regression testing x-hoppers iOS': 18,
'basic visual regression testing simulator x-hoppers iOS': 186,
'basic visual regression testing x-hoppers Android': 245,
'Kite basic visual regression testing (chrome) x-hoppers KITE': 73,
'chrome Kite mobile basic visual regression testing x-hoppers mobile KITE': 41,
'safari Kite mobile basic visual regression testing x-hoppers mobile KITE': 27,
'------C7------': 0,
'basic visual regression testing collaboration WEB': 374,
'DMbasic visual regression testing collaboration WEB': 374,
'basic visual regression testing collaboration iOS': 18,
'basic visual regression testing simulator collaboration iOS': 184,
'basic visual regression testing collaboration Android': 240,
'Kite basic visual regression testing (chrome) collaboration KITE': 60,
'chrome Kite mobile basic visual regression testing collaboration mobile KITE': 37,
'safari Kite mobile basic visual regression testing collaboration mobile KITE': 27,
'------WEBSITE-----': 0,
'widget: visual regression testing WEBSITE' : 24,
'website: visual regression testing WEBSITE' : 64,
'expand widget: visual regression testing WEBSITE': 11,
'link-preview: visual regression testing WEBSITE': 15
};
// 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 environments"]');
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', class: '' },
{ name: '🏗️ Stage', class: 'stage-tab' },
{ name: '🏭 Stable', class: 'stable-tab' }
];
environments.forEach(env => {
const tab = document.createElement('button');
tab.innerText = env.name;
tab.dataset.env = env.name.toLowerCase();
tab.classList.add('environment-tab');
if (env.class) tab.classList.add(env.class);
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);
});
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 environments';
const categoryFilter = activeCategoryTab ? activeCategoryTab.dataset.category : 'all';
projects.forEach(project => {
const projectNameElement = project.querySelector('th');
if (!projectNameElement) return;
const projectName = projectNameElement.innerText.toLowerCase();
let showProject = true;
if (product !== 'all' && !projectName.includes(product)) {
showProject = false;
}
if (product !== 'all' && envFilter !== 'all environments') {
if (!projectName.includes(envFilter.toLowerCase())) {
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() || '';
if (product !== 'all' && !projectName.includes(product)) {
project.classList.add('hidden-project');
return;
}
if (env !== 'all environments') {
const isStage = env.includes('stage');
const isStable = env.includes('stable');
if ((isStage && !projectEnv.includes('stage')) || (isStable && !projectEnv.includes('stable'))) {
project.classList.add('hidden-project');
return;
}
}
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 environments';
const projects = document.querySelectorAll('tr.project');
projects.forEach(project => {
const projectNameElement = project.querySelector('th');
if (!projectNameElement) return;
const projectName = projectNameElement.innerText.toLowerCase();
let showProject = true;
if (product !== 'all' && !projectName.includes(product)) {
showProject = false;
}
if (environment !== 'all environments' && !projectName.includes(environment.toLowerCase())) {
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 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');
const testimgs = document.querySelectorAll('a.test__image');
const baselineimgs = document.querySelectorAll('a.baseline__image');
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();
const containerHeader = $('.container').find('h1');
const archiveName = containerHeader.length ? containerHeader.text() : 'spectre-export';
const imageExtension = '.png';
$('#download-images').on('click', function () {
resetMessage();
const zip = new JSZip();
$('tr.test').each(function () {
const $this = $(this);
const testImgLink = $this.find('td#image-test a.test__image');
if (testImgLink.length) {
const url = testImgLink.attr('data-href');
const testNameElement = $this.find('span.test__name');
const testInfoElement = $this.find('small.text-muted');
if (url && testNameElement.length && testInfoElement.length) {
const testName = testNameElement.text()
.replace(/\s\s+/g, ' ')
.replace(/\s+/g, '-');
const testInfo = testInfoElement.text()
.replace(/\s\s+/g, ' ')
.replace(/,/g, '')
.replace(/\s+/g, '-');
const filename = testName + testInfo;
zip.file(filename + imageExtension, urlToPromise(url), { binary: true });
}
}
});
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);
});
return false;
});
}
}
}
function addModalsToImgs(imgs) {
if (!imgs || imgs.length === 0) return;
imgs.forEach(img => {
if (img && img.href) {
img.setAttribute('data-href', img.href);
img.removeAttribute('href');
}
});
let modalWindow = document.createElement('div');
let imgClass = 'imgModal1';
if (window.location.pathname.indexOf('projects/website/suites') !== -1) {
imgClass = 'imgModal2';
}
modalWindow.classList.add('modal', imgClass);
modalWindow.innerHTML = '<div class="modal-content"><span class="close">×</span><img class="lightbox" src=""/></div>';
document.body.appendChild(modalWindow);
imgs.forEach(element => {
if (element) {
element.addEventListener('click', function() {
const modal = document.querySelector(`div.${imgClass}`);
if (modal) {
modal.classList.add('modal-show');
modal.classList.remove('modal-hide');
const modalimg = modal.querySelector('img');
if (modalimg) {
modalimg.src = element.getAttribute('data-href');
}
const close = modal.querySelector('span.close');
if (close) {
close.onclick = function() {
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
};
}
window.onclick = function(event) {
if (event.target == modal) {
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
}
};
}
});
}
});
}
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');
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');
modalWindow.innerHTML = '<div class="modal-content"><span class="close">×</span><div id="image-compare"><img src="' + reference + '" alt="reference" /><img src="' + current + '" alt="current" /></div></div>';
document.body.appendChild(modalWindow);
}
function addElementPTime(parent, text) {
if (!parent || !text) return;
const time = text.split(' ');
if (time.length < 4) return;
const timeParts = time[3].split(':');
if (timeParts.length < 2) return;
let newP = document.createElement('p');
newP.classList.add('date-time');
newP.innerHTML = '🕒 ' + text;
insertAfter(newP, parent);
}
function setViewers() {
const options = {
showLabels: true,
labelOptions: {
before: 'Baseline',
after: 'Comparison',
onHover: false
},
smoothing: false,
startingPoint: 50,
fluidMode: true,
controlColor: '#FF0000',
controlShadow: false,
addCircle: false,
addCircleBlur: false,
};
const viewers = document.querySelectorAll('#image-compare');
viewers.forEach(element => {
const viewer = new ImageCompare(element, options).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 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`);
}
function setListener() {
const btn = document.querySelectorAll('button.diffBtn');
btn.forEach(element => {
element.addEventListener('click', function(event) {
let modal = document.querySelector(`div#${event.target.id}`);
if (!modal) return;
modal.classList.add('modal-show');
modal.classList.remove('modal-hide');
let close = modal.querySelector('span.close');
if (close) {
close.onclick = function() {
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
};
}
window.onclick = function(event) {
if (event.target == modal) {
modal.classList.add('modal-hide');
modal.classList.remove('modal-show');
}
};
});
});
}
function insertAfter(newNode, existingNode) {
if (!newNode || !existingNode || !existingNode.parentNode) return;
existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
}
function addDownloadBlock() {
let downloadBlock = document.createElement('div');
downloadBlock.id = 'download-block';
downloadBlock.classList.add('container');
let downloadButton = document.createElement('button');
downloadButton.id = 'download-images';
downloadButton.classList.add('download-button');
downloadButton.innerHTML = 'Download images';
downloadBlock.appendChild(downloadButton);
let progressBlock = document.createElement('div');
progressBlock.id = 'progress_bar';
progressBlock.classList.add('progress', 'hide');
progressBlock.innerHTML = '<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>';
downloadBlock.appendChild(progressBlock);
let progressStatus = document.createElement('p');
progressStatus.id = 'result';
progressStatus.classList.add('hide');
progressStatus.innerHTML = '';
downloadBlock.appendChild(progressStatus);
let footer = document.createElement('p');
footer.innerHTML = 'All images from the current run will be archived';
downloadBlock.appendChild(footer);
let versionInfo = document.createElement('p');
versionInfo.classList.add('version');
versionInfo.innerHTML = 'Spectre 5000 v.17.001';
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();
addModalsToImgs(testimgs);
addModalsToImgs(baselineimgs);
addProductTabs();
highlightProjectAge();
addRowDividers();
adjustColumnWidths();
hideProjectsHeader();
combineFilterElements();
setupBaselineButtons();
addLowDiffButtons();
initMassBaselineEditing();
setTimeout(() => {
addLowDiffButtons();
}, 2000);
})();