@applitools/eyes-playwright
Version:
Applitools Eyes SDK for Playwright
274 lines (273 loc) • 13.8 kB
JavaScript
;
/**
* Test Detail UI - Manages Eyes test result display in Playwright test detail view
*
* This module handles creating and managing Eyes test result iframes in the test detail page.
* It observes DOM changes to detect when test detail pages are ready, manages retry tab
* selection changes, and creates the appropriate iframe or mock iframe for each Eyes session.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestDetailUI = void 0;
const urlManager_js_1 = require("../data/urlManager.js");
const statusUtils_js_1 = require("../management/statusUtils.js");
const uiUtils_js_1 = require("../data/uiUtils.js");
const mockIframeRenderer_js_1 = require("./mockIframeRenderer.js");
const log_1 = __importDefault(require("../core/log"));
const logger = (0, log_1.default)();
class TestDetailUI {
constructor(navigationState, options = {}) {
this.navigationState = navigationState;
this._pendingObserver = null;
this._retryObserver = null;
this._lastCreateTimestamp = 0;
this.useTestMode = options.useTestMode || false;
}
cancelPendingOperations() {
if (this._pendingObserver) {
this._pendingObserver.disconnect();
this._pendingObserver = null;
}
if (this._retryObserver) {
this._retryObserver.disconnect();
this._retryObserver = null;
}
// Clear activeTestId when canceling operations (navigation away)
this.navigationState.clearActiveTestId();
}
createEyesTestResults(test) {
logger.log('[TestDetailUI] Creating Eyes test results for testId:', test.testId);
const now = Date.now();
// Debounce: skip if same test was just created within 200ms
if (this.navigationState.activeTestId === test.testId && now - this._lastCreateTimestamp < 200) {
logger.log('[TestDetailUI] Debounce: skipping duplicate call within 200ms');
return;
}
this.cancelPendingOperations();
const currentTestId = test.testId;
// Set activeTestId before timestamp to ensure proper debounce check
this.navigationState.setActiveTestId(currentTestId);
this._lastCreateTimestamp = now;
let hasCreatedResults = false;
const isCorrectTestPage = () => {
const hash = (0, urlManager_js_1.getHashFromCurrentUrl)();
const urlTestId = hash.get('testId');
return urlTestId === currentTestId;
};
const attemptToCreateResults = () => {
if (hasCreatedResults)
return false;
if (!isCorrectTestPage()) {
return false;
}
let tabContent = document.querySelector('.tab-content[role="tabpanel"]');
if (!tabContent) {
tabContent = document.querySelector('.tab-content > .test-result');
}
logger.log('[TestDetailUI] Attempting to create Eyes test results. Tab content found:', !!tabContent);
const chipsInTab = tabContent === null || tabContent === void 0 ? void 0 : tabContent.getElementsByClassName('chip');
if (tabContent && chipsInTab && chipsInTab.length > 0) {
if (!isCorrectTestPage()) {
return false;
}
hasCreatedResults = true;
this.onTestResultPageReady(test);
this._pendingObserver = null;
// Don't clear activeTestId here - keep it set for debouncing
return true;
}
return false;
};
if (attemptToCreateResults())
return;
const testCaseColumn = document.querySelector('.test-case-column');
const htmlReportElement = document.getElementsByClassName('htmlreport')[0];
const observeTarget = testCaseColumn || htmlReportElement;
if (!observeTarget) {
logger.warn('No observe target found, retrying after delay');
setTimeout(() => {
if (this.navigationState.activeTestId === currentTestId && isCorrectTestPage()) {
this.createEyesTestResults(test);
}
}, 200);
return;
}
const mutationObserver = new MutationObserver(() => {
if (attemptToCreateResults()) {
mutationObserver.disconnect();
}
});
this._pendingObserver = mutationObserver;
mutationObserver.observe(observeTarget, {
childList: true,
subtree: true,
});
setTimeout(() => {
if (attemptToCreateResults()) {
mutationObserver.disconnect();
}
else if (isCorrectTestPage()) {
logger.warn(`Timeout: Failed to create Eyes results for test ${currentTestId}`);
}
}, 300);
}
onTestResultPageReady(test) {
const hash = (0, urlManager_js_1.getHashFromCurrentUrl)();
const urlTestId = hash.get('testId');
if (urlTestId !== test.testId) {
return;
}
const shouldFilterUnresolved = new URLSearchParams(window.location.search).get('unresolved');
const frontendPath = new URLSearchParams(window.location.search).get('frontendPath');
const iframeSrc = `app/html-report/index.html?${frontendPath != null ? `frontendPath=${frontendPath}&` : ''}${shouldFilterUnresolved ? 'unresolved=true&' : ''}cache=${Date.now()}&`;
let currentSelectedRetryIndex = Array.from(document.getElementsByClassName('tabbed-pane-tab-element')).findIndex(tab => tab.classList.contains('selected'));
if (currentSelectedRetryIndex === -1) {
currentSelectedRetryIndex = 0;
}
this.createEyesResultIframes(test, iframeSrc, currentSelectedRetryIndex, shouldFilterUnresolved);
this.observeRetryTabSelectionChanged(test, iframeSrc, shouldFilterUnresolved);
}
observeRetryTabSelectionChanged(test, iframeSrc, shouldFilterUnresolved) {
const retryTabs = document.getElementsByClassName('tabbed-pane-tab-element');
if (retryTabs.length === 0)
return;
let selectedRetryIndex = Array.from(retryTabs).findIndex(tab => tab.classList.contains('selected'));
const mutationObserver = new MutationObserver(() => {
const hash = (0, urlManager_js_1.getHashFromCurrentUrl)();
const urlTestId = hash.get('testId');
if (urlTestId !== test.testId) {
mutationObserver.disconnect();
return;
}
const newSelectedRetryIndex = Array.from(retryTabs).findIndex(tab => tab.classList.contains('selected'));
if (newSelectedRetryIndex === selectedRetryIndex)
return;
selectedRetryIndex = newSelectedRetryIndex;
this.createEyesResultIframes(test, iframeSrc, selectedRetryIndex, shouldFilterUnresolved);
});
this._retryObserver = mutationObserver;
if (retryTabs[selectedRetryIndex]) {
mutationObserver.observe(retryTabs[selectedRetryIndex], {
attributes: true,
});
}
}
createTemplateChip() {
const testResultElement = document.querySelector('.test-result');
const firstChip = ((testResultElement === null || testResultElement === void 0 ? void 0 : testResultElement.querySelector('.chip')) ||
document.getElementsByClassName('chip')[0]);
const chipTemplate = firstChip.cloneNode(true);
chipTemplate.classList.add('eyes-test-results');
const chipTemplateHeader = chipTemplate.querySelector('.chip-header');
chipTemplateHeader.setAttribute('title', 'Eyes Test Results');
chipTemplateHeader.removeChild(chipTemplateHeader.lastChild);
const chipHeaderText = document.createElement('span');
chipHeaderText.classList.add('chip-header-text');
chipTemplateHeader.appendChild(chipHeaderText);
chipTemplateHeader.onclick = (event) => {
const target = event.currentTarget;
target.classList.toggle('expanded-false');
target.classList.toggle('expanded-true');
const bodyElement = target.parentNode.querySelector('.test-result') || target.parentNode.querySelector('.chip-body');
bodyElement.classList.toggle('hidden');
};
const chipTemplateBody = (chipTemplate.querySelector('.test-result') ||
chipTemplate.querySelector('.chip-body'));
chipTemplateBody.innerHTML = '';
return { chipTemplate, chipTemplateHeader, firstChip };
}
createChipForTest(result, chipTemplate, chipTemplateHeader) {
const chip = chipTemplate.cloneNode(true);
chip.setAttribute('value', result.id);
const chipHeader = chip.querySelector('.chip-header');
chipHeader.onclick = chipTemplateHeader.onclick;
const sessionText = `${result.hostApp} ${result.hostDisplaySize.width} x ${result.hostDisplaySize.height}`;
const chipHeaderText = chip.querySelector('.chip-header-text');
chipHeaderText.innerHTML = `Eyes Test Results <span class="test-header-info"> - ${sessionText} <div class="eyes-info">${(0, uiUtils_js_1.createEyesInfo)((0, statusUtils_js_1.getSingleSessionStatus)(result), false)}</div><a href=${result.appUrls.session} target='_blank' title='View in Eyes Dashboard'>
<span>${window.__icons.link}</span>
</a></span>`;
const chipLink = chip.querySelector('.chip-header-text a');
chipLink.onclick = event => event.stopPropagation();
return chip;
}
createTestIframe(iframeSrc, result) {
const iframe = document.createElement('iframe');
iframe.classList.add('eyes-session-result');
iframe.setAttribute('value', result.id);
iframe.style.visibility = 'hidden';
iframe.style.opacity = '0';
const url = (0, urlManager_js_1.fixEyesUrl)(result.eyesServer.eyesServerUrl);
iframe.onload = () => {
const iframeUrl = `${url}app/html-report/index.html`;
iframe.contentWindow.postMessage({ call: 'sendValue', value: result }, iframeUrl);
iframe.classList.add('ready');
iframe.style.visibility = 'visible';
iframe.style.opacity = '1';
window.addEventListener('message', async (event) => {
if (event.origin !== url && `${event.origin}/` !== url)
return;
if (event.data === 'forceRefreshData') {
// Handle force refresh if needed
}
});
};
iframe.src = `${url}${iframeSrc}`;
return iframe;
}
createEyesResultIframes(test, iframeSrc, selectedRetryIndex, shouldFilterUnresolved) {
const hash = (0, urlManager_js_1.getHashFromCurrentUrl)();
const urlTestId = hash.get('testId');
if (urlTestId !== test.testId) {
return;
}
const existingChips = document.getElementsByClassName('eyes-test-results');
if (existingChips.length > 0) {
const existingSessionIds = Array.from(existingChips).map(chip => chip.getAttribute('value'));
const expectedSessionIds = test.eyesResults
.filter((r) => r.playwrightRetry === selectedRetryIndex)
.filter((r) => !shouldFilterUnresolved || (0, statusUtils_js_1.getSingleSessionStatus)(r).status === 'unresolved')
.map((r) => r.id);
const hasAllExpectedIframes = existingSessionIds.length === expectedSessionIds.length &&
expectedSessionIds.every((id) => existingSessionIds.includes(id));
if (hasAllExpectedIframes) {
return;
}
Array.from(existingChips).forEach(chip => chip.remove());
}
const { chipTemplate, chipTemplateHeader, firstChip } = this.createTemplateChip();
let createdIframes = 0;
test.eyesResults.forEach((result) => {
if (result.playwrightRetry !== selectedRetryIndex)
return;
if (shouldFilterUnresolved && (0, statusUtils_js_1.getSingleSessionStatus)(result).status !== 'unresolved')
return;
const chip = this.createChipForTest(result, chipTemplate, chipTemplateHeader);
const chipBody = (chip.querySelector('.test-result') || chip.querySelector('.chip-body'));
let bodyElement;
if (result.steps === 0) {
bodyElement = document.createElement('div');
bodyElement.innerHTML = 'No steps were executed for this test';
chipBody === null || chipBody === void 0 ? void 0 : chipBody.classList.add('empty');
}
else {
bodyElement = this.useTestMode ? (0, mockIframeRenderer_js_1.createMockIframe)(result) : this.createTestIframe(iframeSrc, result);
}
chipBody === null || chipBody === void 0 ? void 0 : chipBody.appendChild(bodyElement);
firstChip.parentNode.appendChild(chip);
if (createdIframes > 0) {
const chipHeader = chip.querySelector('.chip-header');
chipHeader.click();
}
++createdIframes;
});
}
cleanupExistingChips() {
const eyesTestResults = document.getElementsByClassName('eyes-test-results');
if (eyesTestResults.length > 0) {
Array.from(eyesTestResults).forEach(eyesTestResult => eyesTestResult.remove());
}
}
}
exports.TestDetailUI = TestDetailUI;