From 1b319f8856c3553594619c6bc816e44f3a962cf1 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Fri, 13 Feb 2026 08:56:59 -0700 Subject: [PATCH] chore: added e2e tests for filter bar (#42709) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Chore: E2E tests to cover the full keyboard and mouse flow for the AI Filters ## Summary by CodeRabbit * **Tests** * Added a comprehensive end-to-end test suite for the Filter Bar: creation/removal of filters, keyboard and dropdown navigation, operator/value flows, boolean/date/timestamp presets, multiple simultaneous filters, and grid result validation. * **Tests / Utilities** * Added reusable test helpers to set up tables, interact with the filter bar, and wait for data updates. * **Style / Test hooks** * Added test IDs and highlight attributes to filter UI elements to improve test reliability. --- e2e/studio/features/filter-bar.spec.ts | 813 ++++++++++++++++++ e2e/studio/utils/filter-bar-helpers.ts | 84 ++ .../src/FilterBar/CommandListItem.tsx | 1 + .../src/FilterBar/FilterCondition.tsx | 5 + .../ui-patterns/src/FilterBar/FilterGroup.tsx | 1 + 5 files changed, 904 insertions(+) create mode 100644 e2e/studio/features/filter-bar.spec.ts create mode 100644 e2e/studio/utils/filter-bar-helpers.ts diff --git a/e2e/studio/features/filter-bar.spec.ts b/e2e/studio/features/filter-bar.spec.ts new file mode 100644 index 00000000000..279afffe8b3 --- /dev/null +++ b/e2e/studio/features/filter-bar.spec.ts @@ -0,0 +1,813 @@ +import { expect } from '@playwright/test' + +import { createTable, dropTable, query } from '../utils/db/index.js' +import { + addFilter, + addFilterWithDropdownValue, + getFilterBarInput, + navigateToTable, + selectColumnFilter, + selectOperator, + selectOperatorByClick, + setupFilterBarPage, +} from '../utils/filter-bar-helpers.js' +import { test } from '../utils/test.js' +import { toUrl } from '../utils/to-url.js' + +const tableNamePrefix = 'pw_filter_bar' + +function getDateValue(daysAgo: number): string { + const date = new Date(Date.now() - daysAgo * 86400000) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} + +test.describe('Filter Bar', () => { + test.beforeEach(async ({ page, ref }) => { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + }) + + test.describe('Basic Filter Operations', () => { + test('selecting a column creates a filter condition', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_col_sel` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, columnName) + + await expect(page.getByTestId(`filter-condition-${columnName}`)).toBeVisible() + await expect(page.getByTestId(`filter-operator-${columnName}`)).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + + test('entering value filters the grid', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_val_filt` + const columnName = 'name' + + await createTable(tableName, columnName, [ + { name: 'Alice' }, + { name: 'Bob' }, + { name: 'Charlie' }, + ]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + await expect(page.getByRole('gridcell', { name: 'Alice' })).toBeVisible() + await expect(page.getByRole('gridcell', { name: 'Bob' })).not.toBeVisible() + await expect(page.getByRole('gridcell', { name: 'Charlie' })).not.toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('removing filter via X button restores all data', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_remove_x` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }, { name: 'Bob' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + await expect(page.getByRole('gridcell', { name: 'Bob' })).not.toBeVisible() + + await page.getByTestId(`filter-remove-${columnName}`).click() + + await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toBeVisible() + await expect(page.getByRole('gridcell', { name: 'Alice' })).toBeVisible() + await expect(page.getByRole('gridcell', { name: 'Bob' })).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('multiple filters can be added', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_multi` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + first_name text, + last_name text + )` + ) + await query( + `INSERT INTO ${tableName} (first_name, last_name) VALUES ('Alice', 'Smith'), ('Bob', 'Smith'), ('Alice', 'Jones')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, 'first_name', '=', 'Alice') + await addFilter(page, ref, 'last_name', '=', 'Smith') + + await expect(page.getByTestId('filter-condition-first_name')).toBeVisible() + await expect(page.getByTestId('filter-condition-last_name')).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Keyboard Navigation - Freeform Input', () => { + test('Enter selects column from dropdown', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_enter_col` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await freeformInput.fill(columnName) + await expect(page.getByTestId(`filter-menu-item-${columnName}`)).toBeVisible() + + await page.keyboard.press('Enter') + + await expect(page.getByTestId(`filter-condition-${columnName}`)).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('Tab selects column from dropdown', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_tab_col` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await freeformInput.fill(columnName) + await expect(page.getByTestId(`filter-menu-item-${columnName}`)).toBeVisible() + + await page.keyboard.press('Tab') + + await expect(page.getByTestId(`filter-condition-${columnName}`)).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('ArrowDown/ArrowUp navigates dropdown items', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_arrow_nav` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + alpha text, + beta text, + gamma text + )` + ) + await query(`INSERT INTO ${tableName} (alpha, beta, gamma) VALUES ('a', 'b', 'g')`) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + + // Items: id (0), alpha (1), beta (2), gamma (3) + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + + await expect(page.getByTestId('filter-condition-beta')).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('Backspace on empty input highlights last filter, second Backspace deletes it', async ({ + page, + ref, + }) => { + const tableName = `${tableNamePrefix}_bksp_hl_del` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await page.keyboard.press('Backspace') + + await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute( + 'data-highlighted', + 'true' + ) + + await page.keyboard.press('Backspace') + + await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('ArrowLeft highlights previous condition', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_arrowl_hl` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await page.keyboard.press('ArrowLeft') + + await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute( + 'data-highlighted', + 'true' + ) + } finally { + await dropTable(tableName) + } + }) + + test('ArrowRight clears highlight at end', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_arrowr_cl` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await page.keyboard.press('ArrowLeft') + await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute( + 'data-highlighted', + 'true' + ) + + await page.keyboard.press('ArrowRight') + await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toHaveAttribute( + 'data-highlighted', + 'true' + ) + } finally { + await dropTable(tableName) + } + }) + + test('Escape clears highlight', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_esc_cl` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await page.keyboard.press('ArrowLeft') + await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute( + 'data-highlighted', + 'true' + ) + + await page.keyboard.press('Escape') + await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toHaveAttribute( + 'data-highlighted', + 'true' + ) + } finally { + await dropTable(tableName) + } + }) + + test('Enter on highlighted condition focuses value input', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_enter_hl` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await page.keyboard.press('Backspace') + await expect(page.getByTestId(`filter-condition-${columnName}`)).toHaveAttribute( + 'data-highlighted', + 'true' + ) + + await page.keyboard.press('Enter') + await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Keyboard Navigation - Operator Input', () => { + test('Enter selects operator and focuses value', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_op_enter` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, columnName) + await page.keyboard.press('Enter') + + await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + + test('Backspace on empty operator removes condition', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_op_bksp` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, columnName) + + const operatorInput = page.getByTestId(`filter-operator-${columnName}`) + await operatorInput.fill('') + await page.keyboard.press('Backspace') + + await expect(page.getByTestId(`filter-condition-${columnName}`)).not.toBeVisible() + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Keyboard Navigation - Value Input', () => { + test('Enter on value moves focus to group freeform', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_val_enter` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + await expect(getFilterBarInput(page)).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + + test('Backspace on empty value moves to operator', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_val_bksp` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, columnName) + await selectOperator(page, columnName, '=') + + await page.keyboard.press('Backspace') + + await expect(page.getByTestId(`filter-operator-${columnName}`)).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + + test('ArrowLeft at position 0 moves to previous condition value', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_val_arrowl` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + first_name text, + last_name text + )` + ) + await query( + `INSERT INTO ${tableName} (first_name, last_name) VALUES ('Alice', 'Smith'), ('Bob', 'Jones')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, 'first_name', '=', 'Alice') + await addFilter(page, ref, 'last_name', '=', 'Smith') + + const lastNameValue = page.getByTestId('filter-value-last_name') + await lastNameValue.click() + await page.keyboard.press('Home') + + await page.keyboard.press('ArrowLeft') + + await expect(page.getByTestId('filter-value-first_name')).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + + test('ArrowRight at end moves to next condition or freeform', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_val_arrowr` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'Alice' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilter(page, ref, columnName, '=', 'Alice') + + // Use keyboard to focus value: highlight condition then Enter to avoid re-opening dropdown + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await page.keyboard.press('Backspace') + await page.keyboard.press('Enter') + + await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused() + await page.keyboard.press('End') + await page.keyboard.press('ArrowRight') + + await expect(freeformInput).toBeFocused() + } finally { + await dropTable(tableName) + } + }) + + test('editing value in the middle preserves cursor position', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_cursor_pos` + const columnName = 'name' + + await createTable(tableName, columnName, [{ name: 'test' }]) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, columnName) + await selectOperator(page, columnName, '=') + + const valueInput = page.getByTestId(`filter-value-${columnName}`) + await valueInput.fill('HelloWorld') + + // Move cursor to middle (after "Hello") and insert text + await page.keyboard.press('Home') + for (let i = 0; i < 5; i++) { + await page.keyboard.press('ArrowRight') + } + await page.keyboard.type('_Middle_') + + // Verify the text was inserted in the middle, not appended at the end + await expect(valueInput).toHaveValue('Hello_Middle_World') + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Boolean Column Filters', () => { + test('filtering by true shows only true rows', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_bool_true` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + is_active boolean + )` + ) + await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false), (true)`) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'is_active', '=', 'true') + + const rows = page.locator('[role="row"]') + // Header row + 2 data rows with true + await expect(rows).toHaveCount(3) + } finally { + await dropTable(tableName) + } + }) + + test('filtering by false shows only false rows', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_bool_false` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + is_active boolean + )` + ) + await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false), (true)`) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'is_active', '=', 'false') + + const rows = page.locator('[role="row"]') + // Header row + 1 data row with false + await expect(rows).toHaveCount(2) + } finally { + await dropTable(tableName) + } + }) + + test('not equal operator with boolean filters correctly', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_bool_neq` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + is_active boolean + )` + ) + await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false), (true)`) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'is_active', '<>', 'true') + + const rows = page.locator('[role="row"]') + // Header row + 1 data row with false (not equal to true) + await expect(rows).toHaveCount(2) + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Date Column Filters', () => { + test('filtering by Today shows only today rows', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_date_today` + const todayValue = getDateValue(0) + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + created_at date + )` + ) + await query( + `INSERT INTO ${tableName} (created_at) VALUES + (CURRENT_DATE), + (CURRENT_DATE - INTERVAL '1 day'), + (CURRENT_DATE - INTERVAL '7 days')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'created_at', '=', todayValue) + + const rows = page.locator('[role="row"]') + await expect(rows).toHaveCount(2) + } finally { + await dropTable(tableName) + } + }) + + test('filtering by Yesterday shows only yesterday rows', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_date_yest` + const yesterdayValue = getDateValue(1) + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + created_at date + )` + ) + await query( + `INSERT INTO ${tableName} (created_at) VALUES + (CURRENT_DATE), + (CURRENT_DATE - INTERVAL '1 day'), + (CURRENT_DATE - INTERVAL '7 days')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'created_at', '=', yesterdayValue) + + const rows = page.locator('[role="row"]') + await expect(rows).toHaveCount(2) + } finally { + await dropTable(tableName) + } + }) + + test('filtering by Last 7 days with greater or equal operator', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_date_7d` + const last7DaysValue = getDateValue(7) + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + created_at date + )` + ) + await query( + `INSERT INTO ${tableName} (created_at) VALUES + (CURRENT_DATE), + (CURRENT_DATE - INTERVAL '3 days'), + (CURRENT_DATE - INTERVAL '7 days'), + (CURRENT_DATE - INTERVAL '30 days')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'created_at', '>=', last7DaysValue) + + const rows = page.locator('[role="row"]') + await expect(rows).toHaveCount(4) + } finally { + await dropTable(tableName) + } + }) + + test('date less than operator filters correctly', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_date_lt` + const todayValue = getDateValue(0) + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + created_at date + )` + ) + await query( + `INSERT INTO ${tableName} (created_at) VALUES + (CURRENT_DATE), + (CURRENT_DATE - INTERVAL '1 day'), + (CURRENT_DATE - INTERVAL '7 days')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'created_at', '<', todayValue) + + const rows = page.locator('[role="row"]') + await expect(rows).toHaveCount(3) + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Timestamp Column Filters', () => { + test('timestamp column shows date preset dropdown options', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_ts_today` + const todayValue = getDateValue(0) + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + created_at timestamp + )` + ) + await query( + `INSERT INTO ${tableName} (created_at) VALUES + (NOW()), + (NOW() - INTERVAL '1 day'), + (NOW() - INTERVAL '7 days')` + ) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await addFilterWithDropdownValue(page, ref, 'created_at', '>=', todayValue) + + const rows = page.locator('[role="row"]') + await expect(rows).toHaveCount(2) + } finally { + await dropTable(tableName) + } + }) + }) + + test.describe('Keyboard Navigation - Dropdown Values', () => { + test('ArrowDown/Enter navigates and selects dropdown value option', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_kb_dropdown` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + is_active boolean + )` + ) + await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false)`) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, 'is_active') + await selectOperator(page, 'is_active', '=') + + // Navigate to 'false' option (second item) and select with Enter + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + + await expect(page.getByTestId('filter-condition-is_active')).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + + test('Tab selects dropdown value option', async ({ page, ref }) => { + const tableName = `${tableNamePrefix}_kb_tab` + + await query( + `CREATE TABLE IF NOT EXISTS ${tableName} ( + id bigint generated by default as identity primary key, + is_active boolean + )` + ) + await query(`INSERT INTO ${tableName} (is_active) VALUES (true), (false)`) + + try { + await setupFilterBarPage(page, ref, toUrl(`/project/${ref}/editor?schema=public`)) + await navigateToTable(page, ref, tableName) + + await selectColumnFilter(page, 'is_active') + await selectOperator(page, 'is_active', '=') + + await page.keyboard.press('Tab') + + await expect(page.getByTestId('filter-condition-is_active')).toBeVisible() + } finally { + await dropTable(tableName) + } + }) + }) +}) diff --git a/e2e/studio/utils/filter-bar-helpers.ts b/e2e/studio/utils/filter-bar-helpers.ts new file mode 100644 index 00000000000..8a9edbbc190 --- /dev/null +++ b/e2e/studio/utils/filter-bar-helpers.ts @@ -0,0 +1,84 @@ +import { expect, Page } from '@playwright/test' + +import { createApiResponseWaiter, waitForTableToLoad } from './wait-for-response.js' + +const FILTER_BAR_KEY = 'supabase-ui-table-filter-bar' + +export async function enableFilterBar(page: Page) { + await page.evaluate((key) => { + localStorage.setItem(key, 'true') + }, FILTER_BAR_KEY) +} + +export function getFilterBarInput(page: Page) { + return page.getByTestId('filter-bar-freeform-input') +} + +export async function navigateToTable(page: Page, ref: string, tableName: string) { + const gridWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-') + await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click() + await page.waitForURL(/\/editor\/\d+\?schema=public$/) + await gridWaiter +} + +export async function selectColumnFilter(page: Page, columnName: string) { + const freeformInput = getFilterBarInput(page) + await freeformInput.click() + await freeformInput.fill(columnName) + await expect(page.getByTestId(`filter-menu-item-${columnName}`)).toBeVisible() + await page.keyboard.press('Enter') + await expect(page.getByTestId(`filter-operator-${columnName}`)).toBeFocused() +} + +export async function selectOperator(page: Page, columnName: string, operator: string) { + await expect(page.getByTestId(`filter-menu-item-${operator}`)).toBeVisible() + await page.keyboard.press('Enter') + await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused() +} + +export async function addFilter( + page: Page, + ref: string, + columnName: string, + operator: string, + value: string +) { + await selectColumnFilter(page, columnName) + await selectOperator(page, columnName, operator) + const rowsWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-') + const valueInput = page.getByTestId(`filter-value-${columnName}`) + await valueInput.fill(value) + await page.keyboard.press('Enter') + await rowsWaiter +} + +export async function selectOperatorByClick(page: Page, columnName: string, operator: string) { + await expect(page.getByTestId(`filter-menu-item-${operator}`)).toBeVisible() + await page.getByTestId(`filter-menu-item-${operator}`).click() + await expect(page.getByTestId(`filter-value-${columnName}`)).toBeFocused() +} + +export async function addFilterWithDropdownValue( + page: Page, + ref: string, + columnName: string, + operator: string, + optionValue: string +) { + await selectColumnFilter(page, columnName) + await selectOperatorByClick(page, columnName, operator) + const rowsWaiter = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-') + await expect(page.getByTestId(`filter-menu-item-${optionValue}`)).toBeVisible() + await page.getByTestId(`filter-menu-item-${optionValue}`).click() + await rowsWaiter +} + +export async function setupFilterBarPage(page: Page, ref: string, editorUrl: string) { + const loadPromise = waitForTableToLoad(page, ref) + await page.goto(editorUrl) + await loadPromise + await enableFilterBar(page) + const reloadPromise = waitForTableToLoad(page, ref) + await page.reload({ waitUntil: 'networkidle' }) + await reloadPromise +} diff --git a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx index d5f42c4a5bd..38b2e9e88bb 100644 --- a/packages/ui-patterns/src/FilterBar/CommandListItem.tsx +++ b/packages/ui-patterns/src/FilterBar/CommandListItem.tsx @@ -29,6 +29,7 @@ export function CommandListItem({ isHighlighted && 'bg-surface-300', !isHighlighted && 'hover:bg-surface-200' )} + data-testid={`filter-menu-item-${item.value}`} > {includeIcon && item.icon} diff --git a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx index 7c685c437fd..f94ea8626db 100644 --- a/packages/ui-patterns/src/FilterBar/FilterCondition.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterCondition.tsx @@ -222,6 +222,8 @@ export function FilterCondition({ variant === 'pill' ? 'rounded border' : 'border-r', isHighlighted && 'ring-2 ring-primary' )} + data-testid={`filter-condition-${property.name}`} + data-highlighted={isHighlighted} > {condition.operator || ' '} @@ -285,6 +288,7 @@ export function FilterCondition({ className="h-full border-none bg-transparent py-0 px-1 text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 w-full absolute left-0 top-0" disabled={isLoading} aria-label={`Value for ${property.label}`} + data-testid={`filter-value-${property.name}`} /> {localValue || ' '} @@ -343,6 +347,7 @@ export function FilterCondition({ className="group hover:text-foreground hover:!bg-surface-600 rounded-none px-1 h-auto py-0" aria-label={`Remove ${property.label} filter`} tabIndex={-1} + data-testid={`filter-remove-${property.name}`} /> ) diff --git a/packages/ui-patterns/src/FilterBar/FilterGroup.tsx b/packages/ui-patterns/src/FilterBar/FilterGroup.tsx index 310708e9627..c256effe0c1 100644 --- a/packages/ui-patterns/src/FilterBar/FilterGroup.tsx +++ b/packages/ui-patterns/src/FilterBar/FilterGroup.tsx @@ -195,6 +195,7 @@ export function FilterGroup({ group, path }: FilterGroupProps) { group.conditions.length === 0 ? emptyPlaceholder : 'Add more filters...' } disabled={isLoading} + data-testid="filter-bar-freeform-input" /> ) : (