Implemented phase 5 for the POC prototype in the Website service.
This commit is contained in:
1 parent
89a7a7b8e5
commit
31d2d98d49
46 files changed
+4039
-162
No files matched your search
+2
-2
@@ -40,11 +40,11 @@ struct CatalogueComponentsTests {
|
||||
// MARK: ListingCard tests
|
||||
|
||||
@Test
|
||||
func `links a card to its listing and gates saving behind the sign-in`() {
|
||||
func `links a card to its listing, and saving it to a want for it`() {
|
||||
let html = ListingCard(.stub()).render()
|
||||
|
||||
#expect(html.contains(#"href="/listing/nightward-1">Nightward #1</a>"#))
|
||||
#expect(html.contains(#"href="/sign-in?return=/listing/nightward-1""#))
|
||||
#expect(html.contains(#"href="/wants/new?listing=nightward-1""#))
|
||||
#expect(html.contains("€240"))
|
||||
#expect(html.contains("CGC 9.8"))
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ struct SiteFooterTests {
|
||||
#expect(html.contains(">\(title)</h2>"))
|
||||
}
|
||||
|
||||
for path in [String.Route.series, .Route.openShop, .Route.gradingGuide, .Route.signIn, .Route.recover, .Route.terms] {
|
||||
for path in [String.Route.series, .Route.openShop, .Route.gradingGuide, .Route.signIn, .Route.wants, .Route.account] {
|
||||
#expect(html.contains(#"href="\#(path)""#))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,13 +35,23 @@ struct SiteHeaderTests {
|
||||
#expect(html.contains(#"href="/sell""#))
|
||||
#expect(html.contains(">MV<"))
|
||||
#expect(html.contains(">Mara<"))
|
||||
#expect(html.contains(#"action="/sign-out" method="post""#))
|
||||
// Signing out lives on the account screen.
|
||||
#expect(!html.contains("/sign-out"))
|
||||
#expect(!html.contains(#"href="/sign-in""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `links the shop of a seller alone`() {
|
||||
#expect(!SiteHeader(viewer: .init(displayName: "Mara")).render().contains(#"href="/sell""#))
|
||||
func `leads a seller to their shop, and anyone else to opening one`() {
|
||||
let buyer = SiteHeader(viewer: .init(displayName: "Mara")).render()
|
||||
|
||||
#expect(!buyer.contains(#"href="/sell""#))
|
||||
#expect(buyer.contains(#"href="/sell/open-a-shop""#))
|
||||
#expect(buyer.contains(">Open a shop<"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `counts one new match in the singular`() {
|
||||
#expect(SiteHeader(viewer: .init(displayName: "Mara", newWantMatches: 1)).render().contains(#"aria-label="Want list, 1 new match""#))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -105,7 +105,8 @@ struct CataloguePagesTests {
|
||||
#expect(html.contains(action))
|
||||
}
|
||||
|
||||
#expect(html.components(separatedBy: #"href="/sign-in?return=/listing/nightward-1""#).count - 1 >= 5)
|
||||
// Buy, offer, message, and the note's own link. Saving goes through the want list, which gates itself.
|
||||
#expect(html.components(separatedBy: #"href="/sign-in?return=/listing/nightward-1""#).count - 1 == 4)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +135,8 @@ struct CataloguePagesTests {
|
||||
#expect(html.contains(#"href="/messages/new?listing=nightward-1""#))
|
||||
#expect(html.contains(#"aria-label="Account of Mara Visser""#))
|
||||
#expect(!html.contains("gate-note"))
|
||||
#expect(html.contains(#"disabled title="Not part of the prototype">Save"#))
|
||||
#expect(html.contains(#"href="/wants/new?listing=nightward-1""#))
|
||||
#expect(html.contains(#"href="/series/nightward?issue=1#copies""#))
|
||||
}
|
||||
|
||||
// MARK: ShopPage tests
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
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("Nothing on your list 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`() {
|
||||
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"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"ShopForm type",
|
||||
.tags(.type)
|
||||
)
|
||||
struct ShopFormTests {
|
||||
|
||||
// MARK: Methods tests
|
||||
|
||||
@Test
|
||||
func `describes a private seller's shop`() throws {
|
||||
let draft = try Self.form().validated().get()
|
||||
|
||||
#expect(draft.name == "Ana's Longbox")
|
||||
#expect(draft.handle == "anas-longbox")
|
||||
#expect(draft.city == "Leiden")
|
||||
#expect(draft.country == "NL")
|
||||
#expect(draft.kind == .privateSeller)
|
||||
#expect(draft.handlingDays == 2)
|
||||
#expect(draft.returnsNote == "14-day returns")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `asks a business for its registration, as Dutch consumer law does`() {
|
||||
#expect(Self.problems(["kind": "business"]) == [.kvk, .legalName])
|
||||
#expect(Self.problems(["kind": "business", "kvk": "1234", "legal-name": "Ana BV"]) == [.kvk])
|
||||
#expect(Self.problems(["kind": "business", "kvk": "12345678", "legal-name": "Ana BV"]).isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `names every field that is missing or malformed`() {
|
||||
#expect(Self.problems(["name": "Ab", "handle": "Bad Handle!", "city": " ", "country": "US", "kind": "charity"]) == [
|
||||
.name, .handle, .city, .country, .kind,
|
||||
])
|
||||
#expect(Self.problems(["handle": "-leading"]) == [.handle])
|
||||
#expect(Self.problems(["handle": "ab"]) == [.handle])
|
||||
#expect(Self.problems(["returns": "Never", "handling": "9"]) == [.returns, .handling])
|
||||
#expect(Self.problems(["declaration": ""]) == [.declaration])
|
||||
#expect(Self.problems(["about": String(repeating: "a", count: 601)]) == [.about])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `lowercases the handle it is given`() throws {
|
||||
#expect(try Self.form(["handle": "Anas-Longbox"]).validated().get().handle == "anas-longbox")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
extension ShopFormTests {
|
||||
|
||||
/// A valid form for a private seller, with the given fields replaced.
|
||||
static func form(
|
||||
_ overrides: [String: String] = [:]
|
||||
) -> ShopForm {
|
||||
.init(values: [
|
||||
"name": "Ana's Longbox", "handle": "anas-longbox", "city": "Leiden", "country": "NL",
|
||||
"about": "Spare copies.", "kind": "private", "returns": "14-day returns", "handling": "2", "declaration": "1",
|
||||
].merging(overrides) { $1 })
|
||||
}
|
||||
|
||||
/// The fields a form with the given overrides has problems with.
|
||||
static func problems(
|
||||
_ overrides: [String: String]
|
||||
) -> Set<ShopForm.Field> {
|
||||
guard case .failure(let problems) = form(overrides).validated() else {
|
||||
return []
|
||||
}
|
||||
|
||||
return Set(problems.fields.keys)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,6 +94,7 @@ private extension StaticFileTests {
|
||||
/// Every file paired with the path it is served at, one per extension, in ``StaticFile/all`` order.
|
||||
static let paths: [(File, [String])] = [
|
||||
(.appleTouchIcon, ["apple-touch-icon.png"]),
|
||||
(.collecting, ["css/collecting.css"]),
|
||||
(.favicon, ["favicon.ico"]),
|
||||
(.fontAnton, ["font/anton-400.woff2"]),
|
||||
(.fontArchivo, ["font/archivo-variable.woff2"]),
|
||||
|
||||
Reference in new issue
Block a user