Skip to content

Commit e08d1f7

Browse files
authored
test: fix flaky table editor tests (supabase#40323)
1 parent 6bb9791 commit e08d1f7

File tree

1 file changed

+52
-62
lines changed

1 file changed

+52
-62
lines changed

e2e/studio/features/table-editor.spec.ts

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { expect, Locator, Page } from '@playwright/test'
22
import fs from 'fs'
33
import path from 'path'
4+
import { isCLI } from '../utils/is-cli'
5+
import { resetLocalStorage } from '../utils/reset-local-storage'
46
import { test } from '../utils/test'
57
import { toUrl } from '../utils/to-url'
68
import {
79
waitForApiResponse,
810
waitForGridDataToLoad,
911
waitForTableToLoad,
1012
} from '../utils/wait-for-response'
11-
import { resetLocalStorage } from '../utils/reset-local-storage'
12-
import { isCLI } from '../utils/is-cli'
1313
import { waitForApiResponseWithTimeout } from '../utils/wait-for-response-with-timeout'
1414

1515
const tableNamePrefix = 'pw_table'
@@ -45,37 +45,46 @@ const createTable = async (page: Page, ref: string, tableName: string) => {
4545
const nameInput = page.getByTestId('table-name-input')
4646
await expect(nameInput).toBeVisible()
4747
await nameInput.fill(tableName)
48+
await expect(nameInput).toHaveValue(tableName)
4849
await page.getByTestId('created_at-extra-options').click()
49-
await page.getByText('Is Nullable').click()
50+
await page.getByRole('checkbox', { name: 'Is Nullable' }).click()
5051
await page.getByTestId('created_at-extra-options').click({ force: true })
5152
await page.getByRole('button', { name: 'Add column' }).click()
5253
await page.getByRole('textbox', { name: 'column_name' }).fill(columnName)
5354
await page.getByText('Choose a column type...').click()
5455
await page.getByRole('option', { name: 'text Variable-length' }).click()
55-
await page.getByRole('button', { name: 'Save' }).click()
56+
const createTablePromise = waitForApiResponseWithTimeout(page, (response) =>
57+
response.url().includes('query?key=table-create')
58+
)
5659
// Wait specifically for tables list refresh instead of generic networkidle
57-
await waitForApiResponseWithTimeout(page, (response) =>
60+
const tablesPromise = waitForApiResponseWithTimeout(page, (response) =>
5861
response.url().includes('tables?include_columns=true&included_schemas=public')
5962
)
6063
// wait for tables to load, we don't need to wait here cause this response may complete before the table creation.
61-
await waitForApiResponseWithTimeout(page, (response) =>
64+
const entitiesPromise = waitForApiResponseWithTimeout(page, (response) =>
6265
response.url().includes('query?key=entity-types-public-')
6366
)
67+
await page.getByRole('button', { name: 'Save' }).click()
68+
await Promise.all([createTablePromise, tablesPromise, entitiesPromise])
6469
await expect(
6570
page.getByRole('button', { name: `View ${tableName}`, exact: true }),
6671
'Table should be visible after creation'
67-
).toBeVisible({ timeout: 10000 })
72+
).toBeVisible({ timeout: 15_000 })
6873
}
6974

7075
const deleteTable = async (page: Page, ref: string, tableName: string) => {
7176
const viewLocator = page.getByLabel(`View ${tableName}`)
7277
if ((await viewLocator.count()) === 0) return
7378
await viewLocator.nth(0).click()
74-
await viewLocator.getByRole('button').nth(1).click({ force: true })
79+
await viewLocator.locator('button[aria-haspopup="menu"]').click({ force: true })
7580
await page.getByText('Delete table').click()
7681
await page.getByRole('checkbox', { name: 'Drop table with cascade?' }).click()
82+
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-delete-', {
83+
method: 'POST',
84+
})
85+
const revalidatePromise = waitForApiResponse(page, 'pg-meta', ref, `query?key=entity-types-`)
7786
await page.getByRole('button', { name: 'Delete' }).click()
78-
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-delete-', { method: 'POST' })
87+
await Promise.all([apiPromise, revalidatePromise])
7988
}
8089

