import AppKit import Foundation import Testing @testable import Kanban /// Printing, reduced to the rules a unit test can hold (11-command-nexus.md ▸ File ▸ Print…). /// /// The typography is not the point and is not tested — that is `PrintDocumentRenderer`'s and it is /// legitimately unverifiable without eyes, the same standing `BodyMarkupRenderer` has. What *is* tested is /// every layer under it, each of which fails silently: /// /// - **Profiles** round-trip through a preferences plist. A field that stopped encoding, or a decoder that /// threw on a shape a future build wrote, costs the user every profile they saved — and nothing on screen /// would say so until they looked for a profile that had gone. /// - **The document's structure** is where every option actually lands: which components appear, in what /// order, which end of a thread comes first, and where a page break falls. All four are invisible in a /// rendered page and obvious in a block list. /// - **The `labels` reading** interprets a key 01-storage-format.md reserves and this version otherwise /// leaves inert, so printing is the one surface that can be wrong about it. /// - **The trash exclusion** is the one rule whose failure would print deleted cards. // MARK: - Helpers /// A profile store on a scratch defaults domain — profiles are app-wide and persisted, so a suite that used /// `.standard` would rewrite the developer's own (`BoardZoomTests`' arrangement, verbatim in intent). @MainActor private func makeStore() -> (PrintProfileStore, UserDefaults, () -> Void) { let name = "dev.rzen.indie.Kanban.print-tests.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: name)! return (PrintProfileStore(defaults: defaults), defaults, { UserDefaults.standard.removePersistentDomain(forName: name) }) } /// Options with every field moved off its default, so a round-trip that dropped one is visible. private func exoticOptions() -> PrintOptions { var options = PrintOptions() options.includesTitle = false options.includesLabels = false options.includesBody = false options.includesComments = true options.commentSort = .newestFirst options.pageBreaks = .betweenCards options.fontFamily = "Palatino" options.fontSize = 13.5 options.headerShowsBoardTitle = false options.headerShowsPrintDate = false options.footerShowsPageNumbers = false options.footerShowsCustomLine = true options.footerCustomLine = "Confidential" return options } private func card( _ title: String?, body: String = "", icon: String? = nil, labels: [String] = [], comments: [PrintComment] = [] ) -> PrintCard { PrintCard(title: title, icon: icon, labels: labels, body: body, comments: comments) } private func board(_ lanes: [PrintLane], titled title: String = "Roadmap") -> PrintSource { PrintSource(scope: .board, boardTitle: title, lanes: lanes) } /// A date some fixed distance from a base, so comment ordering is a fact about the sort rather than about /// the clock. private func stamp(_ minutes: Int) -> Date { Date(timeIntervalSince1970: 1_760_000_000).addingTimeInterval(TimeInterval(minutes * 60)) } // MARK: - Options ▸ Codable @Suite("Print ▸ options round-trip") struct PrintOptionsCodableTests { @Test("Every field survives an encode and a decode") func roundTrips() throws { let options = exoticOptions() let decoded = try JSONDecoder().decode(PrintOptions.self, from: try JSONEncoder().encode(options)) #expect(decoded == options) } @Test("The defaults round-trip too — the state a first print is configured in") func defaultsRoundTrip() throws { let options = PrintOptions() let decoded = try JSONDecoder().decode(PrintOptions.self, from: try JSONEncoder().encode(options)) #expect(decoded == options) #expect(decoded.includesComments == false, "comments are off by default") #expect(decoded.pageBreaks == .flow) #expect(decoded.fontFamily == nil, "the system font is the absence of a family, not a spelling of one") } /// The failure mode the lenient decoder exists to rule out: one unrecognized shape must not cost the /// whole value. @Test("A stored shape from another build decodes to the defaults rather than throwing") func toleratesForeignShapes() throws { let json = """ {"includesTitle": false, "pageBreaks": "betweenParagraphs", "commentSort": 7, "fontSize": "large", "somethingNew": {"a": 1}} """ let decoded = try JSONDecoder().decode(PrintOptions.self, from: Data(json.utf8)) #expect(decoded.includesTitle == false, "the field it did understand is honoured") #expect(decoded.pageBreaks == .flow, "an unknown page-break mode reads as the default") #expect(decoded.commentSort == .oldestFirst, "a wrongly-typed sort reads as the default") #expect(decoded.fontSize == PrintOptions().fontSize, "a wrongly-typed size reads as the default") #expect(decoded.includesBody, "every absent field keeps its default") } @Test("A stored size outside the legal range is clamped, never drawn") func clampsSize() throws { for (stored, expected) in [(0.0, PrintOptions.fontSizeRange.lowerBound), (-4.0, PrintOptions.fontSizeRange.lowerBound), (900.0, PrintOptions.fontSizeRange.upperBound)] { let decoded = try JSONDecoder().decode( PrintOptions.self, from: Data("{\"fontSize\": \(stored)}".utf8) ) #expect(decoded.fontSize == expected) } } @Test("Normalizing is a reading, not a rewrite — it never breaks the round-trip") func normalizingIsARead() throws { var options = PrintOptions() options.footerShowsCustomLine = true options.footerCustomLine = " " // The stored value keeps the toggle: a banner emptied for one print and typed back in for the next // must not lose it. let decoded = try JSONDecoder().decode(PrintOptions.self, from: try JSONEncoder().encode(options)) #expect(decoded == options) // The render's reading drops it. #expect(decoded.normalized.footerShowsCustomLine == false) } @Test("Three components off and comments off is 'nothing to print'") func describesAnyContent() { var options = PrintOptions() #expect(options.describesAnyContent) options.includesTitle = false options.includesLabels = false options.includesBody = false #expect(!options.describesAnyContent) options.includesComments = true #expect(options.describesAnyContent) } } // MARK: - The catalog's rules @Suite("Print ▸ the profile catalog") struct PrintProfileCatalogTests { @Test("A save appends; a save over the same name overwrites in place") func savingAndOverwriting() { var catalog = PrintProfileCatalog() catalog.save(PrintOptions(), as: "Handout") var second = PrintOptions() second.fontSize = 20 catalog.save(second, as: "Archive") #expect(catalog.names == ["Handout", "Archive"], "list order is the order they were saved in") var replacement = PrintOptions() replacement.pageBreaks = .betweenLanes catalog.save(replacement, as: "Handout") #expect(catalog.names == ["Handout", "Archive"], "an overwrite does not move the row") #expect(catalog.options(named: "Handout")?.pageBreaks == .betweenLanes) } @Test("Names compare case-insensitively, and the last spelling typed wins") func namesFoldCase() { var catalog = PrintProfileCatalog() catalog.save(PrintOptions(), as: "Handout") catalog.save(PrintOptions(), as: " handout ") #expect(catalog.names == ["handout"], "one profile, spelled the way it was last saved") #expect(catalog.contains("HANDOUT")) } @Test("The reserved name cannot be claimed, in any spelling") func reservedNameRefused() { var catalog = PrintProfileCatalog() let reserved = catalog.save(PrintOptions(), as: PrintProfile.lastUsedName) let folded = catalog.save(PrintOptions(), as: "last used") let blank = catalog.save(PrintOptions(), as: " ") #expect(reserved == false) #expect(folded == false) #expect(blank == false, "a blank name is not a name") #expect(catalog.names.isEmpty) } @Test("A rename keeps position and options; a collision is refused") func renaming() { var catalog = PrintProfileCatalog() var handout = PrintOptions() handout.fontSize = 15 catalog.save(handout, as: "Handout") catalog.save(PrintOptions(), as: "Archive") let renamed = catalog.rename("Handout", to: "Standup") #expect(renamed) #expect(catalog.names == ["Standup", "Archive"], "position survives the rename") #expect(catalog.options(named: "Standup")?.fontSize == 15) let collision = catalog.rename("Standup", to: "Archive") let reserved = catalog.rename("Standup", to: PrintProfile.lastUsedName) let missing = catalog.rename("Nothing", to: "Something") let recased = catalog.rename("Standup", to: "STANDUP") #expect(collision == false, "a rename never swallows a sibling") #expect(reserved == false) #expect(missing == false) #expect(recased, "re-casing its own name is allowed") #expect(catalog.names == ["STANDUP", "Archive"]) } @Test("A delete of a name that matches nothing is a no-op") func deleting() { var catalog = PrintProfileCatalog() catalog.save(PrintOptions(), as: "Handout") catalog.delete("Nothing") #expect(catalog.names == ["Handout"]) catalog.delete("handout") #expect(catalog.names.isEmpty) } /// The bytes come out of a preferences plist a human may have edited, so the sanitizing rule is what a /// menu can render rather than a guarantee about the writer. @Test("Construction drops blank, reserved and duplicate entries") func sanitizesOnConstruction() { let catalog = PrintProfileCatalog(profiles: [ PrintProfile(name: " Handout ", options: PrintOptions()), PrintProfile(name: "", options: PrintOptions()), PrintProfile(name: PrintProfile.lastUsedName, options: PrintOptions()), PrintProfile(name: "handout", options: PrintOptions()) ]) #expect(catalog.names == ["Handout"], "trimmed, and the first of two spellings kept") } @Test("A catalog round-trips, and one malformed entry does not cost the rest") func catalogCodable() throws { var catalog = PrintProfileCatalog() catalog.save(exoticOptions(), as: "Archive") catalog.save(PrintOptions(), as: "Handout") let decoded = try JSONDecoder().decode(PrintProfileCatalog.self, from: try JSONEncoder().encode(catalog)) #expect(decoded == catalog) let partial = """ {"profiles": [{"name": "Kept"}, {"options": {"fontSize": 12}}, {"name": "Also kept", "options": {}}]} """ let lenient = try JSONDecoder().decode(PrintProfileCatalog.self, from: Data(partial.utf8)) #expect(lenient.names == ["Kept", "Also kept"], "the nameless entry is dropped, the rest survive") #expect(lenient.options(named: "Kept") == PrintOptions()) } } // MARK: - The store's persistence @Suite("Print ▸ profile persistence") @MainActor struct PrintProfileStoreTests { @Test("A saved profile survives a fresh store over the same domain") func savePersists() { let (store, defaults, teardown) = makeStore() defer { teardown() } #expect(store.catalog.names.isEmpty, "a first launch has no profiles") #expect(store.save(exoticOptions(), as: "Archive")) let reopened = PrintProfileStore(defaults: defaults) #expect(reopened.catalog.names == ["Archive"]) #expect(reopened.options(named: "Archive") == exoticOptions()) } @Test("Renames and deletes persist too") func managementPersists() { let (store, defaults, teardown) = makeStore() defer { teardown() } store.save(PrintOptions(), as: "Handout") store.save(exoticOptions(), as: "Archive") #expect(store.rename("Handout", to: "Standup")) store.delete("Archive") #expect(PrintProfileStore(defaults: defaults).catalog.names == ["Standup"]) } @Test("Last Used captures the options a print ran with, and survives a relaunch") func lastUsedCaptures() { let (store, defaults, teardown) = makeStore() defer { teardown() } #expect(store.lastUsed == nil, "nothing has been printed yet") #expect(store.options(named: PrintProfile.lastUsedName) == PrintOptions(), "the reserved row reads as the factory defaults on a first launch") store.captureLastUsed(exoticOptions()) #expect(store.lastUsed == exoticOptions()) let reopened = PrintProfileStore(defaults: defaults) #expect(reopened.lastUsed == exoticOptions()) #expect(reopened.options(named: PrintProfile.lastUsedName) == exoticOptions()) } @Test("The reserved row leads the menu, then the named ones in save order") func menuOrder() { let (store, _, teardown) = makeStore() defer { teardown() } store.save(PrintOptions(), as: "Handout") store.save(PrintOptions(), as: "Archive") #expect(store.menuNames == [PrintProfile.lastUsedName, "Handout", "Archive"]) } @Test("A garbage preference reads as nothing stored rather than taking the surface down") func toleratesGarbage() throws { let name = "dev.rzen.indie.Kanban.print-garbage.\(UUID().uuidString)" let defaults = try #require(UserDefaults(suiteName: name)) defer { UserDefaults.standard.removePersistentDomain(forName: name) } defaults.set(42, forKey: AppPreferences.printProfilesKey) defaults.set(Data("not json".utf8), forKey: AppPreferences.printLastUsedKey) let store = PrintProfileStore(defaults: defaults) #expect(store.catalog.names.isEmpty) #expect(store.lastUsed == nil) } @Test("A refused name writes nothing at all") func refusalWritesNothing() { let (store, defaults, teardown) = makeStore() defer { teardown() } #expect(store.save(PrintOptions(), as: PrintProfile.lastUsedName) == false) #expect(defaults.data(forKey: AppPreferences.printProfilesKey) == nil) } } // MARK: - The document @Suite("Print ▸ document assembly") struct PrintDocumentBuilderTests { private static let source = board([ PrintLane(title: "Doing", cards: [ card("Fix login", body: "Some **words**.", icon: "flag", labels: ["bug", "ui"]), card("Ship it", body: "More words.") ]), PrintLane(title: "Done", cards: [ card("Old thing", body: "Done words.") ]) ]) @Test("The default document is board, lane, card, meta, body — in that order") func defaultOrder() { let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions()) #expect(blocks == [ .boardHeading("Roadmap"), .laneHeading("Doing"), .cardTitle("Fix login"), .cardMeta(icon: "flag", labels: ["bug", "ui"]), .cardBody("Some **words**."), .cardTitle("Ship it"), .cardBody("More words."), .laneHeading("Done"), .cardTitle("Old thing"), .cardBody("Done words.") ]) } @Test("A card with no chosen icon and no labels contributes no meta line") func metaOmittedWhenEmpty() { let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions()) let metas = blocks.filter { if case .cardMeta = $0 { return true } else { return false } } #expect(metas.count == 1, "only the card that has something to say gets the line") } @Test("Each component toggle removes exactly its own block") func componentToggles() { var options = PrintOptions() options.includesTitle = false var blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options) #expect(!blocks.contains { if case .cardTitle = $0 { return true } else { return false } }) #expect(blocks.contains { if case .cardBody = $0 { return true } else { return false } }) options = PrintOptions() options.includesLabels = false blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options) #expect(!blocks.contains { if case .cardMeta = $0 { return true } else { return false } }) options = PrintOptions() options.includesBody = false blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options) #expect(!blocks.contains { if case .cardBody = $0 { return true } else { return false } }) #expect(blocks.contains { if case .cardTitle = $0 { return true } else { return false } }) } @Test("Every component off prints nothing — not a page of running heads") func nothingIncludedIsAnEmptyDocument() { var options = PrintOptions() options.includesTitle = false options.includesLabels = false options.includesBody = false #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options).isEmpty) } @Test("An untitled card and an untitled lane print the placeholder, never a blank line") func untitledPlaceholder() { let source = board([PrintLane(title: nil, cards: [card(nil, body: "Words.")])]) let blocks = PrintDocumentBuilder.blocks(from: source, options: PrintOptions()) #expect(blocks == [ .boardHeading("Roadmap"), .laneHeading(PrintDocumentBuilder.untitled), .cardTitle(PrintDocumentBuilder.untitled), .cardBody("Words.") ]) } /// The whitespace case is the card window's own emptiness rule (`BodyMarkup.isEmpty`), reused so a body /// of one newline does not print a blank paragraph. @Test("A whitespace-only body is no body") func whitespaceBodyOmitted() { let source = board([PrintLane(title: "Doing", cards: [card("Titled", body: "\n \n")])]) let blocks = PrintDocumentBuilder.blocks(from: source, options: PrintOptions()) #expect(blocks == [.boardHeading("Roadmap"), .laneHeading("Doing"), .cardTitle("Titled")]) } @Test("An empty lane is omitted, and so is a card with nothing to show") func emptiesDropOut() { let source = board([ PrintLane(title: "Empty", cards: []), PrintLane(title: "All blank", cards: [card(nil), card(nil)]), PrintLane(title: "Real", cards: [card("Kept", body: "Words.")]) ]) var options = PrintOptions() // With titles off, the two blank cards have nothing left at all — which must take their lane with // them rather than leaving a heading over nothing. options.includesTitle = false let blocks = PrintDocumentBuilder.blocks(from: source, options: options) #expect(blocks == [.boardHeading("Roadmap"), .laneHeading("Real"), .cardBody("Words.")]) } @Test("A card print is the card — no board heading, no lane heading") func cardScope() { let source = PrintSource( scope: .card, boardTitle: "Roadmap", lanes: [PrintLane(title: "Doing", cards: [card("Fix login", body: "Words.")])] ) var options = PrintOptions() options.pageBreaks = .betweenCards #expect(PrintDocumentBuilder.blocks(from: source, options: options) == [ .cardTitle("Fix login"), .cardBody("Words.") ]) } } // MARK: - Page breaks @Suite("Print ▸ page breaks") struct PrintPageBreakTests { private static let source = board([ PrintLane(title: "Doing", cards: [card("A", body: "a"), card("B", body: "b")]), PrintLane(title: "Done", cards: [card("C", body: "c")]) ]) @Test("Continuous emits no break at all") func flow() { let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions()) #expect(!blocks.contains(.pageBreak)) } @Test("Between lanes breaks before each lane after the first, and nowhere else") func betweenLanes() { var options = PrintOptions() options.pageBreaks = .betweenLanes #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [ .boardHeading("Roadmap"), .laneHeading("Doing"), .cardTitle("A"), .cardBody("a"), .cardTitle("B"), .cardBody("b"), .pageBreak, .laneHeading("Done"), .cardTitle("C"), .cardBody("c") ]) } @Test("Between cards breaks before every card but the document's first") func betweenCards() { var options = PrintOptions() options.pageBreaks = .betweenCards #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [ .boardHeading("Roadmap"), .laneHeading("Doing"), .cardTitle("A"), .cardBody("a"), .pageBreak, .cardTitle("B"), .cardBody("b"), .pageBreak, .laneHeading("Done"), .cardTitle("C"), .cardBody("c") ]) } /// The board's name is the first lane's running-in title, not a title page — the one thing that would /// otherwise put a lone heading on sheet one of every print with breaks on. @Test("The board heading never earns a break of its own") func boardHeadingIsNotATitlePage() { for mode in [PrintPageBreaks.betweenLanes, .betweenCards] { var options = PrintOptions() options.pageBreaks = mode let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: options) #expect(blocks.first == .boardHeading("Roadmap")) #expect(blocks.dropFirst().first == .laneHeading("Doing"), "no break between the two") } } @Test("A break is never leading, never trailing, and never doubled") func breaksAreWellFormed() { let source = board([ PrintLane(title: "Empty", cards: []), PrintLane(title: "One", cards: [card("A", body: "a")]), PrintLane(title: "Blank", cards: [card(nil)]), PrintLane(title: "Two", cards: [card("B", body: "b")]) ]) var options = PrintOptions() options.pageBreaks = .betweenCards options.includesTitle = false let blocks = PrintDocumentBuilder.blocks(from: source, options: options) #expect(blocks.first != .pageBreak) #expect(blocks.last != .pageBreak) for (left, right) in zip(blocks, blocks.dropFirst()) { #expect(!(left == .pageBreak && right == .pageBreak)) } #expect(blocks.filter { $0 == .pageBreak }.count == 1, "the two dropped lanes take their breaks too") } @Test("A single-card board never breaks, whatever the mode") func oneCardNeverBreaks() { let source = board([PrintLane(title: "Doing", cards: [card("A", body: "a")])]) for mode in PrintPageBreaks.allCases { var options = PrintOptions() options.pageBreaks = mode #expect(!PrintDocumentBuilder.blocks(from: source, options: options).contains(.pageBreak)) } } } // MARK: - Comments @Suite("Print ▸ comments") struct PrintCommentTests { private static let thread = [ PrintComment(author: "ada", created: stamp(0), body: "first"), PrintComment(author: "grace", created: stamp(10), body: "second"), PrintComment(author: nil, created: stamp(20), body: "third") ] private static let source = board([ PrintLane(title: "Doing", cards: [card("A", body: "a", comments: Self.thread)]) ]) @Test("Comments are off by default and print nothing") func offByDefault() { let blocks = PrintDocumentBuilder.blocks(from: Self.source, options: PrintOptions()) #expect(!blocks.contains { if case .comment = $0 { return true } else { return false } }) #expect(!blocks.contains { if case .commentsHeading = $0 { return true } else { return false } }) } @Test("Oldest first walks the thread's own chronology, heading first") func oldestFirst() { var options = PrintOptions() options.includesComments = true #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [ .boardHeading("Roadmap"), .laneHeading("Doing"), .cardTitle("A"), .cardBody("a"), .commentsHeading(count: 3), .comment(author: "ada", created: stamp(0), body: "first"), .comment(author: "grace", created: stamp(10), body: "second"), .comment(author: nil, created: stamp(20), body: "third") ]) } @Test("Newest first reverses that order and nothing else") func newestFirst() { var options = PrintOptions() options.includesComments = true options.commentSort = .newestFirst let bodies = PrintDocumentBuilder.blocks(from: Self.source, options: options).compactMap { block -> String? in guard case let .comment(_, _, body) = block else { return nil } return body } #expect(bodies == ["third", "second", "first"]) } @Test("A card with no comments gets no heading, even with comments on") func emptyThread() { var options = PrintOptions() options.includesComments = true let source = board([PrintLane(title: "Doing", cards: [card("A", body: "a")])]) #expect(!PrintDocumentBuilder.blocks(from: source, options: options) .contains { if case .commentsHeading = $0 { return true } else { return false } }) } /// A thread is the only content a card may have — with the title, the labels and the body all off, the /// comments still print. @Test("Comments alone are enough to keep a card in the document") func commentsAloneKeepACard() { var options = PrintOptions() options.includesTitle = false options.includesLabels = false options.includesBody = false options.includesComments = true #expect(PrintDocumentBuilder.blocks(from: Self.source, options: options) == [ .boardHeading("Roadmap"), .laneHeading("Doing"), .commentsHeading(count: 3), .comment(author: "ada", created: stamp(0), body: "first"), .comment(author: "grace", created: stamp(10), body: "second"), .comment(author: nil, created: stamp(20), body: "third") ]) } @Test("A thread flattens in the loader's order — printing never re-sorts") func flatteningKeepsThreadOrder() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", "---\nschema: 1\ntitle: Board\n---\n") let lane = "11111111-1111-4111-8111-111111111111" let cardID = "22222222-2222-4222-8222-222222222222" try fixture.item(lane, "---\nschema: 1\ntitle: Doing\norder: 1024\n---\n") try fixture.item("\(lane)/\(cardID)", "---\nschema: 1\ntitle: A\norder: 1024\n---\nBody.\n") // Deliberately written out of chronological order, so an order that came out right could only have // come from the sort. try fixture.item( "\(lane)/\(cardID)/comments/33333333-3333-4333-8333-333333333333", "---\nschema: 1\nkind: comment\nauthor: grace\ncreated: 2026-02-02T09:00:00Z\n---\nsecond\n" ) try fixture.item( "\(lane)/\(cardID)/comments/44444444-4444-4444-8444-444444444444", "---\nschema: 1\nkind: comment\nauthor: ada\ncreated: 2026-01-01T09:00:00Z\n---\nfirst\n" ) let cardFolder = fixture.url("\(lane)/\(cardID)") let comments = PrintComment.list(of: CommentThread.load(inCard: cardFolder, path: "\(lane)/\(cardID)")) #expect(comments.map(\.body) == ["first\n", "second\n"]) #expect(comments.map(\.author) == ["ada", "grace"]) } } // MARK: - Extraction from a real board @Suite("Print ▸ what a board contributes") @MainActor struct PrintSourceTests { /// A board with two lanes, a trashed card and a trashed lane — the one arrangement whose failure would /// print deleted cards. private func fixture() throws -> WriterFixture { let fixture = try WriterFixture() try fixture.item("", "---\nschema: 1\ntitle: Roadmap\n---\n") try fixture.item("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", "---\nschema: 1\ntitle: Doing\norder: 1024\n---\n") try fixture.item("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", "---\nschema: 1\ntitle: Done\norder: 2048\n---\n") try fixture.item( "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/cccccccc-cccc-4ccc-8ccc-cccccccccccc", "---\nschema: 1\ntitle: Second\norder: 2048\nicon: flag\nlabels: [bug, ui]\n---\nSecond body.\n" ) try fixture.item( "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa/dddddddd-dddd-4ddd-8ddd-dddddddddddd", "---\nschema: 1\ntitle: First\norder: 1024\n---\nFirst body.\n" ) try fixture.item( "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb/eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee", "---\nschema: 1\ntitle: Shipped\norder: 1024\n---\nShipped body.\n" ) // The trash: one card and one lane, both of which a board print must not reach. try fixture.item( ".trash/ffffffff-ffff-4fff-8fff-ffffffffffff", "---\nschema: 1\nkind: card\ntitle: Deleted card\nmodified: 2026-03-03T09:00:00Z\n---\nGone.\n" ) try fixture.item( ".trash/99999999-9999-4999-8999-999999999999", "---\nschema: 1\nkind: lane\ntitle: Deleted lane\nmodified: 2026-03-04T09:00:00Z\n---\n" ) return fixture } @Test("Lanes and cards arrive in display order, and the trash is not reachable") func laneAndCardOrder() throws { let fixture = try self.fixture() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model // The fixture really does have a trash — otherwise the exclusion below proves nothing. #expect(snapshot.trash.count == 1) #expect(snapshot.trashedLanes.count == 1) let source = PrintSource.board(snapshot, titled: "Roadmap") #expect(source.scope == .board) #expect(source.lanes.map(\.title) == ["Doing", "Done"]) #expect(source.lanes[0].cards.map(\.title) == ["First", "Second"], "by rank, not by folder name") #expect(source.lanes[1].cards.map(\.title) == ["Shipped"]) let printed = PrintDocumentBuilder.blocks(from: source, options: PrintOptions()) #expect(!printed.contains(.cardTitle("Deleted card"))) #expect(!printed.contains(.laneHeading("Deleted lane"))) } @Test("A card's icon, labels and body cross over; an unknown icon does not") func cardFields() throws { let fixture = try self.fixture() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model let source = PrintSource.board(snapshot, titled: "Roadmap") let second = try #require(source.lanes.first?.cards.last) #expect(second.title == "Second") #expect(second.icon == "flag") #expect(second.labels == ["bug", "ui"]) #expect(second.body == "Second body.\n") let first = try #require(source.lanes.first?.cards.first) #expect(first.icon == nil, "a card with no chosen icon prints none — the level default stays on screen") #expect(first.labels.isEmpty) } @Test("A card print carries its lane and its board without printing headings for them") func cardScopeExtraction() throws { let fixture = try self.fixture() defer { fixture.tearDown() } let snapshot = try BoardLoader.load(boardRoot: fixture.root).model let lane = try #require(snapshot.lanes.first) let card = try #require(lane.cards.first) let source = PrintSource.card(card, laneTitle: lane.title.value, boardTitle: "Roadmap", comments: []) #expect(source.scope == .card) #expect(source.boardTitle == "Roadmap") #expect(source.lanes.map(\.title) == ["Doing"]) #expect(PrintDocumentBuilder.blocks(from: source, options: PrintOptions()) == [ .cardTitle("First"), .cardBody("First body.\n") ]) } } // MARK: - The labels reading @Suite("Print ▸ the reserved labels key") struct PrintLabelsTests { private func labels(_ frontmatter: String) throws -> [String] { PrintCard.labels(of: try FrontmatterDocument.parse("---\nschema: 1\n\(frontmatter)---\nBody.\n")) } @Test("A sequence is its scalar members, in order") func sequence() throws { #expect(try labels("labels: [bug, ui, p1]\n") == ["bug", "ui", "p1"]) #expect(try labels("labels:\n - bug\n - ui\n") == ["bug", "ui"]) } @Test("A comma-separated scalar is the pair a human meant to type") func commaSeparatedScalar() throws { #expect(try labels("labels: bug, ui\n") == ["bug", "ui"]) #expect(try labels("labels: bug\n") == ["bug"]) } @Test("Non-string scalars read as the engine renders them") func nonStringScalars() throws { #expect(try labels("labels: [1, 2]\n") == ["1", "2"]) #expect(try labels("labels: [true]\n") == ["true"]) } @Test("Shapes that are not a label row contribute nothing, and never an error") func exoticShapes() throws { #expect(try labels("labels:\n") == [], "an explicit null") #expect(try labels("labels: ''\n") == []) #expect(try labels("labels: {a: 1}\n") == [], "a mapping is not a label row") #expect(try labels("labels: [[a, b], c]\n") == ["c"], "a nested list is skipped, not flattened") #expect(try labels("labels: [bug, '', ui]\n") == ["bug", "ui"], "blank members drop out") #expect(try labels("title: No labels here\n") == [], "an absent key") } } // MARK: - Header and footer @Suite("Print ▸ the running head and foot") struct PrintRunningHeadTests { @Test("Each toggle contributes exactly its own end") func toggles() { var options = PrintOptions() var header = PrintRunningHead.header(options: options, boardTitle: "Roadmap", dateText: "9 Aug 2026") #expect(header == PrintRunningHead.Line(leading: "Roadmap", trailing: "9 Aug 2026")) options.headerShowsBoardTitle = false header = PrintRunningHead.header(options: options, boardTitle: "Roadmap", dateText: "9 Aug 2026") #expect(header == PrintRunningHead.Line(leading: "", trailing: "9 Aug 2026")) options.headerShowsPrintDate = false header = PrintRunningHead.header(options: options, boardTitle: "Roadmap", dateText: "9 Aug 2026") #expect(header.isEmpty, "an empty line takes no paper at all") } @Test("The folio trails, the custom line leads") func footer() { var options = PrintOptions() options.footerShowsCustomLine = true options.footerCustomLine = "Confidential" #expect(PrintRunningHead.footer(options: options, pageText: "Page 2 of 7") == PrintRunningHead.Line(leading: "Confidential", trailing: "Page 2 of 7")) options.footerShowsPageNumbers = false #expect(PrintRunningHead.footer(options: options, pageText: "Page 2 of 7") == PrintRunningHead.Line(leading: "Confidential", trailing: "")) } @Test("A toggle left on over an emptied field prints nothing rather than an indent of air") func blankCustomLine() { var options = PrintOptions() options.footerShowsCustomLine = true options.footerCustomLine = " " options.footerShowsPageNumbers = false #expect(PrintRunningHead.footer(options: options, pageText: "Page 1 of 1").isEmpty) } @Test("The folio's wording") func pageText() { #expect(PrintRunningHead.pageText(page: 3, of: 7) == "Page 3 of 7") } } // MARK: - The menu row @Suite("Print ▸ menu validation") struct PrintCommandValidationTests { /// **The row never disables now** (revised 2026-08-09) — a print is a read, so neither the /// read-only lock nor the focused-editor rule ever closed it, and the old third state /// ("nothing published, so grey out") turned out to leave the ⌘P chord live anyway, falling /// through to AppKit's own stock print handling (`PrintCommand`'s doc comment tells the whole /// story). `resolveScope` is what the row switches on instead: three answers, no `disabled` left /// to fall through. @Test("The board wins over a card, and neither published is a polite refusal — not a dead key") func resolution() { #expect(PrintCommand.resolveScope(hasBoard: true, hasPrintableCard: false) == .board) #expect(PrintCommand.resolveScope(hasBoard: false, hasPrintableCard: true) == .card) #expect(PrintCommand.resolveScope(hasBoard: true, hasPrintableCard: true) == .board, "the board in front wins") #expect( PrintCommand.resolveScope(hasBoard: false, hasPrintableCard: false) == .refuse, "welcome, or nothing at all — answered with a sentence, never silence" ) } } // MARK: - Finder's half: the `printFiles` Apple Event /// `PrintCoordinator.resolveFinderPrint(atPath:)` — the one part of the Finder-print path a test can /// call without handing AppKit a real print job (`resolveFinderPrint`'s own doc comment). Everything /// past this point (`printFiles`, `printHeadlessBoard`) drives a real `NSPrintOperation`, the same /// AppKit boundary `PrintCoordinator.run`/`refuse` already sit past untested — this suite pins the /// board-or-refuse *decision*, not what AppKit does with it. @Suite("Print ▸ Finder printFiles resolution") @MainActor struct PrintFinderResolutionTests { @Test("A board's path resolves to its snapshot, extension-less exactly like every other board open") func resolvesABoard() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.board(title: "Roadmap") try fixture.lane(Ident.lane1, order: "1024", title: "Doing") try fixture.card(Ident.card1, in: Ident.lane1, order: "1024", title: "A card") // `fixture.root` itself carries no `.kanban` extension — the "both open" half of // `BoardModel`'s own doc comment, exercised by construction rather than by a second fixture. guard case let .board(model) = PrintCoordinator.resolveFinderPrint(atPath: fixture.root.path) else { Issue.record("expected the path to resolve as a board") return } #expect(model.title.value == "Roadmap") #expect(model.lanes.map(\.title.value) == ["Doing"]) #expect(model.lanes.first?.cards.map(\.title.value) == ["A card"]) } @Test("A folder with no root index.md refuses rather than throwing") func refusesANonBoard() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } // No `board()` call — an ordinary empty folder, the shape of "not a board at all". guard case .refuse = PrintCoordinator.resolveFinderPrint(atPath: fixture.root.path) else { Issue.record("expected the path to refuse") return } } @Test("A plain file refuses rather than throwing") func refusesAFile() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } let fileURL = fixture.root.appendingPathComponent("not-a-board.txt") try Data("hello".utf8).write(to: fileURL) guard case .refuse = PrintCoordinator.resolveFinderPrint(atPath: fileURL.path) else { Issue.record("expected the path to refuse") return } } @Test("A board whose root schema is newer than this build refuses, the same fail-fast every open gives") func refusesAnUnsupportedSchema() throws { let fixture = try WriterFixture() defer { fixture.tearDown() } try fixture.item("", "---\nschema: 999\ntitle: Future\n---\n") guard case .refuse = PrintCoordinator.resolveFinderPrint(atPath: fixture.root.path) else { Issue.record("expected the path to refuse") return } } } // MARK: - Rendering and pagination, smoke-tested @Suite("Print ▸ rendering and pagination") @MainActor struct PrintRenderingTests { private func session(_ source: PrintSource, options: PrintOptions) -> PrintSession { let (store, _, _) = makeStore() store.captureLastUsed(options) return PrintSession( provider: PrintSourceProvider(complete: source), profiles: store, cardFolder: nil, jobTitle: source.boardTitle, boardTitle: source.boardTitle ) } private static func longBody(paragraphs: Int) -> String { (0 ..< paragraphs) .map { "Paragraph \($0). " + String(repeating: "Words that fill a line of a printed page. ", count: 6) } .joined(separator: "\n\n") } @Test("A page break really becomes a separately paginated section") func breaksSplitSections() { var options = PrintOptions() options.pageBreaks = .betweenLanes let source = board([ PrintLane(title: "One", cards: [card("A", body: "a")]), PrintLane(title: "Two", cards: [card("B", body: "b")]), PrintLane(title: "Three", cards: [card("C", body: "c")]) ]) let blocks = PrintDocumentBuilder.blocks(from: source, options: options) #expect(PrintDocumentRenderer.sections(for: blocks, options: options).count == 3) // …and continuously, one section. var flowing = options flowing.pageBreaks = .flow let flowingBlocks = PrintDocumentBuilder.blocks(from: source, options: flowing) #expect(PrintDocumentRenderer.sections(for: flowingBlocks, options: flowing).count == 1) } @Test("An empty document renders no sections") func emptyDocument() { #expect(PrintDocumentRenderer.sections(for: [], options: PrintOptions()).isEmpty) } @Test("Between-lanes really costs a sheet per lane") func pageCountPerLane() { var options = PrintOptions() options.pageBreaks = .betweenLanes let source = board((1 ... 4).map { PrintLane(title: "Lane \($0)", cards: [card("Card \($0)", body: "words")]) }) let view = PrintDocumentView(session: session(source, options: options), printInfo: NSPrintInfo()) #expect(view.pageCount() == 4, "four short lanes, four sheets — the break is pagination, not spacing") } @Test("A long body paginates rather than clipping") func longBodyPaginates() { let options = PrintOptions() let short = board([PrintLane(title: "One", cards: [card("A", body: "one line")])]) let long = board([PrintLane(title: "One", cards: [card("A", body: Self.longBody(paragraphs: 80))])]) #expect(PrintDocumentView(session: session(short, options: options), printInfo: NSPrintInfo()).pageCount() == 1) let pages = PrintDocumentView(session: session(long, options: options), printInfo: NSPrintInfo()).pageCount() #expect(pages > 1, "eighty paragraphs do not fit on one sheet") } @Test("A byline reads as a sentence in all four states") func bylines() { #expect(PrintDocumentRenderer.byline(author: "ada", created: nil) == "ada") #expect(PrintDocumentRenderer.byline(author: nil, created: nil) == "Comment") #expect(PrintDocumentRenderer.byline(author: " ", created: nil) == "Comment", "a blank author is no author") let dated = PrintDocumentRenderer.byline(author: "ada", created: stamp(0)) #expect(dated.hasPrefix("ada — ")) #expect(PrintDocumentRenderer.byline(author: nil, created: stamp(0)) == String(dated.dropFirst("ada — ".count))) } @Test("The thread's heading counts, and says 'comment' once") func commentsHeading() { #expect(PrintDocumentRenderer.commentsHeadingText(count: 1) == "1 comment") #expect(PrintDocumentRenderer.commentsHeadingText(count: 3) == "3 comments") #expect(PrintDocumentRenderer.commentsHeadingText(count: 0) == "0 comments") } @Test("The chosen face reaches the text, and code keeps its own") func faceRemap() { // Courier is on every Mac; asserting a family that might not be installed would be asserting a // fixture about the machine. let family = "Times New Roman" guard PrintTypography.families().contains(family) else { return } var options = PrintOptions() options.fontFamily = family let blocks: [PrintBlock] = [.cardBody("Words, and `code`.")] let section = try? #require(PrintDocumentRenderer.sections(for: blocks, options: options).first) guard let section else { return } var sawFace = false var sawMono = false section.enumerateAttribute(.font, in: NSRange(location: 0, length: section.length)) { value, _, _ in guard let font = value as? NSFont else { return } if font.fontDescriptor.symbolicTraits.contains(.monoSpace) { sawMono = true } else if font.familyName == family { sawFace = true } } #expect(sawFace, "the body is set in the chosen family") #expect(sawMono, "inline code stays monospaced — a fenced block in Palatino is nobody's intent") } @Test("A summary line describes what the panel is about to print") func summary() { var options = PrintOptions() #expect(PrintOptionsSummary.includes(options) == "Title, labels, body") #expect(PrintOptionsSummary.pageBreaks(options) == "Continuous") #expect(PrintOptionsSummary.type(options) == "System 11 pt") options.includesComments = true options.commentSort = .newestFirst options.fontFamily = "Palatino" options.fontSize = 13.5 options.pageBreaks = .betweenCards #expect(PrintOptionsSummary.includes(options) == "Title, labels, body, comments (newest first)") #expect(PrintOptionsSummary.pageBreaks(options) == "Between cards") #expect(PrintOptionsSummary.type(options) == "Palatino 13.5 pt") options.includesTitle = false options.includesLabels = false options.includesBody = false options.includesComments = false #expect(PrintOptionsSummary.includes(options) == "Nothing") } } // MARK: - The session @Suite("Print ▸ the panel's session") @MainActor struct PrintSessionTests { private func session(profiles: PrintProfileStore) -> PrintSession { PrintSession( provider: PrintSourceProvider(complete: board([PrintLane(title: "One", cards: [card("A", body: "a")])])), profiles: profiles, cardFolder: nil, jobTitle: "Roadmap", boardTitle: "Roadmap" ) } @Test("A session opens on Last Used, which is the defaults on a first print") func opensOnLastUsed() { let (store, _, teardown) = makeStore() defer { teardown() } #expect(session(profiles: store).options == PrintOptions()) store.captureLastUsed(exoticOptions()) let second = session(profiles: store) #expect(second.selectedProfileName == PrintProfile.lastUsedName) #expect(second.options == exoticOptions()) } @Test("Choosing a profile copies its options in; editing them afterwards does not write back") func selectingIsACopy() { let (store, _, teardown) = makeStore() defer { teardown() } store.save(exoticOptions(), as: "Archive") let session = self.session(profiles: store) session.selectProfile(named: "Archive") #expect(session.options == exoticOptions()) #expect(!session.isModified) session.options.fontSize = 9 #expect(session.isModified, "the popup says modified") #expect(store.options(named: "Archive")?.fontSize == exoticOptions().fontSize, "the profile is untouched") #expect(session.saveProfile(named: "Archive")) #expect(store.options(named: "Archive")?.fontSize == 9) #expect(!session.isModified) } @Test("The reserved row is never 'modified' — drifting from it is what it is for") func reservedRowIsNeverModified() { let (store, _, teardown) = makeStore() defer { teardown() } let session = self.session(profiles: store) session.options.fontSize = 30 #expect(!session.isModified) #expect(!session.canManageSelection, "the reserved row cannot be renamed or deleted") } @Test("Deleting a profile keeps the settings in front of the user") func deleteKeepsOptions() { let (store, _, teardown) = makeStore() defer { teardown() } store.save(exoticOptions(), as: "Archive") let session = self.session(profiles: store) session.selectProfile(named: "Archive") session.deleteSelectedProfile() #expect(store.catalog.names.isEmpty) #expect(session.selectedProfileName == PrintProfile.lastUsedName) #expect(session.options == exoticOptions(), "a management gesture is not a reset") } @Test("Selecting a name that resolves to nothing changes nothing") func selectingAGhost() { let (store, _, teardown) = makeStore() defer { teardown() } let session = self.session(profiles: store) session.options.fontSize = 20 session.selectProfile(named: "Deleted elsewhere") #expect(session.selectedProfileName == PrintProfile.lastUsedName) #expect(session.options.fontSize == 20) } @Test("The comment-bearing source is read only when the options ask for it") func commentsAreReadLazily() { let (store, _, teardown) = makeStore() defer { teardown() } var reads = 0 let withComments = board([PrintLane(title: "One", cards: [ card("A", body: "a", comments: [PrintComment(author: "ada", created: stamp(0), body: "hi")]) ])]) let provider = PrintSourceProvider( withoutComments: board([PrintLane(title: "One", cards: [card("A", body: "a")])]), withComments: { reads += 1 return withComments } ) let session = PrintSession( provider: provider, profiles: store, cardFolder: nil, jobTitle: "Roadmap", boardTitle: "Roadmap" ) _ = session.blocks() _ = session.blocks() #expect(reads == 0, "comments are off by default, so no thread is read") session.options.includesComments = true _ = session.blocks() _ = session.blocks() #expect(reads == 1, "and once the user asks, exactly one read serves every relayout") } } // MARK: - Symbols on paper /// **The printed-symbol regression** (owner report, 2026-08-08: "SF symbols don't render well in the PDF /// output of File ▸ Print…", with a screenshot of solid dark rectangles where the card icons belong). /// /// The typography around it is still legitimately untested — see this file's own note — but this failure is /// not typography. It is a drawing fact with two halves, and both are assertable: /// /// - an `NSImage(systemSymbolName:)` is a **template**, and a print/PDF context tints one across its whole /// box rather than through its coverage, which is the black rectangle the owner photographed; /// - a PDF context is a 1× device, so even a non-template symbol image lands as a 13-pixel bitmap and /// blurs at any zoom. /// /// So the suite asserts what `PrintSymbol` hands over (concrete artwork, at paper resolution, on the /// symbol's own baseline) and then prints a page whose only ink is one icon and measures it: a glyph /// covers a fraction of its own bounding box, a template box covers all of it. The second test would have /// failed on the shipped build at ~1.0 coverage, which is the whole point of writing it that way. @Suite("Print ▸ symbols on paper") @MainActor struct PrintSymbolTests { /// A symbol every macOS has had for years, and one whose artwork is plainly not a rectangle. private static let symbol = "lightbulb" // MARK: The image handed to the page @Test("A printed symbol is concrete artwork, not a template to be tinted by machinery that is not there") func artworkNotTemplate() throws { let rendered = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 11, ink: PrintTypography.secondaryInk)) #expect(!rendered.image.isTemplate, "a template image is what drew the black boxes") let rep = try #require(rendered.image.representations.first as? NSBitmapImageRep) #expect(rep.size == rendered.image.size, "the extra pixels are resolution, not a bigger picture") #expect( CGFloat(rep.pixelsWide) >= rep.size.width * 4, "a page wants far more than the 72 ppi a PDF context would rasterize at" ) } @Test("The artwork is a glyph — most of its box is paper") func artworkIsNotFilled() throws { let rendered = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 11, ink: PrintTypography.secondaryInk)) let rep = try #require(rendered.image.representations.first as? NSBitmapImageRep) let bytes = try #require(rep.bitmapData) var inked = 0 for y in 0 ..< rep.pixelsHigh { for x in 0 ..< rep.pixelsWide { let alpha = bytes[y * rep.bytesPerRow + x * rep.samplesPerPixel + 3] if alpha > 12 { inked += 1 } } } let coverage = Double(inked) / Double(rep.pixelsWide * rep.pixelsHigh) #expect(coverage > 0.02, "a symbol that drew nothing at all is the other way to fail") #expect(coverage < 0.75, "a filled box is the template bug; a lightbulb is an outline") } @Test("An unknown name draws nothing rather than a box — `ItemSymbol`'s promise, kept on paper") func unknownNameIsOmitted() { #expect(PrintSymbol.rendered("not.a.symbol.anybody.ships", pointSize: 11, ink: .black) == nil) } // MARK: The baseline @Test("The offset is the symbol's own baseline, per symbol and scaling with the size") func baselineComesFromTheSymbol() throws { let ink = PrintTypography.secondaryInk let bulb = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 11, ink: ink)) let text = try #require(PrintSymbol.rendered("textformat", pointSize: 11, ink: ink)) let larger = try #require(PrintSymbol.rendered(Self.symbol, pointSize: 22, ink: ink)) // Below the baseline, always — the old constant was `font.descender * 0.5`, which knew nothing // about which symbol it was placing. #expect(bulb.baselineOffset < 0) #expect( bulb.baselineOffset < text.baselineOffset, "a lightbulb's base sits under the baseline; `textformat` sits on it" ) #expect(larger.baselineOffset < bulb.baselineOffset, "and the offset is a length, so it scales") let source = try #require(NSImage(systemSymbolName: Self.symbol, accessibilityDescription: nil)? .withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 11, weight: .regular))) #expect(bulb.baselineOffset == -source.alignmentRect.origin.y, "which is Apple's own metric, not a guess") } // MARK: The renderer's line @Test("The icon reaches the page as an attachment carrying that artwork") func theLineCarriesTheArtwork() throws { let blocks: [PrintBlock] = [.cardMeta(icon: Self.symbol, labels: ["idea"])] let section = try #require(PrintDocumentRenderer.sections(for: blocks, options: PrintOptions()).first) var found: NSImage? section.enumerateAttribute(.attachment, in: NSRange(location: 0, length: section.length)) { value, _, _ in if let attachment = value as? NSTextAttachment { found = attachment.image } } let image = try #require(found, "the icon is a text attachment on the labels line") #expect(!image.isTemplate) #expect(section.string.contains("idea"), "and the labels are still on the line beside it") } @Test("A symbol this system cannot draw still prints its labels") func aMissingSymbolKeepsTheLabels() throws { let blocks: [PrintBlock] = [.cardMeta(icon: "not.a.symbol.anybody.ships", labels: ["idea"])] let section = try #require(PrintDocumentRenderer.sections(for: blocks, options: PrintOptions()).first) #expect(section.string.contains("idea")) } // MARK: What actually lands on paper /// A page whose only ink is one icon: a card print, every component but the labels line switched off, /// a card with an icon and no labels, and no running head or foot. Whatever is dark on that sheet is /// the symbol and nothing else, so it can be measured rather than eyeballed. private func iconOnlyOptions() -> PrintOptions { var options = PrintOptions() options.includesTitle = false options.includesBody = false options.includesComments = false options.includesLabels = true options.headerShowsBoardTitle = false options.headerShowsPrintDate = false options.footerShowsPageNumbers = false options.footerShowsCustomLine = false return options } private func iconOnlyPDF(icon: String) -> Data { let (store, _, teardown) = makeStore() defer { teardown() } let options = iconOnlyOptions() store.captureLastUsed(options) let source = PrintSource( scope: .card, boardTitle: "Roadmap", lanes: [PrintLane(title: "One", cards: [PrintCard(title: "A", icon: icon)])] ) let session = PrintSession( provider: PrintSourceProvider(complete: source), profiles: store, cardFolder: nil, jobTitle: "A", boardTitle: "Roadmap" ) let info = NSPrintInfo() let view = PrintDocumentView(session: session, printInfo: info) _ = view.pageCount() let data = NSMutableData() let operation = NSPrintOperation.pdfOperation(with: view, inside: view.bounds, to: data, printInfo: info) operation.showsPrintPanel = false operation.showsProgressPanel = false operation.run() return data as Data } /// The document's first page, rasterized onto white at `scale` times its point size — what a reader /// with a magnifying glass would see. private func firstPage(of pdf: Data, scale: CGFloat) -> NSBitmapImageRep? { guard let page = NSPDFImageRep(data: pdf) else { return nil } page.currentPage = 0 let box = page.size guard let rep = NSBitmapImageRep( bitmapDataPlanes: nil, pixelsWide: Int(box.width * scale), pixelsHigh: Int(box.height * scale), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0 ) else { return nil } rep.size = box NSGraphicsContext.saveGraphicsState() NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep) NSColor.white.setFill() CGRect(origin: .zero, size: box).fill() page.draw(in: CGRect(origin: .zero, size: box)) NSGraphicsContext.restoreGraphicsState() return rep } /// The bounding box of the ink on a page, and how much of that box the ink fills — in points, and as a /// fraction. A glyph fills a fraction of its box; the template bug filled all of it. private func ink(on rep: NSBitmapImageRep, scale: CGFloat) -> (box: CGRect, coverage: Double)? { guard let bytes = rep.bitmapData else { return nil } var minX = rep.pixelsWide, maxX = -1, minY = rep.pixelsHigh, maxY = -1 var inked = 0 for y in 0 ..< rep.pixelsHigh { let row = y * rep.bytesPerRow for x in 0 ..< rep.pixelsWide where bytes[row + x * rep.samplesPerPixel] < 220 { inked += 1 minX = min(minX, x); maxX = max(maxX, x) minY = min(minY, y); maxY = max(maxY, y) } } guard maxX >= minX, maxY >= minY else { return nil } let width = maxX - minX + 1 let height = maxY - minY + 1 return ( CGRect(x: CGFloat(minX) / scale, y: CGFloat(minY) / scale, width: CGFloat(width) / scale, height: CGFloat(height) / scale), Double(inked) / Double(width * height) ) } @Test("A printed page draws the symbol as a glyph, at the size the type scale asked for") func thePageDrawsAGlyph() throws { let scale: CGFloat = 4 let pdf = iconOnlyPDF(icon: Self.symbol) #expect(!pdf.isEmpty, "the operation produced a document") let page = try #require(firstPage(of: pdf, scale: scale)) let measured = try #require(ink(on: page, scale: scale), "the icon is the only ink on the sheet") // The line's own size, which is what the icon is built at. let expected = try #require(PrintSymbol.rendered( Self.symbol, pointSize: PrintTypography.secondary(iconOnlyOptions()).pointSize, ink: PrintTypography.secondaryInk )).image.size #expect( measured.coverage < 0.75, "coverage \(measured.coverage) — a filled box is the template bug the owner photographed" ) #expect(measured.coverage > 0.05, "and something was drawn") // The ink sits *inside* the symbol's box and fills most of it: a lightbulb is narrower than the // box it is drawn in, but an icon at the wrong size — the whole page, or a stray point size — // would miss either bound by a mile. #expect( measured.box.width <= expected.width + 1 && measured.box.height <= expected.height + 1, "ink measured \(measured.box.size) against a symbol of \(expected)" ) #expect( measured.box.width >= expected.width * 0.5 && measured.box.height >= expected.height * 0.5, "ink measured \(measured.box.size) against a symbol of \(expected)" ) } }