day-core 0.6.2

Declarative app development API using native UI toolkits
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
// Copyright © The Daybrite Project
// SPDX-License-Identifier: MPL-2.0

//! Tests that run inside a built Day app (docs/testing.md): a [`Case`] is a page plus a drive,
//! or a headless run, declared next to the code it tests with `#[day::test]` and collected in a
//! registry. The dayscript engine runs them on a real toolkit under `day test`; the mock
//! harness runs the same functions under `cargo test`. Nothing here knows how a step is
//! executed: a [`DriveBackend`] does, and the engine supplies one.
//!
//! A test's name is its function's, with hyphens for underscores (`button_status` runs as
//! `button-status`): the registry entry carries the identifier, so a tool reading the source
//! (`day test --list`, an editor) and the app agree without either one running the other.

use std::cell::RefCell;
use std::future::Future;
use std::pin::Pin;
use std::rc::Rc;

use day_spec::Cap;

use crate::AnyPiece;

/// A failed assertion, with the sentence the report shows.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Fail(pub String);

impl std::fmt::Display for Fail {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.0)
    }
}

impl From<String> for Fail {
    fn from(s: String) -> Self {
        Fail(s)
    }
}

impl From<&str> for Fail {
    fn from(s: &str) -> Self {
        Fail(s.to_owned())
    }
}

/// What a test's body answers.
pub type TestResult = Result<(), Fail>;

/// What a drive asks of the app, in dayscript's vocabulary. The engine maps each to the step
/// a script would send, so a drive and a script mean the same thing by the same words.
#[derive(Clone, Debug, PartialEq)]
pub enum DriveOp {
    Tap(String),
    Input(String, String),
    Toggle(String, bool),
    SetValue(String, f64),
    Select(String, usize),
    Focus(String),
    Submit(String),
    Navigate(String),
    WaitIdle,
    Pause(f64),
    /// Capture the window under this name into the test's record.
    Shot(String),
    AssertText(String, String),
    AssertVisible(String),
    AssertMissing(String),
    /// That an element is not on screen: missing, empty, or natively hidden.
    AssertHidden(String),
    /// A pointer entering an element, at a point in it or (for `None`) its center.
    Hover(String, Option<(f64, f64)>),
    /// The pointer leaving an element.
    HoverLeave(String),
    /// A two-finger pan over an element, by this much in total.
    Pan(String, f64, f64),
    /// A pinch over an element, to this cumulative scale.
    Pinch(String, f64),
    /// A drag over an element, from one point to another in its coordinates.
    Drag(String, (f64, f64), (f64, f64)),
    /// A tap at a point in an element's coordinates.
    TapAt(String, f64, f64),
    /// A key press, to an element or (for `None`) to whatever holds focus.
    Key(Option<String>, String),
    /// That the app announced this text to the screen reader during the run.
    AssertAnnounced(String),
    AssertValue(String, f64),
    /// A toggle's state.
    AssertOn(String, bool),
    AssertFocused(String, bool),
    AssertRoute(String),
    A11yAudit(Option<String>),
    /// The native widget's own state, as the platform reports it.
    AssertNative(String, NativeExpect),
    /// An element's frame, in Day's layout and natively.
    AssertFrame(String, FrameExpect),
    /// The color at a point of an element (fractions of its frame), `#rrggbb`.
    SamplePixel(String, f64, f64, String),
    /// A URL the app asked to open during the run.
    AssertOpenedUrl(String),
    /// Activate a list's row (a double click, Return, a tap on the phones), by position.
    Activate(String, usize),
    /// Move a list's row from one position to another, as a drag-to-reorder commits it.
    Reorder(String, usize, usize),
    /// Delete a list's row, as the platform's delete gesture commits it.
    DeleteRow(String, usize),
    /// Press a row's swipe action: the list, the row, the edge (`true` = leading) and the
    /// action's position in that edge's offer.
    SwipeRow(String, usize, bool, usize),
    /// Scroll a scroll view or list to an edge (`"top"`, `"bottom"`, `"leading"`, `"trailing"`).
    ScrollTo(String, String),
    /// Expand or collapse a tree's row, named by its row id.
    Expand(String, String, bool),
    /// Go back one page in the innermost navigation stack.
    NavBack,
    /// Move a tree's row (by row id) under a parent (`None` = the root) at a position.
    TreeMove(String, String, Option<String>, Option<usize>),
    /// Choose an item, by label, from an element's context menu.
    ContextMenu(String, String),
    /// Choose an app-menu item by its id.
    Menu(String),
    /// Press a toolbar button, by item id.
    ToolbarPress(String),
    /// Set a toolbar toggle, by item id.
    ToolbarToggle(String, bool),
    /// Answer presentations natively (`false`) or hold them for `respond` (`true`).
    DialogScripted(bool),
    /// That a presentation is pending, with this title where one is given.
    AssertPresented(Option<String>),
    /// Answer the pending presentation: a button by position, or dismiss it (`None`).
    Respond(Option<usize>),
    /// Confirm a prompt with text (including an empty string).
    RespondText(String),
}

