From 913371ca345352372b6431601b2844b5dc3e73f6 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Wed, 10 Dec 2025 13:02:00 -0700 Subject: [PATCH] fix: remove flakiness with launch options and default timeouts (#41245) * attempt to make table editor tests less flaky * updated race conditions for table editor * updated playwright config with optimized launch options * reduce workers * updated timeouts for tests * updated config to have a global timeout of 20 seconds * updated to be visible --- e2e/studio/features/logs.spec.ts | 9 +++- e2e/studio/features/storage.spec.ts | 4 +- e2e/studio/features/table-editor.spec.ts | 50 ++++++++++++++---- e2e/studio/playwright.config.ts | 66 ++++++++++++++++++++++++ e2e/studio/utils/storage-helpers.ts | 8 +-- 5 files changed, 120 insertions(+), 17 deletions(-) diff --git a/e2e/studio/features/logs.spec.ts b/e2e/studio/features/logs.spec.ts index dc33eed1573..5931bf4ac94 100644 --- a/e2e/studio/features/logs.spec.ts +++ b/e2e/studio/features/logs.spec.ts @@ -60,7 +60,7 @@ test.describe('Logs', () => { await expect(logsTable, { message: 'Logs table should be visible', - }).toBeVisible({ timeout: 20000 }) + }).toBeVisible() /** * Shows the logs data without errors @@ -73,10 +73,15 @@ test.describe('Logs', () => { * Can select and view log details */ const gridcells = page.getByText('Random event message') + await expect(gridcells, { + message: 'Grid cell should be visible before clicking', + }).toBeVisible() await gridcells.click() const tabPanel = page.getByTestId('log-selection') - await expect(tabPanel).toBeVisible() + await expect(tabPanel, { + message: 'Log selection panel should be visible after clicking a log', + }).toBeVisible() // Assert known fixed values instead of extracting text await expect(tabPanel, { diff --git a/e2e/studio/features/storage.spec.ts b/e2e/studio/features/storage.spec.ts index 66933bd65d6..e625ef5b882 100644 --- a/e2e/studio/features/storage.spec.ts +++ b/e2e/studio/features/storage.spec.ts @@ -59,7 +59,7 @@ test.describe.serial('Storage', () => { await expect( bucketRow.getByText('Public', { exact: true }), 'Bucket should be marked as Public' - ).toBeVisible({ timeout: 10000 }) + ).toBeVisible() }) test('can edit bucket settings', async ({ page, ref }) => { @@ -91,7 +91,7 @@ test.describe.serial('Storage', () => { await expect( page.getByText('Public').first(), 'Bucket should now be marked as Public' - ).toBeVisible({ timeout: 10_000 }) + ).toBeVisible() }) test('can delete a bucket', async ({ page, ref }) => { diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index 582e91000b0..d4e3445ee92 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -8,6 +8,7 @@ import { test } from '../utils/test.js' import { toUrl } from '../utils/to-url.js' import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout.js' import { + createApiResponseWaiter, waitForApiResponse, waitForGridDataToLoad, waitForTableToLoad, @@ -72,7 +73,7 @@ const createTable = async (page: Page, ref: string, tableName: string) => { await expect( page.getByRole('button', { name: `View ${tableName}`, exact: true }), 'Table should be visible after creation' - ).toBeVisible({ timeout: 15_000 }) + ).toBeVisible() } const deleteTable = async (page: Page, ref: string, tableName: string) => { @@ -493,8 +494,14 @@ testRunner('table editor', () => { await page.getByRole('button', { name: 'id', exact: true }).click() await page.getByRole('menuitem', { name: colName }).click() await page.getByRole('textbox', { name: 'Enter a value' }).fill('789') + const waitForFilterApply = createApiResponseWaiter( + page, + 'pg-meta', + ref, + 'query?key=table-rows-' + ) await page.getByRole('button', { name: 'Apply filter' }).click() - await waitForGridDataToLoad(page, ref) + await waitForFilterApply await page.waitForTimeout(500) await page.getByRole('button', { name: 'Filtered by 1 rule' }).click({ force: true }) await expect(page.getByRole('gridcell', { name: '789' })).toBeVisible() @@ -551,8 +558,14 @@ testRunner('table editor', () => { await page.getByRole('button', { name: 'Sort', exact: true }).click() await page.getByRole('button', { name: 'Pick a column to sort by' }).click() await page.getByRole('menuitem', { name: colName }).click() + const waitForSortingApply = createApiResponseWaiter( + page, + 'pg-meta', + ref, + 'query?key=table-rows-' + ) await page.getByRole('button', { name: 'Apply sorting' }).click() - await waitForGridDataToLoad(page, ref) + await waitForSortingApply await page.getByRole('button', { name: 'Sorted by 1 rule' }).click() // Verify sorted row content asc lexicographically for strings @@ -580,8 +593,11 @@ testRunner('table editor', () => { await page.getByRole('tab', { name: 'Upload CSV' }).click() await page.setInputFiles('input[type="file"]', csvFilePath) await expect(page.getByText('A total of 50 rows will be')).toBeVisible() + const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) await page.getByRole('button', { name: 'Import data' }).click() - await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // insert data + await waitForCsvInsert // insert data await waitForGridDataToLoad(page, ref) // retrieve updated data await expect(page.getByText('50 records')).toBeVisible() @@ -593,23 +609,33 @@ testRunner('table editor', () => { await page.getByRole('tab', { name: 'Paste text' }).click() await page.getByRole('textbox').fill(fileContent) await expect(page.getByText('A total of 51 rows will be')).toBeVisible() + const waitForPasteInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) await page.getByRole('button', { name: 'Import data' }).click() - await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // insert data + await waitForPasteInsert // insert data await waitForGridDataToLoad(page, ref) // retrieve updated data await expect(page.getByText('101 records')).toBeVisible() // test pagination (page 1 -> page 2) await expect(page.getByRole('gridcell', { name: 'value 7', exact: true })).toBeVisible() await expect(page.getByRole('gridcell', { name: 'value 101', exact: true })).not.toBeVisible() + const waitForPageChange = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=table-rows-') await page.getByLabel('Table grid footer').getByRole('button').nth(1).click() - await waitForGridDataToLoad(page, ref) // retrieve next page data + await waitForPageChange // retrieve next page data await expect(page.getByRole('gridcell', { name: 'value 7', exact: true })).not.toBeVisible() await expect(page.getByRole('gridcell', { name: 'value 101', exact: true })).toBeVisible() // change pagination size (100 -> 500) await page.getByRole('button', { name: 'rows' }).click() + const waitForPaginationChange = createApiResponseWaiter( + page, + 'pg-meta', + ref, + 'query?key=table-rows-' + ) await page.getByRole('menuitem', { name: '500 rows' }).click() - await waitForGridDataToLoad(page, ref) // retrieve updated pagination size data + await waitForPaginationChange // retrieve updated pagination size data await expect(page.getByRole('gridcell', { name: 'value 7', exact: true })).toBeVisible() await page.getByRole('grid').evaluate((element) => { element.scrollTop = element.scrollHeight @@ -625,8 +651,11 @@ testRunner('table editor', () => { await page.getByRole('row', { name: 'value 3 to delete' }).getByRole('checkbox').click() await page.getByRole('button', { name: 'Delete 3 rows' }).click() await expect(page.getByText('delete the selected 3 rows')).toBeVisible() + const waitForDeleteRows = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) await page.getByRole('button', { name: 'Delete' }).click() - await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // delete selected rows + await waitForDeleteRows // delete selected rows await waitForGridDataToLoad(page, ref) // retrieve row data // export selected rows when multiple rows action is selected @@ -705,9 +734,12 @@ testRunner('table editor', () => { // select all actions works (delete action) await page.getByRole('checkbox', { name: 'Select All' }).click() await page.getByRole('button', { name: 'Delete 98 rows' }).click() + const waitForDeleteAllRows = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) await page.getByRole('button', { name: 'Delete' }).click() await expect(page.getByText('delete the selected 98 rows')).toBeVisible() - await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' }) // delete all rows + await waitForDeleteAllRows // delete all rows await waitForGridDataToLoad(page, ref) // retrieve rows data await expect(page.getByRole('gridcell', { name: 'value 7' })).not.toBeVisible() await expect(page.getByRole('gridcell', { name: 'value 101' })).not.toBeVisible() diff --git a/e2e/studio/playwright.config.ts b/e2e/studio/playwright.config.ts index d8d2677353e..68e1e420547 100644 --- a/e2e/studio/playwright.config.ts +++ b/e2e/studio/playwright.config.ts @@ -39,6 +39,9 @@ export default defineConfig({ forbidOnly: IS_CI, retries: IS_CI ? 5 : 0, maxFailures: 3, + expect: { + timeout: 20_000, + }, // Due to rate API rate limits run tests in serial mode on platform. fullyParallel: !env.IS_PLATFORM, workers: env.IS_PLATFORM ? 1 : 3, @@ -54,6 +57,69 @@ export default defineConfig({ process.env.VERCEL_AUTOMATION_BYPASS_SELFHOSTED_STUDIO || 'false', 'x-vercel-set-bypass-cookie': 'true', }, + launchOptions: { + args: [ + // Security/Sandbox settings (required for CI environments) + '--no-sandbox', // Disables Chrome's sandbox - required in Docker/CI where user namespaces aren't available + '--disable-setuid-sandbox', // Alternative sandbox method - disabled for CI compatibility + '--allow-insecure-localhost', // Allows tests against localhost with self-signed certificates + + // Memory and resource management + '--disable-dev-shm-usage', // Use /tmp instead of /dev/shm to avoid shared memory issues in containers + '--js-flags=--max_old_space_size=4096', // Increase V8 heap size to 4GB to handle memory-intensive tests + '--memory-pressure-off', // Prevents Chrome from killing tabs due to memory pressure in CI + '--enable-low-end-device-mode', // Optimizes memory usage for resource-constrained environments + + // GPU and rendering (disabled for headless/CI performance) + '--disable-gpu', // Disables hardware GPU - not needed in headless mode + '--disable-software-rasterizer', // Disables software-based rendering fallback + + // Performance optimizations for testing + '--disable-background-timer-throttling', // Prevents Chrome from throttling timers in background tabs + '--disable-backgrounding-occluded-windows', // Keeps hidden windows running at full speed + '--disable-renderer-backgrounding', // Prevents renderer processes from being deprioritized + '--disable-ipc-flooding-protection', // Allows high-frequency IPC messages needed for automation + + // Disable unnecessary features to reduce overhead + '--disable-extensions', // Disables all browser extensions + '--disable-sync', // Disables Chrome sync service + '--disable-default-apps', // Prevents loading of default Chrome apps + '--disable-component-update', // Disables automatic component updates during tests + '--disable-background-networking', // Disables background network requests + '--disable-features=TranslateUI', // Disables translation UI prompts + '--disable-features=MediaRouter,site-per-process', // Disables Cast and site isolation for performance + '--disable-features=HardwareMediaKeyHandling', // Disables hardware media key handling + + // Disable monitoring and crash reporting + '--disable-breakpad', // Disables crash reporting system + '--disable-crash-reporter', // Disables crash reporter UI + '--disable-hang-monitor', // Disables hang detection monitoring + '--metrics-recording-only', // Disables metric uploads while still collecting them + + // Disable security features not needed for testing + '--disable-client-side-phishing-detection', // Disables phishing detection checks + '--safebrowsing-disable-auto-update', // Disables safe browsing database updates + '--disable-domain-reliability', // Disables domain reliability monitoring + + // Disable user prompts and UI elements + '--disable-popup-blocking', // Allows popups without user confirmation + '--disable-prompt-on-repost', // Skips form resubmission confirmation dialogs + '--no-first-run', // Skips first-run wizards and setup dialogs + '--no-default-browser-check', // Prevents "set as default browser" prompts + + // Process management + '--no-zygote', // Disables zygote process for spawning renderers - reduces memory in single-use scenarios + + // Headless mode configuration + '--headless=new', // Uses new headless mode (more stable than old headless) + '--window-size=1280,720', // Sets consistent viewport size for screenshot/visual consistency + '--hide-scrollbars', // Hides scrollbars for cleaner screenshots + '--mute-audio', // Prevents audio output during tests + + // Network configuration + '--enable-features=NetworkService,NetworkServiceInProcess', // Uses modern network service in-process for better performance + ], + }, }, projects: [ { diff --git a/e2e/studio/utils/storage-helpers.ts b/e2e/studio/utils/storage-helpers.ts index 2458af2ebf6..c535da8c234 100644 --- a/e2e/studio/utils/storage-helpers.ts +++ b/e2e/studio/utils/storage-helpers.ts @@ -69,7 +69,7 @@ export const createBucket = async ( await expect( page.getByRole('row').filter({ hasText: bucketName }), `Bucket ${bucketName} should be visible after creation` - ).toBeVisible({ timeout: 15_000 }) + ).toBeVisible() } /** @@ -147,7 +147,7 @@ export const navigateToBucket = async (page: Page, ref: string, bucketName: stri await expect( page.getByRole('button', { name: 'Edit bucket' }), `Should be in bucket ${bucketName}` - ).toBeVisible({ timeout: 10_000 }) + ).toBeVisible() } /** @@ -171,7 +171,7 @@ export const createFolder = async (page: Page, folderName: string) => { await expect( page.getByTitle(folderName), `Folder ${folderName} should be visible after creation` - ).toBeVisible({ timeout: 10_000 }) + ).toBeVisible() } /** @@ -217,7 +217,7 @@ export const deleteItem = async (page: Page, itemName: string) => { await expect( page.getByTitle(itemName), `Item ${itemName} should not be visible after deletion` - ).not.toBeVisible({ timeout: 10_000 }) + ).not.toBeVisible() } /**