This commit contains the latest updates from the generic Website template, which move the toolchain to Swift 6.4, gate analytics and structured data behind traits, and rework page routing: - Updated the Swift toolchain for all the package dependencies and the Website service to version 6.4. (345b305) - Adapted all the existing middlewares in the Infrastructure package to Swift 6.4. (26008df) - Moved test resource teardown in the Persistence and the Website library into defer blocks. (27984a9) - Gated analytics behind a trait and reworked structured data in the Infrastructure package. (c7d4769) - Enabled the Analytics trait in the Website service. (f6a6bbd) - Reworked page routing in the Website service. (d1cb2c1) - Gated the structured data features in the Infrastructure package behind a trait. (f0a8791) - Enabled the StructuredData trait of the Infrastructure package in the Website service. (bbfdeb3) - Fixed the asset pin and a build warning in the Dockerfile of the Website service. (6ed0f9b) The root README change in 345b305 was left out, as this project no longer carries that file. From d1cb2c1, the RootController rework was left out, as the prototype serves a session-aware landing page and routes its other pages through their own controllers; the view-event reporter was appended to the prototype's shared script instead. The prototype code was adapted to the stricter Swift 6.4 settings: `String(format:)` was swapped for safe formatting, the NoIndex middleware takes a `@concurrent` next handler, and the tests flagged by warnings-as-errors were tightened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
197 lines
8.5 KiB
Swift
197 lines
8.5 KiB
Swift
import Elementary
|
||
import Foundation
|
||
import Persistence
|
||
import Testing
|
||
|
||
@testable import WebsiteLibrary
|
||
|
||
@Suite(
|
||
"Account and collector pages",
|
||
.tags(.page)
|
||
)
|
||
struct CollectorPagesTests {
|
||
|
||
// MARK: Constants
|
||
|
||
private static let nightward = Series(slug: "nightward", name: "Nightward", publisher: "Meridian Comics", years: "2019–2021", issueCount: 4, blurb: "A short run.")
|
||
|
||
// MARK: OpenShopPage tests
|
||
|
||
@Test
|
||
func `open-a-shop form starts as a private seller, and gives back what was typed with its problems`() {
|
||
let empty = OpenShopPage().render()
|
||
let sent = OpenShopPage(
|
||
form: ShopFormTests.form(["kind": "business", "about": "Spare <copies>"]),
|
||
problems: [.kvk: "Enter your 8-digit KvK number."]
|
||
).render()
|
||
|
||
#expect(empty.contains(#"action="/sell/open-a-shop" method="post""#))
|
||
#expect(empty.contains(#"name="kind" value="private" checked"#))
|
||
#expect(!empty.contains(#"role="alert""#))
|
||
#expect(sent.contains("One thing to fix before opening."))
|
||
#expect(sent.contains(#"name="kind" value="business" checked"#))
|
||
#expect(sent.contains(#"name="name" value="Ana's Longbox""#) || sent.contains(#"name="name" value="Ana's Longbox""#))
|
||
#expect(sent.contains(#"id="kvk-error">Enter your 8-digit KvK number."#))
|
||
#expect(sent.contains("Spare <copies>"))
|
||
#expect(!sent.contains("style="))
|
||
}
|
||
|
||
// MARK: WelcomePage and AccountPage tests
|
||
|
||
@Test
|
||
func `welcome screen is bare, asks for a name, and carries the intent on`() {
|
||
let html = WelcomePage(displayName: "ana.devries", returnPath: "/listing/nightward-1").render()
|
||
|
||
#expect(html.contains("You're in.") || html.contains("You're in."))
|
||
#expect(html.contains(#"action="/welcome" method="post""#))
|
||
#expect(html.contains(#"name="name" value="ana.devries""#))
|
||
#expect(html.contains(#"name="return" value="/listing/nightward-1""#))
|
||
#expect(html.contains(#"content="noindex""#))
|
||
#expect(!html.contains("site-header"))
|
||
}
|
||
|
||
@Test
|
||
func `account screen lists the passkeys with a way to revoke each, and to add one`() {
|
||
let passkey = Passkey(credentialID: "cred-1", publicKey: [], signCount: 0, user: .mara)
|
||
let html = AccountPage(user: .mara, passkeys: [(passkey, .stub)], viewer: .init(.mara), now: .stub).render()
|
||
let bare = AccountPage(user: .mara, passkeys: [], now: .stub).render()
|
||
|
||
#expect(html.contains(#"action="/account/name" method="post""#))
|
||
#expect(html.contains(#"name="name" value="Mara Visser""#))
|
||
#expect(html.contains("Passkey 1"))
|
||
#expect(html.contains("created today, 09:14"))
|
||
#expect(html.contains(#"name="credential" value="cred-1""#))
|
||
#expect(html.contains(#"hidden data-passkey="add""#))
|
||
#expect(html.contains("/js/passkey.js"))
|
||
#expect(html.contains(#"href="/sell/open-a-shop">Open a shop"#))
|
||
#expect(bare.contains("No passkey yet"))
|
||
}
|
||
|
||
// MARK: WantsPage tests
|
||
|
||
@Test
|
||
func `want list shows each want's limits, its cheapest match, and what is new`() {
|
||
let want = Want(
|
||
id: .init(uuidString: "00000000-0000-0000-0000-0000000000EE") ?? .init(),
|
||
title: "Nightward #1", minGrade: 8, maxPriceCents: 30_000, slabbedOnly: true, alerts: true,
|
||
matches: [.stub(slug: "a", priceCents: 24_000), .stub(slug: "b", priceCents: 21_000)],
|
||
newMatchCount: 1
|
||
)
|
||
let html = WantsPage(wants: [want]).render()
|
||
|
||
#expect(html.contains("1 want · 1 new matches"))
|
||
#expect(html.contains("slabbed only · grade ≥ 8.0 · max €300 · ships from EU only"))
|
||
#expect(html.contains(#"href="/listing/b">From €210"#))
|
||
#expect(html.contains(#"class="wants__new">1 new"#))
|
||
#expect(html.contains(#"role="switch" aria-checked="true""#))
|
||
#expect(html.contains(#"action="/wants/00000000-0000-0000-0000-0000000000ee/remove""#))
|
||
#expect(html.contains(#"href="/search?q=Nightward%20%231&grade-min=8.0&price-max=300&grading=slabbed""#))
|
||
#expect(html.contains(#"href="/wants/new""#))
|
||
}
|
||
|
||
@Test
|
||
func `want list fills its form from what is being saved`() {
|
||
let html = WantsPage(wants: [], draft: .init(title: "Nightward #2", minGrade: 9.6, maxPriceCents: 8_500)).render()
|
||
|
||
#expect(html.contains(#"name="title" value="Nightward #2""#))
|
||
#expect(html.contains(#"<option value="9.6" selected>"#))
|
||
#expect(html.contains(#"name="max-price" value="85""#))
|
||
#expect(!html.contains("No wants yet"))
|
||
}
|
||
|
||
// MARK: SeriesPage tests
|
||
|
||
@Test
|
||
func `series shows every issue of the run, the gaps one tap from an alert`() {
|
||
let html = SeriesPage(
|
||
series: Self.nightward,
|
||
listings: [.stub(keyNotes: "First appearance"), .stub(slug: "cheap", priceCents: 9_000)]
|
||
).render()
|
||
|
||
#expect(html.contains("All 4 issues"))
|
||
#expect(html.contains("2 copies listed across CCN right now."))
|
||
#expect(html.contains(#"href="/series/nightward?issue=1#copies">2 listed · from €90"#))
|
||
#expect(html.components(separatedBy: "series__issue--gap").count - 1 == 3)
|
||
// Signed out, an alert leads through the sign-in and back.
|
||
#expect(html.contains(#"href="/sign-in?return=/series/nightward">None listed · alert me"#))
|
||
#expect(html.contains("#1 · First appearance"))
|
||
#expect(!html.contains(#"id="copies""#))
|
||
}
|
||
|
||
@Test
|
||
func `series lists an issue's copies cheapest delivered first, ready to compare`() throws {
|
||
let html = SeriesPage(
|
||
series: Self.nightward,
|
||
listings: [.stub(), .stub(slug: "cheap", priceCents: 9_000, defects: ["Foxing / staining"])],
|
||
issue: 1,
|
||
viewer: .init(displayName: "Mara")
|
||
).render()
|
||
let cheap = try #require(html.range(of: #"value="cheap""#))
|
||
let dear = try #require(html.range(of: #"value="nightward-1""#))
|
||
|
||
#expect(html.contains("Copies of #1 — 2 listed"))
|
||
#expect(html.contains(#"action="/compare" method="get""#))
|
||
#expect(html.contains("Foxing / staining"))
|
||
#expect(html.contains(#"href="/wants/new?title=Nightward%20%232">None listed · alert me"#))
|
||
|
||
#expect(cheap.lowerBound < dear.lowerBound)
|
||
}
|
||
|
||
@Test
|
||
func `series known only from its listings shows the issues listed, without gaps`() {
|
||
let html = SeriesPage(
|
||
series: .init(slug: "nightward", name: "Nightward", publisher: "Meridian Comics", years: "2019", issueCount: 0, blurb: ""),
|
||
listings: [.stub()]
|
||
).render()
|
||
|
||
#expect(html.contains("Issues listed"))
|
||
#expect(!html.contains("series__issue--gap"))
|
||
}
|
||
|
||
// MARK: ComparePage tests
|
||
|
||
@Test
|
||
func `comparison orders copies by what they cost delivered, and flags a customs border`() throws {
|
||
let abroad = Shop(
|
||
slug: "longbox", name: "Longbox Atlantic", city: "Bristol", country: "GB", sinceYear: 2024, isVerified: false,
|
||
rating: 4.6, ratedSales: 58, soldCount: 71, handlingDays: 4, shipsNote: "", returnsNote: "returns by agreement", about: ""
|
||
)
|
||
let html = ComparePage(copies: [.stub(), .stub(slug: "abroad", shop: abroad, priceCents: 15_000)]).render()
|
||
let first = try #require(html.range(of: "€156"))
|
||
let second = try #require(html.range(of: "€246"))
|
||
|
||
#expect(html.contains("customs may apply"))
|
||
#expect(html.components(separatedBy: "customs may apply").count - 1 == 1)
|
||
#expect(html.contains(#"href="/sign-in?return=/listing/abroad">Buy now"#))
|
||
#expect(html.contains("58 · 4.6 · returns by agreement"))
|
||
|
||
#expect(first.lowerBound < second.lowerBound)
|
||
}
|
||
|
||
@Test
|
||
func `comparison of nothing says where to pick copies`() {
|
||
#expect(ComparePage(copies: []).render().contains("Nothing to compare"))
|
||
}
|
||
|
||
// MARK: Shared tests
|
||
|
||
@Test
|
||
func `no account or collector page renders an inline style`() {
|
||
let pages = [
|
||
OpenShopPage().render(),
|
||
WelcomePage(displayName: "ana", returnPath: "/").render(),
|
||
AccountPage(user: .mara, passkeys: [], now: .stub).render(),
|
||
WantsPage(wants: [], draft: .init()).render(),
|
||
SeriesPage(series: Self.nightward, listings: [.stub()], issue: 1).render(),
|
||
SeriesIndexPage(entries: [(Self.nightward, [.stub()])]).render(),
|
||
ComparePage(copies: [.stub()]).render(),
|
||
]
|
||
|
||
for html in pages {
|
||
#expect(!html.contains("style="))
|
||
#expect(!html.contains("<style"))
|
||
}
|
||
}
|
||
|
||
}
|