logicaffeine_compile/page_host/mod.rs
1//! One driver, several page tiers.
2//!
3//! A `## Test` that works a page has two halves. The TEST — its `Expect`s, its budgets, its
4//! sealed world — is the tree-walker's, always. The PAGE is whoever executes it: the page's lowered
5//! LOGOS on the tree-walker itself (the reference), the same LOGOS on the bytecode VM (what a
6//! browser runs, compiled to WebAssembly), or a real browser. What varies between tiers is only
7//! who executes the page, so the statements that act on a page are carried out by ONE driver
8//! ([`LoweredHost::drive`]) that does for each exactly what `runtime.js` does in a browser, over
9//! whichever engine runs the page's lowered LOGOS:
10//!
11//! | statement | the page sees |
12//! |---|---|
13//! | `Mount C [at S]` | `__mount_C(parent)` |
14//! | `Click`/`Submit`/`Press`/`Type … into`/`Choose`/`Hover` | each of the verb's events dispatched as a browser dispatches it, along [`crate::semantics::dom::dispatch_path`] — every stop through `__lgx_dispatch_node` — and what came of it worded by [`crate::semantics::drive`] |
15//! | `Focus`/`Blur` | the DOM command, then the `focus`/`blur` it raises, dispatched the same way |
16//! | `Raise "offline"` | `__lgx_raise` |
17//! | `Navigate to U` | `pushState`, then the route handler the page registered |
18//! | `Reload the page` | the module re-instantiated, its main run again |
19//! | `Advance time by N` | every interval the page armed, fired as it comes due |
20//! | `Given that M "u" answers …` | `dom::stub_fetch` |
21//!
22//! After every statement the page SETTLES: each request that has an answer is completed through
23//! the closure the page handed over with it, until no completion sends one that has an answer.
24//! Every call into the page goes through one guarded door; a failure is handed to
25//! `__lgx_on_failure`, and one no handler absorbs is the page's to report.
26//!
27//! Page queries (`the text of "#x"`) read the recorder every in-process tier paints into, so a
28//! test's assertions mean the same thing whichever tier ran the page. Two tiers are compared by
29//! [`PageTrace`] — the page after every top-level statement — per [`Axis`].
30
31mod ledger;
32mod lowered;
33mod scenario;
34mod snapshot;
35mod trace;
36
37pub use ledger::{ledger_map, parse_ledger, stale_rows, LedgerRow, LedgerTier};
38pub use lowered::{DriveError, LoweredCalls, LoweredHost, LoweredPage};
39pub use scenario::{
40 arrange, assemble_stable, auto_mounted_page_component, auto_mounted_page_path,
41 boot_at_declared_route, classify, compare_tests, defs_auto_mount_a_page, drive_main,
42 drives_the_page, is_scene, works_a_page, Arranged, Arrival, Classified, GivenAnswer, HereRun,
43 PageTierKind, RemotePageLauncher, Scenario, TestTierComparison, TierRun, World,
44};
45pub use snapshot::{fetch_record, Axis, DocSnapshot};
46pub use trace::{describe_difference, AxisDivergence, Divergence, PageTrace, Stop, TraceStep};
47
48/// How many leading statements of `program` are its page's MAIN: everything before the first
49/// statement that acts on the page ([`drives_the_page`]) — the lowered prelude, the program's
50/// definitions, and the top-level bindings a page's `State:` may read. What follows is whoever
51/// drives the page, the page's own mount among them. A reload runs exactly the main again.
52pub fn page_main_len(
53 program: &[crate::ast::stmt::Stmt],
54 interner: &crate::intern::Interner,
55) -> usize {
56 program
57 .iter()
58 .position(|s| drives_the_page(s, interner))
59 .unwrap_or(program.len())
60}
61
62/// The name every page tier's shell is built with: the browser bundle's module and host
63/// (`/app.wasm`, `/app.js`) and the shell's default `<title>`. A page the host drives in process
64/// arrives in the shell this names ([`World`]), so it arrives in the very document a browser loads.
65pub const PAGE_STEM: &str = "app";
66
67/// One statement that acts on a page, with its operands evaluated — the unit every page tier is
68/// driven in.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum Drive {
71 /// `Mount C [at "#sel"].`
72 Mount {
73 component: String,
74 at: Option<String>,
75 },
76 /// `Click SEL.`
77 Click { selector: String },
78 /// `Submit SEL.`
79 Submit { selector: String },
80 /// `Press KEY [in SEL].` — the page itself (`body`) when no element is named.
81 Press { key: String, selector: String },
82 /// `Type TEXT into SEL.`
83 Type { text: String, selector: String },
84 /// `Choose VALUE in SEL.`
85 Choose { value: String, selector: String },
86 /// `Hover SEL.`
87 Hover { selector: String },
88 /// `Focus SEL.`
89 Focus { selector: String },
90 /// `Blur SEL.`
91 Blur { selector: String },
92 /// `Raise "offline".` — a window event, delivered to the page.
93 Raise {
94 event: String,
95 target: String,
96 value: String,
97 },
98 /// `Raise "filedropped" on SEL with FILE.` — `file` dropped on the element: its name, type and
99 /// content travel with the drop into the handler it reaches.
100 DropFile {
101 target: String,
102 file: crate::semantics::dom::DroppedFile,
103 },
104 /// `Navigate to URL.`
105 Navigate { url: String },
106 /// `Reload the page.`
107 Reload,
108 /// `Advance time by N.`, in milliseconds.
109 AdvanceTime { ms: u64 },
110 /// `Given that METHOD "url" answers STATUS with BODY.`
111 AnswerFetch {
112 method: String,
113 url: String,
114 status: i64,
115 body: String,
116 },
117 /// `Serve fetches with routes.`
118 ServeFetchesWithRoutes,
119 /// `Replay these actions: a, b, c.` — re-run the mounted component's own actions, by name, in
120 /// order, each a turn of its own.
121 Replay { actions: Vec<String> },
122}
123
124impl Drive {
125 /// The statement's own verb, as a test writes it.
126 pub fn verb(&self) -> &'static str {
127 match self {
128 Drive::Mount { .. } => "Mount",
129 Drive::Click { .. } => "Click",
130 Drive::Submit { .. } => "Submit",
131 Drive::Press { .. } => "Press",
132 Drive::Type { .. } => "Type … into",
133 Drive::Choose { .. } => "Choose",
134 Drive::Hover { .. } => "Hover",
135 Drive::Focus { .. } => "Focus",
136 Drive::Blur { .. } => "Blur",
137 Drive::Raise { .. } | Drive::DropFile { .. } => "Raise",
138 Drive::Navigate { .. } => "Navigate to",
139 Drive::Reload => "Reload the page",
140 Drive::AdvanceTime { .. } => "Advance time by",
141 Drive::AnswerFetch { .. } => "Given that … answers",
142 Drive::ServeFetchesWithRoutes => "Serve fetches with routes",
143 Drive::Replay { .. } => "Replay these actions",
144 }
145 }
146}
147
148/// A page the host drives one statement at a time and can observe between statements.
149pub trait DrivenPage {
150 fn drive(&mut self, d: &Drive) -> Result<(), DriveError>;
151 /// The first action failure no handler absorbed, if any.
152 fn unhandled_failure(&self) -> Option<String>;
153 /// The page as it stands.
154 fn snapshot(&mut self) -> Result<DocSnapshot, String>;
155}
156
157impl<L: LoweredCalls> DrivenPage for LoweredPage<L> {
158 fn drive(&mut self, d: &Drive) -> Result<(), DriveError> {
159 LoweredPage::drive(self, d)
160 }
161 fn unhandled_failure(&self) -> Option<String> {
162 LoweredPage::unhandled_failure(self).map(str::to_string)
163 }
164 fn snapshot(&mut self) -> Result<DocSnapshot, String> {
165 Ok(DocSnapshot::capture(
166 LoweredPage::unhandled_failure(self).map(str::to_string),
167 ))
168 }
169}
170
171/// A page executed somewhere other than this process — a real browser, driven over its own
172/// transport. Whoever implements it starts the page (serving its bundle, loading it) and then
173/// answers the same two questions every tier answers: carry out a statement, show the page.
174pub trait RemotePage {
175 fn drive(&mut self, d: &Drive) -> Result<(), DriveError>;
176 fn unhandled_failure(&self) -> Option<String>;
177 fn snapshot(&mut self) -> Result<DocSnapshot, String>;
178}
179
180impl DrivenPage for Box<dyn RemotePage + '_> {
181 fn drive(&mut self, d: &Drive) -> Result<(), DriveError> {
182 (**self).drive(d)
183 }
184 fn unhandled_failure(&self) -> Option<String> {
185 (**self).unhandled_failure()
186 }
187 fn snapshot(&mut self) -> Result<DocSnapshot, String> {
188 (**self).snapshot()
189 }
190}
191
192/// Who executes the page while a scenario runs.
193pub enum PageTier<'p> {
194 /// The page's lowered LOGOS on the bytecode VM, driven through its entries.
195 LoweredVm(LoweredPage<crate::vm::session::Session<'p>>),
196 /// A page in another process, driven over its transport.
197 Browser(Box<dyn RemotePage + 'p>),
198}
199
200impl<'p> PageTier<'p> {
201 /// The page the host drives.
202 pub fn driven(&mut self) -> &mut dyn DrivenPage {
203 match self {
204 PageTier::LoweredVm(page) => page,
205 PageTier::Browser(page) => page,
206 }
207 }
208
209 /// Which tier this is.
210 pub fn kind(&self) -> PageTierKind {
211 match self {
212 PageTier::LoweredVm(_) => PageTierKind::LoweredVm,
213 PageTier::Browser(_) => PageTierKind::Browser,
214 }
215 }
216}