diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c893d8a7dc..439d383f61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,47 +150,6 @@ jobs: - name: Unit tests run: yarn test-unit --forbid-only - test-api-parallel: - timeout-minutes: 20 - strategy: - matrix: - node-version: [18] # just one as integration tests are about testing in browser - runs-on: [ubuntu] # macos is flaky - browser: [chromium, firefox] - runs-on: ${{ matrix.runs-on }}-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }}.x - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }}.x - cache: 'yarn' - - name: Install dependencies - run: | - yarn --frozen-lockfile - yarn install-addons - - name: Install playwright - run: npx playwright install - - name: Wait for build job - uses: NathanFirmo/wait-for-other-job@v1.1.1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - job: build - - uses: actions/download-artifact@v3 - with: - name: build-artifacts - - name: Unzip artifacts - shell: bash - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" - else - unzip -o compressed-build.zip - fi - ls -R - - name: Integration tests (${{ matrix.browser }}) - run: yarn test-api-${{ matrix.browser }} --headless --forbid-only - test-playwright-parallel: timeout-minutes: 20 strategy: @@ -238,43 +197,6 @@ jobs: - name: Integration tests (addon-webgl) run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl - test-api: - needs: build - timeout-minutes: 20 - strategy: - matrix: - node-version: [18] # just one as integration tests are about testing in browser - runs-on: [windows] # macos is flaky - browser: [chromium, firefox] - runs-on: ${{ matrix.runs-on }}-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }}.x - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }}.x - cache: 'yarn' - - name: Install dependencies - run: | - yarn --frozen-lockfile - yarn install-addons - - name: Install playwright - run: npx playwright install - - uses: actions/download-artifact@v3 - with: - name: build-artifacts - - name: Unzip artifacts - shell: bash - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" - else - unzip -o compressed-build.zip - fi - ls -R - - name: Integration tests (${{ matrix.browser }}) - run: yarn test-api-${{ matrix.browser }} --headless --forbid-only - release-dry-run: needs: build runs-on: ubuntu-latest diff --git a/addons/addon-image/test/ImageAddon.api.ts b/addons/addon-image/test/ImageAddon.api.ts deleted file mode 100644 index 776e96bfd0..0000000000 --- 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 0000000000..abd1295c22 --- /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 0000000000..b0e565c546 --- /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 a8c25b5cae..cb6fed28dd 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/addons/addon-serialize/test/SerializeAddon.api.ts b/addons/addon-serialize/test/SerializeAddon.test.ts similarity index 61% rename from addons/addon-serialize/test/SerializeAddon.api.ts rename to addons/addon-serialize/test/SerializeAddon.test.ts index c943e7b299..30ff60026e 100644 --- a/addons/addon-serialize/test/SerializeAddon.api.ts +++ b/addons/addon-serialize/test/SerializeAddon.test.ts @@ -3,49 +3,47 @@ * @license MIT */ -import { assert } from 'chai'; -import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; -import { Browser, Page } from '@playwright/test'; +import test from '@playwright/test'; +import { deepStrictEqual, notDeepStrictEqual, strictEqual } from 'assert'; +import { readFile } from 'fs'; +import { resolve } from 'path'; +import { ITestContext, createTestContext, openTerminal, timeout, writeSync } from '../../../out-test/playwright/TestUtils'; -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -const writeRawSync = (page: any, str: string): Promise => writeSync(page, `' +` + JSON.stringify(str) + `+ '`); +const writeRawSync = (page: any, str: string): Promise => writeSync(ctx.page, `' +` + JSON.stringify(str) + `+ '`); const testNormalScreenEqual = async (page: any, str: string): Promise => { - await writeRawSync(page, str); - const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + await writeRawSync(ctx.page, str); + const originalBuffer = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`); - const result = await page.evaluate(`serializeAddon.serialize();`) as string; - await page.evaluate(`term.reset();`); - await writeRawSync(page, result); - const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + const result = await ctx.page.evaluate(`window.serialize.serialize();`) as string; + await ctx.page.evaluate(`term.reset();`); + await writeRawSync(ctx.page, result); + const newBuffer = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`); - // chai decides -0 and 0 are different number... - // and firefox have a bug that output -0 for unknown reason - assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); + deepStrictEqual(JSON.stringify(originalBuffer), JSON.stringify(newBuffer)); }; async function testSerializeEquals(writeContent: string, expectedSerialized: string): Promise { - await writeRawSync(page, writeContent); - const result = await page.evaluate(`serializeAddon.serialize();`) as string; - assert.strictEqual(result, expectedSerialized); + await writeRawSync(ctx.page, writeContent); + const result = await ctx.page.evaluate(`window.serialize.serialize();`) as string; + strictEqual(result, expectedSerialized); } -describe('SerializeAddon', () => { - before(async function(): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - await page.goto(APP); - await openTerminal(page, { rows: 10, cols: 10 }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx, { rows: 10, cols: 10 }); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('SerializeAddon', () => { + + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.term.reset() + window.serialize?.dispose(); + window.serialize = new SerializeAddon(); + window.term.loadAddon(window.serialize); window.inspectBuffer = (buffer) => { const lines = []; for (let i = 0; i < buffer.length; i++) { @@ -62,56 +60,53 @@ describe('SerializeAddon', () => { `); }); - after(async () => await browser.close()); - beforeEach(async () => await page.evaluate(`window.term.reset()`)); + test.beforeEach(async () => { + await ctx.proxy.reset(); + }); - it('produce different output when we call test util with different text', async function(): Promise { - await writeRawSync(page, '12345'); - const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + test('produce different output when we call test util with different text', async function(): Promise { + await writeRawSync(ctx.page, '12345'); + const buffer1 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`); - await page.evaluate(`term.reset();`); - await writeRawSync(page, '67890'); - const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + await ctx.page.evaluate(`term.reset();`); + await writeRawSync(ctx.page, '67890'); + const buffer2 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`); - assert.throw(() => { - assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2)); - }); + notDeepStrictEqual(JSON.stringify(buffer1), JSON.stringify(buffer2)); }); - it('produce different output when we call test util with different line wrap', async function(): Promise { - await writeRawSync(page, '1234567890\r\n12345'); - const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + test('produce different output when we call test util with different line wrap', async function(): Promise { + await writeRawSync(ctx.page, '1234567890\r\n12345'); + const buffer3 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`); - await page.evaluate(`term.reset();`); - await writeRawSync(page, '123456789012345'); - const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`); + await ctx.page.evaluate(`term.reset();`); + await writeRawSync(ctx.page, '123456789012345'); + const buffer4 = await ctx.page.evaluate(`inspectBuffer(term.buffer.normal);`); - assert.throw(() => { - assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4)); - }); + notDeepStrictEqual(JSON.stringify(buffer3), JSON.stringify(buffer4)); }); - it('empty content', async function(): Promise { - assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); + test('empty content', async function(): Promise { + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), ''); }); - it('unwrap wrapped line', async function(): Promise { + test('unwrap wrapped line', async function(): Promise { const lines = ['123456789123456789']; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('does not unwrap non-wrapped line', async function(): Promise { + test('does not unwrap non-wrapped line', async function(): Promise { const lines = [ '123456789', '123456789' ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('preserve last empty lines', async function(): Promise { + test('preserve last empty lines', async function(): Promise { const cols = 10; const lines = [ '', @@ -126,50 +121,50 @@ describe('SerializeAddon', () => { '', '' ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('digits content', async function(): Promise { + test('digits content', async function(): Promise { const rows = 10; const cols = 10; const digitsLine = digitsString(cols); const lines = newArray(digitsLine, rows); - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize with half of scrollback', async function(): Promise { + test('serialize with half of scrollback', async function(): Promise { const rows = 20; const scrollback = rows - 10; const halfScrollback = scrollback / 2; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n')); }); - it('serialize 0 rows of scrollback', async function(): Promise { + test('serialize 0 rows of scrollback', async function(): Promise { const rows = 20; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n')); }); - it('serialize exclude modes', async () => { - await writeSync(page, 'before\\x1b[?1hafter'); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h'); - assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter'); + test('serialize exclude modes', async () => { + await ctx.proxy.write('before\x1b[?1hafter'); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), 'beforeafter\x1b[?1h'); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ excludeModes: true });`), 'beforeafter'); }); - it('serialize exclude alt buffer', async () => { - await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt'); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt'); - assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal'); + test('serialize exclude alt buffer', async () => { + await ctx.proxy.write('normal\x1b[?1049h\x1b[Halt'); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), 'normal\x1b[?1049h\x1b[Halt'); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize({ excludeAltBuffer: true });`), 'normal'); }); - it('serialize all rows of content with color16', async function(): Promise { + test('serialize all rows of content with color16', async function(): Promise { const cols = 10; const color16 = [ 30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color @@ -182,11 +177,11 @@ describe('SerializeAddon', () => { (index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`), rows ); - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with fg/bg flags', async function(): Promise { + test('serialize all rows of content with fg/bg flags', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -204,22 +199,22 @@ describe('SerializeAddon', () => { sgr(NO_INVISIBLE) + line, sgr(NO_STRIKETHROUGH) + line ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with color256', async function(): Promise { + test('serialize all rows of content with color256', async function(): Promise { const rows = 32; const cols = 10; const lines = newArray( (index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`), rows ); - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with overline', async () => { + test('serialize all rows of content with overline', async () => { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -227,11 +222,11 @@ describe('SerializeAddon', () => { sgr(UNDERLINED) + line, // Overlined, Underlined sgr(NORMAL) + line // Normal ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with color16 and style separately', async function(): Promise { + test('serialize all rows of content with color16 and style separately', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -246,11 +241,11 @@ describe('SerializeAddon', () => { sgr(BG_RESET) + line, // Underlined, Inverse sgr(NORMAL) + line // Back to normal ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with color16 and style together', async function(): Promise { + test('serialize all rows of content with color16 and style together', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -268,11 +263,11 @@ describe('SerializeAddon', () => { sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic sgr(BG_RESET) + line // Italic ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with color256 and style separately', async function(): Promise { + test('serialize all rows of content with color256 and style separately', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -287,11 +282,11 @@ describe('SerializeAddon', () => { sgr(BG_RESET) + line, // Underlined, Inverse sgr(NORMAL) + line // Back to normal ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with color256 and style together', async function(): Promise { + test('serialize all rows of content with color256 and style together', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -309,11 +304,11 @@ describe('SerializeAddon', () => { sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic sgr(BG_RESET) + line // Italic ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with colorRGB and style separately', async function(): Promise { + test('serialize all rows of content with colorRGB and style separately', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -328,11 +323,11 @@ describe('SerializeAddon', () => { sgr(BG_RESET) + line, // Underlined, Inverse sgr(NORMAL) + line // Back to normal ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize all rows of content with colorRGB and style together', async function(): Promise { + test('serialize all rows of content with colorRGB and style together', async function(): Promise { const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -350,11 +345,11 @@ describe('SerializeAddon', () => { sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic sgr(BG_RESET) + line // Italic ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize tabs correctly', async () => { + test('serialize tabs correctly', async () => { const lines = [ 'a\tb', 'aa\tc', @@ -365,11 +360,11 @@ describe('SerializeAddon', () => { 'aa\x1b[6Cc', 'aaa\x1b[5Cd' ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), expected.join('\r\n')); }); - it('serialize CJK correctly', async () => { + test('serialize CJK correctly', async () => { const lines = [ '中文中文', '12中文', @@ -381,22 +376,22 @@ describe('SerializeAddon', () => { // see also #3097 '1中文中文中' ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), lines.join('\r\n')); }); - it('serialize CJK Mixed with tab correctly', async () => { + test('serialize CJK Mixed with tab correctly', async () => { const lines = [ '中文\t12' // CJK mixed with tab ]; const expected = [ '中文\x1b[4C12' ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.serialize.serialize();`), expected.join('\r\n')); }); - it('serialize with alt screen correctly', async () => { + test('serialize with alt screen correctly', async () => { const SMCUP = '\u001b[?1049h'; const CUP = '\u001b[H'; @@ -407,12 +402,12 @@ describe('SerializeAddon', () => { `1${SMCUP}${CUP}2` ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate'); - assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'alternate'); + strictEqual(JSON.stringify(await ctx.page.evaluate(`window.serialize.serialize();`)), JSON.stringify(expected.join('\r\n'))); }); - it('serialize without alt screen correctly', async () => { + test('serialize without alt screen correctly', async () => { const SMCUP = '\u001b[?1049h'; const RMCUP = '\u001b[?1049l'; @@ -423,12 +418,12 @@ describe('SerializeAddon', () => { `1` ]; - await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal'); - assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n'))); + await ctx.proxy.write(lines.join('\r\n')); + strictEqual(await ctx.page.evaluate(`window.term.buffer.active.type`), 'normal'); + strictEqual(JSON.stringify(await ctx.page.evaluate(`window.serialize.serialize();`)), JSON.stringify(expected.join('\r\n'))); }); - it('serialize with background', async () => { + test('serialize with background', async () => { const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; const lines = [ @@ -436,10 +431,10 @@ describe('SerializeAddon', () => { `2${CLEAR_RIGHT(9)}` ]; - await testNormalScreenEqual(page, lines.join('\r\n')); + await testNormalScreenEqual(ctx.page, lines.join('\r\n')); }); - it('cause the BCE on scroll', async () => { + test('cause the BCE on scroll', async () => { const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; const padLines = newArray( @@ -452,10 +447,10 @@ describe('SerializeAddon', () => { `\u001b[44m${CLEAR_RIGHT(5)}1111111111111111` ]; - await testNormalScreenEqual(page, lines.join('\r\n')); + await testNormalScreenEqual(ctx.page, lines.join('\r\n')); }); - it('handle invalid wrap before scroll', async () => { + test('handle invalid wrap before scroll', async () => { const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; const MOVE_UP = (l: number): string => `\u001b[${l}A`; const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; @@ -475,10 +470,10 @@ describe('SerializeAddon', () => { '1' ]; - await testNormalScreenEqual(page, segments.join('')); + await testNormalScreenEqual(ctx.page, segments.join('')); }); - it('handle invalid wrap after scroll', async () => { + test('handle invalid wrap after scroll', async () => { const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`; const MOVE_UP = (l: number): string => `\u001b[${l}A`; const MOVE_DOWN = (l: number): string => `\u001b[${l}B`; @@ -505,27 +500,27 @@ describe('SerializeAddon', () => { '1' ]; - await testNormalScreenEqual(page, lines.join('')); + await testNormalScreenEqual(ctx.page, lines.join('')); }); - describe('handle modes', () => { - it('applicationCursorKeysMode', async () => { + test.describe('handle modes', () => { + test('applicationCursorKeysMode', async () => { await testSerializeEquals('test\u001b[?1h', 'test\u001b[?1h'); await testSerializeEquals('\u001b[?1l', 'test'); }); - it('applicationKeypadMode', async () => { + test('applicationKeypadMode', async () => { await testSerializeEquals('test\u001b[?66h', 'test\u001b[?66h'); await testSerializeEquals('\u001b[?66l', 'test'); }); - it('bracketedPasteMode', async () => { + test('bracketedPasteMode', async () => { await testSerializeEquals('test\u001b[?2004h', 'test\u001b[?2004h'); await testSerializeEquals('\u001b[?2004l', 'test'); }); - it('insertMode', async () => { + test('insertMode', async () => { await testSerializeEquals('test\u001b[4h', 'test\u001b[4h'); await testSerializeEquals('\u001b[4l', 'test'); }); - it('mouseTrackingMode', async () => { + test('mouseTrackingMode', async () => { await testSerializeEquals('test\u001b[?9h', 'test\u001b[?9h'); await testSerializeEquals('\u001b[?9l', 'test'); await testSerializeEquals('\u001b[?1000h', 'test\u001b[?1000h'); @@ -535,20 +530,20 @@ describe('SerializeAddon', () => { await testSerializeEquals('\u001b[?1003h', 'test\u001b[?1003h'); await testSerializeEquals('\u001b[?1003l', 'test'); }); - it('originMode', async () => { + test('originMode', async () => { // origin mode moves cursor to (0,0) await testSerializeEquals('test\u001b[?6h', 'test\u001b[4D\u001b[?6h'); await testSerializeEquals('\u001b[?6l', 'test\u001b[4D'); }); - it('reverseWraparoundMode', async () => { + test('reverseWraparoundMode', async () => { await testSerializeEquals('test\u001b[?45h', 'test\u001b[?45h'); await testSerializeEquals('\u001b[?45l', 'test'); }); - it('sendFocusMode', async () => { + test('sendFocusMode', async () => { await testSerializeEquals('test\u001b[?1004h', 'test\u001b[?1004h'); await testSerializeEquals('\u001b[?1004l', 'test'); }); - it('wraparoundMode', async () => { + test('wraparoundMode', async () => { await testSerializeEquals('test\u001b[?7l', 'test\u001b[?7l'); await testSerializeEquals('\u001b[?7h', 'test'); }); diff --git a/addons/addon-serialize/test/playwright.config.ts b/addons/addon-serialize/test/playwright.config.ts new file mode 100644 index 0000000000..b0e565c546 --- /dev/null +++ b/addons/addon-serialize/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-serialize/test/tsconfig.json b/addons/addon-serialize/test/tsconfig.json index 6eeb5cde04..cb6fed28dd 100644 --- a/addons/addon-serialize/test/tsconfig.json +++ b/addons/addon-serialize/test/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es2021", "lib": [ - "es2015" + "es2021", ], "rootDir": ".", "outDir": "../out-test", @@ -13,13 +13,15 @@ "paths": { "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": [ @@ -29,6 +31,9 @@ "references": [ { "path": "../../../src/common" + }, + { + "path": "../../../src/browser" } ] } diff --git a/addons/addon-unicode-graphemes/test/UnicodeGraphemesAddon.api.ts b/addons/addon-unicode-graphemes/test/UnicodeGraphemesAddon.api.ts deleted file mode 100644 index e13ad2921c..0000000000 --- a/addons/addon-unicode-graphemes/test/UnicodeGraphemesAddon.api.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; -import { Browser, Page } from '@playwright/test'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('UnicodeGraphemesAddon', () => { - before(async function(): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - }); - - after(async () => { - await browser.close(); - }); - - beforeEach(async function(): Promise { - await page.goto(APP); - await openTerminal(page); - }); - async function evalWidth(str: string): Promise { - return page.evaluate(`window.term._core.unicodeService.getStringCellWidth('${str}')`); - } - const ourVersion = '15-graphemes'; - it('wcwidth V15 emoji test', async () => { - await page.evaluate(` - window.unicode = new UnicodeGraphemesAddon(); - window.term.loadAddon(window.unicode); - `); - // should have loaded '15-graphemes' - assert.deepEqual(await page.evaluate(`window.term.unicode.versions`), ['6', '15', '15-graphemes']); - // switch should not throw - await page.evaluate(`window.term.unicode.activeVersion = '${ourVersion}';`); - assert.equal(await page.evaluate(`window.term.unicode.activeVersion`), ourVersion); - assert.equal(await evalWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣'), 20, - '10 emoji - width 10 in V6; 20 in V11 or later'); - assert.equal(await evalWidth('\u{1F476}\u{1F3FF}\u{1F476}'), 4, - 'baby with emoji modifier fitzpatrick type-6; baby'); - assert.equal(await evalWidth('\u{1F469}\u200d\u{1f469}\u200d\u{1f466}'), 2, - 'woman+zwj+woman+zwj+boy'); - assert.equal(await evalWidth('=\u{1F3CB}\u{FE0F}=\u{F3CB}\u{1F3FE}\u200D\u2640='), 7, - 'person lifting weights (plain, emoji); woman lighting weights, medium dark'); - assert.equal(await evalWidth('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393}'), 6, - 'woman; woman student; man student dark'); - assert.equal(await evalWidth('\u{1f1f3}\u{1f1f4}/'), 3, - 'regional indicator symbol letters N and O, cluster'); - assert.equal(await evalWidth('\u{1f1f3}/\u{1f1f4}'), 3, - 'regional indicator symbol letters N and O, separated'); - assert.equal(await evalWidth('\u0061\u0301'), 1, - 'letter a with acute accent'); - assert.equal(await evalWidth('{\u1100\u1161\u11a8\u1100\u1161}'), 6, - 'Korean Jamo'); - assert.equal(await evalWidth('\uAC00=\uD685='), 6, - 'Hangul syllables (pre-composed)'); - assert.equal(await evalWidth('(\u26b0\ufe0e)'), 3, - 'coffin with text presentation'); - assert.equal(await evalWidth('(\u26b0\ufe0f)'), 4, - 'coffin with emoji presentation'); - assert.equal(await evalWidth(''), 16, - 'Égalité (using separate acute) emoij_presentation'); - }); -}); diff --git a/addons/addon-unicode-graphemes/test/UnicodeGraphemesAddon.test.ts b/addons/addon-unicode-graphemes/test/UnicodeGraphemesAddon.test.ts new file mode 100644 index 0000000000..9bf272ef40 --- /dev/null +++ b/addons/addon-unicode-graphemes/test/UnicodeGraphemesAddon.test.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import test from '@playwright/test'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('UnicodeGraphemesAddon', () => { + + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.term.reset() + window.unicode?.dispose(); + window.unicode = new UnicodeGraphemesAddon(); + window.term.loadAddon(window.unicode); + `); + }); + + async function evalWidth(str: string): Promise { + return ctx.page.evaluate(`window.term._core.unicodeService.getStringCellWidth('${str}')`); + } + const ourVersion = '15-graphemes'; + test('wcwidth V15 emoji test', async () => { + // should have loaded '15-graphemes' + deepStrictEqual(await ctx.page.evaluate(`window.term.unicode.versions`), ['6', '15', '15-graphemes']); + // switch should not throw + await ctx.page.evaluate(`window.term.unicode.activeVersion = '${ourVersion}';`); + strictEqual(await ctx.page.evaluate(`window.term.unicode.activeVersion`), ourVersion); + strictEqual(await evalWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣'), 20, + '10 emoji - width 10 in V6; 20 in V11 or later'); + strictEqual(await evalWidth('\u{1F476}\u{1F3FF}\u{1F476}'), 4, + 'baby with emoji modifier fitzpatrick type-6; baby'); + strictEqual(await evalWidth('\u{1F469}\u200d\u{1f469}\u200d\u{1f466}'), 2, + 'woman+zwj+woman+zwj+boy'); + strictEqual(await evalWidth('=\u{1F3CB}\u{FE0F}=\u{F3CB}\u{1F3FE}\u200D\u2640='), 7, + 'person lifting weights (plain, emoji); woman lighting weights, medium dark'); + strictEqual(await evalWidth('\u{1F469}\u{1F469}\u{200D}\u{1F393}\u{1F468}\u{1F3FF}\u{200D}\u{1F393}'), 6, + 'woman; woman student; man student dark'); + strictEqual(await evalWidth('\u{1f1f3}\u{1f1f4}/'), 3, + 'regional indicator symbol letters N and O, cluster'); + strictEqual(await evalWidth('\u{1f1f3}/\u{1f1f4}'), 3, + 'regional indicator symbol letters N and O, separated'); + strictEqual(await evalWidth('\u0061\u0301'), 1, + 'letter a with acute accent'); + strictEqual(await evalWidth('{\u1100\u1161\u11a8\u1100\u1161}'), 6, + 'Korean Jamo'); + strictEqual(await evalWidth('\uAC00=\uD685='), 6, + 'Hangul syllables (pre-composed)'); + strictEqual(await evalWidth('(\u26b0\ufe0e)'), 3, + 'coffin with text presentation'); + strictEqual(await evalWidth('(\u26b0\ufe0f)'), 4, + 'coffin with emoji presentation'); + strictEqual(await evalWidth(''), 16, + 'Égalité (using separate acute) emoij_presentation'); + }); +}); diff --git a/addons/addon-unicode-graphemes/test/playwright.config.ts b/addons/addon-unicode-graphemes/test/playwright.config.ts new file mode 100644 index 0000000000..b0e565c546 --- /dev/null +++ b/addons/addon-unicode-graphemes/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-unicode-graphemes/test/tsconfig.json b/addons/addon-unicode-graphemes/test/tsconfig.json index 4b3cb31cfd..cb6fed28dd 100644 --- a/addons/addon-unicode-graphemes/test/tsconfig.json +++ b/addons/addon-unicode-graphemes/test/tsconfig.json @@ -1,26 +1,27 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2015", + "target": "es2021", "lib": [ - "dom", - "es2015" + "es2021", ], "rootDir": ".", "outDir": "../out-test", "sourceMap": true, "removeComments": true, - "strict": true, "baseUrl": ".", "paths": { "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": [ @@ -30,6 +31,9 @@ "references": [ { "path": "../../../src/common" + }, + { + "path": "../../../src/browser" } ] } diff --git a/addons/addon-unicode11/test/Unicode11Addon.api.ts b/addons/addon-unicode11/test/Unicode11Addon.api.ts deleted file mode 100644 index 83c7a61d47..0000000000 --- a/addons/addon-unicode11/test/Unicode11Addon.api.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; -import { Browser, Page } from '@playwright/test'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - -describe('Unicode11Addon', () => { - before(async function(): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - }); - - after(async () => { - await browser.close(); - }); - - beforeEach(async function(): Promise { - await page.goto(APP); - await openTerminal(page); - }); - - it('wcwidth V11 emoji test', async () => { - await page.evaluate(` - window.unicode11 = new Unicode11Addon(); - window.term.loadAddon(window.unicode11); - `); - // should have loaded '11' - assert.deepEqual((await page.evaluate(`window.term.unicode.versions`) as string[]).includes('11'), true); - // switch should not throw - await page.evaluate(`window.term.unicode.activeVersion = '11';`); - assert.deepEqual(await page.evaluate(`window.term.unicode.activeVersion`), '11'); - // v6: 10, V11: 20 - assert.deepEqual(await page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20); - }); -}); diff --git a/addons/addon-unicode11/test/Unicode11Addon.test.ts b/addons/addon-unicode11/test/Unicode11Addon.test.ts new file mode 100644 index 0000000000..4e941c34d9 --- /dev/null +++ b/addons/addon-unicode11/test/Unicode11Addon.test.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import test from '@playwright/test'; +import { deepStrictEqual } from 'assert'; +import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils'; + +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('Unicode11Addon', () => { + + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.term.reset() + window.unicode11?.dispose(); + window.unicode11 = new Unicode11Addon(); + window.term.loadAddon(window.unicode11); + `); + }); + + test('wcwidth V11 emoji test', async () => { + // should have loaded '11' + deepStrictEqual((await ctx.page.evaluate(`window.term.unicode.versions`) as string[]).includes('11'), true); + // switch should not throw + await ctx.page.evaluate(`window.term.unicode.activeVersion = '11';`); + deepStrictEqual(await ctx.page.evaluate(`window.term.unicode.activeVersion`), '11'); + // v6: 10, V11: 20 + deepStrictEqual(await ctx.page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20); + }); +}); diff --git a/addons/addon-unicode11/test/playwright.config.ts b/addons/addon-unicode11/test/playwright.config.ts new file mode 100644 index 0000000000..b0e565c546 --- /dev/null +++ b/addons/addon-unicode11/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-unicode11/test/tsconfig.json b/addons/addon-unicode11/test/tsconfig.json index 0406d3a414..cb6fed28dd 100644 --- a/addons/addon-unicode11/test/tsconfig.json +++ b/addons/addon-unicode11/test/tsconfig.json @@ -3,24 +3,25 @@ "module": "commonjs", "target": "es2021", "lib": [ - "dom", - "es2015" + "es2021", ], "rootDir": ".", "outDir": "../out-test", "sourceMap": true, "removeComments": true, - "strict": true, "baseUrl": ".", "paths": { "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": [ @@ -30,6 +31,9 @@ "references": [ { "path": "../../../src/common" + }, + { + "path": "../../../src/browser" } ] } diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.test.ts similarity index 65% rename from addons/addon-web-links/test/WebLinksAddon.api.ts rename to addons/addon-web-links/test/WebLinksAddon.test.ts index 99c11600db..abad515097 100644 --- a/addons/addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/addon-web-links/test/WebLinksAddon.test.ts @@ -2,18 +2,11 @@ * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT */ - -import { assert } from 'chai'; -import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; -import { Browser, Page } from '@playwright/test'; - -const APP = 'http://127.0.0.1:3001/test'; - -let browser: Browser; -let page: Page; -const width = 800; -const height = 600; - +import test from '@playwright/test'; +import { deepStrictEqual, strictEqual } from 'assert'; +import { readFile } from 'fs'; +import { resolve } from 'path'; +import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../out-test/playwright/TestUtils'; interface ILinkStateData { uri?: string; @@ -30,21 +23,20 @@ interface ILinkStateData { } -describe('WebLinksAddon', () => { - before(async function(): Promise { - browser = await launchBrowser(); - page = await (await browser.newContext()).newPage(); - await page.setViewportSize({ width, height }); - await page.goto(APP); - await openTerminal(page, { cols: 40 }); - }); +let ctx: ITestContext; +test.beforeAll(async ({ browser }) => { + ctx = await createTestContext(browser); + await openTerminal(ctx, { cols: 40 }); +}); +test.afterAll(async () => await ctx.page.close()); + +test.describe('WebLinksAddon', () => { - after(async () => await browser.close()); - beforeEach(async () => { - await page.evaluate(` + test.beforeEach(async () => { + await ctx.page.evaluate(` + window.term.reset() window._linkaddon?.dispose(); - window.term.reset(); - window._linkaddon = new window.WebLinksAddon(); + window._linkaddon = new WebLinksAddon(); window.term.loadAddon(window._linkaddon); `); }); @@ -71,36 +63,36 @@ describe('WebLinksAddon', () => { '.vc', '.ve', '.vg', '.vi', '.vn', '.vu', '.wf', '.ws', '.ye', '.yt', '.za', '.zm', '.zw' ]; for (const tld of countryTlds) { - it(tld, async () => await testHostName(`foo${tld}`)); + test(tld, async () => await testHostName(`foo${tld}`)); } - it(`.com`, async () => await testHostName(`foo.com`)); + test(`.com`, async () => await testHostName(`foo.com`)); for (const tld of countryTlds) { - it(`.com${tld}`, async () => await testHostName(`foo.com${tld}`)); + test(`.com${tld}`, async () => await testHostName(`foo.com${tld}`)); } - describe('correct buffer offsets & uri', () => { - beforeEach(async () => { - await page.evaluate(` + test.describe('correct buffer offsets & uri', () => { + test.beforeEach(async () => { + await ctx.page.evaluate(` window._linkStateData = {uri:''}; window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; }; `); }); - it('all half width', async () => { - await writeSync(page, 'aaa http://example.com aaa http://example.com aaa'); + test('all half width', async () => { + await ctx.proxy.write('aaa http://example.com aaa http://example.com aaa'); await resetAndHover(5, 0); await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } }); await resetAndHover(1, 1); await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } }); }); - it('url after full width', async () => { - await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa'); + test('url after full width', async () => { + await ctx.proxy.write('¥¥¥ http://example.com ¥¥¥ http://example.com aaa'); await resetAndHover(8, 0); await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } }); await resetAndHover(1, 1); await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } }); }); - it('full width within url and before', async () => { - await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥'); + test('full width within url and before', async () => { + await ctx.proxy.write('¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥'); await resetAndHover(8, 0); await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } }); await resetAndHover(1, 1); @@ -108,15 +100,15 @@ describe('WebLinksAddon', () => { await resetAndHover(17, 1); await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } }); }); - it('name + password url after full width and combining', async () => { - await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path'); + test('name + password url after full width and combining', async () => { + await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path'); await resetAndHover(12, 0); await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); await resetAndHover(5, 1); await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); }); - it('url encoded params work properly', async () => { - await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); + test('url encoded params work properly', async () => { + await ctx.proxy.write('¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); await resetAndHover(12, 0); await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); await resetAndHover(5, 1); @@ -125,13 +117,14 @@ describe('WebLinksAddon', () => { }); // issue #4964 - it('uppercase in protocol and host, default ports', async () => { - const data = ` HTTP://EXAMPLE.COM \\r\\n` + - ` HTTPS://Example.com \\r\\n` + - ` HTTP://Example.com:80 \\r\\n` + - ` HTTP://Example.com:80/staysUpper \\r\\n` + - ` HTTP://Ab:xY@abc.com:80/staysUpper \\r\\n`; - await writeSync(page, data); + test('uppercase in protocol and host, default ports', async () => { + await ctx.proxy.write( + ` HTTP://EXAMPLE.COM \r\n` + + ` HTTPS://Example.com \r\n` + + ` HTTP://Example.com:80 \r\n` + + ` HTTP://Example.com:80/staysUpper \r\n` + + ` HTTP://Ab:xY@abc.com:80/staysUpper \r\n` + ); await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); @@ -141,14 +134,15 @@ describe('WebLinksAddon', () => { }); async function testHostName(hostname: string): Promise { - const data = ` http://${hostname} \\r\\n` + - ` http://${hostname}/a~b#c~d?e~f \\r\\n` + - ` http://${hostname}/colon:test \\r\\n` + - ` http://${hostname}/colon:test: \\r\\n` + - `"http://${hostname}/"\\r\\n` + - `\\'http://${hostname}/\\'\\r\\n` + - `http://${hostname}/subpath/+/id`; - await writeSync(page, data); + await ctx.proxy.write( + ` http://${hostname} \r\n` + + ` http://${hostname}/a~b#c~d?e~f \r\n` + + ` http://${hostname}/colon:test \r\n` + + ` http://${hostname}/colon:test: \r\n` + + `"http://${hostname}/"\r\n` + + `\'http://${hostname}/\'\r\n` + + `http://${hostname}/subpath/+/id` + ); await pollForLinkAtCell(3, 0, `http://${hostname}`); await pollForLinkAtCell(3, 1, `http://${hostname}/a~b#c~d?e~f`); await pollForLinkAtCell(3, 2, `http://${hostname}/colon:test`); @@ -159,28 +153,28 @@ async function testHostName(hostname: string): Promise { } async function pollForLinkAtCell(col: number, row: number, value: string): Promise { - await page.mouse.move(...(await cellPos(col, row))); - await pollFor(page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true); - const text = await page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`); - assert.deepEqual(text, value); + await ctx.page.mouse.move(...(await cellPos(col, row))); + await pollFor(ctx.page, `!!Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').length`, true); + const text = await ctx.page.evaluate(`Array.from(document.querySelectorAll('.xterm-rows > :nth-child(${row+1}) > span[style]')).filter(el => el.style.textDecoration == 'underline').map(el => el.textContent).join('');`); + deepStrictEqual(text, value); } async function resetAndHover(col: number, row: number): Promise { - await page.mouse.move(0, 0); - await page.evaluate(`window._linkStateData = {uri:''};`); + await ctx.page.mouse.move(0, 0); + await ctx.page.evaluate(`window._linkStateData = {uri:''};`); await new Promise(r => setTimeout(r, 200)); - await page.mouse.move(...(await cellPos(col, row))); - await pollFor(page, `!!window._linkStateData.uri.length`, true); + await ctx.page.mouse.move(...(await cellPos(col, row))); + await pollFor(ctx.page, `!!window._linkStateData.uri.length`, true); } async function evalLinkStateData(uri: string, range: any): Promise { - const data: ILinkStateData = await page.evaluate(`window._linkStateData`); - assert.equal(data.uri, uri); - assert.deepEqual(data.range, range); + const data: ILinkStateData = await ctx.page.evaluate(`window._linkStateData`); + strictEqual(data.uri, uri); + deepStrictEqual(data.range, range); } async function cellPos(col: number, row: number): Promise<[number, number]> { - const coords: any = await page.evaluate(` + const coords: any = await ctx.page.evaluate(` (function() { const rect = window.term.element.getBoundingClientRect(); const dim = term._core._renderService.dimensions; diff --git a/addons/addon-web-links/test/playwright.config.ts b/addons/addon-web-links/test/playwright.config.ts new file mode 100644 index 0000000000..b0e565c546 --- /dev/null +++ b/addons/addon-web-links/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-web-links/test/tsconfig.json b/addons/addon-web-links/test/tsconfig.json index 2b4ec58f7a..cb6fed28dd 100644 --- a/addons/addon-web-links/test/tsconfig.json +++ b/addons/addon-web-links/test/tsconfig.json @@ -3,20 +3,37 @@ "module": "commonjs", "target": "es2021", "lib": [ - "es2015" + "es2021", ], "rootDir": ".", "outDir": "../out-test", "sourceMap": true, "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, "strict": true, "types": [ - "../../../node_modules/@types/mocha", - "../../../node_modules/@types/node" + "../../../node_modules/@types/node", + "../../../out-test/playwright/TestUtils" ] }, "include": [ "./**/*", "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + } ] } diff --git a/bin/test_api.js b/bin/test_api.js deleted file mode 100644 index bdfbc7aff9..0000000000 --- a/bin/test_api.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Copyright (c) 2019 The xterm.js authors. All rights reserved. - * @license MIT - */ - -const cp = require('child_process'); -const path = require('path'); - - -// Add `out` to the NODE_PATH so absolute paths can be resolved. -const env = { ...process.env }; -env.NODE_PATH = path.resolve(__dirname, '../out'); - -let testFiles = [ - './addons/**/out-test/*api.js', - './out-test/**/*api.js', -]; - - -let flagArgs = []; - -if (process.argv.length > 2) { - const args = process.argv.slice(2); - flagArgs = args.filter(e => e.startsWith('--')).map(arg => arg.split('=')).reduce((arr, val) => arr.concat(val.slice(), [])); - console.info(flagArgs); - // ability to inject particular test files via - // yarn test [testFileA testFileB ...] - files = args.filter(e => !e.startsWith('--')); - if (files.length) { - testFiles = files; - } -} - - - -env.DEBUG = flagArgs.indexOf('--debug') >= 0 ? 'debug' : ''; -env.PORT = 3001; - -const server = cp.spawn('node', ['demo/start'], { - cwd: path.resolve(__dirname, '..'), - env, - stdio: 'pipe' -}) - -server.stdout.on('data', (data) => { - // await for the server to fully start - if (data.includes("successfully")) { - const run = cp.spawnSync( - npmBinScript('mocha'), - [...testFiles, ...flagArgs], { - cwd: path.resolve(__dirname, '..'), - env, - shell: true, - stdio: 'inherit' - } - ); - - function npmBinScript(script) { - return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? - `${script}.cmd` : script)); - } - - server.kill(); - - if (run.error) { - console.error(run.error); - } - process.exit(run.status ?? -1); - } -}); - -server.stderr.on('data', (data) => { - console.error(data.toString()); -}); diff --git a/bin/test_playwright.js b/bin/test_playwright.js index 0417f2764f..da1733002a 100644 --- a/bin/test_playwright.js +++ b/bin/test_playwright.js @@ -18,13 +18,18 @@ while (argv.some(e => e.startsWith('--suite='))) { } let configs = [ - { name: 'core', path: 'out-test/playwright/playwright.config.js' }, - { name: 'addon-attach', path: 'addons/addon-attach/out-test/playwright.config.js' }, - { 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-search', path: 'addons/addon-search/out-test/playwright.config.js' }, - { name: 'addon-webgl', path: 'addons/addon-webgl/out-test/playwright.config.js' } + { name: 'core', path: 'out-test/playwright/playwright.config.js' }, + { name: 'addon-attach', path: 'addons/addon-attach/out-test/playwright.config.js' }, + { 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' }, + { name: 'addon-unicode11', path: 'addons/addon-unicode11/out-test/playwright.config.js' }, + { name: 'addon-web-links', path: 'addons/addon-web-links/out-test/playwright.config.js' }, + { name: 'addon-webgl', path: 'addons/addon-webgl/out-test/playwright.config.js' } ]; if (suiteFilter) { diff --git a/package.json b/package.json index 5e15baee9a..c1320d60aa 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,6 @@ "lint-api": "eslint --no-eslintrc -c .eslintrc.json.typings --max-warnings 0 --no-ignore --ext .d.ts typings/", "test": "npm run test-unit", "posttest": "npm run lint", - "test-api": "npm run test-api-chromium", - "test-api-chromium": "node ./bin/test_api.js --browser=chromium --timeout=20000", - "test-api-firefox": "node ./bin/test_api.js --browser=firefox --timeout=20000", - "test-api-webkit": "node ./bin/test_api.js --browser=webkit --timeout=20000", "test-playwright": "node ./bin/test_playwright.js --workers=75%", "test-playwright-chromium": "node ./bin/test_playwright.js --workers=75% \"--project=ChromeStable\"", "test-playwright-firefox": "node ./bin/test_playwright.js --workers=75% \"--project=FirefoxStable\"",