Skip to content

Commit b639852

Browse files
authored
fix(browser)!: iframe scale (#9745)
1 parent e399846 commit b639852

11 files changed

Lines changed: 275 additions & 173 deletions

File tree

packages/browser-playwright/src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from './trace'
2020
import { type } from './type'
2121
import { upload } from './upload'
22+
import { viewport } from './viewport'
2223
import { wheel } from './wheel'
2324

2425
export default {
@@ -45,4 +46,5 @@ export default {
4546
__vitest_markTrace: markTrace as typeof markTrace,
4647
__vitest_groupTraceStart: groupTraceStart as typeof groupTraceStart,
4748
__vitest_groupTraceEnd: groupTraceEnd as typeof groupTraceEnd,
49+
__vitest_viewport: viewport as typeof viewport,
4850
}

packages/browser-playwright/src/commands/screenshot.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ interface ScreenshotCommandOptions extends Omit<ScreenshotOptions, 'element' | '
99
element?: string
1010
mask?: readonly string[]
1111
}
12+
13+
const SCREENSHOT_STYLES = /* css */`
14+
iframe[data-vitest="true"] {
15+
position: absolute !important;
16+
inset: 0 !important;
17+
z-index: ${Number.MAX_SAFE_INTEGER} !important;
18+
transform: none !important;
19+
}
20+
`
21+
1222
/**
1323
* Takes a screenshot using the provided browser context and returns a buffer and the expected screenshot path.
1424
*
@@ -43,6 +53,11 @@ export async function takeScreenshot(
4353
}
4454

4555
const mask = options.mask?.map(selector => getDescribedLocator(context, selector))
56+
const style = context.project.config.browser.ui
57+
? options.style === undefined
58+
? SCREENSHOT_STYLES
59+
: SCREENSHOT_STYLES + options.style
60+
: options.style
4661

4762
if (options.element) {
4863
const { element: selector, ...config } = options
@@ -51,6 +66,7 @@ export async function takeScreenshot(
5166
...config,
5267
mask,
5368
path: savePath,
69+
style,
5470
})
5571
return { buffer, path }
5672
}
@@ -59,6 +75,7 @@ export async function takeScreenshot(
5975
...options,
6076
mask,
6177
path: savePath,
78+
style,
6279
})
6380
return { buffer, path }
6481
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { UserEventCommand } from './utils'
2+
3+
export const viewport: UserEventCommand<(options: {
4+
width: number
5+
height: number
6+
}) => void> = async (context, options) => {
7+
await context.page.setViewportSize(options)
8+
}

packages/browser-webdriverio/src/commands/screenshot.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,10 @@ export async function takeScreenshot(
6363
const buffer = await element.saveScreenshot(
6464
platformNormalize(savePathWithExtension),
6565
)
66+
6667
if (!options.save) {
6768
await rm(savePathWithExtension, { force: true })
6869
}
70+
6971
return { buffer, path }
7072
}

packages/browser/src/client/orchestrator.ts

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ export class IframeOrchestrator {
6767
const container = await getContainer(config)
6868

6969
if (config.browser.ui) {
70-
container.className = 'absolute origin-top mt-[8px]'
71-
container.parentElement!.setAttribute('data-ready', 'true')
70+
container.setAttribute('data-ready', 'true')
7271
// in non-isolated mode this will also remove the iframe,
7372
// so we only do this once
7473
if (container.textContent) {
@@ -154,9 +153,8 @@ export class IframeOrchestrator {
154153

155154
const config = getConfig()
156155
const { width, height } = config.browser.viewport
157-
const iframe = this.iframes.get(ID_ALL)!
158156

159-
await setIframeViewport(iframe, width, height)
157+
await setIframeViewport(width, height)
160158
debug('run non-isolated tests', options.files.join(', '))
161159
await this.sendEventToIframe({
162160
event: 'execute',
@@ -187,13 +185,13 @@ export class IframeOrchestrator {
187185
this.iframes.delete(file)
188186
}
189187

190-
const iframe = await this.prepareIframe(
188+
await this.prepareIframe(
191189
container,
192190
file,
193191
startTime,
194192
otelContext,
195193
)
196-
await setIframeViewport(iframe, width, height)
194+
await setIframeViewport(width, height)
197195
// running tests after the "prepare" event
198196
await this.sendEventToIframe({
199197
event: 'execute',
@@ -312,16 +310,40 @@ export class IframeOrchestrator {
312310
private createTestIframe(iframeId: string) {
313311
const iframe = document.createElement('iframe')
314312
const src = `/?sessionId=${getBrowserState().sessionId}&iframeId=${iframeId}`
313+
const config = getConfig()
314+
315315
iframe.setAttribute('loading', 'eager')
316316
iframe.setAttribute('src', src)
317317
iframe.setAttribute('data-vitest', 'true')
318-
319-
iframe.style.border = 'none'
320-
iframe.style.width = '100%'
321-
iframe.style.height = '100%'
322318
iframe.setAttribute('allowfullscreen', 'true')
323319
iframe.setAttribute('allow', 'clipboard-write;')
324320
iframe.setAttribute('name', 'vitest-iframe')
321+
322+
iframe.style.setProperty('border', 'none')
323+
iframe.style.setProperty('background-color', '#fff')
324+
iframe.style.setProperty('width', 'var(--viewport-width)')
325+
iframe.style.setProperty('height', 'var(--viewport-height)')
326+
327+
// enable scaling only when using the UI, without UI the iframe fills the page
328+
if (config.browser.ui) {
329+
if (config.browser.name !== 'firefox') {
330+
iframe.style.setProperty('transform', 'scale(min(1, calc(100cqw / var(--viewport-width)), calc(100cqh / var(--viewport-height))))')
331+
}
332+
else {
333+
// Firefox cannot resolve relative units like `cqw` directly inside `atan2()`
334+
// Storing it in a CSS variable first forces Firefox to resolve `100cqw` to an absolute pixel value
335+
iframe.style.setProperty('--container-width', '100cqw')
336+
iframe.style.setProperty('--container-height', '100cqh')
337+
// Firefox does not support typed arithmetic (divisions between typed values): https://bugzilla.mozilla.org/show_bug.cgi?id=1264520
338+
// `tan(atan2(a, b))` produces a unit-less `a / b` ratio:
339+
// - `atan2()` accepts two lengths and returns an `<angle>`
340+
// - `tan()` converts it back to a unit-less `<number>`
341+
iframe.style.setProperty('transform', 'scale(min(1, tan(atan2(var(--container-width), var(--viewport-width))), tan(atan2(var(--container-height), var(--viewport-height)))))')
342+
}
343+
344+
iframe.style.setProperty('transform-origin', 'top left')
345+
}
346+
325347
return iframe
326348
}
327349

@@ -357,7 +379,7 @@ export class IframeOrchestrator {
357379
)
358380
break
359381
}
360-
await setIframeViewport(iframe, width, height)
382+
await setIframeViewport(width, height)
361383
channel.postMessage({ event: 'viewport:done', iframeId: id } satisfies IframeViewportDoneEvent)
362384
break
363385
}
@@ -447,38 +469,25 @@ function generateFileId(file: string) {
447469
}
448470

449471
async function setIframeViewport(
450-
iframe: HTMLIFrameElement,
451472
width: number,
452473
height: number,
453474
) {
454475
const ui = getUiAPI()
476+
455477
if (ui) {
456478
await ui.setIframeViewport(width, height)
457479
}
458-
else if (getBrowserState().provider === 'webdriverio') {
459-
iframe.parentElement?.setAttribute('data-scale', '1')
480+
else {
481+
document.body.style.setProperty('--viewport-width', `${width}px`)
482+
document.body.style.setProperty('--viewport-height', `${height}px`)
483+
460484
await client.rpc.triggerCommand(
461485
getBrowserState().sessionId,
462486
'__vitest_viewport',
463487
undefined,
464488
[{ width, height }],
465489
)
466490
}
467-
else {
468-
const scale = Math.min(
469-
1,
470-
iframe.parentElement!.parentElement!.clientWidth / width,
471-
iframe.parentElement!.parentElement!.clientHeight / height,
472-
)
473-
iframe.parentElement!.style.cssText = `
474-
width: ${width}px;
475-
height: ${height}px;
476-
transform: scale(${scale});
477-
transform-origin: left top;
478-
`
479-
iframe.parentElement?.setAttribute('data-scale', String(scale))
480-
await new Promise(r => requestAnimationFrame(r))
481-
}
482491
}
483492

484493
function debug(...args: unknown[]) {

packages/browser/src/client/tester/tester-utils.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,16 @@ export function processTimeoutOptions<T extends { timeout?: number }>(options_:
224224
}
225225

226226
export function getIframeScale(): number {
227-
const testerUi = window.parent.document.querySelector(`iframe[data-vitest]`)?.parentElement
228-
if (!testerUi) {
229-
throw new Error(`Cannot find Tester element. This is a bug in Vitest. Please, open a new issue with reproduction.`)
230-
}
231-
const scaleAttribute = testerUi.getAttribute('data-scale')
232-
const scale = Number(scaleAttribute)
233-
if (Number.isNaN(scale)) {
234-
throw new TypeError(`Cannot parse scale value from Tester element (${scaleAttribute}). This is a bug in Vitest. Please, open a new issue with reproduction.`)
227+
const iframe = window.frameElement
228+
229+
if (!iframe) {
230+
throw new Error(`Cannot find iframe element. This is a bug in Vitest. Please, open a new issue with reproduction.`)
235231
}
232+
233+
// DOMMatrix parses the computed 2D transform matrix [a, b, c, d, e, f]
234+
// `a` and `d` are the x and y scale factors - since we only apply uniform scaling, `a === d`
235+
const scale = new DOMMatrix(getComputedStyle(iframe).transform).a
236+
236237
return scale
237238
}
238239

Lines changed: 35 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<script setup lang="ts">
22
import type { ViewportSize } from '~/composables/browser'
3-
import { useWindowSize } from '@vueuse/core'
4-
import { computed } from 'vue'
3+
import { computed, onMounted, onUnmounted, ref, useTemplateRef } from 'vue'
54
import { viewport } from '~/composables/browser'
65
import { browserState } from '~/composables/client'
76
import {
@@ -24,56 +23,42 @@ function isViewport(name: ViewportSize) {
2423
return viewport.value[0] === preset[0] && viewport.value[1] === preset[1]
2524
}
2625
27-
const { width: windowWidth, height: windowHeight } = useWindowSize()
28-
2926
async function changeViewport(name: ViewportSize) {
3027
viewport.value = sizes[name]
3128
if (browserState?.provider === 'webdriverio') {
3229
updateBrowserPanel()
3330
}
3431
}
3532
36-
const PADDING_SIDES = 20
37-
const PADDING_TOP = 100
38-
39-
const containerSize = computed(() => {
40-
if (browserState?.provider === 'webdriverio') {
41-
const [width, height] = viewport.value
42-
return { width, height }
43-
}
33+
const testContainer = useTemplateRef('tester-ui')
34+
const testContainerRect = ref<DOMRectReadOnly | null>(null)
4435
45-
const parentContainerWidth = windowWidth.value * (panels.details.size / 100)
46-
const parentOffsetWidth = parentContainerWidth * (panels.details.browser / 100)
47-
const containerWidth = parentOffsetWidth - PADDING_SIDES
48-
const containerHeight = windowHeight.value - PADDING_TOP
49-
return {
50-
width: containerWidth,
51-
height: containerHeight,
52-
}
36+
const observer = new ResizeObserver(([entry]) => {
37+
testContainerRect.value = entry.contentRect
5338
})
54-
55-
const scale = computed(() => {
56-
if (browserState?.provider === 'webdriverio') {
57-
return 1
39+
onMounted(() => {
40+
if (testContainer.value) {
41+
observer.observe(testContainer.value)
5842
}
59-
60-
const [iframeWidth, iframeHeight] = viewport.value
61-
const { width: containerWidth, height: containerHeight } = containerSize.value
62-
const widthScale = containerWidth > iframeWidth ? 1 : containerWidth / iframeWidth
63-
const heightScale = containerHeight > iframeHeight ? 1 : containerHeight / iframeHeight
64-
return Math.min(1, widthScale, heightScale)
6543
})
66-
67-
const marginLeft = computed(() => {
68-
const containerWidth = containerSize.value.width
69-
const iframeWidth = viewport.value[0]
70-
const offset = Math.trunc((containerWidth + PADDING_SIDES - iframeWidth) / 2)
71-
return `${offset}px`
44+
onUnmounted(() => {
45+
observer.disconnect()
7246
})
47+
48+
const scale = computed(() =>
49+
testContainerRect.value
50+
? Math.floor(
51+
Math.min(
52+
testContainerRect.value.width / viewport.value[0],
53+
testContainerRect.value.height / viewport.value[1],
54+
) * 100,
55+
)
56+
: 100,
57+
)
7358
</script>
7459

7560
<template>
76-
<div h="full" flex="~ col">
61+
<div id="browser-frame" h="full" flex="~ col">
7762
<div p="3" h-10 flex="~ gap-2" items-center bg-header border="b base">
7863
<IconButton
7964
v-show="panels.navigation <= 15"
@@ -118,40 +103,30 @@ const marginLeft = computed(() => {
118103
/>
119104
<span class="pointer-events-none" text-sm>
120105
{{ viewport[0] }}x{{ viewport[1] }}px
121-
<span v-if="scale < 1">({{ (scale * 100).toFixed(0) }}%)</span>
106+
<span v-if="scale < 100">({{ scale }}%)</span>
122107
</span>
123108
</div>
124-
<div id="tester-container" relative>
125-
<div
126-
id="tester-ui"
127-
class="flex h-full justify-center items-center font-light op70"
128-
:data-scale="scale"
129-
:style="{
130-
'--viewport-width': `${viewport[0]}px`,
131-
'--viewport-height': `${viewport[1]}px`,
132-
'--tester-transform': `scale(${scale})`,
133-
'--tester-margin-left': marginLeft,
134-
}"
135-
>
136-
Select a test to run
137-
</div>
109+
<div id="tester-ui" ref="tester-ui">
110+
Select a test to run
138111
</div>
139112
</div>
140113
</template>
141114

142115
<style scoped>
143-
#tester-container:not([data-ready]) {
144-
width: 100%;
116+
#tester-ui {
145117
height: 100%;
118+
container-type: size;
119+
120+
margin-top: 0.5rem;
121+
}
122+
123+
#tester-ui:not([data-ready]) {
146124
display: flex;
147125
align-items: center;
148126
justify-content: center;
149-
}
150127
151-
[data-ready] #tester-ui {
152-
width: var(--viewport-width);
153-
height: var(--viewport-height);
154-
transform: var(--tester-transform);
155-
margin-left: var(--tester-margin-left);
128+
opacity: 0.7;
129+
130+
font-weight: 300;
156131
}
157132
</style>
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import { ref } from 'vue'
1+
import { ref, watch } from 'vue'
22

33
export type ViewportSize
44
= | 'small-mobile'
55
| 'large-mobile'
66
| 'tablet'
77
export const viewport = ref<[number, number]>([414, 896])
8+
9+
watch([viewport], () => {
10+
document.body.style.setProperty('--viewport-width', `${viewport.value[0]}px`)
11+
document.body.style.setProperty('--viewport-height', `${viewport.value[1]}px`)
12+
}, { immediate: true, flush: 'sync' })

0 commit comments

Comments
 (0)