Skip to content

Control over where automation and e2e runs (BL-16804) - #8285

Merged
hatton merged 11 commits into
masterfrom
BL-16804-headless
Sep 3, 2026
Merged

Control over where automation and e2e runs (BL-16804)#8285
hatton merged 11 commits into
masterfrom
BL-16804-headless

Conversation

@hatton

@hatton hatton commented Sep 2, 2026

Copy link
Copy Markdown
Member

BL-16804 — https://issues.bloomlibrary.org/youtrack/issue/BL-16804

Split out of BL-16799 so the window-placement change can be reviewed on its own.

The problem

Every run of the src/BloomE2E suite opened a Bloom window on the developer's desktop, which makes the suite unusable while working. Three separate controls decided where that window went, and none of them was discoverable: an environment variable BLOOM_AUTOMATION_MONITOR that only the main window read, a silent fallback to the primary monitor, and a BLOOM_E2E_HEADED variable in the Playwright fixture.

The fix

BLOOM_AUTOMATION_MONITOR becomes the one control, and every window an automation run opens obeys it, the splash screen included:

  • a monitor number, counted left to right — every window opens on that monitor, so a run stays off the one you are working on. Monitor 1 is the leftmost.
  • headless, or 0 — every window opens far outside every monitor, so a run can go on while you work.
  • unset, or a value Bloom cannot use — Bloom places its windows as it always does, so a typo shows you a window rather than hiding one.

The variable applies only under --automation, so a Bloom you start yourself is unaffected however it is set. The nightly workflow now asks for headless explicitly.

Four things that are not obvious, each explained in a code comment where it matters:

  • The window moves off every monitor rather than minimizing or hiding, because WebView2 stops painting a minimized window and every screenshot then comes back blank. Windows reports a far-off-screen window as occluded, so Chromium also needs CalculateNativeWinOcclusion disabled and --disable-backgrounding-occluded-windows.
  • ShowInTaskbar is set in the constructor. Assigning it to a form that is already showing makes WinForms recreate every handle, the WebView2 host included.
  • The off-screen window goes straight down from the primary monitor, and leaves four window heights of clearance. Both numbers are about DPI, not about space: Windows gives a window the scale factor of the monitor nearest to it, and this process asks for a size in the primary monitor's own scaled pixels. Going sideways makes another monitor nearest, so a window meant to be 3840x2100 came out 3840x2100 real pixels on a 100% monitor beside a 150% primary, taller than any monitor on the machine. Going down keeps the primary nearest on a side-by-side layout. Four heights, because Windows scales a monitor by at most 400%; an earlier sideways version left 27 pixels of a 1000-pixel cushion. Every unit test passed over both bugs, because they compare in the test process's own coordinates.
  • The placement is still wrong for one layout, and the code says so rather than pretending otherwise: a monitor stacked below the primary, overlapping it horizontally, becomes the nearest one. Fixing that needs per-monitor DPI calls Microsoft does not document, so it is written up in src/BloomE2E/AUTOMATION-DEBT.md.
  • An automation run never writes the saved window placement, because every Bloom of one build shares one user.config.

The monitor number is not the one Windows Settings shows

Nothing can reproduce those numbers. QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS), the API that is supposed to give them, returns the paths in exactly the Screen.AllScreens order; the \.\DISPLAY<n> device names do not match either; and Microsoft documents no rule. Measured on one three-monitor machine, Windows Settings said 1 (primary, centre), 2 (right), 3 (left), while both APIs said 1 (right), 2 (left), 3 (primary, centre).

So the number counts left to right, which follows the arrangement picture a developer can read even though it is not the numbers printed on it. This changes what an existing variable means: master indexed Screen.AllScreens, the order of the display drivers. Bloom also writes the whole mapping to %TEMP%\SIL\Bloom\Log.txt on every automation start, so a developer can read the number instead of guessing it.

Tests

src/BloomTests/AutomationWindowPlacementTests.cs covers the variable in 24 tests: every value that names a monitor, headless in four casings, 0, the unusable values, the left-to-right ordering, the off-every-monitor bounds against however many monitors the machine has, and that a run which is not automation ignores the variable.

Recorded rather than done

Two pieces of work this raised are written up in src/BloomE2E/AUTOMATION-DEBT.md instead of being done here:

  • Running the suite at a chosen monitor resolution and scale factor. Both DPI bugs above needed a real window at a real scale factor to show up; no unit test can catch them.
  • Giving every run a small 1024x586 window by default, the working area of a 1024x768 screen, to catch the layout bugs that users on inexpensive machines meet and developers never do. It was written and measured: a full run at that size gives 16 passed, 6 failed, 13 did not run, against 23 passed and 2 failed at monitor size. The entry names the four tests the small window breaks and the triage each one needs.

