| 1 | import Foundation |
| 2 | import RastrilloApple |
| 3 | import Testing |
| 4 | |
| 5 | @testable import SheetsKit |
| 6 | |
| 7 | // B12 and its review (#35). The server says how many versions a write |
| 8 | // consumed (`stamps`); accept() refetches only when the version moved |
| 9 | // further than that, so a multi-cell batch applies in place. Stampless |
| 10 | // responses (format, size, snapshot) allow one. The server-side proof, |
| 11 | // against real responses, is TestNativeVersionContractAgainstRealResponses. |
| 12 | // |
| 13 | // The client points at a closed port, so an attempted refetch shows as a |
| 14 | // connection error and leaves the incremental cells unapplied. |
| 15 | @MainActor private func gapModel(version: UInt64) -> SheetModel { |
| 16 | let model = SheetModel(client: NativeHTTP(origin: URL(string: "http://127.0.0.1:1")!)) |
| 17 | model.page = SheetPage( |
| 18 | Sheet: SheetSummary(ID: 1, Title: "Fixture", TabID: 1, TabName: "Sheet 1"), Rows: 10, Cols: 5, |
| 19 | Truncated: false, MayWrite: true, MayComment: true, MayShare: false, FillSwatches: [], |
| 20 | InkSwatches: [], NumberFormats: [], Warned: nil) |
| 21 | model.version = version |
| 22 | model.error = "" |
| 23 | return model |
| 24 | } |
| 25 | |
| 26 | @MainActor @Test func stampedBatchAppliesWithoutRefetch() async { |
| 27 | let model = gapModel(version: 5) |
| 28 | // Three typed cells (versions 6, 7, 8) plus one formula they changed. |
| 29 | await model.received( |
| 30 | SheetEvent( |
| 31 | name: "cells", |
| 32 | data: #"{"version":8,"stamps":3,"cells":[{"r":0,"c":0,"raw":"1","display":"1"},{"r":1,"c":0,"raw":"2","display":"2"},{"r":2,"c":0,"raw":"3","display":"3"},{"r":3,"c":0,"raw":"=SUM(A1:A3)","display":"6","derived":true}]}"# |
| 33 | ), source: model.grid) |
| 34 | #expect(model.error == "", "a stamped batch attempted a refetch: \(model.error)") |
| 35 | #expect(model.version == 8) |
| 36 | #expect(model.cells[Coordinate(r: 2, c: 0)]?.raw == "3") |
| 37 | #expect(model.cells[Coordinate(r: 3, c: 0)]?.display == "6") |
| 38 | } |
| 39 | |
| 40 | @MainActor @Test func versionGapBeyondTheStampsStillRefetches() async { |
| 41 | let model = gapModel(version: 5) |
| 42 | // Two stamps ending at 9, so versions 6 and 7 were missed. |
| 43 | await model.received( |
| 44 | SheetEvent( |
| 45 | name: "cells", |
| 46 | data: #"{"version":9,"stamps":2,"cells":[{"r":0,"c":0,"raw":"1","display":"1"},{"r":1,"c":0,"raw":"2","display":"2"}]}"# |
| 47 | ), source: model.grid) |
| 48 | #expect(model.error != "", "a real gap was applied incrementally instead of refetched") |
| 49 | #expect(model.version == 5) |
| 50 | #expect(model.cells[Coordinate(r: 0, c: 0)] == nil) |
| 51 | } |
| 52 | |
| 53 | @MainActor @Test func formatCellsDoNotWidenTheAcceptedJump() async { |
| 54 | let model = gapModel(version: 5) |
| 55 | // A format over four cells consumes no stamp; version 7 means a missed |
| 56 | // edit, however many cells the format response lists. |
| 57 | let cells = (0..<4).map { Cell(r: $0, c: 0, raw: "", display: "") } |
| 58 | await model.accept(GridResponse(version: 7, cells: cells)) |
| 59 | #expect(model.error != "") |
| 60 | #expect(model.version == 5) |
| 61 | } |
| 62 | |
| 63 | @MainActor @Test func singleStepsAndStamplessResponsesStillApply() async { |
| 64 | let model = gapModel(version: 5) |
| 65 | await model.accept(GridResponse(version: 6, cells: [Cell(r: 0, c: 0, raw: "x", display: "x")], stamps: 1)) |
| 66 | // A format right after reports the next stamp: one ahead, no stamps. |
| 67 | await model.accept(GridResponse(version: 7, cells: [Cell(r: 0, c: 0, raw: "x", display: "x")])) |
| 68 | #expect(model.error == "") |
| 69 | #expect(model.version == 7) |
| 70 | #expect(model.cells[Coordinate(r: 0, c: 0)]?.raw == "x") |
| 71 | } |
| 72 | |