From 0f501d627cb2e83ad4f5ce802acd84a3960da405 Mon Sep 17 00:00:00 2001 From: Phelipe Lorran <112786693+httpsphl@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:08:04 -0300 Subject: [PATCH 1/2] fix(browser): words built one element per letter keep their spaces The page reader dropped text nodes made of spaces alone. example.com's script now puts each letter of its English text in its own span, spaces included, so the bot read "Thisdomainisforuse...". A text node of spaces is one space now; spaces in a row still collapse. --- crates/botloftd/src/browser/reader.js | 5 +++-- crates/botloftd/tests/browser.rs | 13 +++++++++++++ crates/botloftd/tests/common/site.rs | 11 +++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/botloftd/src/browser/reader.js b/crates/botloftd/src/browser/reader.js index b489f579..46f59c42 100644 --- a/crates/botloftd/src/browser/reader.js +++ b/crates/botloftd/src/browser/reader.js @@ -165,8 +165,9 @@ const walk = (node, parentCursor, depth) => { if (++nodes > MAX_NODES || depth > MAX_DEPTH) return; if (node.nodeType === Node.TEXT_NODE) { - const text = node.nodeValue.replace(/\s+/g, " "); - if (text.trim()) out.push(text); + // A text node of spaces alone still parts words: pages that put + // each letter in its own element keep their spaces that way. + out.push(node.nodeValue.replace(/\s+/g, " ")); return; } if (node.nodeType !== Node.ELEMENT_NODE) return; diff --git a/crates/botloftd/tests/browser.rs b/crates/botloftd/tests/browser.rs index 9b69d4e4..a04195ca 100644 --- a/crates/botloftd/tests/browser.rs +++ b/crates/botloftd/tests/browser.rs @@ -89,6 +89,19 @@ async fn a_bot_fills_a_form_after_the_owner_allows_the_site() { assert!(closed.expect_err("closed").contains("not open")); } +#[tokio::test(flavor = "multi_thread")] +async fn words_made_of_one_element_per_letter_keep_their_spaces() { + let Some(mut b) = setup().await else { return }; + let opening = call( + &b.mcp, + "browser_open", + json!({ "url": format!("{}/letters", b.site) }), + ); + answer_site(&mut b.app, true, None).await; + let page = opening.await.expect("task").expect("page"); + assert!(page.contains("Two words"), "{page}"); +} + #[tokio::test(flavor = "multi_thread")] async fn a_site_the_owner_denies_stays_closed() { let Some(mut b) = setup().await else { return }; diff --git a/crates/botloftd/tests/common/site.rs b/crates/botloftd/tests/common/site.rs index bca71719..d663d1bf 100644 --- a/crates/botloftd/tests/common/site.rs +++ b/crates/botloftd/tests/common/site.rs @@ -1,8 +1,9 @@ //! A small web site on 127.0.0.1 for the browser tests: a form, the page //! it leads to, a link that opens a new tab, a button that shows a dialog, //! a sign-in page with its fields at fixed points, for the owner's hands, -//! a page that says how big its window is, one that counts its loads and -//! one that counts the times it went out of sight. +//! a page that says how big its window is, one that counts its loads, one +//! that counts the times it went out of sight and one with each letter in +//! its own element. use std::collections::HashMap; use std::net::SocketAddr; @@ -25,6 +26,11 @@ const POPUP: &str = "
Tw\ + o word\ + s
"; + /// The fields' middles: user (200, 115), password (200, 215). const LOGIN: &str = "