oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1#if os(macOS)
2import AppKit
3import SwiftUI
4import XCTest
5import RastrilloApple
6@testable import SheetsKit
7
8final class SheetNativeInteractionTests: XCTestCase {
9 @MainActor func testTypingEntersCellAndFailedCommitKeepsDraft() async throws {
10 let model = SheetModel(client: NativeHTTP(origin: URL(string: "http://127.0.0.1:1")!))
11 model.page = SheetPage(Sheet: SheetSummary(ID: 1, Title: "Fixture", TabID: 1, TabName: "Sheet 1"), Rows: 4, Cols: 3, Truncated: false, MayWrite: true, MayComment: true, MayShare: false, FillSwatches: [], InkSwatches: [], NumberFormats: [], Warned: nil)
12 let host = NSHostingView(rootView: SheetGrid(model: model).background(Color.white).preferredColorScheme(.light))
13 let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false)
14 window.contentView = host
15 NSApp.setActivationPolicy(.regular)
16 window.makeKeyAndOrderFront(nil)
17 host.layoutSubtreeIfNeeded()
18 window.displayIfNeeded()
19 NSApp.activate()
20 defer { window.orderOut(nil) }
21 try await Task.sleep(for: .milliseconds(300))
22 for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] {
23 let event = NSEvent.mouseEvent(with: type, location: NSPoint(x: 100, y: 350), modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)!
24 window.sendEvent(event)
25 }
26 try await Task.sleep(for: .milliseconds(100))
27 key("h", code: 4, window: window)
28 try await Task.sleep(for: .milliseconds(150))
29 XCTAssertEqual(model.draft, "h")
30 XCTAssertTrue(window.firstResponder is NSTextView, "Typing must transfer focus into a native cell text field")
31 key("e", code: 14, window: window)
32 try await Task.sleep(for: .milliseconds(100))
33 XCTAssertEqual(model.draft, "he", "The initial character must not be selected and overwritten")
34 if ProcessInfo.processInfo.environment["OFICINA_CAPTURE_FIXTURES"] == "1",
35 let bitmap = host.bitmapImageRepForCachingDisplay(in: host.bounds) {
36 host.cacheDisplay(in: host.bounds, to: bitmap)
37 try bitmap.representation(using: .png, properties: [:])?.write(to: URL(fileURLWithPath: "/tmp/oficina-sheets-grid-fixture.png"))
38 }
39 key("\t", code: 48, window: window)
40 try await Task.sleep(for: .milliseconds(500))
41 XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 0))
42 XCTAssertEqual(model.draft, "he")
43 XCTAssertTrue(model.hasUnconfirmedChanges)
44 XCTAssertFalse(model.failedWrites.isEmpty)
45 XCTAssertTrue(window.firstResponder is NSTextView)
46 key("\u{1b}", code: 53, window: window)
47 try await Task.sleep(for: .milliseconds(100))
48 XCTAssertFalse(model.hasUnconfirmedChanges)
49 XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 0))
50 // Column B header then row 2 header select complete ranges.
51 click(NSPoint(x: 200, y: 375), window: window)
52 try await Task.sleep(for: .milliseconds(100))
53 XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 1))
54 XCTAssertEqual(model.selection.end, Coordinate(r: 3, c: 1))
55 click(NSPoint(x: 24, y: 320), window: window)
56 try await Task.sleep(for: .milliseconds(100))
57 XCTAssertEqual(model.selection.start, Coordinate(r: 1, c: 0))
58 XCTAssertEqual(model.selection.end, Coordinate(r: 1, c: 2))
59 }
60 @MainActor func testRapidTypingDuringFocusTransfer() async throws {
61 for delay in [0, 1, 5, 10, 25, 50] {
62 let model = SheetModel(client: NativeHTTP(origin: URL(string: "http://127.0.0.1:1")!))
63 model.page = SheetPage(Sheet: SheetSummary(ID: 1, Title: "Fixture", TabID: 1, TabName: "Sheet 1"), Rows: 4, Cols: 3, Truncated: false, MayWrite: true, MayComment: true, MayShare: false, FillSwatches: [], InkSwatches: [], NumberFormats: [], Warned: nil)
64 let host = NSHostingView(rootView: SheetGrid(model: model))
65 let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false)
66 window.contentView = host; NSApp.setActivationPolicy(.regular); window.makeKeyAndOrderFront(nil); NSApp.activate()
67 defer { window.orderOut(nil) }
68 try await Task.sleep(for: .milliseconds(200))
69 click(NSPoint(x: 100, y: 350), window: window)
70 try await Task.sleep(for: .milliseconds(100))
71 key("h", code: 4, window: window)
72 if delay > 0 { try await Task.sleep(for: .milliseconds(delay)) }
73 key("e", code: 14, window: window)
74 try await Task.sleep(for: .milliseconds(200))
75 XCTAssertEqual(model.draft, "he", "Typing during focus handoff (\(delay)ms)")
76 }
77 }
78 func testResizePreviewUsesOriginalSizeAndServerBounds() {
79 var preview = SheetResizePreview(axis: "col", index: 0, original: 110)
80 preview.update(translation: 30)
81 XCTAssertEqual(preview.size, 140)
82 preview.update(translation: 40)
83 XCTAssertEqual(preview.size, 150)
84 preview.update(translation: -1000)
85 XCTAssertEqual(preview.size, 16)
86 preview.update(translation: 5000)
87 XCTAssertEqual(preview.size, 2000)
88 }
89 @MainActor func testDragResizeWaitsForReleaseAndRollsBackOnFailure() async throws {
90 for axis in ["col", "row"] {
91 for succeeds in [false, true] {
92 let server = try ResizeFixtureServer()
93 let origin = try await server.start()
94 defer { server.stop() }
95 let model = SheetModel(client: NativeHTTP(origin: succeeds ? origin : URL(string: "http://127.0.0.1:1")!))
96 model.page = SheetPage(Sheet: SheetSummary(ID: 1, Title: "Fixture", TabID: 1, TabName: "Sheet 1"), Rows: 4, Cols: 3, Truncated: false, MayWrite: true, MayComment: true, MayShare: false, FillSwatches: [], InkSwatches: [], NumberFormats: [], Warned: nil)
97 let host = NSHostingView(rootView: SheetGrid(model: model))
98 let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false)
99 window.contentView = host; NSApp.setActivationPolicy(.regular); window.makeKeyAndOrderFront(nil); NSApp.activate()
100 defer { window.orderOut(nil) }
101 try await Task.sleep(for: .milliseconds(200))
102 func handles(_ view: NSView) -> [SheetResizeHandle.Handle] {
103 (view as? SheetResizeHandle.Handle).map { [$0] } ?? view.subviews.flatMap(handles)
104 }
105 let handle = try XCTUnwrap(handles(host).first { $0.actions?.vertical == (axis == "row") })
106 let start = handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil)
107 let end = NSPoint(x: start.x + (axis == "col" ? 40 : 0), y: start.y - (axis == "row" ? 40 : 0))
108 func send(_ type: NSEvent.EventType, _ point: NSPoint) {
109 window.sendEvent(NSEvent.mouseEvent(with: type, location: point, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)!)
110 }
111 // Cancel one drag first: no save, and geometry must return to its original position.
112 send(.leftMouseDown, start)
113 send(.leftMouseDragged, end)
114 try await Task.sleep(for: .milliseconds(100))
115 let preview = handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil)
116 XCTAssertEqual(axis == "col" ? preview.x - start.x : start.y - preview.y, 40, accuracy: 1)
117 handle.keyDown(with: NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "\u{1b}", charactersIgnoringModifiers: "\u{1b}", isARepeat: false, keyCode: 53)!)
118 send(.leftMouseUp, end)
119 try await Task.sleep(for: .milliseconds(100))
120 XCTAssertEqual(server.requests.count, 0)
121 XCTAssertEqual(handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil), start)
122 send(.leftMouseDown, start)
123 try await Task.sleep(for: .milliseconds(50))
124 for step in 1...5 {
125 send(.leftMouseDragged, NSPoint(x: start.x + (end.x - start.x) * Double(step) / 5, y: start.y + (end.y - start.y) * Double(step) / 5))
126 try await Task.sleep(for: .milliseconds(30))
127 }
128 try await Task.sleep(for: .milliseconds(150))
129 XCTAssertFalse(model.busy)
130 XCTAssertEqual(server.requests.count, 0)
131 XCTAssertEqual(model.error, "", "Dragging must not send intermediate requests")
132 send(.leftMouseUp, end)
133 try await Task.sleep(for: .milliseconds(300))
134 if succeeds {
135 XCTAssertEqual(model.error, "")
136 XCTAssertEqual(server.requests, [[axis: 0, "size": axis == "col" ? 150 : 72]])
137 XCTAssertEqual(axis == "col" ? model.sizes.cols?["0"] : model.sizes.rows?["0"], axis == "col" ? 150 : 72)
138 } else {
139 XCTAssertFalse(model.error.isEmpty, "Releasing the \(axis) handle must attempt persistence")
140 XCTAssertNil(model.sizes.cols?["0"])
141 XCTAssertNil(model.sizes.rows?["0"])
142 XCTAssertEqual(handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil), start)
143 }
144 XCTAssertFalse(model.busy)
145 }
146 }
147 }
148 @MainActor private func click(_ point: NSPoint, window: NSWindow) {
149 for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] {
150 let event = NSEvent.mouseEvent(with: type, location: point, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)!
151 window.sendEvent(event)
152 }
153 }
154 @MainActor private func key(_ text: String, code: UInt16, window: NSWindow) {
155 let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: text, charactersIgnoringModifiers: text, isARepeat: false, keyCode: code)!
156 window.sendEvent(event)
157 }
158}
159#endif
160