From 9f44778f210d9d3bb94333d21a7388024f4f0d95 Mon Sep 17 00:00:00 2001 From: nnduc Date: Sat, 24 Oct 2026 23:27:39 +0700 Subject: [PATCH] fix profile page --- frontend/widgets/Avatar/avatar.css | 31 ++++++++++++++++++- src/app.rs | 9 ++---- src/tpl.rs | 24 +++++++++++++++ templates/Avatar/_avatar.html | 21 +++++-------- tests/e2e/browser_avatar.py | 49 ++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 tests/e2e/browser_avatar.py diff --git a/frontend/widgets/Avatar/avatar.css b/frontend/widgets/Avatar/avatar.css index 9c6894b..961a4db 100644 --- a/frontend/widgets/Avatar/avatar.css +++ b/frontend/widgets/Avatar/avatar.css @@ -9,7 +9,36 @@ font-size: 5rem; } +#avatar header.avatar_only { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 2rem 1rem 1rem; +} + +#avatar header.avatar_only span.primary { + margin: 0; + cursor: pointer; + background-size: cover; + background-position: center; +} + form[name=bannerform] img.avatar { max-width: calc(100% - 2rem); margin: 1rem; -} \ No newline at end of file +} +#avatar header.avatar_only { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 2rem 1rem 1rem; +} + +#avatar header.avatar_only span.primary { + margin: 0; + cursor: pointer; + background-size: cover; + background-position: center; +} diff --git a/src/app.rs b/src/app.rs index 1535961..f20c2dd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,6 +1,6 @@ //! Shared application state and the per-call context given to the widgets. -use minijinja::{context, Environment, Value as JValue}; +use minijinja::Environment; use serde::Serialize; use serde_json::{json, Value}; use std::sync::{Arc, Mutex, RwLock}; @@ -193,12 +193,7 @@ impl Ctx { return String::new(); } }; - let ctx = context! { - lang => self.lang(), - base_uri => self.base_uri(), - me => self.jid(), - ..JValue::from_serialize(&data) - }; + let ctx = crate::tpl::render_context(serde_json::to_value(&data).unwrap_or_default(), &self.lang(), &self.base_uri(), &self.jid()); match tpl.render(ctx) { Ok(s) => s.trim().to_string(), Err(e) => { diff --git a/src/tpl.rs b/src/tpl.rs index 3eb2e7e..58af27b 100644 --- a/src/tpl.rs +++ b/src/tpl.rs @@ -91,6 +91,16 @@ fn add_emojis(state: &State, s: String, no_title: Option) -> Value { Value::from_safe_string(utils::replace_emojis(&escaped, &base, no_title.unwrap_or(false)).html) } +/// The widget data wins over the defaults (the Avatar templates pass their own `me`) +pub fn render_context(mut data: serde_json::Value, lang: &str, base_uri: &str, me: &str) -> Value { + if let Some(map) = data.as_object_mut() { + for (k, v) in [("lang", lang), ("base_uri", base_uri), ("me", me)] { + map.entry(k).or_insert_with(|| v.into()); + } + } + Value::from_serialize(&data) +} + #[cfg(test)] mod tests { use super::*; @@ -116,4 +126,18 @@ mod tests { let html = tpl.render(data("vi")).unwrap(); assert!(html.contains("Ghi âm") && html.contains("Tệp đã chia sẻ")); } + + #[test] + fn profile_picture_header() { + let root = Path::new(env!("CARGO_MANIFEST_DIR")); + i18n::init(&root.join("frontend")); + let env = environment(&root.join("templates")); + let tpl = env.get_template("Avatar/_avatar.html").unwrap(); + let data = json!({ "me": { "picture": "/avatar/abc", "color": "yellow" } }); + let html = tpl.render(render_context(data, "en", "/", "alice@example.org")).unwrap(); + assert!(html.contains("url(/avatar/abc)") && html.contains("color yellow"), "{html}"); + // The defaults are still there + let html = env.render_str("{{ me }} {{ lang }}", render_context(json!({}), "vi", "/", "alice@example.org")).unwrap(); + assert_eq!(html, "alice@example.org vi"); + } } diff --git a/templates/Avatar/_avatar.html b/templates/Avatar/_avatar.html index a67baed..e0a0a4d 100644 --- a/templates/Avatar/_avatar.html +++ b/templates/Avatar/_avatar.html @@ -1,14 +1,9 @@ -
- -
+
+ + +
diff --git a/tests/e2e/browser_avatar.py b/tests/e2e/browser_avatar.py new file mode 100644 index 0000000..ff455b6 --- /dev/null +++ b/tests/e2e/browser_avatar.py @@ -0,0 +1,49 @@ +"""Set a profile picture and check it on the profile (configuration) page.""" +import asyncio, os +from playwright.async_api import async_playwright +from browser import login, BASE, OUT + +S = os.path.dirname(os.path.abspath(__file__)) + + +async def header(page): + return await page.evaluate("""() => { + const h = document.querySelector('#avatar header'); + const s = document.querySelector('#avatar header span.primary'); + return h ? [getComputedStyle(h).backgroundImage, s && getComputedStyle(s).backgroundImage, + h.getBoundingClientRect().height] : null; + }""") + + +async def main(): + async with async_playwright() as p: + browser = await p.chromium.launch() + ctx, page, errors = await login(browser, "alice@localhost", "alicepass", "avatar") + bad = [] + page.on("response", lambda r: r.status >= 400 and bad.append(f"{r.status} {r.url}")) + await page.goto(BASE + "/configuration") + await page.wait_for_timeout(4000) + print("before:", await header(page)) + await page.screenshot(path=f"{OUT}/avatar_1_before.png") + + await page.evaluate("Avatar_ajaxGetForm()") + await page.wait_for_selector("#dialog input[type=file]", state="attached", timeout=10000) + await page.set_input_files("#dialog input[type=file]", f"{S}/test.png") + await page.wait_for_timeout(2000) + print("photobin length:", await page.evaluate("document.querySelector('[name=photobin]').value.length")) + await page.screenshot(path=f"{OUT}/avatar_2_form.png") + await page.click("#dialog footer button:last-child") + await page.wait_for_timeout(4000) + print("after submit:", await header(page)) + await page.screenshot(path=f"{OUT}/avatar_3_after.png") + + await page.goto(BASE + "/configuration") + await page.wait_for_timeout(4000) + print("after reload:", await header(page)) + print("nav avatar:", await page.evaluate("[...document.querySelectorAll('nav img, nav span.primary')].map(e => e.src || getComputedStyle(e).backgroundImage).slice(0, 3)")) + await page.screenshot(path=f"{OUT}/avatar_4_reload.png") + for e in errors + bad: + print(e) + await browser.close() + +asyncio.run(main())