/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; /** * This test verifies that when maximized, any browser chrome decoration is removed * so that chrome blocks are flush against the window edges. Any corner touching a * window edge has its radius removed to eliminate non-interative pixels. * *inner* corners are left as-is (rounded in nova) * * Split-view panels are excluded from these assertions: in both pref states they * are deliberately inset cards that keep their inline margin and rounded * corners. */ const NOVA_ENABLED = Services.prefs.getBoolPref("browser.nova.enabled", false); // Allow for sub-pixel rounding const EDGE_TOLERANCE = 1; function getCustomPropertyPx(win, el, name) { return parseFloat(win.getComputedStyle(el).getPropertyValue(name)) || 0; } /** * The nova chrome-block radius is platform-dependent. Get the actual value rather * than assuming a non-zero value, so the "corner stays rounded" assertions only run * when necessary. */ function getBlockRadius(win) { return getCustomPropertyPx( win, win.document.documentElement, "--chrome-block-radius" ); } /** * The corner where the content area meets the sidebar and the toolbox uses the * same radius with and without nova. */ function getContentCornerRadius(win) { return getCustomPropertyPx( win, win.document.documentElement, "--border-radius-medium" ); } function getChromeBlocks(win, extraSelectors = "") { // although deeply nested inside #browser, the .browserContainer elements // are where borders and corners live, so we measure those. let selector = ".chrome-block, .browserContainer"; if (extraSelectors) { selector += ", " + extraSelectors; } let els = new Set(win.document.querySelectorAll(selector)); // Split-view panels are deliberately inset cards rather than window-edge // blocks; assertSplitViewPanels covers them instead. return [...els].filter( el => !el.closest(".split-view-panel") && BrowserTestUtils.isVisible(el) ); } function blockLabel(el) { return el.id || el.className.toString().split(/\s+/)[0] || el.localName; } function assertFlushToWindowEdges(win, blocks, label) { let width = win.innerWidth; let height = win.innerHeight; let rects = blocks.map(b => b.getBoundingClientRect()); let minLeft = Math.min(...rects.map(r => r.left)); let minTop = Math.min(...rects.map(r => r.top)); let maxRight = Math.max(...rects.map(r => r.right)); let maxBottom = Math.max(...rects.map(r => r.bottom)); Assert.equal( Math.floor(minLeft), 0, `${label}: chrome reaches the inline-start window edge (got ${minLeft})` ); Assert.equal( Math.floor(minTop), 0, `${label}: chrome reaches the top window edge (got ${minTop})` ); Assert.lessOrEqual( Math.abs(maxRight - width), EDGE_TOLERANCE, `${label}: chrome reaches the inline-end window edge (got ${maxRight}, want ${width})` ); Assert.lessOrEqual( Math.abs(maxBottom - height), EDGE_TOLERANCE, `${label}: chrome reaches the bottom window edge (got ${maxBottom}, want ${height})` ); } function getWindowEdges(win, el) { let rect = el.getBoundingClientRect(); return { atLeft: Math.abs(rect.left) <= EDGE_TOLERANCE, atRight: Math.abs(rect.right - win.innerWidth) <= EDGE_TOLERANCE, atTop: Math.abs(rect.top) <= EDGE_TOLERANCE, atBottom: Math.abs(rect.bottom - win.innerHeight) <= EDGE_TOLERANCE, }; } /** * A border drawn against a window edge would be a stray 1px line hugging the * screen, so every side of a block that meets a window edge must be * border-less. */ function assertEdgeBordersRemoved(win, blocks, label) { for (let block of blocks) { let edges = getWindowEdges(win, block); let style = win.getComputedStyle(block); let sides = [ ["borderLeftWidth", edges.atLeft, "left"], ["borderRightWidth", edges.atRight, "right"], ["borderTopWidth", edges.atTop, "top"], ["borderBottomWidth", edges.atBottom, "bottom"], ]; for (let [prop, atEdge, name] of sides) { if (atEdge) { Assert.equal( parseFloat(style[prop]) || 0, 0, `${label}: ${blockLabel(block)} has no ${name} border (touches the window edge)` ); } } } } /** * For every chrome block, a corner whose x sits on a vertical window edge OR * whose y sits on a horizontal window edge must be squared. * When Nova is enabled, require that inner corners keep a > 0 radius. */ function assertCornerRadii(win, blocks, { nova, requireInnerRounded }, label) { for (let block of blocks) { let style = win.getComputedStyle(block); let { atLeft, atRight, atTop, atBottom } = getWindowEdges(win, block); let corners = [ { name: "top-left", prop: "borderTopLeftRadius", atEdge: atLeft || atTop, }, { name: "top-right", prop: "borderTopRightRadius", atEdge: atRight || atTop, }, { name: "bottom-left", prop: "borderBottomLeftRadius", atEdge: atLeft || atBottom, }, { name: "bottom-right", prop: "borderBottomRightRadius", atEdge: atRight || atBottom, }, ]; let name = blockLabel(block); let innerRoundedCount = 0; for (let corner of corners) { let radius = parseFloat(style[corner.prop]) || 0; if (corner.atEdge) { Assert.equal( radius, 0, `${label}: ${name} ${corner.name} is squared (touches the window edge)` ); } else if (radius > 0) { innerRoundedCount++; } } // A .browserContainer with a sidebar/toolbox-facing inner corner should keep // it rounded under Nova. Only assert on the content area, and only when the // caller opts in. if ( nova && requireInnerRounded && getContentCornerRadius(win) > 0 && block.classList.contains("browserContainer") ) { let innerCorners = corners.filter(c => !c.atEdge).length; if (innerCorners) { Assert.greater( innerRoundedCount, 0, `${label}: ${name} keeps at least one inner corner rounded` ); } } } } function assertSeparators(win, blocks, label) { for (let block of blocks) { if (block.classList.contains("browserContainer")) { continue; } let style = win.getComputedStyle(block); for (let prop of [ "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", ]) { Assert.equal( parseFloat(style[prop]) || 0, 0, `${label}: ${blockLabel(block)} draws no border of its own (${prop})` ); } } let contentAreas = blocks.filter(b => b.classList.contains("browserContainer") ); Assert.greater( contentAreas.length, 0, `${label}: the content area is present` ); // The content area only separates itself from a sidebar that is actually // there; against a window edge it stays border-less. let sidebarShown = win.document .getElementById("tabbrowser-tabbox") .hasAttribute("sidebar-shown"); for (let block of contentAreas) { let style = win.getComputedStyle(block); Assert.greater( parseFloat(style.borderTopWidth) || 0, 0, `${label}: ${blockLabel(block)} keeps the separator facing the toolbox` ); if (!sidebarShown) { continue; } let inlineBorder = (parseFloat(style.borderLeftWidth) || 0) + (parseFloat(style.borderRightWidth) || 0); Assert.greater( inlineBorder, 0, `${label}: ${blockLabel(block)} keeps the separator facing the sidebar` ); } } const CORNER_RADIUS_PROPS = [ "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius", ]; /** * Split-view panels are inset cards rather than window-edge blocks. In both * pref states they intentionally keep an inline margin and a fully rounded * .browserContainer (--border-radius-medium without nova, * --chrome-block-radius with it), so maximizing must not square those corners * or pull the panels flush against the window. */ function assertSplitViewPanels(win, label) { let tabpanels = win.document.getElementById("tabbrowser-tabpanels"); let panels = [ ...tabpanels.querySelectorAll(".split-view-panel.split-view-panel-active"), ].filter(el => BrowserTestUtils.isVisible(el)); Assert.greater(panels.length, 0, `${label}: split-view panels are present`); // Only the inline-end margin is checked: with nova and a sidebar shown, the // inline-start panel deliberately drops its margin because the #browser gap // beside the sidebar already supplies the separation. let outer = tabpanels.getBoundingClientRect(); let rects = panels.map(p => p.getBoundingClientRect()); Assert.greater( outer.right - Math.max(...rects.map(r => r.right)), 0, `${label}: panels keep their inline-end margin` ); for (let panel of panels) { let container = panel.querySelector(".browserContainer"); let style = win.getComputedStyle(container); let expected = NOVA_ENABLED ? getBlockRadius(win) : getCustomPropertyPx(win, container, "--border-radius-medium"); for (let prop of CORNER_RADIUS_PROPS) { Assert.equal( parseFloat(style[prop]) || 0, expected, `${label}: panel ${prop} matches the unsplit block radius` ); } } } /** * Runs the universal decoration assertions, plus the nova-only checks when * `browser.nova.enabled` is set. `requireInnerRounded` and `checkSeparator` are * only meaningful under nova, and are further opt-in per state (the split-view * and customize states have no unsplit content area to check). */ function assertDecoration( win, blocks, label, { requireInnerRounded = false, checkSeparator = false } = {} ) { if (NOVA_ENABLED && requireInnerRounded && !getContentCornerRadius(win)) { info( `${label}: --border-radius-medium is 0 on this platform; ` + `skipping the inner-corner-rounded assertions.` ); } assertFlushToWindowEdges(win, blocks, label); assertEdgeBordersRemoved(win, blocks, label); assertCornerRadii( win, blocks, { nova: NOVA_ENABLED, requireInnerRounded }, label ); if (NOVA_ENABLED && checkSeparator) { assertSeparators(win, blocks, label); } } const BASE_PREFS = [["sidebar.revamp", true]]; add_task(async function test_vertical_tabs_sidebar_start() { await withMaximizedWindow( { prefs: [ ...BASE_PREFS, ["sidebar.verticalTabs", true], ["sidebar.position_start", true], ], }, win => { let blocks = getChromeBlocks(win); assertDecoration(win, blocks, "vertical tabs, sidebar start", { requireInnerRounded: true, checkSeparator: true, }); } ); }); add_task(async function test_vertical_tabs_sidebar_end() { await withMaximizedWindow( { prefs: [ ...BASE_PREFS, ["sidebar.verticalTabs", true], ["sidebar.position_start", false], ], }, win => { let blocks = getChromeBlocks(win); assertDecoration(win, blocks, "vertical tabs, sidebar end", { requireInnerRounded: true, checkSeparator: true, }); } ); }); add_task(async function test_compact_density() { await withMaximizedWindow( { density: gUIDensity.MODE_COMPACT, prefs: [ ...BASE_PREFS, ["sidebar.verticalTabs", true], ["sidebar.position_start", true], ], }, win => { let blocks = getChromeBlocks(win); assertDecoration(win, blocks, "compact density", { requireInnerRounded: true, checkSeparator: true, }); } ); }); add_task(async function test_horizontal_tabs() { await withMaximizedWindow( { prefs: [...BASE_PREFS, ["sidebar.verticalTabs", false]] }, win => { let blocks = getChromeBlocks(win); assertDecoration(win, blocks, "horizontal tabs", { checkSeparator: true, }); } ); }); add_task(async function test_private_window() { await withMaximizedWindow( { privateWindow: true, prefs: [ ...BASE_PREFS, ["sidebar.verticalTabs", true], ["sidebar.position_start", true], ], }, win => { let blocks = getChromeBlocks(win); assertDecoration(win, blocks, "private window", { checkSeparator: true }); } ); }); add_task(async function test_split_view() { await withMaximizedWindow( { prefs: [ ...BASE_PREFS, ["sidebar.verticalTabs", true], ["browser.tabs.splitView.enabled", true], ], }, async win => { if (typeof win.gBrowser.addTabSplitView != "function") { info("addTabSplitView unavailable; skipping split-view assertions."); ok(true, "split view API unavailable - skipped"); return; } let tab1 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, "about:blank" ); let tab2 = await BrowserTestUtils.openNewForegroundTab( win.gBrowser, "about:blank" ); win.gBrowser.addTabSplitView([tab1, tab2]); await BrowserTestUtils.switchTab(win.gBrowser, tab1); let tabpanels = win.document.getElementById("tabbrowser-tabpanels"); await BrowserTestUtils.waitForMutationCondition( tabpanels, { attributes: true, attributeFilter: ["splitview"] }, () => tabpanels.hasAttribute("splitview") ); await win.promiseDocumentFlushed(() => {}); let blocks = getChromeBlocks(win); // The active split panel has no separator border (it uses a focus // outline), so skip the border check here. assertDecoration(win, blocks, "split view"); assertSplitViewPanels(win, "split view"); } ); }); add_task(async function test_customize_mode() { await withMaximizedWindow( { prefs: [...BASE_PREFS, ["sidebar.verticalTabs", true]] }, async win => { let customizeDone = BrowserTestUtils.waitForEvent( win.gNavToolbox, "customizationready" ); win.gCustomizeMode.enter(); await customizeDone; await win.promiseDocumentFlushed(() => {}); let blocks = getChromeBlocks(win, "#customization-container"); assertDecoration(win, blocks, "customize mode"); let afterExit = BrowserTestUtils.waitForEvent( win.gNavToolbox, "aftercustomization" ); win.gCustomizeMode.exit(); await afterExit; } ); });