/// What [`Drive::assert_frame`] expects of an element's frame, in points; only the fields set
/// are checked. `x`/`y` are relative to `relative_to`'s origin, or the window content's.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FrameExpect {
    pub width: Option<f64>,
    pub height: Option<f64>,
    pub x: Option<f64>,
    pub y: Option<f64>,
    /// The element whose origin `x`/`y` are measured from.
    pub relative_to: Option<String>,
    /// How far each value may be off (default 1 point).
    pub tolerance: Option<f64>,
}

/// What [`Drive::assert_native`] expects the native widget to report; only the fields set are
/// checked (docs/testing.md).
#[derive(Clone, Debug, Default, PartialEq)]
pub struct NativeExpect {
    /// The text the widget displays.
    pub text: Option<String>,
    /// A slider's or progress indicator's value.
    pub number: Option<f64>,
    /// A toggle's state, or a button's selected state.
    pub checked: Option<bool>,
    /// Whether the widget takes input.
    pub enabled: Option<bool>,
    /// Whether the widget is shown.
    pub visible: Option<bool>,
}

/// The future one drive op resolves to.
pub type OpFuture = Pin<Box<dyn Future<Output = TestResult>>>;

/// Executes [`DriveOp`]s. The engine's backend runs each as the matching dayscript step on the
/// main thread and waits, asynchronously, through the step's retry window; the mock harness
/// uses the same backend against the mock toolkit.
pub trait DriveBackend {
    fn run(&self, op: DriveOp) -> OpFuture;
}

/// The handle a test drives the app with. Every method awaits the op and fails the test with
/// the step's own message when the op fails after its retry window.
#[derive(Clone)]
pub struct Drive {
    backend: Rc<dyn DriveBackend>,
}

impl Drive {
    /// A drive over `backend`; the engine and the mock harness build one per test.
    pub fn new(backend: Rc<dyn DriveBackend>) -> Self {
        Drive { backend }
    }

    fn op(&self, op: DriveOp) -> OpFuture {
        self.backend.run(op)
    }

