You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implements the same two-PR pattern used for Geolocation (#24259): port extraction in flow, plus an in-memory client and simulator in vaadin/browserless-test. Addresses the three explicit affordances from PRD vaadin/platform#8759§Browserless testing.
Goals
From PRD 8759 §Browserless testing:
Simulate the result of a clipboard write (success / failure) and verify onSuccess / onError callbacks.
Simulate an incoming paste event with configurable text, HTML, and file bytes; verify paste / paste-start / paste-failed listeners and the upload-handler bytes path.
Query and override availability in tests.
Non-goals: changing the public Clipboard static facade, replicating browser-side click-trusted-event guarantees in tests, or shipping a META-INF/services factory from flow itself (it lives in browserless-test, per the Geolocation precedent in #24259).
Why the Geolocation Pattern Doesn't Apply 1:1
Concern
Geolocation
Clipboard
Decision
Facade shape
Geolocation instance per UI
Clipboard static final utility
Keep static facade; static methods resolve a per-UI ClipboardClient via UIInternals
Lifetime
One tracker / one position request at a time
N copy registrations, N paste/copy/cut listeners, possibly stacked on the same element
Client owns a registry keyed by element + registration ID
Bytes path
None
Pasted files run through UploadHandler over a real HTTP fetch
Browserless client must invoke UploadHandler synthetically without HTTP
Output channel
executeJs + DOM events
Same + DOM property (__clipboardText) + ReturnChannelRegistration (success/error)
Port must expose the return channels so tests can drive them
Reuse UIInternals.setClipboardAvailability(...); drop "framework internal" from its Javadoc or wrap in a public seam
Changes in vaadin/flow
Two PRs, mirroring the Geolocation split. PR-A is the port extraction; PR-B is the Lookup-based factory SPI. Skipping PR-B and going straight to Lookup also works — the only reason Geolocation needed two PRs was classloader fallout from the package-private seam, which we can avoid up front.
3.1 New SPI Types (com.vaadin.flow.component.clipboard)
ClipboardClient — public interface, port for all wire operations.
CopyRegistration / PasteRegistration are the public inspection types — equivalent to Geolocation's GeolocationTrackerSession / GeolocationRequest.
BrowserClipboardClient — package-private default implementation. Lifts the current executeJs / DOM-event / ReturnChannelMap code out of Clipboard.java and moves it behind the interface. Production wire behavior unchanged; Clipboard ends up as a thin facade.
ClipboardClientFactory — public SPI, resolved via com.vaadin.flow.di.Lookup.
Resolution mirrors GeolocationClientFactory (#24259): VaadinService.getCurrent().getContext().getAttribute(Lookup.class).lookup(ClipboardClientFactory.class), falling back to BrowserClipboardClient when none is registered. Flow ships no META-INF/services file — production uses the fallback.
3.2 Clipboard Facade Refactor
Rewrite the static methods to delegate, keeping the public signatures byte-for-byte stable. Each static method becomes:
publicstaticClipboardCopycopyOnClick(Componenttrigger, Stringtext) {
Objects.requireNonNull(trigger, ...);
ClipboardClientclient = client(trigger); // resolve via UI internalsCopyRegistrationreg = client.registerCopyText(
trigger.getElement(), text != null ? text : "", null, null);
returnnewClipboardCopy(reg);
}
ClipboardCopy.setValue(...) and remove() delegate to the CopyRegistration. The current __clipboardText property and Element.setAttribute("__clipboard-paste-upload", uploadHandler) plumbing becomes a BrowserClipboardClient implementation detail and disappears from the public surface.
Side effect: fixes the two registration-collision bugs from the earlier review — per-registration state now lives in the client's registry, not on the element.
3.3 UIInternals Changes
The clipboardAvailabilitySignal stays in place. Add:
publicClipboardClientgetClipboardClient(); // lazy-resolves via Lookup, caches per UI
Drop "For framework use only" from setClipboardAvailability(ClipboardAvailability)'s Javadoc — it's the public override path for tests, matching PRD affordance #3.
3.4 In-JAR Tests
New ClipboardClientSeamTest (parallel to GeolocationClientSeamTest) covering:
The Clipboard facade resolves the client via Lookup.
A registered ClipboardClientFactory overrides the default.
Without a factory the fallback is BrowserClipboardClient.
CopyRegistration.updateText(...) propagates to ClipboardCopy.setValue(...).
Existing ClipboardTest keeps its wire-protocol assertions to pin production behavior. The Clipboard.PasteState inner class becomes a BrowserClipboardClient implementation detail (still package-private, still unit-tested for its dispatch order).
Changes in vaadin/browserless-test
New package com.vaadin.flow.component.clipboard under shared/.
4.1 BrowserlessClipboardClient (package-private)
Implements ClipboardClient with no DOM at all. Maintains:
Map<RegistrationId, CopyHandle> for active copy registrations, each carrying: trigger element ref, current text or live source ref, success/error commands.
Map<Element, PasteHandle> for paste registrations, each carrying: upload handler, the three listener callbacks.
Map<Element, List<SerializableConsumer<ClipboardEvent>>> for copy/cut listeners.
Latest ClipboardAvailability (mirror of UIInternals signal).
Files arrive as in-memory byte[]s. The client invokes UploadHandler.handleUploadRequest with a synthetic UploadEvent backed by a ByteArrayInputStream and a stub VaadinRequest/VaadinResponse. If wiring a full UploadEvent is too brittle, fall back to special-casing InMemoryUploadHandler and TemporaryFileUploadHandler — both are public and easily fed via reflection-free pathways. See open question §6.
PasteBuilder.simulate() runs the same dispatch order the real client guarantees: fire paste-start synchronously → run each file through the upload handler (or emit paste-failed for failFile entries) → fire paste-complete. This exercises both the listener wiring and the PasteState succeeded-count contract.
4.4 Tests
ClipboardSimulatorTest — direct controller behavior: handle inspection, availability override, paste with text/HTML/files, failed-file routing, simulate-on-empty (no registrations).
ClipboardFacadeIntegrationTest — exercises through the real Clipboard static facade:
Clipboard.copyOnClick(btn, "x") → simulator.simulateCopySuccess(btn) triggers nothing (no callback); simulator.copyHandle(btn).currentText() equals "x"; ClipboardCopy.setValue("y") updates the inspection.
With success callback: simulateCopySuccess invokes the Command.
copyOnClick(btn, source): simulateCopyClick(btn) reads source value live (mirrors readSourceText semantics) and fires success.
addPasteListener(target, listener) + simulator.pasteInto(target).text("hi").file("a.png", "image/png", bytes).simulate() → listener receives ClipboardEvent with one ClipboardFile.
Pluggable upload handler variant: handler receives bytes; ClipboardEvent.getFiles() is empty per current contract.
addPasteStartListener + addPasteFailedListener fire in the correct order.
addCopyListener / addCutListener driven via simulator.simulateCopy(...) / simulator.simulateCut(...).
Target: every PRD requirement checkbox testable purely from JUnit.
Recommended PR Sequence
flow PR-A — extract ClipboardClient port, ship BrowserClipboardClient as default, refactor Clipboard facade to delegate. No behavior change. Fixes the two registration-collision bugs as a side effect.
flow PR-B — add ClipboardClientFactory + Lookup resolution. Can be merged with PR-A if reviewers prefer one PR; Geolocation only split because of classloader fallout discovered late.
browserless-test PR — BrowserlessClipboardClient, BrowserlessClipboardClientFactory, ClipboardSimulator, tests. Depends on a 25.x-SNAPSHOT carrying PR-A+B.
Open Questions
UploadEvent synthesis. Constructing a full UploadEvent outside an HTTP request needs prototyping. The cleanest fallback is a package-private hook on InMemoryUploadHandler / TemporaryFileUploadHandler that bypasses the request plumbing (mirrors how Geolocation.setClient stays package-private). Worth a spike before committing to the simulator's file(...) shape.
Per-element registration stacking. Today, two copyOnClicks on the same trigger silently overwrite each other (review item fix: Make ClassGraph discover package-private @Route views #1). The port refactor lets us either reject the second call with IllegalStateException or maintain a stack. The simulator API assumes one handle per trigger; if we keep stack semantics, copyHandle(Component) returns the active head and activeCopyHandles() enumerates the stack. Decision belongs to PR-A.
Clipboard.read* server-initiated methods. The current PR description advertises writeText / readText / writeImage but they don't exist in the code. If added, the port grows by three methods and the simulator gains corresponding simulateReadText(...) / etc. drivers. Out of scope for this spec; flagging for the API author.
Lookup vs. UIInternals storage for the client. Geolocation lazy-creates the client in the Geolocation constructor and caches on the instance. The static Clipboard facade has no instance, so the cache lives on UIInternals. Resolution must happen at first use, not at UI construction, so factories registered after UI init still take effect (matches Geolocation's behavior).
Implements the same two-PR pattern used for Geolocation (#24259): port extraction in flow, plus an in-memory client and simulator in
vaadin/browserless-test. Addresses the three explicit affordances from PRD vaadin/platform#8759 §Browserless testing.Goals
From PRD 8759 §Browserless testing:
onSuccess/onErrorcallbacks.paste/paste-start/paste-failedlisteners and the upload-handler bytes path.Non-goals: changing the public
Clipboardstatic facade, replicating browser-side click-trusted-event guarantees in tests, or shipping aMETA-INF/servicesfactory from flow itself (it lives inbrowserless-test, per the Geolocation precedent in #24259).Why the Geolocation Pattern Doesn't Apply 1:1
Geolocationinstance per UIClipboardstatic final utilityClipboardClientviaUIInternalsUploadHandlerover a real HTTP fetchUploadHandlersynthetically without HTTPexecuteJs+ DOM events__clipboardText) +ReturnChannelRegistration(success/error)UNSUPPORTEDsourcev-cabrowser-details param already wired (ExtendedClientDetails:512-520)UIInternals.setClipboardAvailability(...); drop "framework internal" from its Javadoc or wrap in a public seamChanges in
vaadin/flowTwo PRs, mirroring the Geolocation split. PR-A is the port extraction; PR-B is the Lookup-based factory SPI. Skipping PR-B and going straight to Lookup also works — the only reason Geolocation needed two PRs was classloader fallout from the package-private seam, which we can avoid up front.
3.1 New SPI Types (
com.vaadin.flow.component.clipboard)ClipboardClient— public interface, port for all wire operations.CopyRegistration/PasteRegistrationare the public inspection types — equivalent to Geolocation'sGeolocationTrackerSession/GeolocationRequest.BrowserClipboardClient— package-private default implementation. Lifts the currentexecuteJs/ DOM-event /ReturnChannelMapcode out ofClipboard.javaand moves it behind the interface. Production wire behavior unchanged;Clipboardends up as a thin facade.ClipboardClientFactory— public SPI, resolved viacom.vaadin.flow.di.Lookup.Resolution mirrors
GeolocationClientFactory(#24259):VaadinService.getCurrent().getContext().getAttribute(Lookup.class).lookup(ClipboardClientFactory.class), falling back toBrowserClipboardClientwhen none is registered. Flow ships noMETA-INF/servicesfile — production uses the fallback.3.2
ClipboardFacade RefactorRewrite the static methods to delegate, keeping the public signatures byte-for-byte stable. Each static method becomes:
ClipboardCopy.setValue(...)andremove()delegate to theCopyRegistration. The current__clipboardTextproperty andElement.setAttribute("__clipboard-paste-upload", uploadHandler)plumbing becomes aBrowserClipboardClientimplementation detail and disappears from the public surface.3.3
UIInternalsChangesThe
clipboardAvailabilitySignalstays in place. Add:Drop "For framework use only" from
setClipboardAvailability(ClipboardAvailability)'s Javadoc — it's the public override path for tests, matching PRD affordance #3.3.4 In-JAR Tests
New
ClipboardClientSeamTest(parallel toGeolocationClientSeamTest) covering:Clipboardfacade resolves the client via Lookup.ClipboardClientFactoryoverrides the default.BrowserClipboardClient.CopyRegistration.updateText(...)propagates toClipboardCopy.setValue(...).Existing
ClipboardTestkeeps its wire-protocol assertions to pin production behavior. TheClipboard.PasteStateinner class becomes aBrowserClipboardClientimplementation detail (still package-private, still unit-tested for its dispatch order).Changes in
vaadin/browserless-testNew package
com.vaadin.flow.component.clipboardundershared/.4.1
BrowserlessClipboardClient(package-private)Implements
ClipboardClientwith no DOM at all. Maintains:Map<RegistrationId, CopyHandle>for active copy registrations, each carrying: trigger element ref, current text or live source ref, success/error commands.Map<Element, PasteHandle>for paste registrations, each carrying: upload handler, the three listener callbacks.Map<Element, List<SerializableConsumer<ClipboardEvent>>>for copy/cut listeners.ClipboardAvailability(mirror ofUIInternalssignal).Files arrive as in-memory
byte[]s. The client invokesUploadHandler.handleUploadRequestwith a syntheticUploadEventbacked by aByteArrayInputStreamand a stubVaadinRequest/VaadinResponse. If wiring a fullUploadEventis too brittle, fall back to special-casingInMemoryUploadHandlerandTemporaryFileUploadHandler— both are public and easily fed via reflection-free pathways. See open question §6.4.2
BrowserlessClipboardClientFactoryRegistered via
META-INF/services/com.vaadin.flow.component.clipboard.ClipboardClientFactory.4.3
ClipboardSimulator(public test API)Idempotent
forUi(UI); one instance per UI, stored viaComponentUtil.setData(...), same pattern asGeolocationTestController.Inspection types:
PasteBuilder.simulate()runs the same dispatch order the real client guarantees: fire paste-start synchronously → run each file through the upload handler (or emit paste-failed forfailFileentries) → fire paste-complete. This exercises both the listener wiring and thePasteStatesucceeded-count contract.4.4 Tests
ClipboardSimulatorTest— direct controller behavior: handle inspection, availability override, paste with text/HTML/files, failed-file routing, simulate-on-empty (no registrations).ClipboardFacadeIntegrationTest— exercises through the realClipboardstatic facade:Clipboard.copyOnClick(btn, "x")→simulator.simulateCopySuccess(btn)triggers nothing (no callback);simulator.copyHandle(btn).currentText()equals"x";ClipboardCopy.setValue("y")updates the inspection.simulateCopySuccessinvokes theCommand.copyOnClick(btn, source):simulateCopyClick(btn)reads source value live (mirrorsreadSourceTextsemantics) and fires success.addPasteListener(target, listener)+simulator.pasteInto(target).text("hi").file("a.png", "image/png", bytes).simulate()→ listener receivesClipboardEventwith oneClipboardFile.ClipboardEvent.getFiles()is empty per current contract.addPasteStartListener+addPasteFailedListenerfire in the correct order.addCopyListener/addCutListenerdriven viasimulator.simulateCopy(...)/simulator.simulateCut(...).Clipboard.availabilityHintSignal().peek()reflectssimulator.setAvailability(UNSUPPORTED).Target: every PRD requirement checkbox testable purely from JUnit.
Recommended PR Sequence
ClipboardClientport, shipBrowserClipboardClientas default, refactorClipboardfacade to delegate. No behavior change. Fixes the two registration-collision bugs as a side effect.ClipboardClientFactory+ Lookup resolution. Can be merged with PR-A if reviewers prefer one PR; Geolocation only split because of classloader fallout discovered late.BrowserlessClipboardClient,BrowserlessClipboardClientFactory,ClipboardSimulator, tests. Depends on a25.x-SNAPSHOTcarrying PR-A+B.Open Questions
UploadEventsynthesis. Constructing a fullUploadEventoutside an HTTP request needs prototyping. The cleanest fallback is a package-private hook onInMemoryUploadHandler/TemporaryFileUploadHandlerthat bypasses the request plumbing (mirrors howGeolocation.setClientstays package-private). Worth a spike before committing to the simulator'sfile(...)shape.Per-element registration stacking. Today, two
copyOnClicks on the same trigger silently overwrite each other (review item fix: Make ClassGraph discover package-private @Route views #1). The port refactor lets us either reject the second call withIllegalStateExceptionor maintain a stack. The simulator API assumes one handle per trigger; if we keep stack semantics,copyHandle(Component)returns the active head andactiveCopyHandles()enumerates the stack. Decision belongs to PR-A.Clipboard.read*server-initiated methods. The current PR description advertiseswriteText/readText/writeImagebut they don't exist in the code. If added, the port grows by three methods and the simulator gains correspondingsimulateReadText(...)/ etc. drivers. Out of scope for this spec; flagging for the API author.Lookup vs.
UIInternalsstorage for the client. Geolocation lazy-creates the client in theGeolocationconstructor and caches on the instance. The staticClipboardfacade has no instance, so the cache lives onUIInternals. Resolution must happen at first use, not at UI construction, so factories registered after UI init still take effect (matches Geolocation's behavior).