aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-client/src
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-06-10 16:50:55 +0200
committerGitHub <noreply@github.com>2026-06-10 16:50:55 +0200
commitb6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0 (patch)
tree38c77a381cfb937b428752a588c09a45c81434c9 /zellij-client/src
parentfeat: PWA support for the web client (#5184) (diff)
downloadsl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.tar.gz
sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.tar.xz
sl0ppij-b6a5ad0e5a93d9f584b3f7a0fc90f497a09cbdb0.zip
Mobile layout (#5241)
* phase 2+3 * initial mobile plugin (phases 4 + 5 + 6) * some bugfixes * implement stage 7 * mobile interface improvements * improve mobile pane/tab picker * more ui improvements and fixes * various mobile fixes * some mobile fixes * mobile keyboard triggering fix * some cleanups * initial plugin keyboard implementation * improve keyboard hit zones * improve keyboard * pinch resize * pinching fixes * implement scrolling + panning * initial keyboard resizing * fix: mobile first render issues * keyboard responsiveness * fix some issues with the keyboard * implement fit * fix mobile scrolling * add tilde cell to compact * improve mobile keyboard * fix starting mobile terminal font to natural keyboard size * fix mobile resize blank screen flash * shadow focus for mobile * more keyboard fixes * improve mobile status bar * fix: render plugins to mobile clients * allow opening new panes/tabs from the mobile plugin * allow configuring * fix dot slash * improve mobile native soft keyboard interaction * switch to native keyboard - initial implementation * dirty hacks, but phone keyboard works now * adjust keyboard help strip * fix cursor in mobile * fix keyboard and help line * rename keyboard to modifier bar * allow starting new session * make clicking session name bring us to new session menu * allow switching to desktop from the mobile ui * mobile welcome screen initial implementation * mobile welcome screen fixes * various welcome screen fixes * more mobile fixes * moar mobile fixes * mobile ui redesign * fix: swallowed backspace bug * switch session screen improvement * touch up some interfaces * refactor: move mobile state to own struct * refactor: move fit resizing to the server * remove dead flash code * some cleanups * refactor: clear fit on close * refactor: improve fit api * refactor: pane sync functions * refactor: mouse * refactor: break into modules * some cleanups for the sessions screen * fix scrolling and other stuff * refactor: sessions screen * refactor: panes screen * refactor: new session screen * refactor: menu screen * refactor: render * cleanups * refactor: plugin structure * remove unused welcome screen path * Rename api * refactor input.js * cleanup useless comments * fit/mobile refactoring * take out the garbage * remove more comments * remove mobile flash on startup/attach * fix: various mobile startup issues * fix space bugs * cleanup * rustfmt * fix windows build * adjust modifier bar ui * add pr
Diffstat (limited to 'zellij-client/src')
-rw-r--r--zellij-client/src/input_handler.rs6
-rw-r--r--zellij-client/src/lib.rs8
-rw-r--r--zellij-client/src/unit/terminal_loop_tests.rs42
-rw-r--r--zellij-client/src/web_client/control_message.rs9
-rw-r--r--zellij-client/src/web_client/server_listener.rs5
-rw-r--r--zellij-client/src/web_client/unit/web_client_tests.rs179
-rw-r--r--zellij-client/src/web_client/websocket_handlers.rs22
7 files changed, 60 insertions, 211 deletions
diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs
index cc4b4d75..45e8d9ce 100644
--- a/zellij-client/src/input_handler.rs
+++ b/zellij-client/src/input_handler.rs
@@ -46,6 +46,10 @@ fn termwiz_mouse_convert(original_event: &mut MouseEvent, event: &TermwizMouseEv
&& button_bits.contains(MouseButtons::WHEEL_POSITIVE);
original_event.wheel_down = button_bits.contains(MouseButtons::VERT_WHEEL)
&& !button_bits.contains(MouseButtons::WHEEL_POSITIVE);
+ original_event.wheel_right = button_bits.contains(MouseButtons::HORZ_WHEEL)
+ && button_bits.contains(MouseButtons::WHEEL_POSITIVE);
+ original_event.wheel_left = button_bits.contains(MouseButtons::HORZ_WHEEL)
+ && !button_bits.contains(MouseButtons::WHEEL_POSITIVE);
let mods = &event.modifiers;
original_event.shift = mods.contains(Modifiers::SHIFT);
@@ -73,6 +77,8 @@ pub fn from_termwiz(old_event: &mut MouseEvent, event: TermwizMouseEvent) -> Mou
|| (new_event.middle && !old_event.middle)
|| new_event.wheel_up
|| new_event.wheel_down
+ || new_event.wheel_left
+ || new_event.wheel_right
{
// This is a mouse Press event.
new_event.event_type = MouseEventType::Press;
diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs
index a1fcf387..d674e022 100644
--- a/zellij-client/src/lib.rs
+++ b/zellij-client/src/lib.rs
@@ -156,7 +156,7 @@ use zellij_utils::{
envs,
errors::{ClientContext, ContextType, ErrorInstruction},
input::{cli_assets::CliAssets, config::Config, options::Options},
- ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg},
+ ipc::{ClientToServerMsg, ExitReason, ResizeCause, ServerToClientMsg},
pane_size::Size,
vendored::termwiz::input::InputEvent,
};
@@ -214,6 +214,7 @@ impl From<ServerToClientMsg> for ClientInstruction {
// Subscribe-only messages — not handled by regular interactive clients
ServerToClientMsg::PaneRenderUpdate { .. } => ClientInstruction::UnblockInputThread,
ServerToClientMsg::SubscribedPaneClosed { .. } => ClientInstruction::UnblockInputThread,
+ ServerToClientMsg::SetSoftKeyboard { .. } => ClientInstruction::UnblockInputThread,
}
}
}
@@ -596,6 +597,9 @@ pub async fn run_remote_client_terminal_loop(
Ok(WebServerToWebClientControlMessage::SwitchedSession{ .. }) => {
// no-op
}
+ Ok(WebServerToWebClientControlMessage::SetSoftKeyboard{ .. }) => {
+ // no-op
+ }
Err(e) => {
log::error!("Failed to deserialize control message: {}", e);
}
@@ -1051,6 +1055,7 @@ pub fn start_client(
move || {
os_api.send_to_server(ClientToServerMsg::TerminalResize {
new_size: os_api.get_terminal_size(),
+ cause: ResizeCause::Viewport,
});
}
}),
@@ -1192,6 +1197,7 @@ pub fn start_client(
ClientInstruction::QueryTerminalSize => {
os_input.send_to_server(ClientToServerMsg::TerminalResize {
new_size: os_input.get_terminal_size(),
+ cause: ResizeCause::Viewport,
});
},
ClientInstruction::StartWebServer => {
diff --git a/zellij-client/src/unit/terminal_loop_tests.rs b/zellij-client/src/unit/terminal_loop_tests.rs
index 6dfe96bf..f6d3fade 100644
--- a/zellij-client/src/unit/terminal_loop_tests.rs
+++ b/zellij-client/src/unit/terminal_loop_tests.rs
@@ -20,7 +20,6 @@ use zellij_utils::errors::ErrorContext;
use zellij_utils::ipc::{ClientToServerMsg, ServerToClientMsg};
use zellij_utils::pane_size::Size;
-/// Mock stdin that allows tests to inject input data
struct MockAsyncStdin {
rx: Arc<tokio::sync::Mutex<mpsc::UnboundedReceiver<Vec<u8>>>>,
}
@@ -30,12 +29,11 @@ impl AsyncStdin for MockAsyncStdin {
async fn read(&mut self) -> io::Result<Vec<u8>> {
match self.rx.lock().await.recv().await {
Some(data) => Ok(data),
- None => Ok(Vec::new()), // EOF
+ None => Ok(Vec::new()),
}
}
}
-/// Mock signal listener that allows tests to inject signals
struct MockAsyncSignals {
rx: Arc<tokio::sync::Mutex<mpsc::UnboundedReceiver<SignalEvent>>>,
}
@@ -47,7 +45,6 @@ impl AsyncSignals for MockAsyncSignals {
}
}
-/// Mock ClientOsApi for testing
#[derive(Clone)]
struct TestClientOsApi {
stdout_buffer: Arc<Mutex<Vec<u8>>>,
@@ -223,7 +220,6 @@ mod mock_ws_server {
axum::serve(listener, app).await.unwrap();
});
- // Wait for server to start
tokio::time::sleep(Duration::from_millis(100)).await;
let server = MockWsServer {
@@ -257,7 +253,6 @@ mod mock_ws_server {
let recv_task = tokio::spawn(async move {
while let Some(Ok(axum_msg)) = ws_rx.next().await {
- // Convert axum::extract::ws::Message to tokio_tungstenite::tungstenite::Message
let tungstenite_msg = match axum_msg {
axum::extract::ws::Message::Text(text) => Message::Text(text.to_string()),
axum::extract::ws::Message::Binary(data) => Message::Binary(data.to_vec()),
@@ -282,7 +277,6 @@ mod mock_ws_server {
let msg = state.server_rx.lock().unwrap().try_recv();
match msg {
Ok(tungstenite_msg) => {
- // Convert tokio_tungstenite::tungstenite::Message to axum::extract::ws::Message
let axum_msg = match tungstenite_msg {
Message::Text(text) => axum::extract::ws::Message::Text(text.into()),
Message::Binary(data) => axum::extract::ws::Message::Binary(data.into()),
@@ -300,7 +294,7 @@ mod mock_ws_server {
axum::extract::ws::Message::Close(None)
}
},
- Message::Frame(_) => continue, // Skip raw frames
+ Message::Frame(_) => continue,
};
if ws_tx.send(axum_msg).await.is_err() {
break;
@@ -319,10 +313,8 @@ mod mock_ws_server {
#[tokio::test]
#[serial]
async fn test_stdin_forwarded_to_terminal_websocket() {
- // Setup mock WebSocket server
let (port, server, _server_handle) = mock_ws_server::MockWsServer::start().await;
- // Create WebSocket connections
let terminal_url = format!("ws://127.0.0.1:{}/ws/terminal", port);
let control_url = format!("ws://127.0.0.1:{}/ws/control", port);
@@ -353,21 +345,17 @@ async fn test_stdin_forwarded_to_terminal_websocket() {
web_client_id: "test-stdin".to_string(),
};
- // Create mock OS API with controllable stdin
let (stdin_tx, stdin_rx) = mpsc::unbounded_channel();
let (_signal_tx, signal_rx) = mpsc::unbounded_channel();
let os_input = Box::new(TestClientOsApi::new(stdin_rx, signal_rx));
- // Spawn the async loop
let loop_handle =
tokio::spawn(async move { run_remote_client_terminal_loop(os_input, connections).await });
- // Send stdin data
let test_data = b"hello from stdin\n".to_vec();
stdin_tx.send(test_data.clone()).unwrap();
- // Verify terminal WebSocket received the data
tokio::time::sleep(Duration::from_millis(200)).await;
let received = tokio::time::timeout(
Duration::from_secs(1),
@@ -382,7 +370,6 @@ async fn test_stdin_forwarded_to_terminal_websocket() {
_ => panic!("Expected Binary message, got: {:?}", received),
}
- // Cleanup: send EOF via stdin
drop(stdin_tx);
let _ = tokio::time::timeout(Duration::from_secs(2), loop_handle)
.await
@@ -435,7 +422,6 @@ async fn test_terminal_output_written_to_stdout() {
let loop_handle =
tokio::spawn(async move { run_remote_client_terminal_loop(os_input, connections).await });
- // Send terminal output from server
let test_output = "Hello from terminal";
server
.terminal_to_client_tx
@@ -444,7 +430,6 @@ async fn test_terminal_output_written_to_stdout() {
tokio::time::sleep(Duration::from_millis(200)).await;
- // Verify stdout received the output
let stdout = stdout_buffer.lock().unwrap().clone();
let stdout_str = String::from_utf8_lossy(&stdout);
assert!(
@@ -454,7 +439,6 @@ async fn test_terminal_output_written_to_stdout() {
stdout_str
);
- // Cleanup
server
.terminal_to_client_tx
.send(Message::Close(None))
@@ -508,22 +492,18 @@ async fn test_resize_signal_sends_control_message() {
let loop_handle =
tokio::spawn(async move { run_remote_client_terminal_loop(os_input, connections).await });
- // Wait for initial resize message to be sent on startup
tokio::time::sleep(Duration::from_millis(200)).await;
- // Consume the initial resize message
let _ = tokio::time::timeout(
Duration::from_millis(500),
server.client_to_control_rx.lock().unwrap().recv(),
)
.await;
- // Send resize signal
signal_tx.send(SignalEvent::Resize).unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
- // Verify control WebSocket received resize message
let received = tokio::time::timeout(
Duration::from_secs(1),
server.client_to_control_rx.lock().unwrap().recv(),
@@ -548,7 +528,6 @@ async fn test_resize_signal_sends_control_message() {
_ => panic!("Expected Text message, got: {:?}", received),
}
- // Cleanup
signal_tx.send(SignalEvent::Quit).unwrap();
let _ = tokio::time::timeout(Duration::from_secs(2), loop_handle)
.await
@@ -599,10 +578,8 @@ async fn test_quit_signal_exits_loop() {
let loop_handle =
tokio::spawn(async move { run_remote_client_terminal_loop(os_input, connections).await });
- // Send quit signal
signal_tx.send(SignalEvent::Quit).unwrap();
- // Verify loop exits cleanly
let result = tokio::time::timeout(Duration::from_secs(2), loop_handle)
.await
.expect("Loop didn't exit within timeout")
@@ -654,13 +631,11 @@ async fn test_websocket_close_exits_loop() {
let loop_handle =
tokio::spawn(async move { run_remote_client_terminal_loop(os_input, connections).await });
- // Send close message
server
.terminal_to_client_tx
.send(Message::Close(None))
.unwrap();
- // Verify loop exits cleanly
let result = tokio::time::timeout(Duration::from_secs(2), loop_handle)
.await
.expect("Loop didn't exit within timeout")
@@ -714,17 +689,14 @@ async fn test_control_message_handling() {
let loop_handle =
tokio::spawn(async move { run_remote_client_terminal_loop(os_input, connections).await });
- // Wait for initial resize message to be sent on startup
tokio::time::sleep(Duration::from_millis(200)).await;
- // Consume the initial resize message
let _ = tokio::time::timeout(
Duration::from_millis(500),
server.client_to_control_rx.lock().unwrap().recv(),
)
.await;
- // Send QueryTerminalSize control message
let query_msg = WebServerToWebClientControlMessage::QueryTerminalSize;
server
.control_to_client_tx
@@ -733,7 +705,6 @@ async fn test_control_message_handling() {
tokio::time::sleep(Duration::from_millis(200)).await;
- // Verify we receive a resize response
let received = tokio::time::timeout(
Duration::from_secs(1),
server.client_to_control_rx.lock().unwrap().recv(),
@@ -746,16 +717,16 @@ async fn test_control_message_handling() {
Message::Text(text) => {
let parsed: WebClientToWebServerControlMessage =
serde_json::from_str(&text).expect("Failed to parse");
- let WebClientToWebServerControlMessagePayload::TerminalResize(size) = parsed.payload
- else {
- panic!("Expected TerminalResize, got: {:?}", parsed.payload);
+ let size = match parsed.payload {
+ WebClientToWebServerControlMessagePayload::TerminalResize(size) => size,
+ WebClientToWebServerControlMessagePayload::TerminalResizeRendering(size) => size,
+ other => panic!("Expected TerminalResize, got: {:?}", other),
};
assert_eq!(size, terminal_size);
},
_ => panic!("Expected Text message, got: {:?}", received),
}
- // Test Log message (should not crash)
let log_msg = WebServerToWebClientControlMessage::Log {
lines: vec!["Test log".to_string()],
};
@@ -766,7 +737,6 @@ async fn test_control_message_handling() {
tokio::time::sleep(Duration::from_millis(100)).await;
- // Cleanup
server
.terminal_to_client_tx
.send(Message::Close(None))
diff --git a/zellij-client/src/web_client/control_message.rs b/zellij-client/src/web_client/control_message.rs
index e6c46444..8ab333f9 100644
--- a/zellij-client/src/web_client/control_message.rs
+++ b/zellij-client/src/web_client/control_message.rs
@@ -12,7 +12,10 @@ pub struct WebClientToWebServerControlMessage {
#[serde(tag = "type")]
pub enum WebClientToWebServerControlMessagePayload {
TerminalResize(Size),
+ TerminalResizeRendering(Size),
+ TerminalSizeSettled(Size),
TerminalMetrics(TerminalMetricsPayload),
+ SoftKeyboardVisibilityChanged { visible: bool },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -31,6 +34,7 @@ pub enum WebServerToWebClientControlMessage {
Log { lines: Vec<String> },
LogError { lines: Vec<String> },
SwitchedSession { new_session_name: String },
+ SetSoftKeyboard { on: bool },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
@@ -43,6 +47,8 @@ pub struct SetConfigPayload {
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor_style: Option<String>,
pub mac_option_is_meta: bool,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub font_size: Option<u16>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
@@ -159,6 +165,8 @@ impl From<&Config> for SetConfigPayload {
.as_ref()
.map(|s| s.to_string());
+ let font_size = config.web_client.font_size;
+
SetConfigPayload {
font,
theme,
@@ -166,6 +174,7 @@ impl From<&Config> for SetConfigPayload {
mac_option_is_meta,
cursor_style,
cursor_inactive_style,
+ font_size,
}
}
}
diff --git a/zellij-client/src/web_client/server_listener.rs b/zellij-client/src/web_client/server_listener.rs
index a3b419ef..9daa8fa8 100644
--- a/zellij-client/src/web_client/server_listener.rs
+++ b/zellij-client/src/web_client/server_listener.rs
@@ -178,6 +178,11 @@ pub fn zellij_server_listener(
WebServerToWebClientControlMessage::QueryTerminalSize,
);
},
+ Some(ServerToClientMsg::SetSoftKeyboard{on}) => {
+ client_connection_bus.send_control(
+ WebServerToWebClientControlMessage::SetSoftKeyboard { on },
+ );
+ },
Some(ServerToClientMsg::Log{lines}) => {
client_connection_bus.send_control(
WebServerToWebClientControlMessage::Log { lines },
diff --git a/zellij-client/src/web_client/unit/web_client_tests.rs b/zellij-client/src/web_client/unit/web_client_tests.rs
index 726e73a6..28c2e87c 100644
--- a/zellij-client/src/web_client/unit/web_client_tests.rs
+++ b/zellij-client/src/web_client/unit/web_client_tests.rs
@@ -44,7 +44,6 @@ mod web_client_tests {
.await
{
Ok(Ok(_)) => {
- // server ready
return Ok(());
},
Ok(Err(e)) => {
@@ -113,7 +112,6 @@ mod web_client_tests {
server_handle.abort();
- // time for cleanup
tokio::time::sleep(Duration::from_millis(100)).await;
}
@@ -190,7 +188,6 @@ mod web_client_tests {
server_handle.abort();
revoke_token(test_token_name).expect("Failed to revoke test token");
- // time for cleanup
tokio::time::sleep(Duration::from_millis(100)).await;
}
@@ -428,7 +425,7 @@ mod web_client_tests {
let messages = mock_api.get_sent_messages();
for msg in messages {
match msg {
- ClientToServerMsg::TerminalResize { new_size: _ } => {
+ ClientToServerMsg::TerminalResize { .. } => {
found_resize = true;
},
ClientToServerMsg::Key { .. }
@@ -465,12 +462,6 @@ mod web_client_tests {
#[tokio::test]
#[serial]
async fn test_terminal_metrics_translates_to_pixel_dimensions() {
- // Simulates a browser sending a TerminalMetrics control message
- // (the payload our websockets.js sendTerminalMetrics() helper
- // produces) and verifies the web server translates it into
- // ClientToServerMsg::TerminalPixelDimensions with field-for-field
- // accuracy. This guards the wire-format contract end-to-end
- // through serde + the match arm in websocket_handlers.rs.
let _ = delete_db();
let test_token_name = "test_token_terminal_metrics";
@@ -563,7 +554,6 @@ mod web_client_tests {
serde_json::from_str(&client_response.text().unwrap()).unwrap();
let web_client_id = client_data["web_client_id"].as_str().unwrap().to_string();
- // Open the control WebSocket and consume the initial SetConfig.
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws, _) = timeout(
Duration::from_secs(5),
@@ -577,9 +567,6 @@ mod web_client_tests {
.await
.expect("Timeout waiting for initial control message");
- // The control channel only registers the client_id after the
- // first inbound message. Send a TerminalResize first to mirror
- // what the browser does at startup, then the TerminalMetrics.
let resize_msg = WebClientToWebServerControlMessage {
web_client_id: web_client_id.clone(),
payload: WebClientToWebServerControlMessagePayload::TerminalResize(Size {
@@ -594,11 +581,6 @@ mod web_client_tests {
.await
.expect("Failed to send TerminalResize");
- // Send the actual TerminalMetrics payload — this is what the
- // browser-side sendTerminalMetrics() helper writes onto the wire.
- // Hand-construct the JSON to lock in the exact field names the
- // browser uses, rather than going through the serde Serialize
- // impl (which would mask any rename mismatch).
let metrics_json = serde_json::json!({
"web_client_id": web_client_id,
"payload": {
@@ -614,11 +596,8 @@ mod web_client_tests {
.await
.expect("Failed to send TerminalMetrics");
- // Give the server a moment to process both messages.
tokio::time::sleep(Duration::from_millis(500)).await;
- // Inspect the captured ClientToServerMsg traffic on the mock OS
- // API for this web client.
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
let mut found_pixel_dims: Option<ClientToServerMsg> = None;
for (_, mock_api) in mock_apis.iter() {
@@ -802,13 +781,10 @@ mod web_client_tests {
.await
.expect("Server failed to start");
- // Login and get session token
let session_token = login_and_get_session_token(port, &auth_token).await;
- // Create client session
let web_client_id = create_client_session(port, &session_token).await;
- // Establish control WebSocket connection
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws, _) = timeout(
Duration::from_secs(5),
@@ -820,12 +796,10 @@ mod web_client_tests {
let (mut control_sink, mut control_stream) = control_ws.split();
- // Wait for initial SetConfig message
let _initial_msg = timeout(Duration::from_secs(2), control_stream.next())
.await
.expect("Timeout waiting for initial control message");
- // Send resize message to establish proper connection
let resize_msg = WebClientToWebServerControlMessage {
web_client_id: web_client_id.clone(),
payload: WebClientToWebServerControlMessagePayload::TerminalResize(Size {
@@ -839,7 +813,6 @@ mod web_client_tests {
.await
.expect("Failed to send resize message");
- // Establish terminal WebSocket connection
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, web_client_id
@@ -854,10 +827,8 @@ mod web_client_tests {
let (_terminal_sink, mut terminal_stream) = terminal_ws.split();
- // Trigger server shutdown
server_handle.abort();
- // Verify control WebSocket receives close frame
let control_close_result = timeout(Duration::from_secs(3), control_stream.next()).await;
match control_close_result {
Ok(Some(Ok(Message::Close(_)))) => {
@@ -880,7 +851,6 @@ mod web_client_tests {
},
}
- // Verify terminal WebSocket receives close frame or connection ends
let terminal_close_result = timeout(Duration::from_secs(3), terminal_stream.next()).await;
match terminal_close_result {
Ok(Some(Ok(Message::Close(_)))) => {
@@ -904,7 +874,6 @@ mod web_client_tests {
}
revoke_token(test_token_name).expect("Failed to revoke test token");
- // time for cleanup
tokio::time::sleep(Duration::from_millis(100)).await;
}
@@ -949,14 +918,11 @@ mod web_client_tests {
.await
.expect("Server failed to start");
- // Login and get session token
let session_token = login_and_get_session_token(port, &auth_token).await;
- // Create multiple client sessions
let client_id_1 = create_client_session(port, &session_token).await;
let client_id_2 = create_client_session(port, &session_token).await;
- // Establish WebSocket connections for both clients
let control_ws_url_1 = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws_1, _) = timeout(
Duration::from_secs(5),
@@ -979,11 +945,9 @@ mod web_client_tests {
let (mut control_sink_2, mut control_stream_2) = control_ws_2.split();
- // Wait for initial messages and establish connections
let _initial_msg_1 = timeout(Duration::from_secs(2), control_stream_1.next()).await;
let _initial_msg_2 = timeout(Duration::from_secs(2), control_stream_2.next()).await;
- // Send messages to establish proper connections
let resize_msg_1 = WebClientToWebServerControlMessage {
web_client_id: client_id_1.clone(),
payload: WebClientToWebServerControlMessagePayload::TerminalResize(Size {
@@ -1010,7 +974,6 @@ mod web_client_tests {
.await
.expect("Failed to send resize message for client 2");
- // Establish terminal connections
let terminal_ws_url_1 = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, client_id_1
@@ -1025,7 +988,6 @@ mod web_client_tests {
let (_terminal_sink_1, _terminal_stream_1) = terminal_ws_1.split();
- // Verify both clients are initially present by checking mock APIs
tokio::time::sleep(Duration::from_millis(200)).await;
let initial_api_count = factory_for_verification.mock_apis.lock().unwrap().len();
assert!(
@@ -1033,13 +995,10 @@ mod web_client_tests {
"Should have at least 2 client APIs created"
);
- // Close connection for client 1 by closing WebSocket
let _ = control_sink_1.close().await;
- // Allow time for cleanup
tokio::time::sleep(Duration::from_millis(500)).await;
- // Verify client 2 is still functional by sending another message
let resize_msg_2_again = WebClientToWebServerControlMessage {
web_client_id: client_id_2.clone(),
payload: WebClientToWebServerControlMessagePayload::TerminalResize(Size {
@@ -1059,7 +1018,6 @@ mod web_client_tests {
Err(e) => println!("Client 2 send failed (may be expected): {:?}", e),
}
- // Verify messages were received by checking mock APIs
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
let mut total_resize_messages: usize = 0;
@@ -1123,11 +1081,9 @@ mod web_client_tests {
.await
.expect("Server failed to start");
- // Login and create session
let session_token = login_and_get_session_token(port, &auth_token).await;
let web_client_id = create_client_session(port, &session_token).await;
- // Establish terminal WebSocket connection
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, web_client_id
@@ -1142,16 +1098,13 @@ mod web_client_tests {
let (mut terminal_sink, mut terminal_stream) = terminal_ws.split();
- // Send some data to ensure connection is active and render loop is running
terminal_sink
.send(Message::Text("test input\n".to_string()))
.await
.expect("Failed to send terminal input");
- // Allow connection to stabilize and render loop to start
tokio::time::sleep(Duration::from_millis(500)).await;
- // Trigger shutdown by aborting server - this should trigger cancellation tokens
server_handle.abort();
let mut connection_terminated = false;
@@ -1188,15 +1141,12 @@ mod web_client_tests {
connection_terminated = true;
},
Err(_) => {
- // Timeout on this iteration, continue monitoring
println!("Timeout on stream.next(), continuing to monitor...");
},
}
}
- // If connection hasn't terminated through normal means, check if it's due to server shutdown
if !connection_terminated {
- // Try one more time to see if the connection is actually closed
match timeout(Duration::from_millis(100), terminal_stream.next()).await {
Ok(None) => {
println!("✓ Terminal WebSocket stream ended after server abort");
@@ -1210,8 +1160,6 @@ mod web_client_tests {
},
_ => {
println!("Connection still active after server abort - this may indicate the cancellation token isn't working as expected in test environment");
- // In test environment, server abort might not trigger cancellation tokens immediately
- // We'll consider the test successful if we've aborted the server
termination_reason = "server_aborted";
connection_terminated = true;
},
@@ -1269,11 +1217,9 @@ mod web_client_tests {
.await
.expect("Server failed to start");
- // Login and create session
let session_token = login_and_get_session_token(port, &auth_token).await;
let web_client_id = create_client_session(port, &session_token).await;
- // Establish terminal WebSocket connection
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, web_client_id
@@ -1288,19 +1234,15 @@ mod web_client_tests {
let (mut terminal_sink, mut terminal_stream) = terminal_ws.split();
- // Send terminal input to ensure connection is established
terminal_sink
.send(Message::Text("echo test\n".to_string()))
.await
.expect("Failed to send terminal input");
- // Allow connection to stabilize
tokio::time::sleep(Duration::from_millis(200)).await;
- // Create a mock API and simulate different exit scenarios by sending exit message
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
if let Some((_, mock_api)) = mock_apis.iter().next() {
- // Simulate ClientExited message being sent
mock_api
.messages_to_server
.lock()
@@ -1309,10 +1251,8 @@ mod web_client_tests {
}
drop(mock_apis);
- // Close the WebSocket connection to trigger cleanup
let _ = terminal_sink.close().await;
- // Monitor for connection termination
let close_result = timeout(Duration::from_secs(3), terminal_stream.next()).await;
match close_result {
Ok(Some(Ok(Message::Close(_)))) => {
@@ -1332,7 +1272,6 @@ mod web_client_tests {
},
}
- // Verify that ClientExited message was processed
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
let mut found_client_exited = false;
@@ -1353,7 +1292,6 @@ mod web_client_tests {
server_handle.abort();
revoke_token(test_token_name).expect("Failed to revoke test token");
- // time for cleanup
tokio::time::sleep(Duration::from_millis(100)).await;
}
@@ -1362,13 +1300,10 @@ mod web_client_tests {
async fn test_read_only_token_cannot_create_new_session() {
let _ = delete_db();
- // Create read-only token
let (auth_token, _) = create_token(Some("test-readonly".to_string()), true)
.expect("Failed to create read-only token");
- // Setup mocks
let mock_session_manager = Arc::new(MockSessionManager::new());
- // Do NOT mark any session as existing - we want to verify read-only cannot create new sessions
let mock_os_api_factory = Arc::new(MockClientOsApiFactory::new());
let session_manager_for_verification = mock_session_manager.clone();
@@ -1400,7 +1335,6 @@ mod web_client_tests {
.await
.expect("Server should start successfully");
- // Login and create client
let session_token = login_and_get_session_token(port, &auth_token).await;
let session_url = format!("http://127.0.0.1:{}/session", port);
@@ -1431,22 +1365,16 @@ mod web_client_tests {
assert_eq!(is_read_only, true, "Client should be marked as read-only");
- // Try to connect via control WebSocket
- // This will trigger the server_listener which should close the connection
- // because read-only client is trying to attach to non-existent session
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
- // The connection might fail or close immediately
let _ws_result = timeout(
Duration::from_secs(3),
connect_async_with_cookie(&control_ws_url, &session_token),
)
.await;
- // Give time for server_listener to process and close connection
tokio::time::sleep(Duration::from_millis(500)).await;
- // Verify no session was created
assert!(
!session_manager_for_verification.was_session_created("default"),
"No session should be created for read-only token attempting to create new session"
@@ -1462,11 +1390,9 @@ mod web_client_tests {
async fn test_read_only_token_uses_watcher_message_type() {
let _ = delete_db();
- // Create both regular and read-only tokens
let (regular_token, _) = create_token(Some("regular".to_string()), false).unwrap();
let (readonly_token, _) = create_token(Some("readonly".to_string()), true).unwrap();
- // Setup mocks - mark all sessions as existing so clients use AttachClient/AttachWatcherClient
let mock_session_manager = Arc::new(MockSessionManager::with_all_sessions_existing());
let session_manager_for_verification = mock_session_manager.clone();
@@ -1500,11 +1426,9 @@ mod web_client_tests {
.await
.expect("Server should start");
- // First, attach with REGULAR token to establish baseline
let regular_session_token = login_and_get_session_token(port, &regular_token).await;
let regular_web_client_id = create_client_session(port, &regular_session_token).await;
- // Connect control websocket
let regular_control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (regular_control_ws, _) = timeout(
Duration::from_secs(5),
@@ -1516,7 +1440,6 @@ mod web_client_tests {
let (mut regular_control_sink, _regular_control_stream) = regular_control_ws.split();
- // Connect terminal websocket to trigger server_listener
let regular_terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, regular_web_client_id
@@ -1531,11 +1454,8 @@ mod web_client_tests {
let (mut regular_terminal_sink, _regular_terminal_stream) = regular_terminal_ws.split();
- // Wait for attachment to complete
tokio::time::sleep(Duration::from_millis(500)).await;
- // VERIFY: Regular client should use AttachClient (not FirstClientConnected, since session exists)
- // Check what sessions we actually have
let regular_msg = {
let all_messages = session_manager_for_verification
.first_messages_sent
@@ -1559,11 +1479,9 @@ mod web_client_tests {
regular_msg
);
- // Now attach with READ-ONLY token
let readonly_session_token = login_and_get_session_token(port, &readonly_token).await;
let readonly_web_client_id = create_client_session(port, &readonly_session_token).await;
- // Connect control websocket
let readonly_control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (readonly_control_ws, _) = timeout(
Duration::from_secs(5),
@@ -1575,7 +1493,6 @@ mod web_client_tests {
let (mut readonly_control_sink, _readonly_control_stream) = readonly_control_ws.split();
- // Connect terminal websocket to trigger server_listener
let readonly_terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, readonly_web_client_id
@@ -1590,23 +1507,19 @@ mod web_client_tests {
let (mut readonly_terminal_sink, _readonly_terminal_stream) = readonly_terminal_ws.split();
- // Wait for attachment to complete
tokio::time::sleep(Duration::from_millis(500)).await;
- // VERIFY: Read-only client should use AttachWatcherClient
let readonly_msg = {
let all_messages = session_manager_for_verification
.first_messages_sent
.lock()
.unwrap();
- // Should have at least 2 messages now (regular + readonly)
assert!(
all_messages.len() >= 2,
"Should have at least 2 messages (regular and readonly)"
);
- // Get the second message (readonly client)
let (_readonly_session_name, msg) = all_messages
.get(1)
.expect("Should have message for read-only client");
@@ -1620,7 +1533,6 @@ mod web_client_tests {
readonly_msg
);
- // Verify the terminal size is passed correctly
if let ClientToServerMsg::AttachWatcherClient { terminal_size, .. } = readonly_msg {
assert!(terminal_size.rows > 0 && terminal_size.cols > 0);
}
@@ -1639,12 +1551,9 @@ mod web_client_tests {
async fn test_regular_token_uses_first_client_connected_for_new_session() {
let _ = delete_db();
- // Create regular token
let (regular_token, _) = create_token(Some("regular".to_string()), false).unwrap();
- // Setup mocks
let mock_session_manager = Arc::new(MockSessionManager::new());
- // Do NOT mark session as existing - we want to create a new one
let session_manager_for_verification = mock_session_manager.clone();
let mock_os_api_factory = Arc::new(MockClientOsApiFactory::new());
@@ -1676,11 +1585,9 @@ mod web_client_tests {
.await
.expect("Server should start");
- // Login and create client
let session_token = login_and_get_session_token(port, &regular_token).await;
let web_client_id = create_client_session(port, &session_token).await;
- // Connect control websocket
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws, _) = timeout(
Duration::from_secs(5),
@@ -1692,7 +1599,6 @@ mod web_client_tests {
let (mut control_sink, _control_stream) = control_ws.split();
- // Connect terminal websocket to trigger server_listener
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, web_client_id
@@ -1707,17 +1613,13 @@ mod web_client_tests {
let (mut terminal_sink, _terminal_stream) = terminal_ws.split();
- // Wait for session creation to complete
tokio::time::sleep(Duration::from_millis(500)).await;
- // VERIFY: Regular client creating new session should use FirstClientConnected
- // The session name will be "default" or a generated name
let all_messages = session_manager_for_verification
.first_messages_sent
.lock()
.unwrap();
- // Find the first message (should be FirstClientConnected)
let msg = all_messages
.first()
.map(|(_, msg)| msg)
@@ -1729,7 +1631,6 @@ mod web_client_tests {
msg
);
- // Verify session was marked as created
let sessions_created = session_manager_for_verification
.sessions_created
.lock()
@@ -1751,11 +1652,9 @@ mod web_client_tests {
async fn test_read_only_status_tracked_in_connection_table() {
let _ = delete_db();
- // Create tokens
let (regular_token, _) = create_token(Some("regular".to_string()), false).unwrap();
let (readonly_token, _) = create_token(Some("readonly".to_string()), true).unwrap();
- // Setup mocks
let mock_session_manager = Arc::new(MockSessionManager::new());
let mock_os_api_factory = Arc::new(MockClientOsApiFactory::new());
@@ -1787,7 +1686,6 @@ mod web_client_tests {
.await
.expect("Server should start");
- // Create multiple clients with different tokens
let regular_session_token = login_and_get_session_token(port, &regular_token).await;
let session_url = format!("http://127.0.0.1:{}/session", port);
@@ -1840,7 +1738,6 @@ mod web_client_tests {
serde_json::from_str(&readonly_response.text().unwrap()).unwrap();
let readonly_is_read_only = readonly_client_data["is_read_only"].as_bool().unwrap();
- // Verify is_read_only flag in responses
assert_eq!(
regular_is_read_only, false,
"Regular client should not be read-only"
@@ -1856,7 +1753,6 @@ mod web_client_tests {
tokio::time::sleep(Duration::from_millis(100)).await;
}
- // Helper function to login and get session token
async fn login_and_get_session_token(port: u16, auth_token: &str) -> String {
let login_url = format!("http://127.0.0.1:{}/command/login", port);
let login_payload = serde_json::json!({
@@ -1891,7 +1787,6 @@ mod web_client_tests {
.to_string()
}
- // Helper function to create client session
async fn create_client_session(port: u16, session_token: &str) -> String {
let session_url = format!("http://127.0.0.1:{}/session", port);
let mut client_response = timeout(
@@ -1964,7 +1859,6 @@ mod web_client_tests {
let session_token = login_and_get_session_token(port, &auth_token).await;
let web_client_id = create_client_session(port, &session_token).await;
- // Establish terminal WebSocket connection
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, web_client_id
@@ -1979,7 +1873,6 @@ mod web_client_tests {
let (_terminal_sink, mut terminal_stream) = terminal_ws.split();
- // Establish control WebSocket connection
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws, _) = timeout(
Duration::from_secs(5),
@@ -1991,7 +1884,6 @@ mod web_client_tests {
let (mut control_sink, mut control_stream) = control_ws.split();
- // Wait for initial SetConfig and send resize to register client_id on the control channel
let _initial_msg = timeout(Duration::from_secs(2), control_stream.next()).await;
let resize_msg = WebClientToWebServerControlMessage {
web_client_id: web_client_id.clone(),
@@ -2005,10 +1897,8 @@ mod web_client_tests {
.await
.expect("Failed to send resize message");
- // Allow connection to stabilize
tokio::time::sleep(Duration::from_millis(200)).await;
- // Queue KickedByHost exit message into the mock API
{
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
assert!(
@@ -2022,8 +1912,6 @@ mod web_client_tests {
}
}
- // Wait for terminal WebSocket to close with code 4001
- // Use a polling loop to handle any non-close messages that may arrive first
let mut terminal_got_4001 = false;
let mut terminal_closed = false;
let deadline = tokio::time::Instant::now() + Duration::from_secs(8);
@@ -2040,7 +1928,6 @@ mod web_client_tests {
break;
},
Ok(Some(Ok(_other))) => {
- // skip non-close messages (e.g. buffered render data)
continue;
},
Ok(Some(Err(_))) | Ok(None) => {
@@ -2048,7 +1935,6 @@ mod web_client_tests {
break;
},
Err(_) => {
- // timeout on this iteration, continue polling
continue;
},
}
@@ -2062,7 +1948,6 @@ mod web_client_tests {
"Terminal WebSocket should close with code 4001 when kicked by host"
);
- // Wait for control WebSocket to close with code 4001 (skip any pending non-Close messages)
let mut control_got_4001 = false;
let mut control_closed = false;
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
@@ -2079,7 +1964,6 @@ mod web_client_tests {
break;
},
Ok(Some(Ok(_msg))) => {
- // Skip non-Close messages
continue;
},
Ok(Some(Err(_))) | Ok(None) => {
@@ -2148,7 +2032,6 @@ mod web_client_tests {
let session_token = login_and_get_session_token(port, &auth_token).await;
let web_client_id = create_client_session(port, &session_token).await;
- // Establish terminal WebSocket connection
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, web_client_id
@@ -2163,7 +2046,6 @@ mod web_client_tests {
let (_terminal_sink, mut terminal_stream) = terminal_ws.split();
- // Establish control WebSocket connection
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws, _) = timeout(
Duration::from_secs(5),
@@ -2175,7 +2057,6 @@ mod web_client_tests {
let (mut control_sink, mut control_stream) = control_ws.split();
- // Wait for initial SetConfig and send resize to register client_id on the control channel
let _initial_msg = timeout(Duration::from_secs(2), control_stream.next()).await;
let resize_msg = WebClientToWebServerControlMessage {
web_client_id: web_client_id.clone(),
@@ -2189,10 +2070,8 @@ mod web_client_tests {
.await
.expect("Failed to send resize message");
- // Allow connection to stabilize
tokio::time::sleep(Duration::from_millis(200)).await;
- // Queue Normal exit message into the mock API
{
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
assert!(
@@ -2206,8 +2085,6 @@ mod web_client_tests {
}
}
- // Wait for terminal WebSocket to close with NORMAL (1000) close code
- // Use a polling loop to handle any non-close messages that may arrive first
let mut terminal_got_normal = false;
let mut terminal_closed = false;
let deadline = tokio::time::Instant::now() + Duration::from_secs(8);
@@ -2220,7 +2097,6 @@ mod web_client_tests {
break;
},
Ok(Some(Ok(Message::Close(None)))) => {
- // Close with no code also counts as a normal close
terminal_got_normal = true;
terminal_closed = true;
break;
@@ -2262,9 +2138,6 @@ mod web_client_tests {
),
tokio_tungstenite::tungstenite::Error,
> {
- // Manually construct WebSocket request with required headers since we need to add a custom cookie.
- // When building the request manually, we must include all the standard WebSocket handshake headers
- // that would normally be added automatically by the WebSocket client library.
let request = Request::builder()
.uri(url)
.header("Cookie", format!("session_token={}", session_token))
@@ -2278,8 +2151,6 @@ mod web_client_tests {
connect_async(request).await
}
- // ========== Task 1: Security Headers Tests ==========
-
#[tokio::test]
#[serial]
async fn test_security_headers_present_on_all_responses() {
@@ -2362,7 +2233,6 @@ mod web_client_tests {
csp.contains("default-src 'self'"),
"CSP should contain default-src 'self'"
);
- // HSTS should NOT be present when is_https is false
assert!(
response
.headers()
@@ -2458,14 +2328,11 @@ mod web_client_tests {
tokio::time::sleep(Duration::from_millis(100)).await;
}
- // ========== Task 10: web_client_id binding tests ==========
-
#[tokio::test]
#[serial]
async fn test_control_websocket_rejects_foreign_web_client_id() {
let _ = delete_db();
- // Create a read-write token and a read-only token
let (rw_token, _) = create_token(Some("rw_token".to_string()), false).unwrap();
let (ro_token, _) = create_token(Some("ro_token".to_string()), true).unwrap();
@@ -2501,15 +2368,12 @@ mod web_client_tests {
.await
.expect("Server failed to start");
- // Login with read-write token and create a client session
let rw_session_token = login_and_get_session_token(port, &rw_token).await;
let rw_web_client_id = create_client_session(port, &rw_session_token).await;
- // Login with read-only token and create a client session
let ro_session_token = login_and_get_session_token(port, &ro_token).await;
let _ro_web_client_id = create_client_session(port, &ro_session_token).await;
- // Connect a control WebSocket using the read-only session token
let control_ws_url = format!("ws://127.0.0.1:{}/ws/control", port);
let (control_ws, _) = timeout(
Duration::from_secs(5),
@@ -2521,10 +2385,8 @@ mod web_client_tests {
let (mut control_sink, mut control_stream) = control_ws.split();
- // Wait for initial SetConfig message
let _initial_msg = timeout(Duration::from_secs(2), control_stream.next()).await;
- // Send a TerminalResize message with the READ-WRITE user's web_client_id
let resize_msg = WebClientToWebServerControlMessage {
web_client_id: rw_web_client_id.clone(),
payload: WebClientToWebServerControlMessagePayload::TerminalResize(Size {
@@ -2538,18 +2400,11 @@ mod web_client_tests {
.await
.expect("Failed to send resize message");
- // The WebSocket should be closed by the server
let close_result = timeout(Duration::from_secs(3), control_stream.next()).await;
match close_result {
- Ok(Some(Ok(Message::Close(_)))) | Ok(Some(Err(_))) | Ok(None) => {
- // Expected: connection was closed
- },
- Err(_) => {
- // Timeout - connection may have been silently dropped
- },
+ Ok(Some(Ok(Message::Close(_)))) | Ok(Some(Err(_))) | Ok(None) => {},
+ Err(_) => {},
Ok(Some(Ok(_))) => {
- // If we get another message, the connection wasn't closed - this is a failure
- // but let's check if it closes after
let second_result = timeout(Duration::from_secs(2), control_stream.next()).await;
assert!(
matches!(
@@ -2561,7 +2416,6 @@ mod web_client_tests {
},
}
- // Verify that NO TerminalResize message was forwarded on behalf of the rw client
tokio::time::sleep(Duration::from_millis(200)).await;
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
for (_, mock_api) in mock_apis.iter() {
@@ -2634,10 +2488,8 @@ mod web_client_tests {
let (mut control_sink, mut control_stream) = control_ws.split();
- // Wait for initial SetConfig message
let _initial_msg = timeout(Duration::from_secs(2), control_stream.next()).await;
- // Send a TerminalResize message with the CORRECT web_client_id
let resize_msg = WebClientToWebServerControlMessage {
web_client_id: web_client_id.clone(),
payload: WebClientToWebServerControlMessagePayload::TerminalResize(Size {
@@ -2651,10 +2503,8 @@ mod web_client_tests {
.await
.expect("Failed to send resize message");
- // Allow message processing
tokio::time::sleep(Duration::from_millis(500)).await;
- // Verify the resize was forwarded
let mock_apis = factory_for_verification.mock_apis.lock().unwrap();
let mut found_resize = false;
for (_, mock_api) in mock_apis.iter() {
@@ -2715,14 +2565,12 @@ mod web_client_tests {
.await
.expect("Server failed to start");
- // Create two sessions with different tokens
let rw_session_token = login_and_get_session_token(port, &rw_token).await;
let rw_web_client_id = create_client_session(port, &rw_session_token).await;
let ro_session_token = login_and_get_session_token(port, &ro_token).await;
let _ro_web_client_id = create_client_session(port, &ro_session_token).await;
- // Try to connect terminal WebSocket using ro session token but rw web_client_id
let terminal_ws_url = format!(
"ws://127.0.0.1:{}/ws/terminal?web_client_id={}",
port, rw_web_client_id
@@ -2735,25 +2583,15 @@ mod web_client_tests {
match ws_result {
Ok(Ok((ws, _))) => {
- // Connection was established but should be closed immediately
let (_sink, mut stream) = ws.split();
let msg = timeout(Duration::from_secs(2), stream.next()).await;
match msg {
- Ok(Some(Ok(Message::Close(_)))) | Ok(Some(Err(_))) | Ok(None) | Err(_) => {
- // Expected: connection was rejected/closed
- },
- Ok(Some(Ok(_))) => {
- // Unexpected: received data on a connection that should have been rejected
- // The server may close after initial processing
- },
+ Ok(Some(Ok(Message::Close(_)))) | Ok(Some(Err(_))) | Ok(None) | Err(_) => {},
+ Ok(Some(Ok(_))) => {},
}
},
- Ok(Err(_)) => {
- // Connection failed - expected behavior
- },
- Err(_) => {
- // Timeout - acceptable
- },
+ Ok(Err(_)) => {},
+ Err(_) => {},
}
server_handle.abort();
@@ -2761,8 +2599,6 @@ mod web_client_tests {
tokio::time::sleep(Duration::from_millis(100)).await;
}
- // ========== Task 8: HTML-escape base_url test ==========
-
#[tokio::test]
#[serial]
async fn test_base_url_with_special_characters_is_escaped() {
@@ -3150,7 +2986,6 @@ impl SessionManager for MockSessionManager {
_zellij_ipc_pipe: &PathBuf,
first_message: ClientToServerMsg,
) {
- // Track the message that was sent
self.first_messages_sent
.lock()
.unwrap()
diff --git a/zellij-client/src/web_client/websocket_handlers.rs b/zellij-client/src/web_client/websocket_handlers.rs
index 969911cc..11c81a9e 100644
--- a/zellij-client/src/web_client/websocket_handlers.rs
+++ b/zellij-client/src/web_client/websocket_handlers.rs
@@ -21,7 +21,7 @@ use std::sync::{atomic::AtomicBool, Arc};
use tokio_util::sync::CancellationToken;
use zellij_utils::{
input::mouse::MouseEvent,
- ipc::{ClientToServerMsg, PixelDimensions},
+ ipc::{ClientToServerMsg, PixelDimensions, ResizeCause},
pane_size::SizeInPixels,
};
@@ -76,11 +76,29 @@ async fn handle_ws_control(
};
let client_msg = match deserialized_msg.payload {
WebClientToWebServerControlMessagePayload::TerminalResize(size) => {
- ClientToServerMsg::TerminalResize { new_size: size }
+ ClientToServerMsg::TerminalResize {
+ new_size: size,
+ cause: ResizeCause::Viewport,
+ }
+ },
+ WebClientToWebServerControlMessagePayload::TerminalResizeRendering(size) => {
+ ClientToServerMsg::TerminalResize {
+ new_size: size,
+ cause: ResizeCause::RenderingPreference,
+ }
+ },
+ WebClientToWebServerControlMessagePayload::TerminalSizeSettled(size) => {
+ ClientToServerMsg::TerminalResize {
+ new_size: size,
+ cause: ResizeCause::SizeSettled,
+ }
},
WebClientToWebServerControlMessagePayload::TerminalMetrics(metrics) => {
terminal_metrics_to_ipc(metrics)
},
+ WebClientToWebServerControlMessagePayload::SoftKeyboardVisibilityChanged {
+ visible,
+ } => ClientToServerMsg::SoftKeyboardVisibilityChanged { visible },
};
let _ = client_connection.send_to_server(client_msg);