    pub fn tap(&self, id: &str) -> OpFuture {
        self.op(DriveOp::Tap(id.into()))
    }
    /// Type `text` into the field, replacing what it holds.
    pub fn input(&self, id: &str, text: &str) -> OpFuture {
        self.op(DriveOp::Input(id.into(), text.into()))
    }
    pub fn toggle(&self, id: &str, on: bool) -> OpFuture {
        self.op(DriveOp::Toggle(id.into(), on))
    }
    pub fn set_value(&self, id: &str, value: f64) -> OpFuture {
        self.op(DriveOp::SetValue(id.into(), value))
    }
    pub fn select(&self, id: &str, index: usize) -> OpFuture {
        self.op(DriveOp::Select(id.into(), index))
    }
    pub fn focus(&self, id: &str) -> OpFuture {
        self.op(DriveOp::Focus(id.into()))
    }
    pub fn submit(&self, id: &str) -> OpFuture {
        self.op(DriveOp::Submit(id.into()))
    }
    pub fn navigate(&self, route: &str) -> OpFuture {
        self.op(DriveOp::Navigate(route.into()))
    }
    /// Let native transitions settle.
    pub fn wait_idle(&self) -> OpFuture {
        self.op(DriveOp::WaitIdle)
    }
    pub fn pause(&self, secs: f64) -> OpFuture {
        self.op(DriveOp::Pause(secs))
    }
    /// Capture the window as `name`; the capture rides the test's record to the runner.
    pub fn shot(&self, name: &str) -> OpFuture {
        self.op(DriveOp::Shot(name.into()))
    }
    pub fn assert_text(&self, id: &str, text: &str) -> OpFuture {
        self.op(DriveOp::AssertText(id.into(), text.into()))
    }
    pub fn assert_visible(&self, id: &str) -> OpFuture {
        self.op(DriveOp::AssertVisible(id.into()))
    }
    pub fn assert_missing(&self, id: &str) -> OpFuture {
        self.op(DriveOp::AssertMissing(id.into()))
    }
    /// The pointer entering `id` at (`x`, `y`) in its coordinates (its center for `None`).
    pub fn hover(&self, id: &str, at: Option<(f64, f64)>) -> OpFuture {
        self.op(DriveOp::Hover(id.into(), at))
    }
    /// The pointer leaving `id`.
    pub fn hover_leave(&self, id: &str) -> OpFuture {
        self.op(DriveOp::HoverLeave(id.into()))
    }
    /// A two-finger pan over `id` by (`dx`, `dy`) in total.
    pub fn pan(&self, id: &str, dx: f64, dy: f64) -> OpFuture {
        self.op(DriveOp::Pan(id.into(), dx, dy))
    }
    /// A pinch over `id` to `scale` (cumulative; 1 = unchanged).
    pub fn pinch(&self, id: &str, scale: f64) -> OpFuture {
        self.op(DriveOp::Pinch(id.into(), scale))
    }
    /// A pointer drag over `id` from `from` to `to`, in its coordinates.
    pub fn drag(&self, id: &str, from: (f64, f64), to: (f64, f64)) -> OpFuture {
        self.op(DriveOp::Drag(id.into(), from, to))
    }
    /// A tap at (`x`, `y`) in `id`'s coordinates.
    pub fn tap_at(&self, id: &str, x: f64, y: f64) -> OpFuture {
        self.op(DriveOp::TapAt(id.into(), x, y))
    }
    /// A key press (web `KeyboardEvent.key` names: `"Enter"`, `"ArrowRight"`, `"a"`) to `id`,
    /// or to whatever holds focus for `None`.
    pub fn key(&self, id: Option<&str>, key: &str) -> OpFuture {
        self.op(DriveOp::Key(id.map(str::to_owned), key.into()))
    }
    /// That the app announced `text` to the screen reader during the run.
    pub fn assert_announced(&self, text: &str) -> OpFuture {
        self.op(DriveOp::AssertAnnounced(text.into()))
    }
    /// That an element is not on screen: missing, with an empty frame, or hidden natively (a
    /// collapsed pane a toolkit keeps built).
    pub fn assert_hidden(&self, id: &str) -> OpFuture {
        self.op(DriveOp::AssertHidden(id.into()))
    }
    pub fn assert_value(&self, id: &str, value: f64) -> OpFuture {
        self.op(DriveOp::AssertValue(id.into(), value))
    }
    /// A toggle's on/off state.
    pub fn assert_on(&self, id: &str, on: bool) -> OpFuture {
        self.op(DriveOp::AssertOn(id.into(), on))
    }
    pub fn assert_focused(&self, id: &str, focused: bool) -> OpFuture {
        self.op(DriveOp::AssertFocused(id.into(), focused))
    }
    pub fn assert_route(&self, route: &str) -> OpFuture {
        self.op(DriveOp::AssertRoute(route.into()))
    }
    /// Diff the native accessibility tree against Day's expectation for `id` (all id'd
    /// nodes when `None`), where the toolkit can read its tree back.
    pub fn a11y_audit(&self, id: Option<&str>) -> OpFuture {
        self.op(DriveOp::A11yAudit(id.map(str::to_owned)))
    }

