Test/playwright UI suite - #582
Open
duderoot wants to merge 9 commits into
Open
Conversation
The frontend had no automated coverage. It is a client-side React app that gets
everything from /api/v1/*, so the suite serves the real index.html and stubs the
API with page.route — no cluster, no Go build, and no changes to the page to make
it testable.
Four specs cover the job-card expansion behaviour: cards open according to
JOB_CARDS_EXPANDED_BY_DEFAULT, a collapse the user made survives the 30s
auto-refresh, and the bulk controls act only on the jobs the stat filter and the
search box leave visible.
staticFrontendServer.mjs mirrors serveHTML and registerUiRoutes rather than being
a generic file server: the frontend only learns its sub-path from the <base href>
and window.__BASE_PATH__ the Go server splices into <head>, so serving the files
plainly would exercise a page the operator never ships. BASE_PATH=/renovate runs
the whole suite under a sub-path.
The tests live outside src/static because the Dockerfile copies that tree into the
image, so anything placed there would ship to production and be served.
Three recipes:
just test-ui run the suite
just ui-dev dashboard with mock data on :8098, no cluster
just test-ui-baseline <rev> run the current specs against another revision's
index.html, to confirm a spec fails without the
change it covers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: duderoot <catalin.patruica@xlab-iq.de>
A dashboard with a realistic number of projects is many viewports tall. The stat badges, the search box and the bulk expand/collapse controls all sat in the normal document flow, so scrolling into the job list took every one of them off screen: narrowing a long list meant scrolling back to the top first, filtering, then finding the place again. They now live in their own bar pinned to the top of the viewport, while the brand strip above it scrolls away like ordinary content. SiteHeader grew a hasBottomMargin prop so the dashboard can put that bar flush underneath it; every other page keeps the margin it had. The badges sit in one horizontally scrollable row rather than the wrapping grid they were in — as a four-row grid on a phone the pinned bar would have eaten the viewport it is pinned to. For the same reason the two copies of the search row (one for sm and up, a mobile-only duplicate further down the page) collapse into a single row, with short Expand/Collapse labels below sm so the search box stays usable. A shadow appears once the page has scrolled, so the bar lifts off the cards instead of carrying one while it sits in place. Pinning the bar puts it in the same stacking context as a job card's execution-options popover and the full-page overlay that dismisses it, so the overlay moves above the toolbar: cards < toolbar < overlay < menu. Otherwise the first click on a badge would filter the list and leave the popover open, hanging over cards it no longer belongs to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: duderoot <catalin.patruica@xlab-iq.de>
Three specs for the bar the previous commit pinned to the top of the viewport. Two pin the behaviour it exists for: on a six-job, twenty-project dashboard the brand strip leaves the viewport while the toolbar stays at y=0 with its badges, search box and bulk controls on screen, and both filtering and collapsing work from the bottom of the page without scrolling back up. The third covers the stacking order. It clicks where a stat badge is while a job card's execution-options popover is open and asserts the popover closes with nothing filtered — the dismiss overlay has to stay above the toolbar. It needs a raw mouse click rather than locator.click(), which refuses to fire at a covered element, which is the state under test. test-ui-baseline cannot show this spec has teeth, since a revision without the toolbar passes it for the wrong reason: point INDEX_HTML_PATH at a copy with only the z-index classes reverted instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: duderoot <catalin.patruica@xlab-iq.de>
The suite only knew about the dashboard, so the page a user lands on to find out why a project failed had no coverage at all — and no way to be opened by hand either, since `just ui-dev` answered every /api/v1 call except the one the logs page makes. /api/v1/logs is an event stream rather than a JSON document, so the fixtures build the frames getRenovateJobLogs writes: one `data:` frame per log line, closed by an `event: done`. A spec serves the whole body in a single route.fulfill and the browser's EventSource parses it frame by frame exactly as it would a live stream; the mock server writes the same frames on a timer instead, so clicking a log link under `just ui-dev` shows the page streaming rather than a finished list. Six specs pin what the page does with a stream once it has arrived: the header counters follow the page's own level thresholds, the level filter narrows the list and is remembered for the next visit while an explicit ?levels= overrides it, and the search box keeps the matching entries, highlights what matched, and opens the rows whose match is hidden below the message. test-ui-baseline now swaps pages/logs.html alongside index.html, so a logs spec can be shown to fail against the revision before the change it covers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: duderoot <catalin.patruica@xlab-iq.de>
A Renovate run logs thousands of lines, so the logs page is far taller than the dashboard ever gets. The search box, the level filter and the copy/download buttons all sat in the normal document flow, which meant the moment a user scrolled to the error they came for, every control that could narrow the list was gone — and narrowing it is the whole reason to open the page. The error and warning counters went with them. They now live in the same kind of pinned bar the dashboard got: the counters in one horizontally scrollable row, the search box and the three controls in the row below, with the brand strip above scrolling away like ordinary content. The streaming/completed indicator moves up there too — whether the log is still growing belongs next to the counters that keep changing with it, not on a line that scrolls out of sight. What stays behind in the page body is the heading and the project name, which say what you are looking at, not what you can do to it. Below sm the Copy and Download labels drop to their icons so the search box keeps a usable width; both buttons carry an aria-label, so their accessible name no longer depends on a label that may be hidden. The bar itself is now a component. The dashboard's was written inline — its scroll listener, its stuck state and the six classes that make it a bar — and a second copy on the logs page would have been the kind of thing that drifts apart one fix at a time. StickyToolbar owns all of it, including the z-30 that keeps it above the page content and below the overlays that dismiss a popover, so a page only says what goes in the bar. The dashboard renders identical markup through it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: duderoot <catalin.patruica@xlab-iq.de>
Three specs for the bar the logs page now pins to the top: it stays put while the brand strip scrolls away, the search box narrows the log from wherever the reader already is rather than after a trip back to the top, and the level filter opens over the rows it now floats above — that last one only passes because the bar wins the stacking order, and a click on the checkbox would otherwise land on a log row. All three fail against the revision before the change, while the six specs for the page's filtering behaviour pass on both, which is the split that says the fix is what they are pinning down and not the page in general. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: duderoot <catalin.patruica@xlab-iq.de>
# Conflicts: # CLAUDE.md
duderoot
requested review from
beneiltis,
lukashankeln and
wittdennis
as code owners
August 6, 2026 15:42
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.
TL;DR
According to #559 the UI is expecting to change and to grow by receiving new features. With the e2e test with playright we have a bigger confidence into changes 😜
Closes #581.
What
The harness that issue asks for, built as described: the real
index.htmlandpages/logs.htmlserved through a stand-in for the Go server,/api/v1/*stubbed perspec with
page.route, fixtures mirroring the Go payloads, and everything undertests/uiso nothing lands in the image.No production code is touched. The diff is
tests/ui/**, three recipes in theJustfile, five ignore lines, and two lines ofCLAUDE.md(thetests/uitree andjust test-uiunder Verification). Nothing about the Go build, the image, the chartor the API changes, and neither page was modified to make it testable.
What it covers
16 specs, about 8 seconds, one Chromium project at 1280x900. They pass under
BASE_PATH=/renovateas well, which until now was only exercised by Go unit tests.jobCardExpansion.spec.mjsJOB_CARDS_EXPANDED_BY_DEFAULT; a collapse the user made survives the 30s auto-refresh; the bulk controls act only on the jobs the stat filter and the search box leave visiblestickyToolbar.spec.mjslogsFiltering.spec.mjs?levels=; search keeps the matching entries, highlights them, and opens the rows whose match is hidden below the messagestickyLogsToolbar.spec.mjsHow it works
staticFrontendServer.mjsmirrors
serveHTMLandregisterUiRoutes, including the<base href>andwindow.__BASE_PATH__spliced into<head>. A plain file server would silently testa page that never ships.
page.route, so each test owns its data and canreach states a cluster produces rarely.
ui.RenovateJobInfo,crdManager.RenovateProjectStatus, and the SSE framesgetRenovateJobLogswrites.PROJECT_STATE_VARIANTSis a catalogue of every project state the dashboard rendersdifferently, so a spec can name the states a filter should keep instead of counting
rows. When a field is added to either Go struct, it needs adding there too; the README
says so, and the tests are only as honest as those payloads.
/api/v1/logsis an event stream, so a spec serves the whole body in oneroute.fulfilland the browser'sEventSourceparses it frame by frame exactly as itwould a live stream.
just ui-devwrites the same frames on a timer instead, soclicking a log link shows the page streaming rather than a finished list.
src/staticbecause the Dockerfile copies that tree into theimage, so anything placed there would ship and be served.
Does it actually catch anything?
That is the question a test-only PR has to answer, so
test-ui-baselineexists toanswer it: it serves both pages from any revision while keeping today's specs.
Run against
9e894f1^— the revision before the toolbar change of #572 landed:That split is the point. The five failures are the specs that pin what #572 changed —
they fail on the revision before it and pass on
main. The eleven passes are specsabout behaviour that #572 did not touch, so they hold on both revisions and would have
caught a regression in it.
The one sticky spec that passes on both is deliberate and worth keeping: it guards a
regression that pinning the bar could introduce rather than proving a fix. With the
bar in the same stacking context as a card's execution-options popover, a click on a
stat badge would otherwise filter the list and leave the popover hanging over cards it
no longer belonged to.
Decisions worth a second opinion
data-testid. A row has no role of its own, and I did not want to add test hooks to aproduction page for the sake of the suite. The cost: the level dropdown repeats those
same words, so it has to be closed before counting rows — documented in the README and
handled by the page object. If you would rather have
data-testid="log-row"in themarkup, that is a one-line change and I will make it.
above-the-fold assertions mean the same thing on every machine. The phone layout is
not covered.
page.routestubs in specs,mockOperatorApi.mjsfor
just ui-dev. They share the fixture builders but not the wiring. The alternativewas one mock server driving both, which would have made every spec depend on global
state.
Cost
npm ciplus a Chromium download on the first run ofjust test-ui; both are cachedafterwards.
node_modules,test-resultsand the reports are git-ignored.just build,just test-unit,just test-helmandjust generateare unaffected — nothing in theGo pipeline runs this.
Not in this PR
Both are named as out of scope in #581 and I would rather they be judged on their own:
just test-uitoday;go-test.yamlbuilds and tests Go only.Wiring it in, and deciding whether it gates merges, is the obvious next step.
rather than spot-checked by hand.
About the commit history
The branch is where the two toolbar fixes were developed, so
fix(ui): pin the dashboard filters…andfix(ui): pin the logs filters…appear in its history. Both are alreadyon
mainvia #572, so they contribute nothing to this diff —git diff origin/main...is tests only. There are also two merges from
main. Happy to rebase or squash thehistory into a single
test(ui):commit if you prefer a clean list.Checklist
test/playwright-ui-suite(testis aConventional Commits type;
CONTRIBUTING.mdlists four prefixes and not this one)Signed-off-by(DCO)Co-Authored-Bynaming the modelcontributor docs are in
tests/ui/README.mdand two lines ofCLAUDE.md