Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ concurrency:
env:
# Min and max limits of jupyterlite-core versions that this should work on.
# Changes here should be synchronised with pyproject.toml dependencies section.
MIN_LITE_VERSION: jupyterlite-core==0.7.0rc0
MIN_LITE_VERSION: jupyterlite-core==0.6.0
MAX_LITE_VERSION: --pre jupyterlite-core<0.8.0

LAB_VERSION: jupyterlab>=4.0.0,<5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

- name: Install the dependencies
run: |
python -m pip install "jupyterlite-core>=0.6,<0.7" jupyterlite-pyodide-kernel
python -m pip install "jupyterlite-core>=0.6,<0.8" jupyterlite-pyodide-kernel

# install a dev version of the terminal extension
python -m pip install .
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1

- name: Install dependencies
run: python -m pip install -U "jupyterlite-core>=0.6,<0.7" "jupyterlab>=4,<5"
run: python -m pip install -U "jupyterlite-core>=0.6,<0.8" "jupyterlab>=4,<5"

- name: Install extension
run: |
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@
"@jupyterlab/pluginmanager": "^4.5.0",
"@jupyterlab/services": "^7.5.0",
"@jupyterlab/settingregistry": "^4.5.0",
"@jupyterlite/apputils": "^0.7.0-rc.0",
"@jupyterlite/cockle": "^1.2.0",
"@jupyterlite/services": "^0.7.0-rc.0",
"@jupyterlite/apputils": "^0.7.0",
"@jupyterlite/cockle": "^1.3.0",
"@jupyterlite/services": "^0.7.0",
"@lumino/coreutils": "^2.2.1",
"@lumino/signaling": "^2.1.4",
"mock-socket": "^9.3.1"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
# Changes here should be synchronised with .github/workflows/build.yml
"jupyterlite-core>=0.7.0rc0,<0.8.0"
"jupyterlite-core>=0.6.0,<0.8.0"
]
dynamic = ["version", "description", "authors", "urls", "keywords"]

Expand Down
58 changes: 37 additions & 21 deletions ui-tests/tests/command.spec.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
import { expect, test } from '@jupyterlab/galata';

import { ContentsHelper } from './utils/contents';
import { TERMINAL_SELECTOR, WAIT_MS, inputLine } from './utils/misc';
//import { ContentsHelper } from './utils/contents';
import {
INITIAL_WAIT_MS,
TERMINAL_SELECTOR,
WAIT_MS,
inputLine,
fileContent
} from './utils/misc';

// Long wait such as for starting/stopping a complex WebAssembly command.
export const LONG_WAIT_MS = 300;

