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
4 changes: 4 additions & 0 deletions .github/workflows/frontend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ jobs:
# API server env vars
DJANGO_SETTINGS_MODULE: dandiapi.settings.development
DJANGO_DATABASE_URL: postgres://postgres:postgres@localhost:5432/django
# Close DB connections per-request so the threaded `runserver` doesn't
# accumulate connections under parallel Playwright load and exhaust
# Postgres's max_connections ("sorry, too many clients already").
DJANGO_CONN_MAX_AGE: "0"
DJANGO_CELERY_BROKER_URL: amqp://localhost:5672/
DJANGO_MINIO_STORAGE_URL: http://minioAccessKey:minioSecretKey@localhost:9000/django-storage-testing
DJANGO_DANDI_WEB_APP_URL: http://localhost:8085
Expand Down
11 changes: 11 additions & 0 deletions dandiapi/settings/development.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,14 @@

DANDI_DEV_EMAIL = 'test-dev@example.com'
DANDI_ADMIN_EMAIL = 'test-admin@example.com'

# `manage.py runserver` spawns a thread per request and, with a long-lived
# CONN_MAX_AGE (inherited from base settings), holds one Postgres connection per
# thread for the lifetime of the process. Under parallel load — notably the
# Playwright E2E suite running with multiple workers — the connection count can
# climb past Postgres's default `max_connections` ("sorry, too many clients
# already"). Allow that environment to force per-request connection closing
# (`DJANGO_CONN_MAX_AGE=0`) so the count stays bounded regardless of test count.
DATABASES['default']['CONN_MAX_AGE'] = env.float(
'DJANGO_CONN_MAX_AGE', default=DATABASES['default']['CONN_MAX_AGE']
)
3 changes: 3 additions & 0 deletions e2e/tests/dandisetsPage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ test.describe("dandisets page", async () => {
// Match a stable substring of the search field's placeholder so this
// doesn't break when the placeholder copy is tweaked.
const searchFieldText = "Search Dandisets";
// Vuetify puts role="combobox" on the field's wrapper, but the inner
// <input> still exposes the textbox role with the placeholder as its
// accessible name — so match it as a textbox.
await page.getByRole('textbox', { name: 'Search Dandisets' }).click();
await page.keyboard.press("Enter");
await page.getByRole("button", { name: "󰒓" }).click();
Expand Down
101 changes: 101 additions & 0 deletions e2e/tests/searchOperators.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { test, expect } from "@playwright/test";
import { clientUrl, dismissCookieBanner } from "../utils.ts";

// The operator autocomplete dropdown is a pure-frontend feature: the operator
// list is hardcoded in the component, so these tests don't need any backend
// data. They exercise the search field on the home page (`/`).

const SEARCH_PLACEHOLDER = /Search Dandisets/;

test.describe("search operator autocomplete", async () => {
test("shows the full operator list on focus", async ({ page }) => {
await page.goto(clientUrl);
await dismissCookieBanner(page);

const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await search.click();

const listbox = page.locator(".operator-suggestions");
await expect(listbox).toBeVisible();
// Every operator from the help table should be offered.
await expect(listbox.getByRole("option")).toHaveCount(10);
await expect(listbox.getByText("species:", { exact: true })).toBeVisible();
await expect(listbox.getByText("created_after:", { exact: true })).toBeVisible();
});

test("filters operators as you type", async ({ page }) => {
await page.goto(clientUrl);
await dismissCookieBanner(page);

const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await search.click();
await search.fill("spe");

const listbox = page.locator(".operator-suggestions");
await expect(listbox.getByRole("option")).toHaveCount(1);
await expect(listbox.getByText("species:", { exact: true })).toBeVisible();
await expect(listbox.getByText("approach:", { exact: true })).toHaveCount(0);
});

test("inserts the highlighted operator on Enter", async ({ page }) => {
await page.goto(clientUrl);
await dismissCookieBanner(page);

const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await search.click();
await search.fill("spe");
// The first match auto-highlights while typing a prefix, so Enter inserts
// it (rather than submitting the search).
await page.keyboard.press("Enter");

await expect(search).toHaveValue("species:");
// Focus stays in the field so the value can be typed straight away.
await expect(search).toBeFocused();
});

test("inserts an operator mid-query on the token under the cursor", async ({ page }) => {
await page.goto(clientUrl);
await dismissCookieBanner(page);

const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await search.click();
await search.fill("neuropixels appr");
await page.keyboard.press("Enter");

await expect(search).toHaveValue("neuropixels approach:");
});

test("inserts an operator on click", async ({ page }) => {
await page.goto(clientUrl);
await dismissCookieBanner(page);

const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await search.click();
await search.fill("file");

const listbox = page.locator(".operator-suggestions");
await listbox.getByText("file_type:", { exact: true }).click();

await expect(search).toHaveValue("file_type:");
});

test("dismisses the dropdown on Escape and submits on Enter when browsing", async ({ page }) => {
await page.goto(clientUrl);
await dismissCookieBanner(page);

const search = page.getByPlaceholder(SEARCH_PLACEHOLDER);
await search.click();

const listbox = page.locator(".operator-suggestions");
await expect(listbox).toBeVisible();

await page.keyboard.press("Escape");
await expect(listbox).toBeHidden();

// With nothing highlighted (browsing the full list), Enter submits the
// search and navigates to the search results route.
await search.fill("hippocampus");
await page.keyboard.press("Enter");
await expect(page).toHaveURL(/search=hippocampus/);
});
});
Loading
Loading