Files
ichmagmaus111andClaude Opus 5.5 0622057a4f Ghostgram: open-source release
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:57:09 +01:00

150 lines
6.8 KiB
Swift

#if canImport(UIKit)
import XCTest
import UIKit
@testable import RichTextEditorUIKit
import RichTextEditorCore
/// Phase 2c Task 1: `.cells(TableRect)` table-selection kind + its model plumbing. Model/plumbing only —
/// no drawing/gesture coverage here (that's T3/T4).
@available(iOS 13.0, *)
final class TableCellSelectionTests: XCTestCase {
private func cell(_ id: String, _ text: String) -> Cell {
Cell(id: BlockID(id), blocks: [.paragraph(ParagraphBlock(id: BlockID(id + "p"), runs: [TextRun(text: text)]))])
}
/// A dense 3x3 table, ids "a".."i" row-major (row0: a,b,c; row1: d,e,f; row2: g,h,i).
private func dense3x3() -> TableBlock {
TableBlock(id: BlockID("t"),
columns: [ColumnSpec(width: 100), ColumnSpec(width: 100), ColumnSpec(width: 100)],
rows: [
Row(id: BlockID("r0"), cells: [cell("a", "A"), cell("b", "B"), cell("c", "C")]),
Row(id: BlockID("r1"), cells: [cell("d", "D"), cell("e", "E"), cell("f", "F")]),
Row(id: BlockID("r2"), cells: [cell("g", "G"), cell("h", "H"), cell("i", "I")]),
])
}
/// A 3x3 table with a colspan-2 cell merged at (0,0)-(0,1) (absorbing "b"'s content into "a"'s stack).
private func mergedTopLeftColspan2() -> TableBlock {
dense3x3().mergingCells(in: TableRect(top: 0, left: 0, bottom: 0, right: 1))
}
private func canvas(_ table: TableBlock, width: CGFloat = 390) -> DocumentCanvasView {
let v = DocumentCanvasView()
v.setBlocks([.table(table)], width: width)
v.frame = CGRect(x: 0, y: 0, width: width, height: 600); v.layoutIfNeeded()
return v
}
private func table(_ v: DocumentCanvasView) -> TableBlockBox { v.boxes[0] as! TableBlockBox }
// MARK: - 1. selectedCellCoords dedupes and covers the rect
func test_selectTableCells_selectedCellCoords_dedupesAndCoversRect() {
let v = canvas(dense3x3())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
v.selectTableCells(TableRect(top: 0, left: 0, bottom: 1, right: 1))
let coords = v.selectedCellCoords(in: t)
XCTAssertEqual(coords.count, 4, "no dupes — exactly the 4 cells in the 2x2 rect")
let pairs = coords.map { [$0.row, $0.column] }
XCTAssertEqual(pairs, [[0, 0], [0, 1], [1, 0], [1, 1]], "row-major order")
}
// MARK: - 2. Expands to whole merged cell
func test_selectTableCells_expandsToWholeMergedCell() {
let v = canvas(mergedTopLeftColspan2())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
// Bisects the colspan-2 merged cell at (0,0)-(0,1): requesting only (0,0) must expand to include (0,1).
v.selectTableCells(TableRect(top: 0, left: 0, bottom: 0, right: 0))
guard case .cells(let committed) = v.tableSelection?.kind else { return XCTFail("expected .cells") }
XCTAssertEqual(committed, TableRect(top: 0, left: 0, bottom: 0, right: 1),
"committed rect is expanded to cover the whole merged cell")
let coords = v.selectedCellCoords(in: t)
XCTAssertEqual(coords.count, 1, "the merged cell is reported exactly ONCE, not once per covered slot")
XCTAssertEqual(coords.first?.row, 0)
XCTAssertEqual(coords.first?.column, 0)
}
// MARK: - 3. structuralCellRect + rowRange/columnRange nil for .cells
func test_structuralCellRect_and_rangesNilForCells() {
let v = canvas(dense3x3())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
v.selectTableCells(TableRect(top: 0, left: 0, bottom: 1, right: 1))
XCTAssertEqual(v.structuralCellRect(), TableRect(top: 0, left: 0, bottom: 1, right: 1))
XCTAssertNil(v.structuralRowRange())
XCTAssertNil(v.structuralColumnRange())
}
// MARK: - 4. tableStructuralSelectionRegions returns leaf regions of every covered cell
func test_tableStructuralSelectionRegions_cells() {
let v = canvas(dense3x3())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
v.selectTableCells(TableRect(top: 0, left: 0, bottom: 1, right: 1))
let regions = v.tableStructuralSelectionRegions()
XCTAssertNotNil(regions)
XCTAssertFalse(regions!.isEmpty)
// Exactly the 4 covered cells' own leaf regions (one paragraph each, dense table).
XCTAssertEqual(regions?.count, 4)
let refs = regions!.map { $0.ref }
for expected in [TextNodeRef.paragraph(BlockID("ap")), .paragraph(BlockID("bp")),
.paragraph(BlockID("dp")), .paragraph(BlockID("ep"))] {
XCTAssertTrue(refs.contains(expected), "missing leaf region for \(expected)")
}
}
// MARK: - 5. clearTableSelection clears .cells
func test_clearTableSelection_clearsCells() {
let v = canvas(dense3x3())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
v.selectTableCells(TableRect(top: 0, left: 0, bottom: 1, right: 1))
XCTAssertNotNil(v.tableSelection)
v.clearTableSelection()
XCTAssertNil(v.tableSelection)
}
// MARK: - 6. Dense parity: .rows/.columns behavior unchanged
func test_denseParity_rowsAndColumnsSelectionUnchanged() {
let v = canvas(dense3x3())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 1, column: 0)!, head: t.cellTextStart(row: 1, column: 0)!)
v.selectTableRow(1)
XCTAssertEqual(v.tableSelection?.kind, .rows(1...1))
XCTAssertEqual(v.head, t.cellTextStart(row: 1, column: 0))
let rowCoords = v.selectedCellCoords(in: t)
XCTAssertEqual(rowCoords.count, 3)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 2)!, head: t.cellTextStart(row: 0, column: 2)!)
v.selectTableColumn(2)
XCTAssertEqual(v.tableSelection?.kind, .columns(2...2))
XCTAssertEqual(v.head, t.cellTextStart(row: 0, column: 2))
let colCoords = v.selectedCellCoords(in: t)
XCTAssertEqual(colCoords.count, 3)
}
// MARK: - Single-cell convenience
func test_selectTableCell_singleCellConvenience() {
let v = canvas(dense3x3())
let t = table(v)
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
v.selectTableCell(row: 1, column: 1)
XCTAssertEqual(v.tableSelection?.kind, .cells(TableRect(top: 1, left: 1, bottom: 1, right: 1)))
XCTAssertEqual(v.head, t.cellTextStart(row: 1, column: 1))
}
}
#endif