fix: a batch of QA-found engine defects, plus 0.4.2 - #93
Merged
Merged
Conversation
added 14 commits
September 9, 2026 19:38
The type list that grants the 300px-by-line-height intrinsic content box omitted "number", while the list in layout::construct that decides which inputs get a text editor included it. A number input therefore got an editor and a zero content box, measuring 6x6 (its padding and border) against 306x25.2 for every other text-like type.
…g it flush_styles_to_layout returns early on a display:none subtree, so a stacking-context host hidden after its child list was built never rebuilds that list. resolve_hoisted_positions walks every node that has a context, hidden or not, and indexed the slab directly, so removing one of the listed children panicked with "invalid SlotMap key used" at resolve.rs:863 on the next resolve. Reported 6 times out of 6 from a signup page whose header menu switched language and then opened a chat launcher.
Two defects on the same walk of controls_to_form. The map is never pruned when a control leaves the document, so a framework that re-renders a field leaves a freed id in it. The walk indexed the slab for every id it found, so setting a value twice and then pressing Enter panicked with "invalid SlotMap key used" and took the host down. The walk now goes over the tree, which only yields live nodes, and removal prunes the map so it cannot grow without bound. The "more than one field that blocks implicit submission" rule is gated by the spec on the form having no submit button. It was applied unconditionally, so Enter never submitted any form with two explicitly typed fields even when the form had a Sign in button. Three sites hit this.
Answers a QA finding that reported hover leaving every rendered pixel unchanged on four controls. Neither proposed cause holds in the engine: the hover media feature cannot evaluate false here (make_device passes a fixed PointerCapabilities::default(), with no host input), and a UiEvent::PointerMove followed by a resolve does reach the paint, including when the hover colour arrives through a custom property gated on (hover: hover), which is the shape the component library ships.
…tton The implicit-submission helper had grown a second definition of the button case. Delegate to the existing one, which also handles the command attributes that take a button back out of the Submit Button state, and keep only the input types here.
The legacy alias for the stop-propagation flag, in neither the shim nor the bundled framework, so a delegated dispatcher had nothing to read and stopPropagation did not hold. Measured on two sites: pressing a cookie-preferences category dismissed the entire dialog, because the closing backdrop is an ancestor of the panel. Reading it reports the flag; setting it true raises it, and setting it false does nothing, which is what the DOM standard says.
blitz-dom has had scroll_to_node all along, for the harness's agent action, but page script could not reach it: getElementById(...).scrollIntoView() raised "TypeError: not a callable function". Any anchor-scrolling router, a "back to top" control and a validation-error focuser all call it. The argument is accepted and ignored, because scroll_to_node lands the node at the top-left of each scrollport, which is the default block: "start".
location was a plain data object, so a page calling any of the three threw a TypeError out of whatever ran it. All three now go through the document's navigation provider, which is where a link click already goes; BaseDocument grows navigate_to_url and current_url because both resolve_url and the provider were internal to blitz-dom.
…window dispatchEvent history.back and history.forward changed the URL and announced nothing, so a router's navigate(-1) never redrew and every route was one-way. Announcing it needed a way to raise a window-targeted event at all: Node.prototype.dispatchEvent walks a node chain and never reaches the window listeners, so window.dispatchEvent is added here and the history shim uses it.
…een edited change was synthesised only for checkbox and radio, so any onChange on a text field was dead. That shipped a product bug: a phone-number field bound to change gated a Confirm button that nothing could ever open. A text control commits rather than notifying, so the value at focus is recorded and compared on blur; an untouched field still fires nothing. The markup5ever dev-dependency is for the test, which sets a field's value the way the DOM does.
There was no select rule in the user-agent stylesheet, so a select computed
display: inline, and option { display: none } left it with no in-flow content.
Height on a non-replaced inline is ignored, so even a select with an authored
height reported 0x0 and nothing on a page could find it or press it.
Its content size cannot come from flow, because the options it is sized from
are not in it, so layout measures the widest option label directly. The
character-count estimate is the same one the textarea cols attribute already
uses: a select's label is not laid out as text anywhere yet.
The popup, option hit-testing and selection are NOT implemented. A select now
has a box and can be found and pressed; pressing it still does nothing.
Table layout flattens rows into a CSS grid of cells, so tr, thead and tbody nodes have their box construction damage cleared and never reach Taffy. Nothing ever wrote a layout for them: every one reported 0x0 and, to anything asking whether an element is displayed, "no". A QA check walking a table by row had nothing to walk. A row is not laid out, it is described, so the context now records which cells belong to which row and which rows to which group, and a pass after rounding derives each box from them. After rounding because final_layout is what every geometry query reads and the rounding pass is what fills it; reading the cells any earlier gets zeroes.
…e internals
table { display: block } is the standard wide-table horizontal-scroll pattern,
and it made every row render side by side: the header on one line, then every
data row at the same y and increasing x. With the table no longer a table, its
thead and tbody were plain block children, and their own displays have no
mapping in the style conversion, so they fell through to Taffy's default, which
is flex, and a flex container lays its items out in a row.
CSS 2.1 17.2.1 requires an anonymous table box around misparented
table-internal boxes. When the container holds nothing else, which is this
whole pattern and every case seen on a real page, that anonymous box would be
its only child and take its content box, so it is fused into the container
instead of inserted under it. A container with mixed content still needs a
separate anonymous box and does not get one here.
The QA fixes in this branch are what downstream needs, and nothing can ask for them until they are released. The branch originally carried a bump to 0.4.2; master reached that version independently and has since released 0.4.6, so the same intent is now a bump to 0.4.7. Thirteen commits sit above the 0.4.6 release commit and none of them are on crates.io: 0fbbf52 size input[type=number] like the other text inputs 01b484e prune removed nodes from a stacking context before resolving it 594d298 make implicit form submission safe and spec-correct 3bcf5f8 reuse ElementData::is_submit_button for the default button afcb61e expose Event.cancelBubble 2995467 expose Element.scrollIntoView bddcd96 give window.location assign, replace and reload 44de0c7 dispatch popstate on a history traversal, and give the window dispatchEvent e405cc3 fire change on a text control that loses focus having been edited fc4eee8 give <select> a layout box 1e40949 give table rows and row groups the box their cells occupy c6c185c generate a table box for a block whose children are table internals Two of them close SlotMap panics that took the host process down: a freed control id left in controls_to_form, and a freed child left in a stacking context. The patch, not the minor: every change is a fix or an addition. The new public surface is BaseDocument::navigate_to_url and current_url, plus Element scrollIntoView, Event.cancelBubble, window.dispatchEvent and location assign, replace and reload on the script side. Nothing public is removed or narrowed: the table context gained fields, but blitz_dom::layout is a private module. The in-repo ranges stay at ^0.4.0, which accepts 0.4.7, so the whole bump is one line.
pathscale
force-pushed
the
fix/qa-engine-defects-2026-09-09
branch
2 times, most recently
from
September 9, 2026 13:09
e454d46 to
e6287bd
Compare
added 2 commits
September 9, 2026 20:55
`tests/qa/run.sh` built `qa-inspect-host`. ps-observability deleted that
crate in "refactor: delete qa-inspect-host, so there is one headless
browser", so every run since has died with
error: package ID specification `qa-inspect-host` did not match any packages
including on master, whose last green run predates the deletion by forty
minutes and so only looks fine because nothing has rebuilt it.
`chuzz-headless` replaced it. The patches move with it, because judging this
checkout rather than the last release is the whole reason the host is built
here, and they now cover every ps-blitz crate chuzz reaches instead of the
five the old host needed: patching some and not others resolves the rest
from crates.io, and an engine built half from this checkout and half from a
release does not compile.
`bun install` in chuzz's frontend first. chuzz's build script builds its
Solid chrome even for a headless binary that never links it, and without
`node_modules/.bin` it panics with `solid-layouts-library: command not
found`. The gate that skips it is in chuzz's own open PR, so without this
install ps-blitz could not go green until chuzz merged, while chuzz should
not merge until ps-blitz publishes. It is cheap, and harmless once the gate
lands.
Verified end to end against chuzz master, the state that has the defect:
all fixture groups pass, failed: 0.
`cargo doc --workspace` rebuilt the whole graph on every pull request and nothing downstream read its output. Gating it behind workflow_dispatch was the half measure; the job is gone. Nothing depended on it: no other job named it in `needs`, and it produced no artifact.
pathscale
force-pushed
the
fix/qa-engine-defects-2026-09-09
branch
from
September 9, 2026 14:12
1131c44 to
d6859cd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a batch of engine defects found by twelve end-to-end QA suites run against real sites, each with a regression test that fails before its fix.
Fixed
<select>got no layout box at all: noselectrule in the user-agent sheet, so it computeddisplay: inline, andoption { display: none }left it with no in-flow content. Even an authored height reported 0x0.tests/blitz-tests/tests/select_layout.rsinput[type=number]measured 6x6, its padding and border alone, against 306x25.2 for every other text-like type. The type list that grants the intrinsic content box omittednumberwhile the list that creates the text editor included it.tests/blitz-tests/tests/number_input_sizing.rs<tr>,<thead>and<tbody>reported 0x0 and "not displayed". Table layout flattens rows into a grid of cells, so those nodes never reach Taffy and nothing wrote a layout for them.tests/blitz-tests/tests/table_row_boxes.rstable { display: block }rendered every row side by side.tests/blitz-tests/tests/display_block_table.rsinvalid SlotMap key usedatkeyboard.rs, killing the host.controls_to_formwas never pruned, and the walk indexed the slab for every id it held.tests/blitz-tests/tests/implicit_form_submission.rsinvalid SlotMap key used, atresolve.rs:863. A stacking-context host inside adisplay: nonesubtree never rebuilds its child list, andresolve_hoisted_positionswalks every host, hidden or not.tests/blitz-tests/tests/hoisted_child_survives_removal.rstests/blitz-tests/tests/implicit_form_submission.rschangewas synthesised only for checkbox and radio, so anyonChangeon a text field was dead.packages/blitz-script/tests/text_input_change.rsscrollIntoViewwas not exposed to page scripts.packages/blitz-script/tests/scroll_into_view.rswindow.locationhad noassign,replaceorreload.packages/blitz-script/tests/location_navigation.rscancelBubbleexisted nowhere, so a delegated dispatcher had nothing to read andstopPropagationdid not hold.packages/blitz-script/tests/cancel_bubble.rshistory.backdispatched nopopstate, so routes were one-way.packages/blitz-script/tests/popstate.rsTwo findings needed a mechanism that did not exist.
location.assignhad nowhere to go, becauseresolve_urland the navigation provider are both internal to blitz-dom, soBaseDocumentgrowsnavigate_to_urlandcurrent_url.popstatehad no way to be raised at all, becauseNode.prototype.dispatchEventwalks a node chain and never reaches the window listeners, sowindow.dispatchEventis added and the history shim uses it.changeon a text control is a commit, not a blur notification: the value at focus is recorded and compared on blur, and an untouched field still fires nothing.Corrected
15,
:hoverproducing no observable pixel change: not an engine defect. The finding asked which of two causes it was. It is neither, and the evidence is intests/blitz-tests/tests/hover_pointer_move_repaint.rs.hovermedia feature cannot evaluate false in any host.make_devicepasses a fixedPointerCapabilities::default()for both pointer fields, with no host input and no configuration.UiEvent::PointerMove, the event a host actually sends, followed by a resolve and a paint, flips the sampled pixel from#0000ffto#ff0000on a plain:hoverrule under@media (hover: hover), and on a second case where the hover colour arrives through a CSS custom property redefined inside that media query, which is the shape the component library ships.commit_cpu_framecallsinner.resolveimmediately beforerender_to_buffer.The two new tests are kept as regression guards. The defect is in the QA harness's own hover step or its pixel comparison, not in ps-blitz.
Deferred
2,
<select>as a widget. The box is landed, which is the blocker for everything else: a select can now be found, measured and pressed. Not implemented: the popup, per-option layout and hit-testing, selection state, and thechangea selection would fire. Options remain in the tree and queryable, but0x0,hidden, and pressing a select still does nothing.SpecialElementDatastill has no select variant.5,
position: sticky. Not implemented, and not half-landed.stylo_taffy::convert::positionstill mapsPosition::Stickytotaffy::Position::Relative. Sticky needs a per-scroll adjustment pass that keeps a box within its containing block as the scrollport passes it, plus the paint offsets to match, which is a feature rather than a fix and does not belong in a batch of them. Every sticky navbar and doc sidebar still scrolls away.Notes
refactorcommit folds a second definition of "is this a submit button" back onto the existingElementData::is_submit_button.packages/blitz-dom/src/layout/table.rsgains a cell range per row and a row range per group.blitz_dom::layoutis a private module, so no public type changed shape.ps-blitz-shelldoes not build on this branch, and does not build onmastereither: seven errors against a driftedwinit. Untouched here.