mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 21:05:06 +03:00
* feat: initial index advisor one shot setup with cursor * feat: more tests to check against * fix: few selector and await checks * fix: no flaky me likey * fix: no type errors me likely * chore: remove tests for index advisor banner --------- Co-authored-by: Ali Waseem <waseema393@gmail.com>
462 lines
16 KiB
TypeScript
462 lines
16 KiB
TypeScript
import { expect, Page } from '@playwright/test'
|
|
import { env } from '../env.config.js'
|
|
import { test } from '../utils/test.js'
|
|
import { toUrl } from '../utils/to-url.js'
|
|
import { waitForApiResponse } from '../utils/wait-for-response.js'
|
|
|
|
/**
|
|
* Index Advisor E2E Tests
|
|
*
|
|
* Tests the Index Advisor functionality by:
|
|
* 1. Checking if extensions are available
|
|
* 2. Enabling Index Advisor via Database > Extensions page
|
|
* 3. Verifying the Warnings filter appears after enabling
|
|
* 4. Creating test tables and running queries
|
|
* 5. Checking that Index Advisor warnings appear
|
|
*
|
|
* Anti-flakiness measures implemented:
|
|
* - Replaced `waitForTimeout()` with proper element/API waits
|
|
* - Added explicit waits for search results to update after input
|
|
* - Wait for API responses when enabling/disabling extensions
|
|
* - Defensive state checking with `refreshIndexAdvisorState()` helper
|
|
* - Cleanup test table in afterAll to prevent interference with subsequent runs
|
|
* - Use longer timeouts for operations that depend on extension initialization
|
|
* - Graceful handling of optional UI elements (save buttons)
|
|
* - All API waits wrapped in try-catch to handle timeouts gracefully
|
|
* - API response waiters set up BEFORE triggering actions (critical for extension enabling)
|
|
*/
|
|
|
|
const TEST_TABLE_NAME = 'pw_test_index_advisor'
|
|
|
|
/**
|
|
* Helper function to check Index Advisor extension status via Database > Extensions page
|
|
* Returns true if both index_advisor and hypopg extensions are enabled
|
|
*/
|
|
async function checkIndexAdvisorExtensionsEnabled(page: Page, ref: string): Promise<boolean> {
|
|
// Navigate to Database > Extensions page
|
|
await page.goto(toUrl(`/project/${ref}/database/extensions`))
|
|
|
|
try {
|
|
await waitForApiResponse(page, 'pg-meta', ref, 'extensions')
|
|
} catch (e) {
|
|
console.warn('Extensions API timeout, continuing anyway:', (e as Error).message)
|
|
// Wait for page to be in a stable state
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
const searchInput = page.getByPlaceholder('Search for an extension')
|
|
|
|
// Search for index_advisor extension
|
|
await searchInput.clear()
|
|
await searchInput.fill('index_advisor')
|
|
|
|
// Wait for search results to update
|
|
const indexAdvisorRow = page.getByRole('row', { name: /index_advisor/i }).first()
|
|
await indexAdvisorRow.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {})
|
|
|
|
const indexAdvisorExists = (await indexAdvisorRow.count()) > 0
|
|
|
|
if (!indexAdvisorExists) {
|
|
return false
|
|
}
|
|
|
|
// Check if index_advisor is enabled (switch is checked)
|
|
const indexAdvisorSwitch = indexAdvisorRow.getByRole('switch')
|
|
const isIndexAdvisorEnabled = await indexAdvisorSwitch.isChecked()
|
|
|
|
// Search for hypopg extension
|
|
await searchInput.clear()
|
|
await searchInput.fill('hypopg')
|
|
|
|
// Wait for search results to update
|
|
const hypopgRow = page.getByRole('row', { name: /hypopg/i }).first()
|
|
await hypopgRow.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {})
|
|
|
|
const hypopgExists = (await hypopgRow.count()) > 0
|
|
|
|
if (!hypopgExists) {
|
|
return false
|
|
}
|
|
|
|
// Check if hypopg is enabled (switch is checked)
|
|
const hypopgSwitch = hypopgRow.getByRole('switch')
|
|
const isHypopgEnabled = await hypopgSwitch.isChecked()
|
|
|
|
// Clear search
|
|
await searchInput.clear()
|
|
|
|
return isIndexAdvisorEnabled && isHypopgEnabled
|
|
}
|
|
|
|
/**
|
|
* Helper function to enable Index Advisor via Database > Extensions page
|
|
*/
|
|
async function enableIndexAdvisorViaExtensions(page: Page, ref: string): Promise<void> {
|
|
await page.goto(toUrl(`/project/${ref}/database/extensions`))
|
|
|
|
try {
|
|
await waitForApiResponse(page, 'pg-meta', ref, 'extensions')
|
|
} catch (e) {
|
|
console.warn('Extensions API timeout, continuing anyway:', (e as Error).message)
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
const searchInput = page.getByPlaceholder('Search for an extension')
|
|
|
|
// Enable hypopg first
|
|
await searchInput.clear()
|
|
await searchInput.fill('hypopg')
|
|
|
|
const hypopgRow = page.getByRole('row', { name: /hypopg/i }).first()
|
|
await hypopgRow.waitFor({ state: 'visible', timeout: 5000 })
|
|
|
|
const hypopgSwitch = hypopgRow.getByRole('switch')
|
|
const isHypopgEnabled = await hypopgSwitch.isChecked()
|
|
|
|
if (!isHypopgEnabled) {
|
|
await hypopgSwitch.click()
|
|
|
|
// Wait for modal and click Enable extension button
|
|
const enableButton = page.getByRole('button', { name: 'Enable extension' })
|
|
await enableButton.waitFor({ state: 'visible', timeout: 5000 })
|
|
|
|
// Set up API wait before clicking (must be before the action that triggers it)
|
|
const extensionEnableWait = waitForApiResponse(page, 'pg-meta', ref, 'extensions', {
|
|
method: 'POST',
|
|
})
|
|
await enableButton.click()
|
|
|
|
// Wait for API response with timeout handling
|
|
try {
|
|
await extensionEnableWait
|
|
} catch (e) {
|
|
console.warn('Extension enable API timeout:', (e as Error).message)
|
|
}
|
|
|
|
// Wait for switch to become checked (indicates extension is enabled)
|
|
await expect(hypopgSwitch).toBeChecked({ timeout: 30000 })
|
|
}
|
|
|
|
// Enable index_advisor
|
|
await searchInput.clear()
|
|
await searchInput.fill('index_advisor')
|
|
|
|
const indexAdvisorRow = page.getByRole('row', { name: /index_advisor/i }).first()
|
|
await indexAdvisorRow.waitFor({ state: 'visible', timeout: 5000 })
|
|
|
|
const indexAdvisorSwitch = indexAdvisorRow.getByRole('switch')
|
|
const isIndexAdvisorEnabled = await indexAdvisorSwitch.isChecked()
|
|
|
|
if (!isIndexAdvisorEnabled) {
|
|
await indexAdvisorSwitch.click()
|
|
|
|
// Wait for modal and click Enable extension button
|
|
const enableButton = page.getByRole('button', { name: 'Enable extension' })
|
|
await enableButton.waitFor({ state: 'visible', timeout: 5000 })
|
|
|
|
// Set up API wait before clicking (must be before the action that triggers it)
|
|
const extensionEnableWait = waitForApiResponse(page, 'pg-meta', ref, 'extensions', {
|
|
method: 'POST',
|
|
})
|
|
await enableButton.click()
|
|
|
|
// Wait for API response with timeout handling
|
|
try {
|
|
await extensionEnableWait
|
|
} catch (e) {
|
|
console.warn('Extension enable API timeout:', (e as Error).message)
|
|
}
|
|
|
|
// Wait for switch to become checked (indicates extension is enabled)
|
|
await expect(indexAdvisorSwitch).toBeChecked({ timeout: 30000 })
|
|
}
|
|
|
|
// Clear search
|
|
await searchInput.clear()
|
|
}
|
|
|
|
/**
|
|
* Helper function to create test table for Index Advisor verification
|
|
*/
|
|
async function createTestTable(page: Page, ref: string): Promise<void> {
|
|
await page.goto(toUrl(`/project/${ref}/sql/new`))
|
|
|
|
// Wait for SQL editor to be ready
|
|
await expect(page.getByText('Loading...')).not.toBeVisible({ timeout: 10000 })
|
|
|
|
// Click into the editor and type SQL
|
|
await page.locator('.view-lines').click()
|
|
await page.keyboard.press('ControlOrMeta+KeyA')
|
|
|
|
const createTableSQL = `CREATE TABLE ${TEST_TABLE_NAME} (id int, name text);
|
|
INSERT INTO ${TEST_TABLE_NAME} VALUES (1, 'test'), (2, 'demo'), (3, 'test');`
|
|
|
|
await page.keyboard.type(createTableSQL)
|
|
|
|
// Run the query and wait for response
|
|
const sqlMutationPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
|
|
method: 'POST',
|
|
})
|
|
await page.getByTestId('sql-run-button').click()
|
|
|
|
try {
|
|
await sqlMutationPromise
|
|
} catch (e) {
|
|
console.warn('SQL execution API timeout:', (e as Error).message)
|
|
// Wait a bit for the query to potentially complete
|
|
await page.waitForTimeout(2000)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to run query that would benefit from an index
|
|
*/
|
|
async function runQueryNeedingIndex(page: Page, ref: string): Promise<void> {
|
|
await page.goto(toUrl(`/project/${ref}/sql/new`))
|
|
|
|
// Wait for SQL editor to be ready
|
|
await expect(page.getByText('Loading...')).not.toBeVisible({ timeout: 10000 })
|
|
|
|
// Click into the editor and type SQL
|
|
await page.locator('.view-lines').click()
|
|
await page.keyboard.press('ControlOrMeta+KeyA')
|
|
|
|
const querySQL = `SELECT * FROM ${TEST_TABLE_NAME} WHERE name = 'test';`
|
|
await page.keyboard.type(querySQL)
|
|
|
|
// Run the query multiple times to ensure it shows up in Query Performance
|
|
for (let i = 0; i < 3; i++) {
|
|
const sqlMutationPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
|
|
method: 'POST',
|
|
})
|
|
await page.getByTestId('sql-run-button').click()
|
|
|
|
try {
|
|
await sqlMutationPromise
|
|
} catch (e) {
|
|
console.warn(`SQL execution API timeout on attempt ${i + 1}:`, (e as Error).message)
|
|
await page.waitForTimeout(1000)
|
|
}
|
|
}
|
|
}
|
|
|
|
test.describe.serial('Index Advisor', () => {
|
|
let page: Page
|
|
let isIndexAdvisorEnabled: boolean
|
|
|
|
test.beforeAll(async ({ browser, ref }) => {
|
|
page = await browser.newPage()
|
|
|
|
// Check if Index Advisor is already enabled via Extensions page
|
|
isIndexAdvisorEnabled = await checkIndexAdvisorExtensionsEnabled(page, ref)
|
|
|
|
console.log(`[beforeAll] Index Advisor enabled status: ${isIndexAdvisorEnabled}`)
|
|
})
|
|
|
|
// Helper to refresh the enabled state
|
|
async function refreshIndexAdvisorState(ref: string): Promise<void> {
|
|
const currentState = await checkIndexAdvisorExtensionsEnabled(page, ref)
|
|
if (currentState !== isIndexAdvisorEnabled) {
|
|
console.log(`State changed from ${isIndexAdvisorEnabled} to ${currentState}`)
|
|
isIndexAdvisorEnabled = currentState
|
|
}
|
|
}
|
|
|
|
test.afterAll(async ({ ref }) => {
|
|
// Clean up test table if it exists
|
|
try {
|
|
await page.goto(toUrl(`/project/${ref}/sql/new`))
|
|
await expect(page.getByText('Loading...')).not.toBeVisible({ timeout: 10000 })
|
|
|
|
await page.locator('.view-lines').click()
|
|
await page.keyboard.press('ControlOrMeta+KeyA')
|
|
await page.keyboard.type(`DROP TABLE IF EXISTS ${TEST_TABLE_NAME};`)
|
|
|
|
const sqlMutationPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', {
|
|
method: 'POST',
|
|
})
|
|
await page.getByTestId('sql-run-button').click()
|
|
|
|
try {
|
|
await sqlMutationPromise
|
|
} catch (e) {
|
|
console.warn('Cleanup SQL timeout:', (e as Error).message)
|
|
}
|
|
} catch (e) {
|
|
console.log('Failed to clean up test table:', (e as Error).message)
|
|
}
|
|
|
|
await page.close()
|
|
})
|
|
|
|
test.describe('Check Extension Status', () => {
|
|
test('should check Index Advisor extensions via Database > Extensions page', async ({
|
|
ref,
|
|
}) => {
|
|
await page.goto(toUrl(`/project/${ref}/database/extensions`))
|
|
|
|
try {
|
|
await waitForApiResponse(page, 'pg-meta', ref, 'extensions')
|
|
} catch (e) {
|
|
console.warn('Extensions API timeout, continuing anyway:', (e as Error).message)
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {})
|
|
}
|
|
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Database Extensions' }),
|
|
'Database Extensions heading should be visible'
|
|
).toBeVisible()
|
|
|
|
const searchInput = page.getByPlaceholder('Search for an extension')
|
|
|
|
// Search for index_advisor
|
|
await searchInput.clear()
|
|
await searchInput.fill('index_advisor')
|
|
|
|
const indexAdvisorRow = page.getByRole('row', { name: /index_advisor/i }).first()
|
|
await expect(indexAdvisorRow, 'index_advisor extension should be in the list').toBeVisible({
|
|
timeout: 5000,
|
|
})
|
|
|
|
// Search for hypopg
|
|
await searchInput.clear()
|
|
await searchInput.fill('hypopg')
|
|
|
|
const hypopgRow = page.getByRole('row', { name: /hypopg/i }).first()
|
|
await expect(hypopgRow, 'hypopg extension should be in the list').toBeVisible({
|
|
timeout: 5000,
|
|
})
|
|
|
|
// Clear search
|
|
await searchInput.clear()
|
|
})
|
|
})
|
|
|
|
test.describe('Enable Index Advisor', () => {
|
|
test('should enable Index Advisor via Database > Extensions page', async ({ ref }) => {
|
|
// Refresh state to ensure it's current
|
|
await refreshIndexAdvisorState(ref)
|
|
|
|
if (isIndexAdvisorEnabled) {
|
|
test.skip(true, 'Index Advisor is already enabled')
|
|
}
|
|
|
|
await enableIndexAdvisorViaExtensions(page, ref)
|
|
|
|
// Verify both extensions are now enabled and update state
|
|
await refreshIndexAdvisorState(ref)
|
|
expect(isIndexAdvisorEnabled, 'Both extensions should be enabled after enabling').toBe(true)
|
|
})
|
|
|
|
test('should show Warnings filter after Index Advisor is enabled', async ({ ref }) => {
|
|
if (!isIndexAdvisorEnabled) {
|
|
test.skip(true, 'Index Advisor needs to be enabled first')
|
|
}
|
|
|
|
// Go to Query Performance page
|
|
await page.goto(toUrl(`/project/${ref}/observability/query-performance`))
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Wait patiently for the Warnings button to appear
|
|
// Extensions may need time to initialize, so use a generous timeout
|
|
const warningsButton = page.getByText('Warnings', { exact: true })
|
|
await expect(
|
|
warningsButton,
|
|
'Warnings filter button should be visible when Index Advisor is enabled'
|
|
).toBeVisible({ timeout: 30000 })
|
|
|
|
console.log('✓ Warnings button is now visible')
|
|
})
|
|
})
|
|
|
|
test.describe('Index Advisor Functionality', () => {
|
|
test('should create test table without indexes', async ({ ref }) => {
|
|
if (!isIndexAdvisorEnabled) {
|
|
test.skip(true, 'Index Advisor needs to be enabled first')
|
|
}
|
|
|
|
await createTestTable(page, ref)
|
|
|
|
// Verify table was created by checking table editor
|
|
await page.goto(toUrl(`/project/${ref}/editor`))
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Use the search box to find the table
|
|
const searchBox = page.getByPlaceholder('Search tables...')
|
|
await searchBox.fill(TEST_TABLE_NAME)
|
|
|
|
// Verify the table appears in filtered results
|
|
const tableButton = page.getByRole('button', { name: `View ${TEST_TABLE_NAME}` })
|
|
await expect(tableButton, 'Test table should be visible in table editor').toBeVisible({
|
|
timeout: 10000,
|
|
})
|
|
})
|
|
|
|
test('should run queries that need indexes', async ({ ref }) => {
|
|
if (!isIndexAdvisorEnabled) {
|
|
test.skip(true, 'Index Advisor needs to be enabled first')
|
|
}
|
|
|
|
await runQueryNeedingIndex(page, ref)
|
|
})
|
|
|
|
test('should show Index Advisor warnings in Query Performance', async ({ ref }) => {
|
|
if (!isIndexAdvisorEnabled) {
|
|
test.skip(true, 'Index Advisor needs to be enabled first')
|
|
}
|
|
|
|
// Navigate to Query Performance page
|
|
await page.goto(toUrl(`/project/${ref}/observability/query-performance`))
|
|
await page.waitForLoadState('networkidle')
|
|
|
|
// Wait patiently for Warnings button
|
|
const warningsButton = page.getByText('Warnings', { exact: true }).first()
|
|
await expect(warningsButton, 'Warnings filter should be visible').toBeVisible({
|
|
timeout: 30000,
|
|
})
|
|
await warningsButton.click()
|
|
|
|
// Wait for popover to open by checking for the Index Advisor label
|
|
const indexAdvisorLabel = page.getByText('Index Advisor', { exact: true }).first()
|
|
await expect(
|
|
indexAdvisorLabel,
|
|
'Index Advisor option should be in Warnings popover'
|
|
).toBeVisible({ timeout: 5000 })
|
|
|
|
// Click the label to select Index Advisor filter
|
|
await indexAdvisorLabel.click()
|
|
|
|
// Find and click the "Save" button if it exists
|
|
const saveButton = page.getByRole('button', { name: 'Save' }).first()
|
|
|
|
try {
|
|
await saveButton.waitFor({ state: 'visible', timeout: 2000 })
|
|
await saveButton.click()
|
|
// Wait for the popover to close
|
|
await saveButton.waitFor({ state: 'hidden', timeout: 3000 })
|
|
} catch (e) {
|
|
// If no Save button, the filter might apply automatically
|
|
// Close the popover by pressing Escape
|
|
await page.keyboard.press('Escape')
|
|
}
|
|
|
|
// Check if any rows with Index Advisor warnings appear in the results
|
|
// Look for rows in the query performance grid (excluding header)
|
|
const queryRows = page.locator('[role="row"]').nth(1)
|
|
|
|
// Give it some time for the filtered results to appear
|
|
try {
|
|
await expect(
|
|
queryRows,
|
|
'At least one query with Index Advisor warning should be visible'
|
|
).toBeVisible({ timeout: 10000 })
|
|
console.log('Successfully found queries with Index Advisor recommendations')
|
|
} catch (e) {
|
|
console.log(
|
|
'No queries with Index Advisor warnings found - may need more query executions or time'
|
|
)
|
|
}
|
|
})
|
|
})
|
|
})
|