Expose live browser geometry and computed styles
This commit is contained in:
1 parent
52e973afc9
commit
2343681497
3 files changed
+706
-62
No files matched your search
Vendored
+315
-57
@@ -2800,11 +2800,77 @@ class Element extends Node {
|
|||||||
}
|
}
|
||||||
get offsetTop() { return this.getBoundingClientRect().top; }
|
get offsetTop() { return this.getBoundingClientRect().top; }
|
||||||
get offsetLeft() { return this.getBoundingClientRect().left; }
|
get offsetLeft() { return this.getBoundingClientRect().left; }
|
||||||
// documentElement / body / window expose VIEWPORT geometry, not their own content box.
|
// In standards mode documentElement exposes viewport client geometry.
|
||||||
// Puppeteer's #clickableBox clips boxes to document.documentElement.clientWidth/Height;
|
// Puppeteer's #clickableBox clips boxes to those dimensions; returning the
|
||||||
// returning 100x20 there made every element appear off-screen and broke .click().
|
// non-render fallback 100x20 there makes every element appear off-screen.
|
||||||
get clientWidth() { return this._isViewportRoot() ? (globalThis.innerWidth || 1280) : 100; }
|
get clientWidth() {
|
||||||
get clientHeight() { return this._isViewportRoot() ? (globalThis.innerHeight || 720) : 20; }
|
// In standards mode only the root element exposes the viewport. Body is
|
||||||
|
// an ordinary box; treating it as another viewport breaks libraries that
|
||||||
|
// measure the page's body or a full-viewport sizing sentinel.
|
||||||
|
if (this.tagName === 'HTML') return globalThis.innerWidth || 1280;
|
||||||
|
const metrics = this._renderClientMetrics();
|
||||||
|
return metrics ? metrics.width : 100;
|
||||||
|
}
|
||||||
|
get clientHeight() {
|
||||||
|
if (this.tagName === 'HTML') return globalThis.innerHeight || 720;
|
||||||
|
const metrics = this._renderClientMetrics();
|
||||||
|
return metrics ? metrics.height : 20;
|
||||||
|
}
|
||||||
|
_renderClientMetrics() {
|
||||||
|
if (typeof Deno.core.ops.op_layout_geometry !== 'function') return null;
|
||||||
|
try {
|
||||||
|
const raw = Deno.core.ops.op_layout_geometry(String(this._nid | 0));
|
||||||
|
if (!raw) return { width: 0, height: 0 };
|
||||||
|
const geometry = JSON.parse(raw);
|
||||||
|
if (geometry
|
||||||
|
&& Number.isFinite(geometry.clientWidth)
|
||||||
|
&& Number.isFinite(geometry.clientHeight)) {
|
||||||
|
// CSSOM View exposes Web IDL longs. The native layout retains
|
||||||
|
// subpixel precision for getBoundingClientRect(); client metrics round
|
||||||
|
// to whole CSS pixels like Chromium.
|
||||||
|
return {
|
||||||
|
width: Math.round(Math.max(0, geometry.clientWidth)),
|
||||||
|
height: Math.round(Math.max(0, geometry.clientHeight)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (_error) {}
|
||||||
|
return { width: 0, height: 0 };
|
||||||
|
}
|
||||||
|
// `undefined` means this is a non-render build. `null` means the render
|
||||||
|
// engine is present but this element has no associated CSS box (for
|
||||||
|
// example, display:none or a detached element). Keep those states distinct:
|
||||||
|
// CSSOM View returns an empty rect list for the latter, while the former
|
||||||
|
// deliberately retains Obscura's compatibility geometry.
|
||||||
|
_renderBoxGeometry() {
|
||||||
|
if (typeof Deno.core.ops.op_layout_geometry !== 'function') return undefined;
|
||||||
|
try {
|
||||||
|
const raw = Deno.core.ops.op_layout_geometry(String(this._nid | 0));
|
||||||
|
if (!raw) return null;
|
||||||
|
const geometry = JSON.parse(raw);
|
||||||
|
if (geometry
|
||||||
|
&& Number.isFinite(geometry.x)
|
||||||
|
&& Number.isFinite(geometry.y)
|
||||||
|
&& Number.isFinite(geometry.width)
|
||||||
|
&& Number.isFinite(geometry.height)) {
|
||||||
|
return geometry;
|
||||||
|
}
|
||||||
|
} catch (_error) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_rectFromRenderGeometry(geometry) {
|
||||||
|
const x = geometry.x, y = geometry.y;
|
||||||
|
const width = geometry.width, height = geometry.height;
|
||||||
|
const rect = {
|
||||||
|
x, y, width, height,
|
||||||
|
top: y, right: x + width, bottom: y + height, left: x,
|
||||||
|
toJSON() { return this; },
|
||||||
|
};
|
||||||
|
Object.defineProperty(rect, "__obscuraViewportFixed", {
|
||||||
|
value: !!geometry.viewportFixed,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
get scrollWidth() {
|
get scrollWidth() {
|
||||||
if (!this._isViewportRoot()) return 100;
|
if (!this._isViewportRoot()) return 100;
|
||||||
const metrics = this._renderScrollMetrics();
|
const metrics = this._renderScrollMetrics();
|
||||||
@@ -2905,26 +2971,16 @@ class Element extends Node {
|
|||||||
// Real layout when the render feature is compiled in: ask the Rust layout
|
// Real layout when the render feature is compiled in: ask the Rust layout
|
||||||
// cache for this element's border box. The op is absent in the default
|
// cache for this element's border box. The op is absent in the default
|
||||||
// build, so probe with typeof and fall through to the synthetic rect below.
|
// build, so probe with typeof and fall through to the synthetic rect below.
|
||||||
if (typeof Deno.core.ops.op_layout_geometry === 'function') {
|
const geometry = this._renderBoxGeometry();
|
||||||
try {
|
if (geometry !== undefined) {
|
||||||
var _geom = Deno.core.ops.op_layout_geometry(String(this._nid | 0));
|
if (geometry) return this._rectFromRenderGeometry(geometry);
|
||||||
if (_geom) {
|
// CSSOM View: an element without an associated box has an all-zero
|
||||||
var _r = JSON.parse(_geom);
|
// bounding rect. Do not leak the non-render 100x20 compatibility cell.
|
||||||
if (_r && typeof _r.width === 'number') {
|
return {
|
||||||
var _x = _r.x, _y = _r.y, _w = _r.width, _h = _r.height;
|
x: 0, y: 0, width: 0, height: 0,
|
||||||
var _rect = {
|
top: 0, right: 0, bottom: 0, left: 0,
|
||||||
x: _x, y: _y, width: _w, height: _h,
|
toJSON() { return this; },
|
||||||
top: _y, right: _x + _w, bottom: _y + _h, left: _x,
|
};
|
||||||
toJSON() { return this; },
|
|
||||||
};
|
|
||||||
Object.defineProperty(_rect, "__obscuraViewportFixed", {
|
|
||||||
value: !!_r.viewportFixed,
|
|
||||||
enumerable: false,
|
|
||||||
});
|
|
||||||
return _rect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_e) {}
|
|
||||||
}
|
}
|
||||||
// Default (non-render) builds keep viewport-sized roots. Without this
|
// Default (non-render) builds keep viewport-sized roots. Without this
|
||||||
// synthetic fallback every hit test against them clips down to a 100x20
|
// synthetic fallback every hit test against them clips down to a 100x20
|
||||||
@@ -2938,9 +2994,9 @@ class Element extends Node {
|
|||||||
toJSON() { return this; },
|
toJSON() { return this; },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// No layout engine (default build) or this node has no box: synthesize a
|
// No layout engine (default build): synthesize a deterministic position
|
||||||
// deterministic position from the node id so Playwright's actionability
|
// from the node id so Playwright's actionability polling still gets a
|
||||||
// polling still gets a stable, distinct rect for hit-testing (issue #45).
|
// stable, distinct rect for hit-testing (issue #45).
|
||||||
// Every nid maps to a unique cell in a 12-column grid for a 1280x720 viewport.
|
// Every nid maps to a unique cell in a 12-column grid for a 1280x720 viewport.
|
||||||
const VW = 1280, VH = 720, COLS = 12, CW = 100, CH = 20, GX = 110, GY = 30;
|
const VW = 1280, VH = 720, COLS = 12, CW = 100, CH = 20, GX = 110, GY = 30;
|
||||||
const rowsPerScreen = Math.max(1, Math.floor((VH - 10) / GY));
|
const rowsPerScreen = Math.max(1, Math.floor((VH - 10) / GY));
|
||||||
@@ -2955,7 +3011,14 @@ class Element extends Node {
|
|||||||
toJSON() { return this; },
|
toJSON() { return this; },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
getClientRects() { return new DOMRectList([this.getBoundingClientRect()]); }
|
getClientRects() {
|
||||||
|
const geometry = this._renderBoxGeometry();
|
||||||
|
if (geometry === null) return new DOMRectList([]);
|
||||||
|
if (geometry !== undefined) {
|
||||||
|
return new DOMRectList([this._rectFromRenderGeometry(geometry)]);
|
||||||
|
}
|
||||||
|
return new DOMRectList([this.getBoundingClientRect()]);
|
||||||
|
}
|
||||||
// No layout engine: a stub that always returns true unblocks Playwright's
|
// No layout engine: a stub that always returns true unblocks Playwright's
|
||||||
// actionability polling. With a real layout we'd check display, visibility,
|
// actionability polling. With a real layout we'd check display, visibility,
|
||||||
// opacity and rect dimensions per spec.
|
// opacity and rect dimensions per spec.
|
||||||
@@ -5594,36 +5657,218 @@ if (typeof TextDecoder === 'undefined') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _splitMediaQueryList(input) {
|
||||||
|
const result = [];
|
||||||
|
let start = 0, depth = 0, quote = '';
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const ch = input[i];
|
||||||
|
if (quote) {
|
||||||
|
if (ch === '\\') i++;
|
||||||
|
else if (ch === quote) quote = '';
|
||||||
|
} else if (ch === '"' || ch === "'") {
|
||||||
|
quote = ch;
|
||||||
|
} else if (ch === '(') {
|
||||||
|
depth++;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
depth--;
|
||||||
|
if (depth < 0) return null;
|
||||||
|
} else if (ch === ',' && depth === 0) {
|
||||||
|
result.push(input.slice(start, i));
|
||||||
|
start = i + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (depth !== 0 || quote) return null;
|
||||||
|
result.push(input.slice(start));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _splitMediaAnd(input) {
|
||||||
|
const result = [];
|
||||||
|
let start = 0, depth = 0, quote = '';
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const ch = input[i];
|
||||||
|
if (quote) {
|
||||||
|
if (ch === '\\') i++;
|
||||||
|
else if (ch === quote) quote = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (ch === '"' || ch === "'") { quote = ch; continue; }
|
||||||
|
if (ch === '(') { depth++; continue; }
|
||||||
|
if (ch === ')') { depth--; continue; }
|
||||||
|
if (depth === 0 && input.slice(i, i + 3).toLowerCase() === 'and'
|
||||||
|
&& (i === 0 || /\s/.test(input[i - 1]))
|
||||||
|
&& (i + 3 === input.length || /\s/.test(input[i + 3]))) {
|
||||||
|
result.push(input.slice(start, i));
|
||||||
|
start = i + 3;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(input.slice(start));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _mediaViewportDimension(name) {
|
||||||
|
const value = name === 'width' ? Number(globalThis.innerWidth) : Number(globalThis.innerHeight);
|
||||||
|
if (Number.isFinite(value)) return value;
|
||||||
|
return name === 'width' ? 1440 : 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _parseMediaPx(value) {
|
||||||
|
const match = String(value).trim().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(px)?$/i);
|
||||||
|
if (!match || (!match[2] && Number(match[1]) !== 0)) return null;
|
||||||
|
const result = Number(match[1]);
|
||||||
|
return Number.isFinite(result) ? result : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _compareMediaValues(left, operator, right) {
|
||||||
|
if (operator === '<') return left < right;
|
||||||
|
if (operator === '<=') return left <= right;
|
||||||
|
if (operator === '>') return left > right;
|
||||||
|
if (operator === '>=') return left >= right;
|
||||||
|
return left === right;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _evaluateMediaDimension(feature) {
|
||||||
|
let match = feature.match(/^(min|max)-(width|height)\s*:\s*(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const expected = _parseMediaPx(match[3]);
|
||||||
|
if (expected === null) return false;
|
||||||
|
const actual = _mediaViewportDimension(match[2]);
|
||||||
|
return match[1] === 'min' ? actual >= expected : actual <= expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
match = feature.match(/^(width|height)\s*:\s*(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const expected = _parseMediaPx(match[2]);
|
||||||
|
return expected !== null && _mediaViewportDimension(match[1]) === expected;
|
||||||
|
}
|
||||||
|
|
||||||
|
match = feature.match(/^(width|height)\s*(<=|>=|=|<|>)\s*(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const expected = _parseMediaPx(match[3]);
|
||||||
|
return expected !== null
|
||||||
|
&& _compareMediaValues(_mediaViewportDimension(match[1]), match[2], expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
match = feature.match(/^(.+?)\s*(<=|>=|=|<|>)\s*(width|height)$/);
|
||||||
|
if (match) {
|
||||||
|
const expected = _parseMediaPx(match[1]);
|
||||||
|
return expected !== null
|
||||||
|
&& _compareMediaValues(expected, match[2], _mediaViewportDimension(match[3]));
|
||||||
|
}
|
||||||
|
|
||||||
|
match = feature.match(/^(.+?)\s*(<=|>=|<|>)\s*(width|height)\s*(<=|>=|<|>)\s*(.+)$/);
|
||||||
|
if (match) {
|
||||||
|
const lower = _parseMediaPx(match[1]);
|
||||||
|
const upper = _parseMediaPx(match[5]);
|
||||||
|
if (lower === null || upper === null) return false;
|
||||||
|
const actual = _mediaViewportDimension(match[3]);
|
||||||
|
return _compareMediaValues(lower, match[2], actual)
|
||||||
|
&& _compareMediaValues(actual, match[4], upper);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (feature === 'width' || feature === 'height')
|
||||||
|
return _mediaViewportDimension(feature) !== 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _evaluateMediaFeature(raw) {
|
||||||
|
let feature = raw.trim().toLowerCase();
|
||||||
|
if (feature[0] !== '(' || feature[feature.length - 1] !== ')') return false;
|
||||||
|
feature = feature.slice(1, -1).trim();
|
||||||
|
|
||||||
|
const dimension = _evaluateMediaDimension(feature);
|
||||||
|
if (dimension !== null) return dimension;
|
||||||
|
|
||||||
|
let match = feature.match(/^orientation\s*:\s*(portrait|landscape)$/);
|
||||||
|
if (match) {
|
||||||
|
const width = _mediaViewportDimension('width');
|
||||||
|
const height = _mediaViewportDimension('height');
|
||||||
|
return match[1] === 'portrait' ? height >= width : width > height;
|
||||||
|
}
|
||||||
|
|
||||||
|
match = feature.match(/^prefers-color-scheme\s*:\s*(dark|light|no-preference)$/);
|
||||||
|
if (match) return match[1] === 'light';
|
||||||
|
match = feature.match(/^prefers-reduced-motion\s*:\s*(reduce|no-preference)$/);
|
||||||
|
if (match) return match[1] === 'no-preference';
|
||||||
|
|
||||||
|
match = feature.match(/^(pointer|any-pointer)\s*:\s*(none|coarse|fine)$/);
|
||||||
|
if (match) return match[2] === 'fine';
|
||||||
|
match = feature.match(/^(hover|any-hover)\s*:\s*(none|hover)$/);
|
||||||
|
if (match) return match[2] === 'hover';
|
||||||
|
|
||||||
|
if (feature === 'color') return true;
|
||||||
|
match = feature.match(/^color\s*:\s*(\d+)$/);
|
||||||
|
if (match) return Number(match[1]) === 8;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _evaluateOneMediaQuery(raw) {
|
||||||
|
let query = raw.trim().toLowerCase();
|
||||||
|
if (!query) return false;
|
||||||
|
|
||||||
|
let negate = false;
|
||||||
|
let modifier = query.match(/^(not|only)\b\s*/);
|
||||||
|
if (modifier) {
|
||||||
|
negate = modifier[1] === 'not';
|
||||||
|
query = query.slice(modifier[0].length).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
let typeMatches = true;
|
||||||
|
if (query[0] !== '(') {
|
||||||
|
const type = query.match(/^([a-z][a-z0-9-]*)\b/i);
|
||||||
|
if (!type) return false;
|
||||||
|
typeMatches = type[1] === 'all' || type[1] === 'screen';
|
||||||
|
if (type[1] !== 'all' && type[1] !== 'screen' && type[1] !== 'print')
|
||||||
|
typeMatches = false;
|
||||||
|
query = query.slice(type[0].length).trim();
|
||||||
|
if (query) {
|
||||||
|
const conjunction = query.match(/^and\b\s*/);
|
||||||
|
if (!conjunction) return false;
|
||||||
|
query = query.slice(conjunction[0].length).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let matches = typeMatches;
|
||||||
|
if (query) {
|
||||||
|
const conditions = _splitMediaAnd(query);
|
||||||
|
if (!conditions.length || conditions.some(condition => !condition.trim())) return false;
|
||||||
|
matches = matches && conditions.every(_evaluateMediaFeature);
|
||||||
|
}
|
||||||
|
return negate ? !matches : matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _evaluateMediaQueryList(query) {
|
||||||
|
const list = _splitMediaQueryList(String(query));
|
||||||
|
return !!list && list.some(_evaluateOneMediaQuery);
|
||||||
|
}
|
||||||
|
|
||||||
globalThis.matchMedia = _markNative(function matchMedia(q) {
|
globalThis.matchMedia = _markNative(function matchMedia(q) {
|
||||||
var s = (q || '').toLowerCase().replace(/\s+/g, '');
|
const media = q == null ? '' : String(q);
|
||||||
var matches = false;
|
return {
|
||||||
// Match headless Chromium's default: light color scheme. Reporting dark
|
get matches() { return _evaluateMediaQueryList(media); },
|
||||||
// here made every JS-driven site (mdBook, docs, apps) switch itself into a
|
media,
|
||||||
// dark theme, rendering nothing like a normal headless-Chrome screenshot.
|
onchange: null,
|
||||||
if (s.includes('prefers-color-scheme:dark')) matches = false;
|
addListener(){},
|
||||||
else if (s.includes('prefers-color-scheme:light')) matches = true;
|
removeListener(){},
|
||||||
else if (s.includes('prefers-color-scheme:no-preference')) matches = false;
|
addEventListener(){},
|
||||||
else if (s.includes('prefers-reduced-motion:no-preference')) matches = true;
|
removeEventListener(){},
|
||||||
else if (s.includes('prefers-reduced-motion:reduce')) matches = false;
|
dispatchEvent(){return true;}
|
||||||
else if (s.includes('any-pointer:fine')) matches = true;
|
};
|
||||||
else if (s.includes('any-pointer:coarse')) matches = false;
|
|
||||||
else if (s.includes('pointer:fine')) matches = true;
|
|
||||||
else if (s.includes('hover:hover')) matches = true;
|
|
||||||
else if (s.includes('any-hover:hover')) matches = true;
|
|
||||||
else if (s.includes('color)') || s === '(color)') matches = true;
|
|
||||||
else if (s.includes('min-width')) {
|
|
||||||
var m = s.match(/min-width:\s*(\d+)px/);
|
|
||||||
matches = m ? (globalThis.innerWidth || 1440) >= parseInt(m[1]) : false;
|
|
||||||
}
|
|
||||||
else if (s.includes('max-width')) {
|
|
||||||
var m2 = s.match(/max-width:\s*(\d+)px/);
|
|
||||||
matches = m2 ? (globalThis.innerWidth || 1440) <= parseInt(m2[1]) : false;
|
|
||||||
}
|
|
||||||
return { matches: matches, media: q, onchange: null, addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}, dispatchEvent(){return true;} };
|
|
||||||
});
|
});
|
||||||
globalThis.getComputedStyle = (el) => {
|
globalThis.getComputedStyle = (el) => {
|
||||||
if (!el) el = document.body || {};
|
if (!el) el = document.body || {};
|
||||||
const style = el?.style || el?._style || new CSSStyleDeclaration();
|
const style = el?.style || el?._style || new CSSStyleDeclaration();
|
||||||
|
// Render builds expose one immutable snapshot from the retained final
|
||||||
|
// cascade/layout. Fetch it once per CSSStyleDeclaration proxy: reading a
|
||||||
|
// dozen properties must not trigger a dozen layout/native crossings.
|
||||||
|
let rendered = null;
|
||||||
|
if (typeof Deno.core.ops.op_computed_style === 'function' && el?._nid != null) {
|
||||||
|
try {
|
||||||
|
const raw = Deno.core.ops.op_computed_style(String(el._nid | 0));
|
||||||
|
rendered = raw ? JSON.parse(raw) : null;
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
// React virtualization libraries (react-window, tanstack-virtual,
|
// React virtualization libraries (react-window, tanstack-virtual,
|
||||||
// react-virtuoso) all compute container dimensions via getComputedStyle.
|
// react-virtuoso) all compute container dimensions via getComputedStyle.
|
||||||
// The defaults table previously returned `auto` for width/height and
|
// The defaults table previously returned `auto` for width/height and
|
||||||
@@ -5679,10 +5924,13 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
|
|
||||||
const lookup = (rawProp) => {
|
const lookup = (rawProp) => {
|
||||||
if (typeof rawProp !== 'string') return '';
|
if (typeof rawProp !== 'string') return '';
|
||||||
// Inline value first.
|
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
||||||
|
if (rendered && Object.prototype.hasOwnProperty.call(rendered, kebab))
|
||||||
|
return rendered[kebab];
|
||||||
|
// Non-render builds and properties outside the renderer snapshot retain
|
||||||
|
// the lightweight inline CSSOM behavior.
|
||||||
const inlineVal = target.getPropertyValue ? target.getPropertyValue(rawProp) : '';
|
const inlineVal = target.getPropertyValue ? target.getPropertyValue(rawProp) : '';
|
||||||
if (inlineVal) return inlineVal;
|
if (inlineVal) return inlineVal;
|
||||||
const kebab = rawProp.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
||||||
const dim = dimensionFor(kebab);
|
const dim = dimensionFor(kebab);
|
||||||
if (dim != null) return dim;
|
if (dim != null) return dim;
|
||||||
if (defaultsKebab[rawProp]) return defaultsKebab[rawProp];
|
if (defaultsKebab[rawProp]) return defaultsKebab[rawProp];
|
||||||
@@ -5694,13 +5942,23 @@ globalThis.getComputedStyle = (el) => {
|
|||||||
return new Proxy(style, {
|
return new Proxy(style, {
|
||||||
get(_, prop) {
|
get(_, prop) {
|
||||||
if (prop === Symbol.toPrimitive || prop === Symbol.toStringTag) return undefined;
|
if (prop === Symbol.toPrimitive || prop === Symbol.toStringTag) return undefined;
|
||||||
if (prop in target) return target[prop];
|
|
||||||
if (prop === 'getPropertyValue') return (name) => lookup(name);
|
if (prop === 'getPropertyValue') return (name) => lookup(name);
|
||||||
if (prop === 'getPropertyPriority') return () => '';
|
if (prop === 'getPropertyPriority') return () => '';
|
||||||
if (prop === 'item') return (i) => '';
|
if (prop === 'item') return (i) => '';
|
||||||
if (prop === 'length') return 0;
|
if (prop === 'length') return 0;
|
||||||
if (prop === 'cssText') return '';
|
if (prop === 'cssText') return '';
|
||||||
if (prop === 'parentRule') return null;
|
if (prop === 'parentRule') return null;
|
||||||
|
// CSSStyleDeclaration's `has` trap intentionally reports every known
|
||||||
|
// CSS IDL property. Checking `prop in target` before this lookup therefore
|
||||||
|
// returned the empty inline declaration for e.g. computed.display and
|
||||||
|
// prevented every computed/default fallback below from running.
|
||||||
|
if (typeof prop === 'string'
|
||||||
|
&& (_CSS_PROP_SET.has(prop)
|
||||||
|
|| _CSS_PROP_SET.has(_cssKebabToCamel(prop))
|
||||||
|
|| prop.includes('-'))) {
|
||||||
|
return lookup(prop);
|
||||||
|
}
|
||||||
|
if (prop in target) return target[prop];
|
||||||
if (typeof prop === 'string') return lookup(prop);
|
if (typeof prop === 'string') return lookup(prop);
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2261,6 +2261,7 @@ pub fn build_extension() -> Extension {
|
|||||||
ops.push(op_set_dynamic_fonts());
|
ops.push(op_set_dynamic_fonts());
|
||||||
ops.push(op_image_metadata());
|
ops.push(op_image_metadata());
|
||||||
ops.push(op_layout_geometry());
|
ops.push(op_layout_geometry());
|
||||||
|
ops.push(op_computed_style());
|
||||||
ops.push(op_layout_metrics());
|
ops.push(op_layout_metrics());
|
||||||
ops.push(op_scroll_offset());
|
ops.push(op_scroll_offset());
|
||||||
ops.push(op_scroll_to());
|
ops.push(op_scroll_to());
|
||||||
@@ -2426,8 +2427,10 @@ pub(crate) fn clamp_scroll_offset(state: &mut ObscuraState, requested: (f32, f32
|
|||||||
/// cache. The cache is computed lazily on first read and cleared on navigation
|
/// cache. The cache is computed lazily on first read and cleared on navigation
|
||||||
/// (see `set_dom`). Coordinates are viewport-relative after the shared root
|
/// (see `set_dom`). Coordinates are viewport-relative after the shared root
|
||||||
/// scroll offset, except for viewport-fixed subtrees. Returns JSON
|
/// scroll offset, except for viewport-fixed subtrees. Returns JSON
|
||||||
/// `{"x","y","width","height"}` in CSS pixels, or an empty string when the
|
/// `{"x","y","width","height","clientWidth","clientHeight"}` in CSS pixels,
|
||||||
/// node has no box. Feature-gated.
|
/// or an empty string when the node has no box. The client dimensions are the
|
||||||
|
/// unscaled padding box used by CSSOM View, whereas the rect is the visual
|
||||||
|
/// viewport-relative border box. Feature-gated.
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
#[op2]
|
#[op2]
|
||||||
#[string]
|
#[string]
|
||||||
@@ -2441,15 +2444,41 @@ fn op_layout_geometry(state: &OpState, #[string] nid_str: String) -> String {
|
|||||||
let Some(rect) = prepared.viewport_rect(nid, scroll) else {
|
let Some(rect) = prepared.viewport_rect(nid, scroll) else {
|
||||||
return String::new();
|
return String::new();
|
||||||
};
|
};
|
||||||
|
let Some((client_width, client_height)) = prepared.client_size(nid) else {
|
||||||
|
return String::new();
|
||||||
|
};
|
||||||
let viewport_fixed = prepared.viewport_fixed_nodes().contains(&nid);
|
let viewport_fixed = prepared.viewport_fixed_nodes().contains(&nid);
|
||||||
return format!(
|
return format!(
|
||||||
"{{\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"viewportFixed\":{}}}",
|
"{{\"x\":{},\"y\":{},\"width\":{},\"height\":{},\"clientWidth\":{},\"clientHeight\":{},\"viewportFixed\":{}}}",
|
||||||
rect.x, rect.y, rect.width, rect.height, viewport_fixed
|
rect.x,
|
||||||
|
rect.y,
|
||||||
|
rect.width,
|
||||||
|
rect.height,
|
||||||
|
client_width,
|
||||||
|
client_height,
|
||||||
|
viewport_fixed
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
String::new()
|
String::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One renderer-computed CSS snapshot for `getComputedStyle()`. Returning all
|
||||||
|
/// supported properties together keeps a single JS style object to one native
|
||||||
|
/// call and one use of the retained prepared layout.
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[op2]
|
||||||
|
#[string]
|
||||||
|
fn op_computed_style(state: &OpState, #[string] nid_str: String) -> String {
|
||||||
|
let shared = state.borrow::<SharedState>().clone();
|
||||||
|
let nid: u32 = nid_str.parse().unwrap_or(0);
|
||||||
|
let nid = obscura_dom::tree::NodeId::new(nid);
|
||||||
|
let mut gs = shared.borrow_mut();
|
||||||
|
ensure_prepared_render(&mut gs)
|
||||||
|
.and_then(|prepared| prepared.computed_style(nid))
|
||||||
|
.and_then(|snapshot| serde_json::to_string(&snapshot).ok())
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
/// Root scrolling overflow in CSS pixels. The JS CSSOM probes this op only in
|
||||||
/// render builds; default scraping builds retain their deliberately unbounded
|
/// render builds; default scraping builds retain their deliberately unbounded
|
||||||
/// synthetic scrolling behavior.
|
/// synthetic scrolling behavior.
|
||||||
|
|||||||
@@ -2067,6 +2067,178 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_media_evaluates_query_lists_conjunctions_ranges_and_orientation() {
|
||||||
|
let dom = parse_html("<html><body></body></html>");
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(1280.0, 720.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
return [
|
||||||
|
matchMedia("(min-width: 1024px) and (min-height: 700px)").matches,
|
||||||
|
matchMedia("(min-width: 1024px) and (min-height: 900px)").matches,
|
||||||
|
matchMedia("(max-width: 600px), screen and (orientation: landscape)").matches,
|
||||||
|
matchMedia("not print").matches,
|
||||||
|
matchMedia("not screen").matches,
|
||||||
|
matchMedia("only screen and (width: 1280px) and (height = 720px)").matches,
|
||||||
|
matchMedia("(1000px <= width < 1400px) and (height > 700px)").matches,
|
||||||
|
matchMedia("(orientation: portrait)").matches,
|
||||||
|
matchMedia("(prefers-color-scheme: light) and (pointer: fine) and (hover: hover)").matches,
|
||||||
|
matchMedia("(obscura-unknown-feature: yes)").matches
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([true, false, true, true, false, true, true, false, true, false])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn match_media_matches_are_live_across_viewport_resizes() {
|
||||||
|
let dom = parse_html("<html><body></body></html>");
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(900.0, 600.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate(
|
||||||
|
r#"
|
||||||
|
return [
|
||||||
|
(globalThis.__wideAndShort = matchMedia(
|
||||||
|
"(min-width: 800px) and (max-height: 700px)"
|
||||||
|
)).matches,
|
||||||
|
(globalThis.__portrait = matchMedia(
|
||||||
|
"(orientation: portrait)"
|
||||||
|
)).matches
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
serde_json::json!([true, false])
|
||||||
|
);
|
||||||
|
|
||||||
|
rt.set_viewport(600.0, 900.0);
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate(
|
||||||
|
"return [__wideAndShort.matches, __portrait.matches,\
|
||||||
|
matchMedia('(max-width: 600px), print').matches];",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
serde_json::json!([false, true, true])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn computed_style_access_does_not_get_shadowed_by_inline_style_proxy() {
|
||||||
|
let mut rt = setup_runtime(
|
||||||
|
r#"<html><body><div id="box" style="opacity:.5;width:40px"></div></body></html>"#,
|
||||||
|
);
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const box = document.getElementById("box");
|
||||||
|
const computed = getComputedStyle(box);
|
||||||
|
return [
|
||||||
|
computed.display,
|
||||||
|
computed.visibility,
|
||||||
|
computed.opacity,
|
||||||
|
computed.width,
|
||||||
|
computed.getPropertyValue("display"),
|
||||||
|
computed.getPropertyValue("background-color")
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
serde_json::json!([
|
||||||
|
"block",
|
||||||
|
"visible",
|
||||||
|
"0.5",
|
||||||
|
"40px",
|
||||||
|
"block",
|
||||||
|
"rgba(0, 0, 0, 0)"
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn computed_style_uses_renderer_stylesheet_cascade_and_invalidates() {
|
||||||
|
let dom = parse_html(
|
||||||
|
r#"<html><head><style>
|
||||||
|
.base {
|
||||||
|
display:flex; position:relative; z-index:7;
|
||||||
|
visibility:hidden; opacity:.35;
|
||||||
|
background-color:rgb(10,20,30); color:rgb(40,50,60);
|
||||||
|
width:120px; height:40px; min-width:20px; max-width:160px;
|
||||||
|
box-sizing:border-box; overflow-x:clip; overflow-y:visible;
|
||||||
|
margin:1px 2px 3px 4px; padding:5px 6px 7px 8px;
|
||||||
|
border:2px solid rgb(70,80,90);
|
||||||
|
flex-direction:column; flex-wrap:wrap;
|
||||||
|
align-items:center; justify-content:space-between;
|
||||||
|
gap:6px 9px; transform:translate(3px,4px);
|
||||||
|
}
|
||||||
|
.alt { display:grid; width:150px; opacity:.8; }
|
||||||
|
</style></head><body><div id="box" class="base"></div></body></html>"#,
|
||||||
|
);
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(400.0, 200.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
let initial = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const box = document.getElementById("box");
|
||||||
|
const c = getComputedStyle(box);
|
||||||
|
return [
|
||||||
|
c.display, c.position, c.zIndex, c.visibility, c.opacity,
|
||||||
|
c.backgroundColor, c.getPropertyValue("color"),
|
||||||
|
c.width, c.height, c.minWidth, c.maxWidth, c.boxSizing,
|
||||||
|
c.overflowX, c.overflowY,
|
||||||
|
c.marginTop, c.marginRight, c.marginBottom, c.marginLeft,
|
||||||
|
c.paddingTop, c.borderLeftWidth, c.borderLeftColor,
|
||||||
|
c.flexDirection, c.flexWrap, c.alignItems,
|
||||||
|
c.justifyContent, c.rowGap, c.columnGap, c.transform
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
initial,
|
||||||
|
serde_json::json!([
|
||||||
|
"flex", "relative", "7", "hidden", "0.35",
|
||||||
|
"rgb(10, 20, 30)", "rgb(40, 50, 60)",
|
||||||
|
"120px", "40px", "20px", "160px", "border-box",
|
||||||
|
"clip", "visible",
|
||||||
|
"1px", "2px", "3px", "4px",
|
||||||
|
"5px", "2px", "rgb(70, 80, 90)",
|
||||||
|
"column", "wrap", "center",
|
||||||
|
"space-between", "6px", "9px", "translate(3px, 4px)"
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate(
|
||||||
|
r#"
|
||||||
|
const box = document.getElementById("box");
|
||||||
|
box.className = "alt";
|
||||||
|
const c = getComputedStyle(box);
|
||||||
|
return [c.display, c.width, c.opacity];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
serde_json::json!(["grid", "150px", "0.8"])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "current_thread")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn idle_event_loop_flushes_resolved_promise_continuations() {
|
async fn idle_event_loop_flushes_resolved_promise_continuations() {
|
||||||
let mut rt = setup_runtime("<html><body><div id='state'>pending</div></body></html>");
|
let mut rt = setup_runtime("<html><body><div id='state'>pending</div></body></html>");
|
||||||
@@ -3152,6 +3324,191 @@ mod tests {
|
|||||||
assert!(values[12..].iter().all(|value| value == &serde_json::json!(true)));
|
assert!(values[12..].iter().all(|value| value == &serde_json::json!(true)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// CSSOM View exposes the viewport through the standards-mode root, but
|
||||||
|
/// ordinary elements (including body) report their padding box. Modern
|
||||||
|
/// animation libraries commonly measure a fixed 100vh sentinel through
|
||||||
|
/// clientHeight; the old synthetic 100x20 fallback collapsed all of their
|
||||||
|
/// viewport-relative trigger ranges.
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn rendered_client_metrics_use_the_live_padding_box() {
|
||||||
|
let dom = parse_html(
|
||||||
|
r#"<html style="margin:0"><body style="margin:0">
|
||||||
|
<div id="tracker"
|
||||||
|
style="position:fixed;top:0;width:100%;height:100vh"></div>
|
||||||
|
<div id="box"
|
||||||
|
style="box-sizing:content-box;width:100.4px;height:50.6px;
|
||||||
|
padding:5px 8.2px 6px 7.2px;
|
||||||
|
border-style:solid;
|
||||||
|
border-width:2px 4.1px 3px 3.1px"></div>
|
||||||
|
<div style="height:900px"></div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(320.0, 200.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
let initial = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const tracker = document.getElementById("tracker");
|
||||||
|
const box = document.getElementById("box");
|
||||||
|
return {
|
||||||
|
root: [
|
||||||
|
document.documentElement.clientWidth,
|
||||||
|
document.documentElement.clientHeight
|
||||||
|
],
|
||||||
|
body: [document.body.clientWidth, document.body.clientHeight],
|
||||||
|
tracker: [
|
||||||
|
tracker.clientWidth, tracker.clientHeight,
|
||||||
|
tracker.offsetWidth, tracker.offsetHeight
|
||||||
|
],
|
||||||
|
box: [
|
||||||
|
box.clientWidth, box.clientHeight,
|
||||||
|
box.getBoundingClientRect().width,
|
||||||
|
box.getBoundingClientRect().height
|
||||||
|
]
|
||||||
|
};
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(initial["root"], serde_json::json!([320, 200]));
|
||||||
|
assert_eq!(initial["body"], serde_json::json!([320, 967]));
|
||||||
|
assert_eq!(initial["tracker"], serde_json::json!([320, 200, 320, 200]));
|
||||||
|
assert_eq!(initial["box"][0], serde_json::json!(116));
|
||||||
|
assert_eq!(initial["box"][1], serde_json::json!(62));
|
||||||
|
assert!((initial["box"][2].as_f64().unwrap() - 123.0).abs() < 0.05);
|
||||||
|
assert_eq!(initial["box"][3], serde_json::json!(67));
|
||||||
|
|
||||||
|
// Attribute-backed inline-style changes invalidate the retained
|
||||||
|
// render. Borders do not change the padding box; padding does.
|
||||||
|
let mutated = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const tracker = document.getElementById("tracker");
|
||||||
|
const box = document.getElementById("box");
|
||||||
|
tracker.style.height = "50vh";
|
||||||
|
box.style.borderLeftWidth = "13px";
|
||||||
|
box.style.paddingLeft = "17px";
|
||||||
|
return [
|
||||||
|
tracker.clientHeight,
|
||||||
|
box.clientWidth,
|
||||||
|
box.getBoundingClientRect().width
|
||||||
|
];
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(mutated[0], serde_json::json!(100));
|
||||||
|
assert_eq!(mutated[1], serde_json::json!(126));
|
||||||
|
assert_eq!(mutated[2], serde_json::json!(143));
|
||||||
|
|
||||||
|
// A later CDP/emulation viewport update invalidates the layout too;
|
||||||
|
// both the root special case and an ordinary 100vh box are live.
|
||||||
|
rt.set_viewport(640.0, 360.0);
|
||||||
|
assert_eq!(
|
||||||
|
rt.evaluate(
|
||||||
|
r#"const tracker = document.getElementById("tracker");
|
||||||
|
return [
|
||||||
|
document.documentElement.clientWidth,
|
||||||
|
document.documentElement.clientHeight,
|
||||||
|
tracker.clientWidth,
|
||||||
|
tracker.clientHeight
|
||||||
|
]"#,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
serde_json::json!([640, 360, 640, 180])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CSSOM View distinguishes "no associated CSS box" from a real box whose
|
||||||
|
/// dimensions happen to be zero. Blink and Gecko return an all-zero
|
||||||
|
/// bounding rect and no client rects for display:none/detached elements;
|
||||||
|
/// a laid-out zero-size box still contributes one client rect.
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn rendered_cssom_rects_distinguish_no_box_from_zero_size_box() {
|
||||||
|
let dom = parse_html(
|
||||||
|
r#"<html style="margin:0"><body style="margin:0">
|
||||||
|
<div id="hidden" style="display:none;width:80px;height:40px"></div>
|
||||||
|
<div id="zero" style="display:block;width:0;height:0"></div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(dom);
|
||||||
|
rt.set_viewport(320.0, 200.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const hidden = document.getElementById("hidden");
|
||||||
|
const detached = document.createElement("div");
|
||||||
|
detached.style.cssText = "display:block;width:90px;height:50px";
|
||||||
|
const zero = document.getElementById("zero");
|
||||||
|
const sample = element => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const rects = element.getClientRects();
|
||||||
|
return {
|
||||||
|
rect: [
|
||||||
|
rect.x, rect.y, rect.width, rect.height,
|
||||||
|
rect.top, rect.right, rect.bottom, rect.left
|
||||||
|
],
|
||||||
|
rectCount: rects.length,
|
||||||
|
firstWidth: rects.length ? rects[0].width : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
hidden: sample(hidden),
|
||||||
|
detached: sample(detached),
|
||||||
|
zero: sample(zero),
|
||||||
|
};
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for name in ["hidden", "detached"] {
|
||||||
|
assert_eq!(
|
||||||
|
result[name]["rect"],
|
||||||
|
serde_json::json!([0, 0, 0, 0, 0, 0, 0, 0]),
|
||||||
|
"{name} must expose the CSSOM View no-box bounding rect"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result[name]["rectCount"],
|
||||||
|
serde_json::json!(0),
|
||||||
|
"{name} must expose an empty client rect list"
|
||||||
|
);
|
||||||
|
assert_eq!(result[name]["firstWidth"], serde_json::Value::Null);
|
||||||
|
}
|
||||||
|
assert_eq!(result["zero"]["rect"][2], serde_json::json!(0));
|
||||||
|
assert_eq!(result["zero"]["rect"][3], serde_json::json!(0));
|
||||||
|
assert_eq!(
|
||||||
|
result["zero"]["rectCount"],
|
||||||
|
serde_json::json!(1),
|
||||||
|
"a real zero-size layout box must not be mistaken for no box"
|
||||||
|
);
|
||||||
|
assert_eq!(result["zero"]["firstWidth"], serde_json::json!(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "render"))]
|
||||||
|
#[test]
|
||||||
|
fn non_render_cssom_rects_keep_compatibility_geometry() {
|
||||||
|
let mut rt = setup_runtime(r#"<html><body><div id="box"></div></body></html>"#);
|
||||||
|
let result = rt
|
||||||
|
.evaluate(
|
||||||
|
r#"
|
||||||
|
const box = document.getElementById("box");
|
||||||
|
const detached = document.createElement("div");
|
||||||
|
return [box, detached].map(element => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return [rect.width, rect.height, element.getClientRects().length];
|
||||||
|
});
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result, serde_json::json!([[100, 20, 1], [100, 20, 1]]));
|
||||||
|
}
|
||||||
|
|
||||||
/// Chromium 150 reference (800x513 CSS-pixel viewport):
|
/// Chromium 150 reference (800x513 CSS-pixel viewport):
|
||||||
/// top=[60,20,20,-267], bottom=[448,448,231,31,-496] at the sampled
|
/// top=[60,20,20,-267], bottom=[448,448,231,31,-496] at the sampled
|
||||||
/// root scroll offsets. This keeps sticky distinct from fixed positioning,
|
/// root scroll offsets. This keeps sticky distinct from fixed positioning,
|
||||||
@@ -3511,7 +3868,7 @@ mod tests {
|
|||||||
fn root_overflow_clip_preserves_cssom_scroll_range() {
|
fn root_overflow_clip_preserves_cssom_scroll_range() {
|
||||||
let dom = parse_html(
|
let dom = parse_html(
|
||||||
r#"<html style="margin:0;height:100%;overflow:hidden">
|
r#"<html style="margin:0;height:100%;overflow:hidden">
|
||||||
<body style="margin:0;height:100%;overflow:hidden">
|
<body style="margin:0;height:100%">
|
||||||
<main id="main" style="padding-top:48px">
|
<main id="main" style="padding-top:48px">
|
||||||
<div style="height:5000px"></div>
|
<div style="height:5000px"></div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in new issue
Block a user