diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 10d4c0bf4f73..d9ce4a1e243f 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -273,10 +273,25 @@ async function setIframeViewport( if (ui) { await ui.setIframeViewport(width, height) } - else { + else if (getBrowserState().provider === 'webdriverio') { iframe.style.width = `${width}px` iframe.style.height = `${height}px` } + else { + const scale = Math.min( + 1, + iframe.parentElement!.parentElement!.clientWidth / width, + iframe.parentElement!.parentElement!.clientHeight / height, + ) + iframe.parentElement!.style.cssText = ` + width: ${width}px; + height: ${height}px; + transform: scale(${scale}); + transform-origin: left top; + ` + iframe.parentElement?.setAttribute('data-scale', String(scale)) + await new Promise(r => requestAnimationFrame(r)) + } } function debug(...args: unknown[]) { diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index f498e89128a2..c040e323571b 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -15,7 +15,6 @@ import { convertElementToCssSelector, getBrowserState, getWorkerState } from '.. // this file should not import anything directly, only types and utils -const state = () => getWorkerState() // @ts-expect-error not typed global const provider = __vitest_browser_runner__.provider function filepath() { @@ -222,8 +221,7 @@ function getTaskFullName(task: RunnerTask): string { } function processClickOptions(options_?: UserEventClickOptions) { - // only ui scales the iframe, so we need to adjust the position - if (!options_ || !state().config.browser.ui) { + if (!options_) { return options_ } if (provider === 'playwright') { @@ -250,8 +248,7 @@ function processClickOptions(options_?: UserEventClickOptions) { } function processHoverOptions(options_?: UserEventHoverOptions) { - // only ui scales the iframe, so we need to adjust the position - if (!options_ || !state().config.browser.ui) { + if (!options_) { return options_ } @@ -277,8 +274,7 @@ function processHoverOptions(options_?: UserEventHoverOptions) { } function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) { - // only ui scales the iframe, so we need to adjust the position - if (!options_ || !state().config.browser.ui) { + if (!options_) { return options_ } if (provider === 'playwright') { @@ -336,11 +332,14 @@ function processPlaywrightPosition(position: { x: number; y: number }) { } function getIframeScale() { - const testerUi = window.parent.document.querySelector('#tester-ui') as HTMLElement | null + const testerUi = window.parent.document.querySelector(`iframe[data-vitest]`)?.parentElement if (!testerUi) { throw new Error(`Cannot find Tester element. This is a bug in Vitest. Please, open a new issue with reproduction.`) } const scaleAttribute = testerUi.getAttribute('data-scale') + if (scaleAttribute === null) { + return 1 + } const scale = Number(scaleAttribute) if (Number.isNaN(scale)) { throw new TypeError(`Cannot parse scale value from Tester element (${scaleAttribute}). This is a bug in Vitest. Please, open a new issue with reproduction.`) diff --git a/test/browser/fixtures/locators/blog.test.tsx b/test/browser/fixtures/locators/blog.test.tsx index 991c0eacbf0e..41a6834dd4d6 100644 --- a/test/browser/fixtures/locators/blog.test.tsx +++ b/test/browser/fixtures/locators/blog.test.tsx @@ -18,6 +18,7 @@ test('renders blog posts', async () => { await expect.element(secondPost.getByRole('heading')).toHaveTextContent('qui est esse') + // TODO: click doesn't work on webdriverio when iframe is scaled await userEvent.click(secondPost.getByRole('button', { name: 'Delete' })) expect(screen.getByRole('listitem').all()).toHaveLength(3) diff --git a/test/browser/fixtures/locators/vitest.config.ts b/test/browser/fixtures/locators/vitest.config.ts index e32545f4ab22..a9652cc4257d 100644 --- a/test/browser/fixtures/locators/vitest.config.ts +++ b/test/browser/fixtures/locators/vitest.config.ts @@ -16,7 +16,6 @@ export default defineConfig({ enabled: true, provider, name, - headless: true, }, onConsoleLog(log) { if (log.includes('ReactDOMTestUtils.act')) { diff --git a/test/browser/fixtures/viewport/basic.test.ts b/test/browser/fixtures/viewport/basic.test.ts new file mode 100644 index 000000000000..56746956d933 --- /dev/null +++ b/test/browser/fixtures/viewport/basic.test.ts @@ -0,0 +1,58 @@ +import { page, userEvent, server } from "@vitest/browser/context"; +import { expect, test } from "vitest"; + +test("drag and drop over large viewport", async () => { + // put boxes horizontally [1] [2] ... [30] + // then drag-and-drop from [1] to [30] + + const wrapper = document.createElement("div"); + wrapper.style.cssText = "display: flex; width: 3000px;"; + document.body.appendChild(wrapper); + + const events: { i: number; type: string }[] = []; + + for (let i = 1; i <= 30; i++) { + const el = document.createElement("div"); + el.textContent = `[${i}]`; + el.style.cssText = ` + flex: none; + width: 100px; + height: 100px; + border: 1px solid black; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + `; + el.draggable = true; + wrapper.append(el); + + el.addEventListener("dragstart", (ev) => { + ev.dataTransfer.effectAllowed = "move"; + events.push({ type: "dragstart", i }); + }); + el.addEventListener("dragover", (ev) => { + ev.preventDefault(); + ev.dataTransfer.dropEffect = "move"; + events.push({ type: "dragover", i }); + }); + el.addEventListener("drop", (ev) => { + ev.preventDefault(); + events.push({ type: "drop", i }); + }); + } + + // drag and drop only works reliably on playwright + if (server.provider !== 'playwright') { + return + } + + await userEvent.dragAndDrop(page.getByText("[1]"), page.getByText("[30]")); + + expect(events).toMatchObject( + expect.arrayContaining([ + { type: "dragstart", i: 1 }, + { type: "drop", i: 30 }, + ]), + ); +}); diff --git a/test/browser/fixtures/viewport/vitest.config.ts b/test/browser/fixtures/viewport/vitest.config.ts new file mode 100644 index 000000000000..403a56690a82 --- /dev/null +++ b/test/browser/fixtures/viewport/vitest.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +// pnpm -C test/browser test-fixtures --root fixtures/viewport --browser.ui=false +// pnpm -C test/browser test-fixtures --root fixtures/viewport --browser.headless=true + +const provider = process.env.PROVIDER || 'playwright' +const name = + process.env.BROWSER || (provider === 'playwright' ? 'chromium' : 'chrome') + +export default defineConfig({ + test: { + browser: { + enabled: true, + name, + provider, + viewport: { width: 3000, height: 400 } + }, + }, + cacheDir: fileURLToPath(new URL("./node_modules/.vite", import.meta.url)), +}) diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 419aa61df9c4..78a746240713 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -10,11 +10,16 @@ describe('running browser tests', async () => { let passedTests: any[] let failedTests: any[] + beforeAll(() => { + const id = setInterval(() => console.log('[debug]', new Date().toISOString()), 2000) + return () => clearInterval(id) + }) + beforeAll(async () => { ({ stderr, stdout, - } = await runBrowserTests()) + } = await runBrowserTests(undefined, undefined, undefined, { std: 'inherit' })) const browserResult = await readFile('./browser.json', 'utf-8') browserResultJson = JSON.parse(browserResult) diff --git a/test/browser/specs/utils.ts b/test/browser/specs/utils.ts index 944d1492f61f..2e9ea19dfcbe 100644 --- a/test/browser/specs/utils.ts +++ b/test/browser/specs/utils.ts @@ -9,6 +9,7 @@ export async function runBrowserTests( config?: Omit & { browser?: Partial }, include?: string[], viteOverrides?: Partial, + runnerOptions?: Parameters[4], ) { return runVitest({ watch: false, @@ -18,5 +19,5 @@ export async function runBrowserTests( headless: browser !== 'safari', ...config?.browser, } as UserConfig['browser'], - }, include, 'test', viteOverrides) + }, include, 'test', viteOverrides, runnerOptions) } diff --git a/test/browser/specs/viewport.test.ts b/test/browser/specs/viewport.test.ts new file mode 100644 index 000000000000..e6daf48badd2 --- /dev/null +++ b/test/browser/specs/viewport.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from 'vitest' +import { runBrowserTests } from './utils' + +test('viewport', async () => { + const { stderr, ctx } = await runBrowserTests({ + root: './fixtures/viewport', + }) + + expect(stderr).toBe('') + expect( + Object.fromEntries( + ctx.state.getFiles().map(f => [f.name, f.result.state]), + ), + ).toMatchInlineSnapshot(` + { + "basic.test.ts": "pass", + } + `) +}) diff --git a/test/browser/test/userEvent.test.ts b/test/browser/test/userEvent.test.ts index d4b43802b600..20b5ceec3a7e 100644 --- a/test/browser/test/userEvent.test.ts +++ b/test/browser/test/userEvent.test.ts @@ -135,9 +135,10 @@ describe('userEvent.click', () => { }, }) + // not exact due to scaling and rounding expect(spy).toHaveBeenCalledWith({ - x: 200, - y: 150, + x: expect.closeTo(200, -1), + y: expect.closeTo(150, -1), }) }) }) diff --git a/test/browser/vitest.config.mts b/test/browser/vitest.config.mts index b9f6ca89a18b..303f5fd77806 100644 --- a/test/browser/vitest.config.mts +++ b/test/browser/vitest.config.mts @@ -41,7 +41,7 @@ export default defineConfig({ name: browser, headless: false, provider, - isolate: false, + // isolate: false, testerScripts: [ { content: 'globalThis.__injected = []',