Files
lanework/KanbanTests/ViewEquatableTests.swift
T
rzen 9c857ae0cc Selected-ness rides down as a compared parameter — a marquee crossing repaints its faces, not the board
A selection change re-ran every CardFaceView on the board (180 bodies ≈ 85 ms on
the 6×30 fixture, 515 ≈ 233 ms on a real 515-card board, debug): the face's body
read store.selection in three places — isSelected, the drag replica's count, and
the context menu's styleTarget — and Observation invalidates every reader of the
property, past the equatable gate entirely. The band overlay stayed cheap, which
is why the marquee tracked the cursor while the highlight lagged ~0.4 s behind.

Now LaneView and TrashLaneView hoist one selection read per body and hand each
face isSelected/selectedCount as compared parameters; StyleMenuItems takes its
target as a deferred closure; TrashLaneRowView gains the same treatment plus the
Equatable gate it never needed before. Select-one-card: 180 bodies → 1. A
growing band costs the selection's own running size; the real board's crossing
fell 233 → 112 ms — the remainder is lane bodies re-measuring their masonry, a
separate lane-level finding recorded in RENDER-INSTRUMENTATION.md.

Also: select() gains defaultsSoleMember — the marquee's explicit nils never
avoided the sole-member default, so a one-card band acquired a selectionHead and
could scroll the lane out from under its own drag.

MarqueeRenderCostTests pins the shape: redundant samples cost zero bodies, a
growing band pays per crossing, and selectionStillRepaints holds a ≤8 budget.
2026-08-07 15:10:35 -04:00

