mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Separate out test, add failing//skip test for cursor placements
This commit is contained in:
@@ -675,6 +675,22 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler {
|
||||
} else {
|
||||
this._storage.addImage(bitmap);
|
||||
}
|
||||
|
||||
// TODO: Implement cursor movement per Kitty graphics protocol spec
|
||||
// Per spec: "After placing an image on the screen the cursor must be moved to the
|
||||
// right by the number of cols in the image placement rectangle and down by the
|
||||
// number of rows in the image placement rectangle."
|
||||
//
|
||||
// Default behavior (C=0 or unspecified): Move cursor by cols/rows
|
||||
// With C=1: Don't move cursor at all
|
||||
//
|
||||
// Implementation would need:
|
||||
// 1. Get placement.C value (cursor movement policy)
|
||||
// 2. Calculate cols = placement.columns || Math.ceil(w / cellWidth)
|
||||
// 3. Calculate rows = placement.rows || Math.ceil(h / cellHeight)
|
||||
// 4. If C !== 1: Move cursor right by cols and down by rows
|
||||
// this._bufferService.buffer.x += cols;
|
||||
// this._bufferService.buffer.y += rows;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,11 +75,6 @@ const TESTDATA_IIP: [string, [number, number]][] = [
|
||||
[readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
|
||||
];
|
||||
|
||||
// Kitty graphics test images
|
||||
const KITTY_BLACK_1X1_BASE64 = readFileSync('./addons/addon-image/fixture/kitty/black-1x1.png').toString('base64');
|
||||
const KITTY_BLACK_1X1_BYTES = Array.from(readFileSync('./addons/addon-image/fixture/kitty/black-1x1.png'));
|
||||
const KITTY_RGB_3X1_BASE64 = readFileSync('./addons/addon-image/fixture/kitty/rgb-3x1.png').toString('base64');
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
@@ -305,310 +300,6 @@ test.describe('ImageAddon', () => {
|
||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Kitty graphics support', () => {
|
||||
test('stores 1x1 black PNG with a=T (transmit and display)', async () => {
|
||||
const seq = `\x1b_Ga=T,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
deepStrictEqual(await getOrigSize(1), [1, 1]);
|
||||
});
|
||||
|
||||
test('stores 3x1 RGB PNG with a=T', async () => {
|
||||
const seq = `\x1b_Ga=T,f=100;${KITTY_RGB_3X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
||||
});
|
||||
|
||||
test('transmit only (a=t) does not display but stores in handler', async () => {
|
||||
const seq = `\x1b_Ga=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
});
|
||||
|
||||
test('uses specified image ID', async () => {
|
||||
const seq = `\x1b_Ga=t,f=100,i=42;${KITTY_BLACK_1X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(42)`), true);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(1)`), false);
|
||||
});
|
||||
|
||||
test('assigns auto-incrementing IDs when not specified', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100;${KITTY_RGB_3X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 2);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(1)`), true);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(2)`), true);
|
||||
});
|
||||
|
||||
test('defaults to transmit action when action is omitted', async () => {
|
||||
const seq = `\x1b_Gf=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
});
|
||||
|
||||
test('ignores command when action is empty string', async () => {
|
||||
const seq = `\x1b_Ga=,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
|
||||
});
|
||||
|
||||
test('handles chunked transmission (m=1)', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
const seq1 = `\x1b_Ga=T,f=100,i=99,m=1;${part1}\x1b\\`;
|
||||
const seq2 = `\x1b_Ga=T,f=100,i=99;${part2}\x1b\\`;
|
||||
|
||||
await ctx.proxy.write(seq1);
|
||||
await timeout(50);
|
||||
strictEqual(await getImageStorageLength(), 0);
|
||||
|
||||
await ctx.proxy.write(seq2);
|
||||
await timeout(100);
|
||||
strictEqual(await getImageStorageLength(), 1);
|
||||
});
|
||||
|
||||
test('verifies chunked data is assembled correctly', async () => {
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=99,m=1;${part1}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=99;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const storedData = await ctx.page.evaluate(`Array.from(window.imageAddon._handlers.get('kitty').images.get(99).data)`);
|
||||
deepStrictEqual(storedData, KITTY_BLACK_1X1_BYTES);
|
||||
});
|
||||
|
||||
test('enforces size limit across chunked transmissions', async () => {
|
||||
// Create a custom addon with very small size limit (100 bytes)
|
||||
// The 1x1 PNG is ~164 bytes base64, so 2 chunks should exceed 100
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).smallLimitAddon = new ImageAddon({
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 100 // Very small limit
|
||||
});
|
||||
(window as any).term.loadAddon((window as any).smallLimitAddon);
|
||||
});
|
||||
|
||||
// Split the base64 data into two chunks
|
||||
const half = Math.floor(KITTY_BLACK_1X1_BASE64.length / 2);
|
||||
const part1 = KITTY_BLACK_1X1_BASE64.substring(0, half);
|
||||
const part2 = KITTY_BLACK_1X1_BASE64.substring(half);
|
||||
|
||||
// Send chunked data - first chunk (~82 bytes) is under limit
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=777,m=1;${part1}\x1b\\`);
|
||||
await timeout(50);
|
||||
|
||||
// Second chunk brings total to ~164 bytes, exceeding 100 byte limit
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=777;${part2}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
// Image should NOT be stored due to size limit
|
||||
strictEqual(await ctx.page.evaluate(`window.smallLimitAddon._handlers.get('kitty').images.has(777)`), false);
|
||||
|
||||
// Cleanup
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).smallLimitAddon.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('delete command (a=d) removes specific image by id', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=10;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d,i=10\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
|
||||
});
|
||||
|
||||
test('delete command (a=d) removes all images when no id specified', async () => {
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=1;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=2;${KITTY_RGB_3X1_BASE64}\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 2);
|
||||
|
||||
await ctx.proxy.write(`\x1b_Ga=d\x1b\\`);
|
||||
await timeout(50);
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Kitty query support (a=q)', () => {
|
||||
test('responds with OK for capability query without payload', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=31,a=q;\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=31;OK\x1b\\');
|
||||
});
|
||||
|
||||
test('responds with OK for valid PNG query', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=42,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=42;OK\x1b\\');
|
||||
});
|
||||
|
||||
test('query does NOT store the image (unlike transmit)', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).term.onData(() => { /* consume response */ });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=50,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(50)`), false);
|
||||
});
|
||||
|
||||
test('responds with error for invalid base64', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=60,a=q,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=60;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('responds with error for RGB data without dimensions', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=70,a=q,f=24;AAAA\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=70;EINVAL:width and height required for raw pixel data\x1b\\');
|
||||
});
|
||||
|
||||
test('suppresses OK response when q=1', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write(`\x1b_Gi=80,a=q,q=1,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('suppresses error response when q=2', async () => {
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyGotResponse = false;
|
||||
(window as any).term.onData(() => { (window as any).kittyGotResponse = true; });
|
||||
});
|
||||
|
||||
await ctx.proxy.write('\x1b_Gi=90,a=q,q=2,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
strictEqual(await ctx.page.evaluate('window.kittyGotResponse'), false);
|
||||
});
|
||||
|
||||
test('responds with EINVAL when both i and I keys are specified', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
// Per spec: "Specifying both i and I keys in any command is an error"
|
||||
await ctx.proxy.write(`\x1b_Gi=100,I=200,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=100;EINVAL:cannot specify both i and I keys\x1b\\');
|
||||
});
|
||||
|
||||
test('responds with EINVAL for i+I conflict even without payload', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
});
|
||||
|
||||
// Delete command with both i and I (no payload case)
|
||||
await ctx.proxy.write('\x1b_Gi=101,I=201,a=d\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=101;EINVAL:cannot specify both i and I keys\x1b\\');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Kitty pixel verification', () => {
|
||||
test('renders 1x1 black PNG at cursor position', async () => {
|
||||
const seq = `\x1b_Ga=T,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
|
||||
const pixel = await ctx.page.evaluate(() => {
|
||||
const canvas = (window as any).imageAddon.getImageAtBufferCell(0, 0);
|
||||
if (!canvas) return null;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return null;
|
||||
return Array.from(ctx.getImageData(0, 0, 1, 1).data);
|
||||
});
|
||||
|
||||
deepStrictEqual(pixel, [0, 0, 0, 255]);
|
||||
});
|
||||
|
||||
test('renders 3x1 RGB PNG (red, green, blue pixels)', async () => {
|
||||
const seq = `\x1b_Ga=T,f=100;${KITTY_RGB_3X1_BASE64}\x1b\\`;
|
||||
await ctx.proxy.write(seq);
|
||||
await timeout(100);
|
||||
|
||||
const pixels = await ctx.page.evaluate(() => {
|
||||
const canvas = (window as any).imageAddon.getImageAtBufferCell(0, 0);
|
||||
if (!canvas) return null;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return null;
|
||||
const imageData = ctx.getImageData(0, 0, 3, 1).data;
|
||||
return {
|
||||
red: Array.from(imageData.slice(0, 4)),
|
||||
green: Array.from(imageData.slice(4, 8)),
|
||||
blue: Array.from(imageData.slice(8, 12))
|
||||
};
|
||||
});
|
||||
|
||||
deepStrictEqual(pixels?.red, [255, 0, 0, 255]);
|
||||
deepStrictEqual(pixels?.green, [0, 255, 0, 255]);
|
||||
deepStrictEqual(pixels?.blue, [0, 0, 255, 255]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user