Skip to content

Commit ef4ce38

Browse files
authored
Merge pull request #105 from httpsphl/fix/reader-spaces
fix(browser): keep the spaces of words built one element per letter
2 parents b512b72 + 651abbb commit ef4ce38

4 files changed

Lines changed: 26 additions & 4 deletions

File tree

‎crates/botloftd/src/browser/reader.js‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@
165165
const walk = (node, parentCursor, depth) => {
166166
if (++nodes > MAX_NODES || depth > MAX_DEPTH) return;
167167
if (node.nodeType === Node.TEXT_NODE) {
168-
const text = node.nodeValue.replace(/\s+/g, " ");
169-
if (text.trim()) out.push(text);
168+
// A text node of spaces alone still parts words: pages that put
169+
// each letter in its own element keep their spaces that way.
170+
out.push(node.nodeValue.replace(/\s+/g, " "));
170171
return;
171172
}
172173
if (node.nodeType !== Node.ELEMENT_NODE) return;

‎crates/botloftd/tests/browser.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ async fn a_bot_fills_a_form_after_the_owner_allows_the_site() {
8989
assert!(closed.expect_err("closed").contains("not open"));
9090
}
9191

92+
#[tokio::test(flavor = "multi_thread")]
93+
async fn words_made_of_one_element_per_letter_keep_their_spaces() {
94+
let Some(mut b) = setup().await else { return };
95+
let opening = call(
96+
&b.mcp,
97+
"browser_open",
98+
json!({ "url": format!("{}/letters", b.site) }),
99+
);
100+
answer_site(&mut b.app, true, None).await;
101+
let page = opening.await.expect("task").expect("page");
102+
assert!(page.contains("Two words"), "{page}");
103+
}
104+
92105
#[tokio::test(flavor = "multi_thread")]
93106
async fn a_site_the_owner_denies_stays_closed() {
94107
let Some(mut b) = setup().await else { return };

‎crates/botloftd/tests/common/site.rs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
//! A small web site on 127.0.0.1 for the browser tests: a form, the page
22
//! it leads to, a link that opens a new tab, a button that shows a dialog,
33
//! a sign-in page with its fields at fixed points, for the owner's hands,
4-
//! a page that says how big its window is, one that counts its loads and
5-
//! one that counts the times it went out of sight.
4+
//! a page that says how big its window is, one that counts its loads, one
5+
//! that counts the times it went out of sight and one with each letter in
6+
//! its own element.
67
78
use std::collections::HashMap;
89
use std::net::SocketAddr;
@@ -25,6 +26,11 @@ const POPUP: &str = "<!doctype html><title>Links</title>\
2526
<a href=\"/done?name=Tab&size=Small\" target=\"_blank\">Open in a new tab</a>\
2627
<button onclick=\"alert('Hi there')\">Warn me</button>";
2728

29+
/// Each letter in a span, spaces too, as example.com's own script does.
30+
const LETTERS: &str = "<!doctype html><title>Letters</title><p><span>T</span><span>w</span>\
31+
<span>o</span><span> </span><span>w</span><span>o</span><span>r</span><span>d</span>\
32+
<span>s</span></p>";
33+
2834
/// The fields' middles: user (200, 115), password (200, 215).
2935
const LOGIN: &str = "<!doctype html><title>Sign in</title><style>body{margin:0}\
3036
input{position:absolute;left:100px;width:200px;height:30px;margin:0;padding:0;\
@@ -86,6 +92,7 @@ pub async fn serve() -> SocketAddr {
8692
.route("/login", get(|| async { Html(LOGIN) }))
8793
.route("/size", get(|| async { Html(SIZE) }))
8894
.route("/sight", get(|| async { Html(SIGHT) }))
95+
.route("/letters", get(|| async { Html(LETTERS) }))
8996
.route("/account", post(account));
9097
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
9198
let addr = listener.local_addr().expect("addr");

‎docs/spec.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,7 @@ Esqueceu a senha? [e5 link "Recuperar acesso"]
12611261
```
12621262

12631263
- Um script injetado (`Runtime.evaluate`) percorre o documento em ordem de leitura: texto visível, títulos com `#`, itens de lista com `-`, quebras de bloco, e cada controle entre colchetes com uma `ref` (`e<n>`), o papel (`link`, `button`, `textbox`, `checkbox`, `radio`, `select`, `clickable`...), o nome acessível (`aria-label`, `<label>`, texto, `placeholder`, `title`, `alt`) e o estado (valor, marcado, desabilitado). Senhas aparecem só como `(password)`.
1264+
- Um nó de texto só com espaços vira um espaço, e espaços seguidos viram um. Assim uma página que põe cada letra num elemento próprio, espaços incluídos (o script do example.com faz isso com o texto em inglês), não chega ao bot com as palavras grudadas.
12641265
- Entra o que está visível na página inteira, não só na tela: nada com `display: none`, `visibility: hidden`, `aria-hidden` ou tamanho zero. `iframe` do mesmo site e shadow DOM aberto entram; de outro site, só `[frame]`.
12651266
- Elementos clicáveis sem papel (um `div` com `cursor: pointer` que não herda o cursor do pai) viram `clickable`.
12661267
- A mesma ref aponta sempre para o mesmo elemento enquanto ele existir, também entre leituras.

0 commit comments

Comments
 (0)