Skip to content

Remove the jest cases the browser suite already asserts - #2239

Merged
umputun merged 3 commits into
masterfrom
jest-thin-stacked
Sep 7, 2026
Merged

umputun merged 3 commits into
masterfrom
jest-thin-stacked

Conversation

@paskal

@paskal paskal commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

The browser suite drives the real widget and #2234 reports what it reaches, so the jest cases it duplicates are two tests of one behaviour: one fails when the widget breaks, the other tells nobody anything new.

Twenty-four cases go, across nine suites. Thirty-three suites give up nothing.

Stacked on #2228 and #2234; the reviewable diff is the last commit alone.

The rule

A case may go only when a named e2e case asserts the same behaviour, and the unit of the decision is the assertion, not the case. A case asserting that a control renders and that its countdown reads 300s needs both covered; the browser suite only waits for that element to appear, so that one was split and the countdown assertion kept.

Coverage rules candidates out, never in. A component renders during unrelated browser tests and marks its lines covered while nothing checks it behaves correctly.

The commonest claim is that a browser case clicking a control covers a jest case asserting it renders. Playwright's Click requires the element to exist, be visible, enabled and stable, and renaming the Vote up title makes TestVote_UpvoteCountsOnce fail at its click, at vote_test.go:49, rather than somewhere incidental downstream.

What went, and from where

suite before after
comment.test.tsx 10 3
comment-actions.spec.tsx 16 12
comment-votes.spec.tsx 13 9
comment-form.spec.tsx 17 14
create-iframe.test.ts 9 7
auth.spec.tsx 18 17
comment-form__subscribe-by-telegram 14 13
profile.spec.tsx 12 11
sort-picker.spec.tsx 4 3

comment.test.tsx gave up the most because its cases were almost all "does this element render", which a click proves.

Why thirty-three suites gave up nothing

They assert redux action objects, spy call counts, postMessage payloads, execCommand calls, CSS-module class names, refs, icon dimensions and translation-tag fallbacks. The browser suite asserts none of those, and mostly should not: asserting a request and its response is the better test of the same thing.

What the deletions were checked against

Jest coverage was generated with the deletions reverted and again with them applied, on this same tree, and the covered-line sets diffed against the widget profile from make e2e-cover. Jest now covers no line it covered before that the browser suite does not also cover.

That check is necessary and not sufficient, and three deletions got past it:

  • the auth dropdown ignoring a clickOutside whose source is not window.parent. auth.hooks.ts:30 is entered by nothing else, because the only such message the browser suite sends comes from the real parent and takes another branch
  • Hide rendering for an ordinary reader on another reader's comment. TestThread_HideUserRemovesTheirCommentsOnly clicks Hide as the dev user, which the stack gives ADMIN_SHARED_ID, so it would pass unchanged if the control became admin-only
  • a duplicate pair deduped the wrong way round, leaving a case whose title says it asserts an empty form while its body asserts a restored draft

The first came from the coverage diff; the other two from review, and neither could have been caught by it, since equal lines can carry different assertions. All three are resolved.

docs/backlog/jest-cases-the-browser-suite-could-take.md records what a browser-side change would release, grouped by cause rather than by file, since one fix frees cases across several suites. Only comments-counter genuinely lacks a production selector; spinner, preloader and comment-actions-additional already carry static classes and simply have no browser case written yet.

@paskal
paskal requested a review from umputun as a code owner August 30, 2026 13:38
Copilot AI lite review requested due to automatic review settings August 30, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codecov

codecov Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.09%. Comparing base (c4c307c) to head (afe41b8).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2239      +/-   ##
==========================================
- Coverage   74.20%   74.09%   -0.11%     
==========================================
  Files         129      129              
  Lines        3745     3745              
  Branches      829      865      +36     
==========================================
- Hits         2779     2775       -4     
- Misses        960      964       +4     
  Partials        6        6              
Flag Coverage Δ
unit 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 30, 2026 •

Copy link
Copy Markdown

size-limit report 📦

Path Size
public/embed.mjs 2.05 KB (0%)
public/remark.mjs 53.81 KB (-0.01% 🔽)
public/remark.css 7 KB (0%)
public/last-comments.mjs 17.37 KB (-0.01% 🔽)
public/last-comments.css 3.22 KB (+0.04% 🔺)
public/deleteme.mjs 2.98 KB (0%)
public/counter.mjs 731 B (0%)

@umputun

umputun commented Sep 7, 2026

Copy link
Copy Markdown
Owner

the replacement holds everywhere except score visibility, so this is the one thing to settle before it goes in.

comment-votes.spec.tsx asserted toBeVisible on Votes score at 0, +1 and -1, and comment.test.tsx had its own visibility check. Nothing picks those up. The browser suite reads the score through score() in e2e/vote_test.go:19, which goes to pollText and so to Locator.InnerText, and innerText returns the descendant text content when the element is not rendered. The vote buttons are separate siblings, so clicking them proves nothing about the score. display: none on that div still satisfies these score assertions after this PR. What survives in jest is toHaveTextContent and one not.toBeInTheDocument, neither of which is a visibility check.