Verified against a real Bloom as well: BLOOM_AUTOMATION_MONITOR=2 put the window on the primary monitor, 0 put it off every monitor, and the tab test passed both times.


This change is Reviewable

Devin review

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Greptile has paused reviews on this repository — it used its 100 free open-source review credits for this billing period. Reviews resume automatically on September 24. To continue before then, an organization admin can keep reviews running past the free credits — those bill as normal usage.

Every run of the src/BloomE2E suite opened a Bloom window on the developer's
desktop, which makes the suite unusable while working.

A new --headless startup flag puts the shell and the splash screen far to the
left of every monitor and keeps them out of the task bar, and Bloom does not save
those bounds as the window placement. The window goes off-screen rather than
minimized or hidden because WebView2 stops painting a minimized window, which
makes every screenshot blank; off-screen it paints exactly as it would in front
of a person, so rendering and keyboard input behave the same.

Windows reports a window that far out as occluded, and Chromium then stops
rendering it, so a headless run also turns off CalculateNativeWinOcclusion and
the backgrounding of occluded windows.

Shell sets ShowInTaskbar in its constructor rather than in Shell_Load, where the
rest of the headless placement happens. Shell_Load runs during Show(), when the
form already has a window handle, and Windows Forms answers an assignment to
ShowInTaskbar there by recreating that handle and every child handle under it,
including the WebView2 host. The Edit tab was left with a browser that no longer
answered editView/jumpToPage, so every e2e test that moves between pages hung
until goToPage gave up.

The e2e fixture passes the flag by default. BLOOM_E2E_HEADED=1 shows the window,
and Playwright's --debug implies it, because stepping through a test whose window
you cannot see is pointless.

Split out of BL-16799 so that it can be reviewed on its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/BloomExe/SplashScreen.cs
Comment thread src/BloomExe/Program.cs Outdated
@hatton

hatton commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 5 (1M context) from Hatton's machine during preflight]

Consulted Devin on 2026-09-02, up to commit a07b973b1309a399b1f21db8b7bd512e48d8264c. Five rounds, one per pushed commit. Every finding is accounted for, and no thread is left open. This replaces the earlier version of this note, which named an older head.

Devin's two informational notes confirm the design and are not mirrored here.

Also for the record, at commit 81f2876a00, whose only difference from the head is comment text in one test file: the C# suite passes (3316 passed, 13 skipped, 3m52s) and CI is green. The end-to-end suite does not pass on this branch alone: 18 passed, 2 failed, 8 did not run (5.1 minutes). Both failures are in duplicate-page.spec.ts, and both also fail on master and on this branch with the window on screen, while they pass on #8276. So they are inherited, not caused by this pull request, and merging 8276 first clears them. Greptile has not spoken about this pull request. CodeRabbit is switched off for this repository by .coderabbit.yml.

hatton and others added 4 commits September 2, 2026 16:57
…ifference

Master now says the espagnol / espanol difference in the Text Languages test
is machine-dependent, not a flake: LibPalaso needs a findable native ICU
library, and Bloom ships icu.net without icuuc.dll.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BLOOM_AUTOMATION_MONITOR becomes the only control, and every window an
automation run opens obeys it, the splash screen included. It takes a
1-based monitor number, the word "headless" for off every monitor, or
anything else, which leaves Bloom to place its windows as it always does.

This replaces three controls that each did part of the job: the --headless
startup flag, the BLOOM_E2E_HEADED variable in the Playwright fixture, and
a silent fallback to the primary monitor when the variable named a monitor
the machine did not have. A value Bloom cannot use now gives a visible
window, which is the outcome that tells the developer the variable did not
take effect.

The new class AutomationWindowPlacement holds the whole decision. Its
Parse method takes the raw value and the monitor count as arguments, so
the 21 new unit tests need neither the environment variable nor a machine
with a particular set of monitors.

The off-screen position leaves four window widths of clearance, not a
fixed 1000 pixels. Bloom computes the position in its own coordinate
space, and on a monitor scaled to 160% a window it believed was 1587
pixels wide came out 2560 pixels wide, which left 27 pixels of the old
cushion. Windows scales a monitor by at most 400%, so four widths clears
the leftmost monitor whatever the scale factors are.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/BloomE2E/fixtures/launchBloom.ts
hatton and others added 2 commits September 3, 2026 07:20
Before this branch the Playwright fixture passed --headless on every
launch, so the nightly ran off-screen. Removing that flag left nothing in
its place, so the nightly would have started opening a window without
anybody choosing that. The Run BloomE2E tests step now sets
BLOOM_AUTOMATION_MONITOR=headless.

