//! 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"); } } }