diff --git a/addons/addon-image/test/ImageAddon.api.ts b/addons/addon-image/test/ImageAddon.api.ts deleted file mode 100644 index 776e96bf..00000000 --- a/addons/addon-image/test/ImageAddon.api.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * Copyright (c) 2020 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { openTerminal, launchBrowser, pollFor } from '../../../out-test/api/TestUtils'; -import { Browser, Page } from '@playwright/test'; -import { IImageAddonOptions } from '../src/Types'; -import { FINALIZER, introducer, sixelEncode } from 'sixel'; -import { readFileSync } from 'fs'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -// eslint-disable-next-line -declare const ImageAddon: { - new(options?: Partial): any; -}; - -interface ITestData { - width: number; - height: number; - bytes: Uint8Array; - palette: number[]; - sixel: string; -} - -interface IDimensions { - cellWidth: number; - cellHeight: number; - width: number; - height: number; -} - -// image: 640 x 80, 512 color -const TESTDATA: ITestData = (() => { - const data8 = readFileSync('./addons/addon-image/fixture/palette.blob'); - const data32 = new Uint32Array(data8.buffer); - const palette = new Set(); - for (let i = 0; i < data32.length; ++i) palette.add(data32[i]); - const sixel = sixelEncode(data8, 640, 80, [...palette]); - return { - width: 640, - height: 80, - bytes: data8, - palette: [...palette], - sixel - }; -})(); -const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER; -// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER; -// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER; - -// NOTE: the data is loaded as string for easier transport through playwright -const TESTDATA_IIP: [string, [number, number]][] = [ - [readFileSync('./addons/addon-image/fixture/iip/palette.iip', { encoding: 'utf-8' }), [640, 80]], - [readFileSync('./addons/addon-image/fixture/iip/spinfox.iip', { encoding: 'utf-8' }), [148, 148]], - [readFileSync('./addons/addon-image/fixture/iip/w3c_gif.iip', { encoding: 'utf-8' }), [72, 48]], - [readFileSync('./addons/addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]], - [readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]] -]; - -describe('ImageAddon', () => { - before(async () => { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - }); - - after(async () => { - await browser.close(); - }); - - beforeEach(async () => { - await page.goto(APP); - await openTerminal(page); - await page.evaluate(opts => { - (window as any).imageAddon = new ImageAddon(opts.opts); - (window as any).term.loadAddon((window as any).imageAddon); - }, { opts: { sixelPaletteLimit: 512 } }); - }); - - it('test for private accessors', async () => { - // terminal privates - const accessors = [ - '_core', - '_core._renderService', - '_core._inputHandler', - '_core._inputHandler._parser', - '_core._inputHandler._curAttrData', - '_core._inputHandler._dirtyRowTracker', - '_core._themeService.colors', - '_core._coreBrowserService' - ]; - for (const prop of accessors) { - assert.equal( - await page.evaluate('(() => { const v = window.term.' + prop + '; return v !== undefined && v !== null; })()'), - true, `problem at ${prop}` - ); - } - // bufferline privates - assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true); - assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true); - // inputhandler privates - assert.equal(await page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), 'AttributeData'); - assert.equal(await page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser'); - }); - - describe('ctor options', () => { - it('empty settings should load defaults', async () => { - const DEFAULT_OPTIONS: IImageAddonOptions = { - enableSizeReports: true, - pixelLimit: 16777216, - sixelSupport: true, - sixelScrolling: true, - sixelPaletteLimit: 512, // set to 512 to get example image working - sixelSizeLimit: 25000000, - storageLimit: 128, - showPlaceholder: true, - iipSupport: true, - iipSizeLimit: 20000000 - }; - assert.deepEqual(await page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS); - }); - it('custom settings should overload defaults', async () => { - const customSettings: IImageAddonOptions = { - enableSizeReports: false, - pixelLimit: 5, - sixelSupport: false, - sixelScrolling: false, - sixelPaletteLimit: 1024, - sixelSizeLimit: 1000, - storageLimit: 10, - showPlaceholder: false, - iipSupport: false, - iipSizeLimit: 1000 - }; - await page.evaluate(opts => { - (window as any).imageAddonCustom = new ImageAddon(opts.opts); - (window as any).term.loadAddon((window as any).imageAddonCustom); - }, { opts: customSettings }); - assert.deepEqual(await page.evaluate(`window.imageAddonCustom._opts`), customSettings); - }); - }); - - describe('scrolling & cursor modes', () => { - it('testdata default (scrolling with VT240 cursor pos)', async () => { - const dim = await getDimensions(); - await writeToTerminal(SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); - // moved to right by 10 cells - await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); - }); - it('write testdata noScrolling', async () => { - await writeToTerminal('\x1b[?80h' + SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [0, 0]); - // second draw does not change anything - await writeToTerminal(SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [0, 0]); - }); - it('testdata cursor always at VT240 pos', async () => { - const dim = await getDimensions(); - // offset 0 - await writeToTerminal(SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); - // moved to right by 10 cells - await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); - // moved by 30 cells (+10 prev) - await writeToTerminal('#'.repeat(30) + SIXEL_SEQ_0); - assert.deepEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]); - }); - }); - - describe('image lifecycle & eviction', () => { - it('delete image once scrolled off', async () => { - await writeToTerminal(SIXEL_SEQ_0); - pollFor(page, 'window.imageAddon._storage._images.size', 1); - // scroll to scrollback + rows - 1 - await page.evaluate( - scrollback => new Promise(res => (window as any).term.write('\n'.repeat(scrollback), res)), - (await getScrollbackPlusRows() - 1) - ); - // wait here, as we have to make sure, that eviction did not yet occur - await new Promise(r => setTimeout(r, 100)); - pollFor(page, 'window.imageAddon._storage._images.size', 1); - // scroll one further should delete the image - await page.evaluate(() => new Promise(res => (window as any).term.write('\n', res))); - pollFor(page, 'window.imageAddon._storage._images.size', 0); - }); - it('get storageUsage', async () => { - assert.equal(await page.evaluate('imageAddon.storageUsage'), 0); - await writeToTerminal(SIXEL_SEQ_0); - assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05); - }); - it('get/set storageLimit', async () => { - assert.equal(await page.evaluate('imageAddon.storageLimit'), 128); - assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1); - assert.equal(await page.evaluate('imageAddon.storageLimit'), 1); - }); - it('remove images by storage limit pressure', async () => { - assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1); - // never go beyond storage limit - await writeToTerminal(SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); - const usage = await page.evaluate('imageAddon.storageUsage'); - await writeToTerminal(SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); - assert.equal(await page.evaluate('imageAddon.storageUsage'), usage); - assert.equal(usage as number < 1, true); - }); - it('set storageLimit removes images synchronously', async () => { - await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - const usage: number = await page.evaluate('imageAddon.storageUsage'); - const newUsage: number = await page.evaluate('imageAddon.storageLimit = 0.5; imageAddon.storageUsage'); - assert.equal(newUsage < usage, true); - assert.equal(newUsage < 0.5, true); - }); - it('clear alternate images on buffer change', async () => { - assert.equal(await page.evaluate('imageAddon.storageUsage'), 0); - await writeToTerminal('\x1b[?1049h' + SIXEL_SEQ_0); - assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05); - await writeToTerminal('\x1b[?1049l'); - assert.equal(await page.evaluate('imageAddon.storageUsage'), 0); - }); - it('evict tiles by in-place overwrites (only full overwrite tested)', async () => { - await new Promise(r => setTimeout(r, 50)); - await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H'); - let usage = await page.evaluate('imageAddon.storageUsage'); - while (usage === 0) { - await new Promise(r => setTimeout(r, 50)); - usage = await page.evaluate('imageAddon.storageUsage'); - } - await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H'); - await new Promise(r => setTimeout(r, 200)); // wait some time and re-check - assert.equal(await page.evaluate('imageAddon.storageUsage'), usage); - }); - it('manual eviction on alternate buffer must not miss images', async () => { - await writeToTerminal('\x1b[?1049h'); - await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); - const usage: number = await page.evaluate('imageAddon.storageUsage'); - await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); - await new Promise(r => setTimeout(r, 50)); - const newUsage: number = await page.evaluate('imageAddon.storageUsage'); - assert.equal(newUsage, usage); - }); - }); - - describe('IIP support - testimages', () => { - it('palette.png', async () => { - await writeToTerminal(TESTDATA_IIP[0][0]); - assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[0][1]); - }); - it('spinfox.png', async () => { - await writeToTerminal(TESTDATA_IIP[1][0]); - assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[1][1]); - }); - it('w3c gif', async () => { - await writeToTerminal(TESTDATA_IIP[2][0]); - assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[2][1]); - }); - it('w3c jpeg', async () => { - await writeToTerminal(TESTDATA_IIP[3][0]); - assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[3][1]); - }); - it('w3c png', async () => { - await writeToTerminal(TESTDATA_IIP[4][0]); - assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[4][1]); - }); - }); -}); - -/** - * terminal access helpers. - */ -async function getDimensions(): Promise { - const dimensions: any = await page.evaluate(`term._core._renderService.dimensions`); - return { - cellWidth: Math.round(dimensions.css.cell.width), - cellHeight: Math.round(dimensions.css.cell.height), - width: Math.round(dimensions.css.canvas.width), - height: Math.round(dimensions.css.canvas.height) - }; -} - -async function getCursor(): Promise<[number, number]> { - return page.evaluate('[window.term.buffer.active.cursorX, window.term.buffer.active.cursorY]'); -} - -async function getImageStorageLength(): Promise { - return page.evaluate('window.imageAddon._storage._images.size'); -} - -async function getScrollbackPlusRows(): Promise { - return page.evaluate('window.term.options.scrollback + window.term.rows'); -} - -async function writeToTerminal(d: string): Promise { - return page.evaluate(data => new Promise(res => (window as any).term.write(data, res)), d); -} - -async function getOrigSize(id: number): Promise<[number, number]> { - return page.evaluate(`[ - window.imageAddon._storage._images.get(${id}).orig.width, - window.imageAddon._storage._images.get(${id}).orig.height - ]`); -} diff --git a/addons/addon-image/test/ImageAddon.test.ts b/addons/addon-image/test/ImageAddon.test.ts new file mode 100644 index 00000000..abd1295c --- /dev/null +++ b/addons/addon-image/test/ImageAddon.test.ts @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2020 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import test from '@playwright/test'; +import { readFileSync } from 'fs'; +import { FINALIZER, introducer, sixelEncode } from 'sixel'; +import { ITestContext, createTestContext, openTerminal, pollFor } from '../../../out-test/playwright/TestUtils'; +import { IImageAddonOptions } from '../src/Types'; +import { deepStrictEqual, ok, strictEqual } from 'assert'; + +// eslint-disable-next-line +declare const ImageAddon: { + new(options?: Partial): any; +}; + +interface ITestData { + width: number; + height: number; + bytes: Uint8Array; + palette: number[]; + sixel: string; +} + +interface IDimensions { + cellWidth: number; + cellHeight: number; + width: number; + height: number; +} + +// image: 640 x 80, 512 color +const TESTDATA: ITestData = (() => { + const data8 = readFileSync('./addons/addon-image/fixture/palette.blob'); + const data32 = new Uint32Array(data8.buffer); + const palette = new Set(); + for (let i = 0; i < data32.length; ++i) palette.add(data32[i]); + const sixel = sixelEncode(data8, 640, 80, [...palette]); + return { + width: 640, + height: 80, + bytes: data8, + palette: [...palette], + sixel + }; +})(); +const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER; +// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER; +// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER; + +// NOTE: the data is loaded as string for easier transport through playwright +const TESTDATA_IIP: [string, [number, number]][] = [ + [readFileSync('./addons/addon-image/fixture/iip/palette.iip', { encoding: 'utf-8' }), [640, 80]], + [readFileSync('./addons/addon-image/fixture/iip/spinfox.iip', { encoding: 'utf-8' }), [148, 148]], + [readFileSync('./addons/addon-image/fixture/iip/w3c_gif.iip', { encoding: 'utf-8' }), [72, 48]], + [readFileSync('./addons/addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]], + [readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]] +]; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('ImageAddon', () => { + + test.beforeEach(async ({}, testInfo) => { + // DEBT: This test never worked on webkit + if (ctx.browser.browserType().name() === 'webkit') { + testInfo.skip(); + return; + } + await ctx.page.evaluate(` + window.term.reset() + window.imageAddon?.dispose(); + window.imageAddon = new ImageAddon({ sixelPaletteLimit: 512 }); + window.term.loadAddon(window.imageAddon); + `); + }); + + test('test for private accessors', async () => { + // terminal privates + const accessors = [ + '_core', + '_core._renderService', + '_core._inputHandler', + '_core._inputHandler._parser', + '_core._inputHandler._curAttrData', + '_core._inputHandler._dirtyRowTracker', + '_core._themeService.colors', + '_core._coreBrowserService' + ]; + for (const prop of accessors) { + strictEqual( + await ctx.page.evaluate('(() => { const v = window.term.' + prop + '; return v !== undefined && v !== null; })()'), + true, `problem at ${prop}` + ); + } + // bufferline privates + strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true); + strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true); + // inputhandler privates + strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), 'AttributeData'); + strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser'); + }); + + test.describe('ctor options', () => { + test('empty settings should load defaults', async () => { + const DEFAULT_OPTIONS: IImageAddonOptions = { + enableSizeReports: true, + pixelLimit: 16777216, + sixelSupport: true, + sixelScrolling: true, + sixelPaletteLimit: 512, // set to 512 to get example image working + sixelSizeLimit: 25000000, + storageLimit: 128, + showPlaceholder: true, + iipSupport: true, + iipSizeLimit: 20000000 + }; + deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS); + }); + test('custom settings should overload defaults', async () => { + const customSettings: IImageAddonOptions = { + enableSizeReports: false, + pixelLimit: 5, + sixelSupport: false, + sixelScrolling: false, + sixelPaletteLimit: 1024, + sixelSizeLimit: 1000, + storageLimit: 10, + showPlaceholder: false, + iipSupport: false, + iipSizeLimit: 1000 + }; + await ctx.page.evaluate(opts => { + (window as any).imageAddonCustom = new ImageAddon(opts.opts); + (window as any).term.loadAddon((window as any).imageAddonCustom); + }, { opts: customSettings }); + deepStrictEqual(await ctx.page.evaluate(`window.imageAddonCustom._opts`), customSettings); + }); + }); + + test.describe('scrolling & cursor modes', () => { + test('testdata default (scrolling with VT240 cursor pos)', async () => { + const dim = await getDimensions(); + await ctx.proxy.write(SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); + // moved to right by 10 cells + await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); + }); + test('write testdata noScrolling', async () => { + await ctx.proxy.write('\x1b[?80h' + SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [0, 0]); + // second draw does not change anything + await ctx.proxy.write(SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [0, 0]); + }); + test('testdata cursor always at VT240 pos', async () => { + const dim = await getDimensions(); + // offset 0 + await ctx.proxy.write(SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]); + // moved to right by 10 cells + await ctx.proxy.write('#'.repeat(10) + SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]); + // moved by 30 cells (+10 prev) + await ctx.proxy.write('#'.repeat(30) + SIXEL_SEQ_0); + deepStrictEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]); + }); + }); + + test.describe('image lifecycle & eviction', () => { + test('delete image once scrolled off', async () => { + await ctx.proxy.write(SIXEL_SEQ_0); + pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1); + // scroll to scrollback + rows - 1 + await ctx.page.evaluate( + scrollback => new Promise(res => (window as any).term.write('\n'.repeat(scrollback), res)), + (await getScrollbackPlusRows() - 1) + ); + // wait here, as we have to make sure, that eviction did not yet occur + await new Promise(r => setTimeout(r, 100)); + pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1); + // scroll one further should delete the image + await ctx.page.evaluate(() => new Promise(res => (window as any).term.write('\n', res))); + pollFor(ctx.page, 'window.imageAddon._storage._images.size', 0); + }); + test('get storageUsage', async () => { + strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); + await ctx.proxy.write(SIXEL_SEQ_0); + ok(Math.abs((await ctx.page.evaluate('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05); + }); + test('get/set storageLimit', async () => { + strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit'), 128); + strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit = 1'), 1); + strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit'), 1); + }); + test('remove images by storage limit pressure', async () => { + strictEqual(await ctx.page.evaluate('window.imageAddon.storageLimit = 1'), 1); + // never go beyond storage limit + await ctx.proxy.write(SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0); + await new Promise(r => setTimeout(r, 50)); + const usage = await ctx.page.evaluate('window.imageAddon.storageUsage'); + await ctx.proxy.write(SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0); + await new Promise(r => setTimeout(r, 50)); + strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), usage); + strictEqual(usage as number < 1, true); + }); + test('set storageLimit removes images synchronously', async () => { + await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); + const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); + const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageLimit = 0.5; window.imageAddon.storageUsage'); + strictEqual(newUsage < usage, true); + strictEqual(newUsage < 0.5, true); + }); + test('clear alternate images on buffer change', async () => { + strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); + await ctx.proxy.write('\x1b[?1049h' + SIXEL_SEQ_0); + ok(Math.abs((await ctx.page.evaluate('window.imageAddon.storageUsage')) - 640 * 80 * 4 / 1000000) < 0.05); + await ctx.proxy.write('\x1b[?1049l'); + strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), 0); + }); + test('evict tiles by in-place overwrites (only full overwrite tested)', async () => { + await new Promise(r => setTimeout(r, 50)); + await ctx.proxy.write('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H'); + let usage = await ctx.page.evaluate('window.imageAddon.storageUsage'); + while (usage === 0) { + await new Promise(r => setTimeout(r, 50)); + usage = await ctx.page.evaluate('window.imageAddon.storageUsage'); + } + await ctx.proxy.write('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H'); + await new Promise(r => setTimeout(r, 200)); // wait some time and re-check + strictEqual(await ctx.page.evaluate('window.imageAddon.storageUsage'), usage); + }); + test('manual eviction on alternate buffer must not miss images', async () => { + await ctx.proxy.write('\x1b[?1049h'); + await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); + await new Promise(r => setTimeout(r, 50)); + const usage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); + await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); + await ctx.proxy.write(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0); + await new Promise(r => setTimeout(r, 50)); + const newUsage: number = await ctx.page.evaluate('window.imageAddon.storageUsage'); + strictEqual(newUsage, usage); + }); + }); + + test.describe('IIP support - testimages', () => { + test('palette.png', async () => { + await ctx.proxy.write(TESTDATA_IIP[0][0]); + deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]); + }); + test('spinfox.png', async () => { + await ctx.proxy.write(TESTDATA_IIP[1][0]); + deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]); + }); + test('w3c gif', async () => { + await ctx.proxy.write(TESTDATA_IIP[2][0]); + deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]); + }); + test('w3c jpeg', async () => { + await ctx.proxy.write(TESTDATA_IIP[3][0]); + deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]); + }); + test('w3c png', async () => { + await ctx.proxy.write(TESTDATA_IIP[4][0]); + deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]); + }); + }); +}); + +/** + * terminal access helpers. + */ +async function getDimensions(): Promise { + const dimensions: any = await ctx.page.evaluate(`term._core._renderService.dimensions`); + return { + cellWidth: Math.round(dimensions.css.cell.width), + cellHeight: Math.round(dimensions.css.cell.height), + width: Math.round(dimensions.css.canvas.width), + height: Math.round(dimensions.css.canvas.height) + }; +} + +async function getCursor(): Promise<[number, number]> { + return ctx.page.evaluate('[window.term.buffer.active.cursorX, window.term.buffer.active.cursorY]'); +} + +async function getImageStorageLength(): Promise { + return ctx.page.evaluate('window.imageAddon._storage._images.size'); +} + +async function getScrollbackPlusRows(): Promise { + return ctx.page.evaluate('window.term.options.scrollback + window.term.rows'); +} + +async function getOrigSize(id: number): Promise<[number, number]> { + return ctx.page.evaluate(`[ + window.imageAddon._storage._images.get(${id}).orig.width, + window.imageAddon._storage._images.get(${id}).orig.height + ]`); +} diff --git a/addons/addon-image/test/playwright.config.ts b/addons/addon-image/test/playwright.config.ts new file mode 100644 index 00000000..b0e565c5 --- /dev/null +++ b/addons/addon-image/test/playwright.config.ts @@ -0,0 +1,35 @@ +import { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + testDir: '.', + timeout: 10000, + projects: [ + { + name: 'Chrome Stable', + use: { + browserName: 'chromium', + channel: 'chrome' + } + }, + { + name: 'Firefox Stable', + use: { + browserName: 'firefox' + } + }, + { + name: 'WebKit', + use: { + browserName: 'webkit' + } + } + ], + reporter: 'list', + webServer: { + command: 'npm run start-server-only', + port: 3000, + timeout: 120000, + reuseExistingServer: !process.env.CI + } +}; +export default config; diff --git a/addons/addon-image/test/tsconfig.json b/addons/addon-image/test/tsconfig.json index a8c25b5c..cb6fed28 100644 --- a/addons/addon-image/test/tsconfig.json +++ b/addons/addon-image/test/tsconfig.json @@ -1,21 +1,27 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2017", + "target": "es2021", + "lib": [ + "es2021", + ], "rootDir": ".", "outDir": "../out-test", "sourceMap": true, "removeComments": true, - "strict": true, "baseUrl": ".", "paths": { - "browser/*": [ "../../../src/browser/*" ], - "common/*": [ "../../../src/common/*" ] + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] }, + "strict": true, "types": [ - "../../../node_modules/@types/mocha", "../../../node_modules/@types/node", - "../../../out-test/api/TestUtils" + "../../../out-test/playwright/TestUtils" ] }, "include": [ @@ -23,7 +29,11 @@ "../../../typings/xterm.d.ts" ], "references": [ - { "path": "../../../src/browser" }, - { "path": "../../../src/common" } + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + } ] } diff --git a/bin/test_playwright.js b/bin/test_playwright.js index c48ccdb6..da173300 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -23,6 +23,7 @@ let configs = [ { name: 'addon-canvas', path: 'addons/addon-canvas/out-test/playwright.config.js' }, { name: 'addon-clipboard', path: 'addons/addon-clipboard/out-test/playwright.config.js' }, { name: 'addon-fit', path: 'addons/addon-fit/out-test/playwright.config.js' }, + { name: 'addon-image', path: 'addons/addon-image/out-test/playwright.config.js' }, { name: 'addon-search', path: 'addons/addon-search/out-test/playwright.config.js' }, { name: 'addon-serialize', path: 'addons/addon-serialize/out-test/playwright.config.js' }, { name: 'addon-unicode-graphemes', path: 'addons/addon-unicode-graphemes/out-test/playwright.config.js' },