Summary
WorkspaceApp.vue::activateSearchResult() throws a TypeError on every invocation, so pressing Enter on a quick-search match never opens the tile (tile-quick-search REQ-QSEARCH-003, "Enter opens the selected tile").
A second, closely related defect in applySearchDimming() means the de-emphasis is applied to every tile including the matches.
Both have the same root cause: placement ids are numbers, and both functions treat them as strings.
Found during hydra gate-19 round 2 (#92). Measured in CI, then isolated statically.
Defect 1 — Enter never activates (crash)
src/views/WorkspaceApp.vue:
activateSearchResult(item) {
const placementId = item?.placement?.id
if (!placementId || !this.$el) { return }
const el = this.$el.querySelector(
`.launchpad-grid-item[data-placement-id="${placementId.replace(/"/g, '\\"')}"]`,
)
item.placement.id is the WidgetPlacement entity id — an integer through jsonSerialize(). Number.prototype.replace does not exist, so placementId.replace(...) raises TypeError: placementId.replace is not a function. The guard above it does not catch this: a non-zero integer is truthy, so execution reaches the crash.
The exception is thrown inside a Vue event handler, so nothing surfaces to the user — Enter simply does nothing.
How it was isolated
The e2e test at tests/e2e/tile-quick-search.spec.ts pressed Enter on a single match and recorded clicks with a capture-phase listener. It failed with an empty activation list. Two splitting probes were added and both passed in CI run 31483882342:
- the rendered cells do carry
a[href] (so activateSearchResult()'s fallback-to-focus() branch is not the explanation);
- the search input does still hold focus when Enter is pressed.
The sibling test proving aria-activedescendant selection tracking passes, so the selection mechanism is sound. Anchor present, focus correct, selection correct, no click — which the TypeError explains exactly.
Defect 2 — every tile is dimmed, including the matches
Same file:
const id = el.getAttribute('data-placement-id') // ALWAYS a string
el.classList.toggle('launchpad-grid-item--dimmed', matchIds.includes(id) === false)
matchIds comes from the search results, i.e. the same numeric placement ids. Array.prototype.includes is strict-equality, so [12, 13].includes('12') is false — no tile ever matches, and the class is applied to all of them.
REQ-QSEARCH-002 requires non-matching tiles to be de-emphasised; applying it to matches as well defeats the feature.
A note on the test that did not catch Defect 2
tests/e2e/tile-quick-search.spec.ts currently asserts dimmed count > 0 after typing. That is satisfied by "everything is dimmed", so it passes on the buggy behaviour. It should assert that the tiles matching the query are NOT dimmed while the others are. That tightening is deliberately not applied in #92, because on current development it would be red — it belongs with the fix.
Suggested fix
Coerce once, at the boundary, in both functions:
const placementId = String(item?.placement?.id ?? '')
if (placementId === '' || !this.$el) { return }
const matchSet = new Set((matchIds ?? []).map(String))
…
el.classList.toggle('launchpad-grid-item--dimmed', matchSet.has(id) === false)
Coverage status
tile-quick-search::enter-opens-the-selected-tile is left as an open gate-19 finding — not annotated with @e2e exclude — because the scenario is browser-observable and the reason it has no passing test is that the product is broken. The other 8 scenarios in REQ-QSEARCH-001..003 have real, passing tests on #92.
Summary
WorkspaceApp.vue::activateSearchResult()throws aTypeErroron every invocation, so pressing Enter on a quick-search match never opens the tile (tile-quick-search REQ-QSEARCH-003, "Enter opens the selected tile").A second, closely related defect in
applySearchDimming()means the de-emphasis is applied to every tile including the matches.Both have the same root cause: placement ids are numbers, and both functions treat them as strings.
Found during hydra gate-19 round 2 (#92). Measured in CI, then isolated statically.
Defect 1 — Enter never activates (crash)
src/views/WorkspaceApp.vue:item.placement.idis theWidgetPlacemententity id — an integer throughjsonSerialize().Number.prototype.replacedoes not exist, soplacementId.replace(...)raisesTypeError: placementId.replace is not a function. The guard above it does not catch this: a non-zero integer is truthy, so execution reaches the crash.The exception is thrown inside a Vue event handler, so nothing surfaces to the user — Enter simply does nothing.
How it was isolated
The e2e test at
tests/e2e/tile-quick-search.spec.tspressed Enter on a single match and recorded clicks with a capture-phase listener. It failed with an empty activation list. Two splitting probes were added and both passed in CI run 31483882342:a[href](soactivateSearchResult()'s fallback-to-focus()branch is not the explanation);The sibling test proving
aria-activedescendantselection tracking passes, so the selection mechanism is sound. Anchor present, focus correct, selection correct, no click — which theTypeErrorexplains exactly.Defect 2 — every tile is dimmed, including the matches
Same file:
matchIdscomes from the search results, i.e. the same numeric placement ids.Array.prototype.includesis strict-equality, so[12, 13].includes('12')isfalse— no tile ever matches, and the class is applied to all of them.REQ-QSEARCH-002 requires non-matching tiles to be de-emphasised; applying it to matches as well defeats the feature.
A note on the test that did not catch Defect 2
tests/e2e/tile-quick-search.spec.tscurrently assertsdimmed count > 0after typing. That is satisfied by "everything is dimmed", so it passes on the buggy behaviour. It should assert that the tiles matching the query are NOT dimmed while the others are. That tightening is deliberately not applied in #92, because on currentdevelopmentit would be red — it belongs with the fix.Suggested fix
Coerce once, at the boundary, in both functions:
Coverage status
tile-quick-search::enter-opens-the-selected-tileis left as an open gate-19 finding — not annotated with@e2e exclude— because the scenario is browser-observable and the reason it has no passing test is that the product is broken. The other 8 scenarios in REQ-QSEARCH-001..003 have real, passing tests on #92.