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: 2 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ extend-ignore-re = [
astroid = "astroid"
# Correct Italian singular of "command".
comando = "comando"
# Correct Portuguese word for "current" in localized UI labels.
atual = "atual"
# A deliberately misspelled manifest key: `crates/shell/src/plugin.rs` refuses
# it by name, and the test that proves it has to contain the misspelling.
capabilites = "capabilites"
Expand Down
44 changes: 40 additions & 4 deletions crates/base/src/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ fn advance_focus(window: &mut Window, cx: &mut App, advance: fn(&mut Window, &mu
};

advance(window, cx);
let first_candidate = window.focused(cx);
// GPUI's tab order can cycle short of the whole window (a stop whose
// previous stop is itself), so the walk ends at the first repeated stop,
// not only on a return to the one it began with.
let mut visited = Vec::new();
while !container.contains_focused(window, cx) {
advance(window, cx);
if window.focused(cx) == first_candidate {
// The trap has no rendered tab stops. Its own handle remains a
let focused = window.focused(cx);
if visited.contains(&focused) {
// No tab stop in the trap is reachable. Its own handle remains a
// safe focus target even when excluded from ordinary tab order.
container.focus(window, cx);
break;
}
visited.push(focused);
advance(window, cx);
}
}

Expand Down Expand Up @@ -427,6 +432,37 @@ mod tests {
assert_eq!(background_focuses.get(), 0);
}

thread_local! {
static SCRIPTED_STOPS: std::cell::RefCell<Vec<FocusHandle>> =
const { std::cell::RefCell::new(Vec::new()) };
}

/// Moves focus to the next scripted stop, in place of `focus_prev`. Running
/// out fails the test, so a search that never stops cannot hang it.
fn scripted_advance(window: &mut Window, cx: &mut App) {
SCRIPTED_STOPS
.with(|stops| stops.borrow_mut().pop())
.expect("the search kept advancing after a stop repeated")
.focus(window, cx);
}

#[gpui::test]
fn focus_trap_search_ends_at_a_short_cycle_on_its_container(cx: &mut TestAppContext) {
let (cx, container, controls, background, _) = trap_harness(cx, 2);
cx.update(|window, cx| {
controls[0].focus(window, cx);
window.draw(cx).clear(cx);
});
// A tab order in which the second stop is its own neighbor never
// returns to the first. Popped from the end: first, second, second.
let (first, second) = (background[0].clone(), background[1].clone());
SCRIPTED_STOPS.with(|stops| *stops.borrow_mut() = vec![second.clone(), second, first]);

cx.update(|window, cx| advance_focus(window, cx, scripted_advance));
assert_trapped_focus(cx, &container, &container);
assert!(SCRIPTED_STOPS.with(|stops| stops.borrow().is_empty()));
}

#[gpui::test]
fn inactive_focus_trap_does_not_capture_background_navigation(cx: &mut TestAppContext) {
let (cx, _, _, background, background_focuses) = trap_harness(cx, 2);
Expand Down
17 changes: 14 additions & 3 deletions crates/shell/src/tests/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,9 +647,20 @@ fn websocket_sends_ordinary_and_custom_protocol_headers(cx: &mut TestAppContext)
Err(error) => panic!("WebSocket accept failed: {error}"),
})
.expect("WebSocket connection");
let mut request = [0; 4096];
let count = stream.read(&mut request).expect("handshake request");
let request = String::from_utf8_lossy(&request[..count]).to_ascii_lowercase();
// Accepted sockets can inherit the listener's nonblocking mode on macOS.
stream.set_nonblocking(false).expect("blocking connection");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.expect("handshake timeout");
// TCP may split the headers across reads.
let mut request = Vec::new();
while !request.ends_with(b"\r\n\r\n") {
let mut byte = [0];
stream.read_exact(&mut byte).expect("handshake request");
request.push(byte[0]);
assert!(request.len() <= 4096, "handshake request too large");
}
let request = String::from_utf8_lossy(&request).to_ascii_lowercase();
assert!(request.contains("accept-language: zh-cn\r\n"), "{request}");
assert!(
request.contains("user-agent: protocol-client/1\r\n"),
Expand Down
Loading