| 1 | // Run: tsx src/__tests__/ref-token.test.tsx |
| 2 | // |
| 3 | // Tests for the @-token escape grammar (lib/refToken) and the composer/file |
| 4 | // menu helpers that produce and consume it. Mirrors internal/control/refs.go: |
| 5 | // backslash-escaped space/tab stays in the token; other backslashes are |
| 6 | // literal so Windows separators keep their meaning. |
| 7 | |
| 8 | import { escapeRefPath, unescapeRefPath, activeRefTokenRe } from "../lib/refToken"; |
| 9 | import { activeFileReferenceToken, pickInlineFileReference } from "../components/FileReferenceMenu"; |
| 10 | import { composerPickFileEntry } from "../components/Composer"; |
| 11 | import type { DirEntry } from "../lib/types"; |
| 12 | |
| 13 | let passed = 0; |
| 14 | let failed = 0; |
| 15 | |
| 16 | function eq(a: unknown, b: unknown, label: string) { |
| 17 | if (JSON.stringify(a) === JSON.stringify(b)) { |
| 18 | process.stdout.write(` PASS ${label}\n`); |
| 19 | passed += 1; |
| 20 | } else { |
| 21 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}\n`); |
| 22 | failed += 1; |
| 23 | } |
| 24 | } |
| 25 | |
| 26 | console.log("\nref token grammar"); |
| 27 | |
| 28 | eq(escapeRefPath("my file.txt"), "my\\ file.txt", "escapeRefPath escapes spaces"); |
| 29 | eq(escapeRefPath("plain.txt"), "plain.txt", "escapeRefPath leaves space-free paths alone"); |
| 30 | eq(unescapeRefPath("my\\ file.txt"), "my file.txt", "unescapeRefPath reverses escaping"); |
| 31 | eq(unescapeRefPath("C:\\dir\\file.png"), "C:\\dir\\file.png", "unescapeRefPath keeps Windows separators literal"); |
| 32 | eq(unescapeRefPath(escapeRefPath("a b\tc.md")), "a b\tc.md", "escape/unescape round-trips"); |
| 33 | |
| 34 | eq(activeRefTokenRe.exec("see @docs/my\\ file.md")?.[1], "docs/my\\ file.md", "active token keeps escaped spaces"); |
| 35 | eq(activeRefTokenRe.exec("see @docs/x plain")?.[1], undefined, "unescaped space still ends the token"); |
| 36 | |
| 37 | console.log("\nfile reference menu"); |
| 38 | |
| 39 | eq( |
| 40 | activeFileReferenceToken("look at @docs/my\\ dir/fra"), |
| 41 | { raw: "docs/my\\ dir/fra", dir: "docs/my\\ dir/", frag: "fra" }, |
| 42 | "token splits dir/frag with escapes intact", |
| 43 | ); |
| 44 | eq(activeFileReferenceToken("@my\\ f")?.frag, "my f", "frag is unescaped for entry matching"); |
| 45 | |
| 46 | const spacedFile: DirEntry = { name: "my file.md", isDir: false }; |
| 47 | eq( |
| 48 | pickInlineFileReference("see @docs/", "docs/", "docs/", spacedFile), |
| 49 | "see @docs/my\\ file.md ", |
| 50 | "inline pick escapes whitespace in the inserted ref", |
| 51 | ); |
| 52 | eq( |
| 53 | pickInlineFileReference("see @docs/my\\ dir/", "docs/my\\ dir/", "docs/my\\ dir/", spacedFile), |
| 54 | "see @docs/my\\ dir/my\\ file.md ", |
| 55 | "inline pick through an escaped dir stays escaped once", |
| 56 | ); |
| 57 | |
| 58 | console.log("\ncomposer pick"); |
| 59 | |
| 60 | const inline = composerPickFileEntry("note @sr", "sr", "", { name: "my report.pdf", isDir: false }); |
| 61 | eq(inline.text, "note @my\\ report.pdf ", "composer inline fallback escapes whitespace"); |
| 62 | eq(inline.workspaceRef, undefined, "no structured ref for a name-only entry"); |
| 63 | |
| 64 | const structured = composerPickFileEntry("note @sr", "sr", "", { |
| 65 | name: "my report.pdf", |
| 66 | path: "docs/my report.pdf", |
| 67 | isDir: false, |
| 68 | }); |
| 69 | eq(structured.workspaceRef?.path, "docs/my report.pdf", "structured ref keeps the real unescaped path"); |
| 70 | eq(structured.text, "note ", "structured pick removes the typed token"); |
| 71 | |
| 72 | process.stdout.write(`\n${passed} passed, ${failed} failed\n`); |
| 73 | if (failed > 0) process.exit(1); |
| 74 |