snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
import { test as base } from '../../lib/fixtures/screenshot';
import { ConfirmationModal } from '../../lib/pom/helper/confirmationModal';
import { LeftMenu } from '../../lib/pom/leftMenu';
import { LoginPage } from '../../lib/pom/loginPage';
import { MembersSection } from '../../lib/pom/settings/membersSection';
import { ProfileSection } from '../../lib/pom/settings/profileSection';
import { SettingsPage } from '../../lib/pom/settingsPage';
type Fixtures = {
confirmationModal: ConfirmationModal;
loginPage: LoginPage;
leftMenu: LeftMenu;
settingsPage: SettingsPage;
membersSection: MembersSection;
profileSection: ProfileSection;
};
export const test = base.extend<Fixtures>({
confirmationModal: async ({ page }, use) => {
const confirmationModal = new ConfirmationModal(page);
await use(confirmationModal);
},
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
leftMenu: async ({ page }, use) => {
const leftMenu = new LeftMenu(page);
await use(leftMenu);
},
settingsPage: async ({ page }, use) => {
const settingsPage = new SettingsPage(page);
await use(settingsPage);
},
membersSection: async ({ page }, use) => {
const membersSection = new MembersSection(page);
await use(membersSection);
},
profileSection: async ({ page }, use) => {
const profileSection = new ProfileSection(page);
await use(profileSection);
},
});
export { expect } from '@playwright/test';

View File

@@ -0,0 +1,97 @@
import { randomUUID } from 'crypto';
import { expect, test } from './fixture';
// Signing up on the workspace subdomain the shared fixture points at is
// refused, so create the workspace from the base domain instead.
test.use({
storageState: { cookies: [], origins: [] },
baseURL: process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001',
});
test('New workspace signup goes through every onboarding stage', async ({
page,
loginPage,
}) => {
test.setTimeout(180000);
const suffix = randomUUID().replaceAll('-', '').slice(0, 12);
const email = `test${suffix}@apple.dev`;
const workspaceName = `Test ${suffix}`;
const subdomain = `test-${suffix}`;
await test.step('Create a new account', async () => {
await page.goto('/welcome');
await loginPage.clickLoginWithEmailIfVisible();
await loginPage.typeEmail(email);
await loginPage.clickContinueButton();
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
await loginPage.clickSignUpButton();
});
await test.step('Create the workspace', async () => {
await expect(page.getByText('Create your workspace')).toBeVisible();
await loginPage.typeWorkspaceName(workspaceName);
await loginPage.typeSubdomain(subdomain);
await loginPage.clickCreateWorkspaceButton();
});
await test.step('Workspace activation stage', async () => {
await page.waitForURL('**/workspace-activation', { timeout: 90000 });
});
const syncEmailsHeading = page.getByText('Import your contacts');
const installAppsHeading = page.getByText('Install your first apps');
const createProfileHeading = page.getByText('Create profile');
// Both stages auto-skip when the instance has no connected-account provider
// and no vetted marketplace app, which is how the e2e server is configured.
await test.step('Sync-email stage (when shown)', async () => {
await expect(
syncEmailsHeading.or(installAppsHeading).or(createProfileHeading),
).toBeVisible({
timeout: 90000,
});
if (!(await syncEmailsHeading.isVisible())) {
return;
}
await loginPage.clickSkipOnboardingStep();
await expect(installAppsHeading).toBeVisible();
await test.step('Goes back to the skipped sync-email stage', async () => {
await page.getByRole('button', { name: 'Go back' }).click();
await expect(syncEmailsHeading).toBeVisible();
await page.reload();
await expect(syncEmailsHeading).toBeVisible({
timeout: 30000,
});
await loginPage.clickSkipOnboardingStep();
await expect(installAppsHeading).toBeVisible();
});
});
await test.step('Install-apps stage (when shown)', async () => {
if (await installAppsHeading.isVisible()) {
await loginPage.clickSkipOnboardingStep();
}
});
await test.step('Create-profile stage', async () => {
await expect(page.getByText('Create profile')).toBeVisible();
await loginPage.typeFirstName('Ada');
await loginPage.typeLastName('Lovelace');
await loginPage.clickContinueButton();
});
await test.step('Invite-team stage', async () => {
await expect(page.getByText('Invite your team')).toBeVisible();
await loginPage.clickSkipOnboardingStep();
});
await test.step('Lands in the app', async () => {
await expect(page.getByTestId('workspace-dropdown')).toBeVisible();
});
});

View File

