Repository navigation
feat(browser): introduce experimental pointer API - #10780
Conversation
d442819 to
0f796cd
Compare
✅ Deploy Preview for vitest-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
I did some work here #6026 if you want to take a look, map the modifiers will be a pain iirc 🤔 |
Yes, following the same API as Testing Library makes it difficult. That's why I changed it a bit (not having to use I might use some of the tests in your PR 😄 |
5e9cdcf to
8bbb4c8
Compare
|
Todo: check if Playwright allows resizing a textarea/resizable element. |
sheremet-va
left a comment
There was a problem hiding this comment.
The implementation looks good to me. Answered TODOs, I think we can go with the documentation now
bc617b5 to
dbfed0c
Compare
|
Moved to Testing Library's API, there's a bit of gluing needed to have it work with Playwright, so there's no 100% API compatibility. I'll have to document what's supported and what behaves differently. Not sure if this can still make it in time for v5, but I'd like to release it as an experimental feature anyway, so there's no rush on my end. Still need to solve some todos and check UE's tests to see if any are worth porting. |
cd61f6a to
46a13ac
Compare
| async pointer(options) { | ||
| // @todo | ||
| const _ = options | ||
| // await userEvent.pointer() | ||
| }, |
There was a problem hiding this comment.
There's an issue with locators here which I'm not sure how to solve... We can't resolve them before-hand because interactions might insert them in the DOM, maybe a getter on target could work 🤔
There was a problem hiding this comment.
Implemented target as a getter when it's a Locator.
macarie
left a comment
There was a problem hiding this comment.
I don't think user-event has any tests worth adding, most of them seem to check that it follows the "spec", which we don't need to.
| // this file should not import anything directly, only types and utils | ||
|
|
||
| interface PointerState extends Pick<UserEventPointerInputNormalized[number], 'coords' | 'target'> { | ||
| unreleased?: string[] |
There was a problem hiding this comment.
Was thinking that this might get merged into keyboard, not sure tho as it would allow doing weird stuff like
userEvent.keyboard(target, '[Shift>]')
userEvent.pointer([{ target, keys: '[MouseLeft][/Shift]' }])There was a problem hiding this comment.
Merged the unreleased part of pointer into the keyboard management.
pointer APIpointer API
| This method is intended for testing UI that explicitly listens to `wheel` events (e.g., custom zoom controls, horizontal tab scrolling, canvas interactions). If you need to scroll the page to bring an element into view, rely on the built-in automatic scrolling functionality provided by other `userEvent` methods or [locator actions](/api/browser/locators#methods) instead. | ||
| ::: | ||
|
|
||
| ## userEvent.pointer <Version type="experimental">5.0.0</Version> <Experimental /> {#userevent-pointer} |
There was a problem hiding this comment.
todo: change version number here and in @since JSDocs
67b0b32 to
787fccc
Compare
|
|
||
| interface Coordinates { x?: number; y?: number } | ||
|
|
||
| export async function resolvePageCoordinates( |
There was a problem hiding this comment.
How does this relate to the client side scale position adjustment processPlaywrightPosition?
There was a problem hiding this comment.
Yes, looks like we already scaled position but only on the client-side, I forgot about that 🙃
The issue here is more like we can't compose actions on the server side rather than it not working in general (it works when called from the client).
I wonder if we should then keep the position-scaling behavior on client-side, server-side, or maybe both? We could provide a "scaled" boolean internal option which disables scaling if it was already computed somewhere else.
There was a problem hiding this comment.
I think the concern of switching fully on server-side is the extra roundtrip of evaluate, so keeping both seems fine to start with.
We could provide a "scaled" boolean internal option which disables scaling if it was already computed somewhere else.
I think it's currently doubling the ui scale for client click API since position is already compensated on client side?
position: options.position
&& await resolvePageCoordinates(context, options.position, true)So it looks like it requires differentiating them somehow, but that means the client can pass the scale directly to server, so we don't need server-side scale evaluation?
There was a problem hiding this comment.
I think it's currently doubling the ui scale for client click API since position is already compensated on client side?
Yes. In this branch, a call from the client, with a 0.5 iframe scale, resolves like this: [userEvent] 100px → [client] 50px → [server] 25px
but that means the client can pass the scale directly to server, so we don't need server-side scale evaluation?
Yes, something like this:
position: options._scaled
? options.position
: options.position && resolvePageCoordinates(context, options.position, true)This way we can compose actions on the server too, without having to scale manually each time we call them. If it's not something we want I can just call the Playwright method on the locator, works either way for me
There was a problem hiding this comment.
but that means the client can pass the scale directly to server, so we don't need server-side scale evaluation?
Yes, something like this:
position: options._scaled ? options.position : options.position && resolvePageCoordinates(context, options.position, true)
Does this require server-side evaluate? If client can pass x/y/scale, then server pointer logic can use that to compensate before internal calling server click/hover? something like:
// client click
// -> server click (options.position already scaled on client)
// client pointer (obtain x/y/scale on client and pass as internal option)
// -> server pointer
// -> for each server click, adjust position with x/y/scale passed from client
// -> server click (options.position already scaled on caller side)So, server click can assume position is always compensated
There was a problem hiding this comment.
Unfortunately, I don't think we can get rid of the server-side evaluate 😕
With pointer being an asynchronous chain of actions, there's nothing stopping the user from resizing the pane containing the iframe while it runs, so we can't safely save the scaling and iframe coordinates when calling the server
For now I might just scale the values when calling the function, we can re-evaluate in the future if we have to call these functions in other places
There was a problem hiding this comment.
With
pointerbeing an asynchronous chain of actions, there's nothing stopping the user from resizing the pane containing theiframewhile it runs, so we can't safely save the scaling andiframecoordinates when calling the server
Personally, I wouldn't worry about this case. This sounds like also already true for existing click scaling, which can take time if playwright retries internally, but compensated position is computed once on client side before playwright-level click.
I'm okay with current shape of having resolvePageCoordinates on server though.
There was a problem hiding this comment.
I agree with Hiroshi here, the rescaling while the function runs looks more like intentionally breaking the intended implementation rather than a real use case (especially in headless mode!).
If we can avoid the round trip, I think we should. We already do that during "click" where you could technically also resize while it tries to find the element. The resizing argument is just too contrived in my opinion, we can come up with bad faith arguments all day - what if the user switches tabs? what if the user calls this in parallel? what if the user accesses top level context and intentionally changes the scaling!? If you are trying to break it, you will find a way to break it
Browser UI should be treated as a preview environment, not a test one. We should optimize for headless mode, in my opinion.
There was a problem hiding this comment.
That makes sense, yes.
I moved the scaling and offset logic to the client and computed their values while serializing/normalizing the input.
I'm not entirely sure if that's the best place to do it tho since different providers could (do?) treat raw coordinates differently, but afaik we can't override userEvent methods from the provider.
dc3e2d5 to
3639476
Compare
hi-ogawa
left a comment
There was a problem hiding this comment.
The trace view action is missing. We can add it here
Not sure how to handle multi targets in one action, but maybe fine without element highlight for now.
hi-ogawa
left a comment
There was a problem hiding this comment.
I've left reviews and looks good overall!
|
I'm just curious, how does this play with browser parallelism? I'm excited to try this when it's published, but when I was manipulating I ask because this looks like it also manipulates the page's My use case was actually handling mouse interactions on an interactive canvas, so moving the mouse in one test, causes it to move in another. Disabling parallelism resolved my issues. Just curious, if this has similar limitations, then maybe a documentation note is needed to avoid future issues. |
I think parallel mouse is expected to work. If it doesn't, it would be a playwright bug. For example, we had issue with playwright firefox like #10096. It was reproduced upstream in Playwright microsoft/playwright#40562, and after the fix landed in Playwright 1.61.0. If you see any issues with latest vitest/playwright, please feel free to create an issue with reproduction. |
|
Interesting. Good to know on the expectation. I haven't upgraded to v5 yet, and maybe I just need to upgrade the playwright package itself first to try. When I get some time, I'll try and reproduce my scenario in isolation in my current setup. I am on some older versions of various packages, so entirely possible it's a bug already fixed. I am issuing the calls via the |
7dc08f0 to
87b7d38
Compare
|
I squashed all the previous commits to avoid having to fix too many formatting-related conflicts, hopefully I didn't add unrelated stuff while resolving them tho 🫣 Anyway, I addressed all the review comments, but this still depends on #11221 to pass. |
87b7d38 to
36e24a7
Compare

Description
Implemented a subset of Testing Library's
pointerAPI. Some parts behave differently due to constraints on the providers' side.Resolves #8190
Resolves #8099
Resolves #7836
Resolves #7184
Resolves everythingCloses #6026
Not entirely sure about #7836 as this might be a bit too low-level.
Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
pnpm-lock.yamlunless you introduce a new test example.Tests
pnpm test:ci.Documentation
pnpm run docscommand.Changesets
feat:,fix:,perf:,docs:, orchore:.