//! The public creator profile, described.
//!
//! `/u/{username}`: who the creator is, what they have published, and the four
//! ways to keep up with them. It replaces `templates/pages/user.html` and
//! `UserTemplate`.
//!
//! The second of the three pages that wrote `` by hand,
//! after [`super::project_blog`]. The screen says it has a feed and
//! [`quasi_router::FeedKind::media_type`] spells it.
//!
//! # Why the route stays an axum handler
//!
//! [`super::project_blog`]'s reason. The handler resolves a username, refuses a
//! sandbox account, loads projects, links, collections and follow state, and
//! records a page view unless the caller is a crawler. None of that needs a
//! described route, and `super::Viewer` carries no session. What is described
//! is the document.
//!
//! # The creator's theme
//!
//! `user.html` injected `"),
"{rendered}"
);
}
/// The `ProfilePage` block the template opened, built from the same facts
/// and escaped for JSON rather than for HTML.
#[test]
fn the_page_still_describes_itself_to_a_crawler() {
let user = creator();
let rendered = html(&profile(&user));
assert!(rendered.contains("application/ld+json"), "{rendered}");
assert!(rendered.contains("\"@type\":\"ProfilePage\""), "{rendered}");
assert!(rendered.contains("\"name\":\"Max Johnson\""), "{rendered}");
assert!(
rendered.contains("\"url\":\"https://makenot.work/u/maxj\""),
"{rendered}"
);
}
/// Following is a write, so a signed-out reader gets the count and not the
/// control, and a creator does not follow themselves.
#[test]
fn only_a_reader_who_could_follow_is_offered_the_control() {
let user = creator();
let mut signed_out = profile(&user);
signed_out.follower_count = 4;
let rendered = html(&signed_out);
assert!(!rendered.contains("/api/follow/user/u1"), "{rendered}");
assert!(rendered.contains("4 followers"), "{rendered}");
let mut own = profile(&user);
own.signed_in = true;
own.is_own_profile = true;
let rendered = html(&own);
assert!(!rendered.contains("/api/follow/user/u1"), "{rendered}");
assert!(rendered.contains("Edit Profile"), "{rendered}");
let mut other = profile(&user);
other.signed_in = true;
let rendered = html(&other);
assert!(rendered.contains("/api/follow/user/u1"), "{rendered}");
}
/// A creator with no followers is not told so on their own page.
#[test]
fn a_profile_with_no_followers_says_nothing_about_it() {
let user = creator();
assert!(!html(&profile(&user)).contains("followers"));
}
/// `Act::copies` is what `data-copy-link` meant, and the value is absolute:
/// a relative path on a clipboard is not an address anybody can paste.
#[test]
fn the_share_control_copies_the_whole_address() {
let user = creator();
let rendered = html(&profile(&user));
assert!(
rendered.contains("data-copies=\"https://makenot.work/u/maxj\""),
"{rendered}"
);
}
/// A creator's off-site links leave the app and are expected to be come
/// back from, which is what the template's `target="_blank"` said.
#[test]
fn a_custom_link_opens_away_from_the_page() {
let user = creator();
let links = vec![CustomLink {
url: "https://example.com".to_owned(),
title: "My shop".to_owned(),
description: "Elsewhere".to_owned(),
}];
let mut with_links = profile(&user);
with_links.custom_links = &links;
let rendered = html(&with_links);
assert!(rendered.contains("https://example.com"), "{rendered}");
assert!(rendered.contains("target=\"_blank\""), "{rendered}");
assert!(rendered.contains("noopener"), "{rendered}");
}
/// Every project and every collection keeps its row and its address.
#[test]
fn the_published_work_keeps_its_rows_and_its_addresses() {
let user = creator();
let projects = vec![Project {
id: "p1".to_owned(),
slug: "blue-hour".to_owned(),
title: "Blue Hour".to_owned(),
description: String::new(),
item_count: 3,
project_type: "Music".to_owned(),
cover_image_url: None,
}];
let collections = vec![Collection {
id: "c1".to_owned(),
slug: "favourites".to_owned(),
title: "Favourites".to_owned(),
description: None,
is_public: true,
item_count: 2,
created_at: String::new(),
}];
let mut full = profile(&user);
full.projects = &projects;
full.collections = &collections;
let rendered = html(&full);
assert!(rendered.contains("Blue Hour"), "{rendered}");
assert!(rendered.contains("/p/blue-hour"), "{rendered}");
assert!(rendered.contains("3 items"), "{rendered}");
assert!(rendered.contains("Favourites"), "{rendered}");
assert!(rendered.contains("/c/maxj/favourites"), "{rendered}");
}
/// A creator on a break says so above their work, which is where the
/// template put it: a reader about to buy is the one who needs it.
#[test]
fn a_paused_creator_says_so_before_anything_they_published() {
let user = creator();
let mut paused = profile(&user);
paused.paused = true;
let rendered = html(&paused);
let notice = rendered.find("currently on break").expect("said");
let name = rendered.rfind("Max Johnson").expect("drawn");
assert!(notice < name, "the notice landed under the work");
}
/// A creator with no bio still gets a preview sentence, which is what the
/// template's `{% else %}` wrote.
#[test]
fn a_creator_with_no_bio_still_previews() {
let mut user = creator();
user.bio = None;
let rendered = html(&profile(&user));
assert!(
rendered.contains("content=\"Creator on Makenotwork\""),
"{rendered}"
);
}
/// `736f45a5`: none of the four spellings.
#[test]
fn the_page_spells_no_spinner() {
let user = creator();
let rendered = html(&profile(&user));
for spelling in ["htmx-indicator", "spinner", "loading-text", "loading-state"] {
assert!(!rendered.contains(spelling), "{spelling} survives");
}
}
}