587 lines
28 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import AppKit
import Foundation
import Testing
@testable import Kanban
/// The board strip's three rebuild gates — `LaneView.==`, `CardFaceView.==` and
/// `TrashLaneRowView.==`, applied through `.equatable()` at `BoardView.laneSlot`,
/// `LaneView.scrollableCards` and `TrashLaneView.scrollableCards`.
///
/// They exist for the drag: `BoardView`'s body reads the drag session, so **every drop-proposal
/// change re-evaluates the whole strip**, and a lane's body reads it too, so every proposal change
/// reached every card face as well. The gates are what turn "the cursor moved" into "the one lane
/// under it re-runs" rather than "every lane and every face on the board re-runs".
///
/// What these tests pin is the *comparison list*, because that is where a gate goes wrong in either
/// direction. Too strict and the gate does nothing — the strip rebuilds the drop context and the
/// card opener on every body pass, so any view comparing closures is unequal every time. Too loose
/// and the board stops repainting — an edited card must make its lane a different value, and a
/// newly selected card must make its face one.
///
/// **Selection is on the compared side now, and these tests are where that is held.** It used to be
/// the headline example of the Observation half these gates deliberately do not cover: every card
/// face read `store.selection` for itself, so one click invalidated all of them directly and
/// `.equatable()` never got a say (measured at 180 bodies per marquee sample on the 6×30 fixture —
/// RENDER-INSTRUMENTATION.md ▸ Selection is O(board) in card bodies). A face now takes `isSelected`
/// and `selectedCount` from its parent, which means the gate is what decides whether a selection
/// change repaints a given face — so it has to be unequal exactly when the flags differ, and
/// `MarqueeRenderCostTests` measures the consequence.
///
/// They still do **not** pin the rest of the Observation half, and cannot: `store.transient`'s
/// pending cut and rename editor, `drops.session`'s proposal and the rest invalidate these views
/// directly. That is the design — the gate only suppresses *parent-driven* re-evaluation.
///
/// Boards are real loads off real temp trees, `SearchFilterTests`' reason: a hand-assembled `Lane`
/// would be comparing something `BoardLoader` can never produce, and "an edit makes the lane
/// unequal" is only worth asserting against the values a reload actually lands.
// MARK: - Fixtures
/// Two lanes, two cards in the first — enough for a lane value that changes when a card under it
/// does, and for two card values that differ from each other.
@MainActor
private func makeFixture() throws -> WriterFixture {
let fixture = try WriterFixture()
try fixture.board()
try fixture.lane(Ident.lane1, order: "1024", title: "Todo")
try fixture.card(Ident.card1, in: Ident.lane1, order: "1024", title: "Fix login", body: "The auth flow.")
try fixture.card(Ident.card2, in: Ident.lane1, order: "2048", title: "Polish copy", body: "Tighten it.")
try fixture.lane(Ident.lane2, order: "2048", title: "Doing")
return fixture
}
/// A drop context with the four members the gates compare made explicit, and the three closures
/// freshly allocated on every call — which is exactly what `BoardView.dropContext` does per body
/// pass, and the thing the gates must survive.
@MainActor
private func makeDrops(
store: BoardStore,
session: DragSession,
registry: LaneDropRegistry,
gap: CGFloat = 12
) -> BoardDropContext {
BoardDropContext(
store: store,
session: session,
registry: registry,
gap: gap,
window: { nil },
stripFrame: { .zero },
standard: { 260 }
)
}
@MainActor
private func makeLane(
store: BoardStore,
lane: Lane,
columns: Int = 1,
slotWidth: CGFloat = 260,
drops: BoardDropContext,
marquee: MarqueeControl,
openCard: @escaping @MainActor (ItemID) -> Void = { _ in }
) -> LaneView {
LaneView(
store: store,
lane: lane,
columns: columns,
slotWidth: slotWidth,
drops: drops,
marquee: marquee,
openCard: openCard
)
}
private func firstLane(_ model: BoardModel) throws -> Lane {
try #require(model.lanes.first { $0.id == ItemID(rawValue: Ident.lane1) })
}
private func firstCard(_ model: BoardModel) throws -> Card {
try #require(try firstLane(model).cards.first { $0.id == ItemID(rawValue: Ident.card1) })
}
// MARK: - The strip's gate
@MainActor
@Suite("LaneView — the strip's rebuild gate")
struct LaneViewEquatableTests {
@Test("Identical inputs compare equal — the pass a proposal change would otherwise rebuild")
func identicalInputsAreEqual() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let lane = try firstLane(fixture.snapshot())
let drops = makeDrops(store: store, session: DragSession(), registry: LaneDropRegistry())
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
#expect(makeLane(store: store, lane: lane, drops: drops, marquee: marquee)
== makeLane(store: store, lane: lane, drops: drops, marquee: marquee))
}
@Test("A rebuilt drop context and a fresh opener still compare equal — the whole point of the gate")
func freshClosuresAreNotADifference() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let lane = try firstLane(fixture.snapshot())
let session = DragSession()
let registry = LaneDropRegistry()
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
// Two body passes of `BoardView`: same store, same session, same registry, same gap — three
// brand-new closures in the context and a brand-new `openCard` besides. A gate that compared
// any of them would be unequal here, which is to say it would never suppress anything.
let before = makeLane(
store: store,
lane: lane,
drops: makeDrops(store: store, session: session, registry: registry),
marquee: marquee,
openCard: { _ in }
)
let after = makeLane(
store: store,
lane: lane,
drops: makeDrops(store: store, session: session, registry: registry),
marquee: marquee,
openCard: { _ in Issue.record("the gate must not care which opener it holds") }
)
#expect(before == after)
}
@Test("An edited card makes its lane unequal — the gate never withholds a repaint")
func anEditUnderTheLaneIsADifference() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let drops = makeDrops(store: store, session: DragSession(), registry: LaneDropRegistry())
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let before = try firstLane(fixture.snapshot())
// A foreign edit, the way a reload lands one: `Lane` is `Equatable` through its cards, so a
// card's new title is a new lane value even though the lane's own frontmatter is untouched.
try fixture.card(Ident.card1, in: Ident.lane1, order: "1024", title: "Fix auth", body: "The auth flow.")
let after = try firstLane(fixture.snapshot())
#expect(before != after)
#expect(makeLane(store: store, lane: before, drops: drops, marquee: marquee)
!= makeLane(store: store, lane: after, drops: drops, marquee: marquee))
}
@Test("The two layout figures the strip resolves are compared: a resize tick and a width change")
func theStripsLayoutFiguresAreCompared() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let lane = try firstLane(fixture.snapshot())
let drops = makeDrops(store: store, session: DragSession(), registry: LaneDropRegistry())
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let base = makeLane(store: store, lane: lane, columns: 1, slotWidth: 260, drops: drops, marquee: marquee)
// `columns` follows the resize session's snapped unit count and `slotWidth` the strip's
// standard width — neither is readable off `lane`, so neither can ride in on its value.
#expect(base != makeLane(store: store, lane: lane, columns: 2, slotWidth: 260, drops: drops, marquee: marquee))
#expect(base != makeLane(store: store, lane: lane, columns: 1, slotWidth: 532, drops: drops, marquee: marquee))
}
@Test("The window-lived collaborators are compared by identity, the strip's gap by value")
func theCollaboratorsAreComparedByIdentity() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let other = try BoardStore(rootURL: fixture.root)
let lane = try firstLane(fixture.snapshot())
let session = DragSession()
let registry = LaneDropRegistry()
let bandSession = MarqueeSession()
let bandRegistry = MarqueeTargetRegistry()
let marquee = MarqueeControl(session: bandSession, registry: bandRegistry, store: store)
let drops = makeDrops(store: store, session: session, registry: registry)
let base = makeLane(store: store, lane: lane, drops: drops, marquee: marquee)
// A different board window in every direction the two structs can differ.
#expect(base != makeLane(store: other, lane: lane, drops: drops, marquee: marquee))
#expect(base != makeLane(
store: store, lane: lane,
drops: makeDrops(store: store, session: DragSession(), registry: registry),
marquee: marquee
))
#expect(base != makeLane(
store: store, lane: lane,
drops: makeDrops(store: store, session: session, registry: LaneDropRegistry()),
marquee: marquee
))
#expect(base != makeLane(
store: store, lane: lane,
drops: makeDrops(store: store, session: session, registry: registry, gap: 20),
marquee: marquee
))
#expect(base != makeLane(
store: store, lane: lane, drops: drops,
marquee: MarqueeControl(session: MarqueeSession(), registry: bandRegistry, store: store)
))
#expect(base != makeLane(
store: store, lane: lane, drops: drops,
marquee: MarqueeControl(session: bandSession, registry: MarqueeTargetRegistry(), store: store)
))
}
}
// MARK: - The lane's gate
@MainActor
@Suite("CardFaceView — the lane's rebuild gate")
struct CardFaceViewEquatableTests {
@Test("Identical inputs compare equal, fresh opener and fresh drop context included")
func identicalInputsAreEqual() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let card = try firstCard(fixture.snapshot())
let session = DragSession()
let registry = LaneDropRegistry()
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
// The `.board` role's payload is a closure the lane rebuilds on every pass; comparing it
// would make every face on the board unequal on every proposal change, which is the rebuild
// this gate exists to stop.
let before = CardFaceView(
store: store,
card: card,
role: .board(openCard: { _ in }),
marquee: marquee,
drops: makeDrops(store: store, session: session, registry: registry),
isSelected: false,
selectedCount: 1
)
let after = CardFaceView(
store: store,
card: card,
role: .board(openCard: { _ in Issue.record("the gate must not care which opener it holds") }),
marquee: marquee,
drops: makeDrops(store: store, session: session, registry: registry),
isSelected: false,
selectedCount: 1
)
#expect(before == after)
}
@Test("Selected-ness is a compared input — the whole reason the faces stopped reading the store")
func selectednessIsADifference() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let card = try firstCard(fixture.snapshot())
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let drops = makeDrops(store: store, session: DragSession(), registry: LaneDropRegistry())
let unselected = CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1
)
// The gate is now the *only* thing standing between a click and this face's repaint: nothing
// in this body reads `store.selection` any more, so a gate that swallowed the flag would
// leave a selected card wearing no accent ring at all.
#expect(unselected != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: true, selectedCount: 1
))
// And the count, which the drag replica's fan and count badge are drawn from: a card that is
// still selected but now travels with four others has a different image under the cursor.
let alone = CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: true, selectedCount: 1
)
#expect(alone != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: true, selectedCount: 5
))
}
@Test("An edited card is unequal — the gate never withholds a repaint")
func anEditedCardIsADifference() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let session = DragSession()
let registry = LaneDropRegistry()
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let drops = makeDrops(store: store, session: session, registry: registry)
let before = try firstCard(fixture.snapshot())
try fixture.card(Ident.card1, in: Ident.lane1, order: "1024", title: "Fix auth", body: "The auth flow.")
let after = try firstCard(fixture.snapshot())
#expect(before != after)
#expect(CardFaceView(store: store, card: before, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1)
!= CardFaceView(store: store, card: after, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1))
// And a different card, which is the ordinary within-lane case.
let sibling = try #require(try firstLane(fixture.snapshot()).cards.first {
$0.id == ItemID(rawValue: Ident.card2)
})
#expect(CardFaceView(store: store, card: after, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1)
!= CardFaceView(store: store, card: sibling, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1))
}
@Test("The two homes are never equal, and the trash's confirmation host is compared by identity")
func theRolesHomeIsCompared() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let card = try firstCard(fixture.snapshot())
let session = DragSession()
let registry = LaneDropRegistry()
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let drops = makeDrops(store: store, session: session, registry: registry)
let confirmations = TrashConfirmations()
let board = CardFaceView(store: store, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1)
let trash = CardFaceView(store: store, card: card, role: .trash(confirmations: confirmations),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1)
// The role decides which container a click selects in, whether the face has an Open gesture
// at all, and whether Delete is the permanent one — never a difference to swallow.
#expect(board != trash)
#expect(trash == CardFaceView(store: store, card: card,
role: .trash(confirmations: confirmations),
marquee: marquee, drops: drops,
isSelected: false, selectedCount: 1))
// Window-lived state, so identity is meaningful as well as cheap.
#expect(trash != CardFaceView(store: store, card: card,
role: .trash(confirmations: TrashConfirmations()),
marquee: marquee, drops: drops,
isSelected: false, selectedCount: 1))
}
@Test("The window-lived collaborators are compared by identity, the strip's gap by value")
func theCollaboratorsAreComparedByIdentity() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let other = try BoardStore(rootURL: fixture.root)
let card = try firstCard(fixture.snapshot())
let session = DragSession()
let registry = LaneDropRegistry()
let bandSession = MarqueeSession()
let bandRegistry = MarqueeTargetRegistry()
let marquee = MarqueeControl(session: bandSession, registry: bandRegistry, store: store)
let drops = makeDrops(store: store, session: session, registry: registry)
let base = CardFaceView(store: store, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops, isSelected: false, selectedCount: 1)
#expect(base != CardFaceView(store: other, card: card, role: .board(openCard: { _ in }),
marquee: marquee, drops: drops,
isSelected: false, selectedCount: 1))
#expect(base != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }), marquee: marquee,
drops: makeDrops(store: store, session: DragSession(), registry: registry),
isSelected: false, selectedCount: 1
))
#expect(base != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }), marquee: marquee,
drops: makeDrops(store: store, session: session, registry: LaneDropRegistry()),
isSelected: false, selectedCount: 1
))
#expect(base != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }), marquee: marquee,
drops: makeDrops(store: store, session: session, registry: registry, gap: 20),
isSelected: false, selectedCount: 1
))
#expect(base != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }),
marquee: MarqueeControl(session: MarqueeSession(), registry: bandRegistry, store: store),
drops: drops, isSelected: false, selectedCount: 1
))
#expect(base != CardFaceView(
store: store, card: card, role: .board(openCard: { _ in }),
marquee: MarqueeControl(session: bandSession, registry: MarqueeTargetRegistry(), store: store),
drops: drops, isSelected: false, selectedCount: 1
))
}
}
// MARK: - The trash column's gate
/// `TrashLaneRowView.==` — the card face's gate at the other level, and new with the selection
/// change: the column's body did not read the selection until the rows stopped reading it for
/// themselves, so until then there was nothing here worth suppressing
/// (`TrashLaneView.scrollableCards`).
@MainActor
@Suite("TrashLaneRowView — the trash column's rebuild gate")
struct TrashLaneRowViewEquatableTests {
/// The opaque unit the column draws — hand-assembled rather than loaded, because a trashed lane
/// is exactly the row's whole input and `BoardLoader` needs a real deletion to produce one.
private func makeRow(title: String, heldCards: Int = 5) -> TrashedLane {
TrashedLane(
id: ItemID(rawValue: Ident.lane3),
schema: 1,
title: .valid(title),
modified: .missing,
order: 1024,
heldCards: heldCards,
document: FrontmatterDocument(body: "")
)
}
@MainActor
private func makeView(
store: BoardStore,
lane: TrashedLane,
confirmations: TrashConfirmations,
drops: BoardDropContext,
marquee: MarqueeControl,
isSelected: Bool = false,
selectedCount: Int = 1
) -> TrashLaneRowView {
TrashLaneRowView(
store: store,
lane: lane,
confirmations: confirmations,
drops: drops,
marquee: marquee,
isSelected: isSelected,
selectedCount: selectedCount
)
}
@Test("Identical inputs compare equal, a freshly rebuilt drop context included")
func identicalInputsAreEqual() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let session = DragSession()
let registry = LaneDropRegistry()
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let confirmations = TrashConfirmations()
let lane = makeRow(title: "Retired")
// Two body passes of the window: same collaborators, three brand-new closures in the drop
// context each time — a gate that compared any of them would never suppress anything.
#expect(makeView(
store: store, lane: lane, confirmations: confirmations,
drops: makeDrops(store: store, session: session, registry: registry), marquee: marquee
) == makeView(
store: store, lane: lane, confirmations: confirmations,
drops: makeDrops(store: store, session: session, registry: registry), marquee: marquee
))
}
@Test("A different lane value is unequal — the gate never withholds a repaint")
func aDifferentLaneValueIsADifference() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let drops = makeDrops(store: store, session: DragSession(), registry: LaneDropRegistry())
let confirmations = TrashConfirmations()
let base = makeView(store: store, lane: makeRow(title: "Retired"),
confirmations: confirmations, drops: drops, marquee: marquee)
// The row draws exactly two things — the title and the held-card count — so both have to be
// differences, and `TrashedLane` being `Equatable` is what makes them one comparison.
#expect(base != makeView(store: store, lane: makeRow(title: "Retired lanes"),
confirmations: confirmations, drops: drops, marquee: marquee))
#expect(base != makeView(store: store, lane: makeRow(title: "Retired", heldCards: 6),
confirmations: confirmations, drops: drops, marquee: marquee))
}
@Test("Selected-ness and the selection's size are compared, the card face's rule")
func selectednessIsADifference() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let marquee = MarqueeControl(
session: MarqueeSession(), registry: MarqueeTargetRegistry(), store: store
)
let drops = makeDrops(store: store, session: DragSession(), registry: LaneDropRegistry())
let confirmations = TrashConfirmations()
let lane = makeRow(title: "Retired")
let base = makeView(store: store, lane: lane, confirmations: confirmations,
drops: drops, marquee: marquee)
#expect(base != makeView(store: store, lane: lane, confirmations: confirmations,
drops: drops, marquee: marquee, isSelected: true))
#expect(makeView(store: store, lane: lane, confirmations: confirmations, drops: drops,
marquee: marquee, isSelected: true, selectedCount: 1)
!= makeView(store: store, lane: lane, confirmations: confirmations, drops: drops,
marquee: marquee, isSelected: true, selectedCount: 4))
}
@Test("The window-lived collaborators are compared by identity, the strip's gap by value")
func theCollaboratorsAreComparedByIdentity() throws {
let fixture = try makeFixture()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let other = try BoardStore(rootURL: fixture.root)
let session = DragSession()
let registry = LaneDropRegistry()
let bandSession = MarqueeSession()
let bandRegistry = MarqueeTargetRegistry()
let marquee = MarqueeControl(session: bandSession, registry: bandRegistry, store: store)
let drops = makeDrops(store: store, session: session, registry: registry)
let confirmations = TrashConfirmations()
let lane = makeRow(title: "Retired")
let base = makeView(store: store, lane: lane, confirmations: confirmations,
drops: drops, marquee: marquee)
#expect(base != makeView(store: other, lane: lane, confirmations: confirmations,
drops: drops, marquee: marquee))
// Window-lived state the row's permanent Delete goes through — identity is meaningful here
// for `CardFaceRole.isEquivalent(to:)`'s reason exactly.
#expect(base != makeView(store: store, lane: lane, confirmations: TrashConfirmations(),
drops: drops, marquee: marquee))
#expect(base != makeView(
store: store, lane: lane, confirmations: confirmations,
drops: makeDrops(store: store, session: DragSession(), registry: registry),
marquee: marquee
))
#expect(base != makeView(
store: store, lane: lane, confirmations: confirmations,
drops: makeDrops(store: store, session: session, registry: LaneDropRegistry()),
marquee: marquee
))
#expect(base != makeView(
store: store, lane: lane, confirmations: confirmations,
drops: makeDrops(store: store, session: session, registry: registry, gap: 20),
marquee: marquee
))
#expect(base != makeView(
store: store, lane: lane, confirmations: confirmations, drops: drops,
marquee: MarqueeControl(session: MarqueeSession(), registry: bandRegistry, store: store)
))
#expect(base != makeView(
store: store, lane: lane, confirmations: confirmations, drops: drops,
marquee: MarqueeControl(session: bandSession, registry: MarqueeTargetRegistry(), store: store)
))
}
}