@@ -0,0 +1,116 @@
import { expect, test as base } from '@playwright/test';
import { LoginPage } from '../../lib/pom/loginPage';
const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
});
const loginAndSelectWorkspace = async (loginPage: LoginPage, page: any) => {
await page.waitForLoadState('domcontentloaded');
await loginPage.clickLoginWithEmailIfVisible();
await loginPage.typeEmail(process.env.DEFAULT_LOGIN!);
await loginPage.clickContinueButton();
await loginPage.typePassword(process.env.DEFAULT_PASSWORD!);
await page.waitForLoadState('domcontentloaded');
await loginPage.clickSignInButton();
await page.waitForLoadState('domcontentloaded');
const workspaceButton = page.getByText('Apple', { exact: true });
await workspaceButton.waitFor({ state: 'visible', timeout: 30000 }).catch(
() => {
// Single workspace mode — no workspace selection
},
);
if (await workspaceButton.isVisible()) {
await workspaceButton.click();
}
await page.waitForFunction(
() =>
!window.location.href.includes('verify') &&
!window.location.href.includes('welcome'),
{ timeout: 30000 },
);
};
test.describe('Return-to-path after login', () => {
test.use({ storageState: { cookies: [], origins: [] } });
test('should redirect to deep link after login', async ({
page,
loginPage,
}) => {
const deepLink = '/settings/accounts';
await test.step('Navigate to deep link while logged out', async () => {
await page.goto(deepLink);
await page.waitForURL('**/welcome');
await page.waitForLoadState('domcontentloaded');
});
await test.step('Log in and select workspace', async () => {
await loginAndSelectWorkspace(loginPage, page);
});
await test.step(
'Verify redirected to original deep link',
async () => {
await page.waitForURL(`**${deepLink}`, {
timeout: 30000,
waitUntil: 'commit',
});
expect(new URL(page.url()).pathname).toBe(deepLink);
},
);
await test.step(
'Verify return-to-path query param was consumed',
async () => {
const url = new URL(page.url());
expect(url.searchParams.has('returnToPath')).toBe(false);
},
);
});
test('should preserve path with query params across login', async ({
page,
loginPage,
}) => {
const targetPath = '/settings/accounts';
const targetPathWithParams = `${targetPath}?tab=emails&filter=unread`;
await test.step(
'Navigate to path with query params while logged out',
async () => {
await page.goto(targetPathWithParams);
await page.waitForURL('**/welcome');
await page.waitForLoadState('domcontentloaded');
},
);
await test.step('Log in and select workspace', async () => {
await loginAndSelectWorkspace(loginPage, page);
});
await test.step(
'Verify redirected to original path with query params',
async () => {
await page.waitForURL(`**${targetPath}**`, {
timeout: 30000,
waitUntil: 'commit',
});
const url = new URL(page.url());
expect(url.pathname).toBe(targetPath);
expect(url.searchParams.get('tab')).toBe('emails');
expect(url.searchParams.get('filter')).toBe('unread');
},
);
});
});

View File

@@ -0,0 +1,59 @@
import { randomUUID } from 'crypto';
import { expect, test } from './fixture';
test('Sign up with invite link via email', async ({
page,
loginPage,
leftMenu,
membersSection,
settingsPage,
profileSection,
confirmationModal,
}) => {
const email = `test${randomUUID().replaceAll('-', '')}@apple.dev`;
const firstName = 'John';
const lastName = 'Doe';
const inviteLink: string =
await test.step('Go to Settings and copy invite link', async () => {
await page.goto(process.env.LINK); // skip login page (and redirect) when running on environments with multi-workspace enabled
await leftMenu.goToSettings();
await settingsPage.goToMembersSection();
await membersSection.copyInviteLink();
return await page.evaluate('navigator.clipboard.readText()');
});
await test.step('Go to invite link', async () => {
await settingsPage.logout();
// Logging out replaces the document, which would interrupt the goto below.
await page.waitForURL('**/welcome');
await page.goto(inviteLink);
await expect(page.getByText(/Join .+ team/)).toBeVisible();
});
await test.step('Create new account', async () => {
await loginPage.clickLoginWithEmailIfVisible();
await loginPage.typeEmail(email);
await loginPage.clickContinueButton();
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
await loginPage.clickSignUpButton();
await expect(page.getByText('Create profile')).toBeVisible();
await expect(page.getByPlaceholder('Head of Partnerships')).toBeVisible();
await loginPage.typeFirstName(firstName);
await loginPage.typeLastName(lastName);
await loginPage.clickContinueButton();
});
await test.step('Delete account from workspace', async () => {
await expect(page.getByTestId('workspace-dropdown')).toBeVisible();
await leftMenu.goToSettings();
await settingsPage.goToProfileSection();
await profileSection.deleteAccount();
await expect(page.getByText('Account Deletion')).toBeVisible();
await confirmationModal.typePlaceholderToInput();
await confirmationModal.clickConfirmButton();
await page.waitForURL('**/welcome');
});
});

