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.test.ts
403 lines · 13.4 KBRaw
1import { describe, expect, test, tier } from 'claude-code/testing'
2
3import Fixtures from './fixtures'
4
5tier('builtin')
6
7describe('views', () => {
8 test('docked, every file has its hunks under the list', async ($, on) => {
9 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
10
11 await $.session.start(Fixtures.SESSION)
12 await $.command.run(Fixtures.DIFF)
13 await world.clock.advance(Fixtures.SETTLE_MS)
14
15 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
16
17 expect(drawn).toContain('2 files changed +3 -1')
18
19 expect(drawn, "a closing empty row, as the built-in's").toContain(
20 '+const a = 2\n ',
21 )
22
23 expect(drawn, 'none after the last file').toMatch(/\+export const c = 2$/)
24 expect(drawn).not.toContain('❯')
25 })
26
27 test('a failed hunks read blanks every body, not the list', async ($, on) => {
28 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES, {
29 hunksRefusal: () => Fixtures.GIT_HUNG,
30 })
31
32 await $.session.start(Fixtures.SESSION)
33 await $.command.run(Fixtures.DIFF)
34 await world.clock.advance(Fixtures.SETTLE_MS)
35
36 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
37
38 expect(drawn).toContain('2 files changed +3 -1')
39 expect(drawn).toContain('app.ts')
40 expect(drawn).toContain('lib.ts')
41 expect(drawn.match(/Diff unavailable/g), 'once a file').toHaveLength(2)
42 expect(drawn).not.toContain('const a')
43 })
44
45 test('the read after a failed one draws the hunks again', async ($, on) => {
46 let refusal: string | null = Fixtures.GIT_HUNG
47
48 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES, {
49 hunksRefusal: () => refusal,
50 })
51
52 on('tool.call', () => ({ result: 'ran' }))
53
54 await $.session.start(Fixtures.SESSION)
55 await $.command.run(Fixtures.DIFF)
56 await world.clock.advance(Fixtures.SETTLE_MS)
57
58 expect(
59 Fixtures.textOf(await $.ui.render(Fixtures.PANE)),
60 'the failed read blanked the hunks first',
61 ).toContain('Diff unavailable')
62
63 refusal = null
64
65 await $.tool.call({ tool: 'Bash', command: 'make' })
66 await world.clock.advance(Fixtures.SETTLE_MS)
67
68 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
69
70 expect(drawn).not.toContain('Diff unavailable')
71 expect(drawn).toContain('+const a = 2')
72 expect(drawn).toContain('+export const c = 2')
73 })
74
75 test('inline, a row opens that file alone', async ($, on) => {
76 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
77
78 await $.session.start(Fixtures.SESSION)
79 await $.command.run(Fixtures.DIALOG_DIFF)
80 await world.clock.advance(Fixtures.SETTLE_MS)
81 await $.ui.render(Fixtures.INLINE_PANE)
82
83 expect(await $.ui.press({ plugin: 'diff', key: 'file:lib.ts' })).toEqual({
84 element: 'file:lib.ts',
85 })
86
87 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
88
89 expect(drawn).toContain('+export const c = 2')
90 expect(drawn).not.toContain('+const a = 2')
91 expect(drawn).not.toContain('app.ts')
92 expect(drawn).toContain('\u2191/\u2193 to scroll \u00b7 Esc to back')
93 })
94
95 test('inline, a file edited before the session opens too', async ($, on) => {
96 const world = Fixtures.inRepository(on, Fixtures.MOVED_IN)
97
98 on('session.usage', () => ({ value: Fixtures.usageAt(Fixtures.SETTLE_MS) }))
99 Fixtures.oldFiles(on)
100
101 await $.session.start(Fixtures.SESSION)
102 await $.command.run(Fixtures.DIALOG_DIFF)
103 await world.clock.advance(Fixtures.SETTLE_MS)
104
105 expect(
106 Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)),
107 "the dialog lists it beside the session's file",
108 ).toContain('old.ts')
109
110 expect(await $.ui.press({ plugin: 'diff', key: 'file:old.ts' })).toEqual({
111 element: 'file:old.ts',
112 })
113
114 await world.clock.advance(Fixtures.SETTLE_MS)
115
116 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
117
118 expect(
119 drawn,
120 'a listed row can be pressed, so its hunks were read',
121 ).not.toContain('Loading diff')
122
123 expect(drawn).toContain('+b')
124 expect(drawn).not.toContain('+d')
125 })
126
127 test('inline, a file the docked pane hides opens too', async ($, on) => {
128 const world = Fixtures.inRepository(on, Fixtures.WITH_LOCKFILE)
129
130 await $.session.start(Fixtures.SESSION)
131 await $.command.run(Fixtures.DIALOG_DIFF)
132 await world.clock.advance(Fixtures.SETTLE_MS)
133 await $.ui.render(Fixtures.INLINE_PANE)
134
135 expect(await $.ui.press({ plugin: 'diff', key: 'file:bun.lock' })).toEqual({
136 element: 'file:bun.lock',
137 })
138
139 await world.clock.advance(Fixtures.SETTLE_MS)
140
141 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
142
143 expect(drawn, 'generated, and still a listed row').not.toContain(
144 'Loading diff',
145 )
146
147 expect(drawn).toContain('+"left": "1.0.1"')
148 })
149
150 test('inline, a window that moves reads only its new rows', async ($, on) => {
151 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
152
153 on('session.usage', () => ({ value: Fixtures.usageAt(Fixtures.SETTLE_MS) }))
154
155 Fixtures.oldFiles(
156 on,
157 Array.from(
158 { length: Fixtures.MANY_FILE_COUNT },
159 (_, at) => `file${at}.ts`,
160 ),
161 )
162
163 await $.session.start(Fixtures.SESSION)
164 await $.command.run(Fixtures.DIALOG_DIFF)
165 await world.clock.advance(Fixtures.SETTLE_MS)
166 await $.ui.render(Fixtures.INLINE_PANE)
167 await world.clock.advance(Fixtures.SETTLE_MS)
168 await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))
169 await world.clock.advance(Fixtures.SETTLE_MS)
170 await $.ui.render(Fixtures.INLINE_PANE)
171 await $.ui.press({ plugin: 'diff', key: 'file:file5.ts' })
172 await world.clock.advance(Fixtures.SETTLE_MS)
173
174 expect(
175 world.runs
176 .filter(run => run.argv.includes('--raw'))
177 .map(run => run.argv.slice(run.argv.indexOf('--') + 1).join(' ')),
178 'the first five, the one the walk brought in, the two the press did',
179 ).toEqual([
180 'file0.ts file1.ts file2.ts file3.ts file4.ts',
181 'file5.ts',
182 'file6.ts file7.ts',
183 ])
184 })
185
186 test('docked, a wheel tick moves the body, not the list', async ($, on) => {
187 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
188
189 await $.session.start(Fixtures.SESSION)
190 await $.command.run(Fixtures.DIFF)
191 await world.clock.advance(Fixtures.SETTLE_MS)
192
193 const before = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
194
195 expect(before, 'listed, and named over its body').toContain('file0.ts')
196 expect(before.split('file0.ts')).toHaveLength(3)
197
198 expect(await $.ui.scroll(Fixtures.WHEEL_TICK)).toEqual({})
199
200 const after = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
201
202 expect(after.split('file0.ts'), 'its name row scrolled away').toHaveLength(
203 2,
204 )
205
206 expect(after).toContain('-const v = 0')
207 expect(after).toContain('10 files changed +10 -10')
208 expect(after, 'the list stays').not.toContain('more above')
209 })
210
211 test('docked, the wheel over a long list moves the list', async ($, on) => {
212 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
213
214 await $.session.start(Fixtures.SESSION)
215 await $.command.run(Fixtures.DIFF)
216 await world.clock.advance(Fixtures.SETTLE_MS)
217 await $.ui.render(Fixtures.PANE)
218
219 expect(await $.ui.scroll(Fixtures.WHEEL_OVER_LIST)).toEqual({})
220
221 const after = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
222
223 expect(after).toContain('\u2191 1 more above')
224 expect(after).toContain('file8.ts')
225 expect(after.split('file0.ts'), 'the body stays').toHaveLength(2)
226 })
227
228 test('past eight files the docked list scrolls by key', async ($, on) => {
229 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
230
231 await $.session.start(Fixtures.SESSION)
232 await $.command.run(Fixtures.DIFF)
233 await world.clock.advance(Fixtures.SETTLE_MS)
234
235 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
236
237 expect(drawn).toContain('10 files changed +10 -10')
238 expect(drawn).toContain('file7.ts')
239 expect(drawn).toContain('\u2193 2 more below (opt+\u2193 to scroll)')
240 expect(drawn).not.toContain('file8.ts')
241
242 expect(await $.ui.press({ plugin: 'diff', key: 'list-down' })).toEqual({
243 element: 'list-down',
244 })
245
246 const scrolled = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
247
248 expect(scrolled).toContain('\u2191 1 more above')
249 expect(scrolled).toContain('file8.ts')
250 expect(scrolled).toContain('\u2193 1 more below')
251 })
252
253 test('a rename lists as git prints it, and reads no body', async ($, on) => {
254 const world = Fixtures.inRepository(on, Fixtures.RENAMED)
255
256 await $.session.start(Fixtures.SESSION)
257 await $.command.run(Fixtures.DIFF)
258 await world.clock.advance(Fixtures.SETTLE_MS)
259
260 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
261
262 expect(drawn).toContain('1 file changed')
263 expect(drawn).toContain('docs/{notes.txt => renamed-notes.txt}')
264 expect(drawn).toContain('No diff content')
265
266 expect(
267 world.runs.filter(run => run.argv.includes('docs/renamed-notes.txt')),
268 ).toEqual([])
269 })
270
271 test('inline, the dialog lists every file and its keys', async ($, on) => {
272 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
273
274 await $.session.start(Fixtures.SESSION)
275 await $.command.run(Fixtures.DIALOG_DIFF)
276 await world.clock.advance(Fixtures.SETTLE_MS)
277
278 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
279
280 expect(drawn).toContain('Uncommitted changes (git diff HEAD)')
281 expect(drawn).toContain('2 files changed +3 -1')
282 expect(drawn).toContain('\u276f app.ts')
283 expect(drawn).toContain(' lib.ts')
284 expect(drawn).not.toContain('+const a = 2')
285
286 expect(drawn).toContain(
287 '\u2191/\u2193 to select \u00b7 Enter to view \u00b7 Esc to close',
288 )
289
290 expect(world.opened.at(-1), 'sized to its rows once listed').toEqual({
291 id: 'diff',
292 title: 'Diff',
293 holdToasts: true,
294 closeOnEscape: true,
295 rows: 8,
296 })
297 })
298
299 test('narrow under the fullscreen layout, the resize line', async ($, on) => {
300 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
301
302 await $.session.start(Fixtures.SESSION)
303 await $.command.run(Fixtures.DIFF)
304 await world.clock.advance(Fixtures.SETTLE_MS)
305
306 expect(
307 Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)),
308 ).toBe(
309 'Resize your terminal to at least 110 columns to show the diff panel',
310 )
311 })
312
313 test('off fullscreen, the window follows the walk', async ($, on) => {
314 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
315
316 await $.session.start(Fixtures.SESSION)
317 await $.command.run(Fixtures.DIALOG_DIFF)
318 await world.clock.advance(Fixtures.SETTLE_MS)
319
320 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
321
322 expect(drawn).toContain('\u276f file0.ts')
323 expect(drawn).toContain(' file4.ts')
324 expect(drawn).not.toContain('file5.ts')
325 expect(drawn).toContain(' \u2193 5 more files')
326 expect(await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))).toEqual({})
327
328 expect(
329 world.focused.map(focus => focus.element),
330 'its row once centred',
331 ).toEqual(['file:file2.ts'])
332
333 const walked = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
334
335 expect(walked).toContain(' \u2191 1 more file')
336 expect(walked).toContain('\u276f file3.ts')
337 expect(walked).toContain(' file5.ts')
338 expect(walked).toContain(' \u2193 4 more files')
339 expect(walked).not.toContain('file0.ts')
340 })
341
342 test('the walk follows under the name a build registers', async ($, on) => {
343 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
344
345 await $.session.start(Fixtures.SESSION)
346 await $.command.run(Fixtures.DIALOG_DIFF)
347 await world.clock.advance(Fixtures.SETTLE_MS)
348 await $.ui.render(Fixtures.INLINE_PANE)
349
350 expect(
351 await $.ui.focus(Fixtures.ringOnto('file:file3.ts', 'cc-plugin-diff')),
352 ).toEqual({})
353
354 expect(
355 world.focused.map(focus => focus.element),
356 'its row once centred',
357 ).toEqual(['file:file2.ts'])
358 })
359
360 test('off fullscreen, the walk stops at the last file', async ($, on) => {
361 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
362
363 await $.session.start(Fixtures.SESSION)
364 await $.command.run(Fixtures.DIALOG_DIFF)
365 await world.clock.advance(Fixtures.SETTLE_MS)
366 await $.ui.render(Fixtures.INLINE_PANE)
367 await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))
368 await $.ui.render(Fixtures.INLINE_PANE)
369 await $.ui.focus(Fixtures.ringOnto('file:file5.ts'))
370 await $.ui.render(Fixtures.INLINE_PANE)
371 await $.ui.focus(Fixtures.ringOnto('file:file7.ts'))
372 await $.ui.render(Fixtures.INLINE_PANE)
373 await $.ui.focus(Fixtures.ringOnto('file:file9.ts'))
374
375 const last = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
376
377 expect(last).toContain('\u276f file9.ts')
378 expect(last).toContain(' \u2191 5 more files')
379
380 expect(
381 await $.ui.focus(Fixtures.ringOnto('file:file5.ts')),
382 'the ring would wrap to the first row drawn; it stays',
383 ).toEqual({})
384
385 expect(Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))).toContain(
386 '\u276f file9.ts',
387 )
388
389 expect(world.focused, 'the wrap never reached the engine').toHaveLength(4)
390 })
391
392 test('off fullscreen, the dialog draws at any width', async ($, on) => {
393 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
394
395 await $.session.start(Fixtures.SESSION)
396 await $.command.run(Fixtures.DIALOG_DIFF)
397 await world.clock.advance(Fixtures.SETTLE_MS)
398
399 expect(
400 Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)),
401 ).toContain('Uncommitted changes (git diff HEAD)')
402 })
403})