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,18 +1,21 @@ | ||
| 1 | 1 | import type { ProcessRunResult } from 'claude-code' |
| 2 | 2 | |
| 3 | 3 | import { NOT_A_REPOSITORY } from './not-a-repository.js' |
| 4 | -import { REPOSITORY } from './repository.js' | |
| 5 | 4 | |
| 6 | 5 | /** |
| 7 | - * What git answers in /work (REPOSITORY); an invocation it does not know | |
| 8 | - * fails as git does outside a repository. | |
| 6 | + * What git answers in /work; an invocation the answers do not know fails | |
| 7 | + * as git does outside a repository. | |
| 9 | 8 | * |
| 10 | 9 | * @param argv the invocation, program first |
| 10 | + * @param answers git's output by a key the invocation's command line holds | |
| 11 | 11 | * @returns git's exit code and output |
| 12 | 12 | */ |
| 13 | -export function gitIn(argv: readonly string[]): ProcessRunResult { | |
| 13 | +export function gitIn( | |
| 14 | + argv: readonly string[], | |
| 15 | + answers: Readonly<Record<string, string>>, | |
| 16 | +): ProcessRunResult { | |
| 14 | 17 | const line = argv.join(' ') |
| 15 | - const found = Object.entries(REPOSITORY).find(([key]) => line.includes(key)) | |
| 18 | + const found = Object.entries(answers).find(([key]) => line.includes(key)) | |
| 16 | 19 | |
| 17 | 20 | return found |
| 18 | 21 | ? { exitCode: 0, stdout: found[1], stderr: '' } |
| @@ -4,6 +4,7 @@ | ||
| 4 | 4 | import { gitIn } from './git-in.js' |
| 5 | 5 | import { HINT_DRAWN } from './hint-drawn.js' |
| 6 | 6 | import { keeping } from './keeping.js' |
| 7 | +import { REPOSITORY } from './repository.js' | |
| 7 | 8 | import { startsSession } from './starts-session.js' |
| 8 | 9 | |
| 9 | 10 | /** |
| @@ -13,9 +14,13 @@ | ||
| 13 | 14 | * The store starts empty, the clock at 0, and the engine draws the hint. |
| 14 | 15 | * |
| 15 | 16 | * @param on the test's `on` |
| 17 | + * @param answers git's output by invocation key; one changed file when absent | |
| 16 | 18 | * @returns the runs, the panes opened and closed, and the session's clock |
| 17 | 19 | */ |
| 18 | -export function inRepository(on: On) { | |
| 20 | +export function inRepository( | |
| 21 | + on: On, | |
| 22 | + answers: Readonly<Record<string, string>> = REPOSITORY, | |
| 23 | +) { | |
| 19 | 24 | const runs: Args<'process.run'>[] = [] |
| 20 | 25 | const opened = keeping<Args<'ui.open'>>() |
| 21 | 26 | const closed = keeping<Args<'ui.close'>>() |
| @@ -24,7 +29,7 @@ | ||
| 24 | 29 | on('process.run', ($, e) => { |
| 25 | 30 | runs.push(e) |
| 26 | 31 | |
| 27 | - return { value: gitIn(e.argv) } | |
| 32 | + return { value: gitIn(e.argv, answers) } | |
| 28 | 33 | }) |
| 29 | 34 | |
| 30 | 35 | on('ui.open', opened.hook) |
| @@ -6,13 +6,22 @@ | ||
| 6 | 6 | export * from './hint.js' |
| 7 | 7 | export * from './hint-drawn.js' |
| 8 | 8 | export * from './in-repository.js' |
| 9 | +export * from './inline-pane.js' | |
| 9 | 10 | export * from './keeping.js' |
| 11 | +export * from './many-file-count.js' | |
| 12 | +export * from './many-files.js' | |
| 13 | +export * from './moved-in.js' | |
| 14 | +export * from './narrow-inline-pane.js' | |
| 10 | 15 | export * from './not-a-repository.js' |
| 16 | +export * from './old-files.js' | |
| 11 | 17 | export * from './pane.js' |
| 18 | +export * from './renamed.js' | |
| 12 | 19 | export * from './repository.js' |
| 20 | +export * from './repository-of.js' | |
| 13 | 21 | export * from './session.js' |
| 14 | 22 | export * from './settle-ms.js' |
| 15 | 23 | export * from './starts-session.js' |
| 16 | 24 | export * from './text-of.js' |
| 25 | +export * from './two-files.js' | |
| 17 | 26 | |
| 18 | 27 | export * as default from '.' |
| @@ -1,0 +1,12 @@ | ||
| 1 | +import type { RenderInput } from 'claude-code' | |
| 2 | + | |
| 3 | +import { PANE } from './pane.js' | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * The diff pane seated inline above the prompt on a 160-column terminal, | |
| 7 | + * as a session without the fullscreen layout seats it. | |
| 8 | + */ | |
| 9 | +export const INLINE_PANE: RenderInput<'Pane'> = { | |
| 10 | + ...PANE, | |
| 11 | + props: { ...PANE.props, placement: 'inline', bodyColumns: 156 }, | |
| 12 | +} |
| @@ -1,0 +1,5 @@ | ||
| 1 | +/** | |
| 2 | + * How many files MANY_FILES changes: two more than the docked list shows at | |
| 3 | + * once. | |
| 4 | + */ | |
| 5 | +export const MANY_FILE_COUNT = 10 |
| @@ -1,0 +1,19 @@ | ||
| 1 | +import { MANY_FILE_COUNT } from './many-file-count.js' | |
| 2 | +import { repositoryOf } from './repository-of.js' | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * Git's output in /work where MANY_FILE_COUNT files changed a line each: | |
| 6 | + * more rows than the docked list shows at once. | |
| 7 | + */ | |
| 8 | +export const MANY_FILES = repositoryOf( | |
| 9 | + Array.from( | |
| 10 | + { length: MANY_FILE_COUNT }, | |
| 11 | + (_, at) => `1\t1\tfile${at}.ts\0`, | |
| 12 | + ).join(''), | |
| 13 | + Object.fromEntries( | |
| 14 | + Array.from({ length: MANY_FILE_COUNT }, (_, at) => [ | |
| 15 | + `file${at}.ts`, | |
| 16 | + `@@ -1 +1 @@\n-const v = ${at}\n+const v = ${at + 1}\n`, | |
| 17 | + ]), | |
| 18 | + ), | |
| 19 | +) |
| @@ -1,0 +1,14 @@ | ||
| 1 | +import { repositoryOf } from './repository-of.js' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Git's output in /work where two old files differ from HEAD: one dirty | |
| 5 | + * before the session began, one moved into place since (not dirty then). | |
| 6 | + */ | |
| 7 | +export const MOVED_IN = repositoryOf( | |
| 8 | + '1\t1\told.ts\0' + '1\t1\tmoved.ts\0', | |
| 9 | + { | |
| 10 | + 'old.ts': '@@ -1 +1 @@\n-a\n+b\n', | |
| 11 | + 'moved.ts': '@@ -1 +1 @@\n-c\n+d\n', | |
| 12 | + }, | |
| 13 | + ['old.ts'], | |
| 14 | +) |
| @@ -1,0 +1,13 @@ | ||
| 1 | +import type { RenderInput } from 'claude-code' | |
| 2 | + | |
| 3 | +import { INLINE_PANE } from './inline-pane.js' | |
| 4 | + | |
| 5 | +/** | |
| 6 | + * The diff pane seated inline on a 100-column terminal: too narrow for the | |
| 7 | + * built-in panel, which shows only its wider-terminal line there. | |
| 8 | + */ | |
| 9 | +export const NARROW_INLINE_PANE: RenderInput<'Pane'> = { | |
| 10 | + ...INLINE_PANE, | |
| 11 | + viewport: { columns: 100, rows: 30 }, | |
| 12 | + props: { ...INLINE_PANE.props, bodyColumns: 96 }, | |
| 13 | +} |
| @@ -1,0 +1,18 @@ | ||
| 1 | +import type { On } from 'claude-code' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Answers `$.fs` over /work holding MOVED_IN's two files, both last | |
| 5 | + * written at time 0, before any session began. | |
| 6 | + * | |
| 7 | + * @param on the test's `on` | |
| 8 | + */ | |
| 9 | +export function oldFiles(on: On) { | |
| 10 | + on('fs.list', () => ({ | |
| 11 | + value: [ | |
| 12 | + { name: 'old.ts', kind: 'file', size: 2 }, | |
| 13 | + { name: 'moved.ts', kind: 'file', size: 2 }, | |
| 14 | + ], | |
| 15 | + })) | |
| 16 | + | |
| 17 | + on('fs.stat', () => ({ value: { kind: 'file', size: 2, mtimeMs: 0 } })) | |
| 18 | +} |
| @@ -1,0 +1,10 @@ | ||
| 1 | +import { repositoryOf } from './repository-of.js' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Git's output in /work where one file was renamed inside its directory, | |
| 5 | + * its contents untouched. | |
| 6 | + */ | |
| 7 | +export const RENAMED = repositoryOf( | |
| 8 | + '0\t0\t\0docs/notes.txt\0docs/renamed-notes.txt\0', | |
| 9 | + {}, | |
| 10 | +) |
| @@ -1,0 +1,27 @@ | ||
| 1 | +/** | |
| 2 | + * Git's output in /work for each invocation whose command line holds the | |
| 3 | + * key, over the given `--numstat -z` records and per-file bodies. | |
| 4 | + * | |
| 5 | + * `status` lists the paths dirty at the start (the baseline: every path | |
| 6 | + * with a body unless given); a body is keyed by the `-- <path>` its diff is | |
| 7 | + * asked with. | |
| 8 | + * | |
| 9 | + * @param numstat the `--numstat -z` records, joined | |
| 10 | + * @param bodies each path's unified diff after its header | |
| 11 | + * @param dirty the paths `status` lists as dirty when the session starts | |
| 12 | + * @returns the answers by key | |
| 13 | + */ | |
| 14 | +export const repositoryOf = ( | |
| 15 | + numstat: string, | |
| 16 | + bodies: Readonly<Record<string, string>>, | |
| 17 | + dirty: readonly string[] = Object.keys(bodies), | |
| 18 | +): Readonly<Record<string, string>> => ({ | |
| 19 | + 'rev-parse --path-format=absolute': '/work\n/work/.git\n/work/.git\n', | |
| 20 | + 'status --porcelain': dirty.map(path => ` M ${path}\0`).join(''), | |
| 21 | + 'HEAD --shortstat': ` ${Object.keys(bodies).length} files changed`, | |
| 22 | + 'HEAD --numstat': numstat, | |
| 23 | + 'ls-files': '', | |
| 24 | + ...Object.fromEntries( | |
| 25 | + Object.entries(bodies).map(([path, body]) => [`-- ${path}`, body]), | |
| 26 | + ), | |
| 27 | +}) |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * Longer than every wait the pane schedules before it fetches and draws |
| 3 | - * (its open probe, refresh debounce and redraw), short of its HEAD poll. | |
| 3 | + * (its refresh debounce and redraw), short of its HEAD poll. | |
| 4 | 4 | */ |
| 5 | 5 | export const SETTLE_MS = 1000 |
| @@ -7,11 +7,12 @@ | ||
| 7 | 7 | * directory, the time, and each command it registers, under its name. |
| 8 | 8 | * |
| 9 | 9 | * @param on the test's `on` |
| 10 | - * @returns the clock the session reads, at 0 until the test moves it | |
| 10 | + * @param now where the clock starts, in milliseconds (0 when not given) | |
| 11 | + * @returns the clock the session reads, at `now` until the test moves it | |
| 11 | 12 | */ |
| 12 | -export function startsSession(on: On): MockClock { | |
| 13 | +export function startsSession(on: On, now = 0): MockClock { | |
| 13 | 14 | on('session.start', ($, e) => ({ cwd: e.cwd })) |
| 14 | 15 | on('command.register', ($, e) => ({ value: { command: e.name } })) |
| 15 | 16 | |
| 16 | - return mock.clock(on) | |
| 17 | + return mock.clock(on, { now }) | |
| 17 | 18 | } |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | /** |
| 2 | - * A rendered tree's text as it reads: its strings and its labels, in order. | |
| 2 | + * A rendered tree's text as it reads: its strings, its labels and its code | |
| 3 | + * sources, in order. | |
| 3 | 4 | * |
| 4 | 5 | * @param tree what `$.ui.render` resolved to, or a part of it |
| 5 | 6 | * @returns the text |
| @@ -19,10 +20,14 @@ | ||
| 19 | 20 | |
| 20 | 21 | const props: unknown = Reflect.get(tree, 'props') |
| 21 | 22 | |
| 22 | - const label: unknown = | |
| 23 | - typeof props === 'object' && props ? Reflect.get(props, 'label') : undefined | |
| 23 | + function leadOf(prop: string): string { | |
| 24 | + const value: unknown = | |
| 25 | + typeof props === 'object' && props ? Reflect.get(props, prop) : undefined | |
| 24 | 26 | |
| 25 | - const lead = typeof label === 'string' ? label : '' | |
| 27 | + return typeof value === 'string' ? value : '' | |
| 28 | + } | |
| 26 | 29 | |
| 30 | + const lead = `${leadOf('label')}${leadOf('source')}` | |
| 31 | + | |
| 27 | 32 | return `${lead}${textOf(Reflect.get(tree, 'children') ?? [])}` |
| 28 | 33 | } |
| @@ -1,0 +1,9 @@ | ||
| 1 | +import { repositoryOf } from './repository-of.js' | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * Git's output in /work where two files changed, a hunk each. | |
| 5 | + */ | |
| 6 | +export const TWO_FILES = repositoryOf('1\t1\tapp.ts\0' + '2\t0\tlib.ts\0', { | |
| 7 | + 'app.ts': '@@ -1 +1 @@\n-const a = 1\n+const a = 2\n', | |
| 8 | + 'lib.ts': '@@ -3,0 +4,2 @@\n+export const b = 1\n+export const c = 2\n', | |
| 9 | +}) |
| @@ -1,4 +1,4 @@ | ||
| 1 | -import { describe, expect, test, tier } from 'claude-code/testing' | |
| 1 | +import { describe, expect, mock, test, tier } from 'claude-code/testing' | |
| 2 | 2 | |
| 3 | 3 | import Fixtures from './fixtures' |
| 4 | 4 | |
| @@ -28,4 +28,28 @@ | ||
| 28 | 28 | expect(run.init).toMatchObject({ cwd: '/work', env: { LC_ALL: 'C' } }) |
| 29 | 29 | } |
| 30 | 30 | }) |
| 31 | + | |
| 32 | + test('a file moved in since the start is session work', async ($, on) => { | |
| 33 | + const clock = Fixtures.startsSession(on, Fixtures.SETTLE_MS) | |
| 34 | + | |
| 35 | + on('process.run', ($, e) => ({ | |
| 36 | + value: Fixtures.gitIn(e.argv, Fixtures.MOVED_IN), | |
| 37 | + })) | |
| 38 | + | |
| 39 | + on('ui.open', () => ({ value: undefined })) | |
| 40 | + on('ui.invalidate', () => ({ value: undefined })) | |
| 41 | + on('session.messages', () => ({ value: [] })) | |
| 42 | + Fixtures.oldFiles(on) | |
| 43 | + mock.store(on) | |
| 44 | + | |
| 45 | + await $.session.start(Fixtures.SESSION) | |
| 46 | + await $.command.run(Fixtures.DIFF) | |
| 47 | + await clock.advance(Fixtures.SETTLE_MS) | |
| 48 | + | |
| 49 | + const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) | |
| 50 | + | |
| 51 | + expect(drawn).toContain('1 file changed +1 -1') | |
| 52 | + expect(drawn).toContain('moved.ts') | |
| 53 | + expect(drawn).toContain('+1 file edited before this session (show)') | |
| 54 | + }) | |
| 31 | 55 | }) |
| @@ -120,14 +120,17 @@ | ||
| 120 | 120 | ]) |
| 121 | 121 | }) |
| 122 | 122 | |
| 123 | - test('/diff opens the pane over the session changes', async ($, on) => { | |
| 123 | + test('/diff opens the pane, unfocused, and says so', async ($, on) => { | |
| 124 | 124 | const world = Fixtures.inRepository(on) |
| 125 | 125 | |
| 126 | 126 | await $.session.start(Fixtures.SESSION) |
| 127 | 127 | |
| 128 | - expect(await $.command.run(Fixtures.DIFF)).toEqual({}) | |
| 129 | - expect(world.opened.map(pane => pane.id)).toEqual(['diff']) | |
| 128 | + expect(await $.command.run(Fixtures.DIFF)).toEqual({ | |
| 129 | + text: 'Diff panel shown', | |
| 130 | + }) | |
| 130 | 131 | |
| 132 | + expect(world.opened).toEqual([{ id: 'diff', title: 'Diff' }]) | |
| 133 | + | |
| 131 | 134 | await world.clock.advance(Fixtures.SETTLE_MS) |
| 132 | 135 | |
| 133 | 136 | const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) |
| @@ -136,23 +139,17 @@ | ||
| 136 | 139 | expect(drawn).toContain('app.ts') |
| 137 | 140 | }) |
| 138 | 141 | |
| 139 | - test('the close button closes the pane; /diff reopens it', async ($, on) => { | |
| 142 | + test('/diff again closes the pane and says so', async ($, on) => { | |
| 140 | 143 | const world = Fixtures.inRepository(on) |
| 141 | 144 | |
| 142 | 145 | await $.session.start(Fixtures.SESSION) |
| 143 | 146 | await $.command.run(Fixtures.DIFF) |
| 144 | - await world.clock.advance(Fixtures.SETTLE_MS) | |
| 145 | - await $.ui.render(Fixtures.PANE) | |
| 146 | 147 | |
| 147 | - expect(await $.ui.press({ plugin: 'diff', key: 'close' })).toEqual({ | |
| 148 | - element: 'close', | |
| 148 | + expect(await $.command.run(Fixtures.DIFF)).toEqual({ | |
| 149 | + text: 'Diff panel hidden', | |
| 149 | 150 | }) |
| 150 | 151 | |
| 151 | - await world.clock.settle() | |
| 152 | - | |
| 153 | 152 | expect(world.closed.map(pane => pane.id)).toEqual(['diff']) |
| 154 | - expect(await $.command.run(Fixtures.DIFF)).toEqual({}) | |
| 155 | - expect(world.opened.map(pane => pane.id)).toEqual(['diff', 'diff']) | |
| 156 | 153 | }) |
| 157 | 154 | |
| 158 | 155 | test('a wide terminal opens the pane at the first edit', async ($, on) => { |
| @@ -1,0 +1,105 @@ | ||
| 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 | + expect(drawn).toContain('+const a = 2') | |
| 19 | + expect(drawn).toContain('+export const c = 2') | |
| 20 | + expect(drawn).not.toContain('❯') | |
| 21 | + }) | |
| 22 | + | |
| 23 | + test('inline, a row moves the pointer and the body to it', async ($, on) => { | |
| 24 | + const world = Fixtures.inRepository(on, Fixtures.TWO_FILES) | |
| 25 | + | |
| 26 | + await $.session.start(Fixtures.SESSION) | |
| 27 | + await $.command.run(Fixtures.DIFF) | |
| 28 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 29 | + await $.ui.render(Fixtures.INLINE_PANE) | |
| 30 | + | |
| 31 | + expect(await $.ui.press({ plugin: 'diff', key: 'file:lib.ts' })).toEqual({ | |
| 32 | + element: 'file:lib.ts', | |
| 33 | + }) | |
| 34 | + | |
| 35 | + const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)) | |
| 36 | + | |
| 37 | + expect(drawn).toContain('\u276f lib.ts') | |
| 38 | + expect(drawn).toContain('+export const c = 2') | |
| 39 | + expect(drawn).not.toContain('+const a = 2') | |
| 40 | + }) | |
| 41 | + | |
| 42 | + test('past eight files the docked list counts the rest', async ($, on) => { | |
| 43 | + const world = Fixtures.inRepository(on, Fixtures.MANY_FILES) | |
| 44 | + | |
| 45 | + await $.session.start(Fixtures.SESSION) | |
| 46 | + await $.command.run(Fixtures.DIFF) | |
| 47 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 48 | + | |
| 49 | + const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) | |
| 50 | + | |
| 51 | + expect(drawn).toContain('10 files changed +10 -10') | |
| 52 | + expect(drawn).toContain('file7.ts') | |
| 53 | + expect(drawn).toContain('↓ 2 more below') | |
| 54 | + | |
| 55 | + expect(drawn.indexOf('↓ 2 more below')).toBeLessThan( | |
| 56 | + drawn.indexOf('file8.ts'), | |
| 57 | + ) | |
| 58 | + }) | |
| 59 | + | |
| 60 | + test('a rename lists as git prints it, and reads no body', async ($, on) => { | |
| 61 | + const world = Fixtures.inRepository(on, Fixtures.RENAMED) | |
| 62 | + | |
| 63 | + await $.session.start(Fixtures.SESSION) | |
| 64 | + await $.command.run(Fixtures.DIFF) | |
| 65 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 66 | + | |
| 67 | + const drawn = Fixtures.textOf(await $.ui.render(Fixtures.PANE)) | |
| 68 | + | |
| 69 | + expect(drawn).toContain('1 file changed') | |
| 70 | + expect(drawn).toContain('docs/{notes.txt => renamed-notes.txt}') | |
| 71 | + expect(drawn).toContain('No diff content') | |
| 72 | + | |
| 73 | + expect( | |
| 74 | + world.runs.filter(run => run.argv.includes('docs/renamed-notes.txt')), | |
| 75 | + ).toEqual([]) | |
| 76 | + }) | |
| 77 | + | |
| 78 | + test('inline on a wide terminal, the rows round one file', async ($, on) => { | |
| 79 | + const world = Fixtures.inRepository(on, Fixtures.TWO_FILES) | |
| 80 | + | |
| 81 | + await $.session.start(Fixtures.SESSION) | |
| 82 | + await $.command.run(Fixtures.DIFF) | |
| 83 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 84 | + | |
| 85 | + const drawn = Fixtures.textOf(await $.ui.render(Fixtures.INLINE_PANE)) | |
| 86 | + | |
| 87 | + expect(drawn).toContain('❯ app.ts') | |
| 88 | + expect(drawn).toContain('+const a = 2') | |
| 89 | + expect(drawn).not.toContain('+export const c = 2') | |
| 90 | + }) | |
| 91 | + | |
| 92 | + test('inline on a narrow terminal, the resize line', async ($, on) => { | |
| 93 | + const world = Fixtures.inRepository(on, Fixtures.TWO_FILES) | |
| 94 | + | |
| 95 | + await $.session.start(Fixtures.SESSION) | |
| 96 | + await $.command.run(Fixtures.DIFF) | |
| 97 | + await world.clock.advance(Fixtures.SETTLE_MS) | |
| 98 | + | |
| 99 | + expect( | |
| 100 | + Fixtures.textOf(await $.ui.render(Fixtures.NARROW_INLINE_PANE)), | |
| 101 | + ).toBe( | |
| 102 | + 'Resize your terminal to at least 110 columns to show the diff panel', | |
| 103 | + ) | |
| 104 | + }) | |
| 105 | +}) |