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 Fixtures from './fixtures' |
| 4 | |
| 5 | tier('builtin') |
| 6 | |
| 7 | describe('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 | }) |