Devin raised the consequence. It suggested making off-screen the default
in the fixture instead, which is declined: a developer's own run showing a
window is the point of the change. The workflow is the one caller that
cannot ask for what it wants at the moment it runs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BLOOM_AUTOMATION_MONITOR took a 1-based index into Screen.AllScreens, which is
the order of the display drivers. That order means nothing a developer can see:
on a three-monitor machine it was 1 (right), 2 (left), 3 (primary, centre),
while Windows Settings printed 1 (primary, centre), 2 (right), 3 (left). So a
developer who read a number off Windows Settings got a different monitor, and
nothing said so.

Windows Settings' own numbers cannot be reproduced. QueryDisplayConfig with
QDC_ONLY_ACTIVE_PATHS returns the paths in exactly the AllScreens order, the
\.\DISPLAY<n> device names do not match either, and Microsoft documents no
rule. So count left to right instead: monitor 1 is the leftmost. That follows
the arrangement picture in Windows Settings, which a developer can read, even
though it is not the numbers printed on it.

Also accept "0" as a synonym for "headless". It reads as "no monitor" beside
the numbers that name one, and it is quicker to type.

Bloom now writes the whole mapping to its log on every automation start, so a
developer can see which monitor a number means rather than guess.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/BloomExe/Shell.cs
@hatton

hatton commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 5 from Hatton's machine during preflight]

Consulted Devin again, up to a6bde37887 (the left-to-right monitor numbering and 0). Job pr-review-job-d35a884570334aab8472f80143b85dca, complete.

It raised one non-severe Bug for this commit, "Normal fallback ignores saved placement". That is its own thread on src/BloomExe/Shell.cs, assessed as not an issue and resolved: an automation run must place its window the same way every time, and master did not honour the saved placement under --automation either. Everything else Devin lists is a carried-over finding from an earlier commit that it now marks resolved, plus two informational notes.

Also green at this commit: the pr-automation check, the full C# suite (3338 passed, 0 failed, 13 skipped), and a real-Bloom check that BLOOM_AUTOMATION_MONITOR=2 opens on the primary monitor while 0 opens off every monitor.

Greptile is out of free credits until 2026-09-24, by its own comment, so it has not reviewed this branch.

hatton and others added 3 commits September 3, 2026 08:19
An off-screen window asked for the primary monitor's working-area size, but
Windows gives a window the scale factor of the monitor nearest to it. Out to
the left, the nearest monitor is the leftmost one, whose scale factor is very
likely not the primary's. On a machine with a 150% primary and a 100% monitor
beside it, a window meant to match the primary's 3840x2100 came out 3840x2100
REAL pixels, taller than any monitor on the machine, and the page inside it laid
out at a viewport height of 1990 CSS pixels that no user could ever have.
format-gear-positioning.spec.ts failed on exactly that: it asserts the format
gear sits in the lower half of the view, and the gear landed at 846 of 1990.

The file passes on monitor 2 (150%) and on monitor 3 (100%) and failed only
off-screen, on the same build, in back-to-back runs, which is what isolated it.

So the window now goes straight down from the primary monitor, in line with its
left edge. The primary stays the nearest monitor, so an off-screen window paints
at the size a visible one would. The clearance rule is unchanged in substance:
four window heights below the lowest monitor, plus 1000 pixels, clamped to
32000, for the same reason the old rule left four widths.

This is the second bug from one cause. The first ate all but 27 pixels of a
1000-pixel cushion. Both passed every unit test, because a unit test compares
numbers inside one process's own coordinate space, so AUTOMATION-DEBT.md now
carries an entry asking for the ability to run the suite at a chosen resolution
and scale factor.

The full suite off-screen is back to 23 passed, 2 failed, 10 did not run, the
two failures being the pre-existing pair on BL-16807.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The suite sizes its window to whatever monitor it lands on, so it proves the
layout only at the size of a developer's screen. That hides a class of bugs
that users on inexpensive machines meet and we never do.

The change itself is small, so the entry states the design: 1024x586 (the
working area of a 1024x768 screen) for every run, BLOOM_AUTOMATION_WINDOW_SIZE
to ask for something else, a 400x300 floor, and the same size whatever
BLOOM_AUTOMATION_MONITOR says.

