Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ A personal portfolio built as an **in-browser desktop OS** — tiling window man
| **Stack** | TypeScript · Vite 8 · vanilla DOM (no framework) |
| **Terminal** | [@xterm/xterm](https://github.com/xtermjs/xterm.js) + vim input layer |
| **3D** | Three.js (Rubik cube — lazy chunk only) |
| **Tests** | **563** unit · **3** e2e smoke · CI on every `main` push |
| **Tests** | **583** unit · **3** e2e smoke · CI on every `main` push |
| **Deploy** | GitHub Actions → GitHub Pages (`dist/`) |

---
Expand All @@ -33,7 +33,7 @@ Most portfolios list skills. This one **runs** them: modular TypeScript, lazy co
```bash
npm install
npm run dev # desktop → http://localhost:5173/
npm test # 563 unit tests
npm test # 583 unit tests
npm run build && npm run test:e2e
```

Expand Down
21 changes: 15 additions & 6 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,22 @@ class VimInput {
Pure functions in `editor-vim-ops.ts` — safe to unit test without DOM:

```typescript
lineCountTotal(text: string): number
getLineCol(text: string, pos: number): { line: number; col: number }
moveVertPos(text: string, pos: number, delta: -1 | 1): number
moveHorizPos(text: string, pos: number, delta: -1 | 1, steps?: number): number
// Motions
moveVertPos / moveVertRepeat / moveHorizPos
wordForwardPos / wordBackPos / wordEndForwardPos
findNextOnLine(text, kind, ch, fromPos): number | null
deleteLineBlockText / yankLineBlockText / joinLinesText / pasteYankText
wordForwardRepeat / wordBackRepeat / wordEndForwardRepeat
findNextOnLine / repeatFindPos / reverseFindKind
lineEndCaretPos / appendLineEndPos / firstNonBlankOnLine

// Edits
indentLinesText / unindentLinesText // >> / <<
toggleCaseRunText // ~
substituteCharsText // s
deleteCharForwardText / deleteCharBackwardText // x / X
deleteLineBlockText / yankLineBlockText / yankToEOLText
deleteThroughEOLText // D / C
joinLinesText / openLineBelowText / openLineAboveText
applyReplaceRunsText / pasteYankText
// ...
```

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ Bump VFS key version in `os-fs.ts` to reset visitor filesystems.

## Testing

**563 tests** · **46 files** · Vitest in Node · Playwright e2e smoke.
**583 tests** · **46 files** · Vitest in Node · Playwright e2e smoke.

### By domain

Expand Down
2 changes: 1 addition & 1 deletion docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Run before opening a PR:

```bash
npm run lint # ESLint
npm test # Vitest — 563 unit tests
npm test # Vitest — 583 unit tests
npm run build # TypeScript + Vite
npm run test:e2e # Playwright (requires preview build)
```
Expand Down
2 changes: 1 addition & 1 deletion docs/OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Files persist in `localStorage` under `portfolio-vfs-v8-namefailed-home`. First

| Stage | Tool |
|-------|------|
| Unit | Vitest — **563 tests**, **46 files**, Node environment with DOM stubs where needed |
| Unit | Vitest — **583 tests**, **46 files**, Node environment with DOM stubs where needed |
| Lint | ESLint 9 + TypeScript-eslint |
| E2e | Playwright — production build smoke (desktop shell, brochure, nav) |
| Deploy | GitHub Actions → `dist/` → GitHub Pages |
Expand Down
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Personal portfolio site ([mrgrey.site](https://mrgrey.site)) built as an in-brow

| Metric | Value |
|--------|-------|
| Unit tests | **563** across **46** files (`npm test`) |
| Unit tests | **583** across **46** files (`npm test`) |
| E2e smoke | **3** Playwright specs (`npm run test:e2e`) |
| CI | lint → unit tests → build → e2e → GitHub Pages deploy |
| Stack | TypeScript, Vite 8, vanilla DOM — no React/Vue/Svelte |
Expand Down
2 changes: 1 addition & 1 deletion docs/STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ el.innerHTML = escapeHtml(userInput)
## Tooling

- `npm run lint` — ESLint (TypeScript, e2e specs, maintainer scripts)
- `npm test` — **563** Vitest unit tests (46 files)
- `npm test` — **583** Vitest unit tests (46 files)
- `npm run test:coverage` — Vitest v8 coverage (`coverage/` gitignored)
- `npm run test:e2e` — Playwright smoke against `vite preview`
- Desktop CSS: `src/styles/*.css` via `src/style.css` hub; regenerate with `node scripts/split-style-css.mjs`
Expand Down
140 changes: 132 additions & 8 deletions src/editor-vim-ops.test.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import { describe, it, expect } from 'vitest'
import {
applyReplaceRunsText,
appendLineEndPos,
consumeCountDigits,
deleteCharBackwardText,
deleteCharForwardText,
deleteLineBlockText,
findNextOnLine,
getLineCol,
gotoLinePos,
indentLinesText,
joinLinesText,
lineBounds,
lineCountTotal,
lineEndCaretPos,
moveHorizPos,
moveVertPos,
moveVertRepeat,
openLineAboveText,
openLineBelowText,
pasteYankText,
repeatFindPos,
reverseFindKind,
substituteCharsText,
toggleCaseRunText,
unindentLinesText,
wordBackPos,
wordBackRepeat,
wordEndForwardPos,
wordEndForwardRepeat,
wordForwardPos,
wordForwardRepeat,
yankLineBlockText,
yankToEOLText,
} from './editor-vim-ops'
import { insertModeKeyAction, tryAppendCountDigit } from './editor-vim-keys'
import {
consumeCountDigits,
getLineCol,
gotoLinePos,
lineBounds,
lineCountTotal,
moveVertPos,
} from './editor-vim-ops'

describe('lineCountTotal', () => {
it('returns 1 for empty buffer', () => {
Expand Down Expand Up @@ -158,6 +171,117 @@ describe('pasteYankText', () => {
})
})

describe('indentLinesText', () => {
it('indents from caret line and shifts caret right', () => {
const result = indentLinesText(' foo\nbar', 2, 1)
expect(result).toEqual({ text: ' foo\nbar', pos: 4 })
})

it('indents multiple lines from caret line', () => {
const result = indentLinesText('a\nb\nc', 2, 2)
expect(result?.text).toBe('a\n b\n c')
})
})

describe('unindentLinesText', () => {
it('removes two-space indent and shifts caret left', () => {
const result = unindentLinesText(' hello', 4, 1)
expect(result).toEqual({ text: 'hello', pos: 2 })
})

it('removes single space or tab', () => {
expect(unindentLinesText(' hello', 1, 1)?.text).toBe('hello')
expect(unindentLinesText('\tworld', 1, 1)?.text).toBe('world')
})
})

describe('toggleCaseRunText', () => {
it('toggles case on next n characters', () => {
expect(toggleCaseRunText('hello', 0, 2)).toEqual({ text: 'HEllo', pos: 2 })
})

it('skips newlines without counting toward n', () => {
expect(toggleCaseRunText('a\nb', 0, 2)).toEqual({ text: 'A\nB', pos: 3 })
})

it('returns null when nothing toggled', () => {
expect(toggleCaseRunText('123', 0, 1)).toBeNull()
})
})

describe('substituteCharsText', () => {
it('deletes n chars under cursor', () => {
expect(substituteCharsText('hello', 1, 2)).toEqual({ text: 'hlo', pos: 1 })
})
})

describe('deleteCharForwardText', () => {
it('deletes forward (x)', () => {
expect(deleteCharForwardText('abcd', 1, 2)).toEqual({ text: 'ad', pos: 1 })
})
})

describe('deleteCharBackwardText', () => {
it('deletes backward (X)', () => {
expect(deleteCharBackwardText('abcd', 2, 1)).toEqual({ text: 'acd', pos: 1 })
})
})

describe('yankToEOLText', () => {
it('yanks through line end without newline', () => {
expect(yankToEOLText('hello\nworld', 1)).toBe('ello')
})
})

describe('appendLineEndPos', () => {
it('returns index of last char on line', () => {
expect(appendLineEndPos('hello\nworld', 0)).toBe(5)
expect(appendLineEndPos('hello', 0)).toBe(5)
})
})

describe('openLineBelowText / openLineAboveText', () => {
it('opens line below (o)', () => {
expect(openLineBelowText('ab\ncd', 1)).toEqual({ text: 'ab\n\ncd', pos: 3 })
})

it('opens line above (O)', () => {
expect(openLineAboveText('ab\ncd', 3)).toEqual({ text: 'ab\n\ncd', pos: 3 })
})
})

describe('repeatFindPos', () => {
it('repeats f motion', () => {
expect(repeatFindPos('ab ab ab', 2, 'f', 'a', 0)).toBe(6)
})

it('returns null when motion fails', () => {
expect(repeatFindPos('xyz', 1, 'f', 'q', 0)).toBeNull()
})
})

describe('moveVertRepeat', () => {
it('steps j/k multiple times', () => {
expect(moveVertRepeat('a\nb\nc', 0, 1, 2)).toBe(4)
})
})

describe('word repeats', () => {
const text = 'foo bar baz'

it('wordForwardRepeat', () => {
expect(wordForwardRepeat(text, 0, 2)).toBe(7)
})

it('wordBackRepeat', () => {
expect(wordBackRepeat(text, 11, 2)).toBe(4)
})

it('wordEndForwardRepeat', () => {
expect(wordEndForwardRepeat(text, 4, 2)).toBe(6)
})
})

describe('vim editing scenarios', () => {
it('yank-delete-paste round trip restores buffer', () => {
const original = 'line1\nline2\nline3'
Expand Down
Loading
Loading