UNPKG

playwright-core

Version:

A high-level API to automate web browsers

500 lines (499 loc) • 23 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var snapshotRenderer_exports = {}; __export(snapshotRenderer_exports, { SnapshotRenderer: () => SnapshotRenderer, rewriteURLForCustomProtocol: () => rewriteURLForCustomProtocol }); module.exports = __toCommonJS(snapshotRenderer_exports); var import_stringUtils = require("../stringUtils"); function findClosest(items, metric, target) { return items.find((item, index) => { if (index === items.length - 1) return true; const next = items[index + 1]; return Math.abs(metric(item) - target) < Math.abs(metric(next) - target); }); } function isNodeNameAttributesChildNodesSnapshot(n) { return Array.isArray(n) && typeof n[0] === "string"; } function isSubtreeReferenceSnapshot(n) { return Array.isArray(n) && Array.isArray(n[0]); } class SnapshotRenderer { constructor(htmlCache, resources, snapshots, screencastFrames, index) { this._htmlCache = htmlCache; this._resources = resources; this._snapshots = snapshots; this._index = index; this._snapshot = snapshots[index]; this._callId = snapshots[index].callId; this._screencastFrames = screencastFrames; this.snapshotName = snapshots[index].snapshotName; } snapshot() { return this._snapshots[this._index]; } viewport() { return this._snapshots[this._index].viewport; } closestScreenshot() { const { wallTime, timestamp } = this.snapshot(); const closestFrame = wallTime && this._screencastFrames[0]?.frameSwapWallTime ? findClosest(this._screencastFrames, (frame) => frame.frameSwapWallTime, wallTime) : findClosest(this._screencastFrames, (frame) => frame.timestamp, timestamp); return closestFrame?.sha1; } render() { const result = []; const visit = (n, snapshotIndex, parentTag, parentAttrs) => { if (typeof n === "string") { if (parentTag === "STYLE" || parentTag === "style") result.push(escapeURLsInStyleSheet(rewriteURLsInStyleSheetForCustomProtocol(n))); else result.push((0, import_stringUtils.escapeHTML)(n)); return; } if (isSubtreeReferenceSnapshot(n)) { const referenceIndex = snapshotIndex - n[0][0]; if (referenceIndex >= 0 && referenceIndex <= snapshotIndex) { const nodes = snapshotNodes(this._snapshots[referenceIndex]); const nodeIndex = n[0][1]; if (nodeIndex >= 0 && nodeIndex < nodes.length) return visit(nodes[nodeIndex], referenceIndex, parentTag, parentAttrs); } } else if (isNodeNameAttributesChildNodesSnapshot(n)) { const [name, nodeAttrs, ...children] = n; const nodeName = name === "NOSCRIPT" ? "X-NOSCRIPT" : name; const attrs = Object.entries(nodeAttrs || {}); result.push("<", nodeName); const kCurrentSrcAttribute = "__playwright_current_src__"; const isFrame = nodeName === "IFRAME" || nodeName === "FRAME"; const isAnchor = nodeName === "A"; const isImg = nodeName === "IMG"; const isImgWithCurrentSrc = isImg && attrs.some((a) => a[0] === kCurrentSrcAttribute); const isSourceInsidePictureWithCurrentSrc = nodeName === "SOURCE" && parentTag === "PICTURE" && parentAttrs?.some((a) => a[0] === kCurrentSrcAttribute); for (const [attr, value] of attrs) { let attrName = attr; if (isFrame && attr.toLowerCase() === "src") { attrName = "__playwright_src__"; } if (isImg && attr === kCurrentSrcAttribute) { attrName = "src"; } if (["src", "srcset"].includes(attr.toLowerCase()) && (isImgWithCurrentSrc || isSourceInsidePictureWithCurrentSrc)) { attrName = "_" + attrName; } let attrValue = value; if (!isAnchor && (attr.toLowerCase() === "href" || attr.toLowerCase() === "src" || attr === kCurrentSrcAttribute)) attrValue = rewriteURLForCustomProtocol(value); result.push(" ", attrName, '="', (0, import_stringUtils.escapeHTMLAttribute)(attrValue), '"'); } result.push(">"); for (const child of children) visit(child, snapshotIndex, nodeName, attrs); if (!autoClosing.has(nodeName)) result.push("</", nodeName, ">"); return; } else { return; } }; const snapshot = this._snapshot; const html = this._htmlCache.getOrCompute(this, () => { visit(snapshot.html, this._index, void 0, void 0); const prefix = snapshot.doctype ? `<!DOCTYPE ${snapshot.doctype}>` : ""; const html2 = prefix + [ // Hide the document in order to prevent flickering. We will unhide once script has processed shadow. "<style>*,*::before,*::after { visibility: hidden }</style>", `<script>${snapshotScript(this.viewport(), this._callId, this.snapshotName)}</script>` ].join("") + result.join(""); return { value: html2, size: html2.length }; }); return { html, pageId: snapshot.pageId, frameId: snapshot.frameId, index: this._index }; } resourceByUrl(url, method) { const snapshot = this._snapshot; let sameFrameResource; let otherFrameResource; for (const resource of this._resources) { if (typeof resource._monotonicTime === "number" && resource._monotonicTime >= snapshot.timestamp) break; if (resource.response.status === 304) { continue; } if (resource.request.url === url && resource.request.method === method) { if (resource._frameref === snapshot.frameId) sameFrameResource = resource; else otherFrameResource = resource; } } let result = sameFrameResource ?? otherFrameResource; if (result && method.toUpperCase() === "GET") { let override = snapshot.resourceOverrides.find((o) => o.url === url); if (override?.ref) { const index = this._index - override.ref; if (index >= 0 && index < this._snapshots.length) override = this._snapshots[index].resourceOverrides.find((o) => o.url === url); } if (override?.sha1) { result = { ...result, response: { ...result.response, content: { ...result.response.content, _sha1: override.sha1 } } }; } } return result; } } const autoClosing = /* @__PURE__ */ new Set(["AREA", "BASE", "BR", "COL", "COMMAND", "EMBED", "HR", "IMG", "INPUT", "KEYGEN", "LINK", "MENUITEM", "META", "PARAM", "SOURCE", "TRACK", "WBR"]); function snapshotNodes(snapshot) { if (!snapshot._nodes) { const nodes = []; const visit = (n) => { if (typeof n === "string") { nodes.push(n); } else if (isNodeNameAttributesChildNodesSnapshot(n)) { const [, , ...children] = n; for (const child of children) visit(child); nodes.push(n); } }; visit(snapshot.html); snapshot._nodes = nodes; } return snapshot._nodes; } function snapshotScript(viewport, ...targetIds) { function applyPlaywrightAttributes(viewport2, ...targetIds2) { const win = window; const searchParams = new URLSearchParams(win.location.search); const shouldPopulateCanvasFromScreenshot = searchParams.has("shouldPopulateCanvasFromScreenshot"); const isUnderTest = searchParams.has("isUnderTest"); const frameBoundingRectsInfo = { viewport: viewport2, frames: /* @__PURE__ */ new WeakMap() }; win["__playwright_frame_bounding_rects__"] = frameBoundingRectsInfo; const kPointerWarningTitle = "Recorded click position in absolute coordinates did not match the center of the clicked element. This is likely due to a difference between the test runner and the trace viewer operating systems."; const scrollTops = []; const scrollLefts = []; const targetElements = []; const canvasElements = []; let topSnapshotWindow = win; while (topSnapshotWindow !== topSnapshotWindow.parent && !topSnapshotWindow.location.pathname.match(/\/page@[a-z0-9]+$/)) topSnapshotWindow = topSnapshotWindow.parent; const visit = (root) => { for (const e of root.querySelectorAll(`[__playwright_scroll_top_]`)) scrollTops.push(e); for (const e of root.querySelectorAll(`[__playwright_scroll_left_]`)) scrollLefts.push(e); for (const element of root.querySelectorAll(`[__playwright_value_]`)) { const inputElement = element; if (inputElement.type !== "file") inputElement.value = inputElement.getAttribute("__playwright_value_"); element.removeAttribute("__playwright_value_"); } for (const element of root.querySelectorAll(`[__playwright_checked_]`)) { element.checked = element.getAttribute("__playwright_checked_") === "true"; element.removeAttribute("__playwright_checked_"); } for (const element of root.querySelectorAll(`[__playwright_selected_]`)) { element.selected = element.getAttribute("__playwright_selected_") === "true"; element.removeAttribute("__playwright_selected_"); } for (const element of root.querySelectorAll(`[__playwright_popover_open_]`)) { try { element.showPopover(); } catch { } element.removeAttribute("__playwright_popover_open_"); } for (const element of root.querySelectorAll(`[__playwright_dialog_open_]`)) { try { if (element.getAttribute("__playwright_dialog_open_") === "modal") element.showModal(); else element.show(); } catch { } element.removeAttribute("__playwright_dialog_open_"); } for (const targetId of targetIds2) { for (const target of root.querySelectorAll(`[__playwright_target__="${targetId}"]`)) { const style = target.style; style.outline = "2px solid #006ab1"; style.backgroundColor = "#6fa8dc7f"; targetElements.push(target); } } for (const iframe of root.querySelectorAll("iframe, frame")) { const boundingRectJson = iframe.getAttribute("__playwright_bounding_rect__"); iframe.removeAttribute("__playwright_bounding_rect__"); const boundingRect = boundingRectJson ? JSON.parse(boundingRectJson) : void 0; if (boundingRect) frameBoundingRectsInfo.frames.set(iframe, { boundingRect, scrollLeft: 0, scrollTop: 0 }); const src = iframe.getAttribute("__playwright_src__"); if (!src) { iframe.setAttribute("src", 'data:text/html,<body style="background: #ddd"></body>'); } else { const url = new URL(win.location.href); const index = url.pathname.lastIndexOf("/snapshot/"); if (index !== -1) url.pathname = url.pathname.substring(0, index + 1); url.pathname += src.substring(1); iframe.setAttribute("src", url.toString()); } } { const body = root.querySelector(`body[__playwright_custom_elements__]`); if (body && win.customElements) { const customElements = (body.getAttribute("__playwright_custom_elements__") || "").split(","); for (const elementName of customElements) win.customElements.define(elementName, class extends HTMLElement { }); } } for (const element of root.querySelectorAll(`template[__playwright_shadow_root_]`)) { const template = element; const shadowRoot = template.parentElement.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.content); template.remove(); visit(shadowRoot); } for (const element of root.querySelectorAll("a")) element.addEventListener("click", (event) => { event.preventDefault(); }); if ("adoptedStyleSheets" in root) { const adoptedSheets = [...root.adoptedStyleSheets]; for (const element of root.querySelectorAll(`template[__playwright_style_sheet_]`)) { const template = element; const sheet = new CSSStyleSheet(); sheet.replaceSync(template.getAttribute("__playwright_style_sheet_")); adoptedSheets.push(sheet); } root.adoptedStyleSheets = adoptedSheets; } canvasElements.push(...root.querySelectorAll("canvas")); }; const onLoad = () => { win.removeEventListener("load", onLoad); for (const element of scrollTops) { element.scrollTop = +element.getAttribute("__playwright_scroll_top_"); element.removeAttribute("__playwright_scroll_top_"); if (frameBoundingRectsInfo.frames.has(element)) frameBoundingRectsInfo.frames.get(element).scrollTop = element.scrollTop; } for (const element of scrollLefts) { element.scrollLeft = +element.getAttribute("__playwright_scroll_left_"); element.removeAttribute("__playwright_scroll_left_"); if (frameBoundingRectsInfo.frames.has(element)) frameBoundingRectsInfo.frames.get(element).scrollLeft = element.scrollLeft; } win.document.styleSheets[0].disabled = true; const search = new URL(win.location.href).searchParams; const isTopFrame = win === topSnapshotWindow; if (search.get("pointX") && search.get("pointY")) { const pointX = +search.get("pointX"); const pointY = +search.get("pointY"); const hasInputTarget = search.has("hasInputTarget"); const hasTargetElements = targetElements.length > 0; const roots = win.document.documentElement ? [win.document.documentElement] : []; for (const target of hasTargetElements ? targetElements : roots) { const pointElement = win.document.createElement("x-pw-pointer"); pointElement.style.position = "fixed"; pointElement.style.backgroundColor = "#f44336"; pointElement.style.width = "20px"; pointElement.style.height = "20px"; pointElement.style.borderRadius = "10px"; pointElement.style.margin = "-10px 0 0 -10px"; pointElement.style.zIndex = "2147483646"; pointElement.style.display = "flex"; pointElement.style.alignItems = "center"; pointElement.style.justifyContent = "center"; if (hasTargetElements) { const box = target.getBoundingClientRect(); const centerX = box.left + box.width / 2; const centerY = box.top + box.height / 2; pointElement.style.left = centerX + "px"; pointElement.style.top = centerY + "px"; if (isTopFrame && (Math.abs(centerX - pointX) >= 10 || Math.abs(centerY - pointY) >= 10)) { const warningElement = win.document.createElement("x-pw-pointer-warning"); warningElement.textContent = "\u26A0"; warningElement.style.fontSize = "19px"; warningElement.style.color = "white"; warningElement.style.marginTop = "-3.5px"; warningElement.style.userSelect = "none"; pointElement.appendChild(warningElement); pointElement.setAttribute("title", kPointerWarningTitle); } win.document.documentElement.appendChild(pointElement); } else if (isTopFrame && !hasInputTarget) { pointElement.style.left = pointX + "px"; pointElement.style.top = pointY + "px"; win.document.documentElement.appendChild(pointElement); } } } if (canvasElements.length > 0) { let drawCheckerboard2 = function(context, canvas) { function createCheckerboardPattern() { const pattern = win.document.createElement("canvas"); pattern.width = pattern.width / Math.floor(pattern.width / 24); pattern.height = pattern.height / Math.floor(pattern.height / 24); const context2 = pattern.getContext("2d"); context2.fillStyle = "lightgray"; context2.fillRect(0, 0, pattern.width, pattern.height); context2.fillStyle = "white"; context2.fillRect(0, 0, pattern.width / 2, pattern.height / 2); context2.fillRect(pattern.width / 2, pattern.height / 2, pattern.width, pattern.height); return context2.createPattern(pattern, "repeat"); } context.fillStyle = createCheckerboardPattern(); context.fillRect(0, 0, canvas.width, canvas.height); }; var drawCheckerboard = drawCheckerboard2; const img = new Image(); img.onload = () => { for (const canvas of canvasElements) { const context = canvas.getContext("2d"); const boundingRectAttribute = canvas.getAttribute("__playwright_bounding_rect__"); canvas.removeAttribute("__playwright_bounding_rect__"); if (!boundingRectAttribute) continue; let boundingRect; try { boundingRect = JSON.parse(boundingRectAttribute); } catch (e) { continue; } let currWindow = win; while (currWindow !== topSnapshotWindow) { const iframe = currWindow.frameElement; currWindow = currWindow.parent; const iframeInfo = currWindow["__playwright_frame_bounding_rects__"]?.frames.get(iframe); if (!iframeInfo?.boundingRect) break; const leftOffset = iframeInfo.boundingRect.left - iframeInfo.scrollLeft; const topOffset = iframeInfo.boundingRect.top - iframeInfo.scrollTop; boundingRect.left += leftOffset; boundingRect.top += topOffset; boundingRect.right += leftOffset; boundingRect.bottom += topOffset; } const { width, height } = topSnapshotWindow["__playwright_frame_bounding_rects__"].viewport; boundingRect.left = boundingRect.left / width; boundingRect.top = boundingRect.top / height; boundingRect.right = boundingRect.right / width; boundingRect.bottom = boundingRect.bottom / height; const partiallyUncaptured = boundingRect.right > 1 || boundingRect.bottom > 1; const fullyUncaptured = boundingRect.left > 1 || boundingRect.top > 1; if (fullyUncaptured) { canvas.title = `Playwright couldn't capture canvas contents because it's located outside the viewport.`; continue; } drawCheckerboard2(context, canvas); if (shouldPopulateCanvasFromScreenshot) { context.drawImage(img, boundingRect.left * img.width, boundingRect.top * img.height, (boundingRect.right - boundingRect.left) * img.width, (boundingRect.bottom - boundingRect.top) * img.height, 0, 0, canvas.width, canvas.height); if (partiallyUncaptured) canvas.title = `Playwright couldn't capture full canvas contents because it's located partially outside the viewport.`; else canvas.title = `Canvas contents are displayed on a best-effort basis based on viewport screenshots taken during test execution.`; } else { canvas.title = "Canvas content display is disabled."; } if (isUnderTest) console.log(`canvas drawn:`, JSON.stringify([boundingRect.left, boundingRect.top, boundingRect.right - boundingRect.left, boundingRect.bottom - boundingRect.top].map((v) => Math.floor(v * 100)))); } }; img.onerror = () => { for (const canvas of canvasElements) { const context = canvas.getContext("2d"); drawCheckerboard2(context, canvas); canvas.title = `Playwright couldn't show canvas contents because the screenshot failed to load.`; } }; img.src = location.href.replace("/snapshot", "/closest-screenshot"); } }; const onDOMContentLoaded = () => visit(win.document); win.addEventListener("load", onLoad); win.addEventListener("DOMContentLoaded", onDOMContentLoaded); } return ` (${applyPlaywrightAttributes.toString()})(${JSON.stringify(viewport)}${targetIds.map((id) => `, "${id}"`).join("")})`; } const schemas = ["about:", "blob:", "data:", "file:", "ftp:", "http:", "https:", "mailto:", "sftp:", "ws:", "wss:"]; const kLegacyBlobPrefix = "http://playwright.bloburl/#"; function rewriteURLForCustomProtocol(href) { if (href.startsWith(kLegacyBlobPrefix)) href = href.substring(kLegacyBlobPrefix.length); try { const url = new URL(href); if (url.protocol === "javascript:" || url.protocol === "vbscript:") return "javascript:void(0)"; const isBlob = url.protocol === "blob:"; const isFile = url.protocol === "file:"; if (!isBlob && !isFile && schemas.includes(url.protocol)) return href; const prefix = "pw-" + url.protocol.slice(0, url.protocol.length - 1); if (!isFile) url.protocol = "https:"; url.hostname = url.hostname ? `${prefix}--${url.hostname}` : prefix; if (isFile) { url.protocol = "https:"; } return url.toString(); } catch { return href; } } const urlInCSSRegex = /url\(['"]?([\w-]+:)\/\//ig; function rewriteURLsInStyleSheetForCustomProtocol(text) { return text.replace(urlInCSSRegex, (match, protocol) => { const isBlob = protocol === "blob:"; const isFile = protocol === "file:"; if (!isBlob && !isFile && schemas.includes(protocol)) return match; return match.replace(protocol + "//", `https://pw-${protocol.slice(0, -1)}--`); }); } const urlToEscapeRegex1 = /url\(\s*'([^']*)'\s*\)/ig; const urlToEscapeRegex2 = /url\(\s*"([^"]*)"\s*\)/ig; function escapeURLsInStyleSheet(text) { const replacer = (match, url) => { if (url.includes("</")) return match.replace(url, encodeURI(url)); return match; }; return text.replace(urlToEscapeRegex1, replacer).replace(urlToEscapeRegex2, replacer); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { SnapshotRenderer, rewriteURLForCustomProtocol });