Skip to main content

logicaffeine_compile/semantics/
drive.rs

1//! Driving a page from outside it: what a host's driving verb answers, what that answer says, and
2//! the clock a page arms.
3//!
4//! A `## Test` works a page the way a visitor would — `Click`, `Type … into`, `Press`, `Submit`,
5//! `Choose`, `Hover` — and every tier reports the outcome in the same three words: the element the
6//! selector names is not there, it is there and nothing on the page answers what was done to it,
7//! or something answered. A lowered page answers through its node dispatcher, which says whether
8//! anything heard each event the verb raised; the reference tier knows it directly. Either way the
9//! sentence a test author reads comes from HERE, so the two tiers cannot word one failure two ways.
10//!
11//! The page clock is the other half of what a host does to a page it drives. A browser page arms
12//! `setInterval(ms, handler)` and the browser calls the handler every `ms` milliseconds; a host
13//! with a virtual clock does the same arithmetic when a test says `Advance time by …`. The armed
14//! intervals are recorded here, beside the answers, because they are the host's to fire.
15
16use crate::interpreter::RuntimeValue;
17use std::cell::RefCell;
18
19/// What a driving verb found.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum Answer {
22    /// No element matches the selector.
23    NoElement,
24    /// The element is there and nothing on the page answered.
25    Unanswered,
26    /// Something answered.
27    Answered,
28}
29
30/// A driving verb a test aims at an element, and the events it raises there — the events a
31/// browser raises for the same act, in the order it raises them.
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
33pub enum Verb {
34    /// `Click SEL.`
35    Click,
36    /// `Submit SEL.`
37    Submit,
38    /// `Press KEY [in SEL].` — a key goes down and comes up; either answering is an answer.
39    Press,
40    /// `Type TEXT into SEL.` — an `input` and then a `change`.
41    Type,
42    /// `Choose VALUE in SEL.` — a selection raises what typing raises.
43    Choose,
44    /// `Hover SEL.`
45    Hover,
46}
47
48impl Verb {
49    /// Every verb, for a census.
50    pub const ALL: [Verb; 6] = [
51        Verb::Click,
52        Verb::Submit,
53        Verb::Press,
54        Verb::Type,
55        Verb::Choose,
56        Verb::Hover,
57    ];
58
59    /// The events the verb raises on its element.
60    pub fn events(self) -> &'static [&'static str] {
61        match self {
62            Verb::Click => &["click"],
63            Verb::Submit => &["submit"],
64            Verb::Press => &["keydown", "keyup"],
65            Verb::Type | Verb::Choose => &["input", "change"],
66            Verb::Hover => &["mouseover"],
67        }
68    }
69
70    /// Where the browser's own default action of the verb leaves keyboard focus — moved, with its
71    /// `blur` and `focus` raised, before any of the verb's [`Verb::events`]: a click's press focuses
72    /// what was pressed, a control is focused to be typed into or chosen in, and a key pressed in an
73    /// element goes to it. Submitting and hovering move nothing.
74    pub fn focus_target(self) -> FocusTarget {
75        match self {
76            Verb::Click => FocusTarget::PressedAncestor,
77            Verb::Type | Verb::Choose | Verb::Press => FocusTarget::Element,
78            Verb::Submit | Verb::Hover => FocusTarget::Unmoved,
79        }
80    }
81}
82
83/// Where a verb's default action sends keyboard focus ([`Verb::focus_target`]).
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum FocusTarget {
86    /// A pointer press: focus goes to the pressed element's nearest focusable ancestor-or-self, and
87    /// off every element when there is none — pressing on plain text blurs the field that had it.
88    PressedAncestor,
89    /// The element itself takes focus when it can (`element.focus()`); focus stays put when it
90    /// cannot.
91    Element,
92    /// Focus does not move.
93    Unmoved,
94}
95
96/// The failure an answer means for `events` aimed at `selector`, or `None` when it is no failure.
97///
98/// `key` names the key of a press, which is how its refusal names what was pressed.
99pub fn answer_failure(
100    answer: Answer,
101    events: &[&str],
102    key: Option<&str>,
103    selector: &str,
104    must_answer: bool,
105) -> Option<String> {
106    match answer {
107        Answer::Answered => None,
108        Answer::NoElement => Some(no_element(selector)),
109        Answer::Unanswered if must_answer => Some(nothing_answers(events, key, selector)),
110        Answer::Unanswered => None,
111    }
112}
113
114/// `No element matches "#x". …` — the selector names nothing on the page.
115pub fn no_element(selector: &str) -> String {
116    format!(
117        "No element matches {selector:?}. Mount the component first, and check the selector \
118         against the ids and classes its view renders."
119    )
120}
121
122/// `Nothing answers a click on "#x". …` — the element is there and nothing listens.
123pub fn nothing_answers(events: &[&str], key: Option<&str>, selector: &str) -> String {
124    let what = match key {
125        Some(k) => format!("a {k} press"),
126        None => format!("a {}", events.join(" or ")),
127    };
128    let first = events.first().copied().unwrap_or("click");
129    format!(
130        "Nothing answers {what} on {selector:?}. Give the element an `on-{first}={{action}}` \
131         directive, or write a `When {selector:?} is …:` sentence in the component's `Actions:`."
132    )
133}
134
135/// `Typing into "#x" enters nothing. …` / `Choosing "v" in "#x" selects nothing. …` — the element
136/// is there and cannot take the verb, so the browser's own default action does nothing and raises
137/// no `input` or `change`.
138pub fn cannot_take(verb: Verb, selector: &str, value: &str) -> String {
139    match verb {
140        Verb::Choose => format!(
141            "Choosing {value:?} in {selector:?} selects nothing: it is not a `<select>` a visitor can \
142             reach that offers that option, so the browser raises no `input` or `change`. Check the \
143             option's value or text, and that the control is not disabled."
144        ),
145        _ => format!(
146            "Typing into {selector:?} enters nothing: it takes no typed text — it is read-only, \
147             disabled, or not a text field — so the browser raises no `input` or `change`. Type into \
148             a `<textarea>` or a text `<input>` a visitor can edit."
149        ),
150    }
151}
152
153/// `` `Focus "#x"` found no such element. … `` — a command that acts on one element (focus, blur)
154/// aimed at an element that is not there.
155pub fn element_required(verb: &str, selector: &str) -> String {
156    format!(
157        "`{verb} {selector:?}` found no such element. Mount the component first, and check the \
158         selector against the ids and classes its view renders."
159    )
160}
161
162/// `Serve fetches with routes.` in a program that declares no route.
163pub fn no_routes_to_serve() -> String {
164    "`Serve fetches with routes.` needs routes to serve, and this program declares none. Add a \
165     `## Route METHOD /path -> T:` block, or answer the page's requests with `Given that METHOD \
166     \"/url\" answers …`."
167        .to_string()
168}
169
170/// `Mount App at "#root": no element matches that selector. …`
171pub fn mount_target_missing(component: &str, selector: &str) -> String {
172    format!(
173        "Mount {component} at {selector:?}: no element matches that selector. Create the mount \
174         point before mounting, or mount without `at` to render under the page body."
175    )
176}
177
178/// A form's fields as the body a browser submits: `a=1&b=2`, each name and value percent-encoded
179/// with the unreserved characters kept and a space as `+` (`application/x-www-form-urlencoded`).
180pub fn form_urlencoded(fields: &[(String, String)]) -> String {
181    fn enc(s: &str) -> String {
182        let mut out = String::new();
183        for b in s.bytes() {
184            match b {
185                b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
186                    out.push(b as char)
187                }
188                b' ' => out.push('+'),
189                _ => out.push_str(&format!("%{b:02X}")),
190            }
191        }
192        out
193    }
194    fields
195        .iter()
196        .map(|(k, v)| format!("{}={}", enc(k), enc(v)))
197        .collect::<Vec<_>>()
198        .join("&")
199}
200
201/// One interval a page armed: its period and the closure the host fires each period.
202#[derive(Clone, Debug)]
203pub struct Interval {
204    pub period_ms: i64,
205    pub handler: RuntimeValue,
206}
207
208thread_local! {
209    static INTERVALS: RefCell<Vec<Interval>> = const { RefCell::new(Vec::new()) };
210}
211
212/// `setInterval(ms, handler)` — record an interval the host fires every `ms` milliseconds.
213pub fn arm_interval(period_ms: i64, handler: RuntimeValue) {
214    INTERVALS.with(|i| i.borrow_mut().push(Interval { period_ms, handler }));
215}
216
217/// Every interval the page has armed, in the order it armed them.
218pub fn intervals() -> Vec<Interval> {
219    INTERVALS.with(|i| i.borrow().clone())
220}
221
222/// A fresh page: no intervals armed.
223pub fn reset_clock() {
224    INTERVALS.with(|i| i.borrow_mut().clear());
225}
226
227/// The intervals a page has armed — what [`take`] saves and [`restore`] puts back.
228#[derive(Default)]
229pub struct ClockState {
230    intervals: Vec<Interval>,
231}
232
233/// Take the armed intervals, leaving none in their place — like [`reset_clock`], but they come
234/// back to the caller instead of being dropped. A nested run (a candidate page tier compared
235/// against the reference) takes the caller's own armed intervals before it arms its own, and
236/// [`restore`]s them when it is done.
237pub fn take() -> ClockState {
238    ClockState {
239        intervals: INTERVALS.with(RefCell::take),
240    }
241}
242
243/// Put back intervals a matching [`take`] returned.
244pub fn restore(state: ClockState) {
245    INTERVALS.with(|i| *i.borrow_mut() = state.intervals);
246}
247
248#[cfg(test)]
249#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/semantics/drive/tests.rs"]
250mod tests;