diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 39d666de..6abd5fb9 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -532,6 +532,9 @@ export class SelectionService extends Disposable implements ISelectionService { * @param event The mouse event. */ private _handleSingleClick(event: MouseEvent): void { + // Track if there was a selection before clearing + const hadSelection = this.hasSelection; + this._model.selectionStartLength = 0; this._model.isSelectAllActive = false; this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL; @@ -543,6 +546,11 @@ export class SelectionService extends Disposable implements ISelectionService { } this._model.selectionEnd = undefined; + // Fire selection change event if a selection was cleared + if (hadSelection) { + this._fireOnSelectionChange(this._model.finalSelectionStart, this._model.finalSelectionEnd, false); + } + // Ensure the line exists const line = this._bufferService.buffer.lines.get(this._model.selectionStart[1]); if (!line) { diff --git a/test/playwright/Terminal.test.ts b/test/playwright/Terminal.test.ts index acd89f73..6dee11da 100644 --- a/test/playwright/Terminal.test.ts +++ b/test/playwright/Terminal.test.ts @@ -361,17 +361,72 @@ test.describe('API Integration Tests', () => { await pollFor(ctx.page, `window.calls`, [1, 2]); }); - test('onSelectionChange', async () => { - await openTerminal(ctx); - await ctx.page.evaluate(` - window.callCount = 0; - window.term.onSelectionChange(() => window.callCount++); - `); - await pollFor(ctx.page, `window.callCount`, 0); - await ctx.page.evaluate(`window.term.selectAll()`); - await pollFor(ctx.page, `window.callCount`, 1); - await ctx.page.evaluate(`window.term.clearSelection()`); - await pollFor(ctx.page, `window.callCount`, 2); + test.describe('onSelectionChange', () => { + let callCount: number; + + test.beforeEach(async () => { + await openTerminal(ctx); + callCount = 0; + ctx.proxy.onSelectionChange(() => callCount++); + }); + + test('should fire for programmatic selection changes', async () => { + strictEqual(callCount, 0); + await ctx.proxy.selectAll(); + strictEqual(callCount, 1); + await ctx.proxy.clearSelection(); + strictEqual(callCount, 2); + }); + + test('should fire on mousedown when clearing selection', async () => { + await ctx.proxy.write('foo bar baz'); + await ctx.proxy.selectAll(); + strictEqual(callCount, 1); + + const dims = (await ctx.proxy.dimensions)!; + const termRect: any = await ctx.page.evaluate(`window.term.element.getBoundingClientRect()`); + const x = termRect.left + dims.css.cell.width * 5; + const y = termRect.top + dims.css.cell.height * 0.5; + await ctx.page.mouse.click(x, y); + + await pollFor(ctx.page, () => callCount, 2); + }); + + test('should not fire on mousedown when no prior selection', async () => { + await ctx.proxy.write('foo bar baz'); + strictEqual(callCount, 0); + + const dims = (await ctx.proxy.dimensions)!; + const termRect: any = await ctx.page.evaluate(`window.term.element.getBoundingClientRect()`); + const x = termRect.left + dims.css.cell.width * 5; + const y = termRect.top + dims.css.cell.height * 0.5; + await ctx.page.mouse.click(x, y); + await timeout(20); + + strictEqual(callCount, 0); + }); + + test('should fire once on mousedown to clear, and again on mouseup after drag', async () => { + await ctx.proxy.write('foo bar baz'); + await ctx.proxy.selectAll(); + strictEqual(callCount, 1); + + const dims = (await ctx.proxy.dimensions)!; + const termRect: any = await ctx.page.evaluate(`window.term.element.getBoundingClientRect()`); + const startX = termRect.left + dims.css.cell.width * 0.5; + const endX = termRect.left + dims.css.cell.width * 5; + const y = termRect.top + dims.css.cell.height * 0.5; + + await ctx.page.mouse.move(startX, y); + await ctx.page.mouse.down(); + await pollFor(ctx.page, () => callCount, 2); + + await ctx.page.mouse.move(endX, y); + strictEqual(callCount, 2); + + await ctx.page.mouse.up(); + await pollFor(ctx.page, () => callCount, 3); + }); }); test('onRender', async () => {