    /// The native widget's own state, as the platform reports it (docs/testing.md). A field
    /// the toolkit cannot read is recorded in the report as unread rather than failed. In a
    /// conformance run `assert_text`, `assert_value` and `assert_on` check the native widget
    /// too; this is for what they do not cover.
    pub fn assert_native(&self, id: &str, expect: NativeExpect) -> OpFuture {
        self.op(DriveOp::AssertNative(id.into(), expect))
    }
    /// The element's frame in Day's layout and, where the toolkit reads it back, natively.
    pub fn assert_frame(&self, id: &str, expect: FrameExpect) -> OpFuture {
        self.op(DriveOp::AssertFrame(id.into(), expect))
    }
    /// The element's size, the common case of [`Drive::assert_frame`].
    pub fn assert_size(&self, id: &str, width: f64, height: f64) -> OpFuture {
        self.assert_frame(
            id,
            FrameExpect {
                width: Some(width),
                height: Some(height),
                ..Default::default()
            },
        )
    }
    /// The color drawn at (`x`, `y`), fractions of the element's frame, as `#rrggbb`. Passes as
    /// unread where the toolkit has no capture.
    pub fn sample_pixel(&self, id: &str, x: f64, y: f64, color: &str) -> OpFuture {
        self.op(DriveOp::SamplePixel(id.into(), x, y, color.into()))
    }
    /// That the app asked to open `url` (a link, `day::open_url`) during the run; in a run
    /// nothing actually opens.
    pub fn assert_opened_url(&self, url: &str) -> OpFuture {
        self.op(DriveOp::AssertOpenedUrl(url.into()))
    }
    /// Whether the element takes input, in Day's tree and in the native widget.
    pub fn assert_enabled(&self, id: &str, enabled: bool) -> OpFuture {
        self.assert_native(
            id,
            NativeExpect {
                enabled: Some(enabled),
                ..Default::default()
            },
        )
    }

    /// Activate a list's row by position: what a double click, Return or a phone's tap commits.
    pub fn activate(&self, id: &str, index: usize) -> OpFuture {
        self.op(DriveOp::Activate(id.into(), index))
    }
    /// Move a list's row from `from` to `to`, as a native drag-to-reorder commits it.
    pub fn reorder(&self, id: &str, from: usize, to: usize) -> OpFuture {
        self.op(DriveOp::Reorder(id.into(), from, to))
    }
    /// Delete a list's row, as the platform's delete gesture commits it.
    pub fn delete_row(&self, id: &str, row: usize) -> OpFuture {
        self.op(DriveOp::DeleteRow(id.into(), row))
    }
    /// Press the `action`th swipe action on a list row's leading or trailing edge.
    pub fn swipe_row(&self, id: &str, row: usize, leading: bool, action: usize) -> OpFuture {
        self.op(DriveOp::SwipeRow(id.into(), row, leading, action))
    }
    /// Scroll a scroll view or list to an edge: `"top"`, `"bottom"`, `"leading"`, `"trailing"`.
    pub fn scroll_to(&self, id: &str, edge: &str) -> OpFuture {
        self.op(DriveOp::ScrollTo(id.into(), edge.into()))
    }
    /// Expand or collapse a tree's row, named by its row id.
    pub fn expand(&self, id: &str, row: &str, expanded: bool) -> OpFuture {
        self.op(DriveOp::Expand(id.into(), row.into(), expanded))
    }
    /// Go back one page in the innermost navigation stack.
    pub fn nav_back(&self) -> OpFuture {
        self.op(DriveOp::NavBack)
    }
    /// Move a tree's row (by row id) under `parent` (`None` = the root level), at `index` among
    /// its new siblings (`None` = last), as a native drag commits it.
    pub fn tree_move(
        &self,
        id: &str,
        row: &str,
        parent: Option<&str>,
        index: Option<usize>,
    ) -> OpFuture {
        self.op(DriveOp::TreeMove(
            id.into(),
            row.into(),
            parent.map(str::to_owned),
            index,
        ))
    }

