Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playwright API for Notebooks #14098

Merged
merged 7 commits into from
Oct 9, 2024
Merged
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
17 changes: 17 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,18 @@ jobs:
with:
python-version: "3.11"

- name: Install IPython Kernel
shell: bash
run: |
python3 -m pip install ipykernel==6.15.2
python3 -m ipykernel install --user

- name: Build Browser
shell: bash
run: |
yarn global add node-gyp
yarn --skip-integrity-check --network-timeout 100000
yarn download:plugins
yarn browser build
env:
NODE_OPTIONS: --max_old_space_size=4096
Expand All @@ -51,3 +58,13 @@ jobs:
- name: Test (playwright)
shell: bash
run: yarn --cwd examples/playwright ui-tests-ci

- name: Archive test results
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 #v4
if: ${{ !cancelled() }}
with:
name: playwright-test-results
path: |
examples/playwright/test-results/
examples/playwright/playwright-report/
retention-days: 2
4 changes: 3 additions & 1 deletion examples/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"config": {
"applicationName": "Theia Browser Example",
"preferences": {
"files.enableTrash": false
"files.enableTrash": false,
"security.workspace.trust.enabled": false
},
"reloadOnReconnect": true
}
Expand All @@ -19,6 +20,7 @@
}
}
},
"theiaPluginsDir": "../../plugins",
"dependencies": {
"@theia/ai-chat": "1.54.0",
"@theia/ai-chat-ui": "1.54.0",
Expand Down
1 change: 1 addition & 0 deletions examples/playwright/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
allure-results
test-results
playwright-report
.tmp.cfg
8 changes: 7 additions & 1 deletion examples/playwright/configs/playwright.ci.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,13 @@ const ciConfig: PlaywrightTestConfig = {
...baseConfig,
workers: 1,
retries: 2,
reporter: [['list'], ['allure-playwright'], ['github']]
reporter: [
['list'],
['github'],
['html', { open: 'never' }],
],
timeout: 30 * 1000, // Overwrite baseConfig timeout
preserveOutput: 'always'
};

export default ciConfig;
3 changes: 3 additions & 0 deletions examples/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export * from './theia-menu-item';
export * from './theia-menu';
export * from './theia-notification-indicator';
export * from './theia-notification-overlay';
export * from './theia-notebook-cell';
export * from './theia-notebook-editor';
export * from './theia-notebook-toolbar';
export * from './theia-output-channel';
export * from './theia-output-view';
export * from './theia-page-object';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"files.autoSave": "off"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
209 changes: 209 additions & 0 deletions examples/playwright/src/tests/theia-notebook-editor.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
// *****************************************************************************
// Copyright (C) 2024 TypeFox GmbH and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { PlaywrightWorkerArgs, expect, test } from '@playwright/test';
import { TheiaApp } from '../theia-app';
import { TheiaAppLoader, TheiaPlaywrightTestConfig } from '../theia-app-loader';
import { TheiaNotebookCell } from '../theia-notebook-cell';
import { TheiaNotebookEditor } from '../theia-notebook-editor';
import { TheiaWorkspace } from '../theia-workspace';
import path = require('path');

// See .github/workflows/playwright.yml for preferred python version
const preferredKernel = process.env.CI ? 'Python 3.11' : 'Python 3';

test.describe('Theia Notebook Editor interaction', () => {

let app: TheiaApp;
let editor: TheiaNotebookEditor;

test.beforeAll(async ({ playwright, browser }) => {
app = await loadApp({ playwright, browser });
});

test.beforeEach(async ({ playwright, browser }) => {
editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
});

test.afterAll(async () => {
await app.page.close();
});

test.afterEach(async () => {
if (editor) {
await editor.closeWithoutSave();
}
});

test('kernels are installed', async () => {
const kernels = await editor.availableKernels();
const msg = `Available kernels:\n ${kernels.join('\n')}`;
console.log(msg); // Print available kernels, useful when running in CI.
expect(kernels.length, msg).toBeGreaterThan(0);

const py3kernel = kernels.filter(kernel => kernel.match(new RegExp(`^${preferredKernel}`)));
expect(py3kernel.length, msg).toBeGreaterThan(0);
});

test('should select a kernel', async () => {
await editor.selectKernel(preferredKernel);
const selectedKernel = await editor.selectedKernel();
expect(selectedKernel).toMatch(new RegExp(`^${preferredKernel}`));
});

test('should add a new code cell', async () => {
await editor.addCodeCell();
const cells = await editor.cells();
expect(cells.length).toBe(2);
expect(await cells[1].mode()).toBe('python');
});

test('should add a new markdown cell', async () => {
await editor.addMarkdownCell();
await (await editor.cells())[1].addEditorText('print("markdown")');

const cells = await editor.cells();
expect(cells.length).toBe(2);
expect(await cells[1].mode()).toBe('markdown');
expect(await cells[1].editorText()).toBe('print("markdown")');
});

test('should execute all cells', async () => {
const cell = await firstCell(editor);
await cell.addEditorText('print("Hallo Notebook!")');

await editor.addCodeCell();
const secondCell = (await editor.cells())[1];
await secondCell.addEditorText('print("Bye Notebook!")');

await editor.executeAllCells();

expect(await cell.outputText()).toBe('Hallo Notebook!');
expect(await secondCell.outputText()).toBe('Bye Notebook!');
});

test('should split cell', async () => {
const cell = await firstCell(editor);
/*
Add cell text:
print("Line-1")
print("Line-2")
*/
await cell.addEditorText('print("Line-1")\nprint("Line-2")');

/*
Set cursor:
print("Line-1")
<|>print("Line-2")
*/
const line = await cell.editor.lineByLineNumber(1);
await line?.waitForElementState('visible');
await line?.click();
await line?.press('ArrowRight');

// split cell
await cell.splitCell();

// expect two cells with text "print("Line-1")" and "print("Line-2")"
expect(await editor.cells()).toHaveLength(2);
expect(await (await editor.cells())[0].editorText()).toBe('print("Line-1")');
expect(await (await editor.cells())[1].editorText()).toBe('print("Line-2")');
});
});

test.describe('Theia Notebook Cell interaction', () => {

let app: TheiaApp;
let editor: TheiaNotebookEditor;

test.beforeAll(async ({ playwright, browser }) => {
app = await loadApp({ playwright, browser });
});

test.afterAll(async () => {
await app.page.close();
});

test.beforeEach(async () => {
editor = await app.openEditor('sample.ipynb', TheiaNotebookEditor);
const selectedKernel = await editor.selectedKernel();
if (selectedKernel?.match(new RegExp(`^${preferredKernel}`)) === null) {
await editor.selectKernel(preferredKernel);
}
});

test.afterEach(async () => {
if (editor) {
await editor.closeWithoutSave();
}
});

test('should write text in a code cell', async () => {
const cell = await firstCell(editor);
// assume the first cell is a code cell
expect(await cell.isCodeCell()).toBe(true);

await cell.addEditorText('print("Hallo")');
const cellText = await cell.editorText();
expect(cellText).toBe('print("Hallo")');
});

test('should write multi-line text in a code cell', async () => {
const cell = await firstCell(editor);
await cell.addEditorText('print("Hallo")\nprint("Notebook")');

const cellText = await cell.editorText();
expect(cellText).toBe('print("Hallo")\nprint("Notebook")');
});

test('Execute code cell and read output', async () => {
const cell = await firstCell(editor);
await cell.addEditorText('print("Hallo Notebook!")');
await cell.execute();

const cellOutput = await cell.outputText();
expect(cellOutput).toBe('Hallo Notebook!');
});

test('Check execution count matches', async () => {
const cell = await firstCell(editor);
await cell.addEditorText('print("Hallo Notebook!")');
await cell.execute();
await cell.execute();
await cell.execute();

expect(await cell.executionCount()).toBe('3');
});

});

async function firstCell(editor: TheiaNotebookEditor): Promise<TheiaNotebookCell> {
return (await editor.cells())[0];
}

async function loadApp(args: TheiaPlaywrightTestConfig & PlaywrightWorkerArgs): Promise<TheiaApp> {
const workingDir = path.resolve();
// correct WS path. When running from IDE the path is playwright/configs with CLI it's playwright/
const prefix = workingDir.endsWith('playwright/configs') ? '../' : '';
const ws = new TheiaWorkspace([prefix + 'src/tests/resources/notebook-files']);
const app = await TheiaAppLoader.load(args, ws);
// auto-save are disabled using settings.json file
// see examples/playwright/src/tests/resources/notebook-files/.theia/settings.json

// NOTE: Workspace trust is disabled in examples/browser/package.json using default preferences.
// If workspace trust check is on, python extension will not be able to explore Python installations.
return app;
}
6 changes: 6 additions & 0 deletions examples/playwright/src/tests/theia-quick-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,10 @@ test.describe('Theia Quick Command', () => {
expect(await notification.isEntryVisible('Positive Integer: 6')).toBe(true);
});

test('retrieve and check visible items', async () => {
await quickCommand.type('close all tabs', false);
const listItems = await Promise.all((await quickCommand.visibleItems()).map(async item => item.textContent()));
expect(listItems).toContain('View: Close All Tabs in Main Area');
});

});
32 changes: 31 additions & 1 deletion examples/playwright/src/theia-monaco-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class TheiaMonacoEditor extends TheiaPageObject {
await this.page.waitForSelector(this.selector, { state: 'visible' });
}

