Implemented phase 4 for the POC prototype in the Website service.
This commit is contained in:
1 parent
95e0a455e8
commit
9403931109
42 files changed
+3407
-36
No files matched your search
@@ -175,6 +175,17 @@ struct CataloguePagesTests {
|
||||
#expect(!html.contains("style="))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `sign-in page hides the passkey button until its script finds WebAuthn, and keeps the demo sign-in working without it`() {
|
||||
let html = SignInPage(returnPath: "/").render()
|
||||
|
||||
#expect(html.contains(#"type="button" hidden data-passkey"#))
|
||||
#expect(html.contains("/js/passkey.js"))
|
||||
#expect(html.contains(#"role="alert" data-passkey-status"#))
|
||||
#expect(html.contains(#"type="submit">Demo sign-in, without a passkey</button>"#))
|
||||
#expect(html.contains("joost@kestrelrow.example"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `sign-in page ties an error to the email field`() {
|
||||
let html = SignInPage(
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import Elementary
|
||||
import NIOCore
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"Selling pages",
|
||||
.tags(.page)
|
||||
)
|
||||
struct SellingPagesTests {
|
||||
|
||||
// MARK: GradingGuidePage tests
|
||||
|
||||
@Test
|
||||
func `grading guide prints the whole scale and defines every defect`() {
|
||||
let html = GradingGuidePage().render()
|
||||
|
||||
for step in Grading.scale {
|
||||
#expect(html.contains("<strong>\(step.grade.gradeLabel)</strong><span>\(step.code)</span>"))
|
||||
}
|
||||
|
||||
for defect in Grading.Defect.allCases {
|
||||
#expect(html.contains(defect.label))
|
||||
#expect(html.contains(defect.definition))
|
||||
}
|
||||
|
||||
#expect(html.contains("Not at this grade"))
|
||||
#expect(html.contains("/css/guide.css"))
|
||||
#expect(!html.contains("noindex"))
|
||||
#expect(!html.contains("style="))
|
||||
}
|
||||
|
||||
// MARK: NewListingPage tests
|
||||
|
||||
@Test
|
||||
func `new-listing form posts as multipart and offers the guide's vocabulary`() {
|
||||
let html = NewListingPage(shop: .stub()).render()
|
||||
|
||||
#expect(html.contains(#"action="/sell/new" method="post" enctype="multipart/form-data""#))
|
||||
#expect(html.contains(#"type="file" name="photos" accept="image/jpeg,image/png,image/webp" multiple"#))
|
||||
#expect(html.contains(#"name="format" value="single-issues" checked"#))
|
||||
#expect(html.contains(#"name="grading" value="slabbed" checked"#))
|
||||
#expect(html.contains(#"<option value="9.4">9.4 · NM</option>"#))
|
||||
#expect(html.contains(#"name="defects" value="crease""#))
|
||||
#expect(html.contains(#"data-scope="collected""#))
|
||||
#expect(html.contains(#"href="/grading-guide""#))
|
||||
#expect(html.contains("Ships from Amsterdam, NL"))
|
||||
#expect(!html.contains(#"role="alert""#))
|
||||
#expect(!html.contains("style="))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `new-listing form gives back what was typed, with each problem tied to its field`() {
|
||||
let html = NewListingPage(
|
||||
shop: .stub(),
|
||||
form: ListingFormTests.form(["grading": ["raw"], "grade": ["9.4"], "defects": ["crease"], "notes": ["Light <wear>"]]),
|
||||
problems: [.defects: "A colour-breaking crease caps a raw book at 7.0.", .price: "Enter the asking price in whole euros."]
|
||||
).render()
|
||||
|
||||
#expect(html.contains("2 things to fix before publishing."))
|
||||
#expect(html.contains("Photos have to be picked again"))
|
||||
#expect(html.contains(#"name="series" value="Nightward""#))
|
||||
#expect(html.contains(#"<option value="9.4" selected>"#))
|
||||
#expect(html.contains(#"name="grading" value="raw" checked"#))
|
||||
#expect(html.contains(#"name="defects" value="crease" checked"#))
|
||||
#expect(html.contains(#"id="price-error">Enter the asking price in whole euros."#))
|
||||
#expect(html.contains(#"aria-invalid="true" aria-describedby="price-error""#))
|
||||
#expect(html.contains("Light <wear>"))
|
||||
}
|
||||
|
||||
// MARK: Photo tests
|
||||
|
||||
@Test
|
||||
func `listing shows its first photo in place of the placeholder cover`() {
|
||||
let bare = ListingCard(.stub()).render()
|
||||
let shot = ListingCard(.stub(photos: ["/uploads/a.jpg", "/uploads/b.jpg"])).render()
|
||||
let page = ListingPage(listing: .stub(defects: ["Spine stress / ticks"], photos: ["/uploads/a.jpg", "/uploads/b.jpg"]), related: []).render()
|
||||
|
||||
#expect(bare.contains("cover--navy"))
|
||||
#expect(!bare.contains("<img"))
|
||||
#expect(shot.contains(#"<img class="photo" src="/uploads/a.jpg" alt="Photo of Nightward #1, front""#))
|
||||
#expect(!shot.contains("cover--navy"))
|
||||
#expect(page.contains(#"src="/uploads/b.jpg""#))
|
||||
#expect(page.contains("<dt>Declared defects</dt><dd>Spine stress / ticks</dd>"))
|
||||
}
|
||||
|
||||
// MARK: MultipartForm tests
|
||||
|
||||
@Test
|
||||
func `multipart form tells text from files, and drops what was left blank`() throws {
|
||||
let body = [
|
||||
"--x", #"Content-Disposition: form-data; name="series""#, "", " Nightward ",
|
||||
"--x", #"Content-Disposition: form-data; name="defects""#, "", "crease",
|
||||
"--x", #"Content-Disposition: form-data; name="defects""#, "", "foxing",
|
||||
"--x", #"Content-Disposition: form-data; name="issue""#, "", "",
|
||||
"--x", #"Content-Disposition: form-data; name="photos"; filename="front.jpg""#, "Content-Type: image/jpeg", "", "JPEGDATA",
|
||||
"--x", #"Content-Disposition: form-data; name="photos"; filename="""#, "Content-Type: application/octet-stream", "", "",
|
||||
"--x--", "",
|
||||
].joined(separator: "\r\n")
|
||||
let form = try #require(MultipartForm(body: .init(string: body), contentType: #"multipart/form-data; boundary="x""#))
|
||||
|
||||
#expect(form.value("series") == "Nightward")
|
||||
#expect(form.values("defects") == ["crease", "foxing"])
|
||||
#expect(form.value("issue") == nil)
|
||||
#expect(form.values("photos").isEmpty)
|
||||
#expect(form.files("photos").map { String(buffer: $0) } == ["JPEGDATA"])
|
||||
#expect(ListingForm(form).has(.defects, value: "foxing"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `multipart form is nothing without a multipart content type`() {
|
||||
#expect(MultipartForm(body: .init(string: "a=b"), contentType: "application/x-www-form-urlencoded") == nil)
|
||||
#expect(MultipartForm(body: .init(), contentType: nil) == nil)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import Persistence
|
||||
import Testing
|
||||
|
||||
@testable import WebsiteLibrary
|
||||
|
||||
@Suite(
|
||||
"ListingForm type",
|
||||
.tags(.type)
|
||||
)
|
||||
struct ListingFormTests {
|
||||
|
||||
// MARK: Methods tests
|
||||
|
||||
@Test
|
||||
func `describes a slabbed single issue`() throws {
|
||||
let draft = try Self.form().validated(photos: ["/uploads/a.jpg"], currentYear: 2026).get()
|
||||
|
||||
#expect(draft.series == "Nightward")
|
||||
#expect(draft.issue == "1")
|
||||
#expect(draft.grader == "CGC")
|
||||
#expect(draft.grade == 9.8)
|
||||
#expect(draft.condition == "NM/M")
|
||||
#expect(draft.certNumber == "4312087-002")
|
||||
#expect(draft.pageQuality == "White")
|
||||
#expect(draft.priceCents == 24_000)
|
||||
#expect(draft.shippingCents == 600)
|
||||
#expect(draft.acceptsOffers)
|
||||
#expect(draft.photos == ["/uploads/a.jpg"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `describes a raw single issue by the scale's code, dropping what only a slab states`() throws {
|
||||
let draft = try Self.form(["grading": ["raw"], "grade": ["8.0"], "defects": ["spine-stress", "corner-wear", "bogus"]])
|
||||
.validated(photos: [], currentYear: 2026)
|
||||
.get()
|
||||
|
||||
#expect(draft.grader == nil)
|
||||
#expect(draft.certNumber == nil)
|
||||
#expect(draft.pageQuality == nil)
|
||||
#expect(draft.condition == "VF")
|
||||
#expect(draft.defects == ["Spine stress / ticks", "Corner wear / blunting"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `describes a collected edition by its condition, ungraded`() throws {
|
||||
let draft = try Self.form(["format": ["hardcovers"], "issue": [], "condition": ["Sealed"], "offers": ["0"]])
|
||||
.validated(photos: [], currentYear: 2026)
|
||||
.get()
|
||||
|
||||
#expect(draft.issue.isEmpty)
|
||||
#expect(draft.grade == nil)
|
||||
#expect(draft.grader == nil)
|
||||
#expect(draft.condition == "Sealed")
|
||||
#expect(draft.edition.isEmpty)
|
||||
#expect(!draft.acceptsOffers)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `applies the grading guide to a raw book`() {
|
||||
#expect(Self.problems(["grading": ["raw"], "grade": ["9.4"], "defects": ["crease"]]) == [.defects])
|
||||
#expect(Self.problems(["grading": ["raw"], "grade": ["7.0"], "defects": ["crease"]]).isEmpty)
|
||||
#expect(Self.problems(["grading": ["raw"], "defects": ["case-scuffs"]]) == [.defects])
|
||||
// A slab's crease is the grader's business, and its case can be scuffed.
|
||||
#expect(Self.problems(["defects": ["crease", "case-scuffs"]]).isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `names every field that is missing or out of range`() {
|
||||
#expect(Self.problems([
|
||||
"series": [], "publisher": [], "year": ["1850"], "issue": [], "grade": ["9.7"], "grader": ["ACME"],
|
||||
"cert": [], "price": ["0"], "shipping": ["-1"],
|
||||
]) == [.series, .publisher, .year, .issue, .grade, .grader, .cert, .price, .shipping])
|
||||
#expect(Self.problems(["format": ["vinyl"]]) == [.format])
|
||||
#expect(Self.problems(["format": ["manga"], "condition": ["Mint"]]) == [.condition])
|
||||
#expect(Self.problems(["year": ["2028"]]) == [.year])
|
||||
#expect(Self.problems(["price": ["12.50"]]) == [.price])
|
||||
#expect(Self.problems(["notes": [String(repeating: "a", count: 1_001)]]) == [.notes])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `starts out with the common choices made`() {
|
||||
let form = ListingForm()
|
||||
|
||||
#expect(form.format == .singleIssues)
|
||||
#expect(form.isSlabbed)
|
||||
#expect(form.has(.offers, value: "1"))
|
||||
#expect(form.value(.series) == nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
extension ListingFormTests {
|
||||
|
||||
/// A valid form for a slabbed Nightward #1, with the given fields replaced.
|
||||
static func form(
|
||||
_ overrides: [String: [String]] = [:]
|
||||
) -> ListingForm {
|
||||
.init(values: [
|
||||
"format": ["single-issues"], "series": ["Nightward"], "issue": ["1"], "publisher": ["Meridian Comics"],
|
||||
"year": ["2019"], "printing": ["1"], "edition": ["Direct"], "grading": ["slabbed"], "grade": ["9.8"],
|
||||
"grader": ["CGC"], "page-quality": ["White"], "cert": ["4312087-002"], "price": ["240"],
|
||||
"shipping": ["6"], "offers": ["1"],
|
||||
].merging(overrides) { $1 })
|
||||
}
|
||||
|
||||
/// The fields a form with the given overrides has problems with.
|
||||
static func problems(
|
||||
_ overrides: [String: [String]]
|
||||
) -> Set<ListingForm.Field> {
|
||||
guard case .failure(let problems) = form(overrides).validated(photos: [], currentYear: 2026) else {
|
||||
return []
|
||||
}
|
||||
|
||||
return Set(problems.fields.keys)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -98,12 +98,14 @@ private extension StaticFileTests {
|
||||
(.fontAnton, ["font/anton-400.woff2"]),
|
||||
(.fontArchivo, ["font/archivo-variable.woff2"]),
|
||||
(.fontOswald, ["font/oswald-variable.woff2"]),
|
||||
(.guide, ["css/guide.css"]),
|
||||
(.icon, ["icon.svg"]),
|
||||
(.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"]),
|
||||
(.passkey, ["js/passkey.js"]),
|
||||
(.robots, ["robots.txt"]),
|
||||
(.search, ["css/search.css"]),
|
||||
(.shared, ["css/shared.css", "js/shared.js"]),
|
||||
|
||||
Reference in new issue
Block a user