8190
const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => {
@@ -96,49 +105,36 @@ const deleteEnumIfExist = async (page: Page, ref: string, enumName: string) => {
96105
}
97106

98107
test.describe.serial('table editor', () => {
99-
let page: Page
100-
101-
test.beforeEach(async ({ ref }) => {
102-
await resetLocalStorage(page, ref)
103-
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
104-
await waitForTableToLoad(page, ref)
105-
})
106-
107108
test.beforeAll(async ({ browser, ref }) => {
108-
page = await browser.newPage()
109+
const ctx = await browser.newContext()
110+
const page = await ctx.newPage()
111+
112+
const loadPromise = waitForTableToLoad(page, ref)
109113
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
110-
await waitForTableToLoad(page, ref)
114+
await loadPromise
111115

112-
// Delete all tables with prefix pw_table (ensure page is stable first)
113116
const viewButtons = page.getByRole('button', { name: /^View / })
114-
const names = (await viewButtons.allTextContents()).map((t) => t.replace(/^View\s+/, '').trim())
117+
const names = await Promise.all(
118+
(await viewButtons.all()).map(async (btn) => {
119+
const ariaLabel = await btn.getAttribute('aria-label')
120+
const name = ariaLabel ? ariaLabel.replace(/^View\s+/, '').trim() : ''
121+
return name
122+
})
123+
)
115124
const tablesToDelete = names.filter((tableName) => tableName.startsWith(tableNamePrefix))
116125

117126
for (const tableName of tablesToDelete) {
118127
await deleteTable(page, ref, tableName)
119-
await waitForTableToLoad(page, ref) // wait for table data to update
120128
}
121129
})
122130

123-
test.afterAll(async ({ ref }) => {
124-
await resetLocalStorage(page, ref)
125-
126-
// Always navigate explicitly to editor and wait for tables to be loaded
131+
test.beforeEach(async ({ page, ref }) => {
132+
const loadPromise = waitForTableToLoad(page, ref)
127133
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
128-
await waitForTableToLoad(page, ref)
129-
130-
// Delete all tables with prefix pw_table
131-
const viewButtons = page.getByRole('button', { name: /^View / })
132-
const names = (await viewButtons.allTextContents()).map((t) => t.replace(/^View\s+/, '').trim())
133-
const tablesToDelete = names.filter((tableName) => tableName.startsWith(tableNamePrefix))
134-
135-
for (const tableName of tablesToDelete) {
136-
await deleteTable(page, ref, tableName)
137-
await waitForTableToLoad(page, ref) // wait for table data to update
138-
}
134+
await loadPromise
139135
})
140136

141-
test('sidebar actions works as expected', async ({ ref }) => {
137+
test('sidebar actions works as expected', async ({ page, ref }) => {
142138
const tableNameActions = 'pw_table_actions'
143139
const tableNameActionsDuplicate = 'pw_table_actions_duplicate'
144140

@@ -193,7 +189,7 @@ test.describe.serial('table editor', () => {
193189
await deleteTable(page, ref, tableNameActions)
194190
})
195191

196-
test('switching schemas work as expected', async ({ ref }) => {
192+
test('switching schemas work as expected', async ({ page, ref }) => {
197193
const authTableSso = 'identities'
198194
const authTableMfa = 'mfa_factors'
199195

@@ -212,7 +208,7 @@ test.describe.serial('table editor', () => {
212208
await expect(page.getByLabel(`View ${authTableMfa}`)).toBeVisible()
213209
})
214210

215-
test('should show rls accordingly', async ({ ref }) => {
211+
test('should show rls accordingly', async ({ page, ref }) => {
216212
const tableNameRlsEnabled = 'pw_table_rls_enabled'
217213
const tableNameRlsDisabled = 'pw_table_rls_disabled'
218214

@@ -226,22 +222,19 @@ test.describe.serial('table editor', () => {
226222
await page.getByTestId('table-name-input').fill(tableNameRlsDisabled)
227223
await page.getByLabel('Enable Row Level Security (').click()
228224
await page.getByRole('button', { name: 'Confirm' }).click()
229-
await page.getByRole('button', { name: 'Save' }).click()
230-
await waitForApiResponse(
225+
const apiPromise = waitForApiResponse(
231226
page,
232227
'pg-meta',
233228
ref,
234229
'tables?include_columns=false&included_schemas=public'
235230
) // wait for table creation
231+
await page.getByRole('button', { name: 'Save' }).click()
232+
await apiPromise
236233
await page.getByRole('button', { name: `View ${tableNameRlsDisabled}` }).click()
237234
await expect(page.getByRole('button', { name: 'RLS disabled' })).toBeVisible()
238-
239-
// clear all tables
240-
await deleteTable(page, ref, tableNameRlsEnabled)
241-
await deleteTable(page, ref, tableNameRlsDisabled)
242235
})
243236

244-
test('add enums and show enums on table', async ({ ref }) => {
237+
test('add enums and show enums on table', async ({ page, ref }) => {
245238
const tableNameEnum = 'pw_table_enum'
246239
const columnNameEnum = 'pw_column_enum'
247240
const enum_name = 'pw_enum'
@@ -317,7 +310,7 @@ test.describe.serial('table editor', () => {
317310
await resetLocalStorage(page, ref)
318311
})
319312

320-
test('Grid editor exporting works as expected', async ({ ref }) => {
313+
test('Grid editor exporting works as expected', async ({ page, ref }) => {
321314
const tableNameGridEditor = ' pw_table_grid_editor'
322315
const tableNameUpdated = 'pw_table_updated'
323316
const columnNameUpdated = 'pw_column_updated'
@@ -449,14 +442,9 @@ test.describe.serial('table editor', () => {
449442
await page.getByRole('menuitem', { name: 'Export table via CLI' }).click()
450443
await expect(page.getByRole('heading', { name: 'Export table data via CLI' })).toBeVisible()
451444
await page.getByRole('button', { name: 'Close' }).first().click()
452-
453-
// Ensure all menus/dialogs are closed before continuing
454-
await page.keyboard.press('Escape')
455-
await page.keyboard.press('Escape')
456-
await page.waitForTimeout(500)
457445
})
458446

459-
test('filtering rows works as expected', async ({ ref }) => {
447+
test('filtering rows works as expected', async ({ page, ref }) => {
460448
const tableName = 'pw_table_filtering'
461449
const colName = 'pw_column'
462450

@@ -473,13 +461,14 @@ test.describe.serial('table editor', () => {
473461
await page.getByTestId('table-editor-insert-new-row').click()
474462
await page.getByRole('menuitem', { name: 'Insert row Insert a new row' }).click()
475463
await page.getByTestId(`${colName}-input`).fill(value)
464+
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
476465
await page.getByTestId('action-bar-save-row').click()
477-
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=', { method: 'POST' })
466+
await apiPromise
478467
}
479468

480469
await page.getByRole('button', { name: 'Filter', exact: true }).click()
481470
await page.getByRole('button', { name: 'Add filter' }).click()
482-
await page.getByRole('button', { name: 'id' }).click()
471+
await page.getByRole('button', { name: 'id', exact: true }).click()
483472
await page.getByRole('menuitem', { name: colName }).click()
484473
await page.getByRole('textbox', { name: 'Enter a value' }).fill('789')
485474
await page.getByRole('button', { name: 'Apply filter' }).click()
@@ -493,25 +482,27 @@ test.describe.serial('table editor', () => {
493482
await deleteTable(page, ref, tableName)
494483
})
495484

496-
test('view table definition works as expected', async ({ ref }) => {
485+
test('view table definition works as expected', async ({ page, ref }) => {
497486
const tableName = 'pw_table_definition'
498487
const colName = 'pw_column'
499488
if (!page.url().includes('/editor')) {
489+
const tableLoadPromise = waitForTableToLoad(page, ref)
500490
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
501-
await waitForTableToLoad(page, ref)
491+
await tableLoadPromise
502492
}
503493
await createTable(page, ref, tableName)
504494
await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click()
505495
await page.waitForURL(/\/editor\/\d+\?schema=public$/)
496+
const apiPromise = waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-')
506497
await page.getByText('definition', { exact: true }).click()
507-
await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-')
498+
await apiPromise
508499
await expect(page.locator('.view-lines')).toContainText(
509500
`create table public.${tableName} ( id bigint generated by default as identity not null, created_at timestamp with time zone null default now(), ${colName} text null, constraint ${tableName}_pkey primary key (id)) TABLESPACE pg_default;`
510501
)
511502
await deleteTable(page, ref, tableName)
512503
})
513504

514-
test('sorting rows works as expected', async ({ ref }) => {
505+
test('sorting rows works as expected', async ({ page, ref }) => {
515506
const tableName = 'pw_table_sorting'
516507
const colName = 'pw_column'
517508

@@ -552,7 +543,7 @@ test.describe.serial('table editor', () => {
552543
await deleteTable(page, ref, tableName)
553544
})
554545

555-
test('importing, pagination and large data actions works as expected', async ({ ref }) => {
546+
test('importing, pagination and large data actions works as expected', async ({ page, ref }) => {
556547
await page.goto(toUrl(`/project/${ref}/editor?schema=public`))
557548
const tableNameDataActions = 'pw_table_data'
558549

@@ -575,7 +566,6 @@ test.describe.serial('table editor', () => {
575566
// importing 51 data via paste text
576567
const filePath = path.join(__dirname, 'files', 'table-editor-import-paste.txt')
577568
const fileContent = fs.readFileSync(filePath, 'utf-8')
578-
await page.getByRole('button', { name: 'Close toast' }).first().click() // close toast, as paste text is behind toast
579569
await page.getByTestId('table-editor-insert-new-row').click()
580570
await page.getByRole('menuitem', { name: 'Import data from CSV' }).click()
581571
await page.getByRole('tab', { name: 'Paste text' }).click()

0 commit comments

Comments
 (0)