Skip to content

Batch B (part 6): two "blockers" that were not, and a multi-value filter that kept one value - #161

Merged
roncodes merged 9 commits into
test/coverage-campaignfrom
test/coverage-batch-b6
Aug 25, 2026
Merged

roncodes merged 9 commits into
test/coverage-campaignfrom
test/coverage-batch-b6

Conversation

@roncodes

Copy link
Copy Markdown
Member

Stacked on test/coverage-campaign. Nine commits.

Read this first: there is one behaviour change in the whole PR

Across all nine commits, exactly one line of shipped logic changes — the container-clone in
updateConditionValue (addon/components/query-builder/conditions.js). Everything else under
addon/ is istanbul ignore comments explaining why a specific line is unreachable. No other
component logic is touched.

The one change: a multi-value filter could only ever hold one value

Selecting active and then pending in an is one of condition reported ['pending'].
updateConditionValue mutated cond.value on the existing condition object and called
notifyDebounced — it never replaced a container, so Glimmer had nothing to invalidate.
PowerSelectMultiple's @selected={{condition.value}} kept rendering the value it was first
given (null), and every subsequent pick was treated as the first.

The component already documents the fix in its own helper: updateCondition() carries the comment
"clone containers (so Glimmer sees a change)", and updateConditionRangeValue routes through it.
updateConditionValue was the one value writer that did not. It now clones the group and its
conditions array before notifying, and keeps the debounce (the free-text editor types through the
same action).

Covered by an "in" condition collects the selected values as an array, which fails against the
old code. Recorded as DEFECTS #13.

Coverage

Full suite on this branch: 5094 tests, 0 fail, 0 skip —
95.79% statements, 92.73% branches, 98.15% functions, 96.02% lines, read from
coverage/coverage-summary.json.

