163 lines
6.8 KiB
TypeScript
163 lines
6.8 KiB
TypeScript
// @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'
|
|
import { installBridge, removeBridge } from './stub-bridge'
|
|
|
|
const HEAD = 'a\nb\nc\n'
|
|
const INDEX = 'a\nSTAGED\nc\n'
|
|
const DISK = 'a\nSTAGED\nc\nAFTER-STAGING\n'
|
|
|
|
/** Exactly what git-service returns for porcelain "MM". */
|
|
function stubBridge(): void {
|
|
installBridge(
|
|
[
|
|
{ path: 'demo.txt', status: 'M', staged: true, original: HEAD, updated: INDEX },
|
|
{ path: 'demo.txt', status: 'M', staged: false, original: INDEX, updated: DISK },
|
|
],
|
|
{ 'demo.txt': DISK },
|
|
)
|
|
}
|
|
|
|
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()
|
|
removeBridge()
|
|
})
|
|
|
|
/** Row text of the read-only view currently on screen. */
|
|
function viewText(c: HTMLElement): string {
|
|
return Array.from(c.querySelectorAll('.editor-wrap .ln-row')).map((el) => el.textContent ?? '').join('\n')
|
|
}
|
|
|
|
/** The two panes of the full-screen Diff, as line text. */
|
|
function splitText(c: HTMLElement, pane: 'left' | 'right'): string[] {
|
|
return Array.from(c.querySelectorAll(`.split-pane.${pane} .ln-row`)).map((el) => (el.textContent ?? '').replace(/^\d*/, ''))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/** Open one git row, then lift its pair into the full-screen Diff. */
|
|
async function openSplit(c: HTMLElement, label: 'Staged Changes' | 'Changes'): Promise<void> {
|
|
fireEvent.click(group(c, label)[0])
|
|
await waitFor(() => expect(c.querySelector('.diff-bar')).toBeTruthy())
|
|
fireEvent.click(Array.from(c.querySelectorAll<HTMLElement>('.seg button')).find((b) => b.textContent === 'Diff')!)
|
|
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeTruthy())
|
|
}
|
|
|
|
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()
|
|
await openSplit(c, 'Staged Changes')
|
|
expect(splitText(c, 'left')).toEqual(['a', 'b', 'c'])
|
|
expect(splitText(c, 'right')).toEqual(['a', 'STAGED', 'c'])
|
|
// The later edit is not part of what is staged, so neither side may show it.
|
|
expect(splitText(c, 'right').join('\n')).not.toContain('AFTER-STAGING')
|
|
})
|
|
|
|
it('Diff on the unstaged row compares the staged copy with disk', async () => {
|
|
const c = await boot()
|
|
await openSplit(c, 'Changes')
|
|
// 'b' was already replaced before staging, so this half must not mention it.
|
|
expect(splitText(c, 'left')).toEqual(['a', 'STAGED', 'c', ''])
|
|
expect(splitText(c, 'right')).toEqual(['a', 'STAGED', 'c', 'AFTER-STAGING'])
|
|
})
|
|
|
|
it('the picked panel behind Actual always reaches back to HEAD', async () => {
|
|
const c = await boot()
|
|
fireEvent.click(group(c, 'Changes')[0])
|
|
const scroll = await waitFor(() => {
|
|
const el = c.querySelector<HTMLElement>('.ce-scroll')
|
|
if (!el || !c.querySelector('.ce-band')) throw new Error('bands not ready')
|
|
return el
|
|
})
|
|
// Line 2 is 'STAGED'. Unwrapped, its band runs from 10 + (2-1)*20.
|
|
fireEvent.click(scroll, { clientY: 10 + 20 + 5 })
|
|
const peek = await waitFor(() => {
|
|
const p = c.querySelector<HTMLElement>('.peek')
|
|
if (!p) throw new Error('peek not ready')
|
|
return p
|
|
})
|
|
const removed = Array.from(peek.querySelectorAll('.ln-row.del'))
|
|
expect(removed.map((el) => el.textContent?.replace(/^\d+/, ''))).toEqual(['b'])
|
|
|
|
// A click anywhere off the code drops it.
|
|
fireEvent.mouseDown(document.body)
|
|
await waitFor(() => expect(c.querySelector('.peek')).toBeNull())
|
|
})
|
|
|
|
it('labels which pair the diff is comparing', async () => {
|
|
const c = await boot()
|
|
await openSplit(c, 'Staged Changes')
|
|
expect(c.querySelector('.diff-bar .db-side')?.textContent).toBe('HEAD → staged')
|
|
await openSplit(c, 'Changes')
|
|
expect(c.querySelector('.diff-bar .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)
|
|
})
|
|
})
|