| name | galata-tests |
| description | Use when writing, modifying, or debugging Galata UI tests for JupyterLab, JupyterLab extensions, Jupyter Notebook, JupyterLite, or any project using `@jupyterlab/galata` and Playwright. Triggers include requests like "write a Galata test", "add a UI test for my extension", "test this JupyterLab feature", or editing files in a `ui-tests/` directory or any `.spec.ts`/`.test.ts` that imports from `@jupyterlab/galata`. |
Writing Galata Tests for JupyterLab
Galata is a JupyterLab-aware layer on top of Playwright Test. It adds a page.notebook, page.filebrowser, page.menu (etc.) helper API, fixtures that can mock JupyterLab's settings/state/user/config/routes, and a few server-side helpers for file upload and runner cleanup. Everything below Playwright is still Playwright — page.locator(), page.route(), page.keyboard, expect, screenshots, traces all work as-is.
This skill teaches how to write Galata tests. It intentionally does not catalog the API surface — that drifts with every Galata release. When you need a specific helper signature, read the current source (pointers below).
When this skill applies
- A
ui-tests/ folder in a JupyterLab extension (the standard location created by the jupyterlab/extension-template).
galata/test/ in the JupyterLab monorepo.
- Any repo with
@jupyterlab/galata in devDependencies.
Where to look things up
Always prefer reading the live source over assuming from memory. Galata evolves — helpers get added, renamed, or deprecated between majors.
Primary docs
Source files inside the galata/ tree — read these directly when you need a current signature:
src/jupyterlabpage.ts — the IJupyterLabPageFixture type; everything on page.*.
src/helpers/{notebook,filebrowser,menu,sidebar,activity,kernel,theme,statusbar,logconsole,debuggerpanel,performance,style}.ts — individual helper classes.
src/contents.ts — ContentsHelper (used by page.contents and galata.newContentsHelper()).
src/galata.ts — the galata namespace: DEFAULT_SETTINGS, Mock.*, Notebook.*, Routes.*, factories.
src/fixtures.ts — all fixture definitions (autoGoto, tmpPath, mockSettings, mockState, mockUser, mockConfig, kernels, sessions, terminals, serverFiles, baseURL, appPath, waitForApplication).
test/jupyterlab/*.test.ts — real test examples for almost every feature.
Prefer the installed package first — ui-tests/node_modules/@jupyterlab/galata/src/... (or the .d.ts in lib/) matches exactly the version the tests will run against. Fall back to GitHub only when dependencies aren't installed, and prefer a tag matching the installed major/minor over main. Raw URL pattern: https://raw.githubusercontent.com/jupyterlab/jupyterlab/<ref>/galata/<path>.
When you're about to use a helper you're unsure about, WebFetch the relevant source file and check the signature. It's cheap and avoids writing code against a stale mental model.
Also check the project's installed Galata version (package.json / yarn.lock) — majors differ (e.g. 5.x deprecates several ElementHandle-returning helpers in favor of Locator variants; both still exist but the Locator ones are preferred).
Project layout
An extension's ui-tests folder is typically:
my-extension/
└── ui-tests/
├── tests/
│ └── my-extension.spec.ts
├── jupyter_server_test_config.py
├── playwright.config.js
├── package.json
└── yarn.lock
JupyterLab itself uses a multi-project galata/playwright.config.js; tests live under galata/test/<project>/.
Setup templates
These three files are what changes the least and are worth having inline. If the repo already has them, match the existing style — don't rewrite.
ui-tests/package.json
{
"name": "my-extension-ui-tests",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "jupyter lab --config jupyter_server_test_config.py",
"test": "jlpm playwright test",
"test:update": "jlpm playwright test --update-snapshots",
"test:debug": "jlpm playwright test --debug"
},
"devDependencies": {
"@jupyterlab/galata": "^5.0.0",
"@playwright/test": "^1.40.0"
}
}
Pin @jupyterlab/galata to the major matching the target JupyterLab (check the surrounding repo before picking).
ui-tests/playwright.config.js
const baseConfig = require('@jupyterlab/galata/lib/playwright-config');
module.exports = {
...baseConfig,
webServer: {
command: 'jlpm start',
url: 'http://localhost:8888/lab',
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI
}
};
Playwright starts the JupyterLab server itself via webServer.
ui-tests/jupyter_server_test_config.py
"""Server configuration for integration tests.
Do not use in production — opens the server and exposes JupyterLab
JavaScript objects through the global `window`.
"""
from jupyterlab.galata import configure_jupyter_server
configure_jupyter_server(c)
configure_jupyter_server disables auth, enables CORS, and wires the test-only hooks. Keep the production-safety comment.
Test file structure
import { expect, test } from '@jupyterlab/galata';
test.describe('Feature', () => {
test('does the thing', async ({ page, tmpPath }) => {
});
});
Import rule — always from @jupyterlab/galata, not @playwright/test. Galata re-exports expect and extends test with its fixtures; the wrong import silently drops page.notebook, page.filebrowser, etc.
Also importable from @jupyterlab/galata: the galata namespace (for galata.DEFAULT_SETTINGS, galata.Mock.*, galata.newContentsHelper, galata.Notebook.*) and the IJupyterLabPageFixture type.
File naming: *.spec.ts in extension ui-tests/tests/, *.test.ts in the monorepo's galata/test/. Match the surrounding convention.
The page helper landscape
page is a real Playwright Page plus these JupyterLab-aware namespaces (each backed by a helper class in galata/src/helpers/ — read the file when you need a specific method):
| Namespace | What it covers |
|---|
page.notebook | Create/open/save/run notebooks; manipulate cells (add, set type, text, outputs, selection, run, editing mode, gutters); toolbar items |
page.filebrowser | Navigate directories, open files, refresh, reveal in browser |
page.contents | Server-side contents API: exists, create, upload (file/dir/content), delete, rename |
page.menu | Click menubar paths ('File>New>Notebook') and open context menus |
page.sidebar | Open/close, switch tab, move left/right, set width |
page.activity | Main area tabs and panels: activate, close, locators |
page.kernel | Query running sessions; shut down all |
page.theme | Light / Dark / Dark High Contrast |
page.statusbar | Show / hide / visibility |
page.logconsole | Log message count |
page.debugger | Debugger panel: toggle, wait for variables / callstack / breakpoints / sources |
page.performance | Timers, measure(), network throttling (Chromium) |
page.style | Collect selectors, find unused style rules |
Plus page-level utilities: page.goto(), page.reload(), page.resetUI(), page.setSimpleMode(), page.isInSimpleMode(), page.getToken(), page.getBaseUrl(), page.getOption(), page.notifications (readonly Promise), page.waitForCondition(), page.waitForTransition().
The galata namespace also exposes galata.Mock.* (server mocks: mockConfig, mockSettings, mockState, mockUser, mockRunners, mockCustomCSS, freezeContentLastModified, makeNotebookReadonly, clearRunners), galata.Notebook.* (in-memory .ipynb builders), galata.Routes.* (regex patterns for JupyterLab API routes), and factories like galata.newContentsHelper(request).
For exact signatures — read the source files listed in Where to look things up.
Fixture cheatsheet
Override with test.use({ ... }) at file, describe, or test scope. Most common ones:
autoGoto — default true. Set false when you must attach page.on('console', ...) or page.route(...) handlers before JupyterLab loads; then call await page.goto() yourself.
tmpPath — unique per test. Set to a stable string when a describe.serial block shares uploaded fixtures.
mockSettings — default galata.DEFAULT_SETTINGS. Always spread galata.DEFAULT_SETTINGS when overriding, so cursor blink / fonts remain deterministic for screenshots.
mockState, mockConfig, mockUser — booleans or initial mock data; set false when the test needs real endpoints or installs its own galata.Mock.* handlers.
serverFiles — 'off' | 'on' | 'only-on-failure'; set 'on' for post-mortem.
baseURL, appPath — override via config or env (TARGET_URL). appPath: '/doc' for single-document mode.
kernels / sessions / terminals — Map fixtures; any entries are auto-cleaned at test end.
Full, current list: galata/src/fixtures.ts.
Canonical patterns
Extension activation (the extension-template default)
import { expect, test } from '@jupyterlab/galata';
test.use({ autoGoto: false });
test('should emit an activation console message', async ({ page }) => {
const logs: string[] = [];
page.on('console', m => logs.push(m.text()));
await page.goto();
expect(
logs.filter(s => s === 'JupyterLab extension my-extension is activated!')
).toHaveLength(1);
});
autoGoto: false is mandatory — console listeners attached after auto-navigation miss the activation message.
Create, edit, run a notebook
test('runs a notebook', async ({ page }) => {
await page.notebook.createNew('test.ipynb');
await page.notebook.setCell(0, 'code', 'print(1 + 1)');
await page.notebook.runCell(0);
const output = await page.notebook.getCellTextOutput(0);
expect(output?.[0].trim()).toBe('2');
});
Upload fixture notebooks in beforeAll
import * as path from 'path';
import { expect, galata, test } from '@jupyterlab/galata';
test.use({ tmpPath: 'my-suite' });
test.describe.serial('Fixture notebook', () => {
test.beforeAll(async ({ request, tmpPath }) => {
const contents = galata.newContentsHelper(request);
await contents.uploadFile(
path.resolve(__dirname, './notebooks/fixture.ipynb'),
`${tmpPath}/fixture.ipynb`
);
});
test.afterAll(async ({ request, tmpPath }) => {
await galata.newContentsHelper(request).deleteDirectory(tmpPath);
});
test('opens the fixture', async ({ page, tmpPath }) => {
await page.filebrowser.openDirectory(tmpPath);
await page.notebook.openByPath(`${tmpPath}/fixture.ipynb`);
expect(await page.notebook.isAnyActive()).toBe(true);
});
});
Three things: galata.newContentsHelper(request) works in beforeAll where page isn't available (use page.contents.* inside tests). test.describe.serial(...) is required when tests share filesystem state. Overriding tmpPath opts out of automatic cleanup — add an afterAll that deletes the directory.
Visual regression
test('matches launcher', async ({ page }) => {
expect(await page.launcher.screenshot()).toMatchSnapshot('launcher.png');
});
Snapshots live in <test-file>-snapshots/. Update with jlpm playwright test -u. Prefer a specific locator (toolbar, panel, cell, launcher) over page.screenshot() — less pixel churn, fewer false diffs. CI and local machines produce different pixels (OS font rendering); the sibling /update-playwright-snapshots command pulls CI-generated snapshots back into the repo.
Mock settings, state, config, HTTP routes
test.describe('Update check', () => {
test.use({ autoGoto: false, mockConfig: false, mockSettings: false });
test.beforeEach(async ({ page }) => {
await page.route(/.*\/lab\/api\/update.*/, async (route, request) => {
if (request.method() === 'GET') {
return route.fulfill({
status: 200,
body: JSON.stringify({ notification: { message: 'Update!' } })
});
}
return route.continue();
});
});
test('shows update notice', async ({ page }) => {
await galata.Mock.mockConfig(page, {});
await galata.Mock.mockSettings(page, [], {
...galata.DEFAULT_SETTINGS,
'@jupyterlab/apputils-extension:notification': { checkForUpdates: true }
});
await page.goto();
const notifications = await page.notifications;
expect(notifications.some(n => n.message === 'Update!')).toBe(true);
});
});
The pattern: when route/mock setup must happen before JupyterLab bootstraps, turn off autoGoto and any auto-mocks that conflict, install page.route(...) and galata.Mock.* handlers, then await page.goto().
Menus, sidebar, activity, theme
await page.menu.clickMenuItem('File>New>Notebook');
const ctx = await page.menu.openContextMenuLocator('.jp-DirListing-item');
await ctx.getByText('Rename').click();
await page.sidebar.openTab('jp-running-sessions');
await page.activity.activateTab('Launcher');
await page.theme.setDarkTheme();
Waiting
await expect(async () => {
const out = await page.notebook.getCellTextOutput(0);
expect(out?.[0]).toContain('done');
}).toPass({ timeout: 10_000 });
await page.waitForCondition(async () => (await page.notebook.getCellCount()) > 2);
Avoid page.waitForTimeout(ms) for real synchronization; it's for cosmetic pauses only.
Locator conventions
When a Galata helper exists, use it — it tracks DOM changes across JupyterLab versions. Otherwise, prefer locators in this order:
- Role / label / text:
page.getByRole('button', { name: 'Save' }), page.getByRole('tab', { name: 'Untitled.ipynb' }), page.getByLabel(...), page.getByText(...).
data-testid: page.getByTestId(...) when the code adds one.
- JupyterLab CSS classes (fragile but common):
.jp-Notebook, .jp-Cell, .jp-InputArea, .jp-OutputArea-output, .jp-DirListing-item, .jp-BreadCrumbs-item, .jp-Dialog, .jp-Dialog-button.jp-mod-accept, .jp-SideBar, #jp-main-statusbar, .jp-Toolbar, .cm-editor, .cm-content.
Running & debugging
jlpm playwright test
jlpm playwright test tests/foo.spec.ts
jlpm playwright test -g "activation"
jlpm playwright test --headed
jlpm playwright test --ui
jlpm playwright test --debug
jlpm playwright test -u
jlpm playwright codegen localhost:8888/lab
First-time setup: follow the repo's existing install/build instructions at the root (the exact build script name varies per project), then inside ui-tests/ run jlpm install && jlpm playwright install chromium.
Common pitfalls
- Wrong
test/expect import. Importing from @playwright/test silently drops Galata fixtures; page.notebook/page.filebrowser become undefined.
- Missing
autoGoto: false before attaching listeners. page.on('console', ...) or page.route(...) must be registered before navigation, or events fire during bootstrap with no listener.
- Screenshot flake from cursor blink / fonts. Always spread
galata.DEFAULT_SETTINGS into mockSettings overrides.
- Parallel tests sharing state. Galata's base Playwright config sets
fullyParallel: true, so tests in the same file run in parallel. When tests share filesystem or notebook state, prefer isolation (unique tmpPath, separate notebooks); if they really must share, use test.describe.serial(...).
- Snapshot mismatch across OS. Linux-CI vs macOS-local snapshots always differ (font hinting). Update from CI artifacts via the sibling
/update-playwright-snapshots command, not locally, unless the suite only runs locally.
- Deprecated
ElementHandle getters. Helpers that return ElementHandle (e.g. getCell, getNotebookInPanel) lose auto-waiting. Prefer the Locator-returning variants (getCellLocator, getNotebookInPanelLocator).
- Forgetting
await page.goto() after autoGoto: false. The test runs against a blank page.
- Kernel/session leakage. The
kernels/sessions/terminals fixtures auto-clean tracked items. If you bypass them, clean up manually or the next test fails with "kernel already running".
- Dialog interaction. JupyterLab dialogs:
.jp-Dialog-button.jp-mod-accept / .jp-mod-reject, or by role (page.getByRole('button', { name: 'Ok' })). Enter submits the accept button.
Workflow when asked to "write a Galata test for X"
- Read the existing tests first. Look at one or two neighboring spec files to match style, imports, helper patterns. Don't invent a pattern if one already exists.
- Check the Galata major version in
package.json before relying on a helper that might have moved between 4.x and 5.x.
- Pick fixtures deliberately.
autoGoto: false for console/route capture; mockSettings with galata.DEFAULT_SETTINGS spread for settings overrides; tmpPath overrides for serial suites.
- Look up helper signatures in the source (
galata/src/helpers/*.ts, jupyterlabpage.ts) rather than guessing — the API moves.
- Prefer helpers over raw locators when both work; fall back to role/label/text locators otherwise.
- Use
expect(...).toPass() or Galata's waitForCondition over waitForTimeout.
- Run with
--headed once locally before reporting done — a test that passes blind is a test that will break in CI.
- For new visual snapshots — generate locally with
-u, then warn the user that CI will need its own snapshots pulled via /update-playwright-snapshots.