TeraEditingProtectionTests.swift (4932B)
1 import Foundation 2 @testable import TeraApp 3 import XCTest 4 5 @MainActor 6 final class TeraEditingProtectionTests: XCTestCase { 7 func testLateDialogDismissalOrDiscardCannotActOnANewerFailedChoice() async throws { 8 let protection = TeraEditingProtection() 9 var applied = 0 10 _ = await protection.replace(kind: .editing, save: { false }, apply: { applied += 1; return true }) 11 let old = try XCTUnwrap(protection.choiceToken) 12 protection.cancel(token: old) 13 _ = await protection.replace(kind: .editing, save: { false }, apply: { applied += 1; return true }) 14 let current = try XCTUnwrap(protection.choiceToken) 15 XCTAssertNotEqual(old, current) 16 protection.cancel(token: old) 17 protection.discard(token: old) 18 XCTAssertTrue(protection.failed) 19 XCTAssertEqual(protection.choiceToken, current) 20 XCTAssertEqual(applied, 0) 21 protection.discard(token: current) 22 await TeraScopeFixtures.eventually { !protection.isWorking } 23 XCTAssertEqual(applied, 1) 24 } 25 26 func testOnePendingReplacementAwaitsSaveAndPublishesOnlyAppliedReopen() async { 27 let protection = TeraEditingProtection() 28 let pause = ResourceTestPause() 29 var saves = 0 30 var applied = 0 31 let first = Task { 32 await protection.replace(kind: .reopen, save: { 33 saves += 1 34 await pause.wait() 35 return true 36 }, apply: { applied += 1; return true }) 37 } 38 await pause.entered.wait() 39 for _ in 0 ..< 1000 { 40 XCTAssertNil(protection.schedule(kind: .editing, save: { XCTFail("Duplicate save"); return true }, apply: { false })) 41 protection.retry() 42 protection.discard() 43 } 44 XCTAssertEqual(saves, 1) 45 XCTAssertEqual(applied, 0) 46 XCTAssertNil(protection.reopened) 47 await pause.resume.open() 48 let result = await first.value 49 XCTAssertTrue(result) 50 XCTAssertEqual(applied, 1) 51 XCTAssertNotNil(protection.reopened) 52 XCTAssertFalse(protection.failed) 53 XCTAssertFalse(protection.isWorking) 54 } 55 56 func testFailedSaveRetainsChoiceAndRetryAppliesOnce() async { 57 let protection = TeraEditingProtection() 58 var saves = 0 59 var applied = 0 60 let result = await protection.replace(kind: .reopen, save: { saves += 1; return saves == 2 }, apply: { applied += 1; return true }) 61 XCTAssertFalse(result) 62 XCTAssertTrue(protection.failed) 63 XCTAssertNil(protection.reopened) 64 XCTAssertEqual(applied, 0) 65 protection.retry() 66 await TeraScopeFixtures.eventually { !protection.isWorking } 67 XCTAssertEqual(saves, 2) 68 XCTAssertEqual(applied, 1) 69 XCTAssertNotNil(protection.reopened) 70 protection.retry() 71 protection.discard() 72 XCTAssertEqual(applied, 1) 73 } 74 75 func testDiscardIsExplicitAndCancellingTheChoiceKeepsEditing() async { 76 let protection = TeraEditingProtection() 77 var applied = 0 78 var saves = 0 79 let save = { saves += 1; return false } 80 let apply = { applied += 1; return true } 81 _ = await protection.replace(kind: .editing, save: save, apply: apply) 82 protection.cancel() 83 protection.discard() 84 XCTAssertEqual(applied, 0) 85 XCTAssertFalse(protection.failed) 86 _ = await protection.replace(kind: .editing, save: save, apply: apply) 87 XCTAssertTrue(protection.failed) 88 protection.discard() 89 await TeraScopeFixtures.eventually { !protection.isWorking } 90 XCTAssertEqual(applied, 1) 91 XCTAssertEqual(saves, 2) 92 XCTAssertNil(protection.reopened) 93 } 94 95 func testEditingChangedWhileSavingRetainsWorkerUntilLateCompletionWithoutReplacing() async { 96 let protection = TeraEditingProtection() 97 let pause = ResourceTestPause() 98 var applied = false 99 let task = Task { 100 await protection.replace(kind: .reopen, save: { await pause.wait(); return true }, apply: { applied = true; return true }) 101 } 102 await pause.entered.wait() 103 protection.editingChanged() 104 XCTAssertNil(protection.schedule(kind: .editing, save: { true }, apply: { true })) 105 await pause.resume.open() 106 let result = await task.value 107 XCTAssertFalse(result) 108 XCTAssertFalse(applied) 109 XCTAssertNil(protection.reopened) 110 let next = await protection.replace(kind: .editing, save: { true }, apply: { true }) 111 XCTAssertTrue(next) 112 } 113 114 func testCancelledWaiterCannotReplaceAndFailedLoadCannotDismissSavedWork() async { 115 let protection = TeraEditingProtection() 116 let pause = ResourceTestPause() 117 let task = Task { 118 await protection.replace(kind: .reopen, save: { await pause.wait(); return true }, apply: { XCTFail("Cancelled replacement"); return true }) 119 } 120 await pause.entered.wait() 121 task.cancel() 122 await TeraScopeFixtures.eventually { !protection.isWorking } 123 await pause.resume.open() 124 let result = await task.value 125 XCTAssertFalse(result) 126 let refused = await protection.replace(kind: .reopen, save: { true }, apply: { false }) 127 XCTAssertFalse(refused) 128 XCTAssertNil(protection.reopened) 129 XCTAssertFalse(protection.failed) 130 } 131 }