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
344 lines · 11.5 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('docked, a wheel tick moves the body, not the list', async ($, on) => {
128 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
129
130 await $.session.start(Fixtures.SESSION)
131 await $.command.run(Fixtures.DIFF)
132 await world.clock.advance(Fixtures.SETTLE_MS)
133
134 const before = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
135
136 expect(before, 'listed, and named over its body').toContain('file0.ts')
137 expect(before.split('file0.ts')).toHaveLength(3)
138
139 expect(await $.ui.scroll(Fixtures.WHEEL_TICK)).toEqual({})
140
141 const after = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
142
143 expect(after.split('file0.ts'), 'its name row scrolled away').toHaveLength(
144 2,
145 )
146
147 expect(after).toContain('-const v = 0')
148 expect(after).toContain('10 files changed +10 -10')
149 expect(after, 'the list stays').not.toContain('more above')
150 })
151
152 test('docked, the wheel over a long list moves the list', async ($, on) => {
153 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
154
155 await $.session.start(Fixtures.SESSION)
156 await $.command.run(Fixtures.DIFF)
157 await world.clock.advance(Fixtures.SETTLE_MS)
158 await $.ui.render(Fixtures.PANE)
159
160 expect(await $.ui.scroll(Fixtures.WHEEL_OVER_LIST)).toEqual({})
161
162 const after = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
163
164 expect(after).toContain('\u2191 1 more above')
165 expect(after).toContain('file8.ts')
166 expect(after.split('file0.ts'), 'the body stays').toHaveLength(2)
167 })
168
169 test('past eight files the docked list scrolls by key', async ($, on) => {
170 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
171
172 await $.session.start(Fixtures.SESSION)
173 await $.command.run(Fixtures.DIFF)
174 await world.clock.advance(Fixtures.SETTLE_MS)
175
176 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
177
178 expect(drawn).toContain('10 files changed +10 -10')
179 expect(drawn).toContain('file7.ts')
180 expect(drawn).toContain('\u2193 2 more below (opt+\u2193 to scroll)')
181 expect(drawn).not.toContain('file8.ts')
182
183 expect(await $.ui.press({ plugin: 'diff', key: 'list-down' })).toEqual({
184 element: 'list-down',
185 })
186
187 const scrolled = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
188
189 expect(scrolled).toContain('\u2191 1 more above')
190 expect(scrolled).toContain('file8.ts')
191 expect(scrolled).toContain('\u2193 1 more below')
192 })
193
194 test('a rename lists as git prints it, and reads no body', async ($, on) => {
195 const world = Fixtures.inRepository(on, Fixtures.RENAMED)
196
197 await $.session.start(Fixtures.SESSION)
198 await $.command.run(Fixtures.DIFF)
199 await world.clock.advance(Fixtures.SETTLE_MS)
200
201 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE))
202
203 expect(drawn).toContain('1 file changed')
204 expect(drawn).toContain('docs/{notes.txt => renamed-notes.txt}')
205 expect(drawn).toContain('No diff content')
206
207 expect(
208 world.runs.filter(run => run.argv.includes('docs/renamed-notes.txt')),
209 ).toEqual([])
210 })
211
212 test('inline, the dialog lists every file and its keys', async ($, on) => {
213 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
214
215 await $.session.start(Fixtures.SESSION)
216 await $.command.run(Fixtures.DIALOG_DIFF)
217 await world.clock.advance(Fixtures.SETTLE_MS)
218
219 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
220
221 expect(drawn).toContain('Uncommitted changes (git diff HEAD)')
222 expect(drawn).toContain('2 files changed +3 -1')
223 expect(drawn).toContain('\u276f app.ts')
224 expect(drawn).toContain(' lib.ts')
225 expect(drawn).not.toContain('+const a = 2')
226
227 expect(drawn).toContain(
228 '\u2191/\u2193 to select \u00b7 Enter to view \u00b7 Esc to close',
229 )
230
231 expect(world.opened.at(-1), 'sized to its rows once listed').toEqual({
232 id: 'diff',
233 title: 'Diff',
234 holdToasts: true,
235 closeOnEscape: true,
236 rows: 8,
237 })
238 })
239
240 test('narrow under the fullscreen layout, the resize line', async ($, on) => {
241 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
242
243 await $.session.start(Fixtures.SESSION)
244 await $.command.run(Fixtures.DIFF)
245 await world.clock.advance(Fixtures.SETTLE_MS)
246
247 expect(
248 Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)),
249 ).toBe(
250 'Resize your terminal to at least 110 columns to show the diff panel',
251 )
252 })
253
254 test('off fullscreen, the window follows the walk', async ($, on) => {
255 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
256
257 await $.session.start(Fixtures.SESSION)
258 await $.command.run(Fixtures.DIALOG_DIFF)
259 await world.clock.advance(Fixtures.SETTLE_MS)
260
261 const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
262
263 expect(drawn).toContain('\u276f file0.ts')
264 expect(drawn).toContain(' file4.ts')
265 expect(drawn).not.toContain('file5.ts')
266 expect(drawn).toContain(' \u2193 5 more files')
267 expect(await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))).toEqual({})
268
269 expect(
270 world.focused.map(focus => focus.element),
271 'its row once centred',
272 ).toEqual(['file:file2.ts'])
273
274 const walked = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
275
276 expect(walked).toContain(' \u2191 1 more file')
277 expect(walked).toContain('\u276f file3.ts')
278 expect(walked).toContain(' file5.ts')
279 expect(walked).toContain(' \u2193 4 more files')
280 expect(walked).not.toContain('file0.ts')
281 })
282
283 test('the walk follows under the name a build registers', async ($, on) => {
284 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
285
286 await $.session.start(Fixtures.SESSION)
287 await $.command.run(Fixtures.DIALOG_DIFF)
288 await world.clock.advance(Fixtures.SETTLE_MS)
289 await $.ui.render(Fixtures.INLINE_PANE)
290
291 expect(
292 await $.ui.focus(Fixtures.ringOnto('file:file3.ts', 'cc-plugin-diff')),
293 ).toEqual({})
294
295 expect(
296 world.focused.map(focus => focus.element),
297 'its row once centred',
298 ).toEqual(['file:file2.ts'])
299 })
300
301 test('off fullscreen, the walk stops at the last file', async ($, on) => {
302 const world = Fixtures.inRepository(on, Fixtures.MANY_FILES)
303
304 await $.session.start(Fixtures.SESSION)
305 await $.command.run(Fixtures.DIALOG_DIFF)
306 await world.clock.advance(Fixtures.SETTLE_MS)
307 await $.ui.render(Fixtures.INLINE_PANE)
308 await $.ui.focus(Fixtures.ringOnto('file:file3.ts'))
309 await $.ui.render(Fixtures.INLINE_PANE)
310 await $.ui.focus(Fixtures.ringOnto('file:file5.ts'))
311 await $.ui.render(Fixtures.INLINE_PANE)
312 await $.ui.focus(Fixtures.ringOnto('file:file7.ts'))
313 await $.ui.render(Fixtures.INLINE_PANE)
314 await $.ui.focus(Fixtures.ringOnto('file:file9.ts'))
315
316 const last = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))
317
318 expect(last).toContain('\u276f file9.ts')
319 expect(last).toContain(' \u2191 5 more files')
320
321 expect(
322 await $.ui.focus(Fixtures.ringOnto('file:file5.ts')),
323 'the ring would wrap to the first row drawn; it stays',
324 ).toEqual({})
325
326 expect(Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE))).toContain(
327 '\u276f file9.ts',
328 )
329
330 expect(world.focused, 'the wrap never reached the engine').toHaveLength(4)
331 })
332
333 test('off fullscreen, the dialog draws at any width', async ($, on) => {
334 const world = Fixtures.inRepository(on, Fixtures.TWO_FILES)
335
336 await $.session.start(Fixtures.SESSION)
337 await $.command.run(Fixtures.DIALOG_DIFF)
338 await world.clock.advance(Fixtures.SETTLE_MS)
339
340 expect(
341 Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)),
342 ).toContain('Uncommitted changes (git diff HEAD)')
343 })
344})