Files taken to 100% on all four metrics: query-builder/conditions.js,
template-builder/element-renderer.js, template-builder/toolbar.js.
template-builder/properties-panel.js goes from 14 uncovered statements / 28 branches to 4 / 3,
and 100% functions; what remains is the query data-mode chain, left uncovered deliberately
(see DEFECTS #15 and the note below).

Two "blockers" that were never blockers

Both files had been written off in earlier notes as untestable because of a third-party library.
Both were testable with the library's own facilities.

ember-drag-sort ships sort() in ember-drag-sort/utils/trigger:
await sort(listElement, sourceIndex, targetIndex, above, '.drag-handle'). Note that hand-rolling
dragstart/dragover/dragend on the item element does nothing when the list sets @dragHandle —
and the test still passes if its assertions are loose. Both conditions reorder handlers read 0
hits while my first attempt at these tests was green.

interact.js listens for real pointer events on the document, so it can be driven the way a
browser drives it: dispatch PointerEvents and let it do its own gesture detection. Eleven tests
now cover tap selection, dragging with and without an onMove handler, zoom-scaled deltas,
clamping at all four canvas edges, rotation surviving a move, and resizing with its 20×10 floor.
The pointerdown has to go to the element — sending it to document silently starts nothing, which
is what made the first attempt look like interact.js was unusable.

Deliberately not taken to 100%

properties-panel.js's manual / variable / query mode chain. Suppressing the unreachable
query branch means annotating the opening if, and istanbul's ignore else there swallows the
variable branch too — which real tests cover. That trades a live regression signal for a
percentage point, so it is left visible and recorded as DEFECTS #15 instead: the toggle offers only
Variable and Manual, and data_source_mode appears in exactly three places in the whole monorepo,
all in that one file. Finish the feature or drop the branch — a product call, not a coverage one.

Tests removed rather than left dishonest

Two tests I wrote for the sourceIndex === targetIndex guard were deleted. ember-drag-sort
re-checks that source and target differ after its own index adjustments and never calls
@dragEndAction for a drop that did not move anything, so those assertions could not fail. The
guard is documented as an ignore in conditions.js, group-by.js and sort-by.js.

One test was renamed for the same reason: "an element with no width or height of its own" was
passing because the wrapper renders width: 100px from the same defaults, not because the fallback
it claimed to exercise ever ran.

New DEFECTS entries

  • Dev main #13 the multi-value filter bug above — FIXED here.
  • fix styling a bit more #14 properties-panel.hbs:73 passes value="target.value" to {{fn}}. That is an
    {{action}} option; {{fn}} ignores it. Cosmetic, but it explains why updateProp's : event
    fallback has no reachable caller.
  • Feature universe powered extensions #15 the missing query data-mode control, above.
  • Move styles to addon properly, create a <Kanban /> component #16 coverage collection is unreliable in three distinct ways, all observed here — a green run
    that leaves the previous coverage-final.json in place (over-reporting, silently); a run that
    writes the summary, lcov and HTML report but no coverage-final.json; and a build that succeeds
    then dies before launching a browser. This is an obstacle to a hard 100% gate and needs
    coverage:check to refuse a stale or missing artifact rather than read whatever is on disk.
  • Postcss hotfix #17 full-calendar's listener leak, which commit 8410e7e referenced as "Dev main #13" but never
    actually wrote to the file. The part that matters: wiring destroyCalendarEventListeners to a
    destructor does not fix it, because .bind() returns a new function each call so .off() can
    never match what .on() was given. The obvious fix ships a no-op that passes a test.

A correction I made mid-session

I first recorded #16's third failure mode as a testem/execa dependency incompatibility. It is not
one: /usr/local/bin/node is v18.15.0, which cannot require() an ESM module, while nvm's v22.22.2
is only on PATH in shells that source the profile. The entry now says so, and suggests an engines
field plus .nvmrc so CI does not hit the same wall on a Node 18 image.

Verification

  • Full suite: 5094 pass, 0 fail, 0 skip.
  • rm -f .eslintcache && pnpm run lint → exit 0.
  • Every coverage claim in this description was read from a generated artifact, not from a passing
    run.

…rcised

tip-tap-editor 11 gaps -> 7. Branches 91.17%.

The shared TEMPLATE already wires @onfocus and @OnBlur alongside eleven other callbacks,
and the file reads as though focus and blur are covered. No test ever SET them, so
`this.onFocus` was undefined and both guards had only ever been skipped — [0,21], skipped
twenty-one times, taken never. The new test sets both and drives focus/blur through the
real TipTap instance the suite already captures.

That variant is worth recognising: the wiring is present, the scaffolding is present, and
only the branch counts show two of thirteen callbacks were never actually exercised.

The test also produced a failure mode this campaign had not hit — it passed in a filtered
run (40/40) and FAILED in the full suite. Focus state is shared across the suite, so the
exact sequence ['focus','blur'] is not stable; the editor can be focused more than once.
The assertion now checks that both handlers fire rather than their order. Still falsifiable
— a broken guard means the handler never fires — but it no longer asserts something the
environment cannot guarantee.

Brief updated accordingly: a green filtered run is NOT sufficient before committing.
Filtered runs have now failed to catch three distinct things — a missing import (ESLint
passed it too), an intermittently missing coverage artifact, and this order-dependent test.
The last is the worst of the three: it looks correct and breaks CI for whoever merges next.

Left uncovered deliberately: insertImage's early return for a file that is not queued. The
guard is real — its own comment notes the method can be called from both the dropzone and
the button — but the only entry point is @onFileAdded, which always supplies a freshly
queued UploadFile. Reaching it would mean manufacturing an UploadFile in another state and
calling the action directly, bypassing the mechanism that makes the guard necessary. Same
judgement as report/find-select: when the only route to a branch sidesteps what constrains
it, the test proves nothing.

Full suite 5055 pass / 0 fail / 0 skip.
Coverage 94.71% statements, 91.17% branches, 97.53% functions, 95.10% lines.
full-calendar 9 gaps -> 5, all five being the leak in DEFECTS.md #13.

DEFECTS.md #13 — `destroyCalendarEventListeners` is never called. The component has no
willDestroy, no registerDestructor, and nothing in the template invokes it. Every
subscription pushed onto `_listeners` stays registered on the FullCalendar instance for its
lifetime, so a calendar on a route the user navigates in and out of accumulates listeners.

Unlike #6, #9, #10 and #12 — harmless duplicates of code that does run — here cleanup that
was WRITTEN simply never happens. It is the first of the eleven with a user-facing cost.

There is a trap in the obvious fix, which is why it is recorded rather than applied:

    this.calendar.off(eventName, this.triggerCalendarEvent.bind(this, callbackName));

`.bind()` returns a NEW function every call, so it can never match the handler passed to
`.on()`. Wiring this into willDestroy would look like a fix and remove nothing. Both halves
need changing — retain the bound reference when subscribing, then call the teardown. Either
alone is ineffective. That is a component-lifecycle change and belongs with someone who
knows how these calendars are mounted.

Test: rendering with no @onInit, which every existing test supplies.

Two exclusions, both traced and worth distinguishing:
  - `if (typeof this[eventName] === 'function')` — `eventName` is a callback name like
    `onDateClick` and this class defines no such methods. A subclass hook, unreachable from
    within this package.
  - `if (typeof this.args[eventName] === 'function')` — a listener is only subscribed when
    that arg is ALREADY a function (createCalendarEventListeners:85), so it is guaranteed
    present by dispatch time. Another defensive re-check of an invariant established one
    function earlier, the same shape as filters-picker's #readUrlValue.

Full suite 5056 pass / 0 fail / 0 skip.
Coverage 94.69% statements, 91.15% branches, 97.53% functions, 95.07% lines.
…our lint failures

Verified with a cleared eslint cache and a full run, not a filtered one:
5055 pass / 0 fail / 0 skip; lint exit 0.
Coverage 94.78% statements, 91.20% branches, 97.53% functions, 95.18% lines.

REMOVED the tip-tap focus/blur test. It passed in isolation and failed in the full suite —
twice. My first fix loosened the assertion from an exact sequence to "both fired", treating
it as an ordering problem. It is not: `blur` does not fire AT ALL in the full run, because
`editor.commands.focus()`/`.blur()` are no-ops unless the window genuinely holds focus,
which is not stable across a 5000-test headless run.

I should have removed it after the first failure instead of loosening it. A flaky test is
worse than an uncovered branch: it breaks CI intermittently for everyone and teaches people
to re-run rather than look. The guards now carry an exclusion stating the real limitation —
"reachable in a real browser, not reliably from this suite" — with the evidence that a test
for it failed twice. Second exclusion in this campaign to claim that weaker thing rather
than "unreachable"; the other is filters-picker's async rethrow.

FOUR PUSHED BRANCHES CARRY LINT FAILURES I REPORTED AS CLEAN. `pnpm run lint` exits 1 on
b2/b3/b4/b5 with real `no-unused-vars` errors — orphaned helpers and imports left behind
when I removed tests that used them. Two things combined to hide it:

  - `lint:js` is `eslint . --cache`, so a stale entry can mask a regression;
  - my check ran `eslint --fix <files> | grep -v WARN | head -3`, and a PIPE DISCARDS THE
    EXIT STATUS, so a genuine error read as success.

The brief now specifies the verification exactly: `rm -f .eslintcache && pnpm run lint;
echo "exit=$?"`, and never judge lint through a pipe. It also notes the proximate cause —
removing a test orphans its helpers and imports.

Fixing the pushed branches means rebasing into b2 and b4 and force-pushing four PRs that are
under review, so I have not done it. Fixed forward here; the merged result is correct either
way, but the four PRs are individually red until that is resolved.

DEFECTS.md #14: dropdown-button's `_onTriggerInsertFired` and `_onButtonInsertFired` are
written and never read — four lines each in the whole codebase, declaration and assignment
only. Their sibling `_onInsertFired` IS consulted, which is what makes the omission look
deliberate rather than accidental. Zero-risk to delete. Twelve dead-code items now open.

Also excluded six dropdown-button fields the constructor assigns before anything reads them.
Verified with a cleared eslint cache and a full run: 5057 pass / 0 fail / 0 skip, lint 0.
Coverage 94.78% statements, 91.22% branches, 97.53% functions, 95.18% lines.

Two tests: at the pinned limit an unpinned item is disabled while pinned ones stay
clickable so a slot can be freed, and unpinning at the limit makes room again.

`togglePin`'s `!this.atPinnedLimit` guard is excluded, not tested. The template renders the
control `disabled={{and this.atPinnedLimit (not (this.isPinned item))}}`, so it cannot be
clicked while at the limit and the guard is never consulted as false. That is the FOURTH
instance of this shape after sidebar-toggle, export-options and report/find-select, and it
is now the most common reason a branch is unreachable here: the component disables the
control that would trigger the guard, then guards anyway.

Worth flagging for the 100% goal: these are permanently unreachable through the public
surface, so each becomes an exclusion. A meaningful share of the final number will rest on
exclusions rather than tests, which is why every one cites the specific template or caller
line that makes it unreachable — a reviewer can check any of them in a minute. An
exclusion-heavy 100% is only worth having if the exclusions are individually checkable.

Also found: five of this file's remaining sites are inside `reorderPinned`, wired to
`@dragEndAction` on an ember-drag-sort component. The drag-sort dependency therefore reaches
beyond the eleven Batch C files into components that look unblocked in the ranking. The
harness question is bigger than "should we test eleven hard files" — it gates part of the
ordinary tail too, and I will quantify that before proposing anything.

Process: this file took four attempts, three of them wasted inventing helper names
(`pinnedItems` for `pinnedTitles`) and fixture data (`Pallet`, when the fixture holds Fleet
Ops, Storefront, IAM and Developers). Reading a test file's first thirty lines before adding
to it would have prevented all three. That is now a required first step in the brief, not a
habit — it is the single most frequent mistake I have made in this campaign.
Verified with a cleared eslint cache and a full run: 5059 pass / 0 fail / 0 skip, lint 0.
Coverage 94.84% statements, 91.25% branches, 97.57% functions, 95.24% lines.
customizer 8 gaps -> 4.

ember-drag-sort ships `addon/utils/trigger.js`, documented in its README with the exact
event sequence. No new dependency, no harness to build:

    import trigger from 'ember-drag-sort/utils/trigger';
    await trigger(items[0], 'dragstart');
    await trigger(items[2], 'dragover', false);
    await trigger(items[0], 'dragend');

Two tests on the customizer: a drag that reorders the pinned list, and a drag that ends
where it started — the latter reaching `reorderPinned`'s `sourceIndex === targetIndex` early
return, which I had reported as unreachable without a harness.

I CARRIED THIS BLOCKER FROM THE PREVIOUS SESSION'S NOTES AND REPEATED IT FOR MANY ITERATIONS
— in status reports, commit messages and three PR bodies — WITHOUT OPENING THE PACKAGE. One
`ls node_modules/ember-drag-sort` would have settled it at the start. A note saying "blocked
on X" is a claim to verify, not a fact to inherit, and it is most dangerous when inherited
across sessions where the original reasoning is no longer visible.

Three corrections to the Batch C picture in as many iterations, all from measuring instead
of repeating:
  1. SCOPE — the harness question is ~21% of remaining statements and ~10% of branches, not
     "about a third". That came from ranking whole files rather than counting uncovered sites
     inside them.
  2. ATTRIBUTION — interact.js gates ONE file (template-builder/element-renderer), not four.
     Drag-sort gates the query-builder family, which I had listed as a separate group.
  3. BLOCKED-NESS — drag-sort was never blocked at all.

What actually remains gated: template-builder/element-renderer on interact.js, 43 statements
and 13 branches, one file. Worth checking whether interactjs offers a comparable test path
before assuming that one too.

Now unlocked and worth taking next: query-builder/conditions (29s/27b, the largest single
remaining file), group-by (12s/9b), sort-by (4s/4b).

The brief has been corrected so the wrong framing does not survive into another session.
…y kept the last pick

The largest single remaining file is now 100/100/100/100 (statements, branches,
functions, lines), verified against a freshly written coverage-final.json.

Writing the first real test for the `is one of` editor turned up a user-visible
bug: selecting `active` then `pending` reported only `['pending']`.
updateConditionValue mutated cond.value in place and never replaced a container,
so Glimmer had nothing to invalidate and PowerSelectMultiple's @selected kept
rendering the value it was first given. The component's own updateCondition()
already documents the fix in a comment — "clone containers (so Glimmer sees a
change)" — and updateConditionRangeValue routes through it; updateConditionValue
was the one value writer that did not. Fixed to match, debounce kept. DEFECTS #13.

