mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add teams (#848)
## Description Add support for teams which will allow users to collaborate on documents. Teams features allows users to: - Create, manage and transfer teams - Manage team members - Manage team emails - Manage a shared team inbox and documents These changes do NOT include the following, which are planned for a future release: - Team templates - Team API - Search menu integration ## Testing Performed - Added E2E tests for general team management - Added E2E tests to validate document counts ## Checklist - [X] I have tested these changes locally and they work as expected. - [X] I have added/updated tests that prove the effectiveness of these changes. - [ ] I have updated the documentation to reflect these changes, if applicable. - [X] I have followed the project's coding style guidelines.
This commit is contained in:
40
packages/app-tests/e2e/fixtures/authentication.ts
Normal file
40
packages/app-tests/e2e/fixtures/authentication.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
type ManualLoginOptions = {
|
||||
page: Page;
|
||||
email?: string;
|
||||
password?: string;
|
||||
|
||||
/**
|
||||
* Where to navigate after login.
|
||||
*/
|
||||
redirectPath?: string;
|
||||
};
|
||||
|
||||
export const manualLogin = async ({
|
||||
page,
|
||||
email = 'example@documenso.com',
|
||||
password = 'password',
|
||||
redirectPath,
|
||||
}: ManualLoginOptions) => {
|
||||
await page.goto(`${WEBAPP_BASE_URL}/signin`);
|
||||
|
||||
await page.getByLabel('Email').click();
|
||||
await page.getByLabel('Email').fill(email);
|
||||
|
||||
await page.getByLabel('Password', { exact: true }).fill(password);
|
||||
await page.getByLabel('Password', { exact: true }).press('Enter');
|
||||
|
||||
if (redirectPath) {
|
||||
await page.waitForURL(`${WEBAPP_BASE_URL}/documents`);
|
||||
await page.goto(`${WEBAPP_BASE_URL}${redirectPath}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const manualSignout = async ({ page }: ManualLoginOptions) => {
|
||||
await page.getByTestId('menu-switcher').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
await page.waitForURL(`${WEBAPP_BASE_URL}/signin`);
|
||||
};
|
||||
@ -2,6 +2,8 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
import { TEST_USERS } from '@documenso/prisma/seed/pr-711-deletion-of-documents';
|
||||
|
||||
import { manualLogin, manualSignout } from './fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test('[PR-711]: seeded documents should be visible', async ({ page }) => {
|
||||
@ -19,17 +21,11 @@ test('[PR-711]: seeded documents should be visible', async ({ page }) => {
|
||||
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).toBeVisible();
|
||||
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
|
||||
await page.waitForURL('/signin');
|
||||
await manualSignout({ page });
|
||||
|
||||
for (const recipient of recipients) {
|
||||
await page.goto('/signin');
|
||||
|
||||
await page.getByLabel('Email').fill(recipient.email);
|
||||
await page.getByLabel('Password', { exact: true }).fill(recipient.password);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
await page.waitForURL('/signin');
|
||||
await manualLogin({ page, email: recipient.email, password: recipient.password });
|
||||
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
@ -38,10 +34,7 @@ test('[PR-711]: seeded documents should be visible', async ({ page }) => {
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).not.toBeVisible();
|
||||
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
|
||||
await page.waitForURL('/signin');
|
||||
await manualSignout({ page });
|
||||
}
|
||||
});
|
||||
|
||||
@ -74,13 +67,10 @@ test('[PR-711]: deleting a completed document should not remove it from recipien
|
||||
|
||||
await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible();
|
||||
|
||||
// signout
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
|
||||
await page.waitForURL('/signin');
|
||||
await manualSignout({ page });
|
||||
|
||||
for (const recipient of recipients) {
|
||||
await page.waitForURL('/signin');
|
||||
await page.goto('/signin');
|
||||
|
||||
// sign in
|
||||
@ -96,11 +86,7 @@ test('[PR-711]: deleting a completed document should not remove it from recipien
|
||||
await expect(page.getByText('Everyone has signed').nth(0)).toBeVisible();
|
||||
|
||||
await page.goto('/documents');
|
||||
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
|
||||
await page.waitForURL('/signin');
|
||||
await manualSignout({ page });
|
||||
}
|
||||
});
|
||||
|
||||
@ -115,11 +101,7 @@ test('[PR-711]: deleting a pending document should remove it from recipients', a
|
||||
|
||||
await page.goto('/signin');
|
||||
|
||||
// sign in
|
||||
await page.getByLabel('Email').fill(sender.email);
|
||||
await page.getByLabel('Password', { exact: true }).fill(sender.password);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
await manualLogin({ page, email: sender.email, password: sender.password });
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
// open actions menu
|
||||
@ -133,19 +115,12 @@ test('[PR-711]: deleting a pending document should remove it from recipients', a
|
||||
await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible();
|
||||
|
||||
// signout
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
|
||||
await page.waitForURL('/signin');
|
||||
await manualSignout({ page });
|
||||
|
||||
for (const recipient of recipients) {
|
||||
await page.goto('/signin');
|
||||
|
||||
// sign in
|
||||
await page.getByLabel('Email').fill(recipient.email);
|
||||
await page.getByLabel('Password', { exact: true }).fill(recipient.password);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
await page.waitForURL('/signin');
|
||||
|
||||
await manualLogin({ page, email: recipient.email, password: recipient.password });
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).not.toBeVisible();
|
||||
@ -154,11 +129,9 @@ test('[PR-711]: deleting a pending document should remove it from recipients', a
|
||||
await expect(page.getByText(/document.*cancelled/i).nth(0)).toBeVisible();
|
||||
|
||||
await page.goto('/documents');
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
|
||||
await page.waitForURL('/signin');
|
||||
await manualSignout({ page });
|
||||
}
|
||||
});
|
||||
|
||||
@ -167,13 +140,7 @@ test('[PR-711]: deleting a draft document should remove it without additional pr
|
||||
}) => {
|
||||
const [sender] = TEST_USERS;
|
||||
|
||||
await page.goto('/signin');
|
||||
|
||||
// sign in
|
||||
await page.getByLabel('Email').fill(sender.email);
|
||||
await page.getByLabel('Password', { exact: true }).fill(sender.password);
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
await manualLogin({ page, email: sender.email, password: sender.password });
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
// open actions menu
|
||||
|
||||
@ -17,12 +17,6 @@ test('[PR-713]: should see sent documents', async ({ page }) => {
|
||||
|
||||
await page.getByPlaceholder('Type a command or search...').fill('sent');
|
||||
await expect(page.getByRole('option', { name: '[713] Document - Sent' })).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// signout
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
});
|
||||
|
||||
test('[PR-713]: should see received documents', async ({ page }) => {
|
||||
@ -40,12 +34,6 @@ test('[PR-713]: should see received documents', async ({ page }) => {
|
||||
|
||||
await page.getByPlaceholder('Type a command or search...').fill('received');
|
||||
await expect(page.getByRole('option', { name: '[713] Document - Received' })).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// signout
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
});
|
||||
|
||||
test('[PR-713]: should be able to search by recipient', async ({ page }) => {
|
||||
@ -63,10 +51,4 @@ test('[PR-713]: should be able to search by recipient', async ({ page }) => {
|
||||
|
||||
await page.getByPlaceholder('Type a command or search...').fill(recipient.email);
|
||||
await expect(page.getByRole('option', { name: '[713] Document - Sent' })).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// signout
|
||||
await page.getByTitle('Profile Dropdown').click();
|
||||
await page.getByRole('menuitem', { name: 'Sign Out' }).click();
|
||||
});
|
||||
|
||||
87
packages/app-tests/e2e/teams/manage-team.spec.ts
Normal file
87
packages/app-tests/e2e/teams/manage-team.spec.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { manualLogin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[TEAMS]: create team', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: '/settings/teams',
|
||||
});
|
||||
|
||||
const teamId = `team-${Date.now()}`;
|
||||
|
||||
// Create team.
|
||||
await page.getByRole('button', { name: 'Create team' }).click();
|
||||
await page.getByLabel('Team Name*').fill(teamId);
|
||||
await page.getByTestId('dialog-create-team-button').click();
|
||||
|
||||
await page.getByTestId('dialog-create-team-button').waitFor({ state: 'hidden' });
|
||||
|
||||
const isCheckoutRequired = page.url().includes('pending');
|
||||
test.skip(isCheckoutRequired, 'Test skipped because billing is enabled.');
|
||||
|
||||
// Goto new team settings page.
|
||||
await page.getByRole('row').filter({ hasText: teamId }).getByRole('link').nth(1).click();
|
||||
|
||||
await unseedTeam(teamId);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete team', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
redirectPath: `/t/${team.url}/settings`,
|
||||
});
|
||||
|
||||
// Delete team.
|
||||
await page.getByRole('button', { name: 'Delete team' }).click();
|
||||
await page.getByLabel(`Confirm by typing delete ${team.url}`).fill(`delete ${team.url}`);
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
// Check that we have been redirected to the teams page.
|
||||
await page.waitForURL(`${WEBAPP_BASE_URL}/settings/teams`);
|
||||
});
|
||||
|
||||
test('[TEAMS]: update team', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
});
|
||||
|
||||
// Navigate to create team page.
|
||||
await page.getByTestId('menu-switcher').click();
|
||||
await page.getByRole('menuitem', { name: 'Manage teams' }).click();
|
||||
|
||||
// Goto team settings page.
|
||||
await page.getByRole('row').filter({ hasText: team.url }).getByRole('link').nth(1).click();
|
||||
|
||||
const updatedTeamId = `team-${Date.now()}`;
|
||||
|
||||
// Update team.
|
||||
await page.getByLabel('Team Name*').click();
|
||||
await page.getByLabel('Team Name*').clear();
|
||||
await page.getByLabel('Team Name*').fill(updatedTeamId);
|
||||
await page.getByLabel('Team URL*').click();
|
||||
await page.getByLabel('Team URL*').clear();
|
||||
await page.getByLabel('Team URL*').fill(updatedTeamId);
|
||||
|
||||
await page.getByRole('button', { name: 'Update team' }).click();
|
||||
|
||||
// Check we have been redirected to the new team URL and the name is updated.
|
||||
await page.waitForURL(`${WEBAPP_BASE_URL}/t/${updatedTeamId}/settings`);
|
||||
|
||||
await unseedTeam(updatedTeamId);
|
||||
});
|
||||
282
packages/app-tests/e2e/teams/team-documents.spec.ts
Normal file
282
packages/app-tests/e2e/teams/team-documents.spec.ts
Normal file
@ -0,0 +1,282 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { seedDocuments, seedTeamDocuments } from '@documenso/prisma/seed/documents';
|
||||
import { seedTeamEmail, unseedTeam, unseedTeamEmail } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { manualLogin, manualSignout } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
const checkDocumentTabCount = async (page: Page, tabName: string, count: number) => {
|
||||
await page.getByRole('tab', { name: tabName }).click();
|
||||
|
||||
if (tabName !== 'All') {
|
||||
await expect(page.getByRole('tab', { name: tabName })).toContainText(count.toString());
|
||||
}
|
||||
|
||||
if (count === 0) {
|
||||
await expect(page.getByRole('main')).toContainText(`Nothing to do`);
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(page.getByRole('main')).toContainText(`Showing ${count}`);
|
||||
};
|
||||
|
||||
test('[TEAMS]: check team documents count', async ({ page }) => {
|
||||
const { team, teamMember2 } = await seedTeamDocuments();
|
||||
|
||||
// Run the test twice, once with the team owner and once with a team member to ensure the counts are the same.
|
||||
for (const user of [team.owner, teamMember2]) {
|
||||
await manualLogin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
// Check document counts.
|
||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
||||
await checkDocumentTabCount(page, 'Pending', 2);
|
||||
await checkDocumentTabCount(page, 'Completed', 1);
|
||||
await checkDocumentTabCount(page, 'Draft', 2);
|
||||
await checkDocumentTabCount(page, 'All', 5);
|
||||
|
||||
// Apply filter.
|
||||
await page.locator('button').filter({ hasText: 'Sender: All' }).click();
|
||||
await page.getByRole('option', { name: teamMember2.name ?? '' }).click();
|
||||
await page.waitForURL(/senderIds/);
|
||||
|
||||
// Check counts after filtering.
|
||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
||||
await checkDocumentTabCount(page, 'Pending', 2);
|
||||
await checkDocumentTabCount(page, 'Completed', 0);
|
||||
await checkDocumentTabCount(page, 'Draft', 1);
|
||||
await checkDocumentTabCount(page, 'All', 3);
|
||||
|
||||
await manualSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: check team documents count with internal team email', async ({ page }) => {
|
||||
const { team, teamMember2, teamMember4 } = await seedTeamDocuments();
|
||||
const { team: team2, teamMember2: team2Member2 } = await seedTeamDocuments();
|
||||
|
||||
const teamEmailMember = teamMember4;
|
||||
|
||||
await seedTeamEmail({
|
||||
email: teamEmailMember.email,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const testUser1 = await seedUser();
|
||||
|
||||
await seedDocuments([
|
||||
// Documents sent from the team email account.
|
||||
{
|
||||
sender: teamEmailMember,
|
||||
recipients: [testUser1],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: teamEmailMember,
|
||||
recipients: [testUser1],
|
||||
type: DocumentStatus.PENDING,
|
||||
documentOptions: {
|
||||
teamId: team.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: teamMember4,
|
||||
recipients: [testUser1],
|
||||
type: DocumentStatus.DRAFT,
|
||||
},
|
||||
// Documents sent to the team email account.
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmailMember],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
},
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmailMember],
|
||||
type: DocumentStatus.PENDING,
|
||||
},
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmailMember],
|
||||
type: DocumentStatus.DRAFT,
|
||||
},
|
||||
// Document sent to the team email account from another team.
|
||||
{
|
||||
sender: team2Member2,
|
||||
recipients: [teamEmailMember],
|
||||
type: DocumentStatus.PENDING,
|
||||
documentOptions: {
|
||||
teamId: team2.id,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// Run the test twice, one with the team owner and once with the team member email to ensure the counts are the same.
|
||||
for (const user of [team.owner, teamEmailMember]) {
|
||||
await manualLogin({
|
||||
page,
|
||||
email: user.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
// Check document counts.
|
||||
await checkDocumentTabCount(page, 'Inbox', 2);
|
||||
await checkDocumentTabCount(page, 'Pending', 3);
|
||||
await checkDocumentTabCount(page, 'Completed', 3);
|
||||
await checkDocumentTabCount(page, 'Draft', 3);
|
||||
await checkDocumentTabCount(page, 'All', 11);
|
||||
|
||||
// Apply filter.
|
||||
await page.locator('button').filter({ hasText: 'Sender: All' }).click();
|
||||
await page.getByRole('option', { name: teamMember2.name ?? '' }).click();
|
||||
await page.waitForURL(/senderIds/);
|
||||
|
||||
// Check counts after filtering.
|
||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
||||
await checkDocumentTabCount(page, 'Pending', 2);
|
||||
await checkDocumentTabCount(page, 'Completed', 0);
|
||||
await checkDocumentTabCount(page, 'Draft', 1);
|
||||
await checkDocumentTabCount(page, 'All', 3);
|
||||
|
||||
await manualSignout({ page });
|
||||
}
|
||||
|
||||
await unseedTeamEmail({ teamId: team.id });
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: check team documents count with external team email', async ({ page }) => {
|
||||
const { team, teamMember2 } = await seedTeamDocuments();
|
||||
const { team: team2, teamMember2: team2Member2 } = await seedTeamDocuments();
|
||||
|
||||
const teamEmail = `external-team-email-${team.id}@test.documenso.com`;
|
||||
|
||||
await seedTeamEmail({
|
||||
email: teamEmail,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
const testUser1 = await seedUser();
|
||||
|
||||
await seedDocuments([
|
||||
// Documents sent to the team email account.
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmail],
|
||||
type: DocumentStatus.COMPLETED,
|
||||
},
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmail],
|
||||
type: DocumentStatus.PENDING,
|
||||
},
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmail],
|
||||
type: DocumentStatus.DRAFT,
|
||||
},
|
||||
// Document sent to the team email account from another team.
|
||||
{
|
||||
sender: team2Member2,
|
||||
recipients: [teamEmail],
|
||||
type: DocumentStatus.PENDING,
|
||||
documentOptions: {
|
||||
teamId: team2.id,
|
||||
},
|
||||
},
|
||||
// Document sent to the team email account from an individual user.
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmail],
|
||||
type: DocumentStatus.PENDING,
|
||||
documentOptions: {
|
||||
teamId: team2.id,
|
||||
},
|
||||
},
|
||||
{
|
||||
sender: testUser1,
|
||||
recipients: [teamEmail],
|
||||
type: DocumentStatus.DRAFT,
|
||||
documentOptions: {
|
||||
teamId: team2.id,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: teamMember2.email,
|
||||
redirectPath: `/t/${team.url}/documents`,
|
||||
});
|
||||
|
||||
// Check document counts.
|
||||
await checkDocumentTabCount(page, 'Inbox', 3);
|
||||
await checkDocumentTabCount(page, 'Pending', 2);
|
||||
await checkDocumentTabCount(page, 'Completed', 2);
|
||||
await checkDocumentTabCount(page, 'Draft', 2);
|
||||
await checkDocumentTabCount(page, 'All', 9);
|
||||
|
||||
// Apply filter.
|
||||
await page.locator('button').filter({ hasText: 'Sender: All' }).click();
|
||||
await page.getByRole('option', { name: teamMember2.name ?? '' }).click();
|
||||
await page.waitForURL(/senderIds/);
|
||||
|
||||
// Check counts after filtering.
|
||||
await checkDocumentTabCount(page, 'Inbox', 0);
|
||||
await checkDocumentTabCount(page, 'Pending', 2);
|
||||
await checkDocumentTabCount(page, 'Completed', 0);
|
||||
await checkDocumentTabCount(page, 'Draft', 1);
|
||||
await checkDocumentTabCount(page, 'All', 3);
|
||||
|
||||
await unseedTeamEmail({ teamId: team.id });
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete pending team document', async ({ page }) => {
|
||||
const { team, teamMember2: currentUser } = await seedTeamDocuments();
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: currentUser.email,
|
||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||
});
|
||||
|
||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
|
||||
await checkDocumentTabCount(page, 'Pending', 1);
|
||||
});
|
||||
|
||||
test('[TEAMS]: resend pending team document', async ({ page }) => {
|
||||
const { team, teamMember2: currentUser } = await seedTeamDocuments();
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: currentUser.email,
|
||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||
});
|
||||
|
||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
||||
await page.getByRole('menuitem', { name: 'Resend' }).click();
|
||||
|
||||
await page.getByLabel('test.documenso.com').first().click();
|
||||
await page.getByRole('button', { name: 'Send reminder' }).click();
|
||||
|
||||
await expect(page.getByRole('status')).toContainText('Document re-sent');
|
||||
});
|
||||
102
packages/app-tests/e2e/teams/team-email.spec.ts
Normal file
102
packages/app-tests/e2e/teams/team-email.spec.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, seedTeamEmailVerification, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser, unseedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { manualLogin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[TEAMS]: send team email request', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/t/${team.url}/settings`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Add email' }).click();
|
||||
await page.getByPlaceholder('eg. Legal').click();
|
||||
await page.getByPlaceholder('eg. Legal').fill('test@test.documenso.com');
|
||||
await page.getByPlaceholder('example@example.com').click();
|
||||
await page.getByPlaceholder('example@example.com').fill('test@test.documenso.com');
|
||||
await page.getByRole('button', { name: 'Add' }).click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByRole('status')
|
||||
.filter({ hasText: 'We have sent a confirmation email for verification.' })
|
||||
.first(),
|
||||
).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: accept team email request', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
});
|
||||
|
||||
const teamEmailVerification = await seedTeamEmailVerification({
|
||||
email: 'team-email-verification@test.documenso.com',
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/verify/email/${teamEmailVerification.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Team email verified!');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: delete team email', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
createTeamEmail: true,
|
||||
});
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
redirectPath: `/t/${team.url}/settings`,
|
||||
});
|
||||
|
||||
await page.locator('section div').filter({ hasText: 'Team email' }).getByRole('button').click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Remove' }).click();
|
||||
|
||||
await expect(page.getByText('Team email has been removed').first()).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: team email owner removes access', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
createTeamEmail: true,
|
||||
});
|
||||
|
||||
if (!team.teamEmail) {
|
||||
throw new Error('Not possible');
|
||||
}
|
||||
|
||||
const teamEmailOwner = await seedUser({
|
||||
email: team.teamEmail.email,
|
||||
});
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: teamEmailOwner.email,
|
||||
redirectPath: `/settings/teams`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Revoke access' }).click();
|
||||
await page.getByRole('button', { name: 'Revoke' }).click();
|
||||
|
||||
await expect(page.getByText('You have successfully revoked').first()).toBeVisible();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
await unseedUser(teamEmailOwner.id);
|
||||
});
|
||||
110
packages/app-tests/e2e/teams/team-members.spec.ts
Normal file
110
packages/app-tests/e2e/teams/team-members.spec.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, seedTeamInvite, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { manualLogin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[TEAMS]: update team member role', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
});
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/t/${team.url}/settings/members`,
|
||||
});
|
||||
|
||||
const teamMemberToUpdate = team.members[1];
|
||||
|
||||
await page
|
||||
.getByRole('row')
|
||||
.filter({ hasText: teamMemberToUpdate.user.email })
|
||||
.getByRole('button')
|
||||
.click();
|
||||
|
||||
await page.getByRole('menuitem', { name: 'Update role' }).click();
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel('Manager').click();
|
||||
await page.getByRole('button', { name: 'Update' }).click();
|
||||
await expect(
|
||||
page.getByRole('row').filter({ hasText: teamMemberToUpdate.user.email }),
|
||||
).toContainText('Manager');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: accept team invitation without account', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
|
||||
const teamInvite = await seedTeamInvite({
|
||||
email: `team-invite-test-${Date.now()}@test.documenso.com`,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/invite/${teamInvite.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Team invitation');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: accept team invitation with account', async ({ page }) => {
|
||||
const team = await seedTeam();
|
||||
const user = await seedUser();
|
||||
|
||||
const teamInvite = await seedTeamInvite({
|
||||
email: user.email,
|
||||
teamId: team.id,
|
||||
});
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/invite/${teamInvite.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Invitation accepted!');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: member can leave team', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
});
|
||||
|
||||
const teamMember = team.members[1];
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: teamMember.user.email,
|
||||
password: 'password',
|
||||
redirectPath: `/settings/teams`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Leave' }).click();
|
||||
await page.getByRole('button', { name: 'Leave' }).click();
|
||||
|
||||
await expect(page.getByRole('status').first()).toContainText(
|
||||
'You have successfully left this team.',
|
||||
);
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
test('[TEAMS]: owner cannot leave team', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
});
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/settings/teams`,
|
||||
});
|
||||
|
||||
await expect(page.getByRole('button').getByText('Leave')).toBeDisabled();
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
69
packages/app-tests/e2e/teams/transfer-team.spec.ts
Normal file
69
packages/app-tests/e2e/teams/transfer-team.spec.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
|
||||
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
|
||||
import { seedTeam, seedTeamTransfer, unseedTeam } from '@documenso/prisma/seed/teams';
|
||||
|
||||
import { manualLogin } from '../fixtures/authentication';
|
||||
|
||||
test.describe.configure({ mode: 'parallel' });
|
||||
|
||||
test('[TEAMS]: initiate and cancel team transfer', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
});
|
||||
|
||||
const teamMember = team.members[1];
|
||||
|
||||
await manualLogin({
|
||||
page,
|
||||
email: team.owner.email,
|
||||
password: 'password',
|
||||
redirectPath: `/t/${team.url}/settings`,
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'Transfer team' }).click();
|
||||
|
||||
await page.getByRole('combobox').click();
|
||||
await page.getByLabel(teamMember.user.name ?? '').click();
|
||||
await page.getByLabel('Confirm by typing transfer').click();
|
||||
await page.getByLabel('Confirm by typing transfer').fill('transfer');
|
||||
await page.getByRole('button', { name: 'Transfer' }).click();
|
||||
|
||||
await expect(page.locator('[id="\\:r2\\:-form-item-message"]')).toContainText(
|
||||
`You must enter 'transfer ${team.name}' to proceed`,
|
||||
);
|
||||
|
||||
await page.getByLabel('Confirm by typing transfer').click();
|
||||
await page.getByLabel('Confirm by typing transfer').fill(`transfer ${team.name}`);
|
||||
await page.getByRole('button', { name: 'Transfer' }).click();
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Team transfer in progress' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
|
||||
await expect(page.getByRole('status').first()).toContainText(
|
||||
'The team transfer invitation has been successfully deleted.',
|
||||
);
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
|
||||
/**
|
||||
* Current skipped until we disable billing during tests.
|
||||
*/
|
||||
test.skip('[TEAMS]: accept team transfer', async ({ page }) => {
|
||||
const team = await seedTeam({
|
||||
createTeamMembers: 1,
|
||||
});
|
||||
|
||||
const newOwnerMember = team.members[1];
|
||||
|
||||
const teamTransferRequest = await seedTeamTransfer({
|
||||
teamId: team.id,
|
||||
newOwnerUserId: newOwnerMember.userId,
|
||||
});
|
||||
|
||||
await page.goto(`${WEBAPP_BASE_URL}/team/verify/transfer/${teamTransferRequest.token}`);
|
||||
await expect(page.getByRole('heading')).toContainText('Team ownership transferred!');
|
||||
|
||||
await unseedTeam(team.url);
|
||||
});
|
||||
@ -30,7 +30,7 @@ test('user can sign up with email and password', async ({ page }: { page: Page }
|
||||
await page.mouse.up();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Sign Up' }).click();
|
||||
await page.getByRole('button', { name: 'Sign Up', exact: true }).click();
|
||||
await page.waitForURL('/documents');
|
||||
|
||||
await expect(page).toHaveURL('/documents');
|
||||
|
||||
Reference in New Issue
Block a user