mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
fix: Test the playwright tests in Github actions (#26799)
* Add an API page for run-lints endpoint. * Fix the playwright test. * Make the check mandatory in the github action. * Fix a console.log. * Use a more stable way of waiting for buttons before clicking them. * Add helper method for dismissing toasts. * Wait for the other toast. * Try and fix the flakiness of the toasts. * Minor fix. * Upload artifacts at the end of the github action run. * Fix yaml stuff. * Use the newer action for uploading. Shorten the retention period to 7 days. * Add waitForResponse for the entity types API call. * snapshot. * Wait for the page to be loaded. * Make the timeout bigger when waiting for response. * Try to fix the timeouts. * Minor restructuring of the test. * Add another test and few minor todo tests. * Fix the table names. * Don't render hcaptcha in test mode. * Expand the test to include sorting and filtering. * Add a test for checking if the other schemas are can be listed. * Make the sort popover button more stable. * Fix a unused forward ref in the NavMenuItem component. * Don't render the Toaster container when in test mode. * Simplify starting the dev server when testing. * Fix all inputs to have proper test ids. * Fix the command for testing.
This commit is contained in:
1 parent
7940e50d93
commit
5847ad54cf
13 files changed
+205
-45
No files matched your search
@@ -36,10 +36,10 @@ jobs:
|
||||
- name: Run Playwright tests
|
||||
run: npm run test:playwright
|
||||
# mark the action as succeeded even if the tests failed. This is temporarily until we make the tests more stable.
|
||||
continue-on-error: true
|
||||
# - uses: actions/upload-artifact@v3
|
||||
# if: always()
|
||||
# with:
|
||||
# name: playwright-report
|
||||
# path: playwright-report/
|
||||
# retention-days: 30
|
||||
# continue-on-error: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: playwright-tests/playwright-report/
|
||||
retention-days: 7
|
||||
@@ -143,11 +143,13 @@ const SortOverlay = ({ table, sorts: sortsFromUrl, onApplySorts }: SortOverlayPr
|
||||
align="start"
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
type="text"
|
||||
iconRight={<ChevronDown size="14" className="text-foreground-light" />}
|
||||
className="sb-grid-dropdown__item-trigger"
|
||||
data-testid="table-editor-pick-column-to-sort-button"
|
||||
>
|
||||
Pick {sorts.length > 1 ? 'another' : 'a'} column to sort by
|
||||
<span>Pick {sorts.length > 1 ? 'another' : 'a'} column to sort by</span>
|
||||
</Button>
|
||||
</DropdownControl>
|
||||
) : (
|
||||
|
||||
+3
@@ -38,6 +38,7 @@ interface InputWithSuggestionsProps {
|
||||
suggestionsHeader?: string
|
||||
onChange: (event: any) => void
|
||||
onSelectSuggestion: (suggestion: Suggestion) => void
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
const InputWithSuggestions = ({
|
||||
@@ -54,6 +55,7 @@ const InputWithSuggestions = ({
|
||||
suggestionsHeader,
|
||||
onChange = noop,
|
||||
onSelectSuggestion = noop,
|
||||
'data-testid': dataTestId,
|
||||
}: InputWithSuggestionsProps) => {
|
||||
const ref = useRef(null)
|
||||
const [filteredSuggestions, setFilteredSuggestions] = useState<Suggestion[]>(suggestions)
|
||||
@@ -92,6 +94,7 @@ const InputWithSuggestions = ({
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={onInputChange}
|
||||
data-testid={dataTestId}
|
||||
actions={
|
||||
showSuggestions && (
|
||||
<DropdownMenu>
|
||||
|
||||
+6
-1
@@ -10,11 +10,11 @@ import {
|
||||
Select,
|
||||
} from 'ui'
|
||||
|
||||
import { MAX_CHARACTERS } from 'data/table-rows/table-rows-query'
|
||||
import { Edit, Edit2, Link } from 'lucide-react'
|
||||
import { DATETIME_TYPES, JSON_TYPES, TEXT_TYPES } from '../SidePanelEditor.constants'
|
||||
import { DateTimeInput } from './DateTimeInput'
|
||||
import type { RowField } from './RowEditor.types'
|
||||
import { MAX_CHARACTERS } from 'data/table-rows/table-rows-query'
|
||||
|
||||
export interface InputFieldProps {
|
||||
field: RowField
|
||||
@@ -41,6 +41,7 @@ const InputField = ({
|
||||
return (
|
||||
<div className="text-area-text-sm">
|
||||
<Input.TextArea
|
||||
data-testid={`${field.name}-input`}
|
||||
layout="horizontal"
|
||||
label={field.name}
|
||||
className="text-sm"
|
||||
@@ -88,6 +89,7 @@ const InputField = ({
|
||||
if (field.foreignKey !== undefined) {
|
||||
return (
|
||||
<Input
|
||||
data-testid={`${field.name}-input`}
|
||||
layout="horizontal"
|
||||
label={field.name}
|
||||
value={field.value ?? ''}
|
||||
@@ -131,6 +133,7 @@ const InputField = ({
|
||||
return (
|
||||
<div className="text-area-text-sm">
|
||||
<Input.TextArea
|
||||
data-testid={`${field.name}-input`}
|
||||
layout="horizontal"
|
||||
label={field.name}
|
||||
className="text-sm"
|
||||
@@ -187,6 +190,7 @@ const InputField = ({
|
||||
|
||||
return (
|
||||
<Input
|
||||
data-testid={`${field.name}-input`}
|
||||
layout="horizontal"
|
||||
value={field.value ?? ''}
|
||||
label={field.name}
|
||||
@@ -284,6 +288,7 @@ const InputField = ({
|
||||
|
||||
return (
|
||||
<Input
|
||||
data-testid={`${field.name}-input`}
|
||||
layout="horizontal"
|
||||
label={field.name}
|
||||
descriptionText={field.comment}
|
||||
|
||||
+2
@@ -243,6 +243,7 @@ const Column = ({
|
||||
<div className={`${isNewRecord ? 'w-[25%]' : 'w-[30%]'}`}>
|
||||
<div className="w-[95%]">
|
||||
<InputWithSuggestions
|
||||
data-testid={`${column.name}-default-value`}
|
||||
placeholder={
|
||||
typeof column.defaultValue === 'string' && column.defaultValue.length === 0
|
||||
? 'EMPTY'
|
||||
@@ -280,6 +281,7 @@ const Column = ({
|
||||
className="pointer-events-auto"
|
||||
align="end"
|
||||
modal={true}
|
||||
data-testid={`${column.name}-extra-options`}
|
||||
header={
|
||||
<div className="flex items-center justify-center">
|
||||
<h5 className="text-sm text-foreground">Extra options</h5>
|
||||
|
||||
+2
-6
@@ -255,6 +255,7 @@ const TableEditor = ({
|
||||
>
|
||||
<SidePanel.Content className="space-y-10 py-6">
|
||||
<Input
|
||||
data-testid="table-name-input"
|
||||
label="Name"
|
||||
layout="horizontal"
|
||||
type="text"
|
||||
@@ -282,12 +283,7 @@ const TableEditor = ({
|
||||
<Badge>Recommended</Badge>
|
||||
</div>
|
||||
}
|
||||
// @ts-ignore
|
||||
description={
|
||||
<>
|
||||
<p>Restrict access to your table by enabling RLS and writing Postgres policies.</p>
|
||||
</>
|
||||
}
|
||||
description="Restrict access to your table by enabling RLS and writing Postgres policies."
|
||||
checked={tableFields.isRLSEnabled}
|
||||
onChange={() => {
|
||||
// if isEnabled, show confirm modal to turn off
|
||||
|
||||
@@ -213,7 +213,7 @@ const TableEditorMenu = () => {
|
||||
/>
|
||||
)}
|
||||
{(entityTypes?.length ?? 0) > 0 && (
|
||||
<div className="flex flex-1">
|
||||
<div className="flex flex-1" data-testid="tables-list">
|
||||
<InfiniteList
|
||||
items={entityTypes}
|
||||
ItemComponent={EntityListItem}
|
||||
|
||||
@@ -204,8 +204,8 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
</TooltipProvider>
|
||||
</PageTelemetry>
|
||||
|
||||
<HCaptchaLoadedStore />
|
||||
<Toaster />
|
||||
{!isTestEnv && <HCaptchaLoadedStore />}
|
||||
{!isTestEnv && <Toaster />}
|
||||
<PortalToast />
|
||||
{!isTestEnv && <ReactQueryDevtools initialIsOpen={false} position="bottom-right" />}
|
||||
</FlagProvider>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
import apiWrapper from 'lib/api/apiWrapper'
|
||||
import { extractResponse } from 'pages/api/constants'
|
||||
|
||||
export default (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler)
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { method } = req
|
||||
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
return handleGet(req, res)
|
||||
default:
|
||||
res.setHeader('Allow', ['GET'])
|
||||
res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } })
|
||||
}
|
||||
}
|
||||
|
||||
type ResponseData = extractResponse<'/platform/projects/{ref}/run-lints', 'get'>
|
||||
|
||||
const handleGet = async (req: NextApiRequest, res: NextApiResponse<ResponseData>) => {
|
||||
return res.status(200).json([])
|
||||
}
|
||||
@@ -20,17 +20,16 @@ export const NavMenu = forwardRef<HTMLDivElement, NavMenuProps>(
|
||||
}
|
||||
)
|
||||
|
||||
export const NavMenuItem = forwardRef(
|
||||
({
|
||||
children,
|
||||
className,
|
||||
active,
|
||||
...props
|
||||
}: PropsWithChildren<{
|
||||
interface NavMenuItemProps
|
||||
extends PropsWithChildren<{
|
||||
className?: string
|
||||
active: boolean
|
||||
}>) => (
|
||||
}> {}
|
||||
|
||||
export const NavMenuItem = forwardRef<HTMLLIElement, NavMenuItemProps>(
|
||||
({ children, className, active, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
aria-selected={active ? 'true' : 'false'}
|
||||
data-state={active ? 'active' : 'inactive'}
|
||||
className={cn(
|
||||
|
||||
@@ -26,6 +26,7 @@ interface RootProps {
|
||||
header?: React.ReactNode
|
||||
footer?: React.ReactNode
|
||||
size?: 'tiny' | 'small' | 'medium' | 'large' | 'xlarge' | 'content'
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,6 +50,7 @@ function Popover({
|
||||
footer,
|
||||
size = 'content',
|
||||
disabled,
|
||||
'data-testid': dataTestId,
|
||||
}: RootProps) {
|
||||
const __styles = styleHandler('popover')
|
||||
|
||||
@@ -69,6 +71,7 @@ function Popover({
|
||||
disabled={disabled}
|
||||
className={__styles.trigger}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={dataTestId}
|
||||
>
|
||||
{children}
|
||||
</RadixPopover.Trigger>
|
||||
|
||||
@@ -29,7 +29,7 @@ export default defineConfig({
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
// record a video for failed tests, but only for local testing. We can't store videos on CI atm.
|
||||
video: process.env.CI ? 'off' : 'retain-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
@@ -72,8 +72,16 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'NODE_ENV=test npm --prefix ../apps/studio run dev',
|
||||
// using npm run dev instead of turbo because turbo doesn't stop the server after a test (doesn't handle SIGTERM).
|
||||
command: 'npm run dev',
|
||||
cwd: '../apps/studio',
|
||||
url: 'http://localhost:8082',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
env: {
|
||||
NODE_ENV: 'test',
|
||||
},
|
||||
// Show the nextjs logs in the terminal. Useful for debugging.
|
||||
// stdout: 'pipe',
|
||||
// stderr: 'pipe',
|
||||
},
|
||||
})
|
||||
@@ -1,30 +1,148 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { Page, expect, test } from '@playwright/test'
|
||||
import { kebabCase } from 'lodash'
|
||||
|
||||
const dismissToast = async (page: Page) => {
|
||||
await page.locator('#toast').getByRole('button').waitFor({ state: 'visible' })
|
||||
await page.locator('#toast').getByRole('button').click()
|
||||
}
|
||||
|
||||
test.describe('Table Editor page', () => {
|
||||
test('should create a column and insert a row', async ({ page }) => {
|
||||
const name = 'TestTable' + Math.floor(Math.random() * 10)
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const tableResponsePromise = page.waitForResponse(
|
||||
'http://localhost:8082/api/pg-meta/default/query?key=public-entity-types',
|
||||
{ timeout: 0 }
|
||||
)
|
||||
await page.goto('/project/default/editor')
|
||||
await tableResponsePromise
|
||||
})
|
||||
|
||||
test('should create a new table, view its definition, add new rows, sort and filter', async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
const tableName = `${kebabCase(testInfo.title).slice(0, 24)}-${testInfo.retry}-${Math.floor(Math.random() * 100)}`
|
||||
|
||||
// The page has been loaded with the table data, we can now interact with the page
|
||||
await page.getByRole('button', { name: 'New table', exact: true }).click()
|
||||
// need to wait for the panel animation
|
||||
await page.waitForTimeout(1000)
|
||||
await page.locator('.col-span-8 > div > .relative > .peer\\/input').first().click()
|
||||
await page.locator('.col-span-8 > div > .relative > .peer\\/input').first().fill(name)
|
||||
await page.getByTestId('table-name-input').waitFor({ state: 'visible' })
|
||||
await page.getByTestId('table-name-input').click()
|
||||
await page.getByTestId('table-name-input').fill(tableName)
|
||||
|
||||
// make the built-in created_at column nullable
|
||||
await page.getByTestId('created_at-extra-options').click()
|
||||
await page.getByText('Is Nullable').click()
|
||||
// the force option is needed because the button is obscured by the popover but we just want to close the popover.
|
||||
await page.getByTestId('created_at-extra-options').click({ force: true })
|
||||
|
||||
// add a new column and add default value
|
||||
await page.getByRole('button', { name: 'Add column' }).click()
|
||||
await page.getByRole('textbox', { name: 'column_name' }).click()
|
||||
await page.getByRole('textbox', { name: 'column_name' }).fill('textColumn')
|
||||
await page.getByRole('textbox', { name: 'column_name' }).fill('defaultValueColumn')
|
||||
await page.getByRole('button', { name: '---' }).click()
|
||||
await page.getByText('textVariable-length character').click()
|
||||
await page.waitForTimeout(1000)
|
||||
await page.getByText('Signed two-byte integer').click()
|
||||
await page.getByTestId('defaultValueColumn-default-value').click()
|
||||
await page.getByTestId('defaultValueColumn-default-value').fill('2')
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).waitFor({ state: 'visible' })
|
||||
await page.getByRole('button', { name: 'Save' }).click()
|
||||
await page.locator('#toast').getByRole('button').click()
|
||||
await page.getByRole('link', { name }).click()
|
||||
await dismissToast(page)
|
||||
|
||||
// view its definition
|
||||
await page.getByText('definition').click()
|
||||
await expect(page.locator('div.view-lines')).toContainText(
|
||||
`CREATE TABLE public.${tableName} ( id bigint GENERATED BY DEFAULT AS IDENTITY , created_at timestamp with time zone NULL DEFAULT now(), \"defaultValueColumn\" smallint NULL DEFAULT '2'::smallint, CONSTRAINT ${tableName}_pkey PRIMARY KEY (id)) TABLESPACE pg_default;`
|
||||
)
|
||||
|
||||
// add a new row
|
||||
await page.getByRole('button', { name: tableName }).click()
|
||||
await page.getByTestId('table-editor-insert-new-row').click()
|
||||
await page.getByText('Insert a new row into').click()
|
||||
await page.getByPlaceholder('NULL').click()
|
||||
await page.getByPlaceholder('NULL').fill('some text')
|
||||
await page.getByTestId('defaultValueColumn-input').click()
|
||||
await page.getByTestId('defaultValueColumn-input').fill('100')
|
||||
await page.getByTestId('action-bar-save-row').click()
|
||||
await page.locator('#toast').getByRole('button').click()
|
||||
await expect(page.getByRole('grid')).toContainText('some text')
|
||||
await dismissToast(page)
|
||||
|
||||
// add a second row
|
||||
await page.getByRole('button', { name: tableName }).click()
|
||||
await page.getByTestId('table-editor-insert-new-row').click()
|
||||
await page.getByText('Insert a new row into').click()
|
||||
// the default value should be '100' for defaultValueColumn
|
||||
await page.getByTestId('action-bar-save-row').click()
|
||||
await dismissToast(page)
|
||||
|
||||
await expect(page.getByRole('grid')).toContainText('2')
|
||||
await expect(page.getByRole('grid')).toContainText('100')
|
||||
|
||||
// sort by the a column
|
||||
await page.getByRole('button', { name: 'Sort' }).click()
|
||||
await page.getByTestId('table-editor-pick-column-to-sort-button').click()
|
||||
await page.getByLabel('Pick a column to sort by').getByText('defaultValueColumn').click()
|
||||
await page.getByRole('button', { name: 'Apply sorting' }).click()
|
||||
// click away to close the sorting dialog
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Table Editor$/ })
|
||||
.click()
|
||||
// expect the row to be sorted by defaultValueColumn. They're inserted in the order 100, 2
|
||||
await expect(page.locator('div.rdg-row:nth-child(2)')).toContainText('2')
|
||||
await expect(page.locator('div.rdg-row:nth-child(3)')).toContainText('100')
|
||||
// remove the sorting
|
||||
await page.getByRole('button', { name: 'Sorted by 1 rule' }).click()
|
||||
await page.getByRole('dialog').getByRole('button').nth(1).click()
|
||||
|
||||
// filter by a column
|
||||
await page.getByRole('button', { name: 'Filter' }).click()
|
||||
await page.getByRole('button', { name: 'Add filter' }).click()
|
||||
await page.getByRole('button', { name: 'id' }).click()
|
||||
await page.getByLabel('id').getByText('defaultValueColumn').click()
|
||||
await page.getByPlaceholder('Enter a value').click()
|
||||
await page.getByPlaceholder('Enter a value').fill('2')
|
||||
await page.getByRole('button', { name: 'Apply filter' }).click()
|
||||
// click away to close the filter dialog
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Table Editor$/ })
|
||||
.click()
|
||||
await expect(page.getByRole('grid')).toContainText('2')
|
||||
await expect(page.getByRole('grid')).not.toContainText('100')
|
||||
})
|
||||
|
||||
test('should check the auth schema', async ({ page }) => {
|
||||
const tableResponsePromise = page.waitForResponse(
|
||||
'http://localhost:8082/api/pg-meta/default/query?key=public-entity-types',
|
||||
{ timeout: 0 }
|
||||
)
|
||||
|
||||
await page.getByRole('button', { name: 'schema: public' }).click()
|
||||
await page.getByRole('option', { name: 'auth' }).click()
|
||||
|
||||
// wait for the table data to load for the auth schema
|
||||
await tableResponsePromise
|
||||
|
||||
// extract the tables names from the sidebar
|
||||
const tables = await page
|
||||
.getByTestId('tables-list')
|
||||
.innerText()
|
||||
.then((text) => text.split('\n'))
|
||||
|
||||
// expect the tables list to contain the following tables (additional tables may be present)
|
||||
expect(tables).toEqual(
|
||||
expect.arrayContaining([
|
||||
'audit_log_entries',
|
||||
'flow_state',
|
||||
'identities',
|
||||
'instances',
|
||||
'mfa_amr_claims',
|
||||
'mfa_challenges',
|
||||
'mfa_factors',
|
||||
'refresh_tokens',
|
||||
'saml_providers',
|
||||
'saml_relay_states',
|
||||
'schema_migrations',
|
||||
'sessions',
|
||||
'sso_domains',
|
||||
'sso_providers',
|
||||
'users',
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user