WIP: add card capture rules
This commit is contained in:
@@ -19,7 +19,7 @@ The project is intentionally dependency-light: Swift Package Manager, AppKit, Ca
|
|||||||
- Custom named collections for organizing clips from the card context menu
|
- Custom named collections for organizing clips from the card context menu
|
||||||
- Copy and paste actions with Accessibility permission fallback
|
- Copy and paste actions with Accessibility permission fallback
|
||||||
- Image thumbnail cache with byte and file-count pruning
|
- Image thumbnail cache with byte and file-count pruning
|
||||||
- Configurable history length, cache limit, polling profile, ignored apps, content kinds, launch-at-login, Dock/menu-bar presence, and clear-on-quit behavior
|
- Configurable history length, cache limit, polling profile, ignored apps, content kinds, launch-at-login, Dock/menu-bar presence, and clear-on-quit behavior, with card-level capture rules for ignoring a source app or content type
|
||||||
- Local-only storage, with optional sensitive-content exclusion for common secrets
|
- Local-only storage, with optional sensitive-content exclusion for common secrets
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti
|
|||||||
8. Right-click another card and confirm Add to Collection offers Client Work as a reusable destination.
|
8. Right-click another card and confirm Add to Collection offers Client Work as a reusable destination.
|
||||||
9. Select the Client Work chip and confirm the rail filters to assigned items; quit and reopen ClipBored and confirm the assignment persists.
|
9. Select the Client Work chip and confirm the rail filters to assigned items; quit and reopen ClipBored and confirm the assignment persists.
|
||||||
10. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry.
|
10. Double-click an item and confirm it attempts to paste or falls back to copy without creating a duplicate history entry.
|
||||||
|
11. Right-click a card, use Capture Rules to ignore its source app, copy from that app again, and confirm the new item is skipped.
|
||||||
|
|
||||||
## Copy And Paste
|
## Copy And Paste
|
||||||
|
|
||||||
|
|||||||
@@ -595,6 +595,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
self?.viewModel.assignSelected(to: collectionName)
|
self?.viewModel.assignSelected(to: collectionName)
|
||||||
}
|
}
|
||||||
|
card.onIgnoreSourceApp = { [weak self] selected in
|
||||||
|
self?.viewModel.selectItem(at: selected)
|
||||||
|
self?.viewModel.ignoreSelectedSourceApp()
|
||||||
|
}
|
||||||
|
card.onIgnoreKind = { [weak self] selected in
|
||||||
|
self?.viewModel.selectItem(at: selected)
|
||||||
|
self?.viewModel.ignoreSelectedKind()
|
||||||
|
}
|
||||||
card.onDelete = { [weak self] selected in
|
card.onDelete = { [weak self] selected in
|
||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
self?.viewModel.deleteSelected()
|
self?.viewModel.deleteSelected()
|
||||||
@@ -710,7 +718,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
|
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
|
||||||
return .ready
|
return .ready
|
||||||
}
|
}
|
||||||
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") {
|
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") || lower.hasPrefix("ignored") {
|
||||||
return .action
|
return .action
|
||||||
}
|
}
|
||||||
if lower.hasPrefix("error") || lower.contains("failed") {
|
if lower.hasPrefix("error") || lower.contains("failed") {
|
||||||
@@ -1014,6 +1022,10 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
cardViews.first?.debugCollectionMenuTitles ?? []
|
cardViews.first?.debugCollectionMenuTitles ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var debugFirstCardCaptureRuleMenuTitles: [String] {
|
||||||
|
cardViews.first?.debugCaptureRuleMenuTitles ?? []
|
||||||
|
}
|
||||||
|
|
||||||
var debugFirstCardVisibleActionLabels: [String] {
|
var debugFirstCardVisibleActionLabels: [String] {
|
||||||
cardViews.first?.debugVisibleActionLabels ?? []
|
cardViews.first?.debugVisibleActionLabels ?? []
|
||||||
}
|
}
|
||||||
@@ -1388,6 +1400,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
var onReveal: (Int) -> Void = { _ in }
|
var onReveal: (Int) -> Void = { _ in }
|
||||||
var onTogglePin: (Int) -> Void = { _ in }
|
var onTogglePin: (Int) -> Void = { _ in }
|
||||||
var onAssignCollection: (Int, String?) -> Void = { _, _ in }
|
var onAssignCollection: (Int, String?) -> Void = { _, _ in }
|
||||||
|
var onIgnoreSourceApp: (Int) -> Void = { _ in }
|
||||||
|
var onIgnoreKind: (Int) -> Void = { _ in }
|
||||||
var onDelete: (Int) -> Void = { _ in }
|
var onDelete: (Int) -> Void = { _ in }
|
||||||
|
|
||||||
private let index: Int
|
private let index: Int
|
||||||
@@ -1395,6 +1409,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
private let itemIsPinned: Bool
|
private let itemIsPinned: Bool
|
||||||
private let itemIsStacked: Bool
|
private let itemIsStacked: Bool
|
||||||
private let stackCount: Int
|
private let stackCount: Int
|
||||||
|
private let itemSourceAppName: String?
|
||||||
|
private let itemSourceAppBundleID: String?
|
||||||
private let itemCollectionName: String?
|
private let itemCollectionName: String?
|
||||||
private let collectionNames: [String]
|
private let collectionNames: [String]
|
||||||
private let contentView = NSView()
|
private let contentView = NSView()
|
||||||
@@ -1422,6 +1438,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
self.itemIsPinned = item.isPinned
|
self.itemIsPinned = item.isPinned
|
||||||
self.itemIsStacked = isStacked
|
self.itemIsStacked = isStacked
|
||||||
self.stackCount = stackCount
|
self.stackCount = stackCount
|
||||||
|
self.itemSourceAppName = Self.presentSourceText(item.sourceApp)
|
||||||
|
self.itemSourceAppBundleID = Self.presentSourceText(item.sourceAppBundleId)
|
||||||
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
|
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
|
||||||
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
|
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
@@ -1551,6 +1569,13 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
return collectionMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
|
return collectionMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var debugCaptureRuleMenuTitles: [String] {
|
||||||
|
guard let rulesMenu = contextMenu().items.first(where: { $0.title == "Capture Rules" })?.submenu else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return rulesMenu.items.map { $0.isSeparatorItem ? "-" : $0.title }
|
||||||
|
}
|
||||||
|
|
||||||
var debugVisibleActionLabels: [String] {
|
var debugVisibleActionLabels: [String] {
|
||||||
actionRail.isHidden ? [] : actionRailButtons.map { $0.toolTip ?? "" }
|
actionRail.isHidden ? [] : actionRailButtons.map { $0.toolTip ?? "" }
|
||||||
}
|
}
|
||||||
@@ -1598,6 +1623,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
}
|
}
|
||||||
addMenuItem(itemIsPinned ? "Unpin" : "Pin", action: #selector(togglePinFromMenu), to: menu)
|
addMenuItem(itemIsPinned ? "Unpin" : "Pin", action: #selector(togglePinFromMenu), to: menu)
|
||||||
addCollectionMenu(to: menu)
|
addCollectionMenu(to: menu)
|
||||||
|
addCaptureRulesMenu(to: menu)
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
let open = addMenuItem("Open", action: #selector(openFromMenu), to: menu)
|
let open = addMenuItem("Open", action: #selector(openFromMenu), to: menu)
|
||||||
open.isEnabled = canOpen
|
open.isEnabled = canOpen
|
||||||
@@ -1639,6 +1665,48 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
menu.setSubmenu(submenu, for: parent)
|
menu.setSubmenu(submenu, for: parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func addCaptureRulesMenu(to menu: NSMenu) {
|
||||||
|
let parent = NSMenuItem(title: "Capture Rules", action: nil, keyEquivalent: "")
|
||||||
|
let submenu = NSMenu(title: "Capture Rules")
|
||||||
|
submenu.autoenablesItems = false
|
||||||
|
|
||||||
|
let ignoreSource = NSMenuItem(
|
||||||
|
title: ignoreSourceTitle(),
|
||||||
|
action: #selector(ignoreSourceAppFromMenu),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
ignoreSource.target = self
|
||||||
|
ignoreSource.isEnabled = itemSourceAppName != nil || itemSourceAppBundleID != nil
|
||||||
|
submenu.addItem(ignoreSource)
|
||||||
|
|
||||||
|
let ignoreKind = NSMenuItem(
|
||||||
|
title: "Ignore \(kindLabel(for: itemKind)) Items",
|
||||||
|
action: #selector(ignoreKindFromMenu),
|
||||||
|
keyEquivalent: ""
|
||||||
|
)
|
||||||
|
ignoreKind.target = self
|
||||||
|
ignoreKind.isEnabled = true
|
||||||
|
submenu.addItem(ignoreKind)
|
||||||
|
|
||||||
|
menu.addItem(parent)
|
||||||
|
menu.setSubmenu(submenu, for: parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func ignoreSourceTitle() -> String {
|
||||||
|
if let itemSourceAppName {
|
||||||
|
return "Ignore \(itemSourceAppName)"
|
||||||
|
}
|
||||||
|
if let itemSourceAppBundleID {
|
||||||
|
return "Ignore \(itemSourceAppBundleID)"
|
||||||
|
}
|
||||||
|
return "Ignore Source App"
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func presentSourceText(_ value: String?) -> String? {
|
||||||
|
guard let text = value?.clipboardTrimmed, !text.isEmpty else { return nil }
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
private func availableCollectionNames() -> [String] {
|
private func availableCollectionNames() -> [String] {
|
||||||
var names: [String] = []
|
var names: [String] = []
|
||||||
var seen = Set<String>()
|
var seen = Set<String>()
|
||||||
@@ -1876,6 +1944,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
onAssignCollection(index, nil)
|
onAssignCollection(index, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func ignoreSourceAppFromMenu() {
|
||||||
|
onIgnoreSourceApp(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func ignoreKindFromMenu() {
|
||||||
|
onIgnoreKind(index)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func deleteFromMenu() {
|
@objc private func deleteFromMenu() {
|
||||||
onDelete(index)
|
onDelete(index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -426,6 +426,34 @@ final class ClipboardPanelViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ignoreSelectedSourceApp() {
|
||||||
|
guard let item = selectedItem else { return }
|
||||||
|
guard let rule = sourceIgnoreRule(for: item) else {
|
||||||
|
statusMessage = "Source app unavailable"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let existing = settings.ignoredApps.map { $0.clipboardTrimmed.lowercased() }
|
||||||
|
guard !existing.contains(rule.value.lowercased()) else {
|
||||||
|
statusMessage = "\(rule.displayName) is already ignored"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ignoredApps.append(rule.value)
|
||||||
|
statusMessage = "Ignored \(rule.displayName) for future captures"
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoreSelectedKind() {
|
||||||
|
guard let item = selectedItem else { return }
|
||||||
|
guard !settings.ignoredItemKindsRaw.contains(item.kind.rawValue) else {
|
||||||
|
statusMessage = "\(Self.statusKindName(item.kind)) items are already ignored"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.ignoredItemKindsRaw.append(item.kind.rawValue)
|
||||||
|
statusMessage = "Ignored \(Self.statusKindName(item.kind)) items for future captures"
|
||||||
|
}
|
||||||
|
|
||||||
func selectCollection(named name: String) {
|
func selectCollection(named name: String) {
|
||||||
guard let normalizedName = ClipboardCollectionDefaults.normalizedName(name) else { return }
|
guard let normalizedName = ClipboardCollectionDefaults.normalizedName(name) else { return }
|
||||||
isStackFilterSelected = false
|
isStackFilterSelected = false
|
||||||
@@ -791,6 +819,25 @@ final class ClipboardPanelViewModel {
|
|||||||
return min(lhs, rhs)
|
return min(lhs, rhs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func sourceIgnoreRule(for item: ClipboardItem) -> (value: String, displayName: String)? {
|
||||||
|
if let bundleID = item.sourceAppBundleId?.clipboardTrimmed, !bundleID.isEmpty {
|
||||||
|
let sourceApp = item.sourceApp?.clipboardTrimmed
|
||||||
|
let display = sourceApp?.isEmpty == false ? sourceApp ?? bundleID : bundleID
|
||||||
|
return (bundleID, display)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let sourceApp = item.sourceApp?.clipboardTrimmed, !sourceApp.isEmpty {
|
||||||
|
return (sourceApp, sourceApp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func statusKindName(_ kind: ClipboardItemKind) -> String {
|
||||||
|
let name = kind.displayName
|
||||||
|
return name == name.uppercased() ? name : name.capitalized
|
||||||
|
}
|
||||||
|
|
||||||
private func searchTokens(from query: String) -> [String] {
|
private func searchTokens(from query: String) -> [String] {
|
||||||
query
|
query
|
||||||
.split { character in
|
.split { character in
|
||||||
|
|||||||
@@ -504,6 +504,63 @@ final class ClipboardPanelViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(viewModel.statusMessage, "Cleared Stack")
|
XCTAssertEqual(viewModel.statusMessage, "Cleared Stack")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testIgnoreSelectedSourceAppAddsPreciseCaptureRule() {
|
||||||
|
let settings = makeSettings()
|
||||||
|
let cacheService = makeCacheService()
|
||||||
|
let store = makeStore(settings: settings, cacheService: cacheService)
|
||||||
|
var item = makeTextItem("source rule clip", createdAt: Date(timeIntervalSince1970: 100))
|
||||||
|
item.sourceApp = "Slack"
|
||||||
|
item.sourceAppBundleId = "com.tinyspeck.slackmacgap"
|
||||||
|
store.upsert(item)
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
|
||||||
|
waitForVisibleItems(in: viewModel, count: 1)
|
||||||
|
let initialIgnoredApps = settings.ignoredApps
|
||||||
|
|
||||||
|
viewModel.ignoreSelectedSourceApp()
|
||||||
|
|
||||||
|
XCTAssertEqual(settings.ignoredApps, initialIgnoredApps + ["com.tinyspeck.slackmacgap"])
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Ignored Slack for future captures")
|
||||||
|
|
||||||
|
viewModel.ignoreSelectedSourceApp()
|
||||||
|
XCTAssertEqual(settings.ignoredApps, initialIgnoredApps + ["com.tinyspeck.slackmacgap"])
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Slack is already ignored")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testIgnoreSelectedKindAddsContentTypeCaptureRule() {
|
||||||
|
let settings = makeSettings()
|
||||||
|
let cacheService = makeCacheService()
|
||||||
|
let store = makeStore(settings: settings, cacheService: cacheService)
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .image,
|
||||||
|
displayText: "Image",
|
||||||
|
payload: "/tmp/image.png",
|
||||||
|
payloadHash: hash("image"),
|
||||||
|
createdAt: Date(timeIntervalSince1970: 100),
|
||||||
|
lastUsedAt: Date(timeIntervalSince1970: 100),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: "Photos",
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
store.upsert(item)
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
|
||||||
|
waitForVisibleItems(in: viewModel, count: 1)
|
||||||
|
|
||||||
|
viewModel.ignoreSelectedKind()
|
||||||
|
|
||||||
|
XCTAssertEqual(settings.ignoredItemKindsRaw, [ClipboardItemKind.image.rawValue])
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Ignored Image items for future captures")
|
||||||
|
|
||||||
|
viewModel.ignoreSelectedKind()
|
||||||
|
XCTAssertEqual(settings.ignoredItemKindsRaw, [ClipboardItemKind.image.rawValue])
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Image items are already ignored")
|
||||||
|
}
|
||||||
|
|
||||||
func testSelectingStackFiltersVisibleItemsInQueueOrder() {
|
func testSelectingStackFiltersVisibleItemsInQueueOrder() {
|
||||||
let settings = makeSettings()
|
let settings = makeSettings()
|
||||||
let cacheService = makeCacheService()
|
let cacheService = makeCacheService()
|
||||||
|
|||||||
@@ -345,12 +345,16 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardMenuTitles,
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
["Paste", "Copy", "Add to Stack", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
["Paste", "Copy", "Add to Stack", "Edit", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
)
|
)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardCollectionMenuTitles,
|
fixture.view.debugFirstCardCollectionMenuTitles,
|
||||||
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
|
["Useful Links", "Important Notes", "Code Snippets", "Read Later", "-", "New Collection..."]
|
||||||
)
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
fixture.view.debugFirstCardCaptureRuleMenuTitles,
|
||||||
|
["Ignore Ghostty", "Ignore Text Items"]
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testPreviewableCardsExposeQuickLookContextMenuAction() {
|
func testPreviewableCardsExposeQuickLookContextMenuAction() {
|
||||||
@@ -361,7 +365,11 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardMenuTitles,
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Add to Stack", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Add to Stack", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
fixture.view.debugFirstCardCaptureRuleMenuTitles,
|
||||||
|
["Ignore Ghostty", "Ignore File Items"]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +385,7 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardMenuTitles,
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
["Paste", "Copy", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
["Paste", "Copy", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
)
|
)
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Remove from Stack", "Edit", "Delete"])
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Remove from Stack", "Edit", "Delete"])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user