    /// Choose the item labeled `item` from `id`'s context menu, as a right click (or long
    /// press) and a choice would.
    pub fn context_menu(&self, id: &str, item: &str) -> OpFuture {
        self.op(DriveOp::ContextMenu(id.into(), item.into()))
    }
    /// Choose an app-menu item by the id the app gave it (`MenuEntry::id`).
    pub fn menu(&self, item_id: &str) -> OpFuture {
        self.op(DriveOp::Menu(item_id.into()))
    }
    /// Press the toolbar button `item`.
    pub fn toolbar_press(&self, item: &str) -> OpFuture {
        self.op(DriveOp::ToolbarPress(item.into()))
    }
    /// Turn the toolbar toggle `item` on or off.
    pub fn toolbar_toggle(&self, item: &str, on: bool) -> OpFuture {
        self.op(DriveOp::ToolbarToggle(item.into(), on))
    }
    /// Show presentations natively (`false`, the default) or hold them for
    /// [`Drive::respond`] without any native dialog (`true`). In native mode `respond` still
    /// answers, and the toolkit dismisses the dialog it showed.
    pub fn dialogs_scripted(&self, scripted: bool) -> OpFuture {
        self.op(DriveOp::DialogScripted(scripted))
    }
    /// That a dialog or picker is pending, titled `title` where one is given.
    pub fn assert_presented(&self, title: Option<&str>) -> OpFuture {
        self.op(DriveOp::AssertPresented(title.map(str::to_owned)))
    }
    /// Answer the pending dialog with the button at `button` (presentation order), or dismiss
    /// it for `None`.
    pub fn respond(&self, button: Option<usize>) -> OpFuture {
        self.op(DriveOp::Respond(button))
    }

    /// Confirm a pending text prompt.
    pub fn respond_text(&self, text: &str) -> OpFuture {
        self.op(DriveOp::RespondText(text.into()))
    }

    /// A plain assertion for a headless test: fails with `what` when `ok` is false.
    pub fn check(&self, ok: bool, what: &str) -> TestResult {
        if ok { Ok(()) } else { Err(Fail(what.into())) }
    }
    /// Equality for a headless test, naming both sides on a mismatch.
    pub fn check_eq<T: PartialEq + std::fmt::Debug>(&self, got: T, want: T) -> TestResult {
        if got == want {
            Ok(())
        } else {
            Err(Fail(format!("{got:?} ≠ expected {want:?}")))
        }
    }
}

/// Whether a case shows a page and drives it, or runs with no page.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TestKind {
    Gui,
    Headless,
}

type Body = Rc<dyn Fn(Drive) -> Pin<Box<dyn Future<Output = TestResult>>>>;

/// One test: what it proves, what it needs, the page it shows and the body that drives it.
/// Built by a `#[day::test]` function each time the test runs, so the signals a page and its
/// drive share are fresh per run; the registry names it after that function.
#[derive(Clone)]
pub struct Case {
    name: String,
    kind: TestKind,
    proves: Vec<String>,
    requires: Vec<Cap>,
    /// Supports the case needs beyond a `Cap`: a piece's own `support()` answer, named.
    requires_support: Vec<(String, fn() -> day_spec::Support)>,
    page: Option<Rc<dyn Fn() -> AnyPiece>>,
    shots: Vec<String>,
    body: Option<Body>,
    timeout_secs: Option<f64>,
}

