Skip to content

feat: PromiseAction + CopyTextToClipboardAction + RequestFullscreenAction#24394

Merged
mshabarov merged 10 commits into
mainfrom
feature/trigger-step2
May 22, 2026
Merged

feat: PromiseAction + CopyTextToClipboardAction + RequestFullscreenAction#24394
mshabarov merged 10 commits into
mainfrom
feature/trigger-step2

Conversation

@Artur-
Copy link
Copy Markdown
Member

@Artur- Artur- commented May 21, 2026

Adds a PromiseAction base class in the trigger framework and two concrete subclasses — CopyTextToClipboardAction and RequestFullscreenAction — on top of the executeJs-based trigger framework introduced in step 1.

PromiseAction is the generic primitive: many gesture-bound browser APIs are asynchronous (clipboard, fullscreen, file picker, share, payments, …) and follow the same shape — call the API, then handle the resolved value or the rejection. Subclasses override appendPromiseExpression(JsBuilder, StringBuilder) to emit the promise-yielding JS; the base wires the outcome reporting.

Two construction modes, mirroring the Geolocation API:

  • No-arg super() — fire-and-forget; the rendered JS is just the promise expression and the server never sees the outcome.
  • super(onSuccess, onError)onSuccess runs after the promise resolves; onError receives the browser's error message after the promise rejects. Both run on the UI thread; both are required (pass () -> {} or err -> {} to opt out of one, matching the Geolocation pattern).

The with-outcome mode lazily registers one
ReturnChannelRegistration per trigger host node and appends .then(()=>$N(true,null)).catch(e=>$N(false, msg)) to the promise expression, so the client invokes the channel after the promise resolves or rejects. JsBuilder.capture(Object) is the new package-private hook that lets actions allocate a $N placeholder for non-Element captures (the return channel here, which materialises client-side as a callable JS function).

Concrete subclasses:

  • CopyTextToClipboardAction — supplies navigator.clipboard.writeText(textExpr) as the promise expression. Takes an Action.Input<String> for the text. Named verb-first so parallels like CopyImageToClipboardAction and PasteFromClipboardAction read naturally.
  • RequestFullscreenAction — supplies <target>.requestFullscreen() as the promise expression. Takes a target Component whose root element will be fullscreened.

Both ITs (TriggerCopyTextToClipboardView/IT,
TriggerRequestFullscreenView/IT) shim the underlying browser API to resolving/rejecting promises and assert the server-side status div updates accordingly — avoiding dependence on browser clipboard / fullscreen permissions in CI.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 21, 2026

Test Results

 1 418 files  + 5   1 418 suites  +5   1h 19m 20s ⏱️ - 4m 37s
10 000 tests +18   9 931 ✅ +18  69 💤 ±0  0 ❌ ±0 
10 475 runs  +18  10 404 ✅ +18  71 💤 ±0  0 ❌ ±0 

Results for commit a38364e. ± Comparison against base commit 9b240a3.

♻️ This comment has been updated with latest results.

…tion

Adds a `PromiseAction` base class in the trigger framework and two
concrete subclasses — `CopyTextToClipboardAction` and
`RequestFullscreenAction` — on top of the executeJs-based trigger
framework introduced in step 1.

`PromiseAction` is the generic primitive: many gesture-bound browser
APIs are asynchronous (clipboard, fullscreen, file picker, share,
payments, …) and follow the same shape — call the API, then handle the
resolved value or the rejection. Subclasses override
`appendPromiseExpression(JsBuilder, StringBuilder)` to emit the
promise-yielding JS; the base wires the outcome reporting.

Two construction modes, mirroring the Geolocation API:

- No-arg `super()` — fire-and-forget; the rendered JS is just the
  promise expression and the server never sees the outcome.
- `super(onSuccess, onError)` — `onSuccess` runs after the promise
  resolves; `onError` receives the browser's error message after the
  promise rejects. Both run on the UI thread; both are required (pass
  `() -> {}` or `err -> {}` to opt out of one, matching the
  Geolocation pattern).

The with-outcome mode lazily registers one
`ReturnChannelRegistration` per trigger host node and appends
`.then(()=>$N(true,null)).catch(e=>$N(false, msg))` to the promise
expression, so the client invokes the channel after the promise
resolves or rejects. `JsBuilder.capture(Object)` is the new
package-private hook that lets actions allocate a `$N` placeholder for
non-Element captures (the return channel here, which materialises
client-side as a callable JS function).

