vaadin / vaadin/browserless-test
Browserless simulators for browser features (Screen Orientation, Screen Lock etc)
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 1
- Forks
- 3
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 51
Description
Extend browserless trigger/action simulation to Flow's other browser features
Overall idea
The feat/trigger-actions-tester-api branch added browserless-test support for Flow's client-side trigger/action framework (com.vaadin.flow.component.trigger.internal), with Clipboard as the first vertical slice. That framework runs client-only JS (via Element.addJsInitializer) and is therefore invisible to browserless's server-side click()/fireDomEvent; the approach bridges it without a JS engine via flow-server seams (PromiseAction.deliverSuccess/deliverError, CallbackAction.deliver, Action.Input.evaluate, "for browserless simulation" input getters) consumed by a browserless SPI (TriggerSimulation + ActionSimulator + per-UI *Simulator state holders).
Flow ships four more browser features on this same framework — Fullscreen, Web Share, Download, Open-in-new-tab — plus framework-level generics (double-click, CallbackAction/SetSignalAction, the UI.triggerAfter timer, resize) that no simulator covers yet. This issue tracks extending the existing SPI to all of them, reusing the established *Simulator + *ActionSimulators patterns, landed incrementally in phases (Fullscreen+WebShare → Download+OpenInNewTab → framework generics → resize).
Per-feature analysis
| Feature | Simulatable? | flow-server seam needed | What a test can assert |
|---|---|---|---|
| Fullscreen | ✅ outcome + state signal | none (setStateFromClient/exit/stateSignal already public) |
onSuccess/onError ran; stateSignal().peek() |
| Web Share | ✅ outcome + recorded content | ShareAction input getters |
onShared/onError ran; recorded share content; support signal |
| Download | ✅ record URL/filename + capture handler bytes | DownloadAction getters + HandlerBinding.evaluate |
download initiated, resolved URL/filename, served bytes |
| Open in new tab | ✅ record args | OpenInNewTabAction getters + reusable isBlockedUrl |
tab requested, URL, features, javascript: blocked |
| Double-click | ✅ trivially | none (dblclick already matches fire) |
same as click, on dblclick |
| CallbackAction / SetSignalAction | ✅ callback runs / signal set | CallbackAction.deliverFromEvent |
callback side-effect; signal.get() |
| UI.triggerAfter (timer) | ✅ task runs when fired | none beyond deliverFromEvent |
deferred task ran after advanceTimers() |
| Resize (SizeTrigger) | ⚠️ only if wired to a server-effect action | SizeTrigger.EventData.size.evaluate |
callback/signal receives synthetic {width,height} |
| SetPropertyAction | ❌ pure client-side, no server effect | — | skip (unregistered = correct no-op) |
Limitations
- No real browser state. Actual fullscreen DOM, the fullscreen component-move-into-wrapper reparent, the native share sheet, and whether a file was truly saved / a tab truly opened cannot be observed server-side — only the server-initiated effect and the promise outcome.
- Outcome is chosen, not inferred. The user-gesture requirement can't be reproduced (a simulated click always "has" a gesture), and success-vs-
AbortErrorfor Web Share must be selected by the test rather than derived. - Download byte capture works only for the
DownloadHandlerflavour (resolves to anElementStreamResource); a plain-URL /blob:/ external download records the URL string only.Input-derived URLs are observable only when backed by a server-syncedPropertyInput. - Signals start
UNKNOWN. Bootstrap params (v-fs,v-ws) never run in browserless, sostateSignal()/supportSignal()must be seeded via the simulator. - Timer has no elapsed-time semantics — the harness fires it explicitly;
Durationis ignored. - Resize requires the harness to invent width/height and is only meaningful when wired to a server-effect action (
SetPropertyActionhas no server effect). SignalInput-sourced callbacks remain unsimulatable (evaluatenot overridden).
Out of scope (different mechanism, future work)
Flow browser APIs that do not use the trigger/action framework — WebStorage.getItem, WakeLock.request, ScreenOrientation.lock, Page.fetchCurrentURL, ExtendedClientDetails.refresh — would need a different seam (resolving PendingJavaScriptInvocation round-trips or seeding ExtendedClientDetails), and are not planned here.
Browserless simulation for Flow's non-trigger browser features
Overall idea
These are the Flow browser features that do not ride the client-side trigger/action framework (the follow-up set to Clipboard/Fullscreen/WebShare/Download etc.). They reach the browser through three other mechanisms:
- Client JS round-trips —
executeJs(...).then(...)/.toCompletableFuture(...), where the client must post a JSON value back to resolve the callback/future. - Bootstrap-parameter seeding —
ExtendedClientDetailsand several capability/availability signals are populated from bootstrap request params (v-sw,v-tzid,v-pv,v-ww,v-ga, …), which browserless never runs. - Event-push — the server registers a listener/handler that the browser later invokes (focus/blur, window resize, page visibility, history state change, drag & drop).
Key finding: browserless today never resolves pending JS invocations — MockVaadin.clientRoundtrip() drains the UI queue but never dumps the JS queue, so every .then(...)/CompletableFuture on an executeJs silently never fires. The one new primitive this effort needs is a per-UI JavaScriptExecutionSimulator that captures UIInternals.dumpPendingJavaScriptInvocations() and calls complete(json)/completeExceptionally(json) on them, then drains the UI queue. Everything else layers on top of that plus already-public state setters.
Headline: unlike the trigger/action features (which each needed a small flow-server seam), this entire family is buildable on public flow-server API — zero flow-server changes required. All simulators follow the shipped ClipboardSimulator/GeolocationSimulator convention (per-UI, current()/forUI(UI), Serializable), placed in the matching com.vaadin.flow.component.* package to reach package-private members.
Per-feature analysis
| Feature | Simulatable? | Mechanism | flow-server seam |
|---|---|---|---|
| executeJs round-trip (generic) | ✅ | new JavaScriptExecutionSimulator (dump + complete) |
none |
| Page.fetchCurrentURL / fetchPageDirection | ✅ | resolve via the primitive | none |
| WebStorage (getItem + set/remove/clear) | ✅ | in-memory WebStorageSimulator; auto-resolves getItem |
none |
| WakeLock (request / active / availability) | ✅ | resolve round-trip + public UIInternals.setWakeLock* |
none |
| ScreenOrientation (lock / unlock / signal) | ✅ | resolve round-trip + UIInternals.setScreenOrientationFromClient |
none |
| Focus / blur | ⚠️ partial | request = record-only; focus/blur events = synthetic DomEvent |
none |
| Element.scrollIntoView | ⚠️ record-only | match pending-JS expression | none |
| Drag & drop | ✅ | direct state reads + synthetic dragstart/drop/dragend |
none |
| ExtendedClientDetails (all fields) | ✅ | seed via public updateFromValues |
none |
| Env signals (visibility, window size, availability) | ✅ | seed via updateFromValues + synthetic resize/visibility events |
none |
History (reverse: HistoryStateChangeEvent) |
✅ | invoke registered handler directly | none |
| History (outbound: pushState/back/forward/go) | ⚠️ low-value / fragile | match pending-JS expression | none |
| Page nav (open / reload / setLocation) | ✅ (mostly already) | existing MockPage recording (openUnsafe is a gap) |
none |
Limitations
- No JS actually runs. Round-trip return values are synthesized by the test; expressions whose later steps depend on real side effects aren't modeled.
- Round-trip and signal state are independent channels. For WakeLock/ScreenOrientation a "grant" resolves the promise, but the
active/orientationsignal must be moved separately (mirrors production, where a DOM event — not the promise — flips the signal). focus()/blur()/scrollIntoView/outboundHistoryhave no server outcome — a test can only assert they were requested, by matching pending-JS expression strings (brittle; thepushStatetemplate even differs between React and legacy branches). Real focus-success is unobservable. The reverse-direction APIs (fire focus/blur events; fireHistoryStateChangeEvent) are the real deliverables.- Drag & drop couples to Flow-internal
@EventDataexpression strings (no public constants likeClipboard.PASTE_TEXT_EXPR) — the main fragility; browsereffectAllowed/dropEffectgating that would allow/prevent a drop is not enforced (a simulated drop always fires). - WebStorage is per-UI, not per-origin/tab — cross-UI
localStoragesharing isn't modeled; theCompletableFutureform requires resolve-then-get ordering (the deadlock detector trips otherwise). ExtendedClientDetails.refresh()won't fire in browserless (no browser to answer) — seed first soretrieveExtendedClientDetailstakes its synchronous branch; timezone-offset/devicePixelRatio encoding quirks must be hidden behind a typed builder.
One design decision to make
Auto vs. explicit pending-JS resolution. For zero-boilerplate WebStorage, capture+auto-resolve at the client boundary (one line in MockVaadin.clientRoundtrip() — the only Kotlin touch, and it's behavior, not new installed state). Alternatively keep it fully explicit (test calls flush()/pending()); no Kotlin change. Recommendation: do the one-line auto-capture and also expose the explicit API.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the existing ClipboardSimulator/GeolocationSimulator convention, then trace TriggerSimulation, ActionSimulator, MockVaadin.clientRoundtrip(), and UIInternals.dumpPendingJavaScriptInvocations(). Break the work into the listed feature phases, adding per-UI simulators and coverage for the stated observable outcomes while preserving the documented limitations and deciding whether pending JavaScript resolution is automatic, explicit, or both.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java, kotlin
- Domain
- devtools, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100