@applitools/eyes-playwright 1.40.6 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/dist/fixture/report-plugin/core/log.js +44 -0
  3. package/dist/fixture/report-plugin/core/types.js +5 -0
  4. package/dist/fixture/report-plugin/data/dataParser.js +126 -0
  5. package/dist/fixture/report-plugin/data/uiUtils.js +10 -0
  6. package/dist/fixture/report-plugin/data/urlManager.js +53 -0
  7. package/dist/fixture/report-plugin/handlers/domChangesHandler.js +58 -0
  8. package/dist/fixture/report-plugin/handlers/statusUpdateHandler.js +52 -0
  9. package/dist/fixture/report-plugin/handlers/urlChangeHandler.js +52 -0
  10. package/dist/fixture/report-plugin/management/playwrightStatusUpdater.js +73 -0
  11. package/dist/fixture/report-plugin/management/pollingManager.js +235 -0
  12. package/dist/fixture/report-plugin/management/reportDataManager.js +32 -0
  13. package/dist/fixture/report-plugin/management/statusUtils.js +102 -0
  14. package/dist/fixture/report-plugin/reportRenderer.js +107 -0
  15. package/dist/fixture/report-plugin/state/navigationState.js +24 -0
  16. package/dist/fixture/report-plugin/ui/eyesResultsBatchLinkUI.js +51 -0
  17. package/dist/fixture/report-plugin/ui/filterManager.js +60 -0
  18. package/dist/fixture/report-plugin/ui/mockIframeRenderer.js +39 -0
  19. package/dist/fixture/report-plugin/ui/navigationUI.js +98 -0
  20. package/dist/fixture/report-plugin/ui/testDetailUI.js +265 -0
  21. package/dist/fixture/report-plugin/ui/testListUI.js +43 -0
  22. package/dist/fixture/report-plugin/utils/scrollPreserver.js +33 -0
  23. package/dist/fixture/reportRenderer.js +1 -834
  24. package/dist/fixture/reportRenderer.js.map +1 -0
  25. package/package.json +6 -5
  26. package/dist/fixture/build/rollup.config.js +0 -16
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PollingManager = void 0;
7
+ const statusUtils_js_1 = require("./statusUtils.js");
8
+ const playwrightStatusUpdater_js_1 = require("./playwrightStatusUpdater.js");
9
+ const reportDataManager_js_1 = require("./reportDataManager.js");
10
+ const urlManager_js_1 = require("../data/urlManager.js");
11
+ const log_1 = __importDefault(require("../core/log"));
12
+ const logger = (0, log_1.default)();
13
+ class PollingManager {
14
+ constructor() {
15
+ this._statusPollingTimeout = null;
16
+ this._isPolling = false;
17
+ this._currentResults = null;
18
+ this.onStatusUpdate = null;
19
+ }
20
+ async startPolling(waitForResults) {
21
+ if (this._isPolling) {
22
+ return waitForResults;
23
+ }
24
+ this._isPolling = true;
25
+ this._pollLoop(waitForResults);
26
+ return waitForResults;
27
+ }
28
+ stopPolling() {
29
+ if (this._statusPollingTimeout) {
30
+ clearTimeout(this._statusPollingTimeout);
31
+ this._statusPollingTimeout = null;
32
+ }
33
+ this._isPolling = false;
34
+ }
35
+ async _pollLoop(waitForResults) {
36
+ try {
37
+ logger.log('[Eyes Polling Manager] Polling started.');
38
+ const testResults = await waitForResults;
39
+ // Store current results for next iteration
40
+ if (!this._currentResults) {
41
+ this._currentResults = testResults;
42
+ }
43
+ // Check if all tests are settled (not new/unresolved/running)
44
+ const allSettled = this._areAllTestsSettled(this._currentResults);
45
+ if (allSettled) {
46
+ this.stopPolling();
47
+ return;
48
+ }
49
+ // Fetch status updates from Eyes server and merge
50
+ const hasChanges = await this._fetchAndMergeStatuses(this._currentResults);
51
+ if (!hasChanges) {
52
+ // No status changes detected - skip data refresh and UI update
53
+ }
54
+ else {
55
+ // Status changes detected - refresh data and notify UI
56
+ // Refresh report data (re-zip and update window.playwrightReportBase64)
57
+ await (0, reportDataManager_js_1.refreshReportData)(this._currentResults.testsFiles, this._currentResults.report);
58
+ // Call the update callback if provided
59
+ if (this.onStatusUpdate && typeof this.onStatusUpdate === 'function') {
60
+ this.onStatusUpdate(this._currentResults);
61
+ }
62
+ // Check again after updates - tests may have just settled
63
+ const nowSettled = this._areAllTestsSettled(this._currentResults);
64
+ if (nowSettled) {
65
+ this.stopPolling();
66
+ return;
67
+ }
68
+ }
69
+ }
70
+ catch (error) {
71
+ logger.error('[Eyes Polling Manager] Error during polling:', error);
72
+ }
73
+ finally {
74
+ // Schedule next poll in 5 seconds (only if still polling)
75
+ if (this._isPolling) {
76
+ this._statusPollingTimeout = setTimeout(() => this._pollLoop(Promise.resolve(this._currentResults)), PollingManager.POLLING_INTERVAL_MS);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Fetch status updates from Eyes server and merge with local results
82
+ * Updates the testResults object in place
83
+ */
84
+ async _fetchAndMergeStatuses(testResults) {
85
+ const { sessionsByBatchId } = testResults;
86
+ if (!sessionsByBatchId || Object.keys(sessionsByBatchId).length === 0) {
87
+ return false;
88
+ }
89
+ try {
90
+ // Fetch status for all batches in parallel
91
+ const results = await Promise.all(Object.keys(sessionsByBatchId).map(async (batchId) => {
92
+ return this._getServerDataForBatch(batchId, sessionsByBatchId[batchId]);
93
+ }));
94
+ // Flatten all session results
95
+ const allResults = results.map(result => (result === null || result === void 0 ? void 0 : result.sessions) || []).flat();
96
+ if (allResults.length === 0) {
97
+ return false;
98
+ }
99
+ // Create lookup map by session ID
100
+ const statusBySessionId = allResults.reduce((acc, result) => {
101
+ acc[result.id] = result;
102
+ return acc;
103
+ }, {});
104
+ // Merge server statuses into test results (modifies testResults in place)
105
+ const hasChanges = this._mergeServerStatuses(testResults, statusBySessionId);
106
+ return hasChanges;
107
+ }
108
+ catch (error) {
109
+ logger.error('[Eyes Polling Manager] Failed to fetch status updates:', error);
110
+ return false;
111
+ }
112
+ }
113
+ async _getServerDataForBatch(batchId, batchData) {
114
+ const chunkSize = 300;
115
+ const chunks = [];
116
+ // Split sessions into chunks for API calls
117
+ for (let i = 0; i < batchData.sessions.length; i += chunkSize) {
118
+ chunks.push(batchData.sessions.slice(i, i + chunkSize));
119
+ }
120
+ const url = (0, urlManager_js_1.fixEyesUrl)(batchData.eyesServerUrl);
121
+ const apiKey = batchData.apiKey;
122
+ try {
123
+ const headers = new Headers();
124
+ headers.set('Content-Type', 'application/json');
125
+ // Fetch status for all chunks in parallel
126
+ const results = await Promise.all(chunks.map(async (chunk) => {
127
+ try {
128
+ const response = await fetch(`${url}api/sessions/batches/${batchId}/sessions/sdkquery?apiKey=${apiKey}`, {
129
+ method: 'POST',
130
+ headers,
131
+ body: JSON.stringify({ sessions: chunk }),
132
+ });
133
+ if (!response.ok) {
134
+ logger.warn(`[Eyes Polling Manager] Failed to get status for batch ${batchId}:`, response.statusText);
135
+ return { sessions: [], errors: [] };
136
+ }
137
+ return response.json();
138
+ }
139
+ catch (error) {
140
+ logger.error(`[Eyes Polling Manager] Error fetching batch ${batchId}:`, error);
141
+ return { sessions: [], errors: [] };
142
+ }
143
+ }));
144
+ return {
145
+ sessions: results.map(result => result.sessions).flat(),
146
+ errors: results.map(result => result.errors).flat(),
147
+ };
148
+ }
149
+ catch (error) {
150
+ logger.error(`[Eyes Polling Manager] Failed to get status for batch ${batchId}:`, error);
151
+ return { sessions: [], errors: [] };
152
+ }
153
+ }
154
+ _mergeServerStatuses(testResults, statusBySessionId) {
155
+ let changedCount = 0;
156
+ // Update Eyes test results with server status
157
+ Object.values(testResults.eyesTestResult).forEach(test => {
158
+ test.eyesResults.forEach(result => {
159
+ const serverStatus = statusBySessionId[result.id];
160
+ if (!serverStatus)
161
+ return;
162
+ // Check if status actually changed
163
+ const statusChanged = result.status !== serverStatus.status ||
164
+ result.isDifferent !== serverStatus.isDifferent ||
165
+ result.isNew !== serverStatus.isNew ||
166
+ result.isAborted !== serverStatus.isAborted;
167
+ if (statusChanged) {
168
+ changedCount++;
169
+ }
170
+ // Update high-level status fields
171
+ result.status = serverStatus.status;
172
+ result.isDifferent = serverStatus.isDifferent;
173
+ result.isNew = serverStatus.isNew;
174
+ result.isAborted = serverStatus.isAborted;
175
+ // Update step-level information
176
+ if (result.stepsInfo && Array.isArray(result.stepsInfo)) {
177
+ result.stepsInfo.forEach((step, index) => {
178
+ var _a, _b;
179
+ const expectedAppOutput = serverStatus.expectedAppOutput ? serverStatus.expectedAppOutput[index] : null;
180
+ const actualAppOutput = serverStatus.actualAppOutput ? serverStatus.actualAppOutput[index] : null;
181
+ const appOutputResolution = serverStatus.appOutputResolution
182
+ ? serverStatus.appOutputResolution[index]
183
+ : null;
184
+ step.expectedImageSize = (_a = expectedAppOutput === null || expectedAppOutput === void 0 ? void 0 : expectedAppOutput.image) === null || _a === void 0 ? void 0 : _a.size;
185
+ step.actualImageSize = (_b = actualAppOutput === null || actualAppOutput === void 0 ? void 0 : actualAppOutput.image) === null || _b === void 0 ? void 0 : _b.size;
186
+ step.appOutputResolution = appOutputResolution !== null && appOutputResolution !== void 0 ? appOutputResolution : undefined;
187
+ step.expectedAppOutput = expectedAppOutput !== null && expectedAppOutput !== void 0 ? expectedAppOutput : undefined;
188
+ step.actualAppOutput = actualAppOutput !== null && actualAppOutput !== void 0 ? actualAppOutput : undefined;
189
+ });
190
+ }
191
+ });
192
+ });
193
+ // Update Playwright test statuses based on Eyes results (only if changes occurred)
194
+ if (changedCount > 0) {
195
+ (0, playwrightStatusUpdater_js_1.updateAllPlaywrightTestStatuses)(testResults);
196
+ }
197
+ return changedCount > 0;
198
+ }
199
+ /**
200
+ * Check if all tests have reached their final settled status
201
+ * Settled statuses: passed, failed, aborted
202
+ * Unsettled statuses: unresolved, new, running
203
+ *
204
+ * Important: Only checks the LAST retry's status for each test.
205
+ * Uses getLastRetryResults() from statusUtils for consistency.
206
+ * Earlier retries are ignored - only the final attempt determines if test is settled.
207
+ */
208
+ _areAllTestsSettled(testResults) {
209
+ if (!testResults || !testResults.eyesTestResult) {
210
+ return true; // No Eyes tests, nothing to poll
211
+ }
212
+ const eyesTests = Object.values(testResults.eyesTestResult);
213
+ if (eyesTests.length === 0) {
214
+ return true; // No Eyes tests
215
+ }
216
+ // Count unsettled tests
217
+ let unsettledCount = 0;
218
+ eyesTests.forEach(test => {
219
+ // Get only the results from the last retry (using centralized utility)
220
+ const lastRetryResults = (0, statusUtils_js_1.getLastRetryResults)(test.eyesResults);
221
+ // Check if the last retry has any unsettled results
222
+ const hasUnsettledResults = lastRetryResults.some(result => {
223
+ const status = result.status.toLowerCase();
224
+ // Unsettled statuses: unresolved, new, running
225
+ return status !== 'aborted';
226
+ });
227
+ if (hasUnsettledResults) {
228
+ unsettledCount++;
229
+ }
230
+ });
231
+ return unsettledCount === 0;
232
+ }
233
+ }
234
+ exports.PollingManager = PollingManager;
235
+ PollingManager.POLLING_INTERVAL_MS = 5000;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.refreshReportData = void 0;
7
+ const jszip_1 = __importDefault(require("jszip"));
8
+ const log_1 = __importDefault(require("../core/log"));
9
+ const logger = (0, log_1.default)();
10
+ /**
11
+ * Re-zip report data and update window.playwrightReportBase64
12
+ * This is necessary because test statuses have been modified in memory
13
+ * and Playwright's UI needs to see the updated data
14
+ */
15
+ async function refreshReportData(testsFiles, report) {
16
+ const newZip = new jszip_1.default();
17
+ const newFiles = { ...testsFiles, 'report.json': report };
18
+ Object.keys(newFiles).forEach(fileName => {
19
+ newZip.file(fileName, JSON.stringify(newFiles[fileName]));
20
+ });
21
+ const generatedZip = await newZip.generateAsync({ type: 'base64' });
22
+ window.playwrightReportBase64 = `data:application/zip;base64,${generatedZip}`;
23
+ logger.log('[Report Data Manager] Report data refreshed.');
24
+ // Also store in localStorage for HMR support PW 1.55 and above
25
+ try {
26
+ localStorage.setItem('playwrightReportStorageForHMR', generatedZip);
27
+ }
28
+ catch (e) {
29
+ logger.warn('[Report Data Manager] Unable to access localStorage:', e);
30
+ }
31
+ }
32
+ exports.refreshReportData = refreshReportData;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getSingleSessionStatus = exports.countTestsByStatus = exports.getStatus = exports.getLastRetryResults = void 0;
4
+ const SESSION_STATUS = {
5
+ Failed: 'failed',
6
+ Passed: 'passed',
7
+ Running: 'running',
8
+ Unresolved: 'unresolved',
9
+ };
10
+ const STATUS = {
11
+ Aborted: 'aborted',
12
+ Default: 'default',
13
+ Failed: 'failed',
14
+ New: 'new',
15
+ Passed: 'passed',
16
+ Running: 'running',
17
+ Unresolved: 'unresolved',
18
+ Unsaved: 'unsaved',
19
+ Empty: 'empty',
20
+ };
21
+ const STATUS_PRIORITY = [
22
+ SESSION_STATUS.Running,
23
+ SESSION_STATUS.Passed,
24
+ SESSION_STATUS.Failed,
25
+ SESSION_STATUS.Unresolved,
26
+ ];
27
+ function getLastRetryResults(eyesResults) {
28
+ if (eyesResults.length === 0) {
29
+ return [];
30
+ }
31
+ // Find the highest retry number (last retry attempt)
32
+ const maxRetry = Math.max(...eyesResults.map(r => { var _a; return (_a = r.playwrightRetry) !== null && _a !== void 0 ? _a : 0; }));
33
+ // Return only results from the last retry
34
+ return eyesResults.filter(r => { var _a; return ((_a = r.playwrightRetry) !== null && _a !== void 0 ? _a : 0) === maxRetry; });
35
+ }
36
+ exports.getLastRetryResults = getLastRetryResults;
37
+ function getStatus(eyesResults) {
38
+ // Use only the last retry's results
39
+ const lastRetryResults = getLastRetryResults(eyesResults);
40
+ if (lastRetryResults.length === 0) {
41
+ // No results at all
42
+ return { status: 'empty', statusText: STATUS.Empty, icon: null };
43
+ }
44
+ // Aggregate result flags using reduce for better functional style
45
+ const { maxPriority, isAborted, isNew, isDifferent, isEmpty } = lastRetryResults.reduce((acc, result) => {
46
+ const statusPriority = STATUS_PRIORITY.indexOf(result.status.toLowerCase());
47
+ return {
48
+ maxPriority: Math.max(statusPriority, acc.maxPriority),
49
+ isAborted: acc.isAborted || result.isAborted,
50
+ isNew: acc.isNew || result.isNew,
51
+ isDifferent: acc.isDifferent || result.isDifferent,
52
+ isEmpty: acc.isEmpty && result.steps === 0,
53
+ };
54
+ }, { maxPriority: 0, isAborted: false, isNew: false, isDifferent: false, isEmpty: true });
55
+ const priorityIndex = isAborted ? Math.max(2, maxPriority) : maxPriority;
56
+ const status = STATUS_PRIORITY[priorityIndex];
57
+ const statusText = isAborted
58
+ ? STATUS.Aborted
59
+ : isEmpty
60
+ ? STATUS.Empty
61
+ : status !== SESSION_STATUS.Running && isNew
62
+ ? STATUS.New
63
+ : status;
64
+ const icon = isAborted ? null : isNew ? 'new' : isDifferent ? 'different' : 'resolved';
65
+ return { status, statusText, icon };
66
+ }
67
+ exports.getStatus = getStatus;
68
+ function countTestsByStatus(eyesTestResult) {
69
+ return Object.values(eyesTestResult).reduce((counts, test) => {
70
+ counts.total++;
71
+ // getStatus() automatically uses only the last retry's results
72
+ const status = getStatus(test.eyesResults).status;
73
+ // Use type-safe property access with mapped type
74
+ if (status === 'passed' || status === 'failed' || status === 'unresolved') {
75
+ counts[status]++;
76
+ }
77
+ return counts;
78
+ }, { total: 0, passed: 0, failed: 0, unresolved: 0 });
79
+ }
80
+ exports.countTestsByStatus = countTestsByStatus;
81
+ function getSingleSessionStatus(test) {
82
+ let maxPriority = Math.max(STATUS_PRIORITY.indexOf(test.status.toLowerCase()), 0);
83
+ let statusText;
84
+ let status = STATUS_PRIORITY[maxPriority];
85
+ const isEmpty = test.steps === 0;
86
+ if (test.isAborted) {
87
+ maxPriority = Math.max(2, maxPriority);
88
+ status = STATUS_PRIORITY[maxPriority];
89
+ statusText = STATUS.Aborted;
90
+ }
91
+ else if (isEmpty) {
92
+ statusText = STATUS.Empty;
93
+ }
94
+ else if (status !== STATUS.Running && test.isNew) {
95
+ statusText = STATUS.New;
96
+ }
97
+ else {
98
+ statusText = status;
99
+ }
100
+ return { status, statusText };
101
+ }
102
+ exports.getSingleSessionStatus = getSingleSessionStatus;
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ /**
7
+ * Report Renderer - Orchestrates Eyes integration into Playwright HTML reports
8
+ *
9
+ * Initial Flow:
10
+ * 1. Waits for test results, calls window.onload() to refresh Playwright UI with Eyes data
11
+ * 2. After 200ms delay (for React render), creates Eyes UI elements via DOM observer
12
+ * 3. Renders batch link (list mode), navigation filters, test badges (list mode), iframes (detail mode)
13
+ * 4. Starts polling Eyes server every 5s for status updates
14
+ *
15
+ * Polling & Update Flow:
16
+ * - Detects changes by comparing status fields before updating
17
+ * - Continues indefinitely unless all tests reach `aborted` status
18
+ * - On changes: merges updates, recalculates Playwright statuses, updates window.playwrightReportBase64
19
+ * - Triggers window.onload() for React re-render (removes all Eyes elements from DOM)
20
+ * - After 200ms delay, recreates Eyes elements in order: batch link, filters, badges, iframes
21
+ */
22
+ const dataParser_js_1 = require("./data/dataParser.js");
23
+ const navigationState_js_1 = require("./state/navigationState.js");
24
+ const scrollPreserver_js_1 = require("./utils/scrollPreserver.js");
25
+ const eyesResultsBatchLinkUI_js_1 = require("./ui/eyesResultsBatchLinkUI.js");
26
+ const navigationUI_js_1 = require("./ui/navigationUI.js");
27
+ const filterManager_js_1 = require("./ui/filterManager.js");
28
+ const testListUI_js_1 = require("./ui/testListUI.js");
29
+ const testDetailUI_js_1 = require("./ui/testDetailUI.js");
30
+ const pollingManager_js_1 = require("./management/pollingManager.js");
31
+ const urlChangeHandler_js_1 = require("./handlers/urlChangeHandler.js");
32
+ const domChangesHandler_js_1 = require("./handlers/domChangesHandler.js");
33
+ const statusUpdateHandler_js_1 = require("./handlers/statusUpdateHandler.js");
34
+ const log_1 = __importDefault(require("./core/log"));
35
+ const logger = (0, log_1.default)();
36
+ class ReportRenderer {
37
+ constructor(waitForResults, options = {}) {
38
+ this.updatePlaywrightWithEyesResults = () => {
39
+ // Capture scroll position before triggering window.onload
40
+ this.scrollPreserver.captureScrollPosition();
41
+ if (typeof window.onload === 'function') {
42
+ const onloadHandler = window.onload;
43
+ onloadHandler(new Event('load'));
44
+ }
45
+ setTimeout(() => {
46
+ this.urlChangeHandler.handleUrlChange();
47
+ // Restore scroll position after React has re-rendered
48
+ this.scrollPreserver.restoreScrollPosition();
49
+ }, 200);
50
+ };
51
+ this.handleStatusUpdate = async (testResults) => {
52
+ this.waitForResults = Promise.resolve(testResults);
53
+ await this.statusUpdateHandler.handleStatusUpdate(testResults);
54
+ };
55
+ this.waitForResults = waitForResults;
56
+ this.useTestMode = options.useTestMode || false;
57
+ // Create shared navigation state
58
+ this.navigationState = new navigationState_js_1.NavigationState();
59
+ this.scrollPreserver = new scrollPreserver_js_1.ScrollPreserver();
60
+ this.filterManager = new filterManager_js_1.FilterManager();
61
+ this.eyesResultsBatchLinkUI = new eyesResultsBatchLinkUI_js_1.EyesResultsBatchLinkUI();
62
+ this.navigationUI = new navigationUI_js_1.NavigationUI(this.filterManager);
63
+ this.testListUI = new testListUI_js_1.TestListUI();
64
+ this.testDetailUI = new testDetailUI_js_1.TestDetailUI(this.navigationState, { useTestMode: this.useTestMode });
65
+ this.urlChangeHandler = new urlChangeHandler_js_1.UrlChangeHandler(this.waitForResults, this.navigationState, this.filterManager, this.eyesResultsBatchLinkUI, this.navigationUI, this.testDetailUI);
66
+ this.domChangesHandler = new domChangesHandler_js_1.DomChangesHandler(this.waitForResults, this.filterManager, this.eyesResultsBatchLinkUI, this.navigationUI, this.testListUI);
67
+ this.statusUpdateHandler = new statusUpdateHandler_js_1.StatusUpdateHandler(this.scrollPreserver, this.eyesResultsBatchLinkUI, this.navigationUI, this.testListUI, this.testDetailUI);
68
+ this.pollingManager = new pollingManager_js_1.PollingManager();
69
+ this.pollingManager.onStatusUpdate = this.handleStatusUpdate;
70
+ this.waitForResults.then(this.updatePlaywrightWithEyesResults);
71
+ }
72
+ render() {
73
+ window.addEventListener('hashchange', this.urlChangeHandler.handleUrlChange);
74
+ window.addEventListener('popstate', this.urlChangeHandler.handleUrlChange);
75
+ if (document.readyState === 'loading') {
76
+ document.addEventListener('DOMContentLoaded', this.urlChangeHandler.handleUrlChange);
77
+ }
78
+ else {
79
+ this.urlChangeHandler.handleUrlChange();
80
+ }
81
+ this.observeDomChanges();
82
+ this.pollingManager.startPolling(this.waitForResults);
83
+ }
84
+ observeDomChanges() {
85
+ const mutationObserver = new MutationObserver(this.domChangesHandler.handleDomChanges);
86
+ const rootElement = document.getElementById('root');
87
+ if (!rootElement) {
88
+ logger.warn('No root element found for observation');
89
+ return;
90
+ }
91
+ mutationObserver.observe(rootElement, {
92
+ childList: true,
93
+ subtree: true,
94
+ });
95
+ }
96
+ updateFilterCounts(testResults) {
97
+ this.navigationUI.updateFilterCounts(testResults);
98
+ }
99
+ }
100
+ window.__initEyesReport = (options = {}) => {
101
+ const useTestMode = options.useTestMode || false;
102
+ logger.log('Before Initialized');
103
+ const waitForResults = (0, dataParser_js_1.getTestResults)();
104
+ const reportRenderer = new ReportRenderer(waitForResults, { useTestMode });
105
+ reportRenderer.render();
106
+ logger.log('Initialized');
107
+ };
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * Navigation State - Shared state for managing active test navigation
4
+ *
5
+ * This class provides a centralized location for tracking which test is currently
6
+ * active in the UI. It eliminates duplicate state between UrlChangeHandler and TestDetailUI.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.NavigationState = void 0;
10
+ class NavigationState {
11
+ constructor() {
12
+ this._activeTestId = null;
13
+ }
14
+ get activeTestId() {
15
+ return this._activeTestId;
16
+ }
17
+ setActiveTestId(testId) {
18
+ this._activeTestId = testId;
19
+ }
20
+ clearActiveTestId() {
21
+ this._activeTestId = null;
22
+ }
23
+ }
24
+ exports.NavigationState = NavigationState;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.EyesResultsBatchLinkUI = void 0;
7
+ const urlManager_js_1 = require("../data/urlManager.js");
8
+ const log_js_1 = __importDefault(require("../core/log.js"));
9
+ const logger = (0, log_js_1.default)();
10
+ class EyesResultsBatchLinkUI {
11
+ constructor() {
12
+ this._linkCreated = false;
13
+ }
14
+ createLinkToBatch(testResults) {
15
+ var _a, _b;
16
+ if (!(0, urlManager_js_1.isListMode)()) {
17
+ this.removeLinkToBatch();
18
+ return;
19
+ }
20
+ const existingLink = document.getElementsByClassName('eyes-batch-link')[0];
21
+ if (existingLink) {
22
+ return;
23
+ }
24
+ const firstChip = document.getElementsByClassName('chip')[0];
25
+ if (!firstChip) {
26
+ logger.warn('No chip element found, cannot create link');
27
+ return;
28
+ }
29
+ const firstResult = (_a = Object.values(testResults.eyesTestResult)[0]) === null || _a === void 0 ? void 0 : _a.eyesResults[0];
30
+ if (!firstResult || !((_b = firstResult.appUrls) === null || _b === void 0 ? void 0 : _b.batch)) {
31
+ logger.warn('No Eyes results with batch URL found');
32
+ return;
33
+ }
34
+ const linkContainer = document.createElement('div');
35
+ linkContainer.classList.add('eyes-batch-link');
36
+ linkContainer.innerHTML = `<a target='_blank' href='${firstResult.appUrls.batch}'>Results in Eyes Dashboard</a>`;
37
+ firstChip.parentNode.insertBefore(linkContainer, firstChip);
38
+ this._linkCreated = true;
39
+ }
40
+ removeLinkToBatch() {
41
+ const link = document.getElementsByClassName('eyes-batch-link')[0];
42
+ if (link) {
43
+ link.remove();
44
+ this._linkCreated = false;
45
+ }
46
+ }
47
+ isLinkDisplayed() {
48
+ return this._linkCreated && document.getElementsByClassName('eyes-batch-link')[0] !== undefined;
49
+ }
50
+ }
51
+ exports.EyesResultsBatchLinkUI = EyesResultsBatchLinkUI;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.FilterManager = void 0;
7
+ const log_1 = __importDefault(require("../core/log"));
8
+ const logger = (0, log_1.default)();
9
+ class FilterManager {
10
+ constructor() {
11
+ this._activeFilters = {
12
+ eyes: false,
13
+ unresolved: false,
14
+ };
15
+ }
16
+ applyFiltersFromUrl(hash) {
17
+ const eyesFilter = hash.get('eyes');
18
+ const unresolvedFilter = hash.get('unresolved');
19
+ this.setEyesFilter(!!eyesFilter);
20
+ this.setUnresolvedFilter(!!unresolvedFilter);
21
+ }
22
+ setEyesFilter(active) {
23
+ this._activeFilters.eyes = active;
24
+ const htmlreport = document.getElementsByClassName('htmlreport')[0];
25
+ if (!htmlreport) {
26
+ logger.warn('[Filter Manager] No .htmlreport element found');
27
+ return;
28
+ }
29
+ if (active) {
30
+ htmlreport.classList.add('eyes-filter');
31
+ }
32
+ else {
33
+ htmlreport.classList.remove('eyes-filter');
34
+ }
35
+ }
36
+ setUnresolvedFilter(active) {
37
+ this._activeFilters.unresolved = active;
38
+ const htmlreport = document.getElementsByClassName('htmlreport')[0];
39
+ if (!htmlreport) {
40
+ logger.warn('[Filter Manager] No .htmlreport element found');
41
+ return;
42
+ }
43
+ if (active) {
44
+ htmlreport.classList.add('unresolved-filter');
45
+ }
46
+ else {
47
+ htmlreport.classList.remove('unresolved-filter');
48
+ }
49
+ }
50
+ isEyesFilterActive() {
51
+ return this._activeFilters.eyes;
52
+ }
53
+ isUnresolvedFilterActive() {
54
+ return this._activeFilters.unresolved;
55
+ }
56
+ getActiveFilters() {
57
+ return { ...this._activeFilters };
58
+ }
59
+ }
60
+ exports.FilterManager = FilterManager;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ /**
3
+ * Mock Iframe Renderer - Creates mock iframe representations for test mode
4
+ *
5
+ * This module provides functionality to create mock iframe elements that simulate
6
+ * the Eyes test result viewer without actually loading the full iframe. This is
7
+ * useful for testing and development purposes.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.createMockIframe = void 0;
11
+ /**
12
+ * Creates a mock iframe element displaying Eyes test result information
13
+ * @param result - The Eyes test result to display
14
+ * @returns HTML element representing a mock iframe
15
+ */
16
+ function createMockIframe(result) {
17
+ const mockIframe = document.createElement('div');
18
+ mockIframe.classList.add('eyes-session-result', 'eyes-mock-iframe');
19
+ mockIframe.setAttribute('value', result.id);
20
+ mockIframe.innerHTML = `
21
+ <div style="padding: 20px; border: 2px solid #ddd; background: #f5f5f5; font-family: monospace;">
22
+ <h3 style="margin: 0 0 10px 0; color: #333;">Mock Eyes Iframe</h3>
23
+ <div style="margin-bottom: 8px;"><strong>Session ID:</strong> ${result.id}</div>
24
+ <div style="margin-bottom: 8px;"><strong>Status:</strong> ${result.status}</div>
25
+ <div style="margin-bottom: 8px;"><strong>Browser:</strong> ${result.hostApp}</div>
26
+ <div style="margin-bottom: 8px;"><strong>Viewport:</strong> ${result.hostDisplaySize.width}x${result.hostDisplaySize.height}</div>
27
+ <div style="margin-bottom: 8px;"><strong>Steps:</strong> ${result.steps}</div>
28
+ <div style="margin-bottom: 8px;"><strong>Is New:</strong> ${result.isNew}</div>
29
+ <div style="margin-bottom: 8px;"><strong>Has Differences:</strong> ${result.isDifferent}</div>
30
+ <div style="margin-top: 15px;">
31
+ <a href="${result.appUrls.session}" target="_blank" style="color: #0066cc;">
32
+ View in Eyes Dashboard →
33
+ </a>
34
+ </div>
35
+ </div>
36
+ `;
37
+ return mockIframe;
38
+ }
39
+ exports.createMockIframe = createMockIframe;