protected viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
protected async viewElement(): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
return this.page.$(this.selector);
}

Expand Down Expand Up @@ -74,6 +74,36 @@ export class TheiaMonacoEditor extends TheiaPageObject {
return viewElement?.waitForSelector(`.view-lines .view-line:has-text("${text}")`);
}

/**
* @returns The text content of the editor.
*/
async editorText(): Promise<string | undefined> {
const lines: string[] = [];
const linesCount = await this.numberOfLines();
if (linesCount === undefined) {
return undefined;
}
for (let line = 1; line <= linesCount; line++) {
const lineText = await this.textContentOfLineByLineNumber(line);
if (lineText === undefined) {
break;
}
lines.push(lineText);
}
return lines.join('\n');
}

/**
* Adds text to the editor.
* @param text The text to add to the editor.
* @param lineNumber The line number where to add the text. Default is 1.
*/
async addEditorText(text: string, lineNumber: number = 1): Promise<void> {
const line = await this.lineByLineNumber(lineNumber);
await line?.click();
await this.page.keyboard.type(text);
}

protected replaceEditorSymbolsWithSpace(content: string): string | Promise<string | undefined> {
// [ ] &nbsp; => \u00a0 -- NO-BREAK SPACE
// [·] &middot; => \u00b7 -- MIDDLE DOT
Expand Down
Loading
Loading