#if os(macOS) import AppKit import SwiftUI import XCTest import RastrilloApple @testable import SheetsKit final class SheetNativeInteractionTests: XCTestCase { @MainActor func testTypingEntersCellAndFailedCommitKeepsDraft() async throws { let model = SheetModel(client: NativeHTTP(origin: URL(string: "http://127.0.0.1:1")!)) 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) let host = NSHostingView(rootView: SheetGrid(model: model).background(Color.white).preferredColorScheme(.light)) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false) window.contentView = host NSApp.setActivationPolicy(.regular) window.makeKeyAndOrderFront(nil) host.layoutSubtreeIfNeeded() window.displayIfNeeded() NSApp.activate() defer { window.orderOut(nil) } try await Task.sleep(for: .milliseconds(300)) for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { 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)! window.sendEvent(event) } try await Task.sleep(for: .milliseconds(100)) key("h", code: 4, window: window) try await Task.sleep(for: .milliseconds(150)) XCTAssertEqual(model.draft, "h") XCTAssertTrue(window.firstResponder is NSTextView, "Typing must transfer focus into a native cell text field") key("e", code: 14, window: window) try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(model.draft, "he", "The initial character must not be selected and overwritten") if ProcessInfo.processInfo.environment["OFICINA_CAPTURE_FIXTURES"] == "1", let bitmap = host.bitmapImageRepForCachingDisplay(in: host.bounds) { host.cacheDisplay(in: host.bounds, to: bitmap) try bitmap.representation(using: .png, properties: [:])?.write(to: URL(fileURLWithPath: "/tmp/oficina-sheets-grid-fixture.png")) } key("\t", code: 48, window: window) try await Task.sleep(for: .milliseconds(500)) XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 0)) XCTAssertEqual(model.draft, "he") XCTAssertTrue(model.hasUnconfirmedChanges) XCTAssertFalse(model.failedWrites.isEmpty) XCTAssertTrue(window.firstResponder is NSTextView) key("\u{1b}", code: 53, window: window) try await Task.sleep(for: .milliseconds(100)) XCTAssertFalse(model.hasUnconfirmedChanges) XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 0)) // Column B header then row 2 header select complete ranges. click(NSPoint(x: 200, y: 375), window: window) try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(model.selection.start, Coordinate(r: 0, c: 1)) XCTAssertEqual(model.selection.end, Coordinate(r: 3, c: 1)) click(NSPoint(x: 24, y: 320), window: window) try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(model.selection.start, Coordinate(r: 1, c: 0)) XCTAssertEqual(model.selection.end, Coordinate(r: 1, c: 2)) } @MainActor func testRapidTypingDuringFocusTransfer() async throws { for delay in [0, 1, 5, 10, 25, 50] { let model = SheetModel(client: NativeHTTP(origin: URL(string: "http://127.0.0.1:1")!)) 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) let host = NSHostingView(rootView: SheetGrid(model: model)) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false) window.contentView = host; NSApp.setActivationPolicy(.regular); window.makeKeyAndOrderFront(nil); NSApp.activate() defer { window.orderOut(nil) } try await Task.sleep(for: .milliseconds(200)) click(NSPoint(x: 100, y: 350), window: window) try await Task.sleep(for: .milliseconds(100)) key("h", code: 4, window: window) if delay > 0 { try await Task.sleep(for: .milliseconds(delay)) } key("e", code: 14, window: window) try await Task.sleep(for: .milliseconds(200)) XCTAssertEqual(model.draft, "he", "Typing during focus handoff (\(delay)ms)") } } func testResizePreviewUsesOriginalSizeAndServerBounds() { var preview = SheetResizePreview(axis: "col", index: 0, original: 110) preview.update(translation: 30) XCTAssertEqual(preview.size, 140) preview.update(translation: 40) XCTAssertEqual(preview.size, 150) preview.update(translation: -1000) XCTAssertEqual(preview.size, 16) preview.update(translation: 5000) XCTAssertEqual(preview.size, 2000) } @MainActor func testDragResizeWaitsForReleaseAndRollsBackOnFailure() async throws { for axis in ["col", "row"] { for succeeds in [false, true] { let server = try ResizeFixtureServer() let origin = try await server.start() defer { server.stop() } let model = SheetModel(client: NativeHTTP(origin: succeeds ? origin : URL(string: "http://127.0.0.1:1")!)) 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) let host = NSHostingView(rootView: SheetGrid(model: model)) let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 640, height: 400), styleMask: [.titled], backing: .buffered, defer: false) window.contentView = host; NSApp.setActivationPolicy(.regular); window.makeKeyAndOrderFront(nil); NSApp.activate() defer { window.orderOut(nil) } try await Task.sleep(for: .milliseconds(200)) func handles(_ view: NSView) -> [SheetResizeHandle.Handle] { (view as? SheetResizeHandle.Handle).map { [$0] } ?? view.subviews.flatMap(handles) } let handle = try XCTUnwrap(handles(host).first { $0.actions?.vertical == (axis == "row") }) let start = handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil) let end = NSPoint(x: start.x + (axis == "col" ? 40 : 0), y: start.y - (axis == "row" ? 40 : 0)) func send(_ type: NSEvent.EventType, _ point: NSPoint) { window.sendEvent(NSEvent.mouseEvent(with: type, location: point, modifierFlags: [], timestamp: ProcessInfo.processInfo.systemUptime, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)!) } // Cancel one drag first: no save, and geometry must return to its original position. send(.leftMouseDown, start) send(.leftMouseDragged, end) try await Task.sleep(for: .milliseconds(100)) let preview = handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil) XCTAssertEqual(axis == "col" ? preview.x - start.x : start.y - preview.y, 40, accuracy: 1) 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)!) send(.leftMouseUp, end) try await Task.sleep(for: .milliseconds(100)) XCTAssertEqual(server.requests.count, 0) XCTAssertEqual(handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil), start) send(.leftMouseDown, start) try await Task.sleep(for: .milliseconds(50)) for step in 1...5 { send(.leftMouseDragged, NSPoint(x: start.x + (end.x - start.x) * Double(step) / 5, y: start.y + (end.y - start.y) * Double(step) / 5)) try await Task.sleep(for: .milliseconds(30)) } try await Task.sleep(for: .milliseconds(150)) XCTAssertFalse(model.busy) XCTAssertEqual(server.requests.count, 0) XCTAssertEqual(model.error, "", "Dragging must not send intermediate requests") send(.leftMouseUp, end) try await Task.sleep(for: .milliseconds(300)) if succeeds { XCTAssertEqual(model.error, "") XCTAssertEqual(server.requests, [[axis: 0, "size": axis == "col" ? 150 : 72]]) XCTAssertEqual(axis == "col" ? model.sizes.cols?["0"] : model.sizes.rows?["0"], axis == "col" ? 150 : 72) } else { XCTAssertFalse(model.error.isEmpty, "Releasing the \(axis) handle must attempt persistence") XCTAssertNil(model.sizes.cols?["0"]) XCTAssertNil(model.sizes.rows?["0"]) XCTAssertEqual(handle.convert(NSPoint(x: handle.bounds.midX, y: handle.bounds.midY), to: nil), start) } XCTAssertFalse(model.busy) } } } @MainActor private func click(_ point: NSPoint, window: NSWindow) { for type in [NSEvent.EventType.leftMouseDown, .leftMouseUp] { let event = NSEvent.mouseEvent(with: type, location: point, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 1, clickCount: 1, pressure: 1)! window.sendEvent(event) } } @MainActor private func key(_ text: String, code: UInt16, window: NSWindow) { let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: text, charactersIgnoringModifiers: text, isARepeat: false, keyCode: code)! window.sendEvent(event) } } #endif