impl Default for Case {
    fn default() -> Self {
        Case::new()
    }
}

impl Case {
    /// A GUI case: a page the app's test host shows while the case runs, and a drive against it.
    pub fn new() -> Self {
        Case {
            name: String::new(),
            kind: TestKind::Gui,
            proves: Vec::new(),
            requires: Vec::new(),
            requires_support: Vec::new(),
            page: None,
            shots: Vec::new(),
            body: None,
            timeout_secs: None,
        }
    }
    /// A test with no page: app logic run inside the app's own environment.
    pub fn headless() -> Self {
        Case {
            kind: TestKind::Headless,
            ..Case::new()
        }
    }
    /// How long the case may take, page and drive together, before it fails as timed out.
    /// Unset, the run's default applies (30 s, or what `day test --case-timeout` says).
    pub fn timeout(mut self, secs: f64) -> Self {
        self.timeout_secs = Some(secs);
        self
    }
    /// The piece kind this case is the proof of (`kinds::BUTTON`); a pass marks the kind
    /// on this toolkit in the coverage tables.
    pub fn proves(mut self, kind: &str) -> Self {
        self.proves.push(format!("kind:{kind}"));
        self
    }
    /// The capability this case proves.
    pub fn proves_cap(mut self, cap: Cap) -> Self {
        self.proves.push(format!("cap:{cap:?}"));
        self
    }
    /// The `Decorate` modifier this case proves (`"padding"`).
    pub fn proves_modifier(mut self, modifier: &str) -> Self {
        self.proves.push(format!("modifier:{modifier}"));
        self
    }
    /// The toolkit duty this case proves (`"set_input_traits"`).
    pub fn proves_duty(mut self, duty: &str) -> Self {
        self.proves.push(format!("duty:{duty}"));
        self
    }
    /// A capability the case needs; where the toolkit answers `Unsupported` the case is
    /// skipped with that reason rather than failed. The one way a case adapts to a toolkit.
    pub fn requires(mut self, cap: Cap) -> Self {
        self.requires.push(cap);
        self
    }
    /// A piece's own support the case needs (`day_piece_map::support`), named for the skip
    /// reason: where it answers `Unsupported` the case is skipped, as for [`Case::requires`].
    /// For a piece realized only where its platform ships the widget.
    pub fn requires_support(mut self, what: &str, support: fn() -> day_spec::Support) -> Self {
        self.requires_support.push((what.to_owned(), support));
        self
    }
    /// The first support this case needs that the running toolkit lacks, named.
    pub fn missing_support(&self) -> Option<String> {
        self.requires_support
            .iter()
            .find(|(_, f)| f() == day_spec::Support::Unsupported)
            .map(|(what, _)| what.clone())
    }
    /// The page the app shows for this case; any piece.
    pub fn page<P: crate::Piece>(mut self, page: impl Fn() -> P + 'static) -> Self {
        self.page = Some(Rc::new(move || AnyPiece::new(page())));
        self
    }
    /// A capture taken before the drive runs, once the page is shown.
    pub fn shot(mut self, name: &str) -> Self {
        self.shots.push(name.into());
        self
    }
    /// The drive: an async body over a [`Drive`], awaiting each step so the app's main loop
    /// runs between them (a transition settles, a capture paints) as it does between a
    /// script's steps.
    pub fn drive<F>(mut self, body: impl Fn(Drive) -> F + 'static) -> Self
    where
        F: Future<Output = TestResult> + 'static,
    {
        self.body = Some(Rc::new(move |d| Box::pin(body(d))));
        self
    }
    /// [`Case::drive`] under the name a headless test reads better with.
    pub fn run<F>(self, body: impl Fn(Drive) -> F + 'static) -> Self
    where
        F: Future<Output = TestResult> + 'static,
    {
        self.drive(body)
    }

    /// The name the registry gave the case: its function's, hyphenated. Empty for a case
    /// built outside the registry.
    pub fn name(&self) -> &str {
        &self.name
    }
    /// The case's own time limit, if it set one.
    pub fn timeout_secs(&self) -> Option<f64> {
        self.timeout_secs
    }
    pub fn kind(&self) -> TestKind {
        self.kind
    }
    /// The `kind:`, `cap:` and `duty:` keys this case proves, in the matrices' own spelling.
    pub fn proves_keys(&self) -> &[String] {
        &self.proves
    }
    pub fn requirements(&self) -> &[Cap] {
        &self.requires
    }
    /// The captures to take before the drive.
    pub fn shots(&self) -> &[String] {
        &self.shots
    }
    /// Build the case's page, if it has one.
    pub fn build_page(&self) -> Option<AnyPiece> {
        self.page.as_ref().map(|p| p())
    }
    /// Run the body over `drive`; a case with no body passes once its page showed.
    pub fn run_body(&self, drive: Drive) -> Pin<Box<dyn Future<Output = TestResult>>> {
        match &self.body {
            Some(body) => body(drive),
            None => Box::pin(async { Ok(()) }),
        }
    }
}

/// A registered test: the function that builds its [`Case`], and that function's identifier,
/// which is where the test's name comes from. Declared by `#[day::test]`.
#[derive(Clone, Copy)]
pub struct TestFn {
    ident: &'static str,
    build: fn() -> Case,
}

impl TestFn {
    /// An entry for `build`, named by `ident` (`stringify!` of the function or its path).
    pub const fn new(ident: &'static str, build: fn() -> Case) -> Self {
        TestFn { ident, build }
    }
    /// The test's name: the identifier's last path segment, with hyphens for underscores.
    pub fn name(&self) -> String {
        let last = self.ident.rsplit("::").next().unwrap_or(self.ident);
        last.trim().replace('_', "-")
    }
    /// A fresh case, named.
    pub fn case(&self) -> Case {
        let mut case = (self.build)();
        case.name = self.name();
        case
    }
}

mod registry;
pub use registry::validate_registry;
#[cfg(target_arch = "wasm32")]
pub use registry::{Registration, install_web_registry};

/// Every native `#[day::test]`, collected without runtime constructors.
#[cfg(not(target_arch = "wasm32"))]
#[linkme::distributed_slice]
pub static TESTS: [TestFn];

/// What a test host does when the active case changes: rebuild its content.
type HostNotify = Rc<dyn Fn()>;

day_reactive::tls_slots! {
    conformance;
    static RUNTIME: RefCell<Vec<TestFn>> = const { RefCell::new(Vec::new()) };
    /// The case the engine is driving, which the test host shows; its page shares its signals
    /// with the drive.
    static ACTIVE: RefCell<Option<Case>> = const { RefCell::new(None) };
    /// The mounted test host's rebuild, when the app shows one (`day::test_host`).
    static HOST: RefCell<Option<HostNotify>> = const { RefCell::new(None) };
    /// A panic raised while the host built the active case's page, for the runner to report.
    static PAGE_PANIC: RefCell<Option<String>> = const { RefCell::new(None) };
    /// The URLs the app asked to open while a test run was going on, when one is.
    static OPENED: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
    /// What the app announced to the screen reader while a test run was going on, when one is.
    static ANNOUNCED: RefCell<Option<Vec<String>>> = const { RefCell::new(None) };
}

/// Start or end a test run's interception of [`crate::open_url`]: while it is on, a URL the app
/// asks to open is recorded instead of opened, so `assert_opened_url` can check it and no
/// browser starts on the machine running the tests.
pub fn set_intercepting_urls(on: bool) {
    OPENED.with(|o| *o.borrow_mut() = on.then(Vec::new));
    ANNOUNCED.with(|a| *a.borrow_mut() = on.then(Vec::new));
}

/// Record a screen-reader announcement if a test run is going on. Unlike a URL it still goes
/// to the toolkit: announcing has no side effect a run must avoid, and the duty is what a case
/// proves.
pub fn note_announcement(text: &str) {
    ANNOUNCED.with(|a| {
        if let Some(list) = a.borrow_mut().as_mut() {
            list.push(text.to_owned());
        }
    });
}

/// The announcements made since the run began.
pub fn announcements() -> Vec<String> {
    ANNOUNCED.with(|a| a.borrow().clone().unwrap_or_default())
}

/// Record `url` if a test run is intercepting; whether it did.
pub fn intercept_open_url(url: &str) -> bool {
    OPENED.with(|o| match o.borrow_mut().as_mut() {
        Some(list) => {
            list.push(url.to_owned());
            true
        }
        None => false,
    })
}

/// The URLs opened (and intercepted) since the run began.
pub fn opened_urls() -> Vec<String> {
    OPENED.with(|o| o.borrow().clone().unwrap_or_default())
}

/// Add a synthetic case in a test harness. Application tests use `#[day::test]`.
pub fn register(test: TestFn) {
    RUNTIME.with(|r| {
        let mut r = r.borrow_mut();
        if !r.iter().any(|t| std::ptr::fn_addr_eq(t.build, test.build)) {
            r.push(test);
        }
    });
}

/// Every registered test, link-time and run-time ones together.
pub fn tests() -> Vec<TestFn> {
    let mut all: Vec<TestFn> = Vec::new();
    all.extend(registry::compiled_tests());
    RUNTIME.with(|r| {
        for t in r.borrow().iter() {
            if !all.iter().any(|a| std::ptr::fn_addr_eq(a.build, t.build)) {
                all.push(*t);
            }
        }
    });
    all
}

/// Every registered test as a fresh, named [`Case`], sorted by name.
pub fn cases() -> Vec<Case> {
    let mut cases: Vec<Case> = tests().iter().map(TestFn::case).collect();
    cases.sort_by(|a, b| a.name.cmp(&b.name));
    cases
}

/// Show `case` in the app's test host, built fresh, until the next call or [`clear_active`].
/// Answers false when the app mounts no test host, so the runner can say so.
pub fn set_active(case: Case) -> bool {
    ACTIVE.with(|a| *a.borrow_mut() = Some(case));
    PAGE_PANIC.with(|p| *p.borrow_mut() = None);
    notify_host()
}

/// Return the test host to the app's own content.
pub fn clear_active() {
    ACTIVE.with(|a| *a.borrow_mut() = None);
    notify_host();
}

fn notify_host() -> bool {
    let host = HOST.with(|h| h.borrow().clone());
    match host {
        Some(rebuild) => {
            rebuild();
            true
        }
        None => false,
    }
}

/// The active case, for the test host to build.
pub fn active() -> Option<Case> {
    ACTIVE.with(|a| a.borrow().clone())
}

/// Install the mounted test host's rebuild (`day::test_host` does, for the life of its
/// scope); `None` when it goes.
pub fn set_host(rebuild: Option<Rc<dyn Fn()>>) {
    HOST.with(|h| *h.borrow_mut() = rebuild);
}

/// Record a panic the test host caught while building the active case's page.
pub fn note_page_panic(message: String) {
    PAGE_PANIC.with(|p| *p.borrow_mut() = Some(message));
}

/// The panic, if any, the active case's page raised as it was built.
pub fn take_page_panic() -> Option<String> {
    PAGE_PANIC.with(|p| p.borrow_mut().take())
}

/// The sentence a caught panic's payload reads as.
pub fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
    if let Some(s) = payload.downcast_ref::<&str>() {
        (*s).to_owned()
    } else if let Some(s) = payload.downcast_ref::<String>() {
        s.clone()
    } else {
        "a panic with no message".to_owned()
    }
}

/// A fresh case for the test called `name`, for a person browsing an app's tests (the
/// conformance app's navigation does).
pub fn case_named(name: &str) -> Option<Case> {
    tests()
        .into_iter()
        .find(|t| t.name() == name)
        .map(|t| t.case())
}