Osy#betaRun it on Osyrin: osyrin.com

Reference / Testing

Ui.Tick

Ui.Tick();

In a UI test, fires every `on every` clock armed on the screen once, now, and waits for their bodies — server calls included — so a test can assert what a poll brings without waiting for, or racing, the poll's timer.

stable1 example compiled by CItestinguitime

Summary#

Ui.Tick() makes the page's own on every clocks fire — each one once, now — and returns when their bodies have finished, including any server call a body makes and the re-render after it. It is how a test proves what a POLL does: a payment page that learns it was paid, a status that turns "verified", a counter that moves.

Signature#

Ui.Tick();

Description#

A clock (on every (TimeSpan.FromSeconds(2)) { … }, see on every) runs on a wall-clock timer, and an armed timer is not work the page reports as outstanding. A UI assertion waits for the page to finish what it is doing and then judges the screen; a timer that has not fired yet is not "doing" anything. So a test that asserts what the next poll will show is racing that timer against the assertion's own patience, and on a loaded machine it loses — the assertion answers "not there" a moment before the poll would have put it there.

Ui.Tick() removes the race instead of widening it. Every clock armed on the screen runs its body once, exactly as its timer would have run it, and the verb waits for all of them:

  • It takes the timer's place. The clock re-arms a full interval after the tick, as after any run — it does not fire again a moment later because its old timer was nearly due.
  • A body already running is waited for first, so a clock never runs its body twice at once.
  • A navigation the body asks for is part of the tick. A poll's commonest ending is to move on (Navigation.Go("/paid/…")), and the verb waits for the next page the same way a click does.
  • A clock whose last beat failed is still armed, and the tick runs it: a body that throws does not stop its clock (see on every), so a test can fail a beat and then prove the next one recovers.
  • Nothing armed is a refusal, not a quiet no-op: "Ui.Tick() found no on every clock armed on this screen". A tick that fired nothing would leave the next assertion judging a page that never polled.

⚑ It does not move time. A body that asks "has enough time passed?" — the shop asks BankID at most every two seconds — reads the test's clock, so move it first with TestClock.Advance and TestClock.Set: TestClock.Advance(…) then Ui.Tick(). The two compose, and neither does the other's job.

⚠ Write it where the poll is the thing under test. Re-opening the page (Ui.Visit) to make an on mount ask instead proves the mount, not the poll — the page a customer is actually looking at never re-opens itself.

Examples#

[Principal] entity User {
  [Required, MaxLength(200)] string Email;
  security { allow read when IsAuthenticated; }
}

[AllowAnonymous]
int NextSweep(int seen) { return seen + 1; }

[Route("/lighthouse")]
[AllowAnonymous]
[Render(CSR)]
component Lighthouse() {
  int sweeps = 0;
  // An hour: nothing but the tick can fire it inside the test, so every change the test sees is the tick's.
  on every (TimeSpan.FromHours(1)) { sweeps = NextSweep(sweeps); }
  render { Text($"The lamp has swept {sweeps} times."); }
}

[Test]
void each_tick_is_one_sweep_and_waits_for_the_server() {
  Ui.Visit("/lighthouse");
  Assert.Visible("The lamp has swept 0 times.");

  Ui.Tick();
  Assert.Visible("The lamp has swept 1 times.");

  Ui.Tick();
  Assert.Visible("The lamp has swept 2 times.");
}

See also#

Related

on every

`on every (TimeSpan.FromSeconds(5)) { … }` runs a block on a repeating cadence for as long as the component is mounted…

Ui — drive the app's UI from a test

Drive the real UI from a test: navigate to a route, click what a person would click, and assert on what the screen…

TestClock.Advance and TestClock.Set

In a test, moves the run's clock FORWARD by a duration, then lets the next Workflow.Settle fire every workflow timer…