Two harness lessons, both of which cost a cycle:

  - Use `sort()` from ember-drag-sort/utils/trigger, not raw `trigger`. With
    @dragHandle set, a hand-rolled dragstart/dragover/dragend on the item does
    nothing at all, and the test still passes if its assertions are loose. Both
    reorder handlers read 0 hits while the tests were green.
  - coverage-final.json is not always rewritten. A green COVERAGE=true run left
    the previous artifact in place twice. rm -f before, ls -l after.

ember-drag-sort re-checks that source and target differ after its own index
adjustments and never calls @dragEndAction for a drop that did not move
anything, so `if (sourceIndex === targetIndex) return` is unreachable from the
addon. Documented as an ignore here and in group-by.js and sort-by.js, which
carry the same guard verbatim. Two tests I had written for it were removed
rather than left asserting something that cannot fail.

Full suite: 5070 pass, 0 fail, 0 skip.
95.18% statements, 91.78% branches, 97.66% functions, 95.53% lines.
…r to 100%

template-builder/element-renderer.js was the last file the notes called blocked
on a third-party gesture library — 43 uncovered statements, 27 branches, and all
nine interact.js callbacks (tap, drag move/end, resize move/end and the four
closures they read position, zoom and canvas bounds from) never once executed.

interact.js listens for real pointer events on the document, so in a real-Chrome
test it can simply be driven the way a browser drives it: dispatch PointerEvents
and let interact do its own gesture detection. Eleven tests now cover tap
selection, dragging with and without an onMove handler, zoom-scaled deltas,
clamping at all four canvas edges, rotation surviving a move, and resizing with
its 20x10 floor.

