refactor(kanban): compose page with hemplate
Replace board page placeholder string replacement for options/board content with a hemplate page view and partial-rendered options. Cover page structure with DOM-aware scraper assertions. req: html_safety/002 req: view/001 req: test/005
This commit is contained in:
+58
-17
@@ -17,11 +17,11 @@ const COLUMNS: [(&str, &str); 3] = [("backlog", "Backlog"), ("doing", "Doing"),
|
||||
|
||||
#[derive(Default)]
|
||||
struct AppState {
|
||||
board: Mutex<Board>,
|
||||
board: Mutex<BoardState>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct Board {
|
||||
struct BoardState {
|
||||
next_id: u64,
|
||||
cards: Vec<Card>,
|
||||
}
|
||||
@@ -33,6 +33,25 @@ struct Card {
|
||||
column: usize,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
struct Board {
|
||||
options: SafeHtml,
|
||||
board: SafeHtml,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct ColumnOptions {
|
||||
options: Vec<ColumnOption>,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct ColumnOption {
|
||||
id: &'static str,
|
||||
title: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Hemplate)]
|
||||
#[hemplate = "partials"]
|
||||
struct Presence {
|
||||
@@ -42,7 +61,7 @@ struct Presence {
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let state = Arc::new(AppState {
|
||||
board: Mutex::new(Board {
|
||||
board: Mutex::new(BoardState {
|
||||
next_id: 4,
|
||||
cards: vec![
|
||||
Card { id: 1, title: "Write requirements".into(), column: 0 },
|
||||
@@ -153,7 +172,7 @@ fn registry(state: Arc<AppState>) -> HandlerRegistry {
|
||||
})
|
||||
}
|
||||
|
||||
fn board_effects(board: &Board, notice: &'static str) -> impl IntoEffect {
|
||||
fn board_effects(board: &BoardState, notice: &'static str) -> impl IntoEffect {
|
||||
(
|
||||
ui::board::slots::board.html(SafeHtml::trusted(render_board(board))),
|
||||
ui::board::slots::notice.text(notice),
|
||||
@@ -161,7 +180,7 @@ fn board_effects(board: &Board, notice: &'static str) -> impl IntoEffect {
|
||||
)
|
||||
}
|
||||
|
||||
fn update_card(board: &mut Board, card_id: Option<&str>, update: impl FnOnce(&mut Card)) -> bool {
|
||||
fn update_card(board: &mut BoardState, card_id: Option<&str>, update: impl FnOnce(&mut Card)) -> bool {
|
||||
let Some(id) = card_id.and_then(|id| id.parse::<u64>().ok()) else {
|
||||
return false;
|
||||
};
|
||||
@@ -180,10 +199,12 @@ fn parse_column(value: Option<&str>) -> usize {
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn page_html(board: &Board) -> String {
|
||||
ui::board::lower_html(include_str!("../templates/board.heml"))
|
||||
.replace("__OPTIONS__", &render_options())
|
||||
.replace("__BOARD__", &render_board(board))
|
||||
fn page_html(board: &BoardState) -> String {
|
||||
// req: html_safety/002 req: view/001
|
||||
ui::board::lower_html(render_template(&Board {
|
||||
options: render_options(),
|
||||
board: SafeHtml::trusted(render_board(board)),
|
||||
}))
|
||||
}
|
||||
|
||||
fn shell(body: String) -> String {
|
||||
@@ -213,15 +234,17 @@ fn shell(body: String) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_options() -> String {
|
||||
COLUMNS
|
||||
.iter()
|
||||
.map(|(id, title)| format!(r#"<option value="{}">{}</option>"#, escape_html(id), escape_html(title)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
fn render_options() -> SafeHtml {
|
||||
// req: html_safety/002 req: view/001
|
||||
render_html(&ColumnOptions {
|
||||
options: COLUMNS
|
||||
.iter()
|
||||
.map(|(id, title)| ColumnOption { id, title })
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_board(board: &Board) -> String {
|
||||
fn render_board(board: &BoardState) -> String {
|
||||
let mut out = String::from("<div class=\"columns\">");
|
||||
for (idx, (_, title)) in COLUMNS.iter().enumerate() {
|
||||
out.push_str(&format!(r#"<section class="column"><h2>{}</h2>"#, escape_html(title)));
|
||||
@@ -260,7 +283,11 @@ fn render_presence(count: u64) -> SafeHtml {
|
||||
}
|
||||
|
||||
fn render_html(template: &impl Hemplate) -> SafeHtml {
|
||||
SafeHtml::trusted(template.render().expect("kanban hemplate partial renders"))
|
||||
SafeHtml::trusted(render_template(template))
|
||||
}
|
||||
|
||||
fn render_template(template: &impl Hemplate) -> String {
|
||||
template.render().expect("kanban hemplate view renders")
|
||||
}
|
||||
|
||||
fn escape_html(value: &str) -> String {
|
||||
@@ -281,6 +308,20 @@ mod tests {
|
||||
Selector::parse(value).expect("test selector parses")
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn kanban_page_is_composed_by_a_hemplate_view() {
|
||||
let html = page_html(&BoardState::default());
|
||||
assert!(!html.contains("__OPTIONS__"));
|
||||
assert!(!html.contains("__BOARD__"));
|
||||
|
||||
let document = Html::parse_fragment(&html);
|
||||
assert_eq!(document.select(&selector("section[data-slhx-root=\"kanban\"]")).count(), 1);
|
||||
assert_eq!(document.select(&selector("select[name=\"column\"] > option")).count(), 3);
|
||||
assert_eq!(document.select(&selector("[data-sid]")).count(), 3);
|
||||
assert_eq!(document.select(&selector("[data-hid]")).count(), 4);
|
||||
}
|
||||
|
||||
// req: html_safety/002 req: view/001 req: test/005
|
||||
#[test]
|
||||
fn presence_payload_is_rendered_by_a_hemplate_view() {
|
||||
|
||||
Reference in New Issue
Block a user