A trailing swipe on a board row opens Board Settings, whose first setting is location: iCloud or Local, with a confirmed move to the other side — destructive-styled only outbound, because leaving iCloud is the direction that sheds protection. The move is setUbiquitous against the real container and a coordinated move under the DEBUG stand-in; evacuation sweeps materialization first and refuses honestly while content is still downloading. The local home is the sandbox Documents folder, published to the Files app, so a local board is still a folder the user owns. With a second home the iCloud wall softens (user-ruled 2026-08-08): the index always reaches ready, cloud unavailability becomes an inline notice with a retry, creates land locally when there is no account, and LANEWORK_FORCE_NO_ICLOUD makes that state reproducible in tests regardless of the machine's sign-in. Known gap, now user-reachable: backup remains iCloud-only, so local boards sit outside it. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
114 lines
4.9 KiB
Swift
114 lines
4.9 KiB
Swift
import XCTest
|
|
|
|
/// Where a board is stored, and the one operation that changes it.
|
|
///
|
|
/// Both tests drive the real `BoardIndexStore.relocateBoard(at:to:)` over two real directories the
|
|
/// test owns — the DEBUG stand-in cloud root and the DEBUG device root — so the move that lands is a
|
|
/// coordinated move of an actual `.kanban` package, not a model edit. The `setUbiquitous` path a
|
|
/// shipped build takes cannot be exercised without an iCloud account; what these cover is everything
|
|
/// on either side of it.
|
|
final class BoardLocationUITests: XCTestCase {
|
|
|
|
@MainActor
|
|
func testMoveBoardOutOfICloudAndBack() throws {
|
|
let (app, cloudRoot, deviceRoot) = XCUIApplication.launchedWithFixtureBoardInCloud()
|
|
let inCloud = cloudRoot.appendingPathComponent(RichBoard.packageName, isDirectory: true)
|
|
let onDevice = deviceRoot.appendingPathComponent(RichBoard.packageName, isDirectory: true)
|
|
|
|
// Out of iCloud.
|
|
app.openBoardSettings(for: RichBoard.title)
|
|
// `LabeledContent` publishes label and value as one flattened node — "Stored In, iCloud" —
|
|
// which is the sheet's whole claim about where the board is, in one assertion.
|
|
XCTAssertTrue(
|
|
app.staticTexts["Stored In, iCloud"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the sheet did not report the board as stored in iCloud"
|
|
)
|
|
app.buttons["Move to Local"].tap()
|
|
app.confirmMove()
|
|
|
|
XCTAssertTrue(
|
|
app.navigationBars["Board Settings"].waitForNonExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the sheet stayed up after a move that should have succeeded"
|
|
)
|
|
XCTAssertTrue(
|
|
waitForDirectory(at: onDevice),
|
|
"the package never arrived under the device root at \(onDevice.path)"
|
|
)
|
|
XCTAssertTrue(
|
|
waitForDirectory(at: inCloud, toExist: false),
|
|
"the package was still under the cloud root at \(inCloud.path)"
|
|
)
|
|
XCTAssertTrue(
|
|
app.boardRow(RichBoard.title, alsoContaining: "Local")
|
|
.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the row never picked up the Local marker"
|
|
)
|
|
|
|
// And back.
|
|
app.openBoardSettings(for: RichBoard.title)
|
|
XCTAssertTrue(
|
|
app.staticTexts["Stored In, Local"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the sheet did not report the moved board as stored locally"
|
|
)
|
|
let moveToCloud = app.buttons["Move to iCloud"]
|
|
XCTAssertTrue(
|
|
moveToCloud.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the sheet did not offer the return trip"
|
|
)
|
|
moveToCloud.tap()
|
|
app.confirmMove()
|
|
|
|
XCTAssertTrue(
|
|
waitForDirectory(at: inCloud),
|
|
"the package never came back under the cloud root at \(inCloud.path)"
|
|
)
|
|
XCTAssertTrue(
|
|
waitForDirectory(at: onDevice, toExist: false),
|
|
"the package was still under the device root at \(onDevice.path)"
|
|
)
|
|
}
|
|
|
|
/// The softened wall (2026-08-08): no iCloud is a notice above a working list, not a screen
|
|
/// instead of one.
|
|
@MainActor
|
|
func testLocalBoardWorksWithoutICloud() throws {
|
|
let (app, _) = XCUIApplication.launchedWithFixtureBoardOnDevice()
|
|
|
|
XCTAssertTrue(
|
|
app.element(labelContaining: "iCloud Unavailable").waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the iCloud notice never appeared"
|
|
)
|
|
|
|
let boardRow = app.boardRow(RichBoard.title, alsoContaining: "Local")
|
|
XCTAssertTrue(
|
|
boardRow.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the device-home board never listed — the old wall would have replaced the list here"
|
|
)
|
|
|
|
// It is a whole board, not a listing: it opens and navigates like any other.
|
|
boardRow.tap()
|
|
let laneRow = app.element(labelContaining: RichBoard.firstLane)
|
|
XCTAssertTrue(
|
|
laneRow.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the local board's lanes never appeared"
|
|
)
|
|
app.navigationBars.buttons.element(boundBy: 0).tap()
|
|
|
|
// The one thing it cannot do: go somewhere that does not exist.
|
|
app.openBoardSettings(for: RichBoard.title)
|
|
XCTAssertTrue(
|
|
app.staticTexts["Stored In, Local"].waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the sheet did not report the board as stored locally"
|
|
)
|
|
let moveToCloud = app.buttons["Move to iCloud"]
|
|
XCTAssertTrue(
|
|
moveToCloud.waitForExistence(timeout: XCUIApplication.uiTimeout),
|
|
"the sheet never offered a move to iCloud"
|
|
)
|
|
XCTAssertFalse(
|
|
moveToCloud.isEnabled,
|
|
"moving to iCloud was offered as available with no iCloud to move to"
|
|
)
|
|
}
|
|
}
|