test.describe('individual command', () => {
test.beforeEach(async ({ page }) => {
await page.goto();
await page.waitForTimeout(WAIT_MS);

// Overwrite the (read-only) page.contents with our own ContentsHelper.
// @ts-ignore
page.contents = new ContentsHelper(page);
//page.contents = new ContentsHelper(page);

await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);
});

test.describe('which', () => {
test(`should support nano and vim`, async ({ page }) => {
await inputLine(page, 'which nano vim > out.txt');
await page.waitForTimeout(LONG_WAIT_MS);

const content = await fileContent(page, 'out.txt');
expect(content).toEqual('nano\nvim\n');
});
});

test.describe('nano', () => {
Expand All @@ -30,20 +45,21 @@ test.describe('individual command', () => {
await inputLine(page, `cockle-config stdin ${stdinOption}`);
await page.waitForTimeout(WAIT_MS);

await inputLine(page, 'nano a.txt');
const filename = 'a.txt';

await inputLine(page, `nano ${filename}`);
await page.waitForTimeout(LONG_WAIT_MS);

// Insert new characters.
await page.keyboard.type('mnopqrst');
await inputLine(page, 'mnopqrst', false);

// Save and quit.
await page.keyboard.press('Control+x');
await page.keyboard.type('y');
await page.keyboard.press('Enter');
await inputLine(page, 'y', true);
await page.waitForTimeout(LONG_WAIT_MS);

const outputFile = await page.contents.getContentMetadata('a.txt');
expect(outputFile?.content).toEqual('mnopqrst\n');
const content = await fileContent(page, filename);
expect(content).toEqual('mnopqrst\n');
});

test(`should delete data from file using ${stdinOption} for stdin`, async ({
Expand All @@ -62,16 +78,16 @@ test.describe('individual command', () => {
// Delete first 4 characters.
for (let i = 0; i < 4; i++) {
await page.keyboard.press('Delete');
await page.waitForTimeout(20);
}

// Save and quit.
await page.keyboard.press('Control+x');
await page.keyboard.type('y');
await page.keyboard.press('Enter');
await inputLine(page, 'y', true);
await page.waitForTimeout(LONG_WAIT_MS);

const outputFile = await page.contents.getContentMetadata('b.txt');
expect(outputFile?.content).toEqual('qrst\n');
const content = await fileContent(page, 'b.txt');
expect(content).toEqual('qrst\n');
});
});
});
Expand All @@ -89,15 +105,15 @@ test.describe('individual command', () => {
await page.waitForTimeout(LONG_WAIT_MS);

// Insert new characters.
await page.keyboard.type('iabcdefgh');
await inputLine(page, 'iabcdefgh', false);

// Save and quit.
await page.keyboard.press('Escape');
await inputLine(page, ':wq');
await page.waitForTimeout(LONG_WAIT_MS);

const outputFile = await page.contents.getContentMetadata('c.txt');
expect(outputFile?.content).toEqual('abcdefgh\n');
const content = await fileContent(page, 'c.txt');
expect(content).toEqual('abcdefgh\n');
});

test(`should delete data from file using ${stdinOption} for stdin`, async ({
Expand All @@ -114,15 +130,15 @@ test.describe('individual command', () => {
await page.waitForTimeout(LONG_WAIT_MS);

// Delete first 4 characters.
await page.keyboard.type('d4l');
await inputLine(page, 'd4l', false);

// Save and quit.
await page.keyboard.press('Escape');
await inputLine(page, ':wq');
await page.waitForTimeout(LONG_WAIT_MS);

const outputFile = await page.contents.getContentMetadata('d.txt');
expect(outputFile?.content).toEqual('efgh\n');
const content = await fileContent(page, 'd.txt');
expect(content).toEqual('efgh\n');
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions ui-tests/tests/extension.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@jupyterlab/galata';
import { INITIAL_WAIT_MS } from './utils/misc';

test.describe('Terminal extension', () => {
test('should emit activation console messages', async ({ page }) => {
Expand All @@ -8,6 +9,7 @@ test.describe('Terminal extension', () => {
});

await page.goto();
await page.waitForTimeout(INITIAL_WAIT_MS);

expect(
logs.filter(s =>
Expand Down
16 changes: 8 additions & 8 deletions ui-tests/tests/fs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { expect, test } from '@jupyterlab/galata';

import { ContentsHelper } from './utils/contents';
import { TERMINAL_SELECTOR, WAIT_MS, decode64, inputLine } from './utils/misc';
import {
INITIAL_WAIT_MS,
TERMINAL_SELECTOR,
WAIT_MS,
decode64,
inputLine
} from './utils/misc';

const MONTHS_TXT =
'January\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember\n';
Expand All @@ -27,7 +33,7 @@ test.describe('Filesystem', () => {
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);
});

test('should have initial files', async ({ page }) => {
Expand All @@ -49,12 +55,6 @@ test.describe('Filesystem', () => {
});

test('should create a new file', async ({ page }) => {
await page.goto();
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);

await inputLine(page, 'echo Hello > out.txt');
await page.getByTitle('Name: out.txt').waitFor();
});
Expand Down
19 changes: 12 additions & 7 deletions ui-tests/tests/jupyterlite_terminal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expect, test } from '@jupyterlab/galata';

import { TERMINAL_SELECTOR, WAIT_MS, inputLine } from './utils/misc';
import {
INITIAL_WAIT_MS,
TERMINAL_SELECTOR,
WAIT_MS,
inputLine
} from './utils/misc';

test.describe('Terminal', () => {
test('should emit service worker console message', async ({ page }) => {
Expand All @@ -12,7 +17,7 @@ test.describe('Terminal', () => {
await page.goto();
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);

expect(
logs.filter(s => s.match(/^Service worker supports terminal stdin/))
Expand All @@ -24,7 +29,7 @@ test.describe('Terminal', () => {
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);

// Hide modification times.
const modified = page.locator('span.jp-DirListing-itemModified');
Expand All @@ -39,7 +44,7 @@ test.describe('Terminal', () => {
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);

await inputLine(page, 'ls'); // avoid timestamps
await page.waitForTimeout(WAIT_MS);
Expand Down Expand Up @@ -77,7 +82,7 @@ test.describe('Terminal', () => {
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);

await inputLine(page, 'cockle-config stdin');
await page.waitForTimeout(WAIT_MS);
Expand All @@ -91,7 +96,7 @@ test.describe('Terminal', () => {
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);

await inputLine(page, 'cockle-config stdin sw');
await page.waitForTimeout(WAIT_MS);
Expand All @@ -107,7 +112,7 @@ test.describe('Terminal', () => {
await page.menu.clickMenuItem('File>New>Terminal');
await page.locator(TERMINAL_SELECTOR).waitFor();
await page.locator('div.xterm-screen').click(); // sets focus for keyboard input
await page.waitForTimeout(WAIT_MS);
await page.waitForTimeout(INITIAL_WAIT_MS);

await inputLine(page, `cockle-config stdin ${stdinOption}`);
await page.waitForTimeout(WAIT_MS);
Expand Down
1 change: 1 addition & 0 deletions ui-tests/tests/utils/contents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class ContentsHelper {
path: string,
type: 'file' | 'directory' = 'file'
): Promise<Contents.IModel | null> {
await this.page.waitForTimeout(100);
return await this._get(path, true, type);
}

Expand Down
50 changes: 47 additions & 3 deletions ui-tests/tests/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,60 @@
import { Buffer } from 'node:buffer';
import { expect } from '@jupyterlab/galata';
import type { Page } from '@playwright/test';

export const INITIAL_WAIT_MS = 300;
export const WAIT_MS = 100;
export const TERMINAL_SELECTOR = '.jp-Terminal';

export function decode64(encoded: string): string {
return Buffer.from(encoded, 'base64').toString('binary');
}

export async function inputLine(page, text: string) {
export async function inputLine(
page: Page,
text: string,
enter: boolean = true
) {
await page.waitForTimeout(20);
for (const char of text) {
await page.keyboard.type(char);
await page.waitForTimeout(10);
await page.waitForTimeout(20);
}
await page.keyboard.press('Enter');
if (enter) {
await page.keyboard.press('Enter');
await page.waitForTimeout(20);
}
}

async function refreshFilebrowser({ page }): Promise<void> {
try {
await page.filebrowser.refresh();
} catch (e) {
// no-op
}
}

export async function fileContent(
page: any,
filename: string
): Promise<string | undefined> {
await refreshFilebrowser({ page });
expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy();

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:42:11 › individual command › nano › should create new file using sw for stdin

4) tests/command.spec.ts:42:11 › individual command › nano › should create new file using sw for stdin Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:61:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:65:11 › individual command › nano › should delete data from file using sab for stdin

3) tests/command.spec.ts:65:11 › individual command › nano › should delete data from file using sab for stdin Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:89:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:65:11 › individual command › nano › should delete data from file using sab for stdin

3) tests/command.spec.ts:65:11 › individual command › nano › should delete data from file using sab for stdin Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:89:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:65:11 › individual command › nano › should delete data from file using sab for stdin

3) tests/command.spec.ts:65:11 › individual command › nano › should delete data from file using sab for stdin Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:89:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:42:11 › individual command › nano › should create new file using sab for stdin

2) tests/command.spec.ts:42:11 › individual command › nano › should create new file using sab for stdin Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:61:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:42:11 › individual command › nano › should create new file using sab for stdin

2) tests/command.spec.ts:42:11 › individual command › nano › should create new file using sab for stdin Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:61:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:42:11 › individual command › nano › should create new file using sab for stdin

2) tests/command.spec.ts:42:11 › individual command › nano › should create new file using sab for stdin Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:61:25

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:30:9 › individual command › which › should support nano and vim

1) tests/command.spec.ts:30:9 › individual command › which › should support nano and vim ───────── Retry #2 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:34:23

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:30:9 › individual command › which › should support nano and vim

1) tests/command.spec.ts:30:9 › individual command › which › should support nano and vim ───────── Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:34:23

Check failure on line 42 in ui-tests/tests/utils/misc.ts

View workflow job for this annotation

GitHub Actions / Integration tests min

tests/command.spec.ts:30:9 › individual command › which › should support nano and vim

1) tests/command.spec.ts:30:9 › individual command › which › should support nano and vim ───────── Error: expect(received).toBeTruthy() Received: false at tests/utils/misc.ts:42 40 | ): Promise<string | undefined> { 41 | await refreshFilebrowser({ page }); > 42 | expect(await page.filebrowser.isFileListedInBrowser(filename)).toBeTruthy(); | ^ 43 | await page.filebrowser.open(filename); 44 | 45 | const clickMenuItem = async (command): Promise<void> => { at fileContent (/home/runner/work/terminal/terminal/ui-tests/tests/utils/misc.ts:42:66) at /home/runner/work/terminal/terminal/ui-tests/tests/command.spec.ts:34:23
await page.filebrowser.open(filename);

const clickMenuItem = async (command): Promise<void> => {
await page.menu.openContextMenuLocator(
`.jp-DirListing-content >> text="${filename}"`
);
await page.getByText(command).click();
};

const [newTab] = await Promise.all([
page.waitForEvent('popup'),
clickMenuItem('Open in New Browser Tab')
]);

await newTab.waitForLoadState('networkidle');
const content = await newTab.textContent('body');
return content;
}
Loading
Loading