View File

@@ -0,0 +1,57 @@
import { expect, test } from '../lib/fixtures/screenshot';
test.describe.serial('Create Kanban View', () => {
// Unique per run: retries of this serial group re-run field creation against
// the same database, and a duplicate label makes the form unsubmittable.
const industryLabel = `Industry ${Date.now()}`;
test('Create Industry Select Field', async ({ page }) => {
await page.getByRole('button', { name: 'Settings', exact: true }).click();
await page.getByRole('link', { name: 'Data model' }).click();
await page.getByRole('link', { name: 'Opportunities' }).click();
await expect(page.getByRole('button', { name: 'New Field' })).toBeVisible();
await page.getByRole('button', { name: 'New 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(industryLabel);
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=${industryLabel}`);
await expect(page.getByText(industryLabel)).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: industryLabel }).click();
await page.getByRole('button', { name: 'Create new view' }).click();
await expect(page.getByText('Food')).toBeVisible({ timeout: 30000 });
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();
});
})

View File

@@ -0,0 +1,164 @@
import { expect, test } from '../lib/fixtures/screenshot';
import { postBackendGraphQL } from '../lib/requests/post-backend-graphql';
const query = `query FindOnePerson($objectRecordId: UUID!) {
person(
filter: {or: [{deletedAt: {is: NULL}}, {deletedAt: {is: NOT_NULL}}], id: {eq: $objectRecordId}}
) {
previousCompanies {
edges {
node {
company {
name
}
}
}
}
emails {
primaryEmail
additionalEmails
__typename
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks {
label
url
}
__typename
}
name {
firstName
lastName
__typename
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
primaryPhoneCallingCode
additionalPhones {
number
callingCode
countryCode
}
__typename
}
position
workPreference
updatedAt
}
}`;
test('Create and update record', async ({ page }) => {
await page.goto('/objects/people');
await page.getByRole('button', { name: 'Create new Person' }).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: 'F‌‌irst name' });
await expect(firstNameInput).toBeFocused();
await firstNameInput.fill('John');
const lastNameInput = page.getByPlaceholder('L‌‌ast name');
await expect(lastNameInput).toBeVisible();
await lastNameInput.fill('Doe');
await lastNameInput.press('Enter');
// Focus on recordFieldList
const recordFieldList = page.getByTestId('record-fields-widget');
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 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
await recordFieldList.getByText('Linkedin').first().click();
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
await 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
await recordFieldList.getByText('Phones').first().click();
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 });
// Open full record page to get person ID
await page.getByRole('button', { name: 'Expand record' }).click();
await page.waitForURL(/\/object\/person\//);
const newPersonId = page.url().match(/\/object\/person\/([a-f0-9-]+)/)?.[1];
// Check data was saved
const findOnePersonResponse = await postBackendGraphQL({
page,
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',
]);
});

View File

@@ -0,0 +1,43 @@
import { test as base, expect } from '@playwright/test';
import path from 'path';
import { LoginPage } from '../lib/pom/loginPage';
// fixture
const test = base.extend<{ loginPage: LoginPage }>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
});
test('Login test', async ({ loginPage, page }) => {
await test.step('Navigated to login page', async () => {
await page.goto('/');
});
await test.step(
'Logging in '.concat(page.url(), ' as ', process.env.DEFAULT_LOGIN),
async () => {
await page.waitForLoadState('networkidle');
// Click "Continue with Email" if visible (may be skipped if password is the only auth method)
await loginPage.clickLoginWithEmailIfVisible();
await loginPage.typeEmail(process.env.DEFAULT_LOGIN);
await loginPage.clickContinueButton();
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
await page.waitForLoadState('networkidle');
await loginPage.clickSignInButton();
await page.waitForLoadState('networkidle');
await expect(page.getByText(/Welcome, .+/)).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();
},
);
await test.step('Saved auth state', async () => {
await page.context().storageState({
path: path.resolve(__dirname, '..', '.auth', 'user.json'),
});
});
});

View File

@@ -0,0 +1,65 @@
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(process.env.LINK);
const workflowsFolder = page.getByRole('button', { name: 'Workflows' });
await workflowsFolder.click();
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
await workflowsLink.click();
const createWorkflowButton = page.getByRole('button', {
name: 'Create new workflow',
});
const [createWorkflowResponse] = await Promise.all([
page.waitForResponse(async (response) => {
if (!response.url().endsWith('/graphql')) {
return false;
}
const requestBody = response.request().postDataJSON();
return requestBody.operationName === 'CreateOneWorkflow';
}),
createWorkflowButton.click()
]);
const recordName = page.getByTestId('top-bar-title').getByPlaceholder('Name');
await expect(recordName).toBeVisible();
await recordName.fill(NEW_WORKFLOW_NAME);
const workflowDiagramContainer = page.locator('.react-flow__renderer');
await workflowDiagramContainer.click();
const body = await createWorkflowResponse.json();
const newWorkflowId = body.data.createWorkflow.id;
try {
const workflowName = page
.getByTestId('top-bar-title')
.getByText(NEW_WORKFLOW_NAME);
// Wait for the name to be visible and not hidden
await workflowName.waitFor({ state: 'visible' });
await expect(workflowName).toBeVisible();
await expect(page).toHaveURL(`/object/workflow/${newWorkflowId}`);
} finally {
await deleteWorkflow({
page,
workflowId: newWorkflowId,
});
await destroyWorkflow({
page,
workflowId: newWorkflowId,
});
}
});

View File

@@ -0,0 +1,150 @@
// 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');
// const manualTriggerAvailabilitySelect = page.getByRole('button', {
// name: 'When record is selected',
// });
// await manualTriggerAvailabilitySelect.click();
// const alwaysAvailableOption = page.getByText('When no record is selected');
// await alwaysAvailableOption.click();
// await workflowVisualizer.closeSidePanel();
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.closeSidePanel();
// const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
// await launchTestButton.click();
// await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.deleteStep(firstStepId);
// await page.goto('/objects/workflowRuns');
// const recordTableRowForWorkflowRun = page
// .getByRole('row', {
// name: workflowVisualizer.workflowName,
// })
// .first();
// const linkToWorkflowRun = recordTableRowForWorkflowRun
// .getByRole('link', {
// name: workflowVisualizer.workflowName,
// })
// .first();
// await linkToWorkflowRun.click({ force: true });
// const workflowRunNameElement = page
// .getByText(`#1 - ${workflowVisualizer.workflowName}`)
// .nth(1);
// await expect(workflowRunNameElement).toBeVisible();
// const executedFirstStepNode = workflowVisualizer.getStepNode(firstStepId);
// await expect(executedFirstStepNode).toBeVisible();
// await executedFirstStepNode.click();
// 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');
// const manualTriggerAvailabilitySelect = page.getByRole('button', {
// name: 'When record is selected',
// });
// await manualTriggerAvailabilitySelect.click();
// const alwaysAvailableOption = page.getByText('When no record is selected');
// await alwaysAvailableOption.click();
// await workflowVisualizer.closeSidePanel();
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('form');
// const addFormFieldButton = page.getByText('Add Field', { exact: true });
// await addFormFieldButton.click();
// await workflowVisualizer.closeSidePanel();
// const launchTestButton = page.getByLabel(workflowVisualizer.workflowName);
// await launchTestButton.click();
// const workflowRunName = `#1 - ${workflowVisualizer.workflowName}`;
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Form',
// {
// timeout: 30_000,
// },
// );
// await workflowVisualizer.goBackInCommandMenu.click();
// const workflowRunNameInCommandMenu =
// workflowVisualizer.commandMenu.getByText(workflowRunName);
// await expect(workflowRunNameInCommandMenu).toBeVisible();
// await workflowVisualizer.commandMenu
// .locator(workflowVisualizer.triggerNode)
// .click();
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Launch manually',
// );
// await workflowVisualizer.goBackInCommandMenu.click();
// const formStep = workflowVisualizer.commandMenu.locator(
// workflowVisualizer.getStepNode(firstStepId),
// );
// await formStep.click();
// await workflowVisualizer.goBackInCommandMenu.click();
// const openInFullScreenButton = workflowVisualizer.commandMenu.getByRole(
// 'button',
// { name: 'Open' },
// );
// await openInFullScreenButton.click();
// const workflowRunNameInShowPage = page
// .getByText(`#1 - ${workflowVisualizer.workflowName}`)
// .nth(1);
// await expect(workflowRunNameInShowPage).toBeVisible();
// // Expect the side panel to be opened by default on the form.
// await expect(workflowVisualizer.stepHeaderInCommandMenu).toContainText(
// 'Form',
// );
// });

View File

@@ -0,0 +1,161 @@
// test('Use an old version as draft', async ({ workflowVisualizer, page }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
// await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.background.click();
// await workflowVisualizer.activateWorkflowButton.click();
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await workflowVisualizer.createStep('delete-record');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await workflowVisualizer.closeSidePanel();
// await workflowVisualizer.activateWorkflowButton.click();
// 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();
// const workflowsLink = page.getByRole('link', { name: 'Workflows' });
// await workflowsLink.click();
// await workflowVisualizer.setWorkflowsOpenInMode('record-page');
// const recordTableRowForWorkflow = page.getByRole('row', {
// name: workflowVisualizer.workflowName,
// });
// const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
// name: workflowVisualizer.workflowName,
// });
// await expect(linkToWorkflow).toBeVisible();
// const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
// 'link',
// {
// name: 'v1',
// },
// );
// 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 workflowVisualizer.useAsDraftButton.click();
// 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);
// });
// 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.background.click();
// await workflowVisualizer.activateWorkflowButton.click();
// await expect(workflowVisualizer.workflowStatus).toHaveText('Active');
// await workflowVisualizer.createStep('delete-record');
// 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 workflowVisualizer.closeSidePanel();
// const workflowsLink = page.getByRole('link', { name: 'Workflows' });
// await workflowsLink.click();
// const recordTableRowForWorkflow = page.getByRole('row', {
// name: workflowVisualizer.workflowName,
// });
// const linkToWorkflow = recordTableRowForWorkflow.getByRole('link', {
// name: workflowVisualizer.workflowName,
// });
// await expect(linkToWorkflow).toBeVisible();
// const linkToFirstWorkflowVersion = recordTableRowForWorkflow.getByRole(
// 'link',
// {
// name: 'v1',
// },
// );
// 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 workflowVisualizer.useAsDraftButton.click();
// await expect(workflowVisualizer.overrideDraftButton).toBeVisible();
// await workflowVisualizer.overrideDraftButton.click();
// 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();
// });

View File

@@ -0,0 +1,218 @@
// 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');
// 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();
// const draftWorkflowStatus =
// workflowVisualizer.workflowStatus.getByText('Draft');
// await expect(draftWorkflowStatus).toBeVisible();
// await workflowVisualizer.activateWorkflowButton.click();
// 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();
// });
// 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');
// 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 expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// 'Send Email',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(4);
// await workflowVisualizer.deleteStep(fifthStepId);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Update Record',
// 'Delete Record',
// 'Code - Serverless Function',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(3);
// await workflowVisualizer.deleteStep(secondStepId);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Delete Record',
// 'Code - Serverless Function',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(2);
// await workflowVisualizer.deleteStep(fourthStepId);
// await expect(workflowVisualizer.getAllStepNodes()).toContainText([
// 'Delete Record',
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(1);
// await workflowVisualizer.deleteStep(thirdStepId);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
// await Promise.all([
// page.reload(),
// expect(workflowVisualizer.triggerNode).toBeVisible(),
// ]);
// await expect(workflowVisualizer.getAllStepNodes()).toHaveCount(0);
// });
// test('Add a step to an active version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
// await workflowVisualizer.createStep('create-record');
// await expect(workflowVisualizer.workflowStatus).toHaveText('Draft');
// await workflowVisualizer.background.click();
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
// 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);
// };
// await assertEndState();
// await page.reload();
// await assertEndState();
// });
// test('Replace the trigger of an active version', async ({
// workflowVisualizer,
// page,
// }) => {
// await workflowVisualizer.createInitialTrigger('record-created');
// await workflowVisualizer.createStep('create-record');
// await workflowVisualizer.background.click();
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Draft'),
// workflowVisualizer.deleteTrigger(),
// ]);
// await workflowVisualizer.createInitialTrigger('record-deleted');
// await workflowVisualizer.background.click();
// await Promise.all([
// expect(workflowVisualizer.workflowStatus).toHaveText('Active'),
// workflowVisualizer.activateWorkflowButton.click(),
// ]);
// await page.reload();
// 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();
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
// await expect(workflowVisualizer.triggerNode).toBeVisible();
// const { createdStepId: firstStepId } =
// await workflowVisualizer.createStep('create-record');
// const firstStep = workflowVisualizer.getStepNode(firstStepId);
// await firstStep.click();
// await expect(workflowVisualizer.getDeleteNodeButton(firstStep)).toBeVisible();
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
// await expect(firstStep).toBeVisible();
// await workflowVisualizer.addStepButton.click();
// await page.keyboard.press('Backspace');
// await page.keyboard.press('Delete');
// await expect(workflowVisualizer.addStepButton).toBeVisible();
// });