docs(test): document mouse-outside-viewport screenshot helper - #42446
docs(test): document mouse-outside-viewport screenshot helper#42446Karl Horky (karlhorky) wants to merge 5 commits into
Conversation
| }); | ||
| ``` | ||
|
|
||
| </details> |
There was a problem hiding this comment.
In case the helper function seems like too much, a simpler alternative without a helper function could be this:
await page.mouse.move(-1, -1);
await expect(page).toHaveScreenshot();There was a problem hiding this comment.
Pull request overview
Updates Playwright Test visual comparison documentation to explain how preserved mouse position can unintentionally introduce hover states into screenshot assertions, and provides a reusable helper pattern to mitigate it.
Changes:
- Document why mouse position persistence can affect
expect(page).toHaveScreenshot()results after navigations/rerenders. - Add a collapsible TypeScript helper that moves the mouse to
(0, 0)before taking named screenshots. - Show an example of forwarding
toHaveScreenshot()options through the helper.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| screenshotName: string, | ||
| options?: PageAssertionsToHaveScreenshotOptions, | ||
| ) { | ||
| await page.mouse.move(0, 0); |
There was a problem hiding this comment.
(0, 0) is still inside the viewport and commonly hovers a header or any other full width element
(-1, -1) reliably clears hover across all three browsers and matches Playwright’s internal reset behavior
There was a problem hiding this comment.
Oh interesting, I didn't find that position in the codebase - maybe I didn't look hard enough
Done in e158a8f
|
|
||
| export async function expectPageToHaveScreenshotWithMouseAtOrigin( | ||
| page: Page, | ||
| screenshotName: string, |
There was a problem hiding this comment.
the helper unnecessarily excludes the supported ReadonlyArray<string>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Karl Horky <karl.horky@gmail.com>
Signed-off-by: Karl Horky <karl.horky@gmail.com>
Dmitry Gozman (dgozman)
left a comment
There was a problem hiding this comment.
I don't think we want this. There are so many tricks you can employ for better screenshot testing, not sure why this one stands out. The place for it is too prominent - I don't think anyone learning about visual comparisons has to think about this right away. We cannot guarantee that moving to (-1, -1) works consistently to one's expectations across browsers. I am also not sure that the suggested wrapper is the way to go - I personally would prefer explicit mouse.move() calls in the test, or make a custom expect assertion.
I think it's a pretty confusing behavior for users, and the official recommendation from the Playwright team has been to move the mouse (but these details are buried). This can make users and teams lose a lot of time. I think that makes it a good candidate for documentation. Actually, I would personally lean towards Playwright changing the
Yes, that's ok for 1-off calls, but this mouse reset behavior should be the default, so recommending users write 2 lines everywhere they use
Yeah that was my other version - I ended up discarding because it was recreating too much of the import {
expect as baseExpect,
type Page,
type PageAssertionsToHaveScreenshotOptions,
} from '@playwright/test';
export { test } from '@playwright/test';
export const expect = baseExpect.extend({
async toHaveScreenshotWithMouseOutsideViewport(
page: Page,
screenshotName: string | ReadonlyArray<string>,
options?: PageAssertionsToHaveScreenshotOptions,
) {
await page.mouse.move(-1, -1);
let pass: boolean;
let matcherResult: any;
let message = '';
try {
const expectation = this.isNot
? baseExpect(page).not
: baseExpect(page);
await expectation.toHaveScreenshot(screenshotName, options);
pass = true;
} catch (error: any) {
matcherResult = error.matcherResult;
message = error.message;
pass = false;
}
if (this.isNot) {
pass = !pass;
}
return {
message: () => message,
pass,
name: 'toHaveScreenshotWithMouseOutsideViewport',
expected: screenshotName,
actual: matcherResult?.actual,
log: matcherResult?.log,
timeout: matcherResult?.timeout,
};
},
}); |
|
I think the magnitude of the problem is overestimated. Over many years, there have been just a few people how ran into this. I'd say things work very much as expected today. Whatever is hovered on the page will be captured by the screenshot - that's what the user sees. In fact, automatic mouse reset would just break people that screenshot on-hover popups or similar things. I understand that's not what you want, but that does not mean everyone wants that. Let's start with filing an issue, and see how popular that would be. Given the past history, that seems unlikely though. |
This is only based on public GitHub reports right? Not the people who didn't report it because they just accepted whatever snapshot Playwright gave them. I usually review any snapshots carefully, and I also missed an erroneous hover in a snapshot that I had - it was definitely wrong and it was caused by Playwright's stale mouse position behavior, but it was hard to notice. And if the "normal" styling effect would have changed for that element, the snapshot would not have caught it. So in these cases, the snapshot is worthless for that element (but not known to be worthless).
That would be fine, for same-page behavior. But the whole reason for this PR is subsequent navigations: the stale mouse positions persist across page navigations (state leaking across unexpected boundaries). The argument that "this is how a normal browser operates" is maybe idealistically correct, but this doesn't mean much to someone who is debugging why their element appears hovered in the snapshot after 3 subsequent page navigations. Also, Playwright already papers over a lot of the sharp edges of how a normal browser operates for the purpose of testing - I'm not sure why this would be an exception.
Maybe a middle ground that would resolve this issue and also the on-hover popup snapshots would be this issue instead: Issue Title: Reset mouse position to Playwright internal reset location after navigations Instead of changing the Happy to file either this one or the previous idea of adding a In the meantime, what should we do with this PR? Would you accept a short 2-line code block with just the |
Mouse actions leave the pointer at their last position. If a navigation or re-render places another element under that position,
expect(page).toHaveScreenshot()can capture an unrelated hover state.Playwright keeps this browser behavior as-is in Firefox:
Chromium also preserves the last mouse position:
This adds concise guidance to the Visual comparisons page and a collapsed helper for repeated named screenshots. The helper moves the mouse to
-1, -1before callingtoHaveScreenshot()and forwards its screenshot options.The documentation notes that projects should choose another coordinate when an element at the viewport origin responds to hover.