@applitools/eyes-playwright 1.40.7 → 1.41.1
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.
- package/CHANGELOG.md +118 -0
- package/dist/fixture/report-plugin/core/log.js +44 -0
- package/dist/fixture/report-plugin/core/types.js +5 -0
- package/dist/fixture/report-plugin/data/dataParser.js +126 -0
- package/dist/fixture/report-plugin/data/uiUtils.js +10 -0
- package/dist/fixture/report-plugin/data/urlManager.js +53 -0
- package/dist/fixture/report-plugin/handlers/domChangesHandler.js +58 -0
- package/dist/fixture/report-plugin/handlers/statusUpdateHandler.js +52 -0
- package/dist/fixture/report-plugin/handlers/urlChangeHandler.js +52 -0
- package/dist/fixture/report-plugin/management/playwrightStatusUpdater.js +73 -0
- package/dist/fixture/report-plugin/management/pollingManager.js +235 -0
- package/dist/fixture/report-plugin/management/reportDataManager.js +32 -0
- package/dist/fixture/report-plugin/management/statusUtils.js +102 -0
- package/dist/fixture/report-plugin/reportRenderer.js +107 -0
- package/dist/fixture/report-plugin/state/navigationState.js +24 -0
- package/dist/fixture/report-plugin/ui/eyesResultsBatchLinkUI.js +51 -0
- package/dist/fixture/report-plugin/ui/filterManager.js +60 -0
- package/dist/fixture/report-plugin/ui/mockIframeRenderer.js +173 -0
- package/dist/fixture/report-plugin/ui/navigationUI.js +98 -0
- package/dist/fixture/report-plugin/ui/testDetailUI.js +269 -0
- package/dist/fixture/report-plugin/ui/testListUI.js +43 -0
- package/dist/fixture/report-plugin/utils/scrollPreserver.js +33 -0
- package/dist/fixture/reportRenderer.js +1 -834
- package/dist/fixture/reportRenderer.js.map +1 -0
- package/package.json +9 -8
- 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;
|