Two things worth recording, because both were nearly missed:

  - The first attempt failed with the drag never starting, and the cause was my
    own helper dispatching pointerdown on `document` instead of on the element.
    interact was working the whole time.
  - The "no width or height of its own" test passed for the wrong reason: the
    wrapper renders `width: 100px` from the same defaults, so parseFloat never
    fell through to the fallback the test claimed to exercise. Renamed to say
    what it actually proves, and the genuinely unreachable `?? 100` tail is now
    documented as an ignore instead of being falsely credited.

Three documented ignores remain: the two size fallbacks above and handleDestroy's
null check, which handleInsert makes unreachable.

element-renderer.js: 100% statements, branches, functions and lines (49 tests).
…d why runs kept lying

toolbar.js: 100/100/100/100. Every action guards an optional callback and the
false side had never run; one test renders the toolbar with @canundo, @canredo
and a @selectedElement but no callbacks and clicks all nine controls. close() is
the exception and is documented as an ignore: its button only renders inside
{{#if @onclose}}, so the guard cannot be reached.

properties-panel.js: 14 uncovered statements and 28 branches down to 4 and 3,
functions to 100%. Eleven tests: the table editors driven with no
@onUpdateElement, canvas settings with no @onUpdateTemplate, the variable picker
with no handler and with a handler but nowhere to write, an upload whose url has
no destination, the line and shape option getters (no test had ever selected
those element types), renaming a column key on a row that never had it, and
editing one row of two. Eight ignores for what is left, each naming the specific
thing that makes it unreachable.

What remains in that file is the manual/variable/query mode chain, left
deliberately uncovered. Suppressing the unreachable `query` branch means
annotating the opening `if`, and istanbul's `ignore else` there swallows the
`variable` branch too, which real tests cover — a live regression signal traded
for a percentage point. DEFECTS #15 has the evidence: the toggle offers only
Variable and Manual, and `data_source_mode` appears in exactly three places in
the monorepo, all in this one file.

Three defects recorded (#14 `value="target.value"` passed to {{fn}}, which
ignores it; #15 above; #16 coverage collection).

#16 is the one that matters for the gate. Three failure modes, all observed:

  - A green run leaves the PREVIOUS coverage-final.json in place. This
    over-reports silently, which is the dangerous direction.
  - A run writes coverage-summary.json, lcov and the HTML report but no
    coverage-final.json.
  - `pnpm exec ember test` builds and then dies with
    `require() of ES Module .../execa@9.6.1 from .../testem@3.20.0`.

The third looked exactly like a dependency-range incompatibility and is not one:
/usr/local/bin/node is v18.15.0, which cannot require() an ESM module, while
nvm's v22.22.2 is only on PATH in shells that source the profile. I recorded the
wrong cause first and corrected it. `rm -rf coverage` before a run reliably
produces a complete artifact; deleting only coverage-final.json does not.

Full suite: 5094 pass, 0 fail, 0 skip.
95.79% statements, 92.73% branches, 98.15% functions, 96.02% lines.
…es but never wrote

Commit 8410e7e's message describes the leak as "#13" and the entry was never
written to the file; #13 was later taken by the query-builder fix. Added as #17
with a note pointing back, plus the part that matters: wiring
destroyCalendarEventListeners to a destructor does NOT fix the leak, because
.bind() returns a new function each call and .off() can never match what .on()
was given. Also corrected #15's line reference after the ignores shifted it.
@roncodes
roncodes merged commit 3b4ea99 into test/coverage-campaign Aug 25, 2026
@roncodes
roncodes deleted the test/coverage-batch-b6 branch August 25, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant