Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ See [docs.plasmate.app/roadmap](https://docs.plasmate.app/roadmap) for the full
**v0.5 (current):**
- [x] Proxy support (HTTP, HTTPS, SOCKS5 with auth)
- [x] Proxy rotation (pool management, sticky sessions)
- [ ] Iframe support
- [x] Iframe support
- [ ] Shadow DOM support
- [ ] Full ES module support
- [ ] Parallel sessions at scale (500+ concurrent)
Expand Down
1 change: 1 addition & 0 deletions src/cdp/domains.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,7 @@ pub fn accessibility_get_full_ax_tree(id: u64, target: &CdpTarget) -> CdpRespons
ElementRole::Section => "Section",
ElementRole::Separator => "separator",
ElementRole::Details => "group",
ElementRole::Iframe => "Iframe",
};

let name = element
Expand Down
1 change: 1 addition & 0 deletions src/cdp/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,5 +708,6 @@ fn role_to_tag(role: &ElementRole) -> String {
ElementRole::Section => "section".to_string(),
ElementRole::Separator => "hr".to_string(),
ElementRole::Details => "details".to_string(),
ElementRole::Iframe => "iframe".to_string(),
}
}
32 changes: 32 additions & 0 deletions src/som/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,7 @@ fn tag_to_role(tag: &str, attrs: &[(String, String)]) -> Option<ElementRole> {
"section" | "article" => Some(ElementRole::Section),
"hr" => Some(ElementRole::Separator),
"details" => Some(ElementRole::Details),
"iframe" => Some(ElementRole::Iframe),
_ => None,
}
}
Expand Down Expand Up @@ -1326,6 +1327,37 @@ fn build_element_attrs(
map.insert("summary".into(), json!(st));
}
}
"iframe" => {
// Core iframe attributes for agents
if let Some(src) = attrs.iter().find(|(n, _)| n == "src") {
map.insert("src".into(), json!(src.1));
}
if let Some(srcdoc) = attrs.iter().find(|(n, _)| n == "srcdoc") {
// For srcdoc, we just note it exists (content is inline HTML)
map.insert("has_srcdoc".into(), json!(true));
// Optionally extract a preview of the srcdoc content
let preview: String = srcdoc.1.chars().take(200).collect();
if !preview.is_empty() {
map.insert("srcdoc_preview".into(), json!(preview));
}
}
if let Some(name) = attrs.iter().find(|(n, _)| n == "name") {
map.insert("name".into(), json!(name.1));
}
if let Some(sandbox) = attrs.iter().find(|(n, _)| n == "sandbox") {
map.insert("sandbox".into(), json!(sandbox.1));
}
if let Some(allow) = attrs.iter().find(|(n, _)| n == "allow") {
map.insert("allow".into(), json!(allow.1));
}
// Dimensions can be useful for understanding iframe purpose
if let Some(width) = attrs.iter().find(|(n, _)| n == "width") {
map.insert("width".into(), json!(width.1));
}
if let Some(height) = attrs.iter().find(|(n, _)| n == "height") {
map.insert("height".into(), json!(height.1));
}
}
_ => {}
}

Expand Down
3 changes: 3 additions & 0 deletions src/som/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ pub enum ElementRole {
Separator,
/// A `<details>`/`<summary>` disclosure widget.
Details,
/// An `<iframe>` embedded browsing context.
Iframe,
}

impl ElementRole {
Expand Down Expand Up @@ -147,6 +149,7 @@ impl ElementRole {
ElementRole::Section => "section",
ElementRole::Separator => "separator",
ElementRole::Details => "details",
ElementRole::Iframe => "iframe",
}
}
}
41 changes: 41 additions & 0 deletions tests/fixtures/iframe_page.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Page with Iframes</title>
</head>
<body>
<main>
<h1>Iframe Test Page</h1>

<!-- Basic iframe with src -->
<iframe
src="https://example.com/embedded"
title="Embedded Content"
width="800"
height="600"
></iframe>

<!-- Iframe with sandbox and name -->
<iframe
src="https://maps.example.com/embed"
name="map-frame"
sandbox="allow-scripts allow-same-origin"
allow="geolocation"
></iframe>