either keep those three cases, or add a visible-score assertion at zero, positive and negative in the browser cases. #2240 needs a rebase anyway now that master moved, so it can carry the fix if that is easier.

everything else checks out. I read each removed group against the browser case named for it: the vote buttons and the disabled upvote against UpvoteCountsOnce and DownvoteAndCorrection, verification and the admin toggle against AdminPinsAndVerifies, edit and delete against EditWithinTheDeadline, EditExpiresAfterTheDeadline and DeleteRemovesTheText, the restored draft against DraftSurvivesReloadAndClearsAfterPost, simple view against SimpleViewHidesTheEditingFurniture, anonymous profile removal against AnonymousReaderIsNotOfferedRemoval, the iframe pair against StaysHiddenUntilTheDocumentReportsInited and IsRevealedByTheInitedMessage, reply against TestComment_ReplyNestsUnderItsParent, pin and unpin against TestComment_AdminPinsAndVerifies, the enabled Telegram subscription against TestTelegramSub_RoundTrip, and the sort-picker class against the .sort-picker select helper TestThread_SortChangeReordersComments uses. The auth-closes-on-document-click case keeps its proof in the retained height-on-close jest case rather than in the browser, which is fine.

The e2e suite drives the real widget and now reports what it reaches, so the
cases it duplicates are two tests of one behaviour: one of them fails when the
widget breaks and the other tells nobody anything new.

The rule is narrower than coverage. A case goes only when a named e2e case
asserts the same behaviour, and the unit of the decision is the assertion, not
the case: a case that asserts a control renders and that its countdown reads
300s needs both covered, and the browser suite only waits for that element to
appear. Coverage rules candidates out, never in, since a component renders
during unrelated tests and marks its lines covered while nothing checks it.

Twenty-four cases go, across nine suites. The largest share is comment.test.tsx,
whose cases were almost all "does this element render", which a click proves:
Playwright refuses to click a control that is missing, hidden or disabled, and
renaming the Vote up title makes TestVote_UpvoteCountsOnce fail at its click.

Thirty-three suites give up nothing. They assert redux actions, spy call
counts, postMessage payloads, execCommand calls, CSS module class names, refs
and icon dimensions, none of which the browser suite asserts and most of which
it should not: asserting a request and its response is the better test of the
same thing.

Checked by measurement: with these cases gone jest covers no line it covered
before that the browser suite does not also cover. That check caught one
unsound deletion, the auth dropdown ignoring a clickOutside whose source is not
window.parent, which no browser case reaches because the only such message the
suite sends comes from the real parent and takes another branch.

Line coverage cannot catch every kind of mistake, and two more came out of
review. Hide rendering for an ordinary reader on another reader's comment was
restored: TestThread_HideUserRemovesTheirCommentsOnly clicks Hide as the dev
user, which the stack gives ADMIN_SHARED_ID, so it would pass unchanged if the
control became admin-only. And a duplicate pair was deduped the wrong way
round, leaving a case whose title says it asserts an empty form while its body
asserts a restored draft; both are the behaviour the browser suite asserts, so
both are gone.

docs/backlog records what a browser-side change would release, grouped by the
cause rather than by the file, since one fix frees cases across several suites.
The jest cases this branch removes asserted the counter's visibility at
each of those values, and nothing in the browser suite did: the score is
read through innerText, which returns the text of an element that is not
rendered, so a display:none on the counter satisfied every browser
assertion while the reader saw nothing. The three browser cases that
already drive the counter through those values now assert it visible at
each, beside the text they already read.
@paskal

paskal commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

Added the browser assertions instead of keeping the three cases. TestVote_UpvoteCountsOnce now asserts the counter visible at zero before the click and at one after it, and TestVote_DownvoteAndCorrection at minus one, beside the text each already read. Checked the way you described it: with display: none on the counter and the image rebuilt, both cases fail at the new lines, vote_test.go:53 and :213, and the -1 text assertion before the second of those still passes, so innerText was indeed reading the hidden element.

The branch is rebased onto master now that #2228 and #2234 are in, so the diff is the one commit plus this one, and #2240 is rebased on top of it. Nothing else in the removal changed.

Codecov fails the project status on any decrease. The frontend suite is
shedding jest cases whose behaviour the browser suite asserts instead, and
codecov measures only the jest half, so each such removal reads as a drop
and paints the change red while covering nothing less. The threshold lets a
deliberate removal pass and still fails a change that stops covering
something.
@paskal

paskal commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator Author

One more commit since your pass: a codecov.yml giving the project status half a percent of slack. This branch and #2240 remove jest cases whose behaviour the browser suite asserts, and codecov measures only the jest half, so each removal read as a drop of a few hits with the line count unchanged and painted the branch red while covering nothing less. The removal itself is unchanged; #2240 is restacked on top.

@umputun
umputun merged commit 9c79071 into master Sep 7, 2026
13 checks passed
@umputun
umputun deleted the jest-thin-stacked branch September 7, 2026 23:32
@paskal paskal added this to the v1.17.0 milestone Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants