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;