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

208 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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