screenshotter.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. "use strict";
  2. var __defProp = Object.defineProperty;
  3. var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  4. var __getOwnPropNames = Object.getOwnPropertyNames;
  5. var __hasOwnProp = Object.prototype.hasOwnProperty;
  6. var __export = (target, all) => {
  7. for (var name in all)
  8. __defProp(target, name, { get: all[name], enumerable: true });
  9. };
  10. var __copyProps = (to, from, except, desc) => {
  11. if (from && typeof from === "object" || typeof from === "function") {
  12. for (let key of __getOwnPropNames(from))
  13. if (!__hasOwnProp.call(to, key) && key !== except)
  14. __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  15. }
  16. return to;
  17. };
  18. var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
  19. var screenshotter_exports = {};
  20. __export(screenshotter_exports, {
  21. Screenshotter: () => Screenshotter,
  22. validateScreenshotOptions: () => validateScreenshotOptions
  23. });
  24. module.exports = __toCommonJS(screenshotter_exports);
  25. var import_helper = require("./helper");
  26. var import_utils = require("../utils");
  27. var import_multimap = require("../utils/isomorphic/multimap");
  28. function inPagePrepareForScreenshots(screenshotStyle, hideCaret, disableAnimations, syncAnimations) {
  29. if (syncAnimations) {
  30. const style = document.createElement("style");
  31. style.textContent = "body {}";
  32. document.head.appendChild(style);
  33. document.documentElement.getBoundingClientRect();
  34. style.remove();
  35. }
  36. if (!screenshotStyle && !hideCaret && !disableAnimations)
  37. return;
  38. const collectRoots = (root, roots2 = []) => {
  39. roots2.push(root);
  40. const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
  41. do {
  42. const node = walker.currentNode;
  43. const shadowRoot = node instanceof Element ? node.shadowRoot : null;
  44. if (shadowRoot)
  45. collectRoots(shadowRoot, roots2);
  46. } while (walker.nextNode());
  47. return roots2;
  48. };
  49. const roots = collectRoots(document);
  50. const cleanupCallbacks = [];
  51. if (screenshotStyle) {
  52. for (const root of roots) {
  53. const styleTag = document.createElement("style");
  54. styleTag.textContent = screenshotStyle;
  55. if (root === document)
  56. document.documentElement.append(styleTag);
  57. else
  58. root.append(styleTag);
  59. cleanupCallbacks.push(() => {
  60. styleTag.remove();
  61. });
  62. }
  63. }
  64. if (hideCaret) {
  65. const elements = /* @__PURE__ */ new Map();
  66. for (const root of roots) {
  67. root.querySelectorAll("input,textarea,[contenteditable]").forEach((element) => {
  68. elements.set(element, {
  69. value: element.style.getPropertyValue("caret-color"),
  70. priority: element.style.getPropertyPriority("caret-color")
  71. });
  72. element.style.setProperty("caret-color", "transparent", "important");
  73. });
  74. }
  75. cleanupCallbacks.push(() => {
  76. for (const [element, value] of elements)
  77. element.style.setProperty("caret-color", value.value, value.priority);
  78. });
  79. }
  80. if (disableAnimations) {
  81. const infiniteAnimationsToResume = /* @__PURE__ */ new Set();
  82. const handleAnimations = (root) => {
  83. for (const animation of root.getAnimations()) {
  84. if (!animation.effect || animation.playbackRate === 0 || infiniteAnimationsToResume.has(animation))
  85. continue;
  86. const endTime = animation.effect.getComputedTiming().endTime;
  87. if (Number.isFinite(endTime)) {
  88. try {
  89. animation.finish();
  90. } catch (e) {
  91. }
  92. } else {
  93. try {
  94. animation.cancel();
  95. infiniteAnimationsToResume.add(animation);
  96. } catch (e) {
  97. }
  98. }
  99. }
  100. };
  101. for (const root of roots) {
  102. const handleRootAnimations = handleAnimations.bind(null, root);
  103. handleRootAnimations();
  104. root.addEventListener("transitionrun", handleRootAnimations);
  105. root.addEventListener("animationstart", handleRootAnimations);
  106. cleanupCallbacks.push(() => {
  107. root.removeEventListener("transitionrun", handleRootAnimations);
  108. root.removeEventListener("animationstart", handleRootAnimations);
  109. });
  110. }
  111. cleanupCallbacks.push(() => {
  112. for (const animation of infiniteAnimationsToResume) {
  113. try {
  114. animation.play();
  115. } catch (e) {
  116. }
  117. }
  118. });
  119. }
  120. window.__pwCleanupScreenshot = () => {
  121. for (const cleanupCallback of cleanupCallbacks)
  122. cleanupCallback();
  123. delete window.__pwCleanupScreenshot;
  124. };
  125. }
  126. class Screenshotter {
  127. constructor(page) {
  128. this._queue = new TaskQueue();
  129. this._page = page;
  130. this._queue = new TaskQueue();
  131. }
  132. async _originalViewportSize(progress) {
  133. let viewportSize = this._page.emulatedSize()?.viewport;
  134. if (!viewportSize)
  135. viewportSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ width: window.innerWidth, height: window.innerHeight }));
  136. return viewportSize;
  137. }
  138. async _fullPageSize(progress) {
  139. const fullPageSize = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => {
  140. if (!document.body || !document.documentElement)
  141. return null;
  142. return {
  143. width: Math.max(
  144. document.body.scrollWidth,
  145. document.documentElement.scrollWidth,
  146. document.body.offsetWidth,
  147. document.documentElement.offsetWidth,
  148. document.body.clientWidth,
  149. document.documentElement.clientWidth
  150. ),
  151. height: Math.max(
  152. document.body.scrollHeight,
  153. document.documentElement.scrollHeight,
  154. document.body.offsetHeight,
  155. document.documentElement.offsetHeight,
  156. document.body.clientHeight,
  157. document.documentElement.clientHeight
  158. )
  159. };
  160. });
  161. return fullPageSize;
  162. }
  163. async screenshotPage(progress, options) {
  164. const format = validateScreenshotOptions(options);
  165. return this._queue.postTask(async () => {
  166. progress.log("taking page screenshot");
  167. const viewportSize = await this._originalViewportSize(progress);
  168. await this._preparePageForScreenshot(progress, this._page.mainFrame(), options.style, options.caret !== "initial", options.animations === "disabled");
  169. try {
  170. if (options.fullPage) {
  171. const fullPageSize = await this._fullPageSize(progress);
  172. let documentRect = { x: 0, y: 0, width: fullPageSize.width, height: fullPageSize.height };
  173. const fitsViewport = fullPageSize.width <= viewportSize.width && fullPageSize.height <= viewportSize.height;
  174. if (options.clip)
  175. documentRect = trimClipToSize(options.clip, documentRect);
  176. return await this._screenshot(progress, format, documentRect, void 0, fitsViewport, options);
  177. }
  178. const viewportRect = options.clip ? trimClipToSize(options.clip, viewportSize) : { x: 0, y: 0, ...viewportSize };
  179. return await this._screenshot(progress, format, void 0, viewportRect, true, options);
  180. } finally {
  181. await this._restorePageAfterScreenshot();
  182. }
  183. });
  184. }
  185. async screenshotElement(progress, handle, options) {
  186. const format = validateScreenshotOptions(options);
  187. return this._queue.postTask(async () => {
  188. progress.log("taking element screenshot");
  189. const viewportSize = await this._originalViewportSize(progress);
  190. await this._preparePageForScreenshot(progress, handle._frame, options.style, options.caret !== "initial", options.animations === "disabled");
  191. try {
  192. await handle._waitAndScrollIntoViewIfNeeded(
  193. progress,
  194. true
  195. /* waitForVisible */
  196. );
  197. const boundingBox = await progress.race(handle.boundingBox());
  198. (0, import_utils.assert)(boundingBox, "Node is either not visible or not an HTMLElement");
  199. (0, import_utils.assert)(boundingBox.width !== 0, "Node has 0 width.");
  200. (0, import_utils.assert)(boundingBox.height !== 0, "Node has 0 height.");
  201. const fitsViewport = boundingBox.width <= viewportSize.width && boundingBox.height <= viewportSize.height;
  202. const scrollOffset = await this._page.mainFrame().waitForFunctionValueInUtility(progress, () => ({ x: window.scrollX, y: window.scrollY }));
  203. const documentRect = { ...boundingBox };
  204. documentRect.x += scrollOffset.x;
  205. documentRect.y += scrollOffset.y;
  206. return await this._screenshot(progress, format, import_helper.helper.enclosingIntRect(documentRect), void 0, fitsViewport, options);
  207. } finally {
  208. await this._restorePageAfterScreenshot();
  209. }
  210. });
  211. }
  212. async _preparePageForScreenshot(progress, frame, screenshotStyle, hideCaret, disableAnimations) {
  213. if (disableAnimations)
  214. progress.log(" disabled all CSS animations");
  215. const syncAnimations = this._page.delegate.shouldToggleStyleSheetToSyncAnimations();
  216. await progress.race(this._page.safeNonStallingEvaluateInAllFrames("(" + inPagePrepareForScreenshots.toString() + `)(${JSON.stringify(screenshotStyle)}, ${hideCaret}, ${disableAnimations}, ${syncAnimations})`, "utility"));
  217. try {
  218. if (!process.env.PW_TEST_SCREENSHOT_NO_FONTS_READY) {
  219. progress.log("waiting for fonts to load...");
  220. await progress.race(frame.nonStallingEvaluateInExistingContext("document.fonts.ready", "utility").catch(() => {
  221. }));
  222. progress.log("fonts loaded");
  223. }
  224. } catch (error) {
  225. await this._restorePageAfterScreenshot();
  226. throw error;
  227. }
  228. }
  229. async _restorePageAfterScreenshot() {
  230. await this._page.safeNonStallingEvaluateInAllFrames("window.__pwCleanupScreenshot && window.__pwCleanupScreenshot()", "utility");
  231. }
  232. async _maskElements(progress, options) {
  233. if (!options.mask || !options.mask.length)
  234. return () => Promise.resolve();
  235. const framesToParsedSelectors = new import_multimap.MultiMap();
  236. await progress.race(Promise.all((options.mask || []).map(async ({ frame, selector }) => {
  237. const pair = await frame.selectors.resolveFrameForSelector(selector);
  238. if (pair)
  239. framesToParsedSelectors.set(pair.frame, pair.info.parsed);
  240. })));
  241. const frames = [...framesToParsedSelectors.keys()];
  242. const cleanup = async () => {
  243. await Promise.all(frames.map((frame) => frame.hideHighlight()));
  244. };
  245. try {
  246. const promises = frames.map((frame) => frame.maskSelectors(framesToParsedSelectors.get(frame), options.maskColor || "#F0F"));
  247. await progress.race(Promise.all(promises));
  248. return cleanup;
  249. } catch (error) {
  250. cleanup().catch(() => {
  251. });
  252. throw error;
  253. }
  254. }
  255. async _screenshot(progress, format, documentRect, viewportRect, fitsViewport, options) {
  256. if (options.__testHookBeforeScreenshot)
  257. await progress.race(options.__testHookBeforeScreenshot());
  258. const shouldSetDefaultBackground = options.omitBackground && format === "png";
  259. if (shouldSetDefaultBackground)
  260. await progress.race(this._page.delegate.setBackgroundColor({ r: 0, g: 0, b: 0, a: 0 }));
  261. const cleanupHighlight = await this._maskElements(progress, options);
  262. try {
  263. const quality = format === "jpeg" ? options.quality ?? 80 : void 0;
  264. const buffer = await this._page.delegate.takeScreenshot(progress, format, documentRect, viewportRect, quality, fitsViewport, options.scale || "device");
  265. await cleanupHighlight();
  266. if (shouldSetDefaultBackground)
  267. await this._page.delegate.setBackgroundColor();
  268. if (options.__testHookAfterScreenshot)
  269. await progress.race(options.__testHookAfterScreenshot());
  270. return buffer;
  271. } catch (error) {
  272. cleanupHighlight().catch(() => {
  273. });
  274. if (shouldSetDefaultBackground)
  275. this._page.delegate.setBackgroundColor().catch(() => {
  276. });
  277. throw error;
  278. }
  279. }
  280. }
  281. class TaskQueue {
  282. constructor() {
  283. this._chain = Promise.resolve();
  284. }
  285. postTask(task) {
  286. const result = this._chain.then(task);
  287. this._chain = result.catch(() => {
  288. });
  289. return result;
  290. }
  291. }
  292. function trimClipToSize(clip, size) {
  293. const p1 = {
  294. x: Math.max(0, Math.min(clip.x, size.width)),
  295. y: Math.max(0, Math.min(clip.y, size.height))
  296. };
  297. const p2 = {
  298. x: Math.max(0, Math.min(clip.x + clip.width, size.width)),
  299. y: Math.max(0, Math.min(clip.y + clip.height, size.height))
  300. };
  301. const result = { x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y };
  302. (0, import_utils.assert)(result.width && result.height, "Clipped area is either empty or outside the resulting image");
  303. return result;
  304. }
  305. function validateScreenshotOptions(options) {
  306. let format = null;
  307. if (options.type) {
  308. (0, import_utils.assert)(options.type === "png" || options.type === "jpeg", "Unknown options.type value: " + options.type);
  309. format = options.type;
  310. }
  311. if (!format)
  312. format = "png";
  313. if (options.quality !== void 0) {
  314. (0, import_utils.assert)(format === "jpeg", "options.quality is unsupported for the " + format + " screenshots");
  315. (0, import_utils.assert)(typeof options.quality === "number", "Expected options.quality to be a number but found " + typeof options.quality);
  316. (0, import_utils.assert)(Number.isInteger(options.quality), "Expected options.quality to be an integer");
  317. (0, import_utils.assert)(options.quality >= 0 && options.quality <= 100, "Expected options.quality to be between 0 and 100 (inclusive), got " + options.quality);
  318. }
  319. if (options.clip) {
  320. (0, import_utils.assert)(typeof options.clip.x === "number", "Expected options.clip.x to be a number but found " + typeof options.clip.x);
  321. (0, import_utils.assert)(typeof options.clip.y === "number", "Expected options.clip.y to be a number but found " + typeof options.clip.y);
  322. (0, import_utils.assert)(typeof options.clip.width === "number", "Expected options.clip.width to be a number but found " + typeof options.clip.width);
  323. (0, import_utils.assert)(typeof options.clip.height === "number", "Expected options.clip.height to be a number but found " + typeof options.clip.height);
  324. (0, import_utils.assert)(options.clip.width !== 0, "Expected options.clip.width not to be 0.");
  325. (0, import_utils.assert)(options.clip.height !== 0, "Expected options.clip.height not to be 0.");
  326. }
  327. return format;
  328. }
  329. // Annotate the CommonJS export names for ESM import in node:
  330. 0 && (module.exports = {
  331. Screenshotter,
  332. validateScreenshotOptions
  333. });