| 1 | import XCTest |
| 2 | @testable import LChatCore |
| 3 | |
| 4 | // The format-as-you-type brain (ComposerRules.swift) — semantics pinned here, |
| 5 | // and the regex literals drift-gated against web/static/composer-pm.js (the |
| 6 | // ComposerTypeaheadTests pattern): a web-side rule change fails this suite |
| 7 | // until the native mirror follows. |
| 8 | final class ComposerRulesTests: XCTestCase { |
| 9 | private var repoRoot: URL { |
| 10 | URL(fileURLWithPath: #filePath) |
| 11 | .deletingLastPathComponent() // Tests/LChatCoreTests |
| 12 | .deletingLastPathComponent() // Tests |
| 13 | .deletingLastPathComponent() // LChatCore |
| 14 | .deletingLastPathComponent() // ios |
| 15 | .deletingLastPathComponent() // repo |
| 16 | } |
| 17 | |
| 18 | // MARK: inline completions |
| 19 | |
| 20 | func testMarkCompletions() { |
| 21 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "**bold**"), |
| 22 | InlineCompletion(replaceRange: 0..<8, content: "bold", mark: .bold)) |
| 23 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "say *it*"), |
| 24 | InlineCompletion(replaceRange: 4..<8, content: "it", mark: .italic)) |
| 25 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "`code`"), |
| 26 | InlineCompletion(replaceRange: 0..<6, content: "code", mark: .code)) |
| 27 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "~gone~"), |
| 28 | InlineCompletion(replaceRange: 0..<6, content: "gone", mark: .strike)) |
| 29 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "say _it_"), |
| 30 | InlineCompletion(replaceRange: 4..<8, content: "it", mark: .italic)) |
| 31 | } |
| 32 | |
| 33 | func testUnderscoreNeverFiresIntraword() { |
| 34 | XCTAssertNil(inlineCompletion(lineUpToCaret: "snake_case_")) |
| 35 | XCTAssertNil(inlineCompletion(lineUpToCaret: "a_b_")) |
| 36 | } |
| 37 | |
| 38 | func testStrongWinsBeforeEm() { |
| 39 | // "**bold**" must fire strong, and the half-typed "**bold*" must not |
| 40 | // half-fire the em rule (its lookbehind rejects the second star). |
| 41 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "**bold**")?.mark, .bold) |
| 42 | XCTAssertNil(inlineCompletion(lineUpToCaret: "**bold*")) |
| 43 | } |
| 44 | |
| 45 | func testTildeLookbehind() { |
| 46 | XCTAssertNil(inlineCompletion(lineUpToCaret: "~~s~"), "double-tilde opener never pairs with a single closer") |
| 47 | } |
| 48 | |
| 49 | func testNoFireOnWhitespaceEdges() { |
| 50 | XCTAssertNil(inlineCompletion(lineUpToCaret: "** x**")) |
| 51 | XCTAssertNil(inlineCompletion(lineUpToCaret: "*x *")) |
| 52 | } |
| 53 | |
| 54 | func testUnicodeContentAndOffsets() { |
| 55 | // replaceRange is UTF-16: ✨ is BMP (1 unit), 😀 is a surrogate pair (2). |
| 56 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "✨ **bí**"), |
| 57 | InlineCompletion(replaceRange: 2..<8, content: "bí", mark: .bold)) |
| 58 | XCTAssertEqual(inlineCompletion(lineUpToCaret: "😀 **x**"), |
| 59 | InlineCompletion(replaceRange: 3..<8, content: "x", mark: .bold)) |
| 60 | } |
| 61 | |
| 62 | // MARK: drift gate against the web rules |
| 63 | |
| 64 | func testRegexLiteralsMatchComposerPmJs() throws { |
| 65 | let js = try String(contentsOf: repoRoot.appendingPathComponent("web/static/composer-pm.js"), encoding: .utf8) |
| 66 | // The exact JS regex literals the Swift patterns transliterate. If a |
| 67 | // rule changes on the web, its literal disappears and this fails. |
| 68 | let literals = [ |
| 69 | #"/\*\*([^*\s](?:[^*]*[^*\s])?)\*\*$/"#, |
| 70 | #"/(?<!\*)\*([^*\s](?:[^*]*[^*\s])?)\*$/"#, |
| 71 | #"/(?<![\w_])_([^_\s](?:[^_]*[^_\s])?)_$/"#, |
| 72 | #"/`([^`\s](?:[^`]*[^`\s])?)`$/"#, |
| 73 | #"/(?<!~)~([^~\s](?:[^~]*[^~\s])?)~$/"#, |
| 74 | ] |
| 75 | for literal in literals { |
| 76 | XCTAssertTrue(js.contains(literal), "composer-pm.js no longer contains \(literal) — mirror the rule change into ComposerRules.swift") |
| 77 | } |
| 78 | } |
| 79 | } |
| 80 | |