From 5847ad54cfb56903274c88e62183e25f6c63d2f0 Mon Sep 17 00:00:00 2001 From: Ivan Vasilov Date: Fri, 7 Jun 2024 17:44:28 +0200 Subject: [PATCH] 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. --- .github/workflows/playwright.yml | 14 +- .../components/header/sort/SortPopover.tsx | 4 +- .../ColumnEditor/InputWithSuggestions.tsx | 3 + .../SidePanelEditor/RowEditor/InputField.tsx | 7 +- .../SidePanelEditor/TableEditor/Column.tsx | 2 + .../TableEditor/TableEditor.tsx | 8 +- .../TableEditorLayout/TableEditorMenu.tsx | 2 +- apps/studio/pages/_app.tsx | 4 +- .../pages/api/projects/[ref]/run-lints.ts | 24 +++ packages/ui/src/components/NavMenu/index.tsx | 15 +- .../ui/src/components/Popover/Popover.tsx | 3 + playwright-tests/playwright.config.ts | 12 +- .../tests/snapshot/spec/table-editor.spec.ts | 152 ++++++++++++++++-- 13 files changed, 205 insertions(+), 45 deletions(-) create mode 100644 apps/studio/pages/api/projects/[ref]/run-lints.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 2ab350c96ee..2508ef13315 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -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 diff --git a/apps/studio/components/grid/components/header/sort/SortPopover.tsx b/apps/studio/components/grid/components/header/sort/SortPopover.tsx index be08f894b07..c0ff288a5ab 100644 --- a/apps/studio/components/grid/components/header/sort/SortPopover.tsx +++ b/apps/studio/components/grid/components/header/sort/SortPopover.tsx @@ -143,11 +143,13 @@ const SortOverlay = ({ table, sorts: sortsFromUrl, onApplySorts }: SortOverlayPr align="start" > ) : ( diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx index 2a787b8f7a6..80495d7e1c1 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/InputWithSuggestions.tsx @@ -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(suggestions) @@ -92,6 +94,7 @@ const InputWithSuggestions = ({ type="text" value={value} onChange={onInputChange} + data-testid={dataTestId} actions={ showSuggestions && ( diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx index dc36f696942..9303c52acc0 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/InputField.tsx @@ -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 (
Extra options
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx index c4aec7cb423..4bf450d4c53 100644 --- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx +++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx @@ -255,6 +255,7 @@ const TableEditor = ({ > Recommended
} - // @ts-ignore - description={ - <> -

Restrict access to your table by enabling RLS and writing Postgres policies.

- - } + 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 diff --git a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx index 7576fc81c9b..061d1306273 100644 --- a/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/TableEditorMenu.tsx @@ -213,7 +213,7 @@ const TableEditorMenu = () => { /> )} {(entityTypes?.length ?? 0) > 0 && ( -
+
- - + {!isTestEnv && } + {!isTestEnv && } {!isTestEnv && } diff --git a/apps/studio/pages/api/projects/[ref]/run-lints.ts b/apps/studio/pages/api/projects/[ref]/run-lints.ts new file mode 100644 index 00000000000..91c1e8d3ec2 --- /dev/null +++ b/apps/studio/pages/api/projects/[ref]/run-lints.ts @@ -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) => { + return res.status(200).json([]) +} diff --git a/packages/ui/src/components/NavMenu/index.tsx b/packages/ui/src/components/NavMenu/index.tsx index fa2ba5e26be..f58afc18c4d 100644 --- a/packages/ui/src/components/NavMenu/index.tsx +++ b/packages/ui/src/components/NavMenu/index.tsx @@ -20,17 +20,16 @@ export const NavMenu = forwardRef( } ) -export const NavMenuItem = forwardRef( - ({ - children, - className, - active, - ...props - }: PropsWithChildren<{ +interface NavMenuItemProps + extends PropsWithChildren<{ className?: string active: boolean - }>) => ( + }> {} + +export const NavMenuItem = forwardRef( + ({ children, className, active, ...props }, ref) => (
  • {children} diff --git a/playwright-tests/playwright.config.ts b/playwright-tests/playwright.config.ts index 41f761f1a94..c75031d9658 100644 --- a/playwright-tests/playwright.config.ts +++ b/playwright-tests/playwright.config.ts @@ -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', }, }) diff --git a/playwright-tests/tests/snapshot/spec/table-editor.spec.ts b/playwright-tests/tests/snapshot/spec/table-editor.spec.ts index 3a230f88a27..994784a3b0e 100644 --- a/playwright-tests/tests/snapshot/spec/table-editor.spec.ts +++ b/playwright-tests/tests/snapshot/spec/table-editor.spec.ts @@ -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', + ]) + ) }) })