Skip to content

Commit 2c4997f

Browse files
committed
Add test for 200x100 pixel
1 parent 16ea2ec commit 2c4997f

2 files changed

Lines changed: 221 additions & 0 deletions

File tree

449 Bytes
Loading

addons/addon-image/test/KittyGraphics.test.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ interface IDimensions {
4242
const KITTY_BLACK_1X1_BASE64 = readFileSync('./addons/addon-image/fixture/kitty/black-1x1.png').toString('base64');
4343
const KITTY_BLACK_1X1_BYTES = Array.from(readFileSync('./addons/addon-image/fixture/kitty/black-1x1.png'));
4444
const KITTY_RGB_3X1_BASE64 = readFileSync('./addons/addon-image/fixture/kitty/rgb-3x1.png').toString('base64');
45+
const KITTY_MULTICOLOR_200X100_BASE64 = readFileSync('./addons/addon-image/fixture/kitty/multicolor-200x100.png').toString('base64');
46+
const KITTY_MULTICOLOR_200X100_BYTES = Array.from(readFileSync('./addons/addon-image/fixture/kitty/multicolor-200x100.png'));
4547

4648
// Raw RGB pixel data (f=24): 3 bytes per pixel, no header — requires s= and v=
4749
const RAW_RGB_1X1_BLACK = Buffer.from([0, 0, 0]).toString('base64');
@@ -566,6 +568,225 @@ test.describe('Kitty Graphics Protocol', () => {
566568
});
567569
});
568570