The reason it is debt rather than a change is what it costs. A full run at
1024x586 gave 16 passed, 6 failed and 13 not run, against 23 passed and 2
failed at monitor size; the entry names the four tests the small window breaks
and the triage each one needs. The developer chose to record the plan rather
than carry a red suite, so the code that was written for it is not in the
history and the entry says how to rebuild it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Devin's review of the previous commit is right on the geometry. The comment
claimed that keeping the off-screen window directly under the primary keeps
the primary the nearest monitor, and Windows applies the nearest monitor's
scale factor. That claim holds only while no monitor sits below the primary in
the same band of x. Stack one monitor under another at a different scale and
the lower one is nearest, so the window comes out the wrong size again.

Nobody on the team has such a layout, and getting it right for every layout
means asking Windows for the nearest monitor's scale factor and scaling the
requested size by the ratio, which needs the per-monitor DPI calls the
automation-debt entry is about. So this records the limit in both places
rather than overstating what the code does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
// monitor.
const int farDownWindowsAllows = 32000;
const int largestScaleFactorWindowsAllows = 4;
var lowestY = Screen.AllScreens.Max(screen => screen.Bounds.Bottom);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[Claude Opus 5 from Hatton's machine during preflight]

Devin (non-severe Bug): "Lower monitors restore DPI mismatch." Devin says that placing the
off-screen window below the lowest monitor does not, in general, keep the primary monitor the
nearest one. If a monitor sits below the primary and overlaps it horizontally, that lower monitor
is nearest, Windows applies its scale factor, and the window comes out the wrong size again.

Devin is right on the geometry, and the comment here claimed more than the code delivers. The
primary is nearest only while no monitor sits below the primary in the same band of x.

What this means in practice:

  • On every layout anyone on the team has, the monitors sit side by side, so the primary is nearest
    and the placement is correct. The two real DPI failures this PR fixed both came from going
    sideways, which is wrong on ordinary layouts, not from going down.
  • Fixing it for every layout means asking Windows for the scale factor of whichever monitor ends
    up nearest and scaling the requested size by the ratio. That needs the per-monitor DPI calls
    Microsoft does not document, which is a different piece of work.

So rather than overstate what the code does, 6f2a42d244 says plainly where it stops being true,
in the comment and in src/BloomE2E/AUTOMATION-DEBT.md under "No way to run the suite at a chosen
monitor resolution and scale factor", where the fix has a home.

This one is for the developer to decide, so the thread stays open: take the recorded limit, or
spend the per-monitor DPI work now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Decided to live with this. This business of picking the DPI of a neighboring monitor is completely arbitrary. Anyhow, in the future we will pay more attention to specifying what DPI and scale you want to run at.

@hatton

hatton commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 5 from Hatton's machine during preflight]

Consulted Devin again, up to 6f2a42d244. It is complete for this commit and raised nothing new.

Two Bugs it still lists are from earlier commits in the branch:

  • "Lower monitors restore DPI mismatch" — real, and now recorded rather than fixed. It has its
    own thread: Control over where automation and e2e runs (BL-16804) #8285 (comment) That
    thread stays open, because the choice is the developer's.
  • "Normal fallback ignores saved placement" — assessed as not an issue and resolved on its own
    thread in the previous round.

The other two Bugs and the one Investigate flag are ones Devin now marks resolved. Two
informational notes, not posted.

CI (pr-automation) is green at 6f2a42d244. Greptile is out of free credits until 2026-09-24, so
it will not review this branch.

…wise

The class comment said a developer who leaves the variable set in their
shell still gets an ordinary, visible Bloom when they start one themselves.
That is false: ./go.sh passes --automation (scripts/watchBloomExe.mjs), so
a Bloom started to work in obeys the variable too.

The behaviour is right and stays. It is what the variable is for: the
developer sets it once and every Bloom an agent starts stays off the monitor
they are working on. Only the comment was wrong.

It now also records the one consequence worth knowing, because a reader who
meets it without warning loses an afternoon: "headless" hides a ./go.sh
Bloom the developer started themselves, and a hidden Bloom looks exactly
like one that failed to start. The log line DescribeChoice writes is what
settles that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hatton hatton changed the title Run the e2e suite off-screen (BL-16804) Control over where automation and e2e runs (BL-16804) Sep 3, 2026
@hatton
hatton marked this pull request as ready for review September 3, 2026 16:27
@hatton
hatton merged commit f2de306 into master Sep 3, 2026
1 check passed
@hatton
hatton deleted the BL-16804-headless branch September 3, 2026 16:27
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