Skip to content

Commit

Permalink
Some galata improvements (jupyterlab#15375)
Browse files Browse the repository at this point in the history
* Improve helpers

* Hide test-* folders in documentation filebrowser

* Try to make `setCell` more robust

* Fix new helpers

* Minor README improvements

* Don't use try-catch to play nice with Playwright tools

* Fix setCellType

* Fix debugger status

* Fix set cell mode

* Revert some changes in filebrowser helper

* Fix debugger tests

* Fix notebook serach test

* Fix filebrowser refresh helper

* Restoration after rebase

* Fix open file in filebrowser helper

* Fix setCellType in notebook helper, and file-search test

* Use locator to set cell type, fix the cell filling and make debugger activation more robust

* Fix setCell

* Update Playwright Snapshots

* Avoid using functions that return elementHandle from the activity helper

* Avoid using functions that return elementHandle from the sidebar helper

* Avoid using functions that return elementHandle from the menu helper

* Avoid using getNotebookInPanel function that return elementHandle from the notebook helper, and start replacing getCell with getCellLocator

* Avoid using getToolbarItem and getCell functions that return elementHandle from the notebook helper

* Avoid using remaining functions that return elementHandle from the notebook helper

* Avoid using functions that return elementHandle from the debuggerPanel helper

* Remove usage of elementhandle in galata source ('.', '.37183()', .waitForSelector()')

* Fix debugger and waitForLocatortransition

* Remove usage of elementhandle in galata tests ('.', '.37183()', .waitForSelector()')

* Fix tests

* Fix dovumentation->general->Welcome

* Handle Locator or ElementHandle as parameter of some galata functions

* follow up 'Handle Locator or ElementHandle as parameter...'

* prettier

* Add deprecation comment

* Fixes outputarea-stdin ui-test failing after rebase

* Remove new usage of elementHandle

* Cleaning up

---------

Co-authored-by: Frédéric Collonval <fcollonval@users.noreply.github.com>
Co-authored-by: Nicolas Brichet <nicolas.brichet@quantstack.net>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Mar 8, 2024
1 parent f4ccd67 commit ff90432
Show file tree
Hide file tree
Showing 71 changed files with 1,509 additions and 1,172 deletions.
9 changes: 8 additions & 1 deletion galata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,14 @@ To debug tests, a good way is to use the inspector tool of playwright:

```
jupyter lab --config jupyter_server_test_config.py &
PWDEBUG=1 jlpm playwright test
jlpm playwright test --debug
```

Or the [UI mode](https://playwright.dev/docs/test-ui-mode):

```
jupyter lab --config jupyter_server_test_config.py &
jlpm playwright test --ui
```

### Dealing with login
Expand Down
4 changes: 1 addition & 3 deletions galata/src/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,7 @@ export const test: TestType<
page: Page,
helpers: IJupyterLabPage
): Promise<void> => {
await page.waitForSelector('#jupyterlab-splash', {
state: 'detached'
});
await page.locator('#jupyterlab-splash').waitFor({ state: 'detached' });
await helpers.waitForCondition(() => {
return helpers.activity.isTabActive('Launcher');
});
Expand Down
8 changes: 7 additions & 1 deletion galata/src/galata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,7 +578,10 @@ export namespace galata {
* #### Notes
* The goal is to freeze the file browser display
*/
export async function freezeContentLastModified(page: Page): Promise<void> {
export async function freezeContentLastModified(
page: Page,
filter?: <T = any>(directoryList: T[]) => T[]
): Promise<void> {
// Listen for closing connection (may happen when request are still being processed)
let isClosed = false;
const ctxt = page.context();
Expand Down Expand Up @@ -609,6 +612,9 @@ export namespace galata {
data['type'] === 'directory' &&
Array.isArray(data['content'])
) {
if (filter) {
data['content'] = filter(data['content']);
}
const now = Date.now();
const aDayAgo = new Date(now - 24 * 3600 * 1000).toISOString();
for (const entry of data['content'] as any[]) {
Expand Down
50 changes: 26 additions & 24 deletions galata/src/helpers/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ export class ActivityHelper {
.inputValue();
return activeTab === name;
} else {
const tab = await this.getTab(name);
if (!tab) {
const tab = this.getTabLocator(name);
if (!(await tab.count())) {
return false;
}
const classes = ((await tab.getAttribute('class')) ?? '').split(' ');
const classes = await Utils.getLocatorClassList(tab);
return classes.includes('jp-mod-current');
}
}
Expand All @@ -64,15 +64,19 @@ export class ActivityHelper {
*
* @param name Activity name
* @returns Handle on the tab or null if the tab is not found
*
* @deprecated You should use locator instead {@link getTabLocator}
*/
async getTab(name?: string): Promise<ElementHandle<Element> | null> {
let handle: ElementHandle<Element> | null = null;
try {
handle = await this.getTabLocator(name).elementHandle({ timeout: 500 });
} catch {
handle = null;
const locator = this.getTabLocator(name);
const start = Date.now();
while ((await locator.count()) == 0 && Date.now() - start < 500) {
await this.page.waitForTimeout(50);
}
if ((await locator.count()) > 0) {
return locator.elementHandle();
}
return handle;
return null;
}

/**
Expand All @@ -91,6 +95,8 @@ export class ActivityHelper {
*
* @param name Activity name
* @returns Handle on the tab or null if the tab is not found
*
* @deprecated You should use locator instead {@link getPanelLocator}
*/
async getPanel(name?: string): Promise<ElementHandle<Element> | null> {
const page = this.page;
Expand All @@ -108,14 +114,14 @@ export class ActivityHelper {
locator = page.getByRole('main').locator(`[role="tabpanel"][id="${id}"]`);
}

let handle: ElementHandle<Element> | null = null;
try {
handle = await locator.elementHandle({ timeout: 500 });
} catch {
handle = null;
const start = Date.now();
while ((await locator.count()) == 0 && Date.now() - start < 500) {
await this.page.waitForTimeout(50);
}

return handle;
if ((await locator.count()) > 0) {
return locator.elementHandle();
}
return null;
}

/**
Expand Down Expand Up @@ -161,16 +167,12 @@ export class ActivityHelper {
* @returns Whether the action is successful
*/
async activateTab(name: string): Promise<boolean> {
const tab = await this.getTab(name);
if (tab) {
const tab = this.getTabLocator(name);
if ((await tab.count()) === 1) {
await tab.click();
await this.page.waitForFunction(
({ tab }) => {
return tab.ariaSelected === 'true';
},
{ tab }
await Utils.waitForCondition(
async () => (await tab.getAttribute('aria-selected')) === 'true'
);

return true;
}

Expand Down
138 changes: 90 additions & 48 deletions galata/src/helpers/debuggerpanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@
* Distributed under the terms of the Modified BSD License.
*/

import { ElementHandle, Page } from '@playwright/test';
import { ElementHandle, Locator, Page } from '@playwright/test';
import { SidebarHelper } from './sidebar';
import { NotebookHelper } from './notebook';
import { waitForCondition } from '../utils';

const DEBUGGER_ITEM = 'debugger-icon';

/**
* Debugger Helper
*/
Expand All @@ -22,47 +20,57 @@ export class DebuggerHelper {

/**
* Returns true if debugger toolbar item is enabled, false otherwise
*
* @param name Notebook name
*/
async isOn(): Promise<boolean> {
if (!(await this.notebook.isAnyActive())) {
return false;
}
const item = await this.notebook.getToolbarItem(DEBUGGER_ITEM);
if (item) {
const button = await item.$('button');
if (button) {
return (await button.getAttribute('aria-pressed')) === 'true';
}
async isOn(name?: string): Promise<boolean> {
const toolbar = await this.notebook.getToolbarLocator(name);
const button = toolbar?.locator('.jp-DebuggerBugButton');
if (((await button?.count()) ?? 0) > 0) {
return (await button!.getAttribute('aria-pressed')) === 'true';
}
return false;
}

/**
* Enables the debugger toolbar item
*
* @param name Notebook name
*/
async switchOn(): Promise<void> {
await waitForCondition(async () => {
const item = await this.notebook.getToolbarItem(DEBUGGER_ITEM);
if (item) {
const button = await item.$('button');
if (button) {
return (await button.getAttribute('aria-disabled')) !== 'true';
}
}
return false;
}, 2000);
if (!(await this.isOn())) {
await this.notebook.clickToolbarItem(DEBUGGER_ITEM);
async switchOn(name?: string): Promise<void> {
const toolbar = await this.notebook.getToolbarLocator(name);
if (!toolbar) {
return;
}
const button = toolbar.locator('.jp-DebuggerBugButton');
await waitForCondition(async () => (await button.count()) === 1);
await waitForCondition(
async () => (await button!.isDisabled()) === false,
2000
);

if (!(await this.isOn(name))) {
await button!.click();
}
await waitForCondition(async () => await this.isOn(name));
}

/**
* Disables the debugger toolbar item
*
* @param name Notebook name
*/
async switchOff(): Promise<void> {
if (await this.isOn()) {
await this.notebook.clickToolbarItem(DEBUGGER_ITEM);
async switchOff(name?: string): Promise<void> {
const toolbar = await this.notebook.getToolbarLocator(name);
if (!toolbar) {
return;
}
const button = toolbar.locator('.jp-DebuggerBugButton');
await waitForCondition(async () => (await button.count()) === 1);
if (await this.isOn(name)) {
await button!.click();
}
await waitForCondition(async () => !(await this.isOn(name)));
}

/**
Expand All @@ -74,16 +82,25 @@ export class DebuggerHelper {

/**
* Returns handle to the variables panel content
*
* @deprecated You should use locator instead {@link getVariablesPanelLocator}
*/
async getVariablesPanel(): Promise<ElementHandle<Element> | null> {
return (await this.getVariablesPanelLocator()).elementHandle();
}

/**
* Returns locator to the variables panel content
*/
async getVariablesPanelLocator(): Promise<Locator> {
return this._getPanel('.jp-DebuggerVariables');
}

/**
* Waits for variables to be populated in the variables panel
*/
async waitForVariables(): Promise<void> {
await this.page.waitForSelector('.jp-DebuggerVariables-body ul');
await this.page.locator('.jp-DebuggerVariables-body ul').waitFor();
}

/**
Expand All @@ -96,64 +113,89 @@ export class DebuggerHelper {
await this.page
.locator('.lm-Menu-itemLabel:text("Render Variable")')
.click();
await this.page.waitForSelector('.jp-VariableRendererPanel-renderer');
await this.page.locator('.jp-VariableRendererPanel-renderer').waitFor();
}

/**
* Returns handle to callstack panel content
*
* @deprecated You should use locator instead {@link getCallStackPanelLocator}
*/
async getCallStackPanel(): Promise<ElementHandle<Element> | null> {
return (await this.getCallStackPanelLocator()).elementHandle();
}

/**
* Returns locator to callstack panel content
*/
async getCallStackPanelLocator(): Promise<Locator> {
return this._getPanel('.jp-DebuggerCallstack');
}

/**
* Waits for the callstack body to populate in the callstack panel
*/
async waitForCallStack(): Promise<void> {
await this.page.waitForSelector(
'.jp-DebuggerCallstack-body >> .jp-DebuggerCallstackFrame'
);
await this.page
.locator('.jp-DebuggerCallstack-body >> .jp-DebuggerCallstackFrame')
.first()
.waitFor();
}

/**
* Returns handle to breakpoints panel content
*
* @deprecated You should use locator instead {@link getBreakPointsPanelLocator}
*/
async getBreakPointsPanel(): Promise<ElementHandle<Element> | null> {
return (await this.getBreakPointsPanelLocator()).elementHandle();
}

/**
* Returns locator to breakpoints panel content
*/
async getBreakPointsPanelLocator(): Promise<Locator> {
return this._getPanel('.jp-DebuggerBreakpoints');
}

/**
* Waits for the breakpoints to appear in the breakpoints panel
*/
async waitForBreakPoints(): Promise<void> {
await this.page.waitForSelector(
'.jp-DebuggerBreakpoints >> .jp-DebuggerBreakpoint'
);
await this.page
.locator('.jp-DebuggerBreakpoints >> .jp-DebuggerBreakpoint')
.first()
.waitFor();
}

/**
* Returns handle to sources panel content
*
* @deprecated You should use locator instead {@link getSourcePanelLocator}
*/
async getSourcePanel(): Promise<ElementHandle<Element> | null> {
return (await this.getSourcePanelLocator()).elementHandle();
}

/**
* Returns locator to sources panel content
*/
async getSourcePanelLocator(): Promise<Locator> {
return this._getPanel('.jp-DebuggerSources');
}

/**
* Waits for sources to be populated in the sources panel
*/
async waitForSources(): Promise<void> {
await this.page.waitForSelector('.jp-DebuggerSources-body >> .jp-Editor', {
state: 'visible'
});
await this.page
.locator('.jp-DebuggerSources-body >> .jp-Editor')
.first()
.waitFor({ state: 'visible' });
}

private async _getPanel(
selector: string
): Promise<ElementHandle<Element> | null> {
const panel = await this.sidebar.getContentPanel('right');
if (panel) {
return panel.$(selector);
}
return null;
private async _getPanel(selector: string): Promise<Locator> {
const panel = this.sidebar.getContentPanelLocator('right');
return panel.locator(selector);
}
}
Loading

0 comments on commit ff90432

Please sign in to comment.