Skip to main content

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}