commit
This commit is contained in:
@@ -94,3 +94,35 @@ describe('Stage + commit', () => {
|
||||
await waitFor(() => expect(find(c, '.toast', 'Committed')).toBeTruthy())
|
||||
})
|
||||
})
|
||||
|
||||
describe('Explorer background menu', () => {
|
||||
it('right-clicking the empty space offers New file + Show in Finder only', async () => {
|
||||
const c = renderApp()
|
||||
const body = await waitFor(() => {
|
||||
const b = c.querySelector<HTMLElement>('.tree-body')
|
||||
if (!b || !b.querySelector('.tree-row')) throw new Error('tree not ready')
|
||||
return b
|
||||
})
|
||||
fireEvent.contextMenu(body)
|
||||
const labels = await waitFor(() => {
|
||||
const items = Array.from(c.querySelectorAll<HTMLElement>('.ctx-item')).map((el) => el.textContent)
|
||||
if (!items.length) throw new Error('menu not open')
|
||||
return items
|
||||
})
|
||||
expect(labels).toEqual(['New file', 'Show in Finder'])
|
||||
// the root is never renameable or deletable — that would mean the project
|
||||
expect(labels.some((l) => l?.includes('Delete'))).toBe(false)
|
||||
})
|
||||
|
||||
it('a row right-click still shows the file menu, not the root one', async () => {
|
||||
const c = renderApp()
|
||||
const row = await waitFor(() => {
|
||||
const r = find(c, '.tree-row', 'package.json')
|
||||
if (!r) throw new Error('tree not ready')
|
||||
return r
|
||||
})
|
||||
fireEvent.contextMenu(row)
|
||||
await waitFor(() => expect(find(c, '.ctx-item', 'Rename file')).toBeTruthy())
|
||||
expect(find(c, '.ctx-item', 'Copy file name')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,3 +55,45 @@ describe('Editor view modes', () => {
|
||||
await waitFor(() => expect(c.querySelector('.split-overlay')).toBeNull())
|
||||
})
|
||||
})
|
||||
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { simpleGit } from 'simple-git'
|
||||
import { buildTreeFromPaths, createProjectFile, readAll, readDirChildren, readProjectFile, readTree, writeProjectFile } from '../src/main/fs-service'
|
||||
import { buildTreeFromPaths, createProjectDir, createProjectFile, readAll, readDirChildren, readProjectFile, readTree, renameProjectEntry, writeProjectFile } from '../src/main/fs-service'
|
||||
|
||||
let dir = ''
|
||||
afterEach(async () => { if (dir) await rm(dir, { recursive: true, force: true }) })
|
||||
@@ -150,3 +150,48 @@ describe('createProjectFile', () => {
|
||||
await expect(createProjectFile(dir, '../escape.txt')).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('renameProjectEntry', () => {
|
||||
it('renames a file in place and returns the new relative path', async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
||||
await createProjectFile(dir, 'src/old.ts')
|
||||
await writeProjectFile(dir, 'src/old.ts', 'x\n')
|
||||
const next = await renameProjectEntry(dir, 'src/old.ts', 'new.ts')
|
||||
expect(next).toBe('src/new.ts')
|
||||
expect(await readProjectFile(dir, 'src/new.ts')).toBe('x\n')
|
||||
})
|
||||
|
||||
it('renames a folder with everything inside it', async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
||||
await createProjectFile(dir, 'old/a.ts')
|
||||
await writeProjectFile(dir, 'old/a.ts', 'a\n')
|
||||
const next = await renameProjectEntry(dir, 'old', 'new')
|
||||
expect(next).toBe('new')
|
||||
expect(await readProjectFile(dir, 'new/a.ts')).toBe('a\n')
|
||||
})
|
||||
|
||||
it('changes only the case of a name', async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
||||
await writeProjectFile(dir, 'notes.md', 'hi\n')
|
||||
expect(await renameProjectEntry(dir, 'notes.md', 'Notes.md')).toBe('Notes.md')
|
||||
expect(await readProjectFile(dir, 'Notes.md')).toBe('hi\n')
|
||||
})
|
||||
|
||||
it('refuses a name that is already taken', async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
||||
await writeProjectFile(dir, 'a.txt', 'a\n')
|
||||
await writeProjectFile(dir, 'b.txt', 'b\n')
|
||||
await expect(renameProjectEntry(dir, 'a.txt', 'b.txt')).rejects.toThrow()
|
||||
expect(await readProjectFile(dir, 'b.txt')).toBe('b\n')
|
||||
})
|
||||
|
||||
it('refuses a path-shaped or empty name', async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), 'helder-fs-'))
|
||||
await createProjectDir(dir, 'sub')
|
||||
await writeProjectFile(dir, 'a.txt', 'a\n')
|
||||
await expect(renameProjectEntry(dir, 'a.txt', 'sub/a.txt')).rejects.toThrow()
|
||||
await expect(renameProjectEntry(dir, 'a.txt', '../a.txt')).rejects.toThrow()
|
||||
await expect(renameProjectEntry(dir, 'a.txt', ' ')).rejects.toThrow()
|
||||
expect(await readProjectFile(dir, 'a.txt')).toBe('a\n')
|
||||
})
|
||||
})
|
||||
|
||||
55
test/history-open.test.tsx
Normal file
55
test/history-open.test.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
// @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?.includes(text))
|
||||
}
|
||||
async function open(c: HTMLElement, name: string): Promise<void> {
|
||||
const row = await waitFor(() => {
|
||||
const r = find(c, '.tree-row', name)
|
||||
if (!r) throw new Error(`${name} not ready`)
|
||||
return r
|
||||
})
|
||||
fireEvent.click(row)
|
||||
}
|
||||
function key(k: string, meta = false): void {
|
||||
act(() => { window.dispatchEvent(new KeyboardEvent('keydown', { key: k, metaKey: meta, bubbles: true })) })
|
||||
}
|
||||
|
||||
describe('Recent-files navigator (⌘↓)', () => {
|
||||
it('Enter opens the highlighted file', async () => {
|
||||
const c = render(<ProjectProvider><App /></ProjectProvider>).container
|
||||
await open(c, 'composer.json')
|
||||
await open(c, 'README.md')
|
||||
await waitFor(() => expect(find(c, '.tb-crumb', 'README.md')).toBeTruthy())
|
||||
|
||||
key('ArrowDown', true)
|
||||
const modal = await waitFor(() => {
|
||||
const m = c.querySelector<HTMLElement>('.history-modal')
|
||||
if (!m) throw new Error('history not open')
|
||||
return m
|
||||
})
|
||||
// ⌘↓ pre-selects the previous file (index 1), i.e. composer.json.
|
||||
expect(modal.querySelector('.hist-row.sel')?.textContent).toContain('composer.json')
|
||||
|
||||
key('Enter')
|
||||
await waitFor(() => expect(c.querySelector('.history-modal')).toBeNull())
|
||||
await waitFor(() => expect(find(c, '.tb-crumb', 'composer.json')).toBeTruthy())
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user