Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andTyriar 6e39f74f2c Document Alt+- keyboard issue investigation with comprehensive tests
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-08-24 13:08:56 +00:00
copilot-swe-agent[bot]andTyriar 9a64c2cb63 Add comprehensive tests for Alt+- keyboard handling
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-08-24 13:04:42 +00:00
copilot-swe-agent[bot] dc2ac0b78b Initial plan 2025-08-24 12:47:20 +00:00
Daniel Imms e969832946 Merge pull request #5385 from anthonykim1/anthonykim1/fixWrappedTerminalFind
Fix terminal find when wrapped
2025-08-21 12:40:01 -07:00
Anthony Kim 95a9531ec1 Try to add tests 2025-08-20 22:49:03 -07:00
Daniel Imms 7637a2de19 Merge pull request #5386 from alexkozy/safe-guards
fix: ensure that currentRow can not go out of the range
2025-08-20 14:15:08 -07:00
Alexey Kozy a589be2826 fix: ensure that currentRow can not go out of the range 2025-08-20 13:47:40 -07:00
Anthony Kim b53096f4a2 Clean code 2025-08-20 10:55:47 -07:00
Anthony Kim 687d8277e7 Fix terminal find when wrapped 2025-08-20 10:53:40 -07:00
Daniel Imms 4c0cf27cfc Merge pull request #5334 from CodinGame/webgl-renderer-shadow-dom
Support shadow dom in webgl renderer
2025-07-31 08:32:44 -07:00
Loïc Mangeonjean 149527ddb8 refactor: apply requested style change 2025-07-31 17:08:30 +02:00
Daniel Imms c4b707e9ca Merge pull request #5362 from Tyriar/tyriar/steps2
Move copilot steps to correct folder
2025-06-21 11:01:00 -07:00
Daniel Imms 9d35d59fb3 Move copilot steps to correct folder 2025-06-21 11:00:33 -07:00
Daniel Imms 877b37d3cc Merge pull request #5359 from Tyriar/tyriar/copilot_steps
Add copilot setup steps
2025-06-21 10:55:11 -07:00
Daniel Imms f14f55c951 Add copilot setup steps 2025-06-21 10:54:46 -07:00
Daniel Imms 8a16a551e0 Merge pull request #5358 from Tyriar/tyriar/reflow_bounds
Revert "Merge pull request #5321 from Tyriar/5319"
2025-06-18 06:17:46 -07:00
Daniel Imms 2a5239578a Revert "Merge pull request #5321 from Tyriar/5319"
This reverts commit a260f7d288, reversing
changes made to 2042bb8502.
2025-06-18 06:09:53 -07:00
Daniel Imms d28eb696ec Merge pull request #5355 from Tyriar/tyriar/3634
Add note about reverse tabnapping
2025-06-13 08:41:38 -07:00
Daniel Imms 4c0c2559c3 Add note about reverse tabnapping
Fixes #3634
2025-06-13 08:19:31 -07:00
Daniel Imms ee0a3deb6a Merge pull request #5349 from xtermjs/copilot/fix-3382
Add comprehensive tests for alt+shift+letter keyboard combinations
2025-06-05 03:47:13 -07:00
copilot-swe-agent[bot]andTyriar 076f051bb5 Fix ESLint trailing spaces warning in Keyboard.test.ts
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-06-05 10:34:53 +00:00
copilot-swe-agent[bot]andTyriar 658c57546c Add comprehensive tests for alt+shift+letter keyboard combinations
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-06-05 10:24:34 +00:00
copilot-swe-agent[bot] 223a650e3e Initial plan for issue 2025-06-05 10:13:25 +00:00
Daniel Imms 4b6316e510 Merge pull request #5346 from xtermjs/copilot/fix-4538-2
Remove alt -> ctrl+arrow hack in favor of embedder-specific solutions
2025-05-31 06:59:41 -07:00
copilot-swe-agent[bot]andTyriar 41546230dc Remove alt -> ctrl+arrow hack and update tests
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-05-29 13:20:54 +00:00
copilot-swe-agent[bot] 8413c0f94b Initial plan for issue 2025-05-29 13:09:01 +00:00
Daniel Imms 8828b47cea Merge pull request #5345 from Tyriar/tyriar/instruct
Add instructions to help Copilot coding agent
2025-05-29 06:05:50 -07:00
Daniel Imms b0f300e61b Add instructions to help Copilot coding agent 2025-05-29 06:00:41 -07:00
Loïc Mangeonjean 3ad83dd70e chore: add webgl shadow dom test 2025-05-21 21:51:01 +02:00
Loïc Mangeonjean 496b53d84d fix: support shadow dom in webgl renderer 2025-05-20 16:31:17 +02:00
15 changed files with 377 additions and 139 deletions
+10
View File
@@ -0,0 +1,10 @@
This is a TypeScript-based repository with a Ruby client for certain API endpoints. It includes several sub projects called addons which are built separately. It contains a demo application which showcases the functionality of the project.
Please follow these guidelines when contributing:
## Development Flow
- Install dependencies: `npm install && npm run setup`
- Build and bundle demo: `npm run build && npm run esbuild`
- Run unit tests: `npm run test-unit`
- Run playwright-based integration tests: `npm run test-integration`
+36
View File
@@ -0,0 +1,36 @@
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20.x
cache: 'yarn'
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Setup and run tsc
run: yarn setup
- name: Esbuild
run: yarn esbuild
+8 -7
View File
@@ -72,6 +72,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
private _cachedSearchTerm: string | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _searchResultsWithHighlight: ISearchResult[] = [];
private _selectedDecoration: MutableDisposable<IMultiHighlight> = this._register(new MutableDisposable());
private _highlightLimit: number;
private _lastSearchOptions: ISearchOptions | undefined;
@@ -118,6 +119,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
this._selectedDecoration.clear();
dispose(this._highlightDecorations);
this._highlightDecorations = [];
this._searchResultsWithHighlight = [];
this._highlightedLines.clear();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
@@ -167,15 +169,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
// new search, clear out the old decorations
this.clearDecorations(true);
const searchResultsWithHighlight: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this._highlightLimit) {
if (this._searchResultsWithHighlight.length >= this._highlightLimit) {
break;
}
prevResult = result;
searchResultsWithHighlight.push(prevResult);
this._searchResultsWithHighlight.push(prevResult);
result = this._find(
term,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
@@ -183,7 +184,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
searchOptions
);
}
for (const match of searchResultsWithHighlight) {
for (const match of this._searchResultsWithHighlight) {
const decorations = this._createResultDecorations(match, searchOptions.decorations!, false);
if (decorations) {
for (const decoration of decorations) {
@@ -350,15 +351,15 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
let resultIndex = -1;
if (this._selectedDecoration.value) {
const selectedMatch = this._selectedDecoration.value.match;
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
for (let i = 0; i < this._searchResultsWithHighlight.length; i++) {
const match = this._searchResultsWithHighlight[i];
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
this._onDidChangeResults.fire({ resultIndex, resultCount: this._searchResultsWithHighlight.length });
}
}
@@ -480,6 +480,57 @@ test.describe('Search Tests', () => {
]);
});
});
test.describe('Wrapped line search functionality', () => {
test('should correctly count matches across multiple wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'a'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
});
test('should handle reverse search across wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'x'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
});
test('should update counts when content changes across wrapped lines', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const content = 'z'.repeat(300);
await ctx.proxy.write(content);
strictEqual(await ctx.page.evaluate(`window.search.findNext('${content}', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await ctx.proxy.write('\\n\\r' + content);
await timeout(300);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 }
]);
});
});
});
function makeData(length: number): string {
+4
View File
@@ -14,6 +14,10 @@ declare module '@xterm/addon-web-links' {
/**
* Creates a new web links addon.
* @param handler The callback when the link is called.
*
* Note that this may not work when the terminal is hosted inside an iframe,
* in that case provide a custom handler in that case being mindful of
* possible security issues like reverse tabnapping.
* @param options Options for the link provider.
*/
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkProviderOptions);
+2 -2
View File
@@ -139,7 +139,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
[this._rectangleRenderer.value, this._glyphRenderer.value] = this._initializeWebGLState();
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
this._isAttached = this._core.screenElement!.isConnected;
this._register(toDisposable(() => {
for (const l of this._renderLayers) {
@@ -322,7 +322,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
if (this._core.screenElement?.isConnected && this._charSizeService.width && this._charSizeService.height) {
this._updateDimensions();
this._refreshCharAtlas();
this._isAttached = true;
+1 -1
View File
@@ -95,7 +95,7 @@
"jsdom": "^18.0.1",
"mocha": "^10.1.0",
"mustache": "^4.2.0",
"node-pty": "^1.1.0-beta31",
"node-pty": "1.1.0-beta19",
"nyc": "^15.1.0",
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
+3 -1
View File
@@ -199,7 +199,9 @@ function bufferLine(
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
while ((currentCol !== endCol || currentRow !== endRow) &&
currentRow >= 0 &&
currentRow < bufferService.buffer.lines.length) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > bufferService.cols - 1) {
+1 -74
View File
@@ -320,37 +320,7 @@ export class Buffer implements IBuffer {
if (toRemove.length > 0) {
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
// when reflowing larger is to insert empty lines at the bottom of the buffer as when lines
// unwrap conpty's view cannot pull scrollback down, so it adds empty lines at the end.
let removedInViewport = 0;
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
if (isWindowsMode) {
for (let i = (toRemove.length / 2) - 1; i >= 0; i--) {
if (toRemove[i * 2 + 0] > this.ybase + removedInViewport) {
removedInViewport += toRemove[i * 2 + 1];
}
}
}
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
// Apply empty lines for any removed in viewport for conpty.
if (isWindowsMode) {
if (removedInViewport > 0) {
for (let i = 0; i < removedInViewport; i++) {
// Just add the new missing rows on Windows as conpty reprints the screen with it's
// view of the world. Once a line enters scrollback for conpty it remains there
this.lines.push(new BufferLine(newCols, this.getNullCell(DEFAULT_ATTR_DATA)));
}
if (this.ybase === this.ydisp) {
this.ydisp += removedInViewport;
}
this.ybase += removedInViewport;
this.y -= removedInViewport;
}
}
}
}
@@ -382,7 +352,7 @@ export class Buffer implements IBuffer {
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
// batched up and only committed once
const toInsert: { start: number, newLines: IBufferLine[] }[] = [];
const toInsert = [];
let countToInsert = 0;
// Go backwards as many lines may be trimmed and this will avoid considering them
for (let y = this.lines.length - 1; y >= 0; y--) {
@@ -497,20 +467,6 @@ export class Buffer implements IBuffer {
this.savedY = Math.min(this.savedY + linesToAdd, this.ybase + newRows - 1);
}
// For conpty, it has its own copy of the buffer _without scrollback_ internally. Its behavior
// when reflowing smaller is to reflow all lines inside the viewport, and removing empty or
// whitespace only lines from the bottom, until non-whitespace is hit in order to prevent
// content from being pushed into the scrollback.
let addedInViewport = 0;
const isWindowsMode = this._optionsService.rawOptions.windowsMode || this._optionsService.rawOptions.windowsPty.backend !== undefined || this._optionsService.rawOptions.windowsPty.buildNumber !== undefined;
if (isWindowsMode) {
for (let i = toInsert.length - 1; i >= 0; i--) {
if (toInsert[i].start > this.ybase + addedInViewport) {
addedInViewport += toInsert[i].newLines.length;
}
}
}
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
// costly calls to CircularList.splice.
@@ -564,35 +520,6 @@ export class Buffer implements IBuffer {
this.lines.onTrimEmitter.fire(amountToTrim);
}
}
// Apply empty lines to remove calculated earlier for conpty.
if (isWindowsMode) {
if (addedInViewport > 0) {
let emptyLinesAtBottom = 0;
for (let i = this.lines.length - 1; i >= this.ybase + this.y; i--) {
const line = this.lines.get(i) as BufferLine;
if (line.isWrapped || line.getTrimmedLength() > 0) {
break;
}
emptyLinesAtBottom++;
}
const emptyLinesToRemove = Math.min(addedInViewport, emptyLinesAtBottom);
if (emptyLinesToRemove > 0) {
for (let i = 0; i < emptyLinesToRemove; i++) {
this.lines.pop();
}
if (this.ybase === this.ydisp) {
this.ydisp -= emptyLinesToRemove;
}
this.ybase -= emptyLinesToRemove;
this.y += emptyLinesToRemove;
this.lines.onDeleteEmitter.fire({
index: this.lines.length - emptyLinesToRemove,
amount: emptyLinesToRemove
});
}
}
}
}
/**
+169
View File
@@ -0,0 +1,169 @@
/**
* Test for Alt+- keyboard handling specifically
* This tests both the evaluateKeyboardEvent function and potential browser integration issues
*/
import { assert } from 'chai';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { IKeyboardResult, IKeyboardEvent } from 'common/Types';
function createMockKeyboardEvent(partialEvent: Partial<IKeyboardEvent>): IKeyboardEvent {
return {
altKey: false,
ctrlKey: false,
shiftKey: false,
metaKey: false,
keyCode: 0,
code: '',
key: '',
type: 'keydown',
...partialEvent
};
}
describe('Alt+- keyboard handling', () => {
it('should return correct escape sequence for Alt+- on all platforms', () => {
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
// Test on non-Mac (Linux/Windows)
const resultLinux = evaluateKeyboardEvent(event, false, false, false);
assert.equal(resultLinux.key, '\x1b-', 'Alt+- should produce \\x1b- on Linux');
assert.equal(resultLinux.cancel, false, 'Alt+- should not be cancelled on Linux');
// Test on Windows
const resultWindows = evaluateKeyboardEvent(event, false, false, false);
assert.equal(resultWindows.key, '\x1b-', 'Alt+- should produce \\x1b- on Windows');
// Test on Mac with macOptionIsMeta: true
const resultMacMeta = evaluateKeyboardEvent(event, false, true, true);
assert.equal(resultMacMeta.key, '\x1b-', 'Alt+- should produce \\x1b- on Mac with macOptionIsMeta');
// Test on Mac with macOptionIsMeta: false (third level shift)
const resultMacNoMeta = evaluateKeyboardEvent(event, false, true, false);
assert.equal(resultMacNoMeta.key, undefined, 'Alt+- should not produce escape sequence on Mac without macOptionIsMeta');
});
it('should handle Alt+- with shift key (underscore)', () => {
const event = createMockKeyboardEvent({
altKey: true,
shiftKey: true,
keyCode: 189,
key: '_',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
assert.equal(result.key, '\x1b_', 'Alt+Shift+- should produce \\x1b_');
});
it('should verify keyCode 189 maps to minus character', () => {
// This test verifies that keyCode 189 is correctly mapped in KEYCODE_KEY_MAPPINGS
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
assert.isDefined(result.key, 'Result should have a key');
assert.equal(result.key, '\x1b-', 'Should produce the correct escape sequence');
});
it('should handle various browser scenarios for Alt+-', () => {
// Test with different keyCode values that might be used for minus
const keyCodes = [189, 109]; // 189 is standard minus, 109 is numpad minus
keyCodes.forEach(keyCode => {
const event = createMockKeyboardEvent({
altKey: true,
keyCode: keyCode,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
if (keyCode === 189) {
assert.equal(result.key, '\x1b-', `keyCode ${keyCode} should produce \\x1b-`);
} else {
// For numpad minus or other codes, the behavior might be different
// but we should still handle them gracefully
assert.isDefined(result, `keyCode ${keyCode} should return a result`);
}
});
});
it('should not be affected by composition state for regular Alt combinations', () => {
// This tests that Alt+- is not treated as a composition character
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
// Ensure the keyCode is not 229 (composition character)
assert.notEqual(event.keyCode, 229, 'Alt+- should not use composition keyCode');
const result = evaluateKeyboardEvent(event, false, false, false);
assert.equal(result.key, '\x1b-', 'Alt+- should work normally without composition interference');
});
describe('Browser integration considerations', () => {
it('should document known scenarios where Alt+- might not work', () => {
// This test documents potential causes for the reported issue
// NOTE: These are integration issues, not keyboard evaluation issues
const possibleCauses = [
'Terminal not properly focused when testing',
'Custom key event handlers preventing default behavior',
'IME or composition methods intercepting events',
'Browser-specific keyboard layout differences',
'Third-party browser extensions interfering'
];
// The keyboard evaluation should always work correctly
const event = createMockKeyboardEvent({
altKey: true,
keyCode: 189,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
assert.equal(result.key, '\x1b-', 'Keyboard evaluation should always work');
// Document that integration issues are separate concerns
assert.isTrue(possibleCauses.length > 0, 'There are known integration scenarios to consider');
});
it('should handle different keyboard layouts gracefully', () => {
// Some keyboard layouts might use different keyCodes for minus
const potentialMinusKeyCodes = [189, 173, 109]; // Various minus key codes
potentialMinusKeyCodes.forEach(keyCode => {
const event = createMockKeyboardEvent({
altKey: true,
keyCode: keyCode,
key: '-',
type: 'keydown'
});
const result = evaluateKeyboardEvent(event, false, false, false);
if (keyCode === 189) {
// Standard minus key should always work
assert.equal(result.key, '\x1b-', `Standard minus (keyCode ${keyCode}) should work`);
} else {
// Other keys might not be mapped, but should not throw errors
assert.isDefined(result, `KeyCode ${keyCode} should return a valid result`);
}
});
});
});
});
+33 -20
View File
@@ -125,17 +125,17 @@ describe('Keyboard', () => {
describe('On non-macOS platforms', () => {
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
// http://unix.stackexchange.com/a/108106
it('should return \\x1b[5D for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;5D'); // CSI 5 D
it('should return \\x1b[1;3D for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;3D'); // CSI 1;3 D
});
it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
it('should return \\x1b[1;3C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;3C'); // CSI 1;3 C
});
it('should return \\x1b[5D for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D
it('should return \\x1b[1;3A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;3A'); // CSI 1;3 A
});
it('should return \\x1b[5C for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C
it('should return \\x1b[1;3B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;3B'); // CSI 1;3 B
});
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
@@ -149,17 +149,17 @@ describe('Keyboard', () => {
});
describe('On macOS platforms', () => {
it('should return \\x1bb for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1bb'); // CSI 5 D
it('should return \\x1b[1;3D for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1b[1;3D'); // CSI 1;3 D
});
it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
it('should return \\x1b[1;3C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1b[1;3C'); // CSI 1;3 C
});
it('should return \\x1bb for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D
it('should return \\x1b[1;3A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 1;3 A
});
it('should return \\x1bf for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C
it('should return \\x1b[1;3B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 1;3 B
});
it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined);
@@ -176,11 +176,11 @@ describe('Keyboard', () => {
});
});
it('should return \\x1b[5A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
it('should return \\x1b[1;3A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;3A'); // CSI 1;3 A
});
it('should return \\x1b[5B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
it('should return \\x1b[1;3B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;3B'); // CSI 1;3 B
});
it('should return the correct escape sequence for modified F1-F12 keys', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
@@ -336,6 +336,19 @@ describe('Keyboard', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
});
// Characters using alt+shift sequences (letters)
it('should return proper sequences for alt+shift+letter combinations', () => {
// Test alt+shift combinations produce uppercase letters
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 65 }).key, '\x1bA'); // alt+shift+a
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 72 }).key, '\x1bH'); // alt+shift+h
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 90 }).key, '\x1bZ'); // alt+shift+z
// Test alt without shift produces lowercase letters
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 65 }).key, '\x1ba'); // alt+a
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 72 }).key, '\x1bh'); // alt+h
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 90 }).key, '\x1bz'); // alt+z
});
it('should return proper sequence for ctrl+@', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, shiftKey: true, keyCode: 50, key: '@' }).key, '\x00');
});
-24
View File
@@ -117,12 +117,6 @@ export function evaluateKeyboardEvent(
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') {
result.key = C0.ESC + (isMac ? 'b' : '[1;5D');
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
@@ -136,12 +130,6 @@ export function evaluateKeyboardEvent(
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') {
result.key = C0.ESC + (isMac ? 'f' : '[1;5C');
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
@@ -155,12 +143,6 @@ export function evaluateKeyboardEvent(
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (!isMac && result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
@@ -174,12 +156,6 @@ export function evaluateKeyboardEvent(
}
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (!isMac && result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
+9 -2
View File
@@ -1280,10 +1280,10 @@ enum CellColorPosition {
* treatment.
*/
export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext, setupCb: () => Promise<void> | void): void {
test.describe('standalone tests', () => {
const setupTests = ({ shadowDom }: { shadowDom: boolean }): void => {
test.beforeEach(async () => {
// Recreate terminal
await openTerminal(ctx.value);
await openTerminal(ctx.value, {}, { useShadowDom: shadowDom });
await ctx.value.page.evaluate(`
window.term.options.minimumContrastRatio = 1;
window.term.options.allowTransparency = false;
@@ -1308,6 +1308,13 @@ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestCont
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
});
});
};
test.describe('standalone tests', () => {
setupTests({ shadowDom: false });
});
test.describe('standalone tests (Shadow dom)', () => {
setupTests({ shadowDom: true });
});
}
+46 -4
View File
@@ -412,7 +412,14 @@ class TerminalCoreProxy {
}
}
export async function openTerminal(ctx: ITestContext, options: ITerminalOptions | ITerminalInitOnlyOptions = {}, testOptions: { loadUnicodeGraphemesAddon: boolean } = { loadUnicodeGraphemesAddon: true }): Promise<void> {
export async function openTerminal(
ctx: ITestContext,
options: ITerminalOptions | ITerminalInitOnlyOptions = {},
testOptions: { useShadowDom?: boolean, loadUnicodeGraphemesAddon?: boolean } = {}
): Promise<void> {
testOptions.useShadowDom ??= false;
testOptions.loadUnicodeGraphemesAddon ??= true;
await ctx.page.evaluate(`
if ('term' in window) {
try {
@@ -423,10 +430,45 @@ export async function openTerminal(ctx: ITestContext, options: ITerminalOptions
// HACK: Tests may have side effects that could cause the terminal not to be removed. This
// assertion catches this case early.
strictEqual(await ctx.page.evaluate(`document.querySelector('#terminal-container').children.length`), 0, 'there must be no terminals on the page');
await ctx.page.evaluate(`
let script = `
window.term = new window.Terminal(${JSON.stringify({ allowProposedApi: true, ...options })});
window.term.open(document.querySelector('#terminal-container'));
`);
let element = document.querySelector('#terminal-container');
// Remove shadow root if it exists
const newElement = element.cloneNode(false);
element.replaceWith(newElement);
element = newElement
`;
if (testOptions.useShadowDom) {
script += `
const shadowRoot = element.attachShadow({ mode: "open" });
// Copy parent styles to shadow DOM
const styles = Array.from(document.querySelectorAll('link[rel="stylesheet"]'));
styles.forEach((styleEl) => {
const clone = document.createElement('link');
clone.rel = 'stylesheet';
clone.href = styleEl.href;
shadowRoot.appendChild(clone);
});
// Create new element inside the shadow DOM
element = document.createElement('div');
element.style.width = '100%';
element.style.height = '100%';
shadowRoot.appendChild(element);
`;
}
script += `
window.term.open(element);
`;
await ctx.page.evaluate(script);
// HACK: This is a soft layer breaker that's temporarily included until unicode graphemes have
// more complete integration tests. See https://github.com/xtermjs/xterm.js/pull/4519#discussion_r1285234453
if (testOptions.loadUnicodeGraphemesAddon) {
+4 -4
View File
@@ -3302,10 +3302,10 @@ node-preload@^0.2.1:
dependencies:
process-on-spawn "^1.0.0"
node-pty@^1.1.0-beta31:
version "1.1.0-beta9"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta9.tgz#ed643cb3b398d031b4e31c216e8f3b0042435f1d"
integrity sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==
node-pty@1.1.0-beta19:
version "1.1.0-beta19"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.1.0-beta19.tgz#a74dc04429903c5ac49ee81a15a24590da67d4f3"
integrity sha512-/p4Zu56EYDdXjjaLWzrIlFyrBnND11LQGP0/L6GEVGURfCNkAlHc3Twg/2I4NPxghimHXgvDlwp7Z2GtvDIh8A==
dependencies:
node-addon-api "^7.1.0"