diff --git a/README.md b/README.md
index 074a166b68402..f0a92d0e6db67 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,13 @@ Headless execution is supported for all browsers on all platforms. Check out [sy
Looking for Playwright for [Python](https://playwright.dev/python/docs/intro), [.NET](https://playwright.dev/dotnet/docs/intro), or [Java](https://playwright.dev/java/docs/intro)?
+## About this fork
+
+This fork supports some additional tools like:
+ - handling of a test config with baseurl/username/password for more enhanced test management
+ - making screenshots of buttons or elements
+ - writing manuals alongside testing
+
## Installation
Playwright has its own test runner for end-to-end tests, we call it Playwright Test.
diff --git a/packages/injected/src/highlight.css b/packages/injected/src/highlight.css
index 3acfd3fb1c776..2fe128b654f19 100644
--- a/packages/injected/src/highlight.css
+++ b/packages/injected/src/highlight.css
@@ -232,6 +232,11 @@ x-pw-tool-item.snapshot > x-div {
clip-path: url(#icon-gist);
}
+x-pw-tool-item.screenshot > x-div {
+ /* use inspect glyph as placeholder camera */
+ clip-path: url(#icon-screenshot);
+}
+
x-pw-tool-item.accept > x-div {
clip-path: url(#icon-check);
}
diff --git a/packages/injected/src/recorder/clipPaths.ts b/packages/injected/src/recorder/clipPaths.ts
index 1ac490843d292..f666938676548 100644
--- a/packages/injected/src/recorder/clipPaths.ts
+++ b/packages/injected/src/recorder/clipPaths.ts
@@ -27,5 +27,5 @@
import type { SvgJson } from './recorder';
// eslint-disable-next-line key-spacing, object-curly-spacing, comma-spacing, quotes
-const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gist"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"}}]}]}]};
+const svgJson: SvgJson = {"tagName":"svg","children":[{"tagName":"defs","children":[{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gripper"},"children":[{"tagName":"path","attrs":{"d":"M5 3h2v2H5zm0 4h2v2H5zm0 4h2v2H5zm4-8h2v2H9zm0 4h2v2H9zm0 4h2v2H9z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-circle-large-filled"},"children":[{"tagName":"path","attrs":{"d":"M8 1a6.8 6.8 0 0 1 1.86.253 6.899 6.899 0 0 1 3.083 1.805 6.903 6.903 0 0 1 1.804 3.083C14.916 6.738 15 7.357 15 8s-.084 1.262-.253 1.86a6.9 6.9 0 0 1-.704 1.674 7.157 7.157 0 0 1-2.516 2.509 6.966 6.966 0 0 1-1.668.71A6.984 6.984 0 0 1 8 15a6.984 6.984 0 0 1-1.86-.246 7.098 7.098 0 0 1-1.674-.711 7.3 7.3 0 0 1-1.415-1.094 7.295 7.295 0 0 1-1.094-1.415 7.098 7.098 0 0 1-.71-1.675A6.985 6.985 0 0 1 1 8c0-.643.082-1.262.246-1.86a6.968 6.968 0 0 1 .711-1.667 7.156 7.156 0 0 1 2.509-2.516 6.895 6.895 0 0 1 1.675-.704A6.808 6.808 0 0 1 8 1z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-inspect"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 3l1-1h12l1 1v6h-1V3H2v8h5v1H2l-1-1V3zm14.707 9.707L9 6v9.414l2.707-2.707h4zM10 13V8.414l3.293 3.293h-2L10 13z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-whole-word"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M0 11H1V13H15V11H16V14H15H1H0V11Z"}},{"tagName":"path","attrs":{"d":"M6.84048 11H5.95963V10.1406H5.93814C5.555 10.7995 4.99104 11.1289 4.24625 11.1289C3.69839 11.1289 3.26871 10.9839 2.95718 10.6938C2.64924 10.4038 2.49527 10.0189 2.49527 9.53906C2.49527 8.51139 3.10041 7.91341 4.3107 7.74512L5.95963 7.51416C5.95963 6.57959 5.58186 6.1123 4.82632 6.1123C4.16389 6.1123 3.56591 6.33789 3.03238 6.78906V5.88672C3.57307 5.54297 4.19612 5.37109 4.90152 5.37109C6.19416 5.37109 6.84048 6.05501 6.84048 7.42285V11ZM5.95963 8.21777L4.63297 8.40039C4.22476 8.45768 3.91682 8.55973 3.70914 8.70654C3.50145 8.84977 3.39761 9.10579 3.39761 9.47461C3.39761 9.74316 3.4925 9.96338 3.68228 10.1353C3.87564 10.3035 4.13166 10.3877 4.45035 10.3877C4.8872 10.3877 5.24706 10.2355 5.52994 9.93115C5.8164 9.62321 5.95963 9.2347 5.95963 8.76562V8.21777Z"}},{"tagName":"path","attrs":{"d":"M9.3475 10.2051H9.32601V11H8.44515V2.85742H9.32601V6.4668H9.3475C9.78076 5.73633 10.4146 5.37109 11.2489 5.37109C11.9543 5.37109 12.5057 5.61816 12.9032 6.1123C13.3042 6.60286 13.5047 7.26172 13.5047 8.08887C13.5047 9.00911 13.2809 9.74674 12.8333 10.3018C12.3857 10.8532 11.7734 11.1289 10.9964 11.1289C10.2695 11.1289 9.71989 10.821 9.3475 10.2051ZM9.32601 7.98682V8.75488C9.32601 9.20964 9.47282 9.59635 9.76644 9.91504C10.0636 10.2301 10.4396 10.3877 10.8944 10.3877C11.4279 10.3877 11.8451 10.1836 12.1458 9.77539C12.4502 9.36719 12.6024 8.79964 12.6024 8.07275C12.6024 7.46045 12.4609 6.98063 12.1781 6.6333C11.8952 6.28597 11.512 6.1123 11.0286 6.1123C10.5166 6.1123 10.1048 6.29134 9.7933 6.64941C9.48177 7.00391 9.32601 7.44971 9.32601 7.98682Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-eye"},"children":[{"tagName":"path","attrs":{"d":"M7.99993 6.00316C9.47266 6.00316 10.6666 7.19708 10.6666 8.66981C10.6666 10.1426 9.47266 11.3365 7.99993 11.3365C6.52715 11.3365 5.33324 10.1426 5.33324 8.66981C5.33324 7.19708 6.52715 6.00316 7.99993 6.00316ZM7.99993 7.00315C7.07946 7.00315 6.33324 7.74935 6.33324 8.66981C6.33324 9.59028 7.07946 10.3365 7.99993 10.3365C8.9204 10.3365 9.6666 9.59028 9.6666 8.66981C9.6666 7.74935 8.9204 7.00315 7.99993 7.00315ZM7.99993 3.66675C11.0756 3.66675 13.7307 5.76675 14.4673 8.70968C14.5344 8.97755 14.3716 9.24908 14.1037 9.31615C13.8358 9.38315 13.5643 9.22041 13.4973 8.95248C12.8713 6.45205 10.6141 4.66675 7.99993 4.66675C5.38454 4.66675 3.12664 6.45359 2.50182 8.95555C2.43491 9.22341 2.16348 9.38635 1.89557 9.31948C1.62766 9.25255 1.46471 8.98115 1.53162 8.71321C2.26701 5.76856 4.9229 3.66675 7.99993 3.66675Z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-symbol-constant"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M4 6h8v1H4V6zm8 3H4v1h8V9z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M1 4l1-1h12l1 1v8l-1 1H2l-1-1V4zm1 0v8h12V4H2z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-check"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M14.431 3.323l-8.47 10-.79-.036-3.35-4.77.818-.574 2.978 4.24 8.051-9.506.764.646z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","version":"1.1","id":"icon-screenshot","sodipodi:docname":"screenshot.svg","inkscape:version":"1.2.2 (b0a8486541, 2022-12-01)","xmlns:inkscape":"http://www.inkscape.org/namespaces/inkscape","xmlns:sodipodi":"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd","xmlns:svg":"http://www.w3.org/2000/svg"},"children":[{"tagName":"defs","attrs":{"id":"defs8"}},{"tagName":"sodipodi:namedview","attrs":{"id":"namedview6","pagecolor":"#ffffff","bordercolor":"#666666","borderopacity":"1.0","inkscape:showpageshadow":"2","inkscape:pageopacity":"0.0","inkscape:pagecheckerboard":"0","inkscape:deskcolor":"#d1d1d1","showgrid":"false","inkscape:zoom":"16","inkscape:cx":"18.34375","inkscape:cy":"2.53125","inkscape:window-width":"1920","inkscape:window-height":"1043","inkscape:window-x":"1920","inkscape:window-y":"0","inkscape:window-maximized":"1","inkscape:current-layer":"svg4"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M 1,3 2,2 h 12 l 1,1 V 9.7638863 L 14,10.693575 V 3 H 2 v 8 h 11.598395 l -1.061371,1 H 2 L 1,11 Z","id":"path2","sodipodi:nodetypes":"cccccccccccccc"}},{"tagName":"circle","attrs":{"style":"opacity:0.99;fill:#000000;fill-rule:evenodd;stroke-width:1.33333;-inkscape-stroke:none;stop-color:#000000","id":"path439","cx":"8.4392281","cy":"7","r":"3.0332131"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-close"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8 8.707l3.646 3.647.708-.707L8.707 8l3.647-3.646-.707-.708L8 7.293 4.354 3.646l-.707.708L7.293 8l-3.646 3.646.707.708L8 8.707z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-pass"},"children":[{"tagName":"path","attrs":{"d":"M6.27 10.87h.71l4.56-4.56-.71-.71-4.2 4.21-1.92-1.92L4 8.6l2.27 2.27z"}},{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M8.6 1c1.6.1 3.1.9 4.2 2 1.3 1.4 2 3.1 2 5.1 0 1.6-.6 3.1-1.6 4.4-1 1.2-2.4 2.1-4 2.4-1.6.3-3.2.1-4.6-.7-1.4-.8-2.5-2-3.1-3.5C.9 9.2.8 7.5 1.3 6c.5-1.6 1.4-2.9 2.8-3.8C5.4 1.3 7 .9 8.6 1zm.5 12.9c1.3-.3 2.5-1 3.4-2.1.8-1.1 1.3-2.4 1.2-3.8 0-1.6-.6-3.2-1.7-4.3-1-1-2.2-1.6-3.6-1.7-1.3-.1-2.7.2-3.8 1-1.1.8-1.9 1.9-2.3 3.3-.4 1.3-.4 2.7.2 4 .6 1.3 1.5 2.3 2.7 3 1.2.7 2.6.9 3.9.6z"}}]},{"tagName":"clipPath","attrs":{"width":"16","height":"16","viewBox":"0 0 16 16","fill":"currentColor","id":"icon-gist"},"children":[{"tagName":"path","attrs":{"fill-rule":"evenodd","clip-rule":"evenodd","d":"M10.57 1.14l3.28 3.3.15.36v9.7l-.5.5h-11l-.5-.5v-13l.5-.5h7.72l.35.14zM10 5h3l-3-3v3zM3 2v12h10V6H9.5L9 5.5V2H3zm2.062 7.533l1.817-1.828L6.17 7 4 9.179v.707l2.171 2.174.707-.707-1.816-1.82zM8.8 7.714l.7-.709 2.189 2.175v.709L9.5 12.062l-.705-.709 1.831-1.82L8.8 7.714z"}}]}]}]};
export default svgJson;
diff --git a/packages/injected/src/recorder/icons/screenshot.svg b/packages/injected/src/recorder/icons/screenshot.svg
new file mode 100644
index 0000000000000..d9cf53258633e
--- /dev/null
+++ b/packages/injected/src/recorder/icons/screenshot.svg
@@ -0,0 +1,48 @@
+
+
diff --git a/packages/injected/src/recorder/recorder.ts b/packages/injected/src/recorder/recorder.ts
index 3be1809fdb4a3..74b54efcc29b9 100644
--- a/packages/injected/src/recorder/recorder.ts
+++ b/packages/injected/src/recorder/recorder.ts
@@ -1054,6 +1054,7 @@ class Overlay {
private _assertTextToggle: HTMLElement;
private _assertValuesToggle: HTMLElement;
private _assertSnapshotToggle: HTMLElement;
+ private _screenshotToggle: HTMLElement;
private _offsetX = 0;
private _dragState: { offsetX: number, dragStart: { x: number, y: number } } | undefined;
private _measure: { width: number, height: number } = { width: 0, height: 0 };
@@ -1081,6 +1082,13 @@ class Overlay {
this._pickLocatorToggle.appendChild(this._recorder.document.createElement('x-div'));
toolsListElement.appendChild(this._pickLocatorToggle);
+ // Screenshot element tool (placed near pick locator for consistency with Inspector)
+ this._screenshotToggle = this._recorder.document.createElement('x-pw-tool-item');
+ this._screenshotToggle.title = 'Screenshot element';
+ this._screenshotToggle.classList.add('screenshot');
+ this._screenshotToggle.appendChild(this._recorder.document.createElement('x-div'));
+ toolsListElement.appendChild(this._screenshotToggle);
+
this._assertVisibilityToggle = this._recorder.document.createElement('x-pw-tool-item');
this._assertVisibilityToggle.title = 'Assert visibility';
this._assertVisibilityToggle.classList.add('visibility');
@@ -1152,6 +1160,11 @@ class Overlay {
if (!this._assertSnapshotToggle.classList.contains('disabled'))
this._recorder.setMode(this._recorder.state.mode === 'assertingSnapshot' ? 'recording' : 'assertingSnapshot');
}),
+ addEventListener(this._screenshotToggle, 'click', () => {
+ // Toggle dedicated screenshot mode
+ const isScreenshot = this._recorder.state.mode === 'screenshot';
+ this._recorder.setMode(isScreenshot ? 'recording' : 'screenshot');
+ }),
];
}
@@ -1167,7 +1180,9 @@ class Overlay {
setUIState(state: UIState) {
this._recordToggle.classList.toggle('toggled', state.mode === 'recording' || state.mode === 'assertingText' || state.mode === 'assertingVisibility' || state.mode === 'assertingValue' || state.mode === 'assertingSnapshot' || state.mode === 'recording-inspecting');
- this._pickLocatorToggle.classList.toggle('toggled', state.mode === 'inspecting' || state.mode === 'recording-inspecting');
+ // Do not show pick-locator as toggled in screenshot mode.
+ this._pickLocatorToggle.classList.toggle('toggled', (state.mode === 'inspecting' || state.mode === 'recording-inspecting') && state.mode !== 'screenshot');
+ this._screenshotToggle.classList.toggle('toggled', state.mode === 'screenshot');
this._assertVisibilityToggle.classList.toggle('toggled', state.mode === 'assertingVisibility');
this._assertVisibilityToggle.classList.toggle('disabled', state.mode === 'none' || state.mode === 'standby' || state.mode === 'inspecting');
this._assertTextToggle.classList.toggle('toggled', state.mode === 'assertingText');
@@ -1283,6 +1298,7 @@ export class Recorder {
'inspecting': new InspectTool(this, false),
'recording': options?.recorderMode === 'api' ? new JsonRecordActionTool(this) : new RecordActionTool(this),
'recording-inspecting': new InspectTool(this, false),
+ 'screenshot': new InspectTool(this, false),
'assertingText': new TextAssertionTool(this, 'text'),
'assertingVisibility': new InspectTool(this, true),
'assertingValue': new TextAssertionTool(this, 'value'),
@@ -1607,6 +1623,12 @@ export class Recorder {
elementPicked(selector: string, model: HighlightModel) {
const ariaSnapshot = this.injectedScript.ariaSnapshot(model.elements[0], { mode: 'expect' });
+ // In screenshot mode, generate screenshot action, then return to recording
+ if (this.state.mode === 'screenshot') {
+ this.recordAction({ name: 'screenshotElement', selector, signals: [] } as any);
+ this.setMode('recording');
+ return;
+ }
void this._delegate.elementPicked?.({ selector, ariaSnapshot });
}
}
diff --git a/packages/playwright-core/src/cli/program.ts b/packages/playwright-core/src/cli/program.ts
index 5dc18774b7d47..281dcdbbe9bed 100644
--- a/packages/playwright-core/src/cli/program.ts
+++ b/packages/playwright-core/src/cli/program.ts
@@ -70,6 +70,8 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac
['-o, --output ', 'saves the generated script to a file'],
['--target ', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()],
['--test-id-attribute ', 'use the specified attribute to generate data test ID selectors'],
+ ['--include-conf ', 'inject conf loader from this JSON and enable conf-based replacements'],
+ ['--iterate-over ', 'wrap tests in a loop over conf[field]'],
]).action(async function(url, options) {
await codegen(options, url);
}).addHelpText('afterAll', `
@@ -608,7 +610,7 @@ async function open(options: Options, url: string | undefined) {
await openPage(context, url);
}
-async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
+async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string, includeConf?: string, iterateOver?: string }, url: string | undefined) {
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
const tracesDir = path.join(os.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, {
@@ -629,6 +631,8 @@ async function codegen(options: Options & { target: string, output?: string, tes
testIdAttributeName,
outputFile: outputFile ? path.resolve(outputFile) : undefined,
handleSIGINT: false,
+ includeConfPath: options.includeConf,
+ iterateOver: options.iterateOver,
});
await openPage(context, url);
donePromise.resolve();
diff --git a/packages/playwright-core/src/server/codegen/csharp.ts b/packages/playwright-core/src/server/codegen/csharp.ts
index dc6745f73b211..0f7b69d833c23 100644
--- a/packages/playwright-core/src/server/codegen/csharp.ts
+++ b/packages/playwright-core/src/server/codegen/csharp.ts
@@ -83,7 +83,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
}
const lines: string[] = [];
- lines.push(this._generateActionCall(subject, actionInContext));
+ const line = this._generateActionCall(subject, actionInContext);
+ if (line)
+ lines.push(line);
if (signals.download) {
lines.unshift(`var download${signals.download.downloadAlias} = await ${pageAlias}.RunAndWaitForDownloadAsync(async () =>\n{`);
diff --git a/packages/playwright-core/src/server/codegen/java.ts b/packages/playwright-core/src/server/codegen/java.ts
index 19fefdd1db6d1..df333ccb62906 100644
--- a/packages/playwright-core/src/server/codegen/java.ts
+++ b/packages/playwright-core/src/server/codegen/java.ts
@@ -74,6 +74,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
}
let code = this._generateActionCall(subject, actionInContext, !!actionInContext.frame.framePath.length);
+ if (!code)
+ return formatter.format();
if (signals.popup) {
code = `Page ${signals.popup.popupAlias} = ${pageAlias}.waitForPopup(() -> {
@@ -87,7 +89,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
});`;
}
- formatter.add(code);
+ if (code)
+ formatter.add(code);
return formatter.format();
}
diff --git a/packages/playwright-core/src/server/codegen/javascript.ts b/packages/playwright-core/src/server/codegen/javascript.ts
index 9c5f7273c578f..d856342883fc6 100644
--- a/packages/playwright-core/src/server/codegen/javascript.ts
+++ b/packages/playwright-core/src/server/codegen/javascript.ts
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+import fs from 'fs';
+import path from 'path';
import { sanitizeDeviceOptions, toClickOptionsForSourceCode, toKeyboardModifiers, toSignalMap } from './language';
import { asLocator, escapeWithQuotes } from '../../utils';
import { deviceDescriptors } from '../deviceDescriptors';
@@ -28,6 +30,12 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
name: string;
highlighter = 'javascript' as Language;
private _isTest: boolean;
+ private _baseURLPrefix: string | undefined;
+ private _conf: any | undefined;
+ private _iterateOverField: string | undefined;
+ private _iterateFooterSuffix: string | undefined;
+ private _screenshotOrdinal = 0;
+ private _includeConfPath: string | undefined;
constructor(isTest: boolean) {
this.id = isTest ? 'playwright-test' : 'javascript';
@@ -45,8 +53,10 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
if (action.name === 'openPage') {
formatter.add(`const ${pageAlias} = await context.newPage();`);
- if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
- formatter.add(`await ${pageAlias}.goto(${quote(action.url)});`);
+ if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
+ this._maybeSeedBaseFrom(action.url);
+ formatter.add(`await ${pageAlias}.goto(${this._formatURL(action.url)});`);
+ }
return formatter.format();
}
@@ -95,8 +105,22 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return `await ${subject}.${this._asLocator(action.selector)}.check();`;
case 'uncheck':
return `await ${subject}.${this._asLocator(action.selector)}.uncheck();`;
- case 'fill':
+ case 'fill': {
+ const selectorLower = (action.selector || '').toLowerCase();
+ if (this._conf && typeof action.text === 'string') {
+ for (const [key, value] of Object.entries(this._conf)) {
+ if (typeof value !== 'string')
+ continue;
+ const keyLower = String(key).toLowerCase();
+ if (selectorLower.includes(keyLower) && action.text === value) {
+ const isValidIdent = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(String(key));
+ const confExpr = isValidIdent ? `conf.${key}` : `conf[${quote(String(key))}]`;
+ return `await ${subject}.${this._asLocator(action.selector)}.fill(${confExpr});`;
+ }
+ }
+ }
return `await ${subject}.${this._asLocator(action.selector)}.fill(${quote(action.text)});`;
+ }
case 'setInputFiles':
return `await ${subject}.${this._asLocator(action.selector)}.setInputFiles(${formatObject(action.files.length === 1 ? action.files[0] : action.files)});`;
case 'press': {
@@ -105,7 +129,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return `await ${subject}.${this._asLocator(action.selector)}.press(${quote(shortcut)});`;
}
case 'navigate':
- return `await ${subject}.goto(${quote(action.url)});`;
+ return `await ${subject}.goto(${this._formatURL(action.url)});`;
case 'select':
return `await ${subject}.${this._asLocator(action.selector)}.selectOption(${formatObject(action.options.length === 1 ? action.options[0] : action.options)});`;
case 'assertText':
@@ -122,6 +146,21 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
const commentIfNeeded = this._isTest ? '' : '// ';
return `${commentIfNeeded}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.ariaSnapshot, `${commentIfNeeded} `)});`;
}
+ case 'screenshotElement': {
+ const n = ++this._screenshotOrdinal;
+ const locator = `${subject}.${this._asLocator((action as any).selector)}`;
+ const pageAlias = actionInContext.frame.pageAlias;
+ const frameSelectors = actionInContext.frame.framePath;
+ const lines: string[] = [];
+ lines.push(`await (async () => {`);
+ lines.push(` const box = await ${locator}.boundingBox();`);
+ lines.push(` const padding = 30;`);
+ // Base clip from element bounding box in viewport coords.
+ lines.push(` let clip = { x: Math.max(0, box.x - padding), y: Math.max(0, box.y - padding), width: box.width + padding * 2, height: box.height + padding * 2 };`);
+ lines.push(` await ${pageAlias}.screenshot({ path: 'screenshot-${n}.png', clip });`);
+ lines.push(`})();`);
+ return lines.join('\n');
+ }
}
}
@@ -129,7 +168,82 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
return asLocator('javascript', selector);
}
+ private _canonicalBase(input: string): string | undefined {
+ try {
+ const u = new URL(input);
+ if (u.protocol !== 'http:' && u.protocol !== 'https:')
+ return undefined;
+ let base = u.origin + u.pathname;
+ if (base.endsWith('/') && base.length > u.origin.length)
+ base = base.slice(0, -1);
+ return base;
+ } catch {
+ return undefined;
+ }
+ }
+
+ private _maybeSeedBaseFrom(url: string) {
+ if (this._baseURLPrefix)
+ return;
+ const canonical = this._canonicalBase(url);
+ if (canonical)
+ this._baseURLPrefix = canonical;
+ }
+
+ // Convert URLs starting with baseURLPrefix to conf.baseurl + '/path'.
+ private _formatURL(url: string): string {
+ // Only rewrite to conf.baseurl when includeConf was provided (conf present)
+ if (!this._baseURLPrefix || !this._conf)
+ return quote(url);
+ try {
+ const u = new URL(url);
+ const abs = u.toString();
+ const prefix = this._baseURLPrefix;
+ if (abs.startsWith(prefix + '/') || abs === prefix) {
+ const rest = abs.substring(prefix.length);
+ const normalizedRest = rest.startsWith('/') ? rest : ('/' + rest);
+ return `conf.baseurl + ${quote(normalizedRest)}`;
+ }
+ } catch (e) {
+ // Not an absolute URL
+ }
+ return quote(url);
+ }
+
+ private _loadConf(confPath: string): any | undefined {
+ try {
+ const resolved = path.resolve(process.cwd(), confPath);
+ const raw = fs.readFileSync(resolved, 'utf-8');
+ return JSON.parse(raw);
+ } catch {
+ return undefined;
+ }
+ }
+
generateHeader(options: LanguageGeneratorOptions): string {
+ // Initialize base URL prefix from options if provided.
+ if (options.baseURL) {
+ const canonical = this._canonicalBase(options.baseURL);
+ if (canonical)
+ this._baseURLPrefix = canonical;
+ }
+ // Resolve CLI fallbacks for include-conf / iterate-over if not passed through options.
+ this._includeConfPath = options.includeConfPath;
+ this._iterateOverField = options.iterateOver;
+ if (!this._includeConfPath || !this._iterateOverField) {
+ const argv = process.argv || [];
+ for (let i = 0; i < argv.length; i++) {
+ const a = argv[i];
+ if (!this._includeConfPath && (a === '--include-conf' || a === '--include-conf=' || a.startsWith('--include-conf='))) {
+ this._includeConfPath = a.includes('=') ? a.split('=')[1] : argv[i + 1];
+ }
+ if (!this._iterateOverField && (a === '--iterate-over' || a === '--iterate-over=' || a.startsWith('--iterate-over='))) {
+ this._iterateOverField = a.includes('=') ? a.split('=')[1] : argv[i + 1];
+ }
+ }
+ }
+ if (this._includeConfPath)
+ this._conf = this._loadConf(this._includeConfPath);
if (this._isTest)
return this.generateTestHeader(options);
return this.generateStandaloneHeader(options);
@@ -144,10 +258,37 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
generateTestHeader(options: LanguageGeneratorOptions): string {
const formatter = new JavaScriptFormatter();
const useText = formatContextOptions(options.contextOptions, options.deviceName, this._isTest);
+ const confPath = this._includeConfPath;
+ const confLine = confPath ? `\n const conf = JSON.parse(require('fs').readFileSync(${quote(confPath)}));\n` : '';
+ let iteratePrefix = '';
+ let testNameExpr = `'test'`;
+ this._iterateFooterSuffix = '';
+ const field = this._iterateOverField;
+ if (confPath && field) {
+ const confField = this._conf ? this._conf[field] : undefined;
+ const qField = quote(field);
+ if (confField && typeof confField === 'object' && !Array.isArray(confField)) {
+ iteratePrefix = `\n for (const key of Object.keys(conf[${qField}])) {\n const ${field} = conf[${qField}][key];\n`;
+ testNameExpr = `'test-' + key`;
+ this._iterateFooterSuffix = `\n }`;
+ } else if (Array.isArray(confField)) {
+ if (confField.length > 0 && typeof confField[0] === 'string') {
+ iteratePrefix = `\n for (const ${field} of conf[${qField}]) {\n`;
+ testNameExpr = `'test-' + ${field}`;
+ this._iterateFooterSuffix = `\n }`;
+ } else {
+ iteratePrefix = `\n for (let i = 0; i < (conf[${qField}] || []).length; i++) {\n const ${field} = conf[${qField}][i];\n`;
+ testNameExpr = `'test-' + i`;
+ this._iterateFooterSuffix = `\n }`;
+ }
+ }
+ }
formatter.add(`
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
-${useText ? '\ntest.use(' + useText + ');\n' : ''}
- test('test', async ({ page }) => {`);
+
+${confLine}
+${useText ? '\ntest.use(' + useText + ');\n' : ''}${iteratePrefix}
+ test(${testNameExpr}, async ({ page }) => {`);
if (options.contextOptions.recordHar) {
const url = options.contextOptions.recordHar.urlFilter;
formatter.add(` await page.routeFromHAR(${quote(options.contextOptions.recordHar.path)}${url ? `, ${formatOptions({ url }, false)}` : ''});`);
@@ -156,7 +297,7 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
}
generateTestFooter(saveStorage: string | undefined): string {
- return `});`;
+ return `});${this._iterateFooterSuffix || ''}`;
}
generateStandaloneHeader(options: LanguageGeneratorOptions): string {
diff --git a/packages/playwright-core/src/server/codegen/python.ts b/packages/playwright-core/src/server/codegen/python.ts
index 314489441a652..10d3de3a62f3e 100644
--- a/packages/playwright-core/src/server/codegen/python.ts
+++ b/packages/playwright-core/src/server/codegen/python.ts
@@ -64,7 +64,10 @@ export class PythonLanguageGenerator implements LanguageGenerator {
if (signals.dialog)
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
- let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`;
+ const call = this._generateActionCall(subject, actionInContext);
+ if (!call)
+ return formatter.format();
+ let code = `${this._awaitPrefix}${call}`;
if (signals.popup) {
code = `${this._asyncPrefix}with ${pageAlias}.expect_popup() as ${signals.popup.popupAlias}_info {
diff --git a/packages/playwright-core/src/server/codegen/types.ts b/packages/playwright-core/src/server/codegen/types.ts
index c4d3c827bf379..7140e6dc0adde 100644
--- a/packages/playwright-core/src/server/codegen/types.ts
+++ b/packages/playwright-core/src/server/codegen/types.ts
@@ -26,6 +26,12 @@ export type LanguageGeneratorOptions = {
deviceName?: string;
saveStorage?: string;
generateAutoExpect?: boolean;
+ // Base application URL to de-hardcode (from CLI or first navigation).
+ baseURL?: string;
+ // Optional path to JSON config injected into generated tests.
+ includeConfPath?: string;
+ // Optional conf field to iterate over when generating tests.
+ iterateOver?: string;
};
export interface LanguageGenerator {
diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts
index ebd738c648e20..9b385e54f89f5 100644
--- a/packages/playwright-core/src/server/recorder.ts
+++ b/packages/playwright-core/src/server/recorder.ts
@@ -429,7 +429,8 @@ export class Recorder extends EventEmitter implements Instrume
}
private _isRecording() {
- return ['recording', 'assertingText', 'assertingVisibility', 'assertingValue', 'assertingSnapshot'].includes(this._mode);
+ // Treat screenshot mode similarly to recording for overlay/highlighting and listeners
+ return ['recording', 'assertingText', 'assertingVisibility', 'assertingValue', 'assertingSnapshot', 'screenshot'].includes(this._mode as any);
}
private _readSource(fileName: string): string {
diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts
index 5d1ffb81bd11b..1a236260eb293 100644
--- a/packages/playwright-core/src/server/recorder/recorderApp.ts
+++ b/packages/playwright-core/src/server/recorder/recorderApp.ts
@@ -67,6 +67,8 @@ export class RecorderApp {
contextOptions: { ...params.contextOptions },
deviceName: params.device,
saveStorage: params.saveStorage,
+ includeConfPath: params.includeConfPath,
+ iterateOver: (params as any).iterateOver,
};
this._throttledOutputFile = params.outputFile ? new ThrottledFile(params.outputFile) : null;
@@ -111,8 +113,11 @@ export class RecorderApp {
});
const url = this._recorder.url();
- if (url)
+ if (url) {
this._onPageNavigated(url);
+ // Seed baseURL once from the initial inspected page.
+ this._languageGeneratorOptions.baseURL = canonicalBaseURL(url);
+ }
this._onModeChanged(this._recorder.mode());
this._onPausedStateChanged(this._recorder.paused());
this._updateActions('reveal');
@@ -129,6 +134,19 @@ export class RecorderApp {
this._recorder.clear();
return;
}
+ if (data.event === 'recordScreenshot') {
+ const selector: string = data.params.selector;
+ const last = this._actions[this._actions.length - 1];
+ const frame = last ? last.frame : { pageGuid: '', pageAlias: 'page', framePath: [] };
+ const action: actions.ActionInContext = {
+ frame,
+ action: { name: 'screenshotElement', selector, signals: [] } as any,
+ startTime: Date.now(),
+ };
+ this._actions.push(action);
+ this._updateActions('reveal');
+ return;
+ }
if (data.event === 'fileChanged') {
const source = [...this._recorderSources, ...this._userSources].find(s => s.id === data.params.fileId);
if (source) {
@@ -283,6 +301,12 @@ export class RecorderApp {
}
private _onPageNavigated(url: string) {
+ // Seed baseURL from the first real navigation if not set yet.
+ if (!this._languageGeneratorOptions.baseURL) {
+ const base = canonicalBaseURL(url);
+ if (base)
+ this._languageGeneratorOptions.baseURL = base;
+ }
this._page.mainFrame().evaluateExpression((({ url }: { url: string }) => {
window.playwrightSetPageURL(url);
}).toString(), { isFunction: true }, { url }).catch(() => {});
@@ -395,6 +419,7 @@ export class ProgrammaticRecorderApp {
contextOptions: { ...params.contextOptions },
deviceName: params.device,
saveStorage: params.saveStorage,
+ baseURL: canonicalBaseURL(inspectedContext.pages()[0]?.mainFrame().url() || ''),
};
const languageGenerator = languages.find(l => l.id === params.language) ?? languages.find(l => l.id === 'playwright-test')!;
@@ -421,3 +446,21 @@ function findPageByGuid(context: BrowserContext, guid: string) {
}
const recorderAppSymbol = Symbol('recorderApp');
+
+function canonicalBaseURL(url: string | undefined): string | undefined {
+ if (!url)
+ return undefined;
+ try {
+ const u = new URL(url);
+ if (u.protocol !== 'http:' && u.protocol !== 'https:')
+ return undefined;
+ // Keep origin + path prefix as provided; normalize trailing slash off.
+ let base = u.origin + u.pathname;
+ // Remove trailing slash for consistent prefix checks.
+ if (base.endsWith('/') && base.length > u.origin.length)
+ base = base.slice(0, -1);
+ return base;
+ } catch {
+ return undefined;
+ }
+}
diff --git a/packages/recorder/src/actions.d.ts b/packages/recorder/src/actions.d.ts
index 4f1a9bb353198..a29ac669ff09f 100644
--- a/packages/recorder/src/actions.d.ts
+++ b/packages/recorder/src/actions.d.ts
@@ -31,7 +31,8 @@ export type ActionName =
'assertValue' |
'assertChecked' |
'assertVisible' |
- 'assertSnapshot';
+ 'assertSnapshot' |
+ 'screenshotElement';
export type ActionBase = {
name: ActionName,
@@ -121,7 +122,11 @@ export type AssertSnapshotAction = ActionWithSelector & {
ariaSnapshot: string,
};
-export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction;
+export type ScreenshotElementAction = ActionWithSelector & {
+ name: 'screenshotElement',
+};
+
+export type Action = ClickAction | CheckAction | ClosesPageAction | OpenPageAction | UncheckAction | FillAction | NavigateAction | PressAction | SelectAction | SetInputFilesAction | AssertTextAction | AssertValueAction | AssertCheckedAction | AssertVisibleAction | AssertSnapshotAction | ScreenshotElementAction;
export type AssertAction = AssertCheckedAction | AssertValueAction | AssertTextAction | AssertVisibleAction | AssertSnapshotAction;
export type PerformOnRecordAction = ClickAction | CheckAction | UncheckAction | PressAction | SelectAction;
diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx
index de464bec8a83e..16fa6da72bd4f 100644
--- a/packages/recorder/src/recorder.tsx
+++ b/packages/recorder/src/recorder.tsx
@@ -70,6 +70,11 @@ export const Recorder: React.FC = ({
setLocator(asLocator(language, elementInfo.selector));
setAriaSnapshot(elementInfo.ariaSnapshot);
setAriaSnapshotErrors([]);
+ if (mode === 'screenshot') {
+ window.dispatch({ event: 'recordScreenshot', params: { selector: elementInfo.selector } });
+ window.dispatch({ event: 'setMode', params: { mode: 'recording' } }).catch(() => { });
+ return;
+ }
if (userGesture && selectedTab !== 'locator' && selectedTab !== 'aria')
setSelectedTab('locator');
@@ -139,7 +144,7 @@ export const Recorder: React.FC = ({
window.dispatch({ event: 'setMode', params: { mode: mode === 'none' || mode === 'standby' || mode === 'inspecting' ? 'recording' : 'standby' } });
}}>Record
- {
+ {
const newMode = {
'inspecting': 'standby',
'none': 'inspecting',
@@ -153,6 +158,9 @@ export const Recorder: React.FC = ({
}[mode];
window.dispatch({ event: 'setMode', params: { mode: newMode } }).catch(() => { });
}}>
+ {
+ window.dispatch({ event: 'setMode', params: { mode: mode === 'screenshot' ? 'recording' : 'screenshot' } }).catch(() => { });
+ }}>
{
window.dispatch({ event: 'setMode', params: { mode: mode === 'assertingVisibility' ? 'recording' : 'assertingVisibility' } });
}}>
diff --git a/packages/recorder/src/recorderTypes.d.ts b/packages/recorder/src/recorderTypes.d.ts
index 2f5b0ffe4e817..68ad365b1773c 100644
--- a/packages/recorder/src/recorderTypes.d.ts
+++ b/packages/recorder/src/recorderTypes.d.ts
@@ -29,6 +29,7 @@ export type Mode =
| 'assertingVisibility'
| 'assertingValue'
| 'assertingSnapshot';
+ | 'screenshot';
export type ElementInfo = {
selector: string;
@@ -43,7 +44,8 @@ export type EventData = {
| 'pause'
| 'setMode'
| 'highlightRequested'
- | 'languageChanged';
+ | 'languageChanged'
+ | 'recordScreenshot';
params: any;
};
diff --git a/utils/generate_clip_paths.js b/utils/generate_clip_paths.js
index 210a8405818f8..72fdb80457c9f 100644
--- a/utils/generate_clip_paths.js
+++ b/utils/generate_clip_paths.js
@@ -62,6 +62,7 @@ const iconNames = [
'eye',
'symbol-constant',
'check',
+ 'screenshot',
'close',
'pass',
'gist',