packages/app/component-tests/browser-pane.spec.ts
Rama dev · réplica de lectura del repositorio original.
import { expect, sourceURL, story } from "../../stories/playwright/story"
const source = (path: string) => sourceURL(new URL(path, import.meta.url))
const modules = {
fixture: source("../../gui-plugins/src/browser/panel.fixture.tsx"),
embeds: source("../src/runtime/plugin/embeds.tsx"),
panelMotion: source("../src/shell/panel-motion.ts"),
language: source("../src/runtime/i18n/language.tsx"),
}
story.beforeEach(async ({ mount, page }) => {
// Any story loads the app styles; the fixture mounts the pane beside it on the real host embeds.
await mount("ui-line-comment--editor")
await page.evaluate(async (modules) => {
const [{ mountBrowserPane }, { createEmbeds }, panelMotion, language] = await Promise.all([
import(modules.fixture),
import(modules.embeds),
import(modules.panelMotion),
import(modules.language),
])
mountBrowserPane({
createEmbeds,
panelMotion,
LanguageProvider: language.LanguageProvider,
UiI18nBridge: language.UiI18nBridge,
useLanguage: language.useLanguage,
})
}, modules)
await expect(page.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("hides a native page when another takes the pane, the pane hides, or it unmounts", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const alpha = root.getByTestId("native-Alpha")
const beta = root.getByTestId("native-Beta")
await root.getByRole("button", { name: "Beta", exact: true }).click()
await expect(beta).toHaveAttribute("data-visible", "true")
await expect(alpha).toHaveAttribute("data-visible", "false")
await root.getByRole("button", { name: "Empty", exact: true }).click()
await expect(root.locator("#browser-panel")).toHaveCount(0)
await expect(beta).toHaveAttribute("data-visible", "false")
await root.getByRole("button", { name: "Alpha", exact: true }).click()
await expect(alpha).toHaveAttribute("data-visible", "true")
await root.getByRole("button", { name: "Toggle Review tab", exact: true }).click()
await expect(alpha).toHaveAttribute("data-visible", "false")
await root.getByRole("button", { name: "Toggle Review tab", exact: true }).click()
await expect(alpha).toHaveAttribute("data-visible", "true")
await root.getByRole("button", { name: "Unmount pane", exact: true }).click()
await expect(alpha).toHaveAttribute("data-visible", "false")
})
story("hides the native view immediately while the pane stays mounted", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const toggle = root.getByRole("button", { name: "Toggle Review tab", exact: true })
await expect(toggle).toBeEnabled()
// Read in the same task as the click so a deferred animation-frame hide cannot pass.
const visible = await toggle.evaluate((element) => {
element.dispatchEvent(new MouseEvent("click", { bubbles: true }))
return document.querySelector('[data-testid="native-Alpha"]')?.getAttribute("data-visible")
})
expect(visible).toBe("false")
await expect(root.locator("#browser-panel")).toHaveCount(1)
await toggle.click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("paints a still of the page in place of the native view while a panel moves", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const motion = root.getByRole("button", { name: "Toggle panel motion", exact: true })
const pane = root.getByRole("button", { name: "Toggle Review tab", exact: true })
const native = root.getByTestId("native-Alpha")
const still = root.locator("#browser-panel img")
// Read in the same task as the click: a native view left up for even one frame floats over the moving app.
const visible = await motion.evaluate((element) => {
element.dispatchEvent(new MouseEvent("click", { bubbles: true }))
return document.querySelector('[data-testid="native-Alpha"]')?.getAttribute("data-visible")
})
expect(visible).toBe("false")
// A close: the page flies out as a still of itself rather than a blank box.
await expect(still).toBeVisible()
await pane.click()
await expect(still).toBeVisible()
await motion.click()
await expect(still).toHaveCount(0)
// An open shows the still the close left, until the flight lands and the page is back.
await motion.click()
await pane.click()
await expect(still).toBeVisible()
await expect(native).toHaveAttribute("data-visible", "false")
await motion.click()
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
})
story("paints a still of the page in place of the native view while a resize handle is dragged", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const handle = root.getByTestId("pane-resize-handle")
const native = root.getByTestId("native-Alpha")
const still = root.locator("#browser-panel img")
// A native view's bounds land on screen frames before the DOM around it, so one left up through a drag jumps against
// its pane on every move. A click on the handle moves nothing and must not blink the page out. Read in the same task
// as each event, so a view hidden and shown again before the next frame cannot pass.
const visible = await handle.evaluate((element) => {
const read = () => document.querySelector('[data-testid="native-Alpha"]')?.getAttribute("data-visible")
const box = element.getBoundingClientRect()
const at = { bubbles: true, pointerId: 1, clientX: box.x + box.width / 2, clientY: box.y + box.height / 2 }
element.dispatchEvent(new PointerEvent("pointerdown", { ...at, button: 0 }))
document.dispatchEvent(new PointerEvent("pointerup", at))
const click = read()
element.dispatchEvent(new PointerEvent("pointerdown", { ...at, button: 0 }))
const pressed = read()
document.dispatchEvent(new PointerEvent("pointermove", { ...at, clientX: at.clientX - 40 }))
return { click, pressed, dragging: read() }
})
expect(visible).toEqual({ click: "true", pressed: "true", dragging: "false" })
await expect(still).toBeVisible()
await handle.evaluate((element) => {
const box = element.getBoundingClientRect()
document.dispatchEvent(
new PointerEvent("pointerup", { bubbles: true, pointerId: 1, clientX: box.x, clientY: box.y }),
)
})
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
})
story("shows the native view again when the window loses focus mid-drag", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const handle = root.getByTestId("pane-resize-handle")
const native = root.getByTestId("native-Alpha")
const still = root.locator("#browser-panel img")
// Alt+Tab mid-drag delivers no pointerup; the page must not stay locked behind a still.
await handle.evaluate((element) => {
const box = element.getBoundingClientRect()
const at = { bubbles: true, pointerId: 1, clientX: box.x + box.width / 2, clientY: box.y + box.height / 2 }
element.dispatchEvent(new PointerEvent("pointerdown", { ...at, button: 0 }))
document.dispatchEvent(new PointerEvent("pointermove", { ...at, clientX: at.clientX - 40 }))
})
await expect(native).toHaveAttribute("data-visible", "false")
await expect(handle).toHaveAttribute("data-dragging", "")
await handle.evaluate(() => window.dispatchEvent(new FocusEvent("blur")))
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
await expect(handle).not.toHaveAttribute("data-dragging")
expect(await handle.evaluate(() => document.body.style.userSelect)).toBe("")
})
story("never flies a still of a page the tab has navigated away from", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const motion = root.getByRole("button", { name: "Toggle panel motion", exact: true })
const native = root.getByTestId("native-Alpha")
const still = root.locator("#browser-panel img")
await motion.click()
await expect(still).toBeVisible()
await motion.click()
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
await root.getByRole("button", { name: "Hold capture", exact: true }).click()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.fill("https://example.com/")
await address.press("Enter")
await expect(address).toHaveValue("https://example.com/")
await expect(native).toHaveAttribute("data-visible", "true")
await motion.click()
await expect(native).toHaveAttribute("data-visible", "false")
await expect(still).toHaveCount(0)
// The new page's own still stands in once it arrives.
await root.getByRole("button", { name: "Release capture", exact: true }).click()
await expect(still).toBeVisible()
})
story("refreshes page stills by the window's focus, not the document's", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const captures = async () => Number((await root.getByText(/^Captures: \d+$/).textContent())?.split(": ")[1])
await page.clock.install()
await expect(root.getByText(/^Captures: [1-9]/)).toBeVisible()
// Main reports the window unfocused while this document keeps focus: stills follow the window, so they stop.
await root.getByRole("button", { name: "Blur window", exact: true }).click()
const paused = await captures()
await page.clock.runFor(120_000)
expect(await captures()).toBe(paused)
// Focus coming back to the window, even straight into the embed, which never focuses this document, takes a fresh
// still at once.
await root.getByRole("button", { name: "Focus window", exact: true }).click()
await page.clock.runFor(10)
await expect(root.getByText(`Captures: ${paused + 1}`, { exact: true })).toBeVisible()
})
story("keeps a still of the page under floating content that covers it", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const still = root.locator("#browser-panel img")
const native = root.getByTestId("native-Alpha")
// Reduced motion lands panels at once, so the pane keeps no flight still and captures only for the cover.
await page.emulateMedia({ reducedMotion: "reduce" })
await root.getByRole("button", { name: "Hold capture", exact: true }).click()
await root.getByRole("button", { name: "Toggle popover", exact: true }).click()
await expect(root.getByText("Captures: 1", { exact: true })).toBeVisible()
// The native page stays up until its still is ready, so the pane never shows blank.
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
// Main hides the native page at once, so the pane hides it only after a frame has presented the still.
const order = await native.evaluateHandle((element) => {
const events: string[] = []
new PerformanceObserver((list) => {
if (list.getEntries().length > 0) events.push("still presented")
}).observe({ type: "element" })
new MutationObserver(() => {
if (element.getAttribute("data-visible") === "false") events.push("page hidden")
}).observe(element, { attributeFilter: ["data-visible"] })
return events
})
await root.getByRole("button", { name: "Release capture", exact: true }).click()
await expect(native).toHaveAttribute("data-visible", "false")
await expect(still).toBeVisible()
expect(await order.jsonValue()).toEqual(["still presented", "page hidden"])
await root.getByRole("button", { name: "Toggle popover", exact: true }).click()
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
await expect(root.getByText("Captures: 1", { exact: true })).toBeVisible()
})
story(
"freezes on the latest capture when floating content closes and reopens before a capture lands",
async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const popover = root.getByRole("button", { name: "Toggle popover", exact: true })
const still = root.locator("#browser-panel img")
const native = root.getByTestId("native-Alpha")
await page.emulateMedia({ reducedMotion: "reduce" })
await root.getByRole("button", { name: "Hold capture", exact: true }).click()
await popover.click()
await expect(root.getByText("Captures: 1", { exact: true })).toBeVisible()
await popover.click()
await expect(page.locator("[data-popper-positioner]")).toHaveCount(0)
// The pane measures the overlap on the next animation frame; nothing on screen changes while the capture is held.
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))))
await popover.click()
await expect(root.getByText("Captures: 2", { exact: true })).toBeVisible()
// Both captures land in order; the first one belongs to the cover that already closed.
await root.getByRole("button", { name: "Release capture", exact: true }).click()
await expect(native).toHaveAttribute("data-visible", "false")
await expect(still).toBeVisible()
// The fixture draws each capture a pixel wider: the second one is 5 pixels wide.
await expect(still).toHaveJSProperty("naturalWidth", 5)
},
)
story("keeps a still of the page under an open dialog instead of blanking the pane", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const still = root.locator("#browser-panel img")
const native = root.getByTestId("native-Alpha")
const toggle = root.getByRole("button", { name: "Toggle dialog", exact: true })
await root.getByRole("button", { name: "Hold capture", exact: true }).click()
// Read in the same task as the click: the page must not hide before its still exists.
const visible = await toggle.evaluate((element) => {
element.dispatchEvent(new MouseEvent("click", { bubbles: true }))
return document.querySelector('[data-testid="native-Alpha"]')?.getAttribute("data-visible")
})
expect(visible).toBe("true")
await expect(root.getByText("Captures: 1", { exact: true })).toBeVisible()
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
await root.getByRole("button", { name: "Release capture", exact: true }).click()
await expect(native).toHaveAttribute("data-visible", "false")
await expect(still).toBeVisible()
await toggle.click()
await expect(native).toHaveAttribute("data-visible", "true")
await expect(still).toHaveCount(0)
await expect(root.getByText("Captures: 1", { exact: true })).toBeVisible()
})
story("hides the native page under a dialog even when its still never arrives", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const native = root.getByTestId("native-Alpha")
const toggle = root.getByRole("button", { name: "Toggle dialog", exact: true })
await page.clock.install()
await root.getByRole("button", { name: "Hold capture", exact: true }).click()
await toggle.click()
await page.clock.runFor(100)
await expect(native).toHaveAttribute("data-visible", "true")
// The capture never settles: after its timeout the page hides over the box background instead of the dialog.
await page.clock.runFor(5_000)
await expect(native).toHaveAttribute("data-visible", "false")
await expect(root.locator("#browser-panel img")).toHaveCount(0)
await toggle.click()
await page.clock.runFor(100)
await expect(native).toHaveAttribute("data-visible", "true")
})
story("comments on a picked element over a still of the page", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("combobox", { name: "Picked element scenario" }).selectOption("zoomed")
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
await picker.click()
await expect(picker).toHaveAttribute("aria-pressed", "true")
await expect(root.getByText("Picker: on", { exact: true })).toBeVisible()
await expect(root.getByText("Session getter reads: 0", { exact: true })).toBeVisible()
await expect(root.getByRole("status")).toHaveText(
"Click an element in the page to comment on it. Press Escape to cancel.",
)
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(picker).toHaveAttribute("aria-pressed", "false")
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await expect(editor).toBeFocused()
await expect(editor).toHaveAttribute("placeholder", "Add an optional comment...")
await expect(editor).toHaveAccessibleName("Comment on this element")
await expect(root.getByText("Session getter reads: 0", { exact: true })).toBeVisible()
await expect(root.locator('[data-slot="browser-comment-editor"]')).toContainText("button.primary")
// The spotlight frames the picked element in surface pixels.
await expect(root.locator('[data-slot="browser-comment-spotlight"]')).toHaveCSS("left", "38.4px")
await expect(root.locator('[data-slot="browser-comment-spotlight"]')).toHaveCSS("width", "128px")
// The native page hides behind its still so the editor can float over it.
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await expect(root.locator("#browser-panel img")).toBeVisible()
await editor.fill("Make this the primary colour")
await root.getByRole("button", { name: "Add element comment", exact: true }).click()
await expect(root.getByTestId("fixture-comments")).toHaveText("button.primary @e7: Make this the primary colour")
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(root.getByTestId("fixture-comment-metadata")).toContainText('selector \\"main > button.primary\\"')
await expect(root.getByTestId("fixture-comment-metadata")).toContainText("accessible name")
// The pane stays mounted when Beta is routed, and its picker then listens to Beta's page.
await root.getByRole("button", { name: "Beta", exact: true }).click()
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(editor).toBeFocused()
await editor.fill("Beta's button too")
await editor.press("Enter")
await expect(root.getByTestId("fixture-comments").getByRole("listitem")).toHaveText([
"button.primary @e7: Make this the primary colour",
"button.primary @e7: Beta's button too",
])
})
story("cancels the picker and a comment with Escape", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
await picker.click()
await expect(picker).toHaveAttribute("aria-pressed", "true")
await page.keyboard.press("Escape")
await expect(picker).toHaveAttribute("aria-pressed", "false")
await expect(root.getByText("Picker: off", { exact: true })).toBeVisible()
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await expect(editor).toBeFocused()
await editor.press("Escape")
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
await expect(root.getByTestId("fixture-comments")).toHaveText("")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("leaves the picker off and Escape to the app when main never answers", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
await root.getByRole("button", { name: "Drop picker requests", exact: true }).click()
await picker.click()
await expect(picker).toHaveAttribute("aria-pressed", "false")
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.click()
await expect(address).toBeFocused()
await address.press("Escape")
await expect(address).not.toBeFocused()
})
story("replaces an open page note with the comment for an element picked meanwhile", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.locator('[data-component="browser-address"]').hover()
await root.getByRole("button", { name: "Add page to session", exact: true }).click()
await expect(root.locator('[data-slot="browser-comment-editor"] textarea')).toBeFocused()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(1)
await expect(root.locator('[data-slot="browser-comment-editor"]')).toContainText("button.primary")
})
story("cleans up the originating session's picker and comment after a session switch", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
await picker.click()
await expect(root.getByText("Picker Alpha: on", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Beta", exact: true }).click()
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
await expect(root.getByText("Picker Alpha: off", { exact: true })).toBeVisible()
await expect(picker).toHaveAttribute("aria-pressed", "false")
await root.getByRole("button", { name: "Alpha", exact: true }).click()
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(root.locator('[data-slot="browser-comment-editor"] textarea')).toBeFocused()
await root.getByRole("button", { name: "Beta", exact: true }).click()
await expect(root.locator('[data-component="browser-comment"]')).toHaveCount(0)
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
await expect(root.getByText("Highlight owners: Alpha", { exact: true })).toBeVisible()
})
story("ends the native picker and highlight when the pane unmounts", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
await picker.click()
await expect(root.getByText("Picker: on", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Unmount pane", exact: true }).click()
await expect(root.locator("#browser-panel")).toHaveCount(0)
await expect(root.getByText("Picker: off", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Alpha", exact: true }).click()
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(root.locator('[data-slot="browser-comment-editor"] textarea')).toBeFocused()
await root.getByRole("button", { name: "Unmount pane", exact: true }).click()
await expect(root.locator("#browser-panel")).toHaveCount(0)
await expect(root.getByText("Highlights: clear", { exact: true })).toBeVisible()
})
story("keeps a comment draft but drops its ref when the page navigates", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Select an element to comment on", exact: true }).click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await editor.fill("Still misaligned after the reload")
await root.getByRole("button", { name: "Load current page", exact: true }).click()
await expect(editor).toHaveValue("Still misaligned after the reload")
await editor.press("Enter")
await expect(root.getByTestId("fixture-comments")).toHaveText(
"button.primary (no ref): Still misaligned after the reload",
)
})
story("grows the compact comment upward without flipping and keeps its actions inside the page", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("combobox", { name: "Picked element scenario" }).selectOption("low")
await root.getByRole("button", { name: "Select an element to comment on", exact: true }).click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const editor = root.locator('[data-slot="browser-comment-editor"]')
const field = editor.locator("textarea")
await expect(editor).toHaveCSS("width", "320px")
await expect(field).toBeFocused()
await page.evaluate(() => document.fonts.ready.then(() => undefined))
const opening = await editor.boundingBox()
if (!opening) throw new Error("Missing opening annotation")
await field.fill("A line of feedback\n".repeat(6))
await expect(field).toHaveCSS("height", "140px")
// The field lays out before ResizeObserver repositions its parent.
await expect
.poll(async () => {
const grown = await editor.boundingBox()
return !!grown && grown.y < opening.y && Math.abs(grown.y + grown.height - opening.y - opening.height) < 0.5
})
.toBe(true)
await field.fill("A line of feedback\n".repeat(30))
await expect(field).toHaveCSS("height", "160px")
expect(await field.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true)
await expect
.poll(async () => {
const surface = await root.locator('[data-component="browser-comment"]').boundingBox()
const box = await editor.boundingBox()
return !!surface && !!box && box.y >= surface.y && box.y + box.height <= surface.y + surface.height
})
.toBe(true)
const submit = editor.getByRole("button", { name: "Add element comment", exact: true })
await submit.scrollIntoViewIfNeeded()
await expect(submit).toBeInViewport()
// Near the surface's top, growth stops at the opening bottom rather than pushing the card down.
await field.press("Escape")
await root.getByRole("combobox", { name: "Picked element scenario" }).selectOption("default")
await root.getByRole("button", { name: "Select an element to comment on", exact: true }).click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(field).toBeFocused()
const nearTop = await editor.boundingBox()
if (!nearTop) throw new Error("Missing near-top annotation")
await field.fill("A line of feedback\n".repeat(30))
await expect
.poll(() => field.evaluate((element) => element.clientHeight < 160 && element.scrollHeight > element.clientHeight))
.toBe(true)
await expect
.poll(async () => {
const box = await editor.boundingBox()
return !!box && Math.abs(box.y + box.height - nearTop.y - nearTop.height) < 0.5
})
.toBe(true)
// A narrow, short pane still keeps the compact editor and action within its surface.
await root.locator("#browser-panel").evaluate((element) => {
const container = element.parentElement
if (!container) throw new Error("Missing pane container")
container.style.width = "280px"
container.style.height = "150px"
})
await expect(editor).toHaveCSS("width", "262px")
await expect
.poll(async () => {
const surface = await root.locator('[data-component="browser-comment"]').boundingBox()
const box = await editor.boundingBox()
return !!surface && !!box && box.y >= surface.y && box.y + box.height <= surface.y + surface.height
})
.toBe(true)
await expect(submit).toBeInViewport()
})
story(
"wraps the selector, attaches elements without comments, and cancels only empty drafts on clickaway",
async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const scenario = root.getByRole("combobox", { name: "Picked element scenario" })
const picker = root.getByRole("button", { name: "Select an element to comment on", exact: true })
const editor = root.locator('[data-slot="browser-comment-editor"]')
const overlay = root.locator('[data-component="browser-comment"]')
await scenario.selectOption("long")
await root.evaluate((element) => element.setAttribute("dir", "rtl"))
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
const preview = editor.locator('[data-slot="line-comment-v2-preview"] [dir="ltr"]')
const selector = `main > section[data-label="إعدادات"] > ${".nested-selector".repeat(24)} > button.primary`
await expect(preview).toHaveText(selector)
await expect(preview).toHaveAttribute("dir", "ltr")
await expect(preview).toHaveCSS("unicode-bidi", "isolate")
await expect(preview).toHaveCSS("-webkit-line-clamp", "2")
await expect(preview).toHaveCSS("height", "40px")
await preview.hover()
await expect(page.getByRole("tooltip")).toHaveText(selector)
await editor.locator("textarea").press("Enter")
await expect(overlay).toHaveCount(0)
const comments = root.getByTestId("fixture-comments").getByRole("listitem")
await expect(comments).toHaveText(["button.primary @e7:"])
await scenario.selectOption("default")
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await editor.getByRole("button", { name: "Add element", exact: true }).click()
await expect(overlay).toHaveCount(0)
await expect(comments).toHaveText(["button.primary @e7:", "button.primary @e7:"])
expect(JSON.parse(await root.getByTestId("fixture-comment-metadata").innerText())).toEqual(
[selector, "main > button.primary"].map((value) =>
expect.objectContaining({
type: "note",
label: "button.primary",
title: value,
subtitle: "Browser annotation",
comment: "",
subject: `the "button.primary" element in browser tab tab_11111111-1111-1111-1111-111111111111 at https://alpha.example/ (role button; accessible name "Save changes"; selector ${JSON.stringify(value)})`,
live: {
href: "tab_11111111-1111-1111-1111-111111111111#e7",
subject: `the "button.primary" element in browser tab tab_11111111-1111-1111-1111-111111111111 at https://alpha.example/ (role button; accessible name "Save changes"; selector ${JSON.stringify(value)}; browser ref @e7, usable as ref in any browser tool including browser.evaluate until the page navigates)`,
},
}),
),
)
await scenario.selectOption("fallback")
await picker.click()
await root.getByRole("button", { name: "Pick element", exact: true }).click()
await expect(preview).toHaveText("button.primary")
await editor.locator("textarea").fill("Keep this feedback")
await overlay.click({ position: { x: 620, y: 290 } })
await expect(editor.locator("textarea")).toHaveValue("Keep this feedback")
await editor.locator("textarea").fill("")
await overlay.click({ position: { x: 620, y: 290 } })
await expect(overlay).toHaveCount(0)
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(comments).toHaveText(["button.primary @e7:", "button.primary @e7:"])
},
)
story("shows the empty state over a blank native page and restores navigation", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Blank page", exact: true }).click()
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
await expect(root.getByText("Preview your app, read docs, or browse the web.", { exact: true })).toBeVisible()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.fill("https://example.com/")
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
await address.press("Enter")
await expect(address).not.toBeFocused()
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
})
story("keeps Stop available and hides the empty state while a blank page loads", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Loading page", exact: true }).click()
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
await root.getByRole("button", { name: "Stop", exact: true }).click()
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
})
story("keeps the submitted URL visible until the browser reports navigation", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Blank page", exact: true }).click()
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.fill("https://example.com/")
await address.press("Enter")
await expect(address).not.toBeFocused()
await expect(address).toHaveValue("https://example.com/")
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
// Focusing and leaving the field before the browser reports keeps the submitted address.
await address.focus()
await expect(address).toHaveValue("https://example.com/")
await address.press("Escape")
await expect(address).not.toBeFocused()
await expect(address).toHaveValue("https://example.com/")
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(address).toHaveValue("https://example.com/")
await address.fill("https://unsubmitted.example/")
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
await expect(address).toHaveValue("https://example.com/")
})
story("restores the current URL each time the same submitted navigation is blocked", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
for (const submission of [1, 2]) {
await story.step(`blocked submission ${submission}`, async () => {
await address.fill("https://blocked.example/")
await address.press("Enter")
await expect(address).toHaveValue("https://blocked.example/")
await root.getByRole("button", { name: "Block navigation", exact: true }).click()
await expect(root.getByRole("alert")).toHaveText("Only web pages can open here.")
await expect(address).toHaveValue("https://alpha.example/")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
}
})
story("explains why a typed address cannot open instead of searching for it", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
for (const [typed, reason] of [
["ftp://example.com", "Only web pages can open here."],
["https://user:pass@example.com", "Addresses with a user name or password can't open here."],
]) {
await address.fill(typed)
await address.press("Enter")
await expect(root.getByRole("alert")).toHaveText(reason)
await expect(address).toHaveValue("https://alpha.example/")
}
})
story("shows a themed failure state for only the failed tab and allows retry", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Failed page", exact: true }).click()
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
await expect(root.getByText("Check the URL and your connection, then try again.", { exact: true })).toBeVisible()
await expect(root.getByText("Request failed", { exact: true })).toBeHidden()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
await expect(root.getByRole("combobox", { name: "Browser address", exact: true })).toHaveValue(
"https://alpha.example/",
)
await root.getByRole("button", { name: "Beta", exact: true }).click()
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
await expect(root.getByTestId("native-Beta")).toHaveAttribute("data-visible", "true")
await root.getByRole("button", { name: "Alpha", exact: true }).click()
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Reload", exact: true }).click()
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
})
story("returns a failed tab to the empty state when an empty URL is submitted", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Failed page", exact: true }).click()
await expect(root.getByText("URL can't be reached", { exact: true })).toBeVisible()
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.fill("")
await address.press("Enter")
await expect(address).not.toBeFocused()
await expect(address).toHaveValue("")
await root.getByRole("button", { name: "Load current page", exact: true }).click()
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
await expect(root.getByText("URL can't be reached", { exact: true })).toBeHidden()
await expect(root.getByText("Open a page", { exact: true })).toBeVisible()
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeDisabled()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "false")
})
story("selects the full URL when the address field gains focus", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.click()
await expect(address).toHaveJSProperty("selectionStart", 0)
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
await address.pressSequentially("https://example.com/")
await expect(address).toHaveValue("https://example.com/")
await address.press("Enter")
await expect(address).not.toBeFocused()
await address.focus()
await expect(address).toHaveJSProperty("selectionStart", 0)
await expect(address).toHaveJSProperty("selectionEnd", "https://example.com/".length)
// A click on the focused field places the caret instead of selecting the URL again.
await address.press("ArrowRight")
await address.click()
await expect
.poll(() => address.evaluate((input: HTMLInputElement) => input.selectionStart === input.selectionEnd))
.toBe(true)
})
story("draws only the address at rest when focus leaves the window with the field selected", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.click()
// Chromium blurs the field when the window or the native page takes focus, and the field keeps its full selection.
await address.evaluate((input: HTMLInputElement) => {
input.dispatchEvent(new FocusEvent("blur"))
input.select()
})
await expect(address).toHaveJSProperty("selectionStart", 0)
await expect(address).toHaveJSProperty("selectionEnd", "https://alpha.example/".length)
await page.mouse.move(0, 0)
// Computed ::selection styles do not report the default highlight, so compare the input's own paint with the
// selection collapsed. The drawn address is hidden: a selection layer can change how its text is antialiased.
await page.addStyleTag({ content: '[data-slot="browser-address-display"] { visibility: hidden }' })
const field = root.locator('[data-component="browser-address"]')
const selected = (await field.screenshot()).toString("base64")
await address.evaluate((input: HTMLInputElement) => input.setSelectionRange(0, 0))
const collapsed = (await field.screenshot()).toString("base64")
const changed = await page.evaluate(
async (shots) => {
const pixels = await Promise.all(
shots.map(async (data) => {
const image = new Image()
image.src = `data:image/png;base64,${data}`
await image.decode()
const canvas = new OffscreenCanvas(image.width, image.height)
canvas.getContext("2d")?.drawImage(image, 0, 0)
return canvas.getContext("2d")?.getImageData(0, 0, image.width, image.height).data ?? new Uint8ClampedArray()
}),
)
return Array.from({ length: (pixels[0]?.length ?? 0) / 4 }, (_, index) => index * 4).filter((offset) =>
[0, 1, 2].some((channel) => pixels[0]?.[offset + channel] !== pixels[1]?.[offset + channel]),
).length
},
[selected, collapsed],
)
expect(changed).toBe(0)
})
story("keeps the current page visible while a submitted URL loads", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Delay navigation", exact: true }).click()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.fill("https://example.com/")
await address.press("Enter")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(address).toHaveValue("https://example.com/")
await root.getByRole("button", { name: "Load current page", exact: true }).click()
await expect(root.getByRole("button", { name: "Stop", exact: true })).toBeEnabled()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await root.getByRole("button", { name: "Complete navigation", exact: true }).click()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(address).toHaveValue("https://example.com/")
})
story("keeps the current page and restores its URL when an empty address is submitted", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.fill("")
await address.press("Enter")
await expect(address).not.toBeFocused()
await expect(address).toHaveValue("https://alpha.example/")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
await expect(root.getByText("Open a page", { exact: true })).toBeHidden()
await expect(root.getByRole("button", { name: "Reload", exact: true })).toBeEnabled()
})
story("suggests visited pages and opens the first one that completes the typed address", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Seed history", exact: true }).click()
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await address.click()
await address.pressSequentially("localhost")
const options = page.getByRole("listbox", { name: "Suggestions" }).getByRole("option")
await expect(options).toHaveCount(1)
await expect(options.first()).toHaveAttribute("aria-selected", "true")
// Arrow keys that pick IME candidates leave the suggestions alone.
await address.dispatchEvent("keydown", { key: "ArrowDown", isComposing: true })
await expect(options.first()).toHaveAttribute("aria-selected", "true")
await address.press("Enter")
await expect(address).toHaveValue("http://localhost:5173/settings")
// A search query also opens the first match; stepping past the list leaves the typed text, which searches.
await address.click()
await address.pressSequentially("color format")
await expect(options).toHaveCount(1)
await expect(options.first()).toHaveAttribute("aria-selected", "true")
await address.press("ArrowDown")
await expect(options.first()).toHaveAttribute("aria-selected", "false")
await address.press("Enter")
await expect(address).toHaveValue("https://www.google.com/search?q=color%20format")
})
story("lists recent pages on a blank tab and opens one", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const address = root.getByRole("combobox", { name: "Browser address", exact: true })
await root.getByRole("button", { name: "Seed history", exact: true }).click()
await root.getByRole("button", { name: "Blank page", exact: true }).click()
await expect(root.getByText("Recent", { exact: true })).toBeVisible()
// The globe at the field's start belongs to the field: a click there focuses it.
await expect(address).not.toBeFocused()
await root.locator('[data-component="browser-address"]').click({ position: { x: 16, y: 14 } })
await expect(address).toBeFocused()
await address.blur()
await root.getByRole("button", { name: /^Settings - Preview/ }).click()
await expect(address).toHaveValue("http://localhost:5173/settings")
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
// The opened page owns the field afterwards: an edit that is never submitted shows its URL again.
await address.click()
await address.fill("unsubmitted")
await address.blur()
await expect(address).toHaveValue("http://localhost:5173/settings")
})
story("adds the whole page to the session and keeps the draft when the button is pressed again", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const field = root.locator('[data-component="browser-address"]')
const add = root.getByRole("button", { name: "Add page to session", exact: true })
await field.hover()
await add.click()
const editor = root.locator('[data-slot="browser-comment-editor"] textarea')
await editor.fill("Check the header layout")
await field.hover()
await add.click()
await expect(editor).toHaveValue("Check the header layout")
await editor.press("Enter")
await expect(root.getByTestId("fixture-comments").getByRole("listitem")).toHaveText([/: Check the header layout$/])
})
story("shows the page's cookies in the site information and clears them", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Site information", exact: true }).click()
const site = page.getByRole("dialog")
await expect(site).toContainText("3 cookies in use")
await site.getByRole("button", { name: "Clear", exact: true }).click()
await expect(site).toContainText("0 cookies in use")
})
story("says a failed cookie read failed and reads again on retry", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
await root.getByRole("button", { name: "Fail next site read", exact: true }).click()
await root.getByRole("button", { name: "Site information", exact: true }).click()
const site = page.getByRole("dialog")
await expect(site).toContainText("Request failed")
const retry = site.getByRole("button", { name: "Try again", exact: true })
await expect(retry).toBeEnabled()
await retry.click()
await expect(site).toContainText("3 cookies in use")
await expect(site.getByRole("button", { name: "Clear", exact: true })).toBeEnabled()
})
story("zooms the page from the browser options in view and keeps its zoom in the address field", async ({ page }) => {
const root = page.getByTestId("browser-pane-fixture")
const options = root.getByRole("button", { name: "Browser options", exact: true })
await options.click()
await page.getByRole("menuitem", { name: /^Zoom in/ }).click()
await expect(root.getByText("Zoom: 110", { exact: true })).toBeVisible()
// The menu covered the page with a still; it closes so the zoomed page shows at once.
await expect(page.getByRole("menu")).toBeHidden()
await expect(root.getByTestId("native-Alpha")).toHaveAttribute("data-visible", "true")
const zoom = root.getByRole("button", { name: "110%", exact: true })
await expect(zoom).toBeVisible()
await zoom.click()
await expect(root.getByText("Zoom: 100", { exact: true })).toBeVisible()
await expect(zoom).toBeHidden()
})