Skip to content

Commit f50e368

Browse files
committed
docs: explain how shared signal writes are confirmed in tests
A write to a shared signal is applied optimistically and is visible through peek() right away, but the SignalOperation it returns completes only after the queued confirmation task has been run by runPendingSignalsTasks(). Document that, including why blocking on operation.result().get(...) before draining the queue always times out. Refs vaadin/browserless-test#202
1 parent 2d32a3f commit f50e368

3 files changed

Lines changed: 82 additions & 0 deletions

File tree

articles/flow/testing/browserless/multi-user.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ Assertions.assertEquals("Hello!", w2.findParagraph().getText());
204204

205205
[methodname]`runPendingSignalsTasks()` waits up to 100 milliseconds for the first pending task to arrive and then drains the queue; the [methodname]`runPendingSignalsTasks(long, TimeUnit)` overload accepts a custom wait time. The method returns [code]`true` if any tasks were processed. If the calling thread holds the window's [classname]`VaadinSession` lock, the lock is temporarily released during the wait so that background threads can enqueue tasks.
206206

207+
The same call also confirms shared-signal writes made through the window: the [classname]`SignalOperation` returned by a write completes only after the queue has been drained. Write the signal while the window's thread-locals are active -- either from within a DSL call or after [methodname]`window.activate()` -- so that the confirmation is dispatched through the window's UI. See <<testing-signals#shared-signal-writes, Confirming a Write>> for details.
208+
207209

208210
== Authenticated Users with Spring Security
209211

articles/flow/testing/browserless/testing-signals.adoc

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,85 @@ The same method is available on the JUnit extension as [methodname]`ext.runPendi
196196
As a result, a change that an observer should react to needs the same treatment as any other off-thread mutation. After triggering the change, call [methodname]`runPendingSignalsTasks()` before asserting on the observing side. A change made and observed on the same test thread -- such as mutating a shared signal and asserting a binding on the same view -- still propagates synchronously and needs no flush. For tests that drive several sessions or windows observing one shared signal, see <<multi-user#signals, Signals in Multi-User Tests>>.
197197

198198

199+
[#shared-signal-writes]
200+
==== Confirming a Write
201+
202+
A write to a shared signal returns a <<{articles}/flow/ui-state/transactions#operation-results, [classname]`SignalOperation`>> that completes once the underlying signal tree has confirmed the command. The write itself is applied optimistically, so the new value is visible through [methodname]`peek()` as soon as the call returns -- while the confirmation travels through the same queue as the effects.
203+
204+
This view inserts a ticket into a [classname]`SharedListSignal` and updates a status label when the write is confirmed. The result callback is delivered in the context that started the operation, so it can touch components directly:
205+
206+
[source,java]
207+
----
208+
@Route("tickets")
209+
public class TicketView extends Div {
210+
final SharedListSignal<String> tickets =
211+
new SharedListSignal<>(String.class);
212+
final TextField title = new TextField("Title");
213+
final Span status = new Span();
214+
final NativeButton submit = new NativeButton("Submit");
215+
216+
public TicketView() {
217+
submit.addClickListener(e -> submitTicket(title.getValue()));
218+
add(title, submit, status);
219+
}
220+
221+
InsertOperation<SharedValueSignal<String>> submitTicket(String title) {
222+
status.setText("Saving...");
223+
224+
var operation = tickets.insertLast(title);
225+
operation.result().thenAccept(result -> status.setText(
226+
result.successful() ? "Ticket created" : "Save failed"));
227+
return operation;
228+
}
229+
}
230+
----
231+
232+
The entry is in the list right after the click, but the status label still reads [code]`Saving...` -- the callback runs only once the queued confirmation task has been executed:
233+
234+
[source,java]
235+
----
236+
@ViewPackages(classes = TicketView.class)
237+
class TicketViewTest extends BrowserlessTest {
238+
239+
@Test
240+
void submitTicket_statusUpdatesWhenWriteIsConfirmed() {
241+
var view = navigate(TicketView.class);
242+
test(view.title).setValue("Printer is jammed");
243+
244+
test(view.submit).click();
245+
246+
// Inserted optimistically, but not confirmed yet.
247+
Assertions.assertEquals(1, view.tickets.peek().size());
248+
Assertions.assertEquals("Saving...", test(view.status).getText());
249+
250+
runPendingSignalsTasks();
251+
252+
Assertions.assertEquals("Ticket created", test(view.status).getText());
253+
}
254+
}
255+
----
256+
257+
A test that gets hold of the operation itself -- because the code under test returns it, as [methodname]`submitTicket()` does -- can assert on the confirmation directly instead of going through the UI:
258+
259+
[source,java]
260+
----
261+
@Test
262+
void submitTicket_operationConfirmedAfterDrainingQueue() {
263+
var view = navigate(TicketView.class);
264+
265+
var operation = view.submitTicket("Printer is jammed");
266+
Assertions.assertFalse(operation.result().isDone());
267+
268+
runPendingSignalsTasks();
269+
270+
Assertions.assertTrue(operation.result().join().successful());
271+
}
272+
----
273+
274+
[WARNING]
275+
Don't block on the operation before draining the queue. A call such as [code]`operation.result().get(5, TimeUnit.SECONDS)` always times out, because the confirmation task can only run on the very thread that's blocked waiting for it. A timeout there means the queue hasn't been drained -- not that the write was lost.
276+
277+
199278
.Quick Start and Collaborative Scenarios
200279
[TIP]
201280
For a brief introduction to testing signal-based views, see <<getting-started#signals, Testing Signal-Based UIs>>. For collaborative features where several windows or users observe the same shared signal, see <<multi-user#signals, Signals in Multi-User Tests>>.

articles/flow/ui-state/transactions.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Signal.runInTransaction(() -> {
9595
----
9696

9797

98+
[#operation-results]
9899
== Operation Results
99100

100101
Signal operations return result objects that provide information about the operation and allow chaining.

0 commit comments

Comments
 (0)