handling files when stages and dirty at once
This commit is contained in:
167
test/git-two-rows.test.tsx
Normal file
167
test/git-two-rows.test.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// A file can be staged and then edited again. Git calls that "MM": two rows,
|
||||
// one per group. These tests drive the renderer with such a payload and check
|
||||
// that Diff follows the row you clicked, while Original and Actual do not.
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
|
||||
vi.mock('../src/renderer/src/terminals', () => {
|
||||
let n = 0
|
||||
return { Terminal: () => null, lid: () => ++n }
|
||||
})
|
||||
|
||||
import { App } from '../src/renderer/src/App'
|
||||
import { ProjectProvider } from '../src/renderer/src/project'
|
||||
|
||||
const HEAD = 'a\nb\nc\n'
|
||||
const INDEX = 'a\nSTAGED\nc\n'
|
||||
const DISK = 'a\nSTAGED\nc\nAFTER-STAGING\n'
|
||||
|
||||
/** Minimal preload bridge: enough for the store to boot with real git rows. */
|
||||
function stubBridge(): void {
|
||||
const noop = (): void => {}
|
||||
const off = (): (() => void) => noop
|
||||
;(window as unknown as { helder: unknown }).helder = {
|
||||
platform: 'darwin',
|
||||
clipboard: { writeText: noop, readText: () => '' },
|
||||
project: {
|
||||
current: async () => ({ root: '/repo', name: 'repo' }),
|
||||
open: async () => ({ root: '/repo', name: 'repo' }),
|
||||
openPath: async () => ({ root: '/repo', name: 'repo' }),
|
||||
recent: async () => [],
|
||||
},
|
||||
fs: {
|
||||
tree: async () => ({ name: 'repo', type: 'dir', path: '', children: [{ name: 'demo.txt', type: 'file', path: 'demo.txt' }] }),
|
||||
readDir: async () => [],
|
||||
files: async () => ({ 'demo.txt': DISK }),
|
||||
read: async () => DISK,
|
||||
imageDataUrl: async () => '',
|
||||
write: async () => {},
|
||||
delete: async () => {},
|
||||
create: async () => {},
|
||||
mkdir: async () => {},
|
||||
},
|
||||
shell: { reveal: noop },
|
||||
git: {
|
||||
// Exactly what git-service now returns for porcelain "MM".
|
||||
load: async () => ({
|
||||
branch: 'main',
|
||||
changes: [
|
||||
{ path: 'demo.txt', status: 'M', staged: true, original: HEAD, updated: INDEX },
|
||||
{ path: 'demo.txt', status: 'M', staged: false, original: INDEX, updated: DISK },
|
||||
],
|
||||
}),
|
||||
stage: async () => {}, unstage: async () => {}, commit: async () => {},
|
||||
push: async () => ({ ok: true, message: '' }), discard: async () => {},
|
||||
},
|
||||
pty: { available: async () => false, create: async () => 1, write: noop, resize: noop, kill: noop, onData: off, onExit: off },
|
||||
config: { get: async () => (await import('../src/renderer/src/types')).DEFAULT_CONFIG, theme: async () => '' },
|
||||
recent: { get: async () => [], set: async () => {} },
|
||||
search: { content: async () => [], files: async () => [] },
|
||||
dialog: { unsavedClose: async () => 'cancel' },
|
||||
log: { write: noop, path: async () => null, open: async () => {}, reveal: async () => {} },
|
||||
onProjectChanged: off,
|
||||
onConfigChanged: off,
|
||||
onRefresh: off,
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
globalThis.ResizeObserver = class { observe(): void {} unobserve(): void {} disconnect(): void {} } as never
|
||||
if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = (): void => {}
|
||||
})
|
||||
beforeEach(stubBridge)
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
localStorage.clear()
|
||||
delete (window as unknown as { helder?: unknown }).helder
|
||||
})
|
||||
|
||||
/** Row text of the view currently on screen. */
|
||||
function viewText(c: HTMLElement): string {
|
||||
return Array.from(c.querySelectorAll('.editor .ln-row')).map((el) => el.textContent ?? '').join('\n')
|
||||
}
|
||||
|
||||
function group(c: HTMLElement, label: 'Staged Changes' | 'Changes'): HTMLElement[] {
|
||||
const heads = Array.from(c.querySelectorAll<HTMLElement>('.git-group'))
|
||||
const head = heads.find((h) => h.textContent?.startsWith(label))!
|
||||
const rows: HTMLElement[] = []
|
||||
for (let el = head.nextElementSibling; el; el = el.nextElementSibling) {
|
||||
if (el.classList.contains('git-group') || el.classList.contains('git-divider')) break
|
||||
if (el.classList.contains('git-row')) rows.push(el as HTMLElement)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
async function boot(): Promise<HTMLElement> {
|
||||
const c = render(<ProjectProvider><App /></ProjectProvider>).container
|
||||
await waitFor(() => {
|
||||
if (c.querySelectorAll('.git-row').length < 2) throw new Error('git not ready')
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
describe('a file that is staged and then edited again', () => {
|
||||
it('shows up in both groups', async () => {
|
||||
const c = await boot()
|
||||
expect(group(c, 'Staged Changes').map((r) => r.getAttribute('title'))).toEqual(['demo.txt'])
|
||||
expect(group(c, 'Changes').map((r) => r.getAttribute('title'))).toEqual(['demo.txt'])
|
||||
})
|
||||
|
||||
it('Diff on the staged row compares HEAD with the staged copy', async () => {
|
||||
const c = await boot()
|
||||
fireEvent.click(group(c, 'Staged Changes')[0])
|
||||
await waitFor(() => expect(c.querySelector('.diff-bar')).toBeTruthy())
|
||||
const text = viewText(c)
|
||||
expect(text).toContain('b')
|
||||
expect(text).toContain('STAGED')
|
||||
// The later edit is not part of what is staged, so it must not show here.
|
||||
expect(text).not.toContain('AFTER-STAGING')
|
||||
})
|
||||
|
||||
it('Diff on the unstaged row compares the staged copy with disk', async () => {
|
||||
const c = await boot()
|
||||
fireEvent.click(group(c, 'Changes')[0])
|
||||
await waitFor(() => expect(c.querySelector('.diff-bar')).toBeTruthy())
|
||||
const text = viewText(c)
|
||||
expect(text).toContain('AFTER-STAGING')
|
||||
// 'b' was already replaced before staging, so this half must not mention it.
|
||||
expect(text.split('\n').some((l) => l.trim() === 'b')).toBe(false)
|
||||
})
|
||||
|
||||
it('labels which pair the diff is comparing', async () => {
|
||||
const c = await boot()
|
||||
fireEvent.click(group(c, 'Staged Changes')[0])
|
||||
await waitFor(() => expect(c.querySelector('.db-side')?.textContent).toBe('HEAD → staged'))
|
||||
fireEvent.click(group(c, 'Changes')[0])
|
||||
await waitFor(() => expect(c.querySelector('.db-side')?.textContent).toBe('staged → actual'))
|
||||
})
|
||||
|
||||
it('Original stays HEAD and Actual stays the file on disk, from either row', async () => {
|
||||
const c = await boot()
|
||||
for (const label of ['Staged Changes', 'Changes'] as const) {
|
||||
fireEvent.click(group(c, label)[0])
|
||||
await waitFor(() => expect(c.querySelector('.diff-bar')).toBeTruthy())
|
||||
|
||||
const original = Array.from(c.querySelectorAll<HTMLElement>('.seg button')).find((b) => b.textContent === 'Original')!
|
||||
fireEvent.click(original)
|
||||
await waitFor(() => expect(viewText(c)).toContain('b'))
|
||||
expect(viewText(c)).not.toContain('AFTER-STAGING')
|
||||
|
||||
const actual = Array.from(c.querySelectorAll<HTMLElement>('.seg button')).find((b) => b.textContent === 'Actual')!
|
||||
fireEvent.click(actual)
|
||||
await waitFor(() => expect(c.querySelector('.ce-ta')).toBeTruthy())
|
||||
expect((c.querySelector('.ce-ta') as HTMLTextAreaElement).value).toBe(DISK)
|
||||
}
|
||||
})
|
||||
|
||||
it('only the row you opened is highlighted', async () => {
|
||||
const c = await boot()
|
||||
fireEvent.click(group(c, 'Changes')[0])
|
||||
await waitFor(() => expect(c.querySelector('.git-row.active')).toBeTruthy())
|
||||
expect(c.querySelectorAll('.git-row.active')).toHaveLength(1)
|
||||
expect(group(c, 'Changes')[0].classList.contains('active')).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user