Build the style, details, and actions sidebar sections
The sidebar completes: the shared style editor gains a second anchor — StyleEditorLayout carries the geometry (the popover keeps its settled 268/14/7/8 untouched as the default; the sidebar packs columns to its width with no inner scroller) while every well, the batch display, the arrow grammar, and the one applyStyle bracket stay the shared component's. The card anchor is fixed, not tracking: the target is this card, and the fate walk retires the window when the card goes. Details renders every unknown frontmatter key read-only in file order — Card.document already carried them — showing the author's own bytes where the raw span is a value and the engine's rendering for block scalars and empties; reserved enhanced-schema keys are ordinary unknowns, and no keys means no section. Actions: Delete rides the same tombstone bytes as Backspace and drop-on-trash through a one-line seam, says nothing about selection, and lets the fate walk dismiss; Reveal in Finder resolves through the attachment scope so the two paths cannot disagree. History reserves its m7 slot without drawing a header no base board can honor. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Kanban
|
||||
|
||||
/// The card window's sidebar sections, reduced to the rules a unit test can hold (05-card-window.md
|
||||
/// ▸ The attributes sidebar).
|
||||
///
|
||||
/// The views are not the point and are not tested. What is tested is the three seams under them,
|
||||
/// each of which fails *silently* — the failure mode this file exists for:
|
||||
///
|
||||
/// - **Details** shows frontmatter the app deliberately does not understand, so nothing downstream
|
||||
/// can notice when a key goes missing, arrives out of order, or renders as a YAML indicator. The
|
||||
/// only check on it is a test that reads a file and says what the rows must be.
|
||||
/// - **Style** embeds a shared, selection-aware component in a window that has no selection. An
|
||||
/// anchor wired to the wrong target would look completely normal until it restyled something else.
|
||||
/// - **The sidebar's geometry** is a grid of fixed-size wells in a column sized from font metrics:
|
||||
/// at the wrong column count the last well in each row is simply unreachable, at no text size
|
||||
/// anyone tests by eye.
|
||||
|
||||
// MARK: - Details ▸ which keys
|
||||
|
||||
@Suite("Card details ▸ keys")
|
||||
struct CardDetailsKeyTests {
|
||||
|
||||
/// One card carrying every kind of key at once: the schema's own, an agent overlay, four
|
||||
/// reserved enhanced-schema names, and a key written twice.
|
||||
private static let card = """
|
||||
---
|
||||
schema: 1
|
||||
title: Fix login
|
||||
order: 1024
|
||||
project: lanework # agent overlay
|
||||
labels: [a, b, c]
|
||||
assignees:
|
||||
- ada
|
||||
- grace
|
||||
due: 2026-08-01
|
||||
remote: {name: origin, branch: main}
|
||||
created: 2026-01-01T09:00:00Z
|
||||
modified: 2026-02-02T09:00:00Z
|
||||
modified-by: claude
|
||||
background: mint
|
||||
icon: flag
|
||||
iconColor: carnation
|
||||
project: overlay-rewritten
|
||||
---
|
||||
Body text.
|
||||
|
||||
"""
|
||||
|
||||
@Test("Every unknown key appears, in file order")
|
||||
func unknownKeysInFileOrder() throws {
|
||||
let rows = CardDetails.rows(of: try FrontmatterDocument.parse(Self.card))
|
||||
|
||||
// File order, verbatim — 01-storage-format.md preserves it and the sidebar honors it. The
|
||||
// twice-written `project` reads once, and it reads where its *winning* occurrence sits: the
|
||||
// effective view a duplicate collapses to (`FrontmatterDocument.parse`, last-wins), which is
|
||||
// also the order the file itself takes the moment anything rewrites that key.
|
||||
#expect(rows.map(\.key) == ["labels", "assignees", "due", "remote", "project"])
|
||||
}
|
||||
|
||||
@Test("Reserved enhanced-schema keys are ordinary unknown keys in this version")
|
||||
func reservedKeysAreShown() throws {
|
||||
let rows = CardDetails.rows(of: try FrontmatterDocument.parse(Self.card))
|
||||
|
||||
// "`labels`, `assignees`, `due`, `remote`, … are ordinary unknown keys in this version and
|
||||
// appear here like any other — no special rendering" (05 ▸ Details). The day they gain
|
||||
// meaning they leave this section for a control; until then, hiding them would hide data the
|
||||
// file plainly has.
|
||||
for reserved in ["labels", "assignees", "due", "remote"] {
|
||||
#expect(rows.contains { $0.key == reserved }, "\(reserved) belongs in Details")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("Every schema-owned key is excluded — including the ones the sidebar shows elsewhere")
|
||||
func schemaKeysAreExcluded() throws {
|
||||
let rows = CardDetails.rows(of: try FrontmatterDocument.parse(Self.card))
|
||||
let keys = Set(rows.map(\.key))
|
||||
|
||||
// The eleven the app owns. `title`, `created`/`modified`/`modified-by` and the three style
|
||||
// keys have their own surfaces in this very window (the title field, the date line, the Style
|
||||
// section), and `schema`/`order`/`width`/`deleted` are structure — a Details row for any of
|
||||
// them would be the same fact stated twice, in a section whose whole premise is "keys the app
|
||||
// does not own".
|
||||
#expect(keys.isDisjoint(with: FrontmatterKeys.schemaOwned))
|
||||
#expect(keys == ["labels", "assignees", "due", "remote", "project"])
|
||||
}
|
||||
|
||||
@Test("A card with no unknown keys has no section at all")
|
||||
func theSectionDisappearsWithNothingToShow() throws {
|
||||
// The visibility rule, as the seam states it: "shown only when any exist" (05 ▸ Details).
|
||||
// Empty rows are the view's whole condition, so this is that condition.
|
||||
let plain = "---\nschema: 1\ntitle: Plain\norder: 1024\n---\nBody.\n"
|
||||
#expect(CardDetails.rows(of: try FrontmatterDocument.parse(plain)).isEmpty)
|
||||
|
||||
// Style keys and stamps are not "details" either — a styled, stamped card still has none.
|
||||
let styled = """
|
||||
---
|
||||
schema: 1
|
||||
title: Styled
|
||||
order: 1024
|
||||
background: mint
|
||||
icon: flag
|
||||
iconColor: carnation
|
||||
created: 2026-01-01T09:00:00Z
|
||||
modified: 2026-02-02T09:00:00Z
|
||||
modified-by: claude
|
||||
width: 2
|
||||
deleted: 2026-03-03T09:00:00Z
|
||||
---
|
||||
|
||||
"""
|
||||
#expect(CardDetails.rows(of: try FrontmatterDocument.parse(styled)).isEmpty)
|
||||
}
|
||||
|
||||
@Test("The rows come off the snapshot's own card, unknown keys and order intact")
|
||||
func theSnapshotCarriesTheKeys() throws {
|
||||
let fixture = try WriterFixture()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.item("", Item.board)
|
||||
try fixture.item(Ident.lane1, Item.rich(order: "1024", title: "Todo"))
|
||||
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Self.card)
|
||||
|
||||
// Nothing was added to the loader for this section: `Card.document` is the whole parsed
|
||||
// `index.md` and has been since the model existed, so the section reads what the last reload
|
||||
// read — no second parse, no directory walk, no freshness rule of its own.
|
||||
let model = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
let card = try #require(model.lanes.first?.cards.first)
|
||||
|
||||
#expect(CardDetails.rows(of: card.document).map(\.key) == ["labels", "assignees", "due", "remote", "project"])
|
||||
#expect(CardDetails.rows(of: card.document).first?.value == "[a, b, c]")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Details ▸ which text
|
||||
|
||||
@Suite("Card details ▸ values")
|
||||
struct CardDetailsValueTests {
|
||||
|
||||
private func rows(_ frontmatter: String) throws -> [String: String] {
|
||||
let text = "---\nschema: 1\norder: 1024\n\(frontmatter)---\nBody.\n"
|
||||
return Dictionary(uniqueKeysWithValues: CardDetails.rows(of: try FrontmatterDocument.parse(text))
|
||||
.map { ($0.key, $0.value) })
|
||||
}
|
||||
|
||||
@Test("A one-line value renders as the author's own bytes")
|
||||
func singleLineValuesAreVerbatim() throws {
|
||||
let values = try rows("""
|
||||
plain: lanework
|
||||
flow-map: {name: origin, branch: main}
|
||||
flow-seq: [a, b, c]
|
||||
hex: "#FF8800"
|
||||
quoted: "a # b"
|
||||
commented: kept # this comment is the line's, not the value's
|
||||
stamp: 2026-08-01T09:00:00Z
|
||||
number: 2048
|
||||
|
||||
""")
|
||||
|
||||
// The rawest honest form: quotes, braces, brackets, a hash inside a quoted scalar — all of it
|
||||
// exactly as typed, because a section that exists to show what the app did *not* interpret
|
||||
// must not quietly re-serialize it.
|
||||
#expect(values["plain"] == "lanework")
|
||||
#expect(values["flow-map"] == "{name: origin, branch: main}")
|
||||
#expect(values["flow-seq"] == "[a, b, c]")
|
||||
#expect(values["hex"] == "\"#FF8800\"")
|
||||
#expect(values["quoted"] == "\"a # b\"")
|
||||
#expect(values["stamp"] == "2026-08-01T09:00:00Z")
|
||||
#expect(values["number"] == "2048")
|
||||
// A trailing comment is the *line's*, and dropping it is the engine's own read-side rule —
|
||||
// the bytes on disk keep it.
|
||||
#expect(values["commented"] == "kept")
|
||||
}
|
||||
|
||||
@Test("A value spanning several lines reads as its text, not as its YAML syntax")
|
||||
func multiLineValuesFallBackToTheParsedReading() throws {
|
||||
let values = try rows("""
|
||||
notes: |
|
||||
first line
|
||||
second line
|
||||
folded: >-
|
||||
wrapped
|
||||
prose
|
||||
block-seq:
|
||||
- ada
|
||||
- grace
|
||||
block-map:
|
||||
name: origin
|
||||
branch: main
|
||||
wrapped-flow: [one,
|
||||
two]
|
||||
|
||||
""")
|
||||
|
||||
// The raw span of a block scalar carries `|`, `>-` and the continuation indent — YAML syntax
|
||||
// the *value* does not have. So these fall back to the engine's own reading, which is the
|
||||
// text the author meant rather than the punctuation that encoded it.
|
||||
#expect(values["notes"] == "first line\nsecond line")
|
||||
#expect(values["folded"] == "wrapped prose")
|
||||
#expect(values["block-seq"] == "[ada, grace]")
|
||||
#expect(values["block-map"] == "{name: origin, branch: main}")
|
||||
#expect(values["wrapped-flow"] == "[one, two]")
|
||||
}
|
||||
|
||||
@Test("A key with no value reads as null rather than as a blank")
|
||||
func emptyValuesReadAsNull() throws {
|
||||
let values = try rows("""
|
||||
sphere:
|
||||
explicit: null
|
||||
tilde: ~
|
||||
empty-string: ""
|
||||
only-a-comment: # nothing but this
|
||||
space-after: " "
|
||||
|
||||
""")
|
||||
|
||||
// A row showing a key and nothing beside it reads as a bug in the app; YAML's own word for
|
||||
// the value is the honest thing to draw. An explicitly *empty string* is a different value
|
||||
// and keeps its own bytes.
|
||||
#expect(values["sphere"] == "null")
|
||||
#expect(values["explicit"] == "null")
|
||||
#expect(values["tilde"] == "~")
|
||||
#expect(values["only-a-comment"] == "null")
|
||||
#expect(values["empty-string"] == "\"\"")
|
||||
#expect(values["space-after"] == "\" \"")
|
||||
}
|
||||
|
||||
@Test("A duplicated key reads once, with the winning occurrence's value")
|
||||
func duplicateKeysCollapseLastWins() throws {
|
||||
let document = try FrontmatterDocument.parse("""
|
||||
---
|
||||
schema: 1
|
||||
order: 1024
|
||||
project: first
|
||||
sphere: home
|
||||
project: second
|
||||
---
|
||||
Body.
|
||||
|
||||
""")
|
||||
let rows = CardDetails.rows(of: document)
|
||||
|
||||
// 01-storage-format.md's deliberate divergence from strict YAML, surfaced: the section shows
|
||||
// what the app *reads*, and the app reads the last occurrence. Two rows for one key would say
|
||||
// the card has a value it does not have.
|
||||
#expect(rows.map(\.key) == ["sphere", "project"])
|
||||
#expect(rows.map(\.value) == ["home", "second"])
|
||||
}
|
||||
|
||||
@Test("Frontmatter the editor cannot address still renders — readable-but-uneditable")
|
||||
func anUneditableDocumentStillShowsItsKeys() throws {
|
||||
// A whole-frontmatter flow mapping: no key has a line of its own, so `rawValue` has no span
|
||||
// to read and every value falls back to the parsed reading. The document refuses *writes*
|
||||
// (`uneditableShape`) — this section only reads, so it shows the keys like any other card's.
|
||||
let document = try FrontmatterDocument.parse("""
|
||||
---
|
||||
{schema: 1, order: 1024, project: lanework, labels: [a, b]}
|
||||
---
|
||||
Body.
|
||||
|
||||
""")
|
||||
#expect(document.uneditableShape != nil)
|
||||
#expect(CardDetails.rows(of: document).map(\.key) == ["project", "labels"])
|
||||
#expect(CardDetails.rows(of: document).map(\.value) == ["lanework", "[a, b]"])
|
||||
}
|
||||
|
||||
@Test("Nothing a parsed document can hold makes a row throw or vanish")
|
||||
func exoticShapesAreLenient() throws {
|
||||
// "Values render as plain text, leniently — exotic YAML shapes display best-effort, never
|
||||
// error" (05 ▸ Details). Every shape here has *some* row, and none of them is empty: an
|
||||
// unreadable value must still say that the key is there, since the raw source outlet is the
|
||||
// only way to fix it and the user has to know to go there.
|
||||
let values = try rows("""
|
||||
nested: {a: {b: [1, 2, {c: d}]}}
|
||||
booleans: [true, false, yes, no]
|
||||
unicode: "日本語 — ✂ \\u00e9"
|
||||
anchored: &a value
|
||||
aliased: *a
|
||||
colon-in-value: "key: not a key"
|
||||
tabbed: "a\\tb"
|
||||
big: 123456789012345678901234567890
|
||||
exponent: 1.2e+34
|
||||
infinity: .inf
|
||||
not-a-number: .nan
|
||||
dashes: "- not a list"
|
||||
|
||||
""")
|
||||
|
||||
#expect(values.count == 12)
|
||||
for (key, value) in values {
|
||||
#expect(!value.isEmpty, "\(key) rendered as nothing at all")
|
||||
}
|
||||
// An alias resolves to `.null` in the snapshot's value view, but its *source span* is what
|
||||
// the row shows — so an aliased value states the alias rather than lying about being empty.
|
||||
#expect(values["aliased"] == "*a")
|
||||
#expect(values["infinity"] == ".inf")
|
||||
#expect(values["not-a-number"] == ".nan")
|
||||
#expect(values["colon-in-value"] == "\"key: not a key\"")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Style ▸ the anchor
|
||||
|
||||
@Suite("Card sidebar ▸ style anchoring")
|
||||
struct CardStyleAnchorTests {
|
||||
|
||||
@Test("The section's target is this window's card, and only this window's card")
|
||||
func theTargetIsTheWindowsCard() {
|
||||
let card = ItemID(rawValue: Ident.card1)
|
||||
|
||||
// The card window has no selection and inherits none: "the two embedded anchors need none of
|
||||
// this and get none — the card sidebar dismisses with its card's window" (`StyleEditorSession`).
|
||||
// A target derived from anything that moves is the one way this anchor could restyle
|
||||
// something the user is not looking at.
|
||||
#expect(CardStyleSection.target(forCard: card) == .items([card]))
|
||||
|
||||
// Never the board — the fallback the *selection-aware* anchor takes with nothing selected,
|
||||
// and the one this anchor must never reach: a card window styling the whole board would
|
||||
// repaint every lane behind it.
|
||||
#expect(CardStyleSection.target(forCard: card) != .board)
|
||||
}
|
||||
|
||||
@Test("A card respelled in caps is the same target")
|
||||
func theTargetIsKeyedByIdentityNotSpelling() {
|
||||
// The window's own key rule (`CardWindowRef`), so the section cannot disagree with the window
|
||||
// it sits in about which card it is aimed at.
|
||||
let lower = ItemID(rawValue: Ident.card1)
|
||||
let upper = ItemID(rawValue: Ident.card1.uppercased())
|
||||
#expect(CardStyleSection.target(forCard: lower) == CardStyleSection.target(forCard: upper))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Style ▸ the anchor's geometry
|
||||
|
||||
@Suite("Card sidebar ▸ style editor layout")
|
||||
struct StyleEditorLayoutTests {
|
||||
|
||||
/// How wide `columns` wells and the gaps between them actually draw.
|
||||
private func gridWidth(columns: Int) -> CGFloat {
|
||||
CGFloat(columns) * StyleEditorLayout.wellSide + CGFloat(columns - 1) * StyleEditorLayout.wellSpacing
|
||||
}
|
||||
|
||||
@Test("The popover anchor is unchanged by the sidebar's arrival")
|
||||
func thePopoverKeepsItsSettledGeometry() {
|
||||
// 268 points and 7 + 6 background wells are 03-board-ui.md's own numbers ("narrow enough to
|
||||
// sit beside a card"), and the two anchors that were here first must not have moved because a
|
||||
// third one needed different ones.
|
||||
#expect(StyleEditorLayout.popover.width == 268)
|
||||
#expect(StyleEditorLayout.popover.padding == 14)
|
||||
#expect(StyleEditorLayout.popover.backgroundColumns == 7)
|
||||
#expect(StyleEditorLayout.popover.symbolColumns == 8)
|
||||
#expect(StyleEditorLayout.popover.symbolGridMaximumHeight == 168)
|
||||
}
|
||||
|
||||
@Test("The sidebar anchor takes the column it is given and adds nothing to it")
|
||||
func theSidebarBringsNoGeometryOfItsOwn() {
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: 169)
|
||||
|
||||
// No width: the sidebar's is `CardWindowMetrics`' one decision. No padding: the section stack
|
||||
// is already inset by a gutter, and insetting twice would narrow the grids for nothing.
|
||||
#expect(layout.width == nil)
|
||||
#expect(layout.padding == 0)
|
||||
// No inner scroller: the sidebar is already a scroll view, and a scroll view inside a scroll
|
||||
// view is a scroll view that fights (`CardWindowView`'s rule).
|
||||
#expect(layout.symbolGridMaximumHeight == nil)
|
||||
}
|
||||
|
||||
@Test("The grids fit the sidebar at every text size, and waste no room doing it")
|
||||
func theGridsFitTheSidebar() {
|
||||
for size in [11.0, 13.0, 16.0, 18.0, 24.0, 36.0] as [CGFloat] {
|
||||
let available = CardWindowMetrics.sidebarContentWidth(bodyPointSize: size)
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: available)
|
||||
|
||||
#expect(layout.backgroundColumns == layout.symbolColumns, "one column count for one column")
|
||||
// Fits — a grid wider than its column puts the last well of every row out of reach of
|
||||
// the pointer, at a text size nobody checks by eye.
|
||||
#expect(gridWidth(columns: layout.backgroundColumns) <= available, "overflows at \(size)pt")
|
||||
// And is maximal: one more well would not have fitted, so the wells are as large a set as
|
||||
// the column can show rather than an arbitrary count that happened to be safe.
|
||||
#expect(gridWidth(columns: layout.backgroundColumns + 1) > available, "under-packed at \(size)pt")
|
||||
}
|
||||
}
|
||||
|
||||
@Test("The sidebar's grids are narrower than the popover's, at the standard text size")
|
||||
func theSidebarIsTheNarrowerAnchor() {
|
||||
// Which is the entire reason this type exists: the popover's 7 wells across do not fit a
|
||||
// 26-character column, so an editor with one hard-coded frame could not have both anchors.
|
||||
let layout = StyleEditorLayout.sidebar(contentWidth: CardWindowMetrics.sidebarContentWidth(bodyPointSize: 13))
|
||||
#expect(layout.backgroundColumns < StyleEditorLayout.popover.backgroundColumns)
|
||||
#expect(layout.backgroundColumns >= 4, "a grid this narrow would be a strip, not a palette")
|
||||
}
|
||||
|
||||
@Test("A column too narrow for even one well still asks for one")
|
||||
func theFitRuleIsTotal() {
|
||||
// Total over any width, because the caller is a layout system: a zero proposal during a
|
||||
// window's first frame must not produce a grid of zero columns, which is a division by zero
|
||||
// waiting in `LazyVGrid`.
|
||||
#expect(StyleEditorLayout.columns(fitting: 0) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: -100) == 1)
|
||||
#expect(StyleEditorLayout.columns(fitting: StyleEditorLayout.wellSide) == 1)
|
||||
}
|
||||
|
||||
@Test("The sidebar's content width is the column minus its two gutters")
|
||||
func theContentWidthIsTheColumnMinusItsGutters() {
|
||||
for size in [11.0, 13.0, 24.0] as [CGFloat] {
|
||||
#expect(
|
||||
CardWindowMetrics.sidebarContentWidth(bodyPointSize: size)
|
||||
== CardWindowMetrics.sidebarWidth(bodyPointSize: size)
|
||||
- 2 * CardWindowMetrics.gutter(bodyPointSize: size)
|
||||
)
|
||||
}
|
||||
// 26 characters at half an em: 26 × 0.5 × 16 = 208, the text width the column was sized for.
|
||||
#expect(CardWindowMetrics.sidebarContentWidth(bodyPointSize: 16) == 208)
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,50 @@ struct TrashDeleteTests {
|
||||
#expect(store.selection.isEmpty)
|
||||
}
|
||||
|
||||
@Test("The card window's Delete is the same tombstone, and says nothing about the selection")
|
||||
func cardWindowDeleteIsTheSameWrite() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let before = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)")
|
||||
// Something *else* is selected on the board, which is the case the rule is about: the card
|
||||
// window's card need not be the board's selection at all.
|
||||
store.select([card3], liveness: .live, anchor: card3, head: card3)
|
||||
|
||||
store.deleteCard(card1)
|
||||
|
||||
// Byte-indistinguishable from the ⌫ tombstone above — same write op, same stamps, same
|
||||
// minimal touch (05-card-window.md ▸ Actions: "Delete — tombstones the card").
|
||||
let after = try fixture.indexText("\(Ident.lane1)/\(Ident.card1)")
|
||||
#expect(try FrontmatterDocument.parse(after).deleted.value != nil)
|
||||
#expect(!after.contains("modified-by"))
|
||||
#expect(untouchedLines(after) == untouchedLines(before))
|
||||
// ⌫ moves the selection to the successor sibling so a repeated keystroke walks down a lane.
|
||||
// A button in another window has no such continuation, and re-pointing a selection that never
|
||||
// lost anything would be the drag's mistake (`deleteByDrag`'s rule, shared).
|
||||
#expect(store.selection.ids == [card3])
|
||||
}
|
||||
|
||||
@Test("The card window's Delete writes nothing for a card that is already gone")
|
||||
func cardWindowDeleteIsLiveOnly() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let trashed = try stat(fixture, "\(Ident.lane1)/\(Ident.card2)")
|
||||
let underTombstonedLane = try stat(fixture, "\(Ident.lane3)/\(Ident.card4)")
|
||||
|
||||
// Its own tombstone, its lane's tombstone (effective liveness is ancestor-walked), and a card
|
||||
// this board has never heard of. All three are windows already dismissing — nothing is ever
|
||||
// written into a vanished folder.
|
||||
store.deleteCard(card2)
|
||||
store.deleteCard(card4)
|
||||
store.deleteCard(ItemID(rawValue: "00000000-0000-4000-8000-000000000000"))
|
||||
|
||||
#expect(try stat(fixture, "\(Ident.lane1)/\(Ident.card2)").modified == trashed.modified)
|
||||
#expect(try stat(fixture, "\(Ident.lane3)/\(Ident.card4)").modified == underTombstonedLane.modified)
|
||||
#expect(store.banners.oneShots.isEmpty)
|
||||
}
|
||||
|
||||
@Test("Already-tombstoned ids are skipped rather than re-stamped, and an empty set writes nothing")
|
||||
func liveOnly() throws {
|
||||
let fixture = try makeBoard()
|
||||
|
||||
Reference in New Issue
Block a user