From 54e66f3ac4c355b2b12b84f814125cef6eb684c5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Dec 2025 06:48:28 -0800 Subject: [PATCH 1/2] Add some more CSI tests --- .github/copilot-instructions.md | 1 + test/playwright/InputHandler.test.ts | 92 ++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 16088112..e276faea 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,6 +24,7 @@ npm run build && npm run esbuild # Build all TypeScript and bundle - Unit tests filtering to file: `npm run test-unit -- **/fileName.ts - Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js` - Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit) +- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts` - Per-addon integration tests: `npm run test-integration --suite=addon-search` ## Addon Development Pattern diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index b0cd1a08..ade318e2 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -150,11 +150,41 @@ test.describe('InputHandler Integration Tests', () => { await pollFor(ctx.page, () => ctx.proxy.buffer.active.length, 5); await pollFor(ctx.page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']); }); - test.skip('CSI Ps K - EL: Erase in Line, VT100', async () => { - // TODO: Implement + test('CSI Ps K - EL: Erase in Line, VT100', async () => { + const fixture = 'abcde\x1b[1;3H'; + // Default: Erase to Right + await ctx.proxy.write(fixture + '\x1b[K'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['ab']); + // 0: Erase to Right + await ctx.proxy.reset(); + await ctx.proxy.write(fixture + '\x1b[0K'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['ab']); + // 1: Erase to Left + await ctx.proxy.reset(); + await ctx.proxy.write(fixture + '\x1b[1K'); + await pollFor(ctx.page, () => getLinesAsArray(1), [' de']); + // 2: Erase All + await ctx.proxy.reset(); + await ctx.proxy.write(fixture + '\x1b[2K'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['']); }); - test.skip('CSI ? Ps K - DECSEL: Erase in Line, VT220', async () => { - // TODO: Implement + test('CSI ? Ps K - DECSEL: Erase in Line, VT220', async () => { + const fixture = 'abcde\x1b[1;3H'; + // Default: Erase to Right + await ctx.proxy.write(fixture + '\x1b[?K'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['ab']); + // 0: Erase to Right + await ctx.proxy.reset(); + await ctx.proxy.write(fixture + '\x1b[?0K'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['ab']); + // 1: Erase to Left + await ctx.proxy.reset(); + await ctx.proxy.write(fixture + '\x1b[?1K'); + await pollFor(ctx.page, () => getLinesAsArray(1), [' de']); + // 2: Erase All + await ctx.proxy.reset(); + await ctx.proxy.write(fixture + '\x1b[?2K'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['']); }); test('CSI Ps L - IL: Insert Ps Line(s) (default = 1)', async () => { // Default @@ -186,14 +216,28 @@ test.describe('InputHandler Integration Tests', () => { test.skip('CSI # R - XTREPORTCOLORS: Report the current entry on the palette stack, and the number of palettes stored on the stack, using the same form as XTPOPCOLOR (default = 0), xterm', async () => { // TODO: Implement }); - test.skip('CSI Ps S - SU: Scroll up Ps lines (default = 1), VT420, ECMA-48', async () => { - // TODO: Implement + test('CSI Ps S - SU: Scroll up Ps lines (default = 1), VT420, ECMA-48', async () => { + await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['1', '2', '3', '4', '5']); + await ctx.proxy.write('\x1b[S'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['2', '3', '4', '5', '']); + await ctx.proxy.reset(); + await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); + await ctx.proxy.write('\x1b[2S'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['3', '4', '5', '', '']); }); test.skip('CSI ? Pi ; Pa ; Pv S - XTSMGRAPHICS: Set or request graphics attribute, xterm', async () => { // TODO: Implement }); - test.skip('CSI Ps T - SD: Scroll down Ps lines (default = 1), VT420', async () => { - // TODO: Implement + test('CSI Ps T - SD: Scroll down Ps lines (default = 1), VT420', async () => { + await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['1', '2', '3', '4', '5']); + await ctx.proxy.write('\x1b[T'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['', '1', '2', '3', '4']); + await ctx.proxy.reset(); + await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); + await ctx.proxy.write('\x1b[2T'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['', '', '1', '2', '3']); }); test.skip('CSI Ps ; Ps ; Ps ; Ps ; Ps T - XTHIMOUSE: Initiate highlight mouse tracking (XTHIMOUSE), xterm', async () => { // TODO: Implement @@ -202,16 +246,38 @@ test.describe('InputHandler Integration Tests', () => { // TODO: Implement }); test.skip('CSI Ps X - ECH: Erase Ps Character(s) (default = 1)', async () => { - // TODO: Implement + // TODO: Test needs investigation - erase character behavior differs from expectation + await ctx.proxy.write('abcdef\x1b[1;1H\x1b[X'); + await pollFor(ctx.page, () => getLinesAsArray(1), [' bcdef']); + await ctx.proxy.reset(); + await ctx.proxy.write('abcdef\x1b[1;1H\x1b[3X'); + await pollFor(ctx.page, () => getLinesAsArray(1), [' def']); }); + // TODO: CSI Z (CBT) appears to move cursor to column 0 instead of previous tab stop test.skip('CSI Ps Z - CBT: Cursor Backward Tabulation Ps tab stops (default = 1)', async () => { - // TODO: Implement + // Write 'a' at col 17, go back 1 tab stop to col 9, write 'b' + await ctx.proxy.write('\x1b[17Ga\x1b[17G\x1b[Zb'); + await pollFor(ctx.page, () => getLinesAsArray(1), [' b a']); }); test.skip('CSI Ps ^ - SD: Scroll down Ps lines (default = 1) (SD), ECMA-48', async () => { - // TODO: Implement + // CSI ^ for scroll down is not implemented - xterm.js treats ^ (0x5e) as PM string start + await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['1', '2', '3', '4', '5']); + await ctx.proxy.write('\x1b[^'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['', '1', '2', '3', '4']); + await ctx.proxy.reset(); + await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); + await ctx.proxy.write('\x1b[2^'); + await pollFor(ctx.page, () => getLinesAsArray(5), ['', '', '1', '2', '3']); }); - test.skip('CSI Ps ` - HPA: Character Position Absolute [column] (default = [row,1])', async () => { - // TODO: Implement + test('CSI Ps ` - HPA: Character Position Absolute [column] (default = [row,1])', async () => { + // Ensure terminal is wide enough (other tests may resize) + await ctx.proxy.resize(80, 24); + // Default + await ctx.proxy.write('foo\x1b[`a'); + // Explicit + await ctx.proxy.write('\x1b[10`b'); + await pollFor(ctx.page, () => getLinesAsArray(1), ['aoo b']); }); test.skip('CSI Ps a - ', async () => { // TODO: Implement From da4243cf02b42a11305c7fc930a791740c5b0c79 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Dec 2025 06:59:27 -0800 Subject: [PATCH 2/2] Don't skip new tests, implement CSI Ps ^ --- .github/copilot-instructions.md | 4 ++-- src/common/InputHandler.ts | 1 + test/playwright/InputHandler.test.ts | 13 ++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index e276faea..97325cb8 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -24,8 +24,8 @@ npm run build && npm run esbuild # Build all TypeScript and bundle - Unit tests filtering to file: `npm run test-unit -- **/fileName.ts - Per-addon unit tests: `npm run test-unit addons/addon-image/out-esbuild/*.test.js` - Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit) -- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts` -- Per-addon integration tests: `npm run test-integration --suite=addon-search` +- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work +- Integration tests by addon: `npm run test-integration --suite=addon-search`. Suites always follow the format `addon-` ## Addon Development Pattern diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 10e69bbb..1dd1640f 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -240,6 +240,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.registerCsiHandler({ final: 'T' }, params => this.scrollDown(params)); this._parser.registerCsiHandler({ final: 'X' }, params => this.eraseChars(params)); this._parser.registerCsiHandler({ final: 'Z' }, params => this.cursorBackwardTab(params)); + this._parser.registerCsiHandler({ final: '^' }, params => this.scrollDown(params)); this._parser.registerCsiHandler({ final: '`' }, params => this.charPosAbsolute(params)); this._parser.registerCsiHandler({ final: 'a' }, params => this.hPositionRelative(params)); this._parser.registerCsiHandler({ final: 'b' }, params => this.repeatPrecedingCharacter(params)); diff --git a/test/playwright/InputHandler.test.ts b/test/playwright/InputHandler.test.ts index ade318e2..c209379f 100644 --- a/test/playwright/InputHandler.test.ts +++ b/test/playwright/InputHandler.test.ts @@ -26,6 +26,7 @@ test.describe('InputHandler Integration Tests', () => { }); test.beforeEach(async () => { recordedData.length = 0; + await ctx.proxy.resize(80, 24); }); test.describe('CSI', () => { @@ -245,22 +246,18 @@ test.describe('InputHandler Integration Tests', () => { test.skip('CSI > Pm T - XTRMTITLE: Reset title mode features to default value, xterm', async () => { // TODO: Implement }); - test.skip('CSI Ps X - ECH: Erase Ps Character(s) (default = 1)', async () => { - // TODO: Test needs investigation - erase character behavior differs from expectation + test('CSI Ps X - ECH: Erase Ps Character(s) (default = 1)', async () => { await ctx.proxy.write('abcdef\x1b[1;1H\x1b[X'); await pollFor(ctx.page, () => getLinesAsArray(1), [' bcdef']); await ctx.proxy.reset(); await ctx.proxy.write('abcdef\x1b[1;1H\x1b[3X'); await pollFor(ctx.page, () => getLinesAsArray(1), [' def']); }); - // TODO: CSI Z (CBT) appears to move cursor to column 0 instead of previous tab stop - test.skip('CSI Ps Z - CBT: Cursor Backward Tabulation Ps tab stops (default = 1)', async () => { - // Write 'a' at col 17, go back 1 tab stop to col 9, write 'b' + test('CSI Ps Z - CBT: Cursor Backward Tabulation Ps tab stops (default = 1)', async () => { await ctx.proxy.write('\x1b[17Ga\x1b[17G\x1b[Zb'); await pollFor(ctx.page, () => getLinesAsArray(1), [' b a']); }); - test.skip('CSI Ps ^ - SD: Scroll down Ps lines (default = 1) (SD), ECMA-48', async () => { - // CSI ^ for scroll down is not implemented - xterm.js treats ^ (0x5e) as PM string start + test('CSI Ps ^ - SD: Scroll down Ps lines (default = 1) (SD), ECMA-48', async () => { await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5'); await pollFor(ctx.page, () => getLinesAsArray(5), ['1', '2', '3', '4', '5']); await ctx.proxy.write('\x1b[^'); @@ -271,8 +268,6 @@ test.describe('InputHandler Integration Tests', () => { await pollFor(ctx.page, () => getLinesAsArray(5), ['', '', '1', '2', '3']); }); test('CSI Ps ` - HPA: Character Position Absolute [column] (default = [row,1])', async () => { - // Ensure terminal is wide enough (other tests may resize) - await ctx.proxy.resize(80, 24); // Default await ctx.proxy.write('foo\x1b[`a'); // Explicit