157 lines
6.6 KiB
TypeScript
157 lines
6.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
import { act, 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'
|
|
|
|
beforeAll(() => {
|
|
globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {} } as never
|
|
if (!Element.prototype.scrollIntoView) Element.prototype.scrollIntoView = () => {}
|
|
})
|
|
afterEach(() => { cleanup(); localStorage.clear() })
|
|
|
|
function find(c: HTMLElement, sel: string, text: string): HTMLElement | undefined {
|
|
return Array.from(c.querySelectorAll<HTMLElement>(sel)).find((el) => el.textContent?.trim() === text)
|
|
}
|
|
|
|
async function openChanged(): Promise<HTMLElement> {
|
|
const c = render(<ProjectProvider><App /></ProjectProvider>).container
|
|
const row = await waitFor(() => {
|
|
const r = Array.from(c.querySelectorAll<HTMLElement>('.git-row')).find((el) => el.textContent?.includes('UserController.php'))
|
|
if (!r) throw new Error('git not ready')
|
|
return r
|
|
})
|
|
fireEvent.click(row)
|
|
await waitFor(() => expect(c.querySelector('.diff-bar')).toBeTruthy())
|
|
return c
|
|
}
|
|
|
|
describe('Editor view modes', () => {
|
|
it('Actual mode is an editable buffer; Original is read-only', async () => {
|
|
const c = await openChanged()
|
|
fireEvent.click(find(c, '.seg button', 'Actual')!)
|
|
await waitFor(() => expect(c.querySelector('.ce-ta')).toBeTruthy())
|
|
|
|
fireEvent.click(find(c, '.seg button', 'Original')!)
|
|
await waitFor(() => expect(c.querySelector('.ce-ta')).toBeNull())
|
|
expect(c.querySelector('.editor .ln-row')).toBeTruthy()
|
|
})
|
|
|
|
it('a git row opens the file in Actual, not in the full-screen Diff', async () => {
|
|
const c = await openChanged()
|
|
expect(c.querySelector('.split-overlay')).toBeNull()
|
|
expect(find(c, '.seg button.on', 'Actual')).toBeTruthy()
|
|
await waitFor(() => expect(c.querySelector('.ce-ta')).toBeTruthy())
|
|
})
|
|
|
|
it('Actual bands every changed line, over a buffer that stays editable', async () => {
|
|
const c = await openChanged()
|
|
// The mock rewrites six lines of UserController.php and removes none.
|
|
await waitFor(() => expect(c.querySelectorAll('.ce-band')).toHaveLength(6))
|
|
expect(c.querySelector('.ce-gaprule')).toBeNull()
|
|
expect(c.querySelectorAll('.ce-gutter .chg')).toHaveLength(6)
|
|
expect(c.querySelector('.ln-sign')).toBeNull()
|
|
expect((c.querySelector('.ce-ta') as HTMLTextAreaElement).readOnly).toBe(false)
|
|
})
|
|
|
|
it('the Diff segment opens the full-screen view, and Esc puts back the mode it covered', async () => {
|
|
const c = await openChanged()
|
|
fireEvent.click(find(c, '.seg button', 'Original')!)
|
|
await waitFor(() => expect(c.querySelector('.ce-ta')).toBeNull())
|
|
|
|
fireEvent.click(find(c, '.seg button', 'Diff')!)
|
|
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeTruthy())
|
|
|
|
act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })) })
|
|
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeNull())
|
|
expect(find(c, '.seg button.on', 'Original')).toBeTruthy()
|
|
})
|
|
|
|
it('hovering a band in Actual reveals the original, leaving the editor hides it', async () => {
|
|
const c = await openChanged()
|
|
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 30 is the first rewritten line. Unwrapped, its band is arithmetic:
|
|
// a 10px top pad plus 20px per line, and the hit test reads clientY.
|
|
fireEvent.mouseMove(scroll, { clientY: 10 + 29 * 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).toHaveLength(1)
|
|
expect(removed[0].textContent).toContain("'plan' => $user->plan,")
|
|
|
|
fireEvent.mouseLeave(scroll)
|
|
await waitFor(() => expect(c.querySelector('.peek')).toBeNull())
|
|
})
|
|
|
|
it('⌘M cycles Actual → Original → Diff, and nothing else', async () => {
|
|
const c = await openChanged()
|
|
const seen: string[] = []
|
|
for (let i = 0; i < 3; i++) {
|
|
act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'm', metaKey: true })) })
|
|
await waitFor(() => {
|
|
const on = c.querySelector('.seg button.on')?.textContent ?? ''
|
|
if (seen[seen.length - 1] === on) throw new Error('mode not changed')
|
|
seen.push(on)
|
|
})
|
|
}
|
|
// A tab opens in Actual, so the first ⌘M already leaves it.
|
|
expect(seen).toEqual(['Original', 'Diff', 'Actual'])
|
|
})
|
|
})
|
|
|
|
// Default config wraps Markdown only (editor.wordWrap: 'markdown'). A wrapped
|
|
// buffer drops the fixed gutter column for one block per line, so the folded
|
|
// rows carry no number.
|
|
describe('Word wrap', () => {
|
|
async function openTree(c: HTMLElement, name: string): Promise<void> {
|
|
const row = await waitFor(() => {
|
|
const r = Array.from(c.querySelectorAll<HTMLElement>('.tree-row')).find((el) => el.textContent?.includes(name))
|
|
if (!r) throw new Error(`${name} not ready`)
|
|
return r
|
|
})
|
|
fireEvent.click(row)
|
|
}
|
|
|
|
it('folds long lines in Markdown, one numbered block per source line', async () => {
|
|
const c = render(<ProjectProvider><App /></ProjectProvider>).container
|
|
await openTree(c, 'README.md')
|
|
const edit = await waitFor(() => {
|
|
const el = c.querySelector<HTMLElement>('.code-edit')
|
|
if (!el) throw new Error('editor not ready')
|
|
return el
|
|
})
|
|
expect(edit.classList.contains('wrap')).toBe(true)
|
|
expect(edit.querySelector('.ce-gutter')).toBeNull()
|
|
const ta = edit.querySelector<HTMLTextAreaElement>('.ce-ta')!
|
|
expect(ta.getAttribute('wrap')).toBe('soft')
|
|
expect(edit.querySelectorAll('.ce-line').length).toBe(ta.value.split('\n').length)
|
|
})
|
|
|
|
it('leaves other file types on the scrolling gutter layout', async () => {
|
|
const c = render(<ProjectProvider><App /></ProjectProvider>).container
|
|
await openTree(c, 'composer.json')
|
|
const edit = await waitFor(() => {
|
|
const el = c.querySelector<HTMLElement>('.code-edit')
|
|
if (!el) throw new Error('editor not ready')
|
|
return el
|
|
})
|
|
expect(edit.classList.contains('wrap')).toBe(false)
|
|
expect(edit.querySelector('.ce-gutter')).toBeTruthy()
|
|
expect(edit.querySelector('.ce-line')).toBeNull()
|
|
})
|
|
})
|