2026-09-21 13:32:05 +02:00
|
|
|
|
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""#))
|
2026-09-21 13:53:13 +02:00
|
|
|
|
#expect(!html.contains("No wants yet"))
|
2026-09-21 13:32:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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`() {
|
|
|
|
|
|
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"#))
|
|
|
|
|
|
|
|
|
|
|
|
if let cheap, let dear {
|
|
|
|
|
|
#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`() {
|
|
|
|
|
|
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"))
|
|
|
|
|
|
|
|
|
|
|
|
if let first, let second {
|
|
|
|
|
|
#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"))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|