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>
197 lines
10 KiB
Swift
197 lines
10 KiB
Swift
#if canImport(UIKit)
|
|
import XCTest
|
|
import UIKit
|
|
@testable import RichTextEditorUIKit
|
|
import RichTextEditorCore
|
|
|
|
final class CanvasStructuralTests: XCTestCase {
|
|
func makeCanvas(_ paragraphs: [ParagraphBlock]) -> DocumentCanvasView {
|
|
let v = DocumentCanvasView()
|
|
v.setParagraphs(paragraphs, width: 300)
|
|
v.frame = CGRect(x: 0, y: 0, width: 300, height: 300); v.layoutIfNeeded()
|
|
return v
|
|
}
|
|
func caret(_ v: DocumentCanvasView, _ pos: Int) {
|
|
v.selectedTextRange = DocumentTextRange(DocumentTextPosition(pos), DocumentTextPosition(pos))
|
|
}
|
|
|
|
func test_backspaceAtBlockStart_mergesIntoPrevious() {
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), runs: [TextRun(text: "Alpha")]),
|
|
ParagraphBlock(id: BlockID("b"), runs: [TextRun(text: "Beta")]),
|
|
])
|
|
caret(v, v.boxes[1].textStart) // start of "Beta"
|
|
v.deleteBackward()
|
|
XCTAssertEqual(v.boxes.count, 1)
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).currentParagraph().text, "AlphaBeta")
|
|
XCTAssertEqual(v.boxes[0].id, BlockID("a")) // survivor = previous block
|
|
XCTAssertEqual(v.head, v.boxes[0].textStart + 5) // caret at the join (after "Alpha")
|
|
}
|
|
|
|
func test_backspaceAtStartOfNestedListItem_outdents_doesNotMerge() {
|
|
// Backspace at the START of an INDENTED list item cancels one indent level (stays a list item);
|
|
// it does NOT merge into the previous item.
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), list: ListMembership(marker: .bullet, level: 0), runs: [TextRun(text: "A")]),
|
|
ParagraphBlock(id: BlockID("b"), list: ListMembership(marker: .bullet, level: 2), runs: [TextRun(text: "B")]),
|
|
])
|
|
caret(v, v.boxes[1].textStart)
|
|
v.deleteBackward()
|
|
XCTAssertEqual(v.boxes.count, 2, "no merge — the nested item stays put and just outdents")
|
|
let b = v.boxes[1] as! BlockBox
|
|
XCTAssertEqual(b.listMembership?.level, 1, "one indent level cancelled")
|
|
XCTAssertEqual(b.listMembership?.marker, .bullet, "still a list item")
|
|
XCTAssertEqual(b.currentParagraph().text, "B", "contents preserved")
|
|
}
|
|
|
|
func test_backspaceAtStartOfTopLevelListItem_breaksListIntoBodyParagraph() {
|
|
// Backspace at the START of a LEVEL-0 list item breaks the list here: the item becomes a body
|
|
// paragraph keeping its contents, items before stay a list, items after start a fresh list.
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), list: ListMembership(marker: .ordered), runs: [TextRun(text: "A")]),
|
|
ParagraphBlock(id: BlockID("b"), list: ListMembership(marker: .ordered), runs: [TextRun(text: "B")]),
|
|
ParagraphBlock(id: BlockID("c"), list: ListMembership(marker: .ordered), runs: [TextRun(text: "C")]),
|
|
])
|
|
caret(v, v.boxes[1].textStart) // start of "B"
|
|
v.deleteBackward()
|
|
XCTAssertEqual(v.boxes.count, 3, "no merge — B stays as its own block")
|
|
let b = v.boxes[1] as! BlockBox
|
|
XCTAssertNil(b.listMembership, "B is no longer a list item")
|
|
XCTAssertEqual(b.style, .body, "B is a body paragraph")
|
|
XCTAssertEqual(b.currentParagraph().text, "B", "contents preserved")
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).listMembership?.marker, .ordered, "A stays a list item")
|
|
XCTAssertEqual((v.boxes[2] as! BlockBox).listMembership?.marker, .ordered, "C stays a list item")
|
|
}
|
|
|
|
func test_backspaceAtStartOfListItem_undoRestoresList() {
|
|
let v = makeCanvas([ParagraphBlock(id: BlockID("a"), list: ListMembership(marker: .bullet),
|
|
runs: [TextRun(text: "A")])])
|
|
let um = UndoManager(); um.groupsByEvent = false
|
|
v.undoManagerOverride = um
|
|
caret(v, v.boxes[0].textStart)
|
|
um.beginUndoGrouping(); v.deleteBackward(); um.endUndoGrouping()
|
|
XCTAssertNil((v.boxes[0] as! BlockBox).listMembership)
|
|
um.undo()
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).listMembership?.marker, .bullet, "undo restores the list item")
|
|
}
|
|
|
|
func test_backspaceAtVeryStart_isNoOp() {
|
|
let v = makeCanvas([ParagraphBlock(id: BlockID("a"), runs: [TextRun(text: "Alpha")])])
|
|
caret(v, v.boxes[0].textStart) // start of the first block
|
|
v.deleteBackward()
|
|
XCTAssertEqual(v.boxes.count, 1)
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).currentParagraph().text, "Alpha")
|
|
}
|
|
|
|
func test_backspaceMerge_undoRestoresTwoBlocks() {
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), style: .heading1, runs: [TextRun(text: "Alpha")]),
|
|
ParagraphBlock(id: BlockID("b"), runs: [TextRun(text: "Beta")]),
|
|
])
|
|
let um = UndoManager(); um.groupsByEvent = false
|
|
v.undoManagerOverride = um
|
|
caret(v, v.boxes[1].textStart)
|
|
um.beginUndoGrouping(); v.deleteBackward(); um.endUndoGrouping()
|
|
XCTAssertEqual(v.boxes.count, 1)
|
|
um.undo()
|
|
XCTAssertEqual(v.boxes.count, 2)
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).style, .heading1) // style restored
|
|
XCTAssertEqual(v.boxes.map { $0.id }, [BlockID("a"), BlockID("b")])
|
|
}
|
|
|
|
func test_enterSplitsParagraph_intoTwoBlocks() {
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), runs: [TextRun(text: "Alpha")]),
|
|
ParagraphBlock(id: BlockID("b"), runs: [TextRun(text: "Beta")]),
|
|
])
|
|
caret(v, v.boxes[0].textStart + 2) // after "Al"
|
|
v.insertText("\n")
|
|
XCTAssertEqual(v.boxes.count, 3)
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).currentParagraph().text, "Al")
|
|
XCTAssertEqual((v.boxes[1] as! BlockBox).currentParagraph().text, "pha")
|
|
XCTAssertEqual((v.boxes[2] as! BlockBox).currentParagraph().text, "Beta")
|
|
XCTAssertEqual(v.boxes[0].id, BlockID("a")) // upper keeps the parent id
|
|
XCTAssertNotEqual(v.boxes[1].id, BlockID("a")) // lower is a fresh id
|
|
XCTAssertEqual(v.head, v.boxes[1].textStart) // caret at the start of the new block
|
|
}
|
|
|
|
func test_enterAtEnd_appendsEmptyParagraph_inheritingList() {
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), list: ListMembership(marker: .bullet),
|
|
runs: [TextRun(text: "Item")]),
|
|
])
|
|
caret(v, v.boxes[0].textStart + 4) // end of "Item"
|
|
v.insertText("\n")
|
|
XCTAssertEqual(v.boxes.count, 2)
|
|
XCTAssertEqual((v.boxes[1] as! BlockBox).currentParagraph().text, "")
|
|
XCTAssertEqual((v.boxes[1] as! BlockBox).listMembership?.marker, .bullet) // new list item
|
|
}
|
|
|
|
func test_enterSplit_undoRestoresSingleBlockWithOriginalID() {
|
|
let v = makeCanvas([ParagraphBlock(id: BlockID("a"), runs: [TextRun(text: "Alpha")])])
|
|
let um = UndoManager(); um.groupsByEvent = false
|
|
v.undoManagerOverride = um
|
|
caret(v, v.boxes[0].textStart + 2)
|
|
um.beginUndoGrouping(); v.insertText("\n"); um.endUndoGrouping()
|
|
XCTAssertEqual(v.boxes.count, 2)
|
|
um.undo()
|
|
XCTAssertEqual(v.boxes.count, 1)
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).currentParagraph().text, "Alpha")
|
|
XCTAssertEqual(v.boxes[0].id, BlockID("a")) // exact original id restored
|
|
}
|
|
|
|
func test_enterOnEmptyListItem_level0_endsList_becomesBodyParagraph() {
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("a"), list: ListMembership(marker: .bullet),
|
|
runs: [TextRun(text: "Item")]),
|
|
ParagraphBlock(id: BlockID("e"), list: ListMembership(marker: .bullet), runs: []),
|
|
])
|
|
caret(v, v.boxes[1].textStart) // caret in the empty bullet
|
|
v.insertText("\n")
|
|
XCTAssertEqual(v.boxes.count, 2) // converted in place — no new block
|
|
let empty = v.boxes[1] as! BlockBox
|
|
XCTAssertNil(empty.listMembership) // list ended
|
|
XCTAssertEqual(empty.style, .body) // now a body paragraph
|
|
XCTAssertEqual(empty.currentParagraph().text, "")
|
|
XCTAssertEqual(v.head, empty.textStart) // caret lands in it
|
|
}
|
|
|
|
func test_enterOnEmptyNestedListItem_outdentsOneLevel_staysList() {
|
|
let v = makeCanvas([
|
|
ParagraphBlock(id: BlockID("e"), list: ListMembership(marker: .bullet, level: 2), runs: []),
|
|
])
|
|
caret(v, v.boxes[0].textStart)
|
|
v.insertText("\n")
|
|
XCTAssertEqual(v.boxes.count, 1) // no new block
|
|
let item = v.boxes[0] as! BlockBox
|
|
XCTAssertEqual(item.listMembership?.marker, .bullet) // still a list item
|
|
XCTAssertEqual(item.listMembership?.level, 1) // outdented one level
|
|
XCTAssertEqual(v.head, item.textStart)
|
|
}
|
|
|
|
func test_enterOnEmptyListItem_endList_undoRestoresBullet() {
|
|
let v = makeCanvas([ParagraphBlock(id: BlockID("e"),
|
|
list: ListMembership(marker: .bullet), runs: [])])
|
|
let um = UndoManager(); um.groupsByEvent = false
|
|
v.undoManagerOverride = um
|
|
caret(v, v.boxes[0].textStart)
|
|
um.beginUndoGrouping(); v.insertText("\n"); um.endUndoGrouping()
|
|
XCTAssertNil((v.boxes[0] as! BlockBox).listMembership) // ended
|
|
um.undo()
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).listMembership?.marker, .bullet) // restored
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).listMembership?.level, 0)
|
|
}
|
|
|
|
func test_enterAtBlockStart_insertsEmptyParagraphAbove() {
|
|
let v = makeCanvas([ParagraphBlock(id: BlockID("a"), runs: [TextRun(text: "Alpha")])])
|
|
caret(v, v.boxes[0].textStart) // caret at start of the block
|
|
v.insertText("\n")
|
|
XCTAssertEqual(v.boxes.count, 2)
|
|
XCTAssertEqual((v.boxes[0] as! BlockBox).currentParagraph().text, "") // empty paragraph above
|
|
XCTAssertEqual(v.boxes[0].id, BlockID("a")) // upper keeps the original id
|
|
XCTAssertEqual((v.boxes[1] as! BlockBox).currentParagraph().text, "Alpha") // text pushed into the lower block
|
|
XCTAssertEqual(v.head, v.boxes[1].textStart) // caret follows the text
|
|
}
|
|
}
|
|
#endif
|