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
GitHub Actions (publish-mods)chore: Update mods/ e47cc82 · 14 days ago History
402 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 closeOnEscape: true,
294 rows: 8,
295 })
296 })
297
298 test('narrow under the fullscreen layout, the resize line', async ($, on) => {
299 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
300
301 await $.session.start(Fixtures.SESSION)
302 await $.command.run(Fixtures.DIFF)
303 await world.clock.advance(Fixtures.SETTLE_MS)
304
305 expect(
306 Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)),
307 ).toBe(
308 'Resize your terminal to at least 110 columns to show the diff panel',
309 )
310 })
311
312 test('off fullscreen, the window follows the walk', async ($, on) => {
313 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
314
315 await $.session.start(Fixtures.SESSION)
316 await $.command.run(Fixtures.DIALOG_DIFF)
317 await world.clock.advance(Fixtures.SETTLE_MS)
318
319 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
320
321 expect(drawn).toContain('\u276f file0.ts')
322 expect(drawn).toContain(' file4.ts')
323 expect(drawn).not.toContain('file5.ts')
324 expect(drawn).toContain(' \u2193 5 more files')
325 expect(await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))).toEqual({})
326
327 expect(
328 world.focused.map(focus => focus.element),
329 'its row once centred',
330 ).toEqual(['file:file2.ts'])
331
332 const walked = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
333
334 expect(walked).toContain(' \u2191 1 more file')
335 expect(walked).toContain('\u276f file3.ts')
336 expect(walked).toContain(' file5.ts')
337 expect(walked).toContain(' \u2193 4 more files')
338 expect(walked).not.toContain('file0.ts')
339 })
340
341 test('the walk follows under the name a build registers', async ($, on) => {
342 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
343
344 await $.session.start(Fixtures.SESSION)
345 await $.command.run(Fixtures.DIALOG_DIFF)
346 await world.clock.advance(Fixtures.SETTLE_MS)
347 await $.ui.render(Fixtures.INLINE_PANE)
348
349 expect(
350 await $.ui.focus(Fixtures.ringOnto('file:file3.ts', 'cc-plugin-diff')),
351 ).toEqual({})
352
353 expect(
354 world.focused.map(focus => focus.element),
355 'its row once centred',
356 ).toEqual(['file:file2.ts'])
357 })
358
359 test('off fullscreen, the walk stops at the last file', async ($, on) => {
360 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
361
362 await $.session.start(Fixtures.SESSION)
363 await $.command.run(Fixtures.DIALOG_DIFF)
364 await world.clock.advance(Fixtures.SETTLE_MS)
365 await $.ui.render(Fixtures.INLINE_PANE)
366 await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))
367 await $.ui.render(Fixtures.INLINE_PANE)
368 await $.ui.focus(Fixtures.ringOnto('file:file5.ts'))
369 await $.ui.render(Fixtures.INLINE_PANE)
370 await $.ui.focus(Fixtures.ringOnto('file:file7.ts'))
371 await $.ui.render(Fixtures.INLINE_PANE)
372 await $.ui.focus(Fixtures.ringOnto('file:file9.ts'))
373
374 const last = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
375
376 expect(last).toContain('\u276f file9.ts')
377 expect(last).toContain(' \u2191 5 more files')
378
379 expect(
380 await $.ui.focus(Fixtures.ringOnto('file:file5.ts')),
381 'the ring would wrap to the first row drawn; it stays',
382 ).toEqual({})
383
384 expect(Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))).toContain(
385 '\u276f file9.ts',
386 )
387
388 expect(world.focused, 'the wrap never reached the engine').toHaveLength(4)
389 })
390
391 test('off fullscreen, the dialog draws at any width', async ($, on) => {
392 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
393
394 await $.session.start(Fixtures.SESSION)
395 await $.command.run(Fixtures.DIALOG_DIFF)
396 await world.clock.advance(Fixtures.SETTLE_MS)
397
398 expect(
399 Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)),
400 ).toContain('Uncommitted changes (git diff HEAD)')
401 })
402})