Concrete subclasses:

- `CopyTextToClipboardAction` — supplies
  `navigator.clipboard.writeText(textExpr)` as the promise expression.
  Takes an `Action.Input<String>` for the text. Named verb-first so
  parallels like `CopyImageToClipboardAction` and
  `PasteFromClipboardAction` read naturally.
- `RequestFullscreenAction` — supplies `<target>.requestFullscreen()`
  as the promise expression. Takes a target `Component` whose root
  element will be fullscreened.

Both ITs (`TriggerCopyTextToClipboardView`/`IT`,
`TriggerRequestFullscreenView`/`IT`) shim the underlying browser API
to resolving/rejecting promises and assert the server-side status div
updates accordingly — avoiding dependence on browser clipboard /
fullscreen permissions in CI.
@mshabarov mshabarov self-requested a review May 21, 2026 10:47
@Artur- Artur- force-pushed the feature/trigger-step2 branch 2 times, most recently from 1dc9334 to db61e7a Compare May 21, 2026 10:55
Artur- added 5 commits May 21, 2026 14:17
`PromiseAction.appendStatement` used to assemble the `.then`/`.catch`
glue by chained `StringBuilder.append` calls, with the channel
receiving positional `(boolean, message)` args decoded by index in
`dispatch`. Two smells in one place: structural JS hidden in string
concatenation, and an implicit positional wire contract.

Replace both:

- A static `JsFunction OBSERVE_PROMISE` carries the wrapper body once,
  with named runtime arguments `(promise, channel)`. Subclasses are
  unchanged; `appendStatement` now allocates two captures (observer +
  channel), then composes one call: `$observe($promiseExpr, $channel)`.
  No more `.append(".then(()=>")…` chains; the handler body is one
  line, the JS literal lives in one place.

- Introduce a private `record Outcome(boolean ok, @nullable String
  error)` as the wire shape. The OBSERVE_PROMISE body pushes
  `{ok: true}` / `{ok: false, error: …}`; `dispatch` reads it via
  `JacksonUtils.readValue` rather than positional `args.get(0)` /
  `args.get(1)` decoding. Renaming or adding a field can no longer
  silently desynchronize the two sides.

Test assertions follow: the handler body is now `$0(<promise>, $1);`
across `PromiseActionTest`, `CopyTextToClipboardActionTest`, and
`RequestFullscreenActionTest`; the channel-invocation tests pass an
`Outcome`-shaped `ObjectNode` instead of positional booleans/strings.

`OBSERVE_PROMISE` is a `static final` shared instance — `JsFunction`
is `Serializable` and immutable, and each `addJsInitializer`
registration still gets its own capture entry, so there's no
cross-UI state and no leak. 19/19 trigger-internal tests pass.
`LiteralInput` was package-private and accepted `@Nullable T`. Both
were carry-overs from its only original user, `SetPropertyAction`'s
null-clearing convenience constructor, but neither survives as
LiteralInput becomes a building block for other actions:

- Public so callers can wire a fixed value through any action that
  takes an `Action.Input<? extends T>` — e.g. copying a constant
  string with `new CopyTextToClipboardAction(new LiteralInput<>("…"))`.

- Non-null because `null` as a literal payload almost never matches
  a sensible browser API. `writeText(null)` writes the string
  `"null"` to the clipboard; `target.requestFullscreen.call(null)`
  is meaningless. Forbidding it at the `LiteralInput` constructor
  catches the foot-gun at compile time (via NullAway) wherever the
  type permits, and at runtime via `Objects.requireNonNull`
  otherwise.

`SetPropertyAction`'s null-accepting convenience constructor stays —
clearing a property by assigning `null` is a real use case. It now
routes the null branch through a private `NULL_LITERAL` singleton
that emits the JS literal `null`, instead of leaking nullability
into `LiteralInput`.

