mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
7fba97517c
commit
1b319f8856
5 files changed
+904
No files matched your search
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -29,6 +29,7 @@ export function CommandListItem({
|
||||
isHighlighted && 'bg-surface-300',
|
||||
!isHighlighted && 'hover:bg-surface-200'
|
||||
)}
|
||||
data-testid={`filter-menu-item-${item.value}`}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{includeIcon && item.icon}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<span
|
||||
className="text-xs pl-2 pr-1 cursor-pointer shrink-0 whitespace-nowrap text-foreground-light h-full flex items-center"
|
||||
@@ -243,6 +245,7 @@ export function FilterCondition({
|
||||
className="h-full border-none bg-transparent py-0 px-1 text-center text-xs focus:outline-none focus:ring-0 focus:shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 text-brand w-full absolute left-0 top-0"
|
||||
disabled={isLoading}
|
||||
aria-label={`Operator for ${property.label}`}
|
||||
data-testid={`filter-operator-${property.name}`}
|
||||
/>
|
||||
<span className="invisible whitespace-pre text-xs block px-1 shrink-0 px-1">
|
||||
{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}`}
|
||||
/>
|
||||
<span className="invisible whitespace-pre text-xs block px-1">{localValue || ' '}</span>
|
||||
</div>
|
||||
@@ -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}`}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
) : (
|
||||
<div className="relative inline-block">
|
||||
|
||||
Reference in new issue
Block a user