Implemented phase 1 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 11:16:52 +02:00
1 parent 6d7ab4bcba
commit ea89ea892a
37 files changed
+4173 -43

No files matched your search

@@ -0,0 +1,99 @@
import Elementary
import Persistence
import Testing
@testable import WebsiteLibrary
@Suite(
"Catalogue components",
.tags(.component)
)
struct CatalogueComponentsTests {
// MARK: Cover tests
@Test
func `letters a cover line by line on its tone`() {
let html = Cover(Listing.stub().cover, modifier: "cover--hero").render()
#expect(html.contains(#"class="cover cover--navy cover--hero""#))
#expect(html.contains("<span>NIGHT</span><span>WARD</span>"))
#expect(html.contains(#"class="cover__number">1<"#))
#expect(html.contains(#"aria-hidden="true""#))
}
@Test
func `sets a volume marker smaller than an issue number`() {
let html = Cover(.init(tone: "plum", title: "HOLLOW\nCOMET", imprint: "SŌKAI BOOKS", number: "VOL 4")).render()
#expect(html.contains(#"class="cover__number cover__number--text">VOL 4<"#))
}
// MARK: Chip tests
@Test
func `checks a certified chip`() {
#expect(Chip("CGC 9.8", tone: .good, isCertified: true).render().hasPrefix(#"<span class="chip chip--good"><svg"#))
#expect(Chip("Raw · NM").render() == #"<span class="chip">Raw · NM</span>"#)
}
// MARK: ListingCard tests
@Test
func `links a card to its listing and gates saving behind the sign-in`() {
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("€240"))
#expect(html.contains("CGC 9.8"))
}
@Test
func `names the shop by the card's context`() {
#expect(ListingCard(.stub(), context: .browse).render().contains(">Kestrel Row Comics</a>"))
#expect(ListingCard(.stub(), context: .search).render().contains(">Kestrel Row Comics · NL · ship €6</a>"))
#expect(!ListingCard(.stub(), context: .shop).render().contains("/shop/kestrel-row-comics"))
}
// MARK: Pagination tests
@Test
func `renders no page links for a single page`() {
#expect(Pagination(current: 1, total: 1) { "/?page=\($0)" }.render().isEmpty)
}
@Test
func `marks the current page and offers the next until the last`() {
let middle = Pagination(current: 2, total: 3) { "/?page=\($0)" }.render()
let last = Pagination(current: 3, total: 3) { "/?page=\($0)" }.render()
#expect(middle.contains(#"href="/?page=2" aria-current="page""#))
#expect(middle.contains(#"href="/?page=3">Next</a>"#))
#expect(!last.contains("Next"))
}
@Test(arguments: zip(
[1, 2, 3, 9, -1],
[[1, 2], [3, 4], [5], [5], [1, 2]]
))
func `slices a page out of a list, clamping the page`(
page: Int,
expects items: [Int]
) {
let slice = Pagination.slice([1, 2, 3, 4, 5], page: page, size: 2)
#expect(slice.items == items)
#expect(slice.total == 3)
}
@Test
func `counts one page for an empty list`() {
let slice = Pagination.slice([Int](), page: 1, size: 12)
#expect(slice.items.isEmpty)
#expect(slice.current == 1)
#expect(slice.total == 1)
}
}
@@ -0,0 +1,82 @@
import Persistence
import Testing
@testable import WebsiteLibrary
@Suite(
"Listing+Display extension",
.tags(.extensionTests)
)
struct ListingDisplayTests {
// MARK: Computed tests
@Test(arguments: zip(
[9.8, 9.0, 8.5, 7.0, 6.5],
[Chip.Tone.good, .good, .neutral, .neutral, .warn]
))
func `tones a slab's chip by its grade`(
grade: Double,
expects tone: Chip.Tone
) {
let chip = Listing.stub(grade: grade).chip
#expect(chip.label == "CGC \(grade)")
#expect(chip.tone == tone)
#expect(chip.isCertified)
}
@Test
func `marks a raw issue's chip as seller-graded`() {
let chip = Listing.stub(grader: nil, grade: 9.4, condition: "NM").chip
#expect(chip.label == "Raw · NM")
#expect(chip.tone == .neutral)
#expect(!chip.isCertified)
}
@Test
func `states a collected edition's condition alone`() {
#expect(Listing.stub(format: "manga", grader: nil, grade: nil, condition: "Like new").chip.label == "Like new")
}
@Test
func `leads a collected edition's summary with its format`() {
#expect(Listing.stub().summary == "Meridian Comics · 2019 · 1st print")
#expect(Listing.stub(format: "hardcovers").summary == "Hardcover · Meridian Comics · 2019 · 1st print")
}
@Test
func `derives the paths from the slugs`() {
#expect(Listing.stub().path == "/listing/nightward-1")
#expect(Shop.stub().path == "/shop/kestrel-row-comics")
}
@Test(arguments: zip(
["Kestrel Row Comics", "Inkwell & Staple", "Silverfish"],
["KR", "IS", "S"]
))
func `monogram`(
of name: String,
expects monogram: String
) {
#expect(Shop.stub(name: name).monogram == monogram)
}
@Test(arguments: zip(
[24_000, 1_250, 5, 0],
["€240", "€12.50", "€0.05", "€0"]
))
func `euros`(
of cents: Int,
expects price: String
) {
#expect(cents.euros == price)
}
@Test
func `returns to the gated action's path after sign-in`() {
#expect(String.Route.signIn(returningTo: "/shop/a?keys=1&page=2") == "/sign-in?return=/shop/a%3Fkeys%3D1%26page%3D2")
}
}
@@ -0,0 +1,167 @@
import Elementary
import Persistence
import Testing
@testable import WebsiteLibrary
@Suite(
"Catalogue pages",
.tags(.page)
)
struct CataloguePagesTests {
// MARK: IndexPage tests
@Test
func `landing page fills its rows and fans its covers from the listings`() {
let singles = (1...6).map { Listing.stub(slug: "single-\($0)", title: "Single \($0)") }
let manga = Listing.stub(slug: "manga-1", title: "Manga 1", format: "manga")
let html = IndexPage(
locale: .init(identifier: "en"),
listings: .init(from: singles + [manga])
).render()
#expect(html.contains("New in single issues"))
#expect(html.contains("Single 5"))
#expect(!html.contains("Single 6"))
#expect(html.contains("Manga 1"))
#expect(html.contains("hero__covers"))
#expect(html.contains(#"href="/search?format=manga&amp;format=trade-paperbacks&amp;format=hardcovers""#))
}
@Test
func `landing page drops its rows without listings`() {
let html = IndexPage(locale: .init(identifier: "en")).render()
#expect(!html.contains("New in single issues"))
#expect(!html.contains("hero__covers"))
#expect(html.contains("Passkeys, not passwords"))
}
// MARK: SearchPage tests
@Test
func `search page heads the results with the query and ticks the applied filters`() {
let html = SearchPage(
parameters: .init(query: "q=nightward+1&format=single-issues&publisher=Meridian+Comics"),
matches: [.stub(), .stub(slug: "b", shop: .stub(slug: "other", name: "Other"))],
publishers: ["Blackline", "Meridian Comics"]
).render()
#expect(html.contains("<title>“nightward 1” — CCN</title>"))
#expect(html.contains("2 listings"))
#expect(html.contains("from 2 shops"))
#expect(html.contains(#"name="q" value="nightward 1""#))
#expect(html.contains(#"value="Meridian Comics" checked"#))
#expect(!html.contains(#"value="Blackline" checked"#))
#expect(html.contains(#"aria-label="Remove filter: Meridian Comics""#))
#expect(html.contains("/css/search.css"))
}
@Test
func `search page pages through more matches than fit`() {
let matches = (1...13).map { Listing.stub(slug: "l-\($0)", title: "Listing \($0).") }
let html = SearchPage(
parameters: .init(query: "page=2"),
matches: matches,
publishers: []
).render()
#expect(html.contains("13 listings"))
#expect(html.contains("Listing 13."))
#expect(!html.contains("Listing 12."))
#expect(html.contains(#"href="/search?page=2" aria-current="page""#))
}
@Test
func `search page offers a way out of an empty result`() {
let html = SearchPage(
parameters: .init(query: "q=nothing"),
matches: [],
publishers: []
).render()
#expect(html.contains("No listings match"))
#expect(html.contains(#"href="/search?ships-from=anywhere""#))
}
// MARK: ListingPage tests
@Test
func `listing page states the copy and gates every action behind the sign-in`() {
let html = ListingPage(
listing: .stub(keyNotes: "First appearance"),
related: [.stub(slug: "nightward-2", title: "Nightward #2")]
).render()
#expect(html.contains("<title>Nightward #1 · CGC 9.8 — CCN</title>"))
#expect(html.contains("+ €6 tracked shipping to NL · accepts offers"))
#expect(html.contains("CGC 9.8 · cert 4312087-002"))
#expect(html.contains("First appearance"))
#expect(html.contains("More from Kestrel Row Comics"))
#expect(html.contains("Nightward #2"))
for action in ["Buy now", "Make an offer", "Message seller", "Save"] {
#expect(html.contains(action))
}
#expect(html.components(separatedBy: #"href="/sign-in?return=/listing/nightward-1""#).count - 1 >= 5)
}
@Test
func `listing page leaves out what the listing does not offer or state`() {
let html = ListingPage(
listing: .stub(grader: nil, grade: nil, condition: "Sealed", acceptsOffers: false),
related: []
).render()
#expect(!html.contains("Make an offer"))
#expect(!html.contains("Key notes"))
#expect(!html.contains("More from"))
#expect(html.contains("<dt>Condition</dt>"))
}
// MARK: ShopPage tests
@Test
func `shop page counts its tabs from the whole inventory and lists the matches`() {
let inventory = [
Listing.stub(),
.stub(slug: "b", title: "Second"),
.stub(slug: "c", title: "A manga", format: "manga"),
]
let html = ShopPage(
shop: .stub(),
inventory: inventory,
matches: [inventory[2]],
parameters: .init(query: "format=manga&keys=1")
).render()
#expect(html.contains("<title>Kestrel Row Comics — CCN</title>"))
#expect(html.contains("All · 3"))
#expect(html.contains("Single issues · 2"))
#expect(html.contains(#"href="/shop/kestrel-row-comics?keys=1&amp;format=manga" aria-current="page""#))
#expect(!html.contains("Hardcovers ·"))
#expect(html.contains("A manga"))
#expect(!html.contains("Second"))
#expect(html.contains(#"href="/shop/kestrel-row-comics?format=manga" aria-pressed="true""#))
#expect(html.contains(#"type="hidden" name="format" value="manga""#))
}
// MARK: Shared tests
@Test
func `no catalogue page renders an inline style`() {
let pages = [
SearchPage(parameters: .init(), matches: [.stub()], publishers: ["Blackline"]).render(),
ListingPage(listing: .stub(), related: [.stub()]).render(),
ShopPage(shop: .stub(), inventory: [.stub()], matches: [.stub()], parameters: .init()).render(),
]
for html in pages {
#expect(!html.contains("style="))
#expect(!html.contains("<style"))
}
}
}
@@ -0,0 +1,118 @@
import Persistence
import Testing
@testable import WebsiteLibrary
@Suite(
"SearchParameters type",
.tags(.type)
)
struct SearchParametersTests {
// MARK: Initializers tests
@Test
func `decodes a form-encoded query and drops its empty fields`() {
let parameters = SearchParameters(query: "q=nightward+1&publisher=Harbour+%26+Vale&price-min=&grade-max=+")
#expect(parameters.items == [
.init(name: "q", value: "nightward 1"),
.init(name: "publisher", value: "Harbour & Vale"),
])
}
@Test
func `has no parameters without a query`() {
#expect(SearchParameters(query: nil).items.isEmpty)
#expect(SearchParameters(query: nil).queryString.isEmpty)
}
// MARK: Computed tests
@Test
func `reads every criterion into the filter`() {
let query = "q=saint&format=manga&format=hardcovers&grading=slabbed&grade-min=6.0&grade-max=9.8&first-print=1"
+ "&keys=1&publisher=Blackline&price-min=10&price-max=50&ships-from=nl&offers=1&verified=1&sort=price-asc"
#expect(SearchParameters(query: query).filter == .init(
query: "saint",
formats: ["manga", "hardcovers"],
isSlabbed: true,
gradeMin: 6,
gradeMax: 9.8,
firstPrintOnly: true,
keysOnly: true,
publishers: ["Blackline"],
priceMin: 1_000,
priceMax: 5_000,
shipsFrom: .nl,
acceptsOffers: true,
verifiedShops: true,
sort: .priceAscending
))
}
@Test
func `ships from the EU, newest first, by default`() {
#expect(SearchParameters().filter == .init(shipsFrom: .eu))
}
@Test
func `matches slabbed and raw alike when both are ticked`() {
#expect(SearchParameters(query: "grading=slabbed&grading=raw").filter.isSlabbed == nil)
#expect(SearchParameters(query: "grading=raw").filter.isSlabbed == false)
}
@Test
func `ignores values it cannot read`() {
let filter = SearchParameters(query: "grade-min=high&price-max=cheap&ships-from=mars&sort=random&page=-3").filter
#expect(filter == .init(shipsFrom: .eu))
#expect(SearchParameters(query: "page=-3").page == 1)
}
@Test
func `offers a pill per active criterion, each linking to the search without it`() {
let parameters = SearchParameters(query: "format=manga&grade-min=6&price-max=50&ships-from=anywhere&page=2")
let pills = parameters.pills
#expect(pills.map(\.label) == ["Manga", "Grade 6.0–10.0", "Up to €50"])
#expect(pills[0].removed.queryString == "?grade-min=6&price-max=50&ships-from=anywhere")
#expect(pills[1].removed.queryString == "?format=manga&price-max=50&ships-from=anywhere")
}
@Test
func `removes the default region by naming every region instead`() {
let pill = SearchParameters().pills.first
#expect(pill?.label == "Ships from EU")
#expect(pill?.removed.queryString == "?ships-from=anywhere")
}
// MARK: Methods tests
@Test
func `keeps the other parameters when turning the page, and returns to the first when changing them`() {
let parameters = SearchParameters(query: "q=saint&page=2")
#expect(parameters.setting(.page, to: "3").queryString == "?q=saint&page=3")
#expect(parameters.setting(.sort, to: "price-asc").queryString == "?q=saint&sort=price-asc")
#expect(parameters.toggling(.keys, value: "1").toggling(.keys, value: "1").queryString == "?q=saint")
}
@Test
func `escapes what it writes back into a path`() {
let parameters = SearchParameters().setting(.publisher, to: "Harbour & Vale")
#expect(parameters.path("/search") == "/search?publisher=Harbour%20%26%20Vale")
#expect(SearchParameters(query: String(parameters.queryString.dropFirst())) == parameters)
}
@Test
func `narrows a shop's filter to the shop, wherever it ships from`() {
let filter = SearchParameters(query: "keys=1").filter(inShop: "longbox-atlantic")
#expect(filter == .init(keysOnly: true, shipsFrom: .anywhere, shop: "longbox-atlantic"))
}
}
@@ -102,9 +102,12 @@ private extension StaticFileTests {
(.icon192, ["icon-192.png"]),
(.icon512, ["icon-512.png"]),
(.index, ["css/index.css", "js/index.js"]),
(.listing, ["css/listing.css"]),
(.notFound, ["css/not-found.css", "js/not-found.js"]),
(.robots, ["robots.txt"]),
(.search, ["css/search.css"]),
(.shared, ["css/shared.css", "js/shared.js"]),
(.shop, ["css/shop.css"]),
(.site, ["site.webmanifest"]),
(.sitemap, ["sitemap.xml"]),
]