-
Notifications
You must be signed in to change notification settings - Fork 5.7k
e2e tests #16533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
e2e tests #16533
Changes from 10 commits
1b9e2d3
69394a8
2c8316b
296ac89
5082be3
d977378
2732510
5946f06
97c2714
59fa5f4
dbe7b95
7cac768
ab6f56d
f83d7f3
e3c130e
670dd52
418dda9
02196ff
cb5004e
3a0617a
0a5e875
7c26b46
cf7169b
01add78
f82e1c2
46a4a5e
b4e8a19
96913d8
a8cd7ba
b92aad9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { type Page } from '@playwright/test'; | ||
|
|
||
| const decodeToken = (cookie: any) => | ||
| JSON.parse(decodeURIComponent(cookie.value)).accessOrWorkspaceAgnosticToken | ||
| ?.token; | ||
|
|
||
| const decodePayload = (jwt: string) => | ||
| JSON.parse(Buffer.from(jwt.split('.')[1], 'base64url').toString()); | ||
|
|
||
|
|
||
| export const getAccessAuthToken = async (page: Page) => { | ||
| const storageState = await page.context().storageState(); | ||
| const tokenCookies = storageState.cookies.filter( | ||
| (cookie) => cookie.name === 'tokenPair', | ||
| ); | ||
| if (!tokenCookies) { | ||
| throw new Error('No auth cookie found'); | ||
| } | ||
| const accessTokenCookie = tokenCookies.find( | ||
| (cookie) => { | ||
| const payload = decodePayload(decodeToken(cookie) ?? ''); | ||
| return payload.type === 'ACCESS'; | ||
| } | ||
| ); | ||
|
|
||
| const token = JSON.parse(decodeURIComponent(accessTokenCookie?.value ?? '')).accessOrWorkspaceAgnosticToken | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Bug: If no access token cookie is found, Prompt for AI agents |
||
| .token; | ||
|
|
||
| return { authToken: token }; | ||
| }; | ||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { expect, test } from '../lib/fixtures/screenshot'; | ||
|
|
||
| if (process.env.LINK) { | ||
| const baseURL = new URL(process.env.LINK).origin; | ||
| test.use({ baseURL }); | ||
| } | ||
ijreilly marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| test.describe.serial('Create Kanban View', () => { | ||
| test('Create Industry Select Field', async ({ page }) => { | ||
| await page.getByRole('link', { name: 'Settings' }).click(); | ||
| await page.getByRole('link', { name: 'Data model' }).click(); | ||
| await page.getByRole('link', { name: 'Opportunities' }).click(); | ||
| await page.getByRole('button', { name: 'Add Field' }).click(); | ||
| await page.getByRole('link', { name: 'Select', exact: true }).click(); | ||
| await page.getByRole('textbox', { name: 'Employees' }).click(); | ||
| await page.getByRole('textbox', { name: 'Employees' }).fill('Industry'); | ||
| await page.getByRole('textbox').nth(1).click(); | ||
| await page.getByRole('textbox').nth(1).press('ControlOrMeta+a'); | ||
| await page.getByRole('textbox').nth(1).fill('Food'); | ||
| await page.getByRole('button', { name: 'Add option' }).click(); | ||
| await page.getByRole('button', { name: 'Option 2' }).getByRole('textbox').fill('Tech'); | ||
| await page.getByRole('button', { name: 'Add option' }).click(); | ||
| await page.getByRole('button', { name: 'Option 3' }).getByRole('textbox').fill('Travel'); | ||
| await page.getByRole('button', { name: 'Save' }).click(); | ||
| await page.waitForURL('**/objects/opportunities'); | ||
| await page.waitForSelector('text=Industry'); | ||
| await expect(page.getByText('Industry')).toBeVisible(); | ||
| }); | ||
|
|
||
| test('Create Kanban View from Industry Select Field', async ({ page }) => { | ||
| await page.getByRole('link', { name: 'Opportunities' }).click(); | ||
| await page.getByRole('button', { name: 'All Opportunities ·' }).click(); | ||
| await page.getByText('Add view').click(); | ||
| await page.getByRole('textbox').press('ControlOrMeta+a'); | ||
| await page.getByRole('textbox').fill('By industry'); | ||
| await page.getByRole('button', { name: 'Table', exact: true }).click(); | ||
| await page.getByText('Kanban').click(); | ||
| await page.locator('[aria-controls="view-picker-kanban-field-options"]').click(); | ||
| await page.getByRole('option', { name: 'Industry' }).click(); | ||
| // Use exact: true to ensure we only click the button with the label "Create" | ||
| await page.getByRole('button', { name: 'Create new view' }).click(); | ||
| await expect(page.getByText('Food')).toBeVisible(); | ||
| await expect(page.getByText('Tech')).toBeVisible(); | ||
| await expect(page.getByText('Travel')).toBeVisible(); | ||
| await expect(page.getByText('No value')).toBeVisible(); | ||
| const byIndustryElements = await page.locator('text=By industry').all(); | ||
| expect(byIndustryElements.length).toBeGreaterThanOrEqual(1); | ||
| for (const element of byIndustryElements) { | ||
| await expect(element).toBeVisible(); | ||
| } | ||
| await page.getByText('Options').click(); | ||
| await page.getByText('Group', { exact: true }).click(); | ||
| await Promise.all([page.getByRole('button', { name: 'Hide group null', exact: true }).click(), | ||
|
||
| page.waitForRequest((req) => { | ||
| return req.url().includes('/metadata') && | ||
| req.method() === 'POST' | ||
| })]); | ||
|
Comment on lines
+48
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The problem with this kind of assertion is that we can make dozens of calls to We can leave it as is, but since it doesn’t prove that the action we expect was performed, it could lead to flakiness.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we expect other POST requests in this setup, nor that for now we actually do any /metadata post requests while navigating only
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But I intent to make sure this is not flaky so i m keeping that in mind |
||
| await expect(page.getByText('No value')).not.toBeVisible(); | ||
| }); | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| import { expect, test } from '../lib/fixtures/screenshot'; | ||
| import { backendGraphQLUrl } from '../lib/requests/backend'; | ||
| import { getAccessAuthToken } from '../lib/utils/getAccessAuthToken'; | ||
|
|
||
| if (process.env.LINK) { | ||
| const baseURL = new URL(process.env.LINK).origin; | ||
| test.use({ baseURL }); | ||
| } | ||
|
|
||
| const query = `query FindOnePerson($objectRecordId: UUID!) { | ||
| person( | ||
| filter: {or: [{deletedAt: {is: NULL}}, {deletedAt: {is: NOT_NULL}}], id: {eq: $objectRecordId}} | ||
| ) { | ||
| company { | ||
| name | ||
| } | ||
| emails { | ||
| primaryEmail | ||
| additionalEmails | ||
| __typename | ||
| } | ||
| id | ||
| intro | ||
| jobTitle | ||
| linkedinLink { | ||
| primaryLinkUrl | ||
| primaryLinkLabel | ||
| secondaryLinks | ||
| __typename | ||
| } | ||
| name { | ||
| firstName | ||
| lastName | ||
| __typename | ||
| } | ||
| performanceRating | ||
| phones { | ||
| primaryPhoneNumber | ||
| primaryPhoneCountryCode | ||
| primaryPhoneCallingCode | ||
| additionalPhones | ||
| __typename | ||
| } | ||
| position | ||
| workPreference | ||
| updatedAt | ||
| } | ||
| }` | ||
|
|
||
| test('Create and update record', async ({ page }) => { | ||
| await page.getByRole('link', { name: 'People' }).click(); | ||
| await page.getByRole('button', { name: 'Create new record' }).click(); | ||
|
|
||
| // Generate a random email for testing | ||
| const randomEmail = `testuser_${Math.random().toString(36).substring(2, 10)}@example.com`; | ||
| // Fill first name and last name | ||
| const firstNameInput = page.getByRole('textbox', { name: 'First name' }) | ||
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||
| await expect(firstNameInput).toBeFocused(); | ||
| await firstNameInput.fill('John'); | ||
| const lastNameInput = page.getByPlaceholder('Last name'); | ||
| await expect(lastNameInput).toBeVisible(); | ||
| await lastNameInput.fill('Doe'); | ||
| await lastNameInput.press('Enter'); | ||
|
|
||
| // Focus on recordFieldList | ||
| const recordFieldList = page.locator('div[aria-label="Record fields list"]'); | ||
| await expect(recordFieldList).toBeVisible(); | ||
| await recordFieldList.getByText('Emails').first().click(); | ||
|
|
||
| // Fill email | ||
| const emailInput = recordFieldList.getByText('Emails').nth(1); | ||
| await expect(emailInput).toBeVisible(); | ||
| await emailInput.click({ force: true }); | ||
| await page.getByPlaceholder('Email').fill(randomEmail); | ||
| await page.keyboard.press('Enter'); | ||
| await page.keyboard.press('Escape'); | ||
| await recordFieldList.getByText('Emails').first().click(); | ||
|
|
||
|
|
||
| // Fill intro | ||
| const introInput = recordFieldList.getByText('Intro').nth(1); | ||
| await expect(introInput).toBeVisible(); | ||
| await introInput.click({ force: true }); | ||
| await introInput.click({ force: true }); | ||
| await page.getByPlaceholder('Intro').fill('This is an intro'); | ||
| await page.getByPlaceholder('Intro').press('Enter'); | ||
|
|
||
| // Fill URL | ||
| const urlInput = recordFieldList.getByText('Linkedin').nth(1); | ||
| await expect(urlInput).toBeVisible(); | ||
| await urlInput.click({ force: true }); | ||
| await page.getByPlaceholder('URL').fill('linkedin.com/johndoe'); | ||
| await page.getByPlaceholder('URL').press('Enter'); | ||
|
|
||
| // Click on 4th star to rate | ||
| recordFieldList.getByText('Performance Rating').first().click({ force: true }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Missing Prompt for AI agents |
||
| const ratingContainer = recordFieldList.locator('div[aria-label="Rating"]'); | ||
| await ratingContainer.locator('svg').nth(3).click({force: true}); | ||
|
|
||
| // Fill phone field | ||
| const phoneInput = recordFieldList.getByText('Phones').nth(1); | ||
| await expect(phoneInput).toBeVisible(); | ||
| await phoneInput.click({ force: true }); | ||
| await page.getByPlaceholder('Phone').fill('+336 1 122 3344'); | ||
| await page.getByPlaceholder('Phone').press('Enter'); | ||
|
|
||
| // Fill work preference | ||
| await recordFieldList.getByText('Work Preference').first().click({force: true}); | ||
| await recordFieldList.getByText('Work Preference').nth(1).click({force: true}); | ||
| const options = page.getByRole('listbox'); | ||
| await options.getByText('Hybrid').first().click({force: true}); | ||
| recordFieldList.getByText('Work Preference').first().click({force: true}); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Missing Prompt for AI agents |
||
|
|
||
| // Fill company relation | ||
| const companyRelationHeader = page.getByLabel('Company (relation)'); | ||
| await expect(companyRelationHeader).toBeVisible(); | ||
|
|
||
| await companyRelationHeader.locator('.tabler-icon-pencil').click(); | ||
| await page.getByRole('textbox', { name: 'Search' }).fill('Goog'); | ||
| await expect(page.getByRole('option', { name: 'Google' })).toBeVisible(); | ||
| const [updatePersonResponse] = await Promise.all([ | ||
| page.waitForResponse(async (response) => { | ||
| if (!response.url().endsWith('/graphql')) { | ||
| return false; | ||
| } | ||
|
|
||
| const requestBody = response.request().postDataJSON(); | ||
|
|
||
| return requestBody.operationName === 'UpdateOnePerson'; | ||
| }), | ||
| await page.getByRole('option', { name: 'Google' }).click({force: true}) | ||
| ]); | ||
|
|
||
| const body = await updatePersonResponse.json() | ||
github-code-quality[bot] marked this conversation as resolved.
Fixed
Show fixed
Hide fixed
|
||
| const newPersonId = body.data.updatePerson.id; | ||
|
|
||
| // Check data was saved | ||
| const { authToken } = await getAccessAuthToken(page); | ||
| const findOnePersonResponse = await page.request.post(backendGraphQLUrl, { | ||
| headers: { | ||
| Authorization: `Bearer ${authToken}`, | ||
| }, | ||
| data: { | ||
| operationName: 'FindOnePerson', | ||
| query, | ||
| variables: { | ||
| objectRecordId: newPersonId, | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| const findOnePersonReponseBody = await findOnePersonResponse.json(); | ||
|
|
||
| expect(findOnePersonReponseBody.data.person.name.firstName).toBe('John'); | ||
| expect(findOnePersonReponseBody.data.person.name.lastName).toBe('Doe'); | ||
| expect(findOnePersonReponseBody.data.person.emails.primaryEmail).toBe(randomEmail); | ||
| expect(findOnePersonReponseBody.data.person.intro).toBe('This is an intro'); | ||
| expect(findOnePersonReponseBody.data.person.linkedinLink.primaryLinkUrl).toBe('linkedin.com/johndoe'); | ||
| expect(findOnePersonReponseBody.data.person.phones.primaryPhoneNumber).toBe('611223344'); | ||
| expect(findOnePersonReponseBody.data.person.workPreference).toEqual(['HYBRID']); | ||
| expect(findOnePersonReponseBody.data.person.company.name).toBe('Google'); | ||
|
|
||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,11 +1,15 @@ | ||||||
| import { expect, test } from '@playwright/test'; | ||||||
| import { expect, test } from '../lib/fixtures/screenshot'; | ||||||
| import { deleteWorkflow } from '../lib/requests/delete-workflow'; | ||||||
| import { destroyWorkflow } from '../lib/requests/destroy-workflow'; | ||||||
|
|
||||||
| if (process.env.LINK) { | ||||||
| const baseURL = new URL(process.env.LINK).origin; | ||||||
| test.use({ baseURL }); | ||||||
| } | ||||||
| test('Create workflow', async ({ page }) => { | ||||||
| const NEW_WORKFLOW_NAME = 'Test Workflow'; | ||||||
|
|
||||||
| await page.goto('/'); | ||||||
| await page.goto(process.env.LINK); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Prompt for AI agents
Suggested change
✅ Addressed in |
||||||
|
|
||||||
| const workflowsLink = page.getByRole('link', { name: 'Workflows' }); | ||||||
| await workflowsLink.click(); | ||||||
|
|
@@ -25,7 +29,7 @@ test('Create workflow', async ({ page }) => { | |||||
| return requestBody.operationName === 'CreateOneWorkflow'; | ||||||
| }), | ||||||
|
|
||||||
| createWorkflowButton.click(), | ||||||
| createWorkflowButton.click() | ||||||
| ]); | ||||||
|
|
||||||
| const recordName = page.getByTestId('top-bar-title').getByText('Untitled'); | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Bug:
Array.filter()always returns an array (possibly empty), nevernullorundefined. This check will never throw even when no matching cookies exist. Should checktokenCookies.length === 0instead.Prompt for AI agents