mirror of
https://github.com/ichmagmaus111/ghostgram.git
synced 2026-11-03 22:27:13 +01:00
Ghostgram for iOS, rebuilt from scratch on Telegram iOS 13.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
10 KiB
Swift
208 lines
10 KiB
Swift
#if canImport(UIKit)
|
||
import XCTest
|
||
import UIKit
|
||
@testable import RichTextEditorUIKit
|
||
import RichTextEditorCore
|
||
|
||
/// Phase 2c-T2: `mergeSelectedCells()` / `splitSelectedCell()` commands and the Merge/Split entries in
|
||
/// the `.cells` structural menu.
|
||
final class TableMergeSplitCommandTests: XCTestCase {
|
||
func cell(_ id: String, _ t: String) -> Cell {
|
||
Cell(id: BlockID(id), blocks: [.paragraph(ParagraphBlock(id: BlockID(id + "p"), runs: [TextRun(text: t)]))])
|
||
}
|
||
/// A dense 3×3 table (no header) so a 2×2 merge in the top-left corner leaves the last row/column
|
||
/// (P, Q, R, X, Y) intact and observable.
|
||
/// A B X
|
||
/// C D Y
|
||
/// P Q R
|
||
func canvas() -> DocumentCanvasView {
|
||
let v = DocumentCanvasView()
|
||
v.setBlocks([
|
||
.table(TableBlock(id: BlockID("t"),
|
||
columns: [ColumnSpec(width: 120), ColumnSpec(width: 120), ColumnSpec(width: 120)],
|
||
rows: [Row(id: BlockID("r0"), cells: [cell("a", "A"), cell("b", "B"), cell("x", "X")]),
|
||
Row(id: BlockID("r1"), cells: [cell("c", "C"), cell("d", "D"), cell("y", "Y")]),
|
||
Row(id: BlockID("r2"), cells: [cell("p", "P"), cell("q", "Q"), cell("r", "R")])])),
|
||
], width: 390)
|
||
v.frame = CGRect(x: 0, y: 0, width: 390, height: 600); v.layoutIfNeeded()
|
||
return v
|
||
}
|
||
func table(_ v: DocumentCanvasView) -> TableBlockBox { v.boxes.first { $0 is TableBlockBox } as! TableBlockBox }
|
||
func tableBlock(_ v: DocumentCanvasView) -> TableBlock {
|
||
guard case .table(let t) = v.boxes.first { $0 is TableBlockBox }!.currentBlock() else { fatalError() }
|
||
return t
|
||
}
|
||
func cellTexts(_ cell: Cell) -> [String] {
|
||
cell.blocks.compactMap { if case .paragraph(let p) = $0 { return p.text } else { return nil } }
|
||
}
|
||
func actionKinds(_ req: TableStructuralMenuRequest?) -> [TableStructuralMenuRequest.Kind] {
|
||
req?.actions.map { $0.kind } ?? []
|
||
}
|
||
|
||
// MARK: - mergeSelectedCells
|
||
|
||
func test_mergeSelectedCells_mergesRectAndConcatenatesContent() {
|
||
let v = canvas(); 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)) // A, B, C, D
|
||
v.mergeSelectedCells()
|
||
v.layoutIfNeeded()
|
||
|
||
let tb = tableBlock(v)
|
||
let map = TableMap(tb)
|
||
XCTAssertTrue(map.isWellFormed)
|
||
|
||
guard let anchor = map.anchor(atRow: 0, column: 0) else { return XCTFail("no anchor at (0,0)") }
|
||
XCTAssertEqual(anchor.colspan, 2)
|
||
XCTAssertEqual(anchor.rowspan, 2)
|
||
let anchorCell = tb.rows.flatMap { $0.cells }.first { $0.id == anchor.cellID }!
|
||
XCTAssertEqual(cellTexts(anchorCell), ["A", "B", "C", "D"], "merged blocks concatenate row-major, anchor first")
|
||
|
||
// Row 2 and the last column are untouched.
|
||
XCTAssertEqual(cellTexts(tb.rows[0].cells.first { $0.id == BlockID("x") }!), ["X"])
|
||
XCTAssertEqual(cellTexts(tb.rows[1].cells.first { $0.id == BlockID("y") }!), ["Y"])
|
||
XCTAssertEqual(cellTexts(tb.rows[2].cells.first { $0.id == BlockID("p") }!), ["P"])
|
||
XCTAssertEqual(cellTexts(tb.rows[2].cells.first { $0.id == BlockID("q") }!), ["Q"])
|
||
XCTAssertEqual(cellTexts(tb.rows[2].cells.first { $0.id == BlockID("r") }!), ["R"])
|
||
}
|
||
|
||
func test_mergeSelectedCells_isUndoable_oneStep() {
|
||
let v = canvas(); let t = table(v)
|
||
let um = UndoManager(); um.groupsByEvent = false; v.undoManagerOverride = um
|
||
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))
|
||
|
||
um.beginUndoGrouping(); v.mergeSelectedCells(); um.endUndoGrouping()
|
||
v.layoutIfNeeded()
|
||
XCTAssertEqual(TableMap(tableBlock(v)).anchor(atRow: 0, column: 0)?.colspan, 2, "merged")
|
||
|
||
um.undo(); v.layoutIfNeeded()
|
||
let restored = tableBlock(v)
|
||
let restoredMap = TableMap(restored)
|
||
XCTAssertEqual(restoredMap.anchor(atRow: 0, column: 0)?.colspan, 1, "undo restores the 4 separate cells")
|
||
XCTAssertEqual(restoredMap.anchor(atRow: 0, column: 0)?.rowspan, 1)
|
||
XCTAssertEqual(cellTexts(restored.rows[0].cells.first { $0.id == BlockID("a") }!), ["A"])
|
||
XCTAssertEqual(cellTexts(restored.rows[0].cells.first { $0.id == BlockID("b") }!), ["B"])
|
||
XCTAssertEqual(cellTexts(restored.rows[1].cells.first { $0.id == BlockID("c") }!), ["C"])
|
||
XCTAssertEqual(cellTexts(restored.rows[1].cells.first { $0.id == BlockID("d") }!), ["D"])
|
||
}
|
||
|
||
func test_mergeSelectedCells_noopOnSingleCell() {
|
||
let v = canvas(); let t = table(v)
|
||
let um = UndoManager(); um.groupsByEvent = false; v.undoManagerOverride = um
|
||
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
|
||
v.selectTableCell(row: 0, column: 0) // a 1x1 rect
|
||
|
||
v.mergeSelectedCells()
|
||
v.layoutIfNeeded()
|
||
|
||
XCTAssertFalse(um.canUndo, "a no-op merge must not register an undo entry")
|
||
let tb = tableBlock(v)
|
||
XCTAssertEqual(TableMap(tb).anchors.count, 9, "still 9 distinct cells, nothing merged")
|
||
}
|
||
|
||
// MARK: - splitSelectedCell
|
||
|
||
func test_splitSelectedCell_restoresGrid() {
|
||
let v = canvas(); 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))
|
||
v.mergeSelectedCells()
|
||
v.layoutIfNeeded()
|
||
|
||
// Focus the merged cell: caret in it, no `.cells` selection.
|
||
v.clearTableSelection()
|
||
let t2 = table(v)
|
||
v.setSelectionForTesting(anchor: t2.cellTextStart(row: 0, column: 0)!, head: t2.cellTextStart(row: 0, column: 0)!)
|
||
XCTAssertNil(v.tableSelection)
|
||
|
||
v.splitSelectedCell()
|
||
v.layoutIfNeeded()
|
||
|
||
let tb = tableBlock(v)
|
||
let map = TableMap(tb)
|
||
XCTAssertTrue(map.isWellFormed)
|
||
XCTAssertEqual(map.anchors.count, 9, "back to 9 distinct single cells")
|
||
let anchorAt00 = map.anchor(atRow: 0, column: 0)!
|
||
XCTAssertEqual(anchorAt00.colspan, 1)
|
||
XCTAssertEqual(anchorAt00.rowspan, 1)
|
||
let anchorCell = tb.rows.flatMap { $0.cells }.first { $0.id == anchorAt00.cellID }!
|
||
XCTAssertEqual(cellTexts(anchorCell), ["A", "B", "C", "D"], "the anchor keeps all the pooled content")
|
||
// The other three re-materialized slots are fresh, empty cells.
|
||
for (r, c) in [(0, 1), (1, 0), (1, 1)] {
|
||
let a = map.anchor(atRow: r, column: c)!
|
||
XCTAssertEqual(a.colspan, 1); XCTAssertEqual(a.rowspan, 1)
|
||
let cellValue = tb.rows.flatMap { $0.cells }.first { $0.id == a.cellID }!
|
||
XCTAssertEqual(cellTexts(cellValue), [""], "re-materialized cell is empty")
|
||
}
|
||
}
|
||
|
||
func test_splitSelectedCell_noopWhenNotMerged() {
|
||
let v = canvas(); let t = table(v)
|
||
let um = UndoManager(); um.groupsByEvent = false; v.undoManagerOverride = um
|
||
v.setSelectionForTesting(anchor: t.cellTextStart(row: 0, column: 0)!, head: t.cellTextStart(row: 0, column: 0)!)
|
||
v.splitSelectedCell()
|
||
XCTAssertFalse(um.canUndo, "a no-op split must not register an undo entry")
|
||
XCTAssertEqual(TableMap(tableBlock(v)).anchors.count, 9)
|
||
}
|
||
|
||
// MARK: - menu gating
|
||
|
||
func test_menu_cellsSelection_offersMergeWhenMultiCell() {
|
||
let v = canvas(); 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 req = v.tableStructuralMenuRequest()
|
||
let kinds = actionKinds(req)
|
||
XCTAssertTrue(kinds.contains(.mergeCells))
|
||
XCTAssertFalse(kinds.contains(.splitCell))
|
||
XCTAssertNotNil(req?.alignment)
|
||
XCTAssertNotNil(req?.header)
|
||
}
|
||
|
||
func test_menu_singleMergedCell_offersSplit() {
|
||
let v = canvas(); 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))
|
||
v.mergeSelectedCells()
|
||
v.layoutIfNeeded()
|
||
|
||
// Re-select the merged origin as a 1x1 `.cells` rect — `selectTableCells` expands it to the
|
||
// merged footprint, so the map resolves to exactly ONE (already-merged) anchor.
|
||
let t2 = table(v)
|
||
v.setSelectionForTesting(anchor: t2.cellTextStart(row: 0, column: 0)!, head: t2.cellTextStart(row: 0, column: 0)!)
|
||
v.selectTableCell(row: 0, column: 0)
|
||
|
||
let kinds = actionKinds(v.tableStructuralMenuRequest())
|
||
XCTAssertTrue(kinds.contains(.splitCell))
|
||
XCTAssertFalse(kinds.contains(.mergeCells))
|
||
}
|
||
|
||
// MARK: - dense parity (rows/columns menu unchanged)
|
||
|
||
func test_denseParity_columnMenuStillHasAddDeleteAlign_noMergeOrSplit() {
|
||
let v = canvas(); let t = table(v)
|
||
v.setSelectionForTesting(anchor: t.cellTextStart(row: 1, column: 0)!, head: t.cellTextStart(row: 1, column: 0)!)
|
||
v.selectTableColumn(1)
|
||
let kinds = actionKinds(v.tableStructuralMenuRequest())
|
||
XCTAssertTrue(kinds.contains(.addColumnLeft))
|
||
XCTAssertTrue(kinds.contains(.addColumnRight))
|
||
XCTAssertTrue(kinds.contains(.deleteColumn))
|
||
XCTAssertFalse(kinds.contains(.mergeCells))
|
||
XCTAssertFalse(kinds.contains(.splitCell))
|
||
}
|
||
|
||
func test_denseParity_rowMenuStillHasAddDelete_noMergeOrSplit() {
|
||
let v = canvas(); let t = table(v)
|
||
v.setSelectionForTesting(anchor: t.cellTextStart(row: 1, column: 0)!, head: t.cellTextStart(row: 1, column: 0)!)
|
||
v.selectTableRow(1)
|
||
let kinds = actionKinds(v.tableStructuralMenuRequest())
|
||
XCTAssertTrue(kinds.contains(.addRowAbove))
|
||
XCTAssertTrue(kinds.contains(.addRowBelow))
|
||
XCTAssertTrue(kinds.contains(.deleteRow))
|
||
XCTAssertFalse(kinds.contains(.mergeCells))
|
||
XCTAssertFalse(kinds.contains(.splitCell))
|
||
}
|
||
}
|
||
#endif
|