chore - make index advisor e2e tests faster (#43571)

## Problem

Index advisor tests are slow because they install the extension via UI

## Solution

Use an SQL query to enable the extension so that we can just test its
features
This commit is contained in:
Gildas Garcia authored and GitHub committed 2026-03-13 15:57:59 +01:00
1 parent c5b6695380
commit 7fa983a3ea
1 file changed
+57 -421
+57 -421
View File
@@ -1,437 +1,73 @@
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'
import { expect } from '@playwright/test'
/**
* 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)
*/
import { query } from '../utils/db/client.js'
import { createTable, dropTable } from '../utils/db/queries.js'
import { releaseFileOnceCleanup, withFileOnceSetup } from '../utils/once-per-file.js'
import { test, withSetupCleanup } from '../utils/test.js'
import { toUrl } from '../utils/to-url.js'
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('Index Advisor', () => {
test.beforeAll(async () => {
await withFileOnceSetup(import.meta.url, async () => {
await query(`
create schema if not exists extensions;
create extension if not exists hypopg schema extensions cascade;
create extension if not exists index_advisor schema extensions cascade;
`)
})
})
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 Index Advisor 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 Index Advisor button to appear
// Extensions may need time to initialize, so use a generous timeout
const indexAdvisorButton = page.getByRole('button', { name: /Index Advisor/i })
await expect(
indexAdvisorButton,
'Index Advisor filter button should be visible when Index Advisor is enabled'
).toBeVisible({ timeout: 30000 })
console.log('✓ Index Advisor button is now visible')
})
test.afterAll(async () => {
await releaseFileOnceCleanup(import.meta.url)
})
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')
test('should show Index Advisor filter after Index Advisor is enabled', async ({ page, ref }) => {
// Go to Query Performance page
await page.goto(toUrl(`/project/${ref}/observability/query-performance`))
await page.waitForLoadState('networkidle')
// Wait patiently for the Index Advisor button to appear
// Extensions may need time to initialize, so use a generous timeout
const indexAdvisorButton = page.getByRole('button', { name: /Index Advisor/i })
await expect(
indexAdvisorButton,
'Index Advisor filter button should be visible when Index Advisor is enabled'
).toBeVisible({ timeout: 30000 })
})
test('should show Index Advisor warnings in Query Performance', async ({ page, ref }) => {
await using _ = await withSetupCleanup(
async () => {
await createTable(TEST_TABLE_NAME, 'name', [
{
name: 'test',
},
{
name: 'demo',
},
{
name: 'test',
},
])
},
async () => {
await dropTable(TEST_TABLE_NAME)
}
)
await createTestTable(page, ref)
// Run query that need indexes
await query(`SELECT * FROM ${TEST_TABLE_NAME} WHERE name = 'test';`)
// Verify table was created by checking table editor
await page.goto(toUrl(`/project/${ref}/editor`))
await page.waitForLoadState('networkidle')
// Navigate to Query Performance page
await page.goto(toUrl(`/project/${ref}/observability/query-performance`))
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 for Index Advisor button and click to enable the filter
const indexAdvisorButton = page.getByRole('button', { name: /Index Advisor/i })
await expect(indexAdvisorButton, 'Index Advisor filter should be visible').toBeVisible({
timeout: 30000,
})
await indexAdvisorButton.click()
// 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'
)
}
// Wait for Index Advisor button and click to enable the filter
const indexAdvisorButton = page.getByRole('button', { name: /Index Advisor/i })
await expect(indexAdvisorButton, 'Index Advisor filter should be visible').toBeVisible({
timeout: 30000,
})
})
})