diff --git a/docs/src/test-fixtures-js.md b/docs/src/test-fixtures-js.md index a94ce660a3..1d19d0acdb 100644 --- a/docs/src/test-fixtures-js.md +++ b/docs/src/test-fixtures-js.md @@ -115,18 +115,13 @@ test.describe('todo tests', () => { ### With fixtures Fixtures have a number of advantages over before/after hooks: -- Fixtures **encapsulate** setup and teardown in the same place so it is easier to write. -- Fixtures are **reusable** between test files - you can define them once and use in all your tests. That's how Playwright's built-in `page` fixture works. +- Fixtures **encapsulate** setup and teardown in the same place so it is easier to write. So if you have an after hook that tears down what was created in a before hook, consider turning them into a fixture. +- Fixtures are **reusable** between test files - you can define them once and use in all your tests. That's how Playwright's built-in `page` fixture works. So if you have a helper function that is used in multiple tests, consider turning it into a fixture. - Fixtures are **on-demand** - you can define as many fixtures as you'd like, and Playwright Test will setup only the ones needed by your test and nothing else. - Fixtures are **composable** - they can depend on each other to provide complex behaviors. - Fixtures are **flexible**. Tests can use any combinations of the fixtures to tailor precise environment they need, without affecting other tests. - Fixtures simplify **grouping**. You no longer need to wrap tests in `describe`s that set up environment, and are free to group your tests by their meaning instead. -Three good signs that something is better as a fixture than as a helper function or before/after hook are: -1. The after hook performs cleanup, or the helper function returns a cleanup helper. -2. There's dependencies between the helper functions, with some helper functions being commonly reused. -3. The before hook or helper function create an object that is "ready to use", similar to the built-in `page` fixture. -
Click to expand the code for the TodoPage
diff --git a/packages/trace-viewer/public/sw.bundle.js b/packages/trace-viewer/public/sw.bundle.js new file mode 100644 index 0000000000..6e6238e49b --- /dev/null +++ b/packages/trace-viewer/public/sw.bundle.js @@ -0,0 +1,7648 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +function splitProgress(progress, weights) { + const doneList = new Array(weights.length).fill(0); + return new Array(weights.length).fill(0).map((_, i) => { + return (done, total) => { + doneList[i] = done / total * weights[i] * 1e3; + progress(doneList.reduce((a, b) => a + b, 0), 1e3); + }; + }); +} +const escaped = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }; +function escapeHTMLAttribute(s) { + return s.replace(/[&<>"']/ug, (char) => escaped[char]); +} +function escapeHTML(s) { + return s.replace(/[&<]/ug, (char) => escaped[char]); +} +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) { + __publicField(this, "_htmlCache"); + __publicField(this, "_snapshots"); + __publicField(this, "_index"); + __publicField(this, "snapshotName"); + __publicField(this, "_resources"); + __publicField(this, "_snapshot"); + __publicField(this, "_callId"); + __publicField(this, "_screencastFrames"); + 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() { + var _a; + const { wallTime, timestamp } = this.snapshot(); + const closestFrame = wallTime && ((_a = this._screencastFrames[0]) == null ? void 0 : _a.frameSwapWallTime) ? findClosest(this._screencastFrames, (frame) => frame.frameSwapWallTime, wallTime) : findClosest(this._screencastFrames, (frame) => frame.timestamp, timestamp); + return closestFrame == null ? void 0 : closestFrame.sha1; + } + render() { + const result = []; + const visit = (n, snapshotIndex, parentTag, parentAttrs) => { + if (typeof n === "string") { + if (parentTag === "STYLE" || parentTag === "style") + result.push(rewriteURLsInStyleSheetForCustomProtocol(n)); + else + result.push(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 == null ? void 0 : 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") + attrValue = "link://" + value; + else if (attr.toLowerCase() === "href" || attr.toLowerCase() === "src" || attr === kCurrentSrcAttribute) + attrValue = rewriteURLForCustomProtocol(value); + result.push(" ", attrName, '="', escapeHTMLAttribute(attrValue), '"'); + } + result.push(">"); + for (const child of children) + visit(child, snapshotIndex, nodeName, attrs); + if (!autoClosing.has(nodeName)) + result.push(""); + 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 ? `` : ""; + const html2 = prefix + [ + // Hide the document in order to prevent flickering. We will unhide once script has processed shadow. + "", + `