<!-- Iframe with srcdoc (inline content) -->
<iframe
srcdoc="<h1>Inline Content</h1><p>This is embedded directly.</p>"
title="Inline Frame"
></iframe>

<!-- Hidden iframe (should still be detected) -->
<iframe
src="https://analytics.example.com/pixel"
width="0"
height="0"
style="display:none"
></iframe>
</main>
</body>
</html>
79 changes: 79 additions & 0 deletions tests/som_compiler_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,3 +611,82 @@ fn test_inline_html_compiles() {
assert!(som.regions.iter().any(|r| r.role == RegionRole::Navigation));
assert!(som.regions.iter().any(|r| r.role == RegionRole::Main));
}

// ============================================================
// Iframe Support Tests
// ============================================================

#[test]
fn test_iframe_detection() {
let html = load_fixture("iframe_page.html");
let som = compiler::compile(&html, "https://example.com").unwrap();

let elems = all_elements(&som);
let iframes: Vec<&&Element> = elems.iter().filter(|e| e.role == ElementRole::Iframe).collect();

// Should detect at least 3 visible iframes (the hidden one may or may not be stripped)
assert!(
iframes.len() >= 3,
"Expected >=3 iframes, found {}",
iframes.len()
);
}

#[test]
fn test_iframe_attributes() {
let html = load_fixture("iframe_page.html");
let som = compiler::compile(&html, "https://example.com").unwrap();

let elems = all_elements(&som);
let iframes: Vec<&&Element> = elems.iter().filter(|e| e.role == ElementRole::Iframe).collect();

// Check that at least one iframe has src attribute
let has_src = iframes.iter().any(|e| {
e.attrs
.as_ref()
.and_then(|a| a.get("src"))
.is_some()
});
assert!(has_src, "At least one iframe should have src attribute");

// Check that srcdoc iframe is detected
let has_srcdoc = iframes.iter().any(|e| {
e.attrs
.as_ref()
.and_then(|a| a.get("has_srcdoc"))
.and_then(|v| v.as_bool())
.unwrap_or(false)
});
assert!(has_srcdoc, "Should detect iframe with srcdoc");

// Check sandbox attribute is captured
let has_sandbox = iframes.iter().any(|e| {
e.attrs
.as_ref()
.and_then(|a| a.get("sandbox"))
.is_some()
});
assert!(has_sandbox, "Should capture sandbox attribute");
}

#[test]
fn test_iframe_inline() {
// Test inline HTML with iframe
let html = r#"<html><head><title>Inline Iframe</title></head><body>
<main>
<iframe src="https://embed.example.com" name="test-frame" width="640" height="480"></iframe>
</main>
</body></html>"#;
let som = compiler::compile(html, "https://example.com").unwrap();

let elems = all_elements(&som);
let iframe = elems.iter().find(|e| e.role == ElementRole::Iframe);
assert!(iframe.is_some(), "Should find iframe element");

let iframe = iframe.unwrap();
let attrs = iframe.attrs.as_ref().expect("Iframe should have attrs");
assert_eq!(attrs.get("src").and_then(|v| v.as_str()), Some("https://embed.example.com"));
assert_eq!(attrs.get("name").and_then(|v| v.as_str()), Some("test-frame"));
assert_eq!(attrs.get("width").and_then(|v| v.as_str()), Some("640"));
assert_eq!(attrs.get("height").and_then(|v| v.as_str()), Some("480"));
}
4 changes: 2 additions & 2 deletions website/docs/src/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Plasmate's roadmap is public and standards-first. We ship compression and correc
- [ ] Parallel sessions at scale (500+ concurrent per 8GB)
- [x] Proxy support (HTTP, HTTPS, SOCKS5 with auth)
- [x] Proxy rotation (pool management, sticky sessions)
- [ ] Iframe support
- [x] Iframe support
- [ ] Shadow DOM support
- [ ] Full ES module support
- [ ] Chrome extension on Web Store
- [x] Chrome extension on Web Store
Loading