mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Migrate image api tests to playwright
This commit is contained in:
@@ -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<IImageAddonOptions>): 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<number>();
|
||||
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<IDimensions> {
|
||||
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<number> {
|
||||
return page.evaluate('window.imageAddon._storage._images.size');
|
||||
}
|
||||
|
||||
async function getScrollbackPlusRows(): Promise<number> {
|
||||
return page.evaluate('window.term.options.scrollback + window.term.rows');
|
||||
}
|
||||
|
||||
async function writeToTerminal(d: string): Promise<any> {
|
||||
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<any>(`[
|
||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
||||
window.imageAddon._storage._images.get(${id}).orig.height
|
||||
]`);
|
||||
}
|
||||
@@ -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<IImageAddonOptions>): 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<number>();
|
||||
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<number>('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<number>('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<IDimensions> {
|
||||
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<number> {
|
||||
return ctx.page.evaluate('window.imageAddon._storage._images.size');
|
||||
}
|
||||
|
||||
async function getScrollbackPlusRows(): Promise<number> {
|
||||
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
|
||||
}
|
||||
|
||||
async function getOrigSize(id: number): Promise<[number, number]> {
|
||||
return ctx.page.evaluate<any>(`[
|
||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
||||
window.imageAddon._storage._images.get(${id}).orig.height
|
||||
]`);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user