Adds a `fireAndForget_literalInput_encodesValueAsJsonLiteral` test
in `CopyTextToClipboardActionTest` that copies `hello "world"\n` and
asserts the quotes and newline are JSON-escaped — proves callers
can't break out of the JS string by passing values with quotes or
newlines.
The view now has two buttons — `#copy` ("Copy input value") wired to a
PropertyInput on the input field, and `#copy-static` ("Copy static
text") wired to a LiteralInput carrying `hello "world"\n`. The literal
deliberately includes a quote and a newline so the IT verifies the
value the browser ends up with is exactly what Java passed in, i.e.
the JSON encoding done at build time round-trips without losing or
mangling characters.

Button labels updated to describe what each button copies.
Copy link
Copy Markdown
Contributor

@mshabarov mshabarov left a comment

Choose a reason for hiding this comment

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

API and implementation looks good to me overall - clear purposes for added classes, nice classes hierarchy. I left several comments with some of them not being 100% changes requests, so ready to merge even as is with a meaningful elaborations of why it's implemented this way.

Addresses the inline review comments on PR #24394:

PromiseAction
- Drop the Geolocation cross-link from the javadoc — it was a stale
  inline reference, not load-bearing API documentation.
- Replace `onSuccess: Runnable` / `onError: Consumer<String>` with
  typed records: `onSuccess: Consumer<Success>` carrying the resolved
  value (when the subclass' promise produced one) and
  `onError: Consumer<Error>` carrying the rejection's name and
  message. The error name (typically a `DOMException` class like
  "NotAllowedError") is what callers usually want to switch on.
- Update the wire shape accordingly: `{ok, value, error: {name,
  message}}`. The dispatch now decodes one `Outcome` record via
  Jackson and constructs `Success`/`Error` from it.
- Tighten the fire-and-forget detection to check both callbacks
  (defensive against future constructor changes that might break the
  both-null-or-both-non-null invariant).
- Add a javadoc note explaining `final` on `appendStatement` —
  subclasses customise via `appendPromiseExpression`, not by
  overriding the wiring; that's what keeps the `Outcome` wire
  contract stable.

CopyTextToClipboardAction
- Constructor takes `Consumer<String> onCopied` (typed) and adapts to
  PromiseAction's `Consumer<Success>` internally. The promise
  expression is wrapped in an IIFE
  `((v) => writeText(v).then(() => v))(<textExpr>)` so the resolved
  value is the copied string — the server sees what actually reached
  the clipboard even when the input was a client-side `PropertyInput`.

RequestFullscreenAction
- Clarify the "low-level" javadoc note: explicitly point at PR #24326
  (`Component.requestFullscreen()`) as the higher-level facade that
  wraps the element for Vaadin theming/overlays; this action is the
  trigger-framework primitive it builds on.
- Constructor stays `Runnable onSuccess` (no meaningful value to
  deliver) but `Consumer<Error>` for the error path.

Tests
- PromiseActionTest splits the channel-invocation cases into
  "value present", "value missing", and "error with name+message".
- CopyTextToClipboardActionTest gains an `onCopied_receivesTheString`
  test and updates the rendered-JS assertions to the IIFE wrapper.
- ITs use the new typed callbacks; the rejecting shims now throw
  `new DOMException('DeniedByTest', 'NotAllowedError')` so the IT
  can assert both `err.name()` and `err.message()` reach the server.
Artur- added 3 commits May 22, 2026 08:28
mshabarov pointed out that AbortError isn't a spec-documented
rejection for the clipboard / fullscreen APIs the current
PromiseAction subclasses wrap — only NotAllowedError is. The
speculative `AbortError, …` example overstated what callers should
expect. Remove it from both `PromiseAction.Error`'s javadoc and the
`RequestFullscreenAction` class-level note; leave the documented
NotAllowedError as the concrete example.
`STATIC_TEXT` ends with `\n`. Selenium's `WebElement.getText()`
normalises and trims surrounding whitespace, so the status
assertion never matched in CI even though the
`navigator.clipboard.writeText` round-trip check did. Switch to
`document.getElementById('status').textContent` via
`executeScript`, which preserves the trailing newline.

Other IT cases are unaffected — they compare strings without
boundary whitespace.
@mshabarov mshabarov enabled auto-merge May 22, 2026 09:15
@sonarqubecloud
Copy link
Copy Markdown

@mshabarov mshabarov added this pull request to the merge queue May 22, 2026
Merged via the queue into main with commit ca5bac6 May 22, 2026
30 of 31 checks passed
@mshabarov mshabarov deleted the feature/trigger-step2 branch May 22, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants