Files
doctate/server/tests/responsive_test.rs
T
Brummel a37a1ae363 feat(web): make server WebUI usable on phone viewports
The server-rendered pages worked on desktop but were near-unusable on a
phone: no viewport meta tag (mobile browsers laid out in a ~980px canvas
and zoomed out, rendering everything tiny) and a fixed `max-width: 900px`
desktop body with zero responsive breakpoints.

Fix is CSS-only and purely additive:
- Add `<meta name="viewport" content="width=device-width, initial-scale=1">`
  to all four page templates (each owns its own <head> — askama uses
  import/include here, no shared base layout, so the tag lands in 4 places).
- Append a single `@media (max-width: 640px)` block to the three
  desktop-width pages (my_cases, case_recordings, case_page): case rows
  and action bars reflow to a single column, the audio player goes
  full-width, and primary touch targets (delete/reopen/play) get a 44px
  minimum. login.html already has a 360px column layout, so it needs the
  viewport tag only.

Desktop layout is preserved by construction: a `max-width: 640px` query
never matches at >=900px, so the desktop render is byte-identical. The
diff adds lines only — no existing rule is touched.

Breakpoint chosen at 640px (well below the 900px body width) so all
portrait phones, including the ~390px target, fall under it while
tablets keep the desktop layout.

New integration test server/tests/responsive_test.rs pins the two
textual acceptance criteria that survive without a headless browser:
every page ships the viewport meta, and the three desktop-width pages
ship an @media breakpoint. The visual criteria (operable at 390px, 44px
touch targets) are CSS-driven and verified by eye — deliberately not
covered by a browser-automation dependency.

Verified: cargo test -p doctate-server --test responsive_test (4/4
green); web/login/case_page regression suites green; diff is additive.

closes #10
2026-05-30 15:44:53 +02:00

117 lines
3.7 KiB
Rust

//! Responsive WebUI regression pins for issue #10.
//!
//! These guard the two *textual* acceptance criteria that survive
//! without a headless browser: every page ships a `width=device-width`
//! viewport meta tag, and the three desktop-width pages (cases list,
//! case page, recordings) carry at least one `@media` breakpoint so the
//! layout can reflow on narrow viewports. The visual criteria (operable
//! at 390px, 44px touch targets) are verified by eye; these tests stop a
//! future template rewrite from silently dropping the viewport tag or
//! the breakpoints.
mod common;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::util::ServiceExt;
use common::{TestConfig, body_string, get_with_cookie, paths, seed_case, test_user};
const VIEWPORT: &str = "width=device-width";
#[tokio::test]
async fn login_page_has_viewport_meta() {
let cfg = TestConfig::new()
.with_label("resp-login")
.with_user(test_user("dr_a"))
.build();
let app = doctate_server::create_router(cfg);
let resp = app
.oneshot(
Request::builder()
.uri(paths::LOGIN)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(body.contains(VIEWPORT), "login page missing viewport meta");
}
#[tokio::test]
async fn cases_list_has_viewport_and_media_query() {
let cfg = TestConfig::new()
.with_label("resp-cases")
.with_user(test_user("dr_a"))
.build();
let app = doctate_server::create_router(cfg);
let cookie = common::login(&app, "dr_a", "s").await;
let resp = app
.oneshot(get_with_cookie(paths::CASES, &cookie))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(body.contains(VIEWPORT), "cases list missing viewport meta");
assert!(
body.contains("@media"),
"cases list missing @media breakpoint"
);
}
#[tokio::test]
async fn case_page_has_viewport_and_media_query() {
let cfg = TestConfig::new()
.with_label("resp-cp")
.with_user(test_user("dr_a"))
.with_llm("http://unused")
.build();
let case_id = "22222222-2222-2222-2222-222222222222";
seed_case(&cfg.data_path, "dr_a", case_id);
let app = doctate_server::create_router(cfg);
let cookie = common::login(&app, "dr_a", "s").await;
let resp = app
.oneshot(get_with_cookie(paths::case_detail(case_id), &cookie))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(body.contains(VIEWPORT), "case page missing viewport meta");
assert!(
body.contains("@media"),
"case page missing @media breakpoint"
);
}
#[tokio::test]
async fn case_recordings_has_viewport_and_media_query() {
let cfg = TestConfig::new()
.with_label("resp-rec")
.with_user(test_user("dr_a"))
.build();
let case_id = "33333333-3333-3333-3333-333333333333";
seed_case(&cfg.data_path, "dr_a", case_id);
let app = doctate_server::create_router(cfg);
let cookie = common::login(&app, "dr_a", "s").await;
let resp = app
.oneshot(get_with_cookie(paths::case_recordings(case_id), &cookie))
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = body_string(resp).await;
assert!(
body.contains(VIEWPORT),
"recordings page missing viewport meta"
);
assert!(
body.contains("@media"),
"recordings page missing @media breakpoint"
);
}