571+
test.describe('Larger image (200x100 multicolor PNG)', () => {
572+
test.describe('Basic transmission and storage', () => {
573+
test('stores 200x100 PNG with a=T', async () => {
574+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
575+
await timeout(200);
576+
strictEqual(await getImageStorageLength(), 1);
577+
deepStrictEqual(await getOrigSize(1), [200, 100]);
578+
});
579+
580+
test('transmit only (a=t) stores 200x100 image without display', async () => {
581+
await ctx.proxy.write(`\x1b_Ga=t,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
582+
await timeout(200);
583+
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.size`), 1);
584+
});
585+
586+
test('stores with specified image ID', async () => {
587+
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=400;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
588+
await timeout(200);
589+
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(400)`), true);
590+
});
591+
});
592+
593+
test.describe('Chunked transmission', () => {
594+
test('handles 2-chunk transmission', async () => {
595+
const half = Math.floor(KITTY_MULTICOLOR_200X100_BASE64.length / 2);
596+
const part1 = KITTY_MULTICOLOR_200X100_BASE64.substring(0, half);
597+
const part2 = KITTY_MULTICOLOR_200X100_BASE64.substring(half);
598+
599+
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500,m=1;${part1}\x1b\\`);
600+
await timeout(50);
601+
strictEqual(await getImageStorageLength(), 0);
602+
603+
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`);
604+
await timeout(200);
605+
strictEqual(await getImageStorageLength(), 1);
606+
deepStrictEqual(await getOrigSize(1), [200, 100]);
607+
});
608+
609+
test('handles 3-chunk transmission', async () => {
610+
const third = Math.floor(KITTY_MULTICOLOR_200X100_BASE64.length / 3);
611+
const p1 = KITTY_MULTICOLOR_200X100_BASE64.substring(0, third);
612+
const p2 = KITTY_MULTICOLOR_200X100_BASE64.substring(third, third * 2);
613+
const p3 = KITTY_MULTICOLOR_200X100_BASE64.substring(third * 2);
614+
615+
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501,m=1;${p1}\x1b\\`);
616+
await timeout(50);
617+
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501,m=1;${p2}\x1b\\`);
618+
await timeout(50);
619+
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`);
620+
await timeout(200);
621+
strictEqual(await getImageStorageLength(), 1);
622+
deepStrictEqual(await getOrigSize(1), [200, 100]);
623+
});
624+
625+
test('verifies chunked data assembles correctly', async () => {
626+
const half = Math.floor(KITTY_MULTICOLOR_200X100_BASE64.length / 2);
627+
const part1 = KITTY_MULTICOLOR_200X100_BASE64.substring(0, half);
628+
const part2 = KITTY_MULTICOLOR_200X100_BASE64.substring(half);
629+
630+
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=502,m=1;${part1}\x1b\\`);
631+
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=502;${part2}\x1b\\`);
632+
await timeout(200);
633+
634+
const storedData = await ctx.page.evaluate(async () => {
635+
const blob = (window as any).imageAddon._handlers.get('kitty').images.get(502).data;
636+
const buffer = await blob.arrayBuffer();
637+
return Array.from(new Uint8Array(buffer));
638+
});
639+
deepStrictEqual(storedData, KITTY_MULTICOLOR_200X100_BYTES);
640+
});
641+
});
642+
643+
test.describe('Cursor positioning', () => {
644+
test('cursor advances past multi-cell image', async () => {
645+
const dim = await getDimensions();
646+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
647+
await timeout(200);
648+
649+
const expectedCols = Math.ceil(200 / dim.cellWidth);
650+
const expectedRows = Math.ceil(100 / dim.cellHeight) - 1;
651+
const cursor = await getCursor();
652+
strictEqual(cursor[0], expectedCols, 'cursor should advance by image columns');
653+
strictEqual(cursor[1], expectedRows, 'cursor should be on last row of image');
654+
});
655+
656+
test('cursor does not move with C=1', async () => {
657+
await ctx.proxy.write(`\x1b_Ga=T,f=100,C=1;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
658+
await timeout(200);
659+
deepStrictEqual(await getCursor(), [0, 0]);
660+
});
661+
662+
test('cursor uses explicit c and r over image dimensions', async () => {
663+
await ctx.proxy.write(`\x1b_Ga=T,f=100,c=10,r=5;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
664+
await timeout(200);
665+
deepStrictEqual(await getCursor(), [10, 4]);
666+
});
667+
});
668+
669+
test.describe('Pixel verification', () => {
670+
// The 200x100 image has 20 colored rectangles in a 10x2 grid.
671+
// Each rectangle is 20px wide x 50px tall.
672+
// Top row (y=0..49): Red, Orange, Yellow, Lime, Green, Cyan, SkyBlue, Blue, Purple, Magenta
673+
// Bottom row (y=50..99): Pink, Brown, Maroon, Olive, Teal, Navy, Gray, DarkGray, LightGray, White
674+
675+
test('renders red rectangle at top-left origin (0,0)', async () => {
676+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
677+
await timeout(200);
678+
// Pixel (0,0) is in the first rectangle: Red
679+
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
680+
});
681+
682+
test('renders top row colors at rectangle centers', async () => {
683+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
684+
await timeout(200);
685+
686+
// Sample center of each top-row rectangle (y=25, x=10,30,50,...,190)
687+
// All within the first cell row, so we read from the canvas at cell (0,0)
688+
// Red at x=10
689+
deepStrictEqual(await getPixel(0, 0, 10, 25), [255, 0, 0, 255]);
690+
// Orange at x=30
691+
deepStrictEqual(await getPixel(0, 0, 30, 25), [255, 128, 0, 255]);
692+
// Yellow at x=50
693+
deepStrictEqual(await getPixel(0, 0, 50, 25), [255, 255, 0, 255]);
694+
// Lime at x=70
695+
deepStrictEqual(await getPixel(0, 0, 70, 25), [0, 255, 0, 255]);
696+
// Green at x=90
697+
deepStrictEqual(await getPixel(0, 0, 90, 25), [0, 128, 0, 255]);
698+
});
699+
700+
test('renders bottom row colors at rectangle centers', async () => {
701+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
702+
await timeout(200);
703+
704+
// Bottom row starts at y=50. Center at y=75.
705+
// Pink at x=10
706+
deepStrictEqual(await getPixel(0, 0, 10, 75), [255, 192, 203, 255]);
707+
// Brown at x=30
708+
deepStrictEqual(await getPixel(0, 0, 30, 75), [165, 42, 42, 255]);
709+
// Maroon at x=50
710+
deepStrictEqual(await getPixel(0, 0, 50, 75), [128, 0, 0, 255]);
711+
// Olive at x=70
712+
deepStrictEqual(await getPixel(0, 0, 70, 75), [128, 128, 0, 255]);
713+
// Teal at x=90
714+
deepStrictEqual(await getPixel(0, 0, 90, 75), [0, 128, 128, 255]);
715+
});
716+
717+
test('renders correct colors at rectangle boundaries', async () => {
718+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
719+
await timeout(200);
720+
721+
// Last pixel of first rectangle (x=19, y=0): still Red
722+
deepStrictEqual(await getPixel(0, 0, 19, 0), [255, 0, 0, 255]);
723+
// First pixel of second rectangle (x=20, y=0): Orange
724+
deepStrictEqual(await getPixel(0, 0, 20, 0), [255, 128, 0, 255]);
725+
// Last pixel of top row (x=199, y=49): Magenta
726+
deepStrictEqual(await getPixel(0, 0, 199, 49), [255, 0, 255, 255]);
727+
// First pixel of bottom row (x=0, y=50): Pink
728+
deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]);
729+
});
730+
731+
test('renders correct color at bottom-right corner', async () => {
732+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
733+
await timeout(200);
734+
735+
// Bottom-right corner (x=199, y=99): White
736+
deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]);
737+
});
738+
739+
test('renders a strip of top-row pixels via getPixels', async () => {
740+
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
741+
await timeout(200);
742+
743+
// Read 3 pixels starting at x=18 y=0, spanning the Red/Orange boundary
744+
const pixels = await getPixels(0, 0, 18, 0, 3, 1);
745+
// x=18,19 -> Red; x=20 -> Orange
746+
deepStrictEqual(pixels?.slice(0, 4), [255, 0, 0, 255]); // x=18: Red
747+
deepStrictEqual(pixels?.slice(4, 8), [255, 0, 0, 255]); // x=19: Red
748+
deepStrictEqual(pixels?.slice(8, 12), [255, 128, 0, 255]); // x=20: Orange
749+
});
750+
});
751+
752+
test.describe('Query support', () => {
753+
test('responds with OK for valid 200x100 PNG query', async () => {
754+
await ctx.page.evaluate(() => {
755+
(window as any).kittyResponse = '';
756+
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
757+
});
758+
759+
await ctx.proxy.write(`\x1b_Gi=600,a=q,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
760+
await timeout(200);
761+
762+
const response = await ctx.page.evaluate('window.kittyResponse');
763+
strictEqual(response, '\x1b_Gi=600;OK\x1b\\');
764+
});
765+
766+
test('query does not store the 200x100 image', async () => {
767+
await ctx.page.evaluate(() => {
768+
(window as any).term.onData(() => { /* consume response */ });
769+
});
770+
771+
await ctx.proxy.write(`\x1b_Gi=601,a=q,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
772+
await timeout(200);
773+
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(601)`), false);
774+
});
775+
});
776+
777+
test.describe('Delete commands', () => {
778+
test('delete removes 200x100 image by id', async () => {
779+
await ctx.proxy.write(`\x1b_Ga=t,f=100,i=700;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
780+
await timeout(200);
781+
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(700)`), true);
782+
783+
await ctx.proxy.write(`\x1b_Ga=d,i=700\x1b\\`);
784+
await timeout(50);
785+
strictEqual(await ctx.page.evaluate(`window.imageAddon._handlers.get('kitty').images.has(700)`), false);
786+
});
787+
});
788+
});
789+
569790
test.describe('Raw RGB pixel format (f=24)', () => {
570791
test.describe('Pixel verification', () => {
571792
test('renders 1x1 black pixel with alpha set to 255', async () => {

0 commit comments

Comments
 (0)