Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["apps/chuzz", "crates/chuzz-control", "crates/chuzz-qa-fixture"]
resolver = "3"

[workspace.package]
version = "0.1.39"
version = "0.1.40"
edition = "2024"
rust-version = "1.91"
license = "MIT OR Apache-2.0"
Expand Down
257 changes: 219 additions & 38 deletions apps/chuzz/src/document_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,22 +710,54 @@ pub(crate) const WEB_API_SHIM: &str = r#"
* — which is unbounded recursion the moment both exist, and it took the
* whole shim down with it.
*
* The engine does not expose its real size to script: `innerWidth`,
* `outerWidth` and the client dimensions all read 0, and the layout rect
* comes back zero-width. So this is a stated default rather than a
* measurement, chosen to match the driver's default screenshot size. A page
* asking whether it has room for the desktop layout gets a truthful-looking
* desktop answer instead of the zero that silently forces every responsive
* design into its narrowest branch.
* The engine owns `innerWidth`/`innerHeight` as live accessors over the
* document's viewport, but a page's scripts run before the host sets that
* viewport, so at boot they read 0. The defaults below cover only that
* window, chosen to match the driver's default screenshot size: a page
* asking at boot whether it has room for the desktop layout gets a
* truthful-looking desktop answer instead of the zero that forces every
* responsive design into its narrowest branch. Once the engine reports a
* size, that size is the answer. The shim used to replace the accessors
* with the default for good, so a 1344-wide page reported 1440 forever and
* anything clamped to the viewport (an overlay, a tooltip) was placed
* against a width the page does not have.
*
* `engineSize` reads the engine's own getters, captured before anything
* here redefines the names, so nothing reads a shimmed size to compute a
* shimmed size: the recursion described above cannot come back.
*/
var CHUZZ_VIEWPORT_WIDTH = 1440;
var CHUZZ_VIEWPORT_HEIGHT = 960;
var engineSize = (function () {
var read = function (name) {
try {
var descriptor = Object.getOwnPropertyDescriptor(globalThis, name);
if (descriptor && typeof descriptor.get === 'function') {
var get = descriptor.get;
return function () {
try {
var value = Number(get.call(globalThis));
return value > 0 ? value : 0;
} catch (error) { return 0; }
};
}
} catch (error) {}
return function () { return 0; };
};
return { width: read('innerWidth'), height: read('innerHeight') };
})();
var viewportWidth = function () {
return engineSize.width() || CHUZZ_VIEWPORT_WIDTH;
};
var viewportHeight = function () {
return engineSize.height() || CHUZZ_VIEWPORT_HEIGHT;
};
if (typeof globalThis.screen === 'undefined') {
globalThis.screen = {
get width() { return CHUZZ_VIEWPORT_WIDTH; },
get height() { return CHUZZ_VIEWPORT_HEIGHT; },
get availWidth() { return CHUZZ_VIEWPORT_WIDTH; },
get availHeight() { return CHUZZ_VIEWPORT_HEIGHT; },
get width() { return viewportWidth(); },
get height() { return viewportHeight(); },
get availWidth() { return viewportWidth(); },
get availHeight() { return viewportHeight(); },
colorDepth: 24,
pixelDepth: 24,
orientation: { type: 'landscape-primary', angle: 0 }
Expand Down Expand Up @@ -870,28 +902,35 @@ pub(crate) const WEB_API_SHIM: &str = r#"
* must not skip the rest, and none of them may throw out of the shim.
*/
(function () {
var assign = function (name, value) {
var publish = function (name, size) {
try {
if (globalThis[name]) { return; }
/*
* `defineProperty`, not assignment. The engine owns these names as
* `defineProperty`, not assignment: the engine owns these names as
* read-only accessors, so `globalThis.innerWidth = 1440` fails
* silently and the page keeps reading 0. Redefining the property is
* what actually takes. The value is a plain number, never a getter:
* a getter that read another shimmed size recursed without bound.
* silently. The getter reads `viewportWidth`/`viewportHeight`, which
* read the engine's captured getters, never these names, so it cannot
* recurse. The setter mirrors a browser's [Replaceable] attribute: a
* page that assigns its own value keeps it.
*/
Object.defineProperty(globalThis, name, {
configurable: true,
enumerable: true,
writable: true,
value: value
get: size,
set: function (value) {
Object.defineProperty(globalThis, name, {
configurable: true,
enumerable: true,
writable: true,
value: value
});
}
});
} catch (error) {}
};
assign('innerWidth', CHUZZ_VIEWPORT_WIDTH);
assign('innerHeight', CHUZZ_VIEWPORT_HEIGHT);
assign('outerWidth', CHUZZ_VIEWPORT_WIDTH);
assign('outerHeight', CHUZZ_VIEWPORT_HEIGHT);
publish('innerWidth', viewportWidth);
publish('innerHeight', viewportHeight);
publish('outerWidth', viewportWidth);
publish('outerHeight', viewportHeight);
})();
/*
* `location.origin`, and `location.host` with its port.
Expand Down Expand Up @@ -1069,8 +1108,8 @@ pub(crate) const WEB_API_SHIM: &str = r#"
var bound = parseFloat(dimension[3]);
if (dimension[4] === 'em' || dimension[4] === 'rem') bound = bound * 16;
var actual = dimension[2] === 'width'
? CHUZZ_VIEWPORT_WIDTH
: CHUZZ_VIEWPORT_HEIGHT;
? viewportWidth()
: viewportHeight();
matches = dimension[1] === 'min' ? actual >= bound : actual <= bound;
}
}
Expand Down Expand Up @@ -1442,20 +1481,162 @@ pub(crate) const WEB_API_SHIM: &str = r#"
};
}
if (typeof globalThis.ResizeObserver === 'undefined') {
// A stub, and deliberately silent rather than firing once the way
// `IntersectionObserver` above does. The difference is what an invented
// entry would have to say: visibility has an answer that is right for most
// of a page ("yes"), and a size does not. Nothing here can measure a box
// from JavaScript, so the only entry this could deliver carries a zero
// `contentRect`, and a grid or carousel that divides by that width computes
// zero columns and renders nothing. Never firing leaves such a component on
// whatever it renders before it has measured, which is the better of the
// two wrong answers. Backing this with real box data is engine work.
/*
* A real observer over `getBoundingClientRect`, which returns laid-out
* boxes to script.
*
* This was a silent stub on the grounds that nothing could measure a box
* from JavaScript, so any entry would carry a zero size. That stopped
* being true, and the stub's silence became the bug: a component that
* measures, moves, and relies on the observer to settle (an overlay whose
* first measurement preceded its width limits) was left wherever its first
* frame put it.
*
* The engine has no resize notification to hook, so this measures. It does
* not measure forever: a page that observes something permanently would
* otherwise keep the frame loop busy, which is exactly the idle and drift
* that rendered QA fails a page for. Measuring is armed by anything that
* can change a size (an `observe`, a resize or scroll, pointer, keyboard
* and input events) and by a size change it finds, and it disarms after
* `QUIET_FRAMES` frames in which nothing changed.
*
* Entries follow the specification's shape. The content box is the border
* box less the padding and border `getComputedStyle` reports; an engine
* that does not report them (ps-blitz before its box-edge fix) reads as
* zero, and the content box falls back to the border box. `observe`
* honours `{ box: 'border-box' }`, and otherwise watches the content box
* as a browser does. Like a browser, the first observation reports any
* size other than 0x0, and a callback that throws is reported without
* stopping the others.
*/
var QUIET_FRAMES = 30;
var resizeObservers = [];
var measureFrames = 0;
var measureScheduled = false;
var nextFrame = typeof globalThis.requestAnimationFrame === 'function'
? function (run) { globalThis.requestAnimationFrame(run); }
: function (run) { setTimeout(run, 16); };
var measureBoxes = function (target) {
var border = { width: 0, height: 0 };
try {
var rect = target.getBoundingClientRect();
border = { width: Number(rect.width) || 0, height: Number(rect.height) || 0 };
} catch (error) {}
var edge = { top: 0, right: 0, bottom: 0, left: 0 };
try {
if (typeof globalThis.getComputedStyle === 'function') {
var style = globalThis.getComputedStyle(target);
var px = function (name) { return parseFloat(style && style[name]) || 0; };
edge = {
top: px('paddingTop') + px('borderTopWidth'),
right: px('paddingRight') + px('borderRightWidth'),
bottom: px('paddingBottom') + px('borderBottomWidth'),
left: px('paddingLeft') + px('borderLeftWidth')
};
}
} catch (error) {}
return {
border: border,
content: {
width: Math.max(0, border.width - edge.left - edge.right),
height: Math.max(0, border.height - edge.top - edge.bottom)
},
left: edge.left,
top: edge.top
};
};
var resizeEntry = function (target, boxes) {
var content = boxes.content;
var contentBoxSize = [{ inlineSize: content.width, blockSize: content.height }];
return {
target: target,
contentRect: {
x: boxes.left, y: boxes.top, top: boxes.top, left: boxes.left,
width: content.width, height: content.height,
right: boxes.left + content.width, bottom: boxes.top + content.height
},
borderBoxSize: [{ inlineSize: boxes.border.width, blockSize: boxes.border.height }],
contentBoxSize: contentBoxSize,
devicePixelContentBoxSize: contentBoxSize
};
};
var measureObservations = function () {
measureScheduled = false;
var changed = false;
var observing = false;
for (var i = 0; i < resizeObservers.length; i++) {
var observer = resizeObservers[i];
var entries = [];
for (var j = 0; j < observer.observations.length; j++) {
var observation = observer.observations[j];
observing = true;
var boxes = measureBoxes(observation.target);
var size = observation.box === 'border-box' ? boxes.border : boxes.content;
if (size.width !== observation.width || size.height !== observation.height) {
observation.width = size.width;
observation.height = size.height;
entries.push(resizeEntry(observation.target, boxes));
}
}
if (entries.length) {
changed = true;
try {
observer.callback.call(observer.instance, entries, observer.instance);
} catch (error) {
setTimeout(function () { throw error; }, 0);
}
}
}
if (!observing) { measureFrames = 0; return; }
measureFrames = changed ? QUIET_FRAMES : measureFrames - 1;
if (measureFrames > 0) scheduleMeasure();
};
var scheduleMeasure = function () {
if (measureScheduled) return;
measureScheduled = true;
nextFrame(measureObservations);
};
var armMeasure = function () {
if (!resizeObservers.length) return;
measureFrames = QUIET_FRAMES;
scheduleMeasure();
};
['resize', 'scroll', 'pointerdown', 'pointerup', 'click', 'keydown', 'input']
.forEach(function (type) {
try {
globalThis.addEventListener(type, armMeasure, { capture: true, passive: true });
} catch (error) {}
});
globalThis.ResizeObserver = function (callback) {
this.callback = callback;
this.observe = function () {};
this.unobserve = function () {};
this.disconnect = function () {};
if (typeof callback !== 'function') {
throw new TypeError("Failed to construct 'ResizeObserver': callback is not a function");
}
var record = { instance: this, callback: callback, observations: [] };
this.observe = function (target, options) {
if (!target) return;
var box = options && options.box === 'border-box' ? 'border-box' : 'content-box';
if (resizeObservers.indexOf(record) === -1) resizeObservers.push(record);
for (var i = 0; i < record.observations.length; i++) {
if (record.observations[i].target === target) {
record.observations[i].box = box;
return;
}
}
// 0x0 is the specification's initial "last reported" size, so the
// first measurement reports the target unless it has no box.
record.observations.push({ target: target, box: box, width: 0, height: 0 });
armMeasure();
};
this.unobserve = function (target) {
record.observations = record.observations.filter(function (observation) {
return observation.target !== target;
});
};
this.disconnect = function () {
record.observations = [];
resizeObservers = resizeObservers.filter(function (other) { return other !== record; });
};
this.takeRecords = function () { return []; };
};
}
if (typeof globalThis.Image === 'undefined') {
Expand Down
2 changes: 1 addition & 1 deletion scripts/qa-visible-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ root=$(cd "$(dirname "$0")/.." && pwd)
app=${1:-"$root/target/release/bundle/macos/Chuzz.app"}
qa_bin=${PS_QA_BIN:-$(command -v ps-qa || true)}
fixture_bin=${CHUZZ_QA_FIXTURE_BIN:-"$root/target/release/chuzz-qa-fixture"}
artifacts=${QA_ARTIFACT_DIR:-$(mktemp -d /private/tmp/chuzz-visible-XXXXXX)}
artifacts=${QA_ARTIFACT_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/chuzz-visible-XXXXXX")}
qa_home="$artifacts/home"
descriptor=""
app_pid=""
Expand Down
Loading