Skip to main content

orinium_browser/engine/js/web_apis/
missing_api_detector.rs

1//! Missing Web Platform API detector
2//!
3//! Investigation/diagnostic helper ("log mode"). Scripts probe a wide surface
4//! of Web Platform APIs that this engine has not implemented yet; when a page
5//! reaches an unimplemented API it fails with a generic `TypeError`. To make
6//! those gaps visible without breaking execution, this module emits a
7//! `log::info!` report of which global / document / Element.prototype APIs are
8//! *absent* at boot time.
9//!
10//! It is deliberately side-effect free: it only reads, never installs stubs or
11//! shadows already-present implementations. Call [`install_missing_api_report`]
12//! right after creating a [`crate::engine::js::JsRuntime`] in an investigation
13//! harness (`examples/yt.rs`, the youtube fetch test) and grep for
14//! `missing-api` in the logs.
15
16use crate::engine::js::JsRuntime;
17
18/// Globals high-traffic sites probe for. Only entries that are actually
19/// undefined get reported.
20pub(crate) const SUSPECT_GLOBALS: &[&str] = &[
21    "AbortController",
22    "AbortSignal",
23    "Blob",
24    "Comment",
25    "CustomElementRegistry",
26    "Document",
27    "DocumentFragment",
28    "Element",
29    "EventTarget",
30    "FileReader",
31    "FormData",
32    "HTMLAudioElement",
33    "HTMLCanvasElement",
34    "HTMLMediaElement",
35    "HTMLScriptElement",
36    "HTMLSlotElement",
37    "HTMLTemplateElement",
38    "HTMLVideoElement",
39    "Headers",
40    "HTMLElement",
41    "IntersectionObserver",
42    "KeyboardEvent",
43    "MediaSource",
44    "MutationObserver",
45    "MouseEvent",
46    "Node",
47    "PointerEvent",
48    "RegExp",
49    "Request",
50    "Response",
51    "ResizeObserver",
52    "SVGElement",
53    "SVGSVGElement",
54    "ShadowRoot",
55    "SourceBuffer",
56    "Text",
57    "TextDecoder",
58    "TextEncoder",
59    "URL",
60    "URLSearchParams",
61    "WheelEvent",
62    "XMLHttpRequest",
63    "cancelAnimationFrame",
64    "customElements",
65    "getComputedStyle",
66    "matchMedia",
67    "performance",
68    "queueMicrotask",
69    "requestAnimationFrame",
70];
71
72/// Members probed on `document` (object style: `typeof document[name]`).
73///
74/// Properties such as `scripts` and `currentScript` live here too: YouTube's
75/// bootstrap reads them before it defines anything of its own.
76pub(crate) const SUSPECT_DOCUMENT_METHODS: &[&str] = &[
77    "adoptedStyleSheets",
78    "body",
79    "createElementNS",
80    "currentScript",
81    "documentElement",
82    "elementFromPoint",
83    "elementsFromPoint",
84    "execCommand",
85    "fonts",
86    "getSelection",
87    "head",
88    "importNode",
89    "pictureInPictureElement",
90    "scripts",
91];
92
93/// Members probed on `Element.prototype` (object style:
94/// `typeof Element.prototype[name]`).
95pub(crate) const SUSPECT_ELEMENT_PROTOTYPE_METHODS: &[&str] = &[
96    "attachShadow",
97    "closest",
98    "getAnimations",
99    "getRootNode",
100    "insertAdjacentElement",
101    "matches",
102    "requestFullscreen",
103    "scrollIntoView",
104    "scrollTo",
105];
106
107/// Builds the JS prologue that reports which APIs are absent.
108///
109/// The browser-layer probes inject it after their console collector so the
110/// report is captured alongside the page's own warnings.
111pub(crate) fn prologue() -> String {
112    let mut code = String::new();
113    code.push_str("(function(){var missing = [];\n");
114    for name in SUSPECT_GLOBALS {
115        code.push_str(&format!(
116            "if (typeof {name} === 'undefined') missing.push('window.{name}');\n"
117        ));
118    }
119    for name in SUSPECT_DOCUMENT_METHODS {
120        code.push_str(&format!(
121            "if (typeof document.{name} === 'undefined') missing.push('document.{name}');\n"
122        ));
123    }
124    for name in SUSPECT_ELEMENT_PROTOTYPE_METHODS {
125        code.push_str(&format!(
126            "if (typeof Element !== 'undefined' && Element.prototype && typeof Element.prototype.{name} === 'undefined') missing.push('Element.prototype.{name}');\n"
127        ));
128    }
129    code.push_str("if (missing.length) console.warn('[missing-api] ' + missing.join(','));\n");
130    code.push_str("}).call(window);\n");
131    code
132}
133
134/// Runs the missing-API report on `runtime` and forwards each name to
135/// `log::info!(target: "MissingApi", ...)`.
136///
137/// Usage in a harness:
138/// ```no_run
139/// use std::rc::Rc;
140/// use orinium_browser::engine::html::Parser;
141/// use orinium_browser::engine::js::{JsRuntime, missing_api_detector};
142///
143/// let dom = Rc::new(Parser::new("<div></div>").parse());
144/// let mut runtime = JsRuntime::new(dom);
145/// missing_api_detector::install_missing_api_report(&mut runtime);
146/// ```
147pub fn install_missing_api_report(runtime: &mut JsRuntime) {
148    runtime.run_script(&prologue(), None);
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::engine::html::Parser;
155    use std::rc::Rc;
156
157    #[test]
158    fn prologue_evaluates_without_error() {
159        let dom = Rc::new(Parser::new(r#"<div></div>"#).parse());
160        let mut runtime = JsRuntime::new(dom);
161        install_missing_api_report(&mut runtime);
162        // No panic means the prologue ran; absence entries are only log lines.
163    }
164}