Browser: Add resize capability
Clear and concise description of the problem
I have a div that supports resizing and have this div monitored by a resize observer. From what I can tell, I have no means of testing this behavior.
Looking at playwright and webdriver, they have methods that move the cursor, hover and click the mouse down. I would like to see this functionality in vitest.
Suggested solution
page.mouse.down(); page.mouse.move(x, y) page.mouse.up();
Alternative
No response
Additional context
No response
Validations
- [x] Follow our Code of Conduct
- [x] Read the Contributing Guidelines.
- [x] Read the docs.
- [x] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
I've been able to overcome the lack of page.mouse using the following custom plugin:
import type { Plugin } from 'vitest/config'
type OptionsPointer = {
/**
* Defaults to `left`.
*/
button?: 'left' | 'right' | 'middle'
/**
* defaults to 1. See [UIEvent.detail].
*/
clickCount?: number
}
type OptionsMove = {
/**
* Defaults to 1. Sends intermediate `mousemove` events.
*/
steps?: number
}
const error = (e: string) => {
throw new Error(e)
}
export default function VitestMousePlugin(): Plugin {
return {
name: 'custom:vitest:mouse-commands',
config() {
return {
test: {
browser: {
commands: {
mouseDown: (ctx, opts?: OptionsPointer) => ctx.page.mouse.down(opts),
mouseUp: (ctx, opts?: OptionsPointer) => ctx.page.mouse.up(opts),
mouseWheel: (ctx, deltaX: number, deltaY: number) => ctx.page.mouse.wheel(deltaX, deltaY),
mouseMove: async (ctx, x: number, y: number, opts?: OptionsMove) => {
const frame = await ctx.frame()
const element = await frame.frameElement()
const boundingBox = (await element.boundingBox()) ?? error('No frame bounding box?!!')
const frameScale =
(await ctx.iframe.owner().locator('xpath=..').getAttribute('data-scale')) ?? error('No scale?!!')
const scaledX = x * parseFloat(frameScale)
const scaledY = y * parseFloat(frameScale)
return ctx.page.mouse.move(boundingBox.x + scaledX, boundingBox.y + scaledY, opts)
},
},
},
},
}
},
}
}
declare module '@vitest/browser/context' {
interface BrowserCommands {
mouseDown: (opts?: OptionsPointer) => Promise<void>
mouseUp: (opts?: OptionsPointer) => Promise<void>
mouseMove: (x: number, y: number, opts?: OptionsMove) => Promise<void>
mouseWheel: (deltaX: number, deltaY: number) => Promise<void>
}
}
The handling for mouseMove is a bit complex because it has to account for the fact that the frame is scaled; all the measurements took inside of it need to be scaled accordingly to get the coordinates within the full page.
@cyyynthia Thanks for this code, but how do I get it to run?
I have copied this code into a file called src/testHelpers/customCommands, I have included this import in my setup file (src/testHelpers/setupBrowsers.ts) the import like this:-
import "./customLocators";
import "./customCommands";
And my config is setup like this:-
import { defineWorkspace } from 'vitest/config'
export default defineWorkspace([
// If you want to keep running your existing tests in Node.js, uncomment the next line.
// 'vite.config.ts',
{
extends: 'vite.config.ts',
test: {
name: "browser",
include: ["**/*.browser.test.ts[x]"],
browser: {
enabled: true,
headless: true,
provider: 'playwright',
// https://vitest.dev/guide/browser/playwright
instances: [
// { browser: 'chromium' },
{ browser: 'firefox' },
// { browser: 'webkit' },
]
},
setupFiles: ["./src/testHelpers/setupBrowser.ts"]
},
},
{
extends: 'vite.config.ts',
test: {
environment: "jsdom",
exclude: ["**/*.browser.test.ts[x]", "node_modules"],
name: "unit",
globals: true,
setupFiles: ["./src/testHelpers/setup.ts"]
},
},
])
But when I use the command object I do not see these methods.
import { commands } from '@vitest/browser/context'
...
it("should select heading text when editing", async () => {
render(
<Provider store={customStore}>
<div className="app">
<Board />
</div>
</Provider>
);
const workspace = page.getByTestId("workspace");
let column = workspace.getByTestId("card-column");
const heading = column.getByRole("heading");
await column.click({ position: { x: 10, y: 10 } });
await heading.click({ position: { x: 10, y: 10 } });
console.log(commands);
});
The commands only includes readFile, removeFile, and writeFile.
The code I've provided is a Vite plugin that you need to register as you'd register other Vite plugins such as the JSX plugin. You will then have access to those commands within Vitest.
See the custom commands documentation for more details
I crafted a lib vitest-browser-commands to do that! Currently, it only exports the mouse API from Playwright. Pull requests are welcome if you want to export more underlying APIs like mouse/keyboard for WebdriverIO. Please check the README for vitest-browser-commands for usage.
@ocavue pertty cool to have it as a package. 😄
Beware, as our test suites grew larger we started to face significant issues with this ad-hoc mouse control implementations and found it to be extremely flaky as more tests are written. Disabling file concurrency is a must if you don't want seemingly inexplicable behavior to happen :)
I didn't get the chance to hack around a new mouse driver that would do best effort pointer event emulation by manually dispatching events (like you have to do in order to test touch-based interactions), but that's definitely something that'd be worth exploring to improve reliability. I've moved to other projects so I likely won't dive into that myself for a while.
@cyyynthia I have to use the mouse API to test drag-and-drop behavior anyway, as quoted by the Playwright's documentation:
If your page relies on the
dragoverevent being dispatched, you need at least two mouse moves to trigger it in all browsers. To reliably issue the second mouse move, repeat your mouse.move() or locator.hover() twice. The sequence of operations would be: hover the drag element, mouse down, hover the drop element, hover the drop element a second time, mouse up.
In particular, the steps option in mouse.move() makes it much more stable.
Would Pointer API be enough for this?
I must say I'm not sure. It seems to be capable of emulating basic pointer movement from one element to the other, but I've had situations with complex interactive components where I had to test very fine grained behavior and how elements react relative to each other that would most likely not be easily emulated by this mechanism. It also lacks the ability to go from point A to point B in steps to emulate a more human-like dragging behavior.
That being said, it seems like it could work for situations where these don't matter, and perhaps the lack of absolute movements can be seen as a feature the same way the API is designed to encourage use of accessibility-based selectors. But I think relative positionning around a given element would be quite valuable.
It also lacks the ability to go from point A to point B in steps to emulate a more human-like dragging behavior.
Wouldn't it work like this? (From the API):
pointer([{target: element, offset: 2, keys: '[MouseLeft>]'}, {offset: 5}])
// => <div><span>fo[o</span><span>ba]r</span></div>
I think their API could be quite hard to understand, to be honest. We can always extend it with our own interface. I am open to any API proposal that is not just a direct one-to-one copy from playwright or wdio. We don't want to take too many round calls (for example, see the upcoming wheel API: https://github.com/vitest-dev/vitest/pull/9188. It tries to minimize the number of calls between the browser and the server), and it should be easy to understand.