In this PR, 
- current basic E2E tests are fixed, and some were added, covering some
basic scenarios
- some tests avec been commented out, until we decide whether they are
worth fixing

The next steps are
- evaluate the flakiness of the tests. Once they've proved not to be
flaky, we should add more tests + re-write the current ones not using
aria-label (cf @lucasbordeau indication).
- We will add them back to the development flow
This commit is contained in:
Marie
2025-12-17 08:48:17 +01:00
committed by GitHub
parent 1cbbd04761
commit 0145920d7e
29 changed files with 783 additions and 447 deletions
+2 -2
View File
@@ -78,7 +78,7 @@ jobs:
- name: Setup environment files
run: |
cp packages/twenty-front/.env.example packages/twenty-front/.env
npx nx reset:env twenty-server
npx nx reset:env:e2e-testing-server twenty-server
- name: Build frontend
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
@@ -103,7 +103,7 @@ jobs:
npm_config_yes=true npx serve -s packages/twenty-front/build -l 3001 &
echo "Waiting for frontend to be ready..."
timeout 60 bash -c 'until curl -s http://localhost:3001; do sleep 2; done'
- name: Start worker
run: |
npx nx run twenty-server:worker &
+1 -1
View File
@@ -3,4 +3,4 @@ FRONTEND_BASE_URL=http://localhost:3001
BACKEND_BASE_URL=http://localhost:3000
DEFAULT_LOGIN=tim@apple.dev
DEFAULT_PASSWORD=tim@apple.dev
WEBSITE_URL=https://twenty.com
WEBSITE_URL=https://twenty.com
@@ -27,6 +27,7 @@ export const test = base.extend<{ screenshotHook: void }>({
},
{ auto: true },
],
baseURL: process.env.LINK ? new URL(process.env.LINK).origin : 'http://localhost:3001',
});
export { expect } from '@playwright/test';
@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { getAuthToken } from '../utils/getAuthToken';
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const createWorkflow = async ({
@@ -11,7 +11,7 @@ export const createWorkflow = async ({
workflowId: string;
workflowName: string;
}) => {
const { authToken } = await getAuthToken(page);
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { getAuthToken } from '../utils/getAuthToken';
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const deleteWorkflow = async ({
@@ -9,7 +9,7 @@ export const deleteWorkflow = async ({
page: Page;
workflowId: string;
}) => {
const { authToken } = await getAuthToken(page);
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
@@ -1,5 +1,5 @@
import { Page } from '@playwright/test';
import { getAuthToken } from '../utils/getAuthToken';
import { type Page } from '@playwright/test';
import { getAccessAuthToken } from '../utils/getAccessAuthToken';
import { backendGraphQLUrl } from './backend';
export const destroyWorkflow = async ({
@@ -9,7 +9,7 @@ export const destroyWorkflow = async ({
page: Page;
workflowId: string;
}) => {
const { authToken } = await getAuthToken(page);
const { authToken } = await getAccessAuthToken(page);
return page.request.post(backendGraphQLUrl, {
headers: {
@@ -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
.token;
return { authToken: token };
};
@@ -1,15 +0,0 @@
import { Page } from '@playwright/test';
export const getAuthToken = async (page: Page) => {
const storageState = await page.context().storageState();
const authCookie = storageState.cookies.find(
(cookie) => cookie.name === 'tokenPair',
);
if (!authCookie) {
throw new Error('No auth cookie found');
}
const token = JSON.parse(decodeURIComponent(authCookie.value)).accessOrWorkspaceAgnosticToken
.token;
return { authToken: token };
};
@@ -34,7 +34,10 @@ export default defineConfig({
expect: {
timeout: 5000,
},
reporter: process.env.CI ? 'github' : 'list',
reporter: [
[process.env.CI ? 'github' : 'list'],
['./reporters/log-summary-reporter.ts'],
],
projects: [
{
name: 'setup',
@@ -0,0 +1,54 @@
import type {
Reporter,
TestCase,
TestResult,
} from '@playwright/test/reporter';
class LogSummaryReporter implements Reporter {
private passed: string[] = [];
private failed: string[] = [];
onTestEnd(test: TestCase, result: TestResult): void {
const name = test.titlePath().join(' ');
if (result.status === 'passed') {
this.passed.push(name);
return;
}
if (result.status === 'failed' || result.status === 'timedOut') {
this.failed.push(name);
}
}
onEnd(): void {
const passedSet = new Set(this.passed);
const failedSet = new Set(this.failed);
const flaky: string[] = [];
for (const testName of passedSet) {
if (failedSet.has(testName)) {
flaky.push(testName);
}
}
const uniquePassed = this.passed.filter((testName) => !flaky.includes(testName));
const uniqueFailed = this.failed.filter((testName) => !flaky.includes(testName));
console.log('\n=== Playwright summary ===');
if (uniquePassed.length) {
console.log('Passed:');
uniquePassed.forEach((testName) => console.log(`${testName}`));
}
if (uniqueFailed.length) {
console.log('Failed:');
uniqueFailed.forEach((testName) => console.log(`${testName}`));
}
if (flaky.length) {
console.log('Flaky:');
flaky.forEach((testName) => console.log(` ⚠️ ${testName}`));
}
}
}
export default LogSummaryReporter;
@@ -49,12 +49,10 @@ test('Sign up with invite link via email', async ({
await leftMenu.goToSettings();
await settingsPage.goToProfileSection();
await profileSection.deleteAccount();
await expect(page.getByText('Account Deletion')).toBeVisible();
await confirmationModal.typePlaceholderToInput();
await confirmationModal.clickConfirmButton();
await Promise.all([
page.waitForURL('/welcome'),
confirmationModal.clickConfirmButton(),
]);
await page.waitForURL('**/welcome');
});
});
@@ -0,0 +1,54 @@
import { expect, test } from '../lib/fixtures/screenshot';
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.getByTestId('hide-group-').click(),
page.waitForRequest((req) => {
return req.url().includes('/metadata') &&
req.method() === 'POST';
})]);
await expect(page.getByText('No value')).not.toBeVisible();
});
})
@@ -0,0 +1,158 @@
import { expect, test } from '../lib/fixtures/screenshot';
import { backendGraphQLUrl } from '../lib/requests/backend';
import { getAccessAuthToken } from '../lib/utils/getAccessAuthToken';
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' })
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.getByTestId('record-fields-list-container');
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 });
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});
// Fill company relation
const companyRelationHeader = page.getByTestId('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()
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');
});
@@ -29,7 +29,13 @@ test('Login test', async ({ loginPage, page }) => {
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
await page.waitForLoadState('networkidle');
await loginPage.clickSignInButton();
await page.waitForLoadState('networkidle');
await expect(page.getByText(/Welcome to .+/)).not.toBeVisible();
await expect(page.getByText('Choose a workspace')).toBeVisible();
await page.getByText('Apple', {exact: true}).click();
await page.waitForFunction(() => window.location.href.includes('verify'));
await page.waitForFunction(() => !window.location.href.includes('verify'));
process.env.LINK = page.url();
},
);
@@ -37,6 +43,5 @@ test('Login test', async ({ loginPage, page }) => {
await page.context().storageState({
path: path.resolve(__dirname, '..', '.auth', 'user.json'),
});
process.env.LINK = page.url();
});
});
@@ -1,11 +1,11 @@
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';
test('Create workflow', async ({ page }) => {
const NEW_WORKFLOW_NAME = 'Test Workflow';
await page.goto('/');
await page.goto(process.env.LINK);
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
@@ -25,7 +25,7 @@ test('Create workflow', async ({ page }) => {
return requestBody.operationName === 'CreateOneWorkflow';
}),
createWorkflowButton.click(),
createWorkflowButton.click()
]);
const recordName = page.getByTestId('top-bar-title').getByText('Untitled');
@@ -1,150 +1,150 @@
import { expect } from '@playwright/test';
import { test } from '../lib/fixtures/blank-workflow';
// import { expect } from '@playwright/test';
// import { test } from '../lib/fixtures/blank-workflow';
test('The workflow run visualizer shows the executed draft version without the last draft changes', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('manual');
// test('The workflow run visualizer shows the executed draft version without the last draft changes', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('manual');
const manualTriggerAvailabilitySelect = page.getByRole('button', {
name: 'When record is selected',
});
// const manualTriggerAvailabilitySelect = page.getByRole('button', {
// name: 'When record is selected',
// });
await manualTriggerAvailabilitySelect.click();
// await manualTriggerAvailabilitySelect.click();
const alwaysAvailableOption = page.getByText('When no record is selected');
// const alwaysAvailableOption = page.getByText('When no record is selected');
await alwaysAvailableOption.click();
// await alwaysAvailableOption.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('create-record');
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
// const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
await launchTestButton.click();
// await launchTestButton.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
await workflowVisualizer.deleteStep(firstStepId);
// await workflowVisualizer.deleteStep(firstStepId);
await page.goto('/objects/workflowRuns');
// await page.goto('/objects/workflowRuns');
const recordTableRowForWorkflowRun = page
.getByRole('row', {
name: workflowVisualizer.workflowName,
})
.first();
// const recordTableRowForWorkflowRun = page
// .getByRole('row', {
// name: workflowVisualizer.workflowName,
// })
// .first();
const linkToWorkflowRun = recordTableRowForWorkflowRun
.getByRole('link', {
name: workflowVisualizer.workflowName,
})
.first();
// const linkToWorkflowRun = recordTableRowForWorkflowRun
// .getByRole('link', {
// name: workflowVisualizer.workflowName,
// })
// .first();
await linkToWorkflowRun.click({ force: true });
// await linkToWorkflowRun.click({ force: true });
const workflowRunNameElement = page
.getByText(`#1 - ${workflowVisualizer.workflowName}`)
.nth(1);
// const workflowRunNameElement = page
// .getByText(`#1 - ${workflowVisualizer.workflowName}`)
// .nth(1);
await expect(workflowRunNameElement).toBeVisible();
// await expect(workflowRunNameElement).toBeVisible();
const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
// const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
await expect(executedFirstStepNode).toBeVisible();
// await expect(executedFirstStepNode).toBeVisible();
await executedFirstStepNode.click();
// await executedFirstStepNode.click();
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Create Record',
);
});
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Create Record',
// );
// });
test('Workflow Runs with a pending form step can be opened in the side panel and then in full screen', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('manual');
// test('Workflow Runs with a pending form step can be opened in the side panel and then in full screen', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('manual');
const manualTriggerAvailabilitySelect = page.getByRole('button', {
name: 'When record is selected',
});
// const manualTriggerAvailabilitySelect = page.getByRole('button', {
// name: 'When record is selected',
// });
await manualTriggerAvailabilitySelect.click();
// await manualTriggerAvailabilitySelect.click();
const alwaysAvailableOption = page.getByText('When no record is selected');
// const alwaysAvailableOption = page.getByText('When no record is selected');
await alwaysAvailableOption.click();
// await alwaysAvailableOption.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('form');
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('form');
const addFormFieldButton = page.getByText('Add Field', { exact: true });
// const addFormFieldButton = page.getByText('Add Field', { exact: true });
await addFormFieldButton.click();
// await addFormFieldButton.click();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
// const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
await launchTestButton.click();
// await launchTestButton.click();
const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
// const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Form',
{
timeout: 30_000,
},
);
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Form',
// {
// timeout: 30_000,
// },
// );
await workflowVisualizer.goBackInCommandMenu.click();
// await workflowVisualizer.goBackInCommandMenu.click();
const workflowRunNameInCommandMenu =
workflowVisualizer.commandMenu.getByText(workflowRunName);
// const workflowRunNameInCommandMenu =
// workflowVisualizer.commandMenu.getByText(workflowRunName);
await expect(workflowRunNameInCommandMenu).toBeVisible();
// await expect(workflowRunNameInCommandMenu).toBeVisible();
await workflowVisualizer.commandMenu
.locator(workflowVisualizer.triggerNode)
.click();
// await workflowVisualizer.commandMenu
// .locator(workflowVisualizer.triggerNode)
// .click();
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Launch manually',
);
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Launch manually',
// );
await workflowVisualizer.goBackInCommandMenu.click();
// await workflowVisualizer.goBackInCommandMenu.click();
const formStep = workflowVisualizer.commandMenu.locator(
workflowVisualizer.getStepNode(firstStepId),
);
// const formStep = workflowVisualizer.commandMenu.locator(
// workflowVisualizer.getStepNode(firstStepId),
// );
await formStep.click();
// await formStep.click();
await workflowVisualizer.goBackInCommandMenu.click();
// await workflowVisualizer.goBackInCommandMenu.click();
const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole(
'button',
{ name: 'Open' },
);
// const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole(
// 'button',
// { name: 'Open' },
// );
await openInFullScreenButton.click();
// await openInFullScreenButton.click();
const workflowRunNameInShowPage = page
.getByText(`#1 - ${workflowVisualizer.workflowName}`)
.nth(1);
// const workflowRunNameInShowPage = page
// .getByText(`#1 - ${workflowVisualizer.workflowName}`)
// .nth(1);
await expect(workflowRunNameInShowPage).toBeVisible();
// await expect(workflowRunNameInShowPage).toBeVisible();
// Expect the side panel to be opened by default on the form.
await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
'Form',
);
});
// // Expect the side panel to be opened by default on the form.
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Form',
// );
// });
@@ -1,163 +1,161 @@
import { expect } from '@playwright/test';
import { test } from '../lib/fixtures/blank-workflow';
test('Use an old version as draft', async ({ workflowVisualizer, page }) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Use an old version as draft', async ({ workflowVisualizer, page }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await workflowVisualizer.createStep('delete-record');
// await workflowVisualizer.createStep('delete-record');
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
'Delete Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
// const workflowsLink = page.getByRole('link', { name: 'Workflows' });
// await workflowsLink.click();
await workflowVisualizer.setWorkflowsOpenInMode('record-page');
// await workflowVisualizer.setWorkflowsOpenInMode('record-page');
const recordTableRowForWorkflow = page.getByRole('row', {
name: workflowVisualizer.workflowName,
});
// const recordTableRowForWorkflow = page.getByRole('row', {
// name: workflowVisualizer.workflowName,
// });
const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
name: workflowVisualizer.workflowName,
});
await expect(linkToWorkflow).toBeVisible();
// const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
// name: workflowVisualizer.workflowName,
// });
// await expect(linkToWorkflow).toBeVisible();
const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
'link',
{
name: 'v1',
},
);
// const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
// 'link',
// {
// name: 'v1',
// },
// );
await linkToFirstWorkflowVersion.click({ force: true });
// await linkToFirstWorkflowVersion.click({ force: true });
await expect(workflowVisualizer.workflowStatus).toHaveText('Archived');
await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.workflowStatus).toHaveText('Archived');
// await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await workflowVisualizer.useAsDraftButton.click();
// await workflowVisualizer.useAsDraftButton.click();
await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
// await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
});
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// });
test('Use an old version as draft while having a pending draft version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Use an old version as draft while having a pending draft version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await workflowVisualizer.createStep('delete-record');
// await workflowVisualizer.createStep('delete-record');
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
'Delete Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.closeSidePanel();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
// const workflowsLink = page.getByRole('link', { name: 'Workflows' });
// await workflowsLink.click();
const recordTableRowForWorkflow = page.getByRole('row', {
name: workflowVisualizer.workflowName,
});
// const recordTableRowForWorkflow = page.getByRole('row', {
// name: workflowVisualizer.workflowName,
// });
const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
name: workflowVisualizer.workflowName,
});
await expect(linkToWorkflow).toBeVisible();
// const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
// name: workflowVisualizer.workflowName,
// });
// await expect(linkToWorkflow).toBeVisible();
const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
'link',
{
name: 'v1',
},
);
// const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
// 'link',
// {
// name: 'v1',
// },
// );
await linkToFirstWorkflowVersion.click({ force: true });
// await linkToFirstWorkflowVersion.click({ force: true });
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.useAsDraftButton).toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await workflowVisualizer.useAsDraftButton.click();
// await workflowVisualizer.useAsDraftButton.click();
await expect(workflowVisualizer.overrideDraftButton).toBeVisible();
// await expect(workflowVisualizer.overrideDraftButton).toBeVisible();
await workflowVisualizer.overrideDraftButton.click();
// await workflowVisualizer.overrideDraftButton.click();
await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
// await page.waitForURL(`/object/workflow/${workflowVisualizer.workflowId}`);
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await expect(workflowVisualizer.activateWorkflowButton).toBeVisible();
await expect(workflowVisualizer.discardDraftButton).toBeVisible();
});
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.useAsDraftButton).not.toBeVisible();
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.activateWorkflowButton).toBeVisible();
// await expect(workflowVisualizer.discardDraftButton).toBeVisible();
// });
@@ -1,218 +1,218 @@
import { expect } from '@playwright/test';
import { test } from '../lib/fixtures/blank-workflow';
// import { expect } from '@playwright/test';
// import { test } from '../lib/fixtures/blank-workflow';
test('Create workflow with every possible step', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Create workflow with every possible step', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
await workflowVisualizer.createStep('update-record');
await workflowVisualizer.createStep('delete-record');
await workflowVisualizer.createStep('code');
await workflowVisualizer.createStep('send-email');
// await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('update-record');
// await workflowVisualizer.createStep('delete-record');
// await workflowVisualizer.createStep('code');
// await workflowVisualizer.createStep('send-email');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
const draftWorkflowStatus =
workflowVisualizer.workflowStatus.getByText('Draft');
// const draftWorkflowStatus =
// workflowVisualizer.workflowStatus.getByText('Draft');
await expect(draftWorkflowStatus).toBeVisible();
// await expect(draftWorkflowStatus).toBeVisible();
await workflowVisualizer.activateWorkflowButton.click();
// await workflowVisualizer.activateWorkflowButton.click();
const activeWorkflowStatus =
workflowVisualizer.workflowStatus.getByText('Active');
// const activeWorkflowStatus =
// workflowVisualizer.workflowStatus.getByText('Active');
await expect(draftWorkflowStatus).not.toBeVisible();
await expect(activeWorkflowStatus).toBeVisible();
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
});
// await expect(draftWorkflowStatus).not.toBeVisible();
// await expect(activeWorkflowStatus).toBeVisible();
// await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
// await expect(workflowVisualizer.deactivateWorkflowButton).toBeVisible();
// });
test('Delete steps from draft version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Delete steps from draft version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('create-record');
const { createdStepId: secondStepId } =
await workflowVisualizer.createStep('update-record');
const { createdStepId: thirdStepId } =
await workflowVisualizer.createStep('delete-record');
const { createdStepId: fourthStepId } =
await workflowVisualizer.createStep('code');
const { createdStepId: fifthStepId } =
await workflowVisualizer.createStep('send-email');
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
// const { createdStepId: secondStepId } =
// await workflowVisualizer.createStep('update-record');
// const { createdStepId: thirdStepId } =
// await workflowVisualizer.createStep('delete-record');
// const { createdStepId: fourthStepId } =
// await workflowVisualizer.createStep('code');
// const { createdStepId: fifthStepId } =
// await workflowVisualizer.createStep('send-email');
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
'Update Record',
'Delete Record',
'Code - Serverless Function',
'Send Email',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(5);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// 'Send Email',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(5);
await workflowVisualizer.deleteStep(firstStepId);
// await workflowVisualizer.deleteStep(firstStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Update Record',
'Delete Record',
'Code - Serverless Function',
'Send Email',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// 'Send Email',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
await workflowVisualizer.deleteStep(fifthStepId);
// await workflowVisualizer.deleteStep(fifthStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Update Record',
'Delete Record',
'Code - Serverless Function',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
await workflowVisualizer.deleteStep(secondStepId);
// await workflowVisualizer.deleteStep(secondStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Delete Record',
'Code - Serverless Function',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Delete Record',
// 'Code - Serverless Function',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
await workflowVisualizer.deleteStep(fourthStepId);
// await workflowVisualizer.deleteStep(fourthStepId);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Delete Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await workflowVisualizer.deleteStep(thirdStepId);
// await workflowVisualizer.deleteStep(thirdStepId);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
await Promise.all([
page.reload(),
// await Promise.all([
// page.reload(),
expect(workflowVisualizer.triggerNode).toBeVisible(),
]);
// expect(workflowVisualizer.triggerNode).toBeVisible(),
// ]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
});
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
// });
test('Add a step to an active version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Add a step to an active version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
workflowVisualizer.activateWorkflowButton.click(),
]);
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
// await expect(workflowVisualizer.activateWorkflowButton).not.toBeVisible();
const assertEndState = async () => {
await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is created',
);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
};
// const assertEndState = async () => {
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is created',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// };
await assertEndState();
// await assertEndState();
await page.reload();
// await page.reload();
await assertEndState();
});
// await assertEndState();
// });
test('Replace the trigger of an active version', async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.createInitialTrigger('record-created');
// test('Replace the trigger of an active version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.createStep('create-record');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
workflowVisualizer.activateWorkflowButton.click(),
]);
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
workflowVisualizer.deleteTrigger(),
]);
// workflowVisualizer.deleteTrigger(),
// ]);
await workflowVisualizer.createInitialTrigger('record-deleted');
// await workflowVisualizer.createInitialTrigger('record-deleted');
await workflowVisualizer.background.click();
// await workflowVisualizer.background.click();
await Promise.all([
expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
workflowVisualizer.activateWorkflowButton.click(),
]);
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
await page.reload();
// await page.reload();
await expect(workflowVisualizer.triggerNode).toContainText(
'Record is deleted',
);
await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
await expect(workflowVisualizer.getAllStepNodes()).toContainText([
'Create Record',
]);
});
// await expect(workflowVisualizer.triggerNode).toContainText(
// 'Record is deleted',
// );
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Create Record',
// ]);
// });
test("Nodes can't be deleted by pressing Backspace or Delete keys", async ({
workflowVisualizer,
page,
}) => {
await workflowVisualizer.triggerNode.click();
// test("Nodes can't be deleted by pressing Backspace or Delete keys", async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.triggerNode.click();
await page.keyboard.press('Backspace');
await page.keyboard.press('Delete');
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
await expect(workflowVisualizer.triggerNode).toBeVisible();
// await expect(workflowVisualizer.triggerNode).toBeVisible();
const { createdStepId: firstStepId } =
await workflowVisualizer.createStep('create-record');
const firstStep = workflowVisualizer.getStepNode(firstStepId);
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
// const firstStep = workflowVisualizer.getStepNode(firstStepId);
await firstStep.click();
// await firstStep.click();
await expect(workflowVisualizer.getDeleteNodeButton(firstStep)).toBeVisible();
// await expect(workflowVisualizer.getDeleteNodeButton(firstStep)).toBeVisible();
await page.keyboard.press('Backspace');
await page.keyboard.press('Delete');
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
await expect(firstStep).toBeVisible();
// await expect(firstStep).toBeVisible();
await workflowVisualizer.addStepButton.click();
// await workflowVisualizer.addStepButton.click();
await page.keyboard.press('Backspace');
await page.keyboard.press('Delete');
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
await expect(workflowVisualizer.addStepButton).toBeVisible();
});
// await expect(workflowVisualizer.addStepButton).toBeVisible();
// });
@@ -25,6 +25,7 @@ import {
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { getAvailableWorkspacePathAndSearchParams } from '@/auth/utils/availableWorkspacesUtils';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { isDefined } from 'twenty-shared/utils';
@@ -155,6 +156,7 @@ export const SignInUpGlobalScopeForm = () => {
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const { isCaptchaReady } = useCaptcha();
const [showErrors, setShowErrors] = useState(false);
@@ -284,7 +286,9 @@ export const SignInUpGlobalScopeForm = () => {
)}
<MainButton
disabled={
isRequestingCaptchaToken || form.formState.isSubmitting
isRequestingCaptchaToken ||
form.formState.isSubmitting ||
(signInUpStep !== SignInUpStep.Password && !isCaptchaReady)
}
title={
signInUpStep === SignInUpStep.Password
@@ -90,7 +90,7 @@ export const RecordFieldList = ({
instanceId,
}}
>
<PropertyBox>
<PropertyBox dataTestId="record-fields-list-container">
{isPrefetchLoading ? (
<PropertyBoxSkeletonLoader />
) : (
@@ -13,15 +13,16 @@ const StyledRecordDetailSectionContainer = styled(Section)`
const StyledHeader = styled.header<{
isDropdownOpen?: boolean;
areRecordsAvailable?: boolean;
ariaLabel?: string;
}>`
padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
align-items: center;
justify-content: space-between;
display: flex;
height: 24px;
justify-content: space-between;
margin-bottom: ${({ theme, areRecordsAvailable }) =>
areRecordsAvailable && theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledTitle = styled.div`
@@ -51,6 +52,7 @@ type RecordDetailSectionContainerProps = {
rightAdornment?: React.ReactNode;
hideRightAdornmentOnMouseLeave?: boolean;
areRecordsAvailable?: boolean;
dataTestId?: string;
};
export const RecordDetailSectionContainer = ({
@@ -60,6 +62,7 @@ export const RecordDetailSectionContainer = ({
rightAdornment,
hideRightAdornmentOnMouseLeave = true,
areRecordsAvailable = false,
dataTestId,
}: RecordDetailSectionContainerProps) => {
const [isHovered, setIsHovered] = useState(false);
return (
@@ -68,6 +71,7 @@ export const RecordDetailSectionContainer = ({
areRecordsAvailable={areRecordsAvailable}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
data-testid={dataTestId}
>
<StyledTitle>
<StyledTitleLabel>{title}</StyledTitleLabel>
@@ -185,6 +185,7 @@ export const RecordDetailRelationSection = ({
}}
>
<RecordDetailSectionContainer
dataTestId={`${fieldDefinition.label.toLowerCase().replace(' ', '-')}-relation`}
title={fieldDefinition.label}
link={
isToManyObjects
@@ -5,9 +5,9 @@ import {
} from '@/object-record/record-group/types/RecordGroupDefinition';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/components';
import { IconEye, IconEyeOff } from 'twenty-ui/display';
import { MenuItemDraggable } from 'twenty-ui/navigation';
import { Tag } from 'twenty-ui/components';
type RecordGroupMenuItemDraggableProps = {
recordGroupId: string;
@@ -36,6 +36,12 @@ export const RecordGroupMenuItemDraggable = ({
const iconButtons = [
{
Icon: recordGroup.isVisible ? IconEyeOff : IconEye,
ariaLabel: recordGroup.isVisible
? `Hide group ${recordGroup.value}`
: `Show group ${recordGroup.value}`,
dataTestId: recordGroup.isVisible
? `hide-group-${recordGroup.value?.toLowerCase().replace(' ', '-') ?? ''}`
: `show-group-${recordGroup.value?.toLowerCase().replace(' ', '-') ?? ''}`,
onClick: () =>
onVisibilityChange({
...recordGroup,
@@ -1,11 +1,15 @@
import isPropValid from '@emotion/is-prop-valid';
import styled from '@emotion/styled';
interface PropertyBoxProps {
children: React.ReactNode;
className?: string;
dataTestId?: string;
}
const StyledPropertyBoxContainer = styled.div`
const StyledPropertyBoxContainer = styled('div', {
shouldForwardProp: isPropValid,
})`
align-self: stretch;
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
@@ -17,8 +21,12 @@ const StyledPropertyBoxContainer = styled.div`
padding-right: ${({ theme }) => theme.spacing(2)};
`;
export const PropertyBox = ({ children, className }: PropertyBoxProps) => (
<StyledPropertyBoxContainer className={className}>
export const PropertyBox = ({
children,
className,
dataTestId,
}: PropertyBoxProps) => (
<StyledPropertyBoxContainer className={className} data-testid={dataTestId}>
{children}
</StyledPropertyBoxContainer>
);
@@ -94,6 +94,7 @@ export const ViewPickerCreateButton = () => {
<Button
title={t`Create`}
onClick={handleCreateButtonClick}
ariaLabel="Create new view"
accent="blue"
fullWidth
size="small"
@@ -0,0 +1,8 @@
NODE_ENV=development
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
REDIS_URL=redis://localhost:6379
APP_SECRET=replace_me_with_a_random_string
SIGN_IN_PREFILLED=true
IS_MULTIWORKSPACE_ENABLED=true
FRONTEND_URL=http://localhost:3001
+10
View File
@@ -84,6 +84,16 @@
"command": "cp .env.example .env"
}
},
"reset:env:e2e-testing-server": {
"executor": "nx:run-commands",
"inputs": ["{projectRoot}/.env.e2e-testing-server"],
"outputs": ["{projectRoot}/.env"],
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "cp .env.e2e-testing-server .env"
}
},
"command": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
@@ -23,6 +23,8 @@ export type LightIconButtonGroupProps = Pick<
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
disabled?: boolean;
ariaLabel?: string;
dataTestId?: string;
}[];
};
@@ -32,26 +34,30 @@ export const LightIconButtonGroup = ({
className,
}: LightIconButtonGroupProps) => (
<StyledLightIconButtonGroupContainer className={className}>
{iconButtons.map(({ Wrapper, Icon, accent, onClick }, index) => {
const iconButton = (
<LightIconButton
key={`light-icon-button-${index}`}
Icon={Icon}
accent={accent}
disabled={!onClick}
onClick={onClick}
size={size}
/>
);
{iconButtons.map(
({ Wrapper, Icon, accent, onClick, ariaLabel, dataTestId }, index) => {
const iconButton = (
<LightIconButton
key={`light-icon-button-${index}`}
Icon={Icon}
accent={accent}
disabled={!onClick}
onClick={onClick}
size={size}
aria-label={ariaLabel}
testId={dataTestId}
/>
);
return Wrapper ? (
<Wrapper
key={`light-icon-button-wrapper-${index}`}
iconButton={iconButton}
/>
) : (
iconButton
);
})}
return Wrapper ? (
<Wrapper
key={`light-icon-button-wrapper-${index}`}
iconButton={iconButton}
/>
) : (
iconButton
);
},
)}
</StyledLightIconButtonGroupContainer>
);
@@ -24,6 +24,8 @@ export type MenuItemIconButton = {
Icon: IconComponent;
accent?: LightIconButtonProps['accent'];
onClick?: (event: MouseEvent<any>) => void;
ariaLabel?: string;
dataTestId?: string;
};
export type MenuItemProps = {