Skip to content

Commit

Permalink
Playwright API for Notebooks (#14098)
Browse files Browse the repository at this point in the history
  • Loading branch information
dhuebner authored Oct 9, 2024
1 parent f1dc1d3 commit 6b7ceb7
Show file tree
Hide file tree
Showing 18 changed files with 788 additions and 12 deletions.
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

0 comments on commit 6b7ceb7

Please sign in to comment.