From 56c550c9d21de9f1c2b3e5693d53cb2fad2f70af Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 3 Apr 2024 17:13:35 +0800 Subject: [PATCH] fix: refactor tests (#1066) ## Changes Made - Refactor/optimise tests - Reduce flakiness - Add parallel tests (if there's enough CPU capacity) - Removed explicit worker count when running parallel tests. Defaults to 50% of CPU capacity. Might want to consider sharding the test across runners in the future as our tests grows. --- .../e2e/command-menu/document-search.spec.ts | 54 +++++ .../e2e/document-auth/access-auth.spec.ts | 1 - .../e2e/document-auth/action-auth.spec.ts | 2 +- .../stepper-component.spec.ts} | 111 ++++----- .../e2e/documents/delete-documents.spec.ts | 172 ++++++++++++++ .../app-tests/e2e/fixtures/authentication.ts | 33 +-- .../e2e/pr-711-deletion-of-documents.spec.ts | 159 ------------- ...dd-document-search-to-command-menu.spec.ts | 54 ----- .../app-tests/e2e/teams/manage-team.spec.ts | 8 +- .../e2e/templates/manage-templates.spec.ts | 2 +- .../auth-flow.spec.ts} | 7 +- .../delete-account.spec.ts} | 11 +- .../update-name.spec.ts} | 10 +- packages/app-tests/playwright.config.ts | 5 +- packages/prisma/seed/documents.ts | 9 +- .../seed/pr-711-deletion-of-documents.ts | 223 ------------------ ...713-add-document-search-to-command-menu.ts | 168 ------------- packages/prisma/seed/teams.ts | 5 +- packages/prisma/seed/users.ts | 16 +- 19 files changed, 318 insertions(+), 732 deletions(-) create mode 100644 packages/app-tests/e2e/command-menu/document-search.spec.ts rename packages/app-tests/e2e/{pr-718-add-stepper-component.spec.ts => document-flow/stepper-component.spec.ts} (81%) create mode 100644 packages/app-tests/e2e/documents/delete-documents.spec.ts delete mode 100644 packages/app-tests/e2e/pr-711-deletion-of-documents.spec.ts delete mode 100644 packages/app-tests/e2e/pr-713-add-document-search-to-command-menu.spec.ts rename packages/app-tests/e2e/{test-auth-flow.spec.ts => user/auth-flow.spec.ts} (88%) rename packages/app-tests/e2e/{test-delete-user.spec.ts => user/delete-account.spec.ts} (80%) rename packages/app-tests/e2e/{test-update-user-name.spec.ts => user/update-name.spec.ts} (81%) delete mode 100644 packages/prisma/seed/pr-711-deletion-of-documents.ts delete mode 100644 packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts diff --git a/packages/app-tests/e2e/command-menu/document-search.spec.ts b/packages/app-tests/e2e/command-menu/document-search.spec.ts new file mode 100644 index 000000000..bc1a934d0 --- /dev/null +++ b/packages/app-tests/e2e/command-menu/document-search.spec.ts @@ -0,0 +1,54 @@ +import { expect, test } from '@playwright/test'; + +import { seedPendingDocument } from '@documenso/prisma/seed/documents'; +import { seedUser } from '@documenso/prisma/seed/users'; + +import { apiSignin } from '../fixtures/authentication'; + +test('[COMMAND_MENU]: should see sent documents', async ({ page }) => { + const user = await seedUser(); + const recipient = await seedUser(); + const document = await seedPendingDocument(user, [recipient]); + + await apiSignin({ + page, + email: user.email, + }); + + await page.keyboard.press('Meta+K'); + + await page.getByPlaceholder('Type a command or search...').first().fill(document.title); + await expect(page.getByRole('option', { name: document.title })).toBeVisible(); +}); + +test('[COMMAND_MENU]: should see received documents', async ({ page }) => { + const user = await seedUser(); + const recipient = await seedUser(); + const document = await seedPendingDocument(user, [recipient]); + + await apiSignin({ + page, + email: recipient.email, + }); + + await page.keyboard.press('Meta+K'); + + await page.getByPlaceholder('Type a command or search...').first().fill(document.title); + await expect(page.getByRole('option', { name: document.title })).toBeVisible(); +}); + +test('[COMMAND_MENU]: should be able to search by recipient', async ({ page }) => { + const user = await seedUser(); + const recipient = await seedUser(); + const document = await seedPendingDocument(user, [recipient]); + + await apiSignin({ + page, + email: recipient.email, + }); + + await page.keyboard.press('Meta+K'); + + await page.getByPlaceholder('Type a command or search...').first().fill(recipient.email); + await expect(page.getByRole('option', { name: document.title })).toBeVisible(); +}); diff --git a/packages/app-tests/e2e/document-auth/access-auth.spec.ts b/packages/app-tests/e2e/document-auth/access-auth.spec.ts index 0306689ce..b57969b50 100644 --- a/packages/app-tests/e2e/document-auth/access-auth.spec.ts +++ b/packages/app-tests/e2e/document-auth/access-auth.spec.ts @@ -71,7 +71,6 @@ test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page await apiSignin({ page, email: recipientWithAccount.email, - redirectPath: '/', }); // Check that the one logged in is granted access. diff --git a/packages/app-tests/e2e/document-auth/action-auth.spec.ts b/packages/app-tests/e2e/document-auth/action-auth.spec.ts index b263dbd04..ac69a6c22 100644 --- a/packages/app-tests/e2e/document-auth/action-auth.spec.ts +++ b/packages/app-tests/e2e/document-auth/action-auth.spec.ts @@ -14,7 +14,7 @@ import { seedTestEmail, seedUser, unseedUser } from '@documenso/prisma/seed/user import { apiSignin, apiSignout } from '../fixtures/authentication'; -test.describe.configure({ mode: 'parallel' }); +test.describe.configure({ mode: 'parallel', timeout: 60000 }); test('[DOCUMENT_AUTH]: should allow signing when no auth setup', async ({ page }) => { const user = await seedUser(); diff --git a/packages/app-tests/e2e/pr-718-add-stepper-component.spec.ts b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts similarity index 81% rename from packages/app-tests/e2e/pr-718-add-stepper-component.spec.ts rename to packages/app-tests/e2e/document-flow/stepper-component.spec.ts index f24d74076..ee6b160cc 100644 --- a/packages/app-tests/e2e/pr-718-add-stepper-component.spec.ts +++ b/packages/app-tests/e2e/document-flow/stepper-component.spec.ts @@ -4,10 +4,13 @@ import path from 'node:path'; import { getRecipientByEmail } from '@documenso/lib/server-only/recipient/get-recipient-by-email'; import { prisma } from '@documenso/prisma'; import { DocumentStatus } from '@documenso/prisma/client'; -import { seedUser } from '@documenso/prisma/seed/users'; +import { seedBlankDocument } from '@documenso/prisma/seed/documents'; +import { seedUser, unseedUser } from '@documenso/prisma/seed/users'; -import { apiSignin } from './fixtures/authentication'; +import { apiSignin } from '../fixtures/authentication'; +// Can't use the function in server-only/document due to it indirectly using +// require imports. const getDocumentByToken = async (token: string) => { return await prisma.document.findFirstOrThrow({ where: { @@ -20,11 +23,7 @@ const getDocumentByToken = async (token: string) => { }); }; -test(`[PR-718]: should be able to create a document`, async ({ page }) => { - await page.goto('/signin'); - - const documentTitle = `example-${Date.now()}.pdf`; - +test('[DOCUMENT_FLOW]: should be able to upload a PDF document', async ({ page }) => { const user = await seedUser(); await apiSignin({ @@ -32,7 +31,7 @@ test(`[PR-718]: should be able to create a document`, async ({ page }) => { email: user.email, }); - // Upload document + // Upload document. const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), page.locator('input[type=file]').evaluate((e) => { @@ -42,10 +41,23 @@ test(`[PR-718]: should be able to create a document`, async ({ page }) => { }), ]); - await fileChooser.setFiles(path.join(__dirname, '../../../assets/example.pdf')); + await fileChooser.setFiles(path.join(__dirname, '../../../../assets/example.pdf')); - // Wait to be redirected to the edit page + // Wait to be redirected to the edit page. await page.waitForURL(/\/documents\/\d+/); +}); + +test('[DOCUMENT_FLOW]: should be able to create a document', async ({ page }) => { + const user = await seedUser(); + const document = await seedBlankDocument(user); + + await apiSignin({ + page, + email: user.email, + redirectPath: `/documents/${document.id}/edit`, + }); + + const documentTitle = `example-${Date.now()}.pdf`; // Set general settings await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); @@ -91,34 +103,23 @@ test(`[PR-718]: should be able to create a document`, async ({ page }) => { // Assert document was created await expect(page.getByRole('link', { name: documentTitle })).toBeVisible(); + + await unseedUser(user.id); }); -test('should be able to create a document with multiple recipients', async ({ page }) => { - await page.goto('/signin'); - - const documentTitle = `example-${Date.now()}.pdf`; - +test('[DOCUMENT_FLOW]: should be able to create a document with multiple recipients', async ({ + page, +}) => { const user = await seedUser(); + const document = await seedBlankDocument(user); await apiSignin({ page, email: user.email, + redirectPath: `/documents/${document.id}/edit`, }); - // Upload document - const [fileChooser] = await Promise.all([ - page.waitForEvent('filechooser'), - page.locator('input[type=file]').evaluate((e) => { - if (e instanceof HTMLInputElement) { - e.click(); - } - }), - ]); - - await fileChooser.setFiles(path.join(__dirname, '../../../assets/example.pdf')); - - // Wait to be redirected to the edit page - await page.waitForURL(/\/documents\/\d+/); + const documentTitle = `example-${Date.now()}.pdf`; // Set title await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); @@ -187,34 +188,21 @@ test('should be able to create a document with multiple recipients', async ({ pa // Assert document was created await expect(page.getByRole('link', { name: documentTitle })).toBeVisible(); + + await unseedUser(user.id); }); -test('should be able to create, send and sign a document', async ({ page }) => { - await page.goto('/signin'); - - const documentTitle = `example-${Date.now()}.pdf`; - +test('[DOCUMENT_FLOW]: should be able to create, send and sign a document', async ({ page }) => { const user = await seedUser(); + const document = await seedBlankDocument(user); await apiSignin({ page, email: user.email, + redirectPath: `/documents/${document.id}/edit`, }); - // Upload document - const [fileChooser] = await Promise.all([ - page.waitForEvent('filechooser'), - page.locator('input[type=file]').evaluate((e) => { - if (e instanceof HTMLInputElement) { - e.click(); - } - }), - ]); - - await fileChooser.setFiles(path.join(__dirname, '../../../assets/example.pdf')); - - // Wait to be redirected to the edit page - await page.waitForURL(/\/documents\/\d+/); + const documentTitle = `example-${Date.now()}.pdf`; // Set title await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); @@ -271,36 +259,23 @@ test('should be able to create, send and sign a document', async ({ page }) => { // Check if document has been signed const { status: completedStatus } = await getDocumentByToken(token); expect(completedStatus).toBe(DocumentStatus.COMPLETED); + + await unseedUser(user.id); }); -test('should be able to create, send with redirect url, sign a document and redirect to redirect url', async ({ +test('[DOCUMENT_FLOW]: should be able to create, send with redirect url, sign a document and redirect to redirect url', async ({ page, }) => { - await page.goto('/signin'); - - const documentTitle = `example-${Date.now()}.pdf`; - const user = await seedUser(); + const document = await seedBlankDocument(user); await apiSignin({ page, email: user.email, + redirectPath: `/documents/${document.id}/edit`, }); - // Upload document - const [fileChooser] = await Promise.all([ - page.waitForEvent('filechooser'), - page.locator('input[type=file]').evaluate((e) => { - if (e instanceof HTMLInputElement) { - e.click(); - } - }), - ]); - - await fileChooser.setFiles(path.join(__dirname, '../../../assets/example.pdf')); - - // Wait to be redirected to the edit page - await page.waitForURL(/\/documents\/\d+/); + const documentTitle = `example-${Date.now()}.pdf`; // Set title & advanced redirect await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); @@ -355,4 +330,6 @@ test('should be able to create, send with redirect url, sign a document and redi // Check if document has been signed const { status: completedStatus } = await getDocumentByToken(token); expect(completedStatus).toBe(DocumentStatus.COMPLETED); + + await unseedUser(user.id); }); diff --git a/packages/app-tests/e2e/documents/delete-documents.spec.ts b/packages/app-tests/e2e/documents/delete-documents.spec.ts new file mode 100644 index 000000000..3658f1bc9 --- /dev/null +++ b/packages/app-tests/e2e/documents/delete-documents.spec.ts @@ -0,0 +1,172 @@ +import { expect, test } from '@playwright/test'; + +import { + seedCompletedDocument, + seedDraftDocument, + seedPendingDocument, +} from '@documenso/prisma/seed/documents'; +import { seedUser } from '@documenso/prisma/seed/users'; + +import { apiSignin, apiSignout } from '../fixtures/authentication'; + +test.describe.configure({ mode: 'serial' }); + +const seedDeleteDocumentsTestRequirements = async () => { + const [sender, recipientA, recipientB] = await Promise.all([seedUser(), seedUser(), seedUser()]); + + const [draftDocument, pendingDocument, completedDocument] = await Promise.all([ + seedDraftDocument(sender, [recipientA, recipientB], { + createDocumentOptions: { title: 'Document 1 - Draft' }, + }), + seedPendingDocument(sender, [recipientA, recipientB], { + createDocumentOptions: { title: 'Document 1 - Pending' }, + }), + seedCompletedDocument(sender, [recipientA, recipientB], { + createDocumentOptions: { title: 'Document 1 - Completed' }, + }), + ]); + + return { + sender, + recipients: [recipientA, recipientB], + draftDocument, + pendingDocument, + completedDocument, + }; +}; + +test('[DOCUMENTS]: seeded documents should be visible', async ({ page }) => { + const { sender, recipients } = await seedDeleteDocumentsTestRequirements(); + + await apiSignin({ + page, + email: sender.email, + }); + + await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).toBeVisible(); + + await apiSignout({ page }); + + for (const recipient of recipients) { + await apiSignin({ + page, + email: recipient.email, + }); + + await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).not.toBeVisible(); + + await apiSignout({ page }); + } +}); + +test('[DOCUMENTS]: deleting a completed document should not remove it from recipients', async ({ + page, +}) => { + const { sender, recipients } = await seedDeleteDocumentsTestRequirements(); + + await apiSignin({ + page, + email: sender.email, + }); + + // open actions menu + await page + .locator('tr', { hasText: 'Document 1 - Completed' }) + .getByRole('cell', { name: 'Download' }) + .getByRole('button') + .nth(1) + .click(); + + // delete document + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible(); + + await apiSignout({ page }); + + for (const recipient of recipients) { + await apiSignin({ + page, + email: recipient.email, + }); + + await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible(); + await page.getByRole('link', { name: 'Document 1 - Completed' }).click(); + await expect(page.getByText('Everyone has signed').nth(0)).toBeVisible(); + + await apiSignout({ page }); + } +}); + +test('[DOCUMENTS]: deleting a pending document should remove it from recipients', async ({ + page, +}) => { + const { sender, pendingDocument } = await seedDeleteDocumentsTestRequirements(); + + await apiSignin({ + page, + email: sender.email, + }); + + // open actions menu + await page.locator('tr', { hasText: 'Document 1 - Pending' }).getByRole('button').nth(1).click(); + + // delete document + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible(); + + // signout + await apiSignout({ page }); + + for (const recipient of pendingDocument.Recipient) { + await apiSignin({ + page, + email: recipient.email, + }); + + await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).not.toBeVisible(); + + await page.goto(`/sign/${recipient.token}`); + await expect(page.getByText(/document.*cancelled/i).nth(0)).toBeVisible(); + + await page.goto('/documents'); + await page.waitForURL('/documents'); + + await apiSignout({ page }); + } +}); + +test('[DOCUMENTS]: deleting a draft document should remove it without additional prompting', async ({ + page, +}) => { + const { sender } = await seedDeleteDocumentsTestRequirements(); + + await apiSignin({ + page, + email: sender.email, + }); + + // open actions menu + await page + .locator('tr', { hasText: 'Document 1 - Draft' }) + .getByRole('cell', { name: 'Edit' }) + .getByRole('button') + .click(); + + // delete document + await page.getByRole('menuitem', { name: 'Delete' }).click(); + await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + + await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible(); +}); diff --git a/packages/app-tests/e2e/fixtures/authentication.ts b/packages/app-tests/e2e/fixtures/authentication.ts index 9f3a50756..fe52b65d8 100644 --- a/packages/app-tests/e2e/fixtures/authentication.ts +++ b/packages/app-tests/e2e/fixtures/authentication.ts @@ -13,38 +13,11 @@ type LoginOptions = { redirectPath?: string; }; -export const manualLogin = async ({ - page, - email = 'example@documenso.com', - password = 'password', - redirectPath, -}: LoginOptions) => { - 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 }: LoginOptions) => { - await page.waitForTimeout(1000); - await page.getByTestId('menu-switcher').click(); - await page.getByRole('menuitem', { name: 'Sign Out' }).click(); - await page.waitForURL(`${WEBAPP_BASE_URL}/signin`); -}; - export const apiSignin = async ({ page, email = 'example@documenso.com', password = 'password', - redirectPath = '/', + redirectPath = '/documents', }: LoginOptions) => { const { request } = page.context(); @@ -59,9 +32,7 @@ export const apiSignin = async ({ }, }); - if (redirectPath) { - await page.goto(`${WEBAPP_BASE_URL}${redirectPath}`); - } + await page.goto(`${WEBAPP_BASE_URL}${redirectPath}`); }; export const apiSignout = async ({ page }: { page: Page }) => { diff --git a/packages/app-tests/e2e/pr-711-deletion-of-documents.spec.ts b/packages/app-tests/e2e/pr-711-deletion-of-documents.spec.ts deleted file mode 100644 index da95c66f0..000000000 --- a/packages/app-tests/e2e/pr-711-deletion-of-documents.spec.ts +++ /dev/null @@ -1,159 +0,0 @@ -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 }) => { - const [sender, ...recipients] = TEST_USERS; - - await page.goto('/signin'); - - 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 page.waitForURL('/documents'); - - await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).toBeVisible(); - - await manualSignout({ page }); - - for (const recipient of recipients) { - 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 - Completed' })).toBeVisible(); - await expect(page.getByRole('link', { name: 'Document 1 - Pending' })).toBeVisible(); - - await expect(page.getByRole('link', { name: 'Document 1 - Draft' })).not.toBeVisible(); - - await manualSignout({ page }); - } -}); - -test('[PR-711]: deleting a completed document should not remove it from recipients', async ({ - page, -}) => { - const [sender, ...recipients] = 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 page.waitForURL('/documents'); - - // open actions menu - await page - .locator('tr', { hasText: 'Document 1 - Completed' }) - .getByRole('cell', { name: 'Download' }) - .getByRole('button') - .nth(1) - .click(); - - // delete document - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); - await page.getByRole('button', { name: 'Delete' }).click(); - - await expect(page.getByRole('row', { name: /Document 1 - Completed/ })).not.toBeVisible(); - - await manualSignout({ page }); - - for (const recipient of recipients) { - await page.waitForURL('/signin'); - 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('/documents'); - - await expect(page.getByRole('link', { name: 'Document 1 - Completed' })).toBeVisible(); - - await page.goto(`/sign/completed-token-${recipients.indexOf(recipient)}`); - await expect(page.getByText('Everyone has signed').nth(0)).toBeVisible(); - - await page.goto('/documents'); - await manualSignout({ page }); - } -}); - -test('[PR-711]: deleting a pending document should remove it from recipients', async ({ page }) => { - const [sender, ...recipients] = TEST_USERS; - - for (const recipient of recipients) { - await page.goto(`/sign/pending-token-${recipients.indexOf(recipient)}`); - - await expect(page.getByText('Waiting for others to sign').nth(0)).toBeVisible(); - } - - await page.goto('/signin'); - - await manualLogin({ page, email: sender.email, password: sender.password }); - await page.waitForURL('/documents'); - - // open actions menu - await page.locator('tr', { hasText: 'Document 1 - Pending' }).getByRole('button').nth(1).click(); - - // delete document - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await page.getByPlaceholder("Type 'delete' to confirm").fill('delete'); - await page.getByRole('button', { name: 'Delete' }).click(); - - await expect(page.getByRole('row', { name: /Document 1 - Pending/ })).not.toBeVisible(); - - // signout - await manualSignout({ page }); - - for (const recipient of recipients) { - 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(); - - await page.goto(`/sign/pending-token-${recipients.indexOf(recipient)}`); - await expect(page.getByText(/document.*cancelled/i).nth(0)).toBeVisible(); - - await page.goto('/documents'); - await page.waitForURL('/documents'); - - await manualSignout({ page }); - } -}); - -test('[PR-711]: deleting a draft document should remove it without additional prompting', async ({ - page, -}) => { - const [sender] = TEST_USERS; - - await manualLogin({ page, email: sender.email, password: sender.password }); - await page.waitForURL('/documents'); - - // open actions menu - await page - .locator('tr', { hasText: 'Document 1 - Draft' }) - .getByRole('cell', { name: 'Edit' }) - .getByRole('button') - .click(); - - // delete document - await page.getByRole('menuitem', { name: 'Delete' }).click(); - await expect(page.getByPlaceholder("Type 'delete' to confirm")).not.toBeVisible(); - await page.getByRole('button', { name: 'Delete' }).click(); - - await expect(page.getByRole('row', { name: /Document 1 - Draft/ })).not.toBeVisible(); -}); diff --git a/packages/app-tests/e2e/pr-713-add-document-search-to-command-menu.spec.ts b/packages/app-tests/e2e/pr-713-add-document-search-to-command-menu.spec.ts deleted file mode 100644 index 44cfe1e37..000000000 --- a/packages/app-tests/e2e/pr-713-add-document-search-to-command-menu.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { expect, test } from '@playwright/test'; - -import { TEST_USERS } from '@documenso/prisma/seed/pr-713-add-document-search-to-command-menu'; - -test('[PR-713]: should see sent documents', async ({ page }) => { - const [user] = TEST_USERS; - - await page.goto('/signin'); - - await page.getByLabel('Email').fill(user.email); - await page.getByLabel('Password', { exact: true }).fill(user.password); - await page.getByRole('button', { name: 'Sign In' }).click(); - - await page.waitForURL('/documents'); - - await page.keyboard.press('Meta+K'); - - await page.getByPlaceholder('Type a command or search...').first().fill('sent'); - await expect(page.getByRole('option', { name: '[713] Document - Sent' })).toBeVisible(); -}); - -test('[PR-713]: should see received documents', async ({ page }) => { - const [user] = TEST_USERS; - - await page.goto('/signin'); - - await page.getByLabel('Email').fill(user.email); - await page.getByLabel('Password', { exact: true }).fill(user.password); - await page.getByRole('button', { name: 'Sign In' }).click(); - - await page.waitForURL('/documents'); - - await page.keyboard.press('Meta+K'); - - await page.getByPlaceholder('Type a command or search...').first().fill('received'); - await expect(page.getByRole('option', { name: '[713] Document - Received' })).toBeVisible(); -}); - -test('[PR-713]: should be able to search by recipient', async ({ page }) => { - const [user, recipient] = TEST_USERS; - - await page.goto('/signin'); - - await page.getByLabel('Email').fill(user.email); - await page.getByLabel('Password', { exact: true }).fill(user.password); - await page.getByRole('button', { name: 'Sign In' }).click(); - - await page.waitForURL('/documents'); - - await page.keyboard.press('Meta+K'); - - await page.getByPlaceholder('Type a command or search...').first().fill(recipient.email); - await expect(page.getByRole('option', { name: '[713] Document - Sent' })).toBeVisible(); -}); diff --git a/packages/app-tests/e2e/teams/manage-team.spec.ts b/packages/app-tests/e2e/teams/manage-team.spec.ts index a1deb1995..7403ab9c9 100644 --- a/packages/app-tests/e2e/teams/manage-team.spec.ts +++ b/packages/app-tests/e2e/teams/manage-team.spec.ts @@ -11,6 +11,11 @@ test.describe.configure({ mode: 'parallel' }); test('[TEAMS]: create team', async ({ page }) => { const user = await seedUser(); + test.skip( + process.env.NEXT_PUBLIC_FEATURE_BILLING_ENABLED === 'true', + 'Test skipped because billing is enabled.', + ); + await apiSignin({ page, email: user.email, @@ -26,9 +31,6 @@ test('[TEAMS]: create team', async ({ page }) => { 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(); diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts index a89b308eb..a298d1e38 100644 --- a/packages/app-tests/e2e/templates/manage-templates.spec.ts +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -108,7 +108,7 @@ test('[TEMPLATES]: delete template', async ({ page }) => { await page.getByRole('button', { name: 'Delete' }).click(); await expect(page.getByText('Template deleted').first()).toBeVisible(); - await page.waitForTimeout(1000); + await page.reload(); } await unseedTeam(team.url); diff --git a/packages/app-tests/e2e/test-auth-flow.spec.ts b/packages/app-tests/e2e/user/auth-flow.spec.ts similarity index 88% rename from packages/app-tests/e2e/test-auth-flow.spec.ts rename to packages/app-tests/e2e/user/auth-flow.spec.ts index 3b07371f1..94338ec21 100644 --- a/packages/app-tests/e2e/test-auth-flow.spec.ts +++ b/packages/app-tests/e2e/user/auth-flow.spec.ts @@ -2,6 +2,7 @@ import { type Page, expect, test } from '@playwright/test'; import { extractUserVerificationToken, + seedTestEmail, seedUser, unseedUser, unseedUserByEmail, @@ -9,9 +10,9 @@ import { test.use({ storageState: { cookies: [], origins: [] } }); -test('user can sign up with email and password', async ({ page }: { page: Page }) => { +test('[USER] can sign up with email and password', async ({ page }: { page: Page }) => { const username = 'Test User'; - const email = `test-user-${Date.now()}@auth-flow.documenso.com`; + const email = seedTestEmail(); const password = 'Password123#'; await page.goto('/signup'); @@ -50,7 +51,7 @@ test('user can sign up with email and password', async ({ page }: { page: Page } await unseedUserByEmail(email); }); -test('user can login with user and password', async ({ page }: { page: Page }) => { +test('[USER] can sign in using email and password', async ({ page }: { page: Page }) => { const user = await seedUser(); await page.goto('/signin'); diff --git a/packages/app-tests/e2e/test-delete-user.spec.ts b/packages/app-tests/e2e/user/delete-account.spec.ts similarity index 80% rename from packages/app-tests/e2e/test-delete-user.spec.ts rename to packages/app-tests/e2e/user/delete-account.spec.ts index 6eb72bad9..e04283240 100644 --- a/packages/app-tests/e2e/test-delete-user.spec.ts +++ b/packages/app-tests/e2e/user/delete-account.spec.ts @@ -4,19 +4,16 @@ import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email'; import { seedUser } from '@documenso/prisma/seed/users'; -import { manualLogin } from './fixtures/authentication'; +import { apiSignin } from '../fixtures/authentication'; -test('delete user', async ({ page }) => { +test('[USER] delete account', async ({ page }) => { const user = await seedUser(); - await manualLogin({ - page, - email: user.email, - redirectPath: '/settings', - }); + await apiSignin({ page, email: user.email, redirectPath: '/settings' }); await page.getByRole('button', { name: 'Delete Account' }).click(); await page.getByLabel('Confirm Email').fill(user.email); + await expect(page.getByRole('button', { name: 'Confirm Deletion' })).not.toBeDisabled(); await page.getByRole('button', { name: 'Confirm Deletion' }).click(); diff --git a/packages/app-tests/e2e/test-update-user-name.spec.ts b/packages/app-tests/e2e/user/update-name.spec.ts similarity index 81% rename from packages/app-tests/e2e/test-update-user-name.spec.ts rename to packages/app-tests/e2e/user/update-name.spec.ts index 509db651b..ca26fbf3d 100644 --- a/packages/app-tests/e2e/test-update-user-name.spec.ts +++ b/packages/app-tests/e2e/user/update-name.spec.ts @@ -3,16 +3,12 @@ import { expect, test } from '@playwright/test'; import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-email'; import { seedUser } from '@documenso/prisma/seed/users'; -import { manualLogin } from './fixtures/authentication'; +import { apiSignin } from '../fixtures/authentication'; -test('update user name', async ({ page }) => { +test('[USER] update full name', async ({ page }) => { const user = await seedUser(); - await manualLogin({ - page, - email: user.email, - redirectPath: '/settings/profile', - }); + await apiSignin({ page, email: user.email, redirectPath: '/settings/profile' }); await page.getByLabel('Full Name').fill('John Doe'); diff --git a/packages/app-tests/playwright.config.ts b/packages/app-tests/playwright.config.ts index 0796bb1e1..725f4bb04 100644 --- a/packages/app-tests/playwright.config.ts +++ b/packages/app-tests/playwright.config.ts @@ -17,12 +17,11 @@ export default defineConfig({ testDir: './e2e', /* Run tests in files in parallel */ fullyParallel: true, + workers: '50%', /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + retries: process.env.CI ? 2 : 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ diff --git a/packages/prisma/seed/documents.ts b/packages/prisma/seed/documents.ts index 1fceca900..6c1e698c5 100644 --- a/packages/prisma/seed/documents.ts +++ b/packages/prisma/seed/documents.ts @@ -213,7 +213,14 @@ export const seedPendingDocument = async ( }); } - return document; + return prisma.document.findFirstOrThrow({ + where: { + id: document.id, + }, + include: { + Recipient: true, + }, + }); }; export const seedPendingDocumentNoFields = async ({ diff --git a/packages/prisma/seed/pr-711-deletion-of-documents.ts b/packages/prisma/seed/pr-711-deletion-of-documents.ts deleted file mode 100644 index d2706b734..000000000 --- a/packages/prisma/seed/pr-711-deletion-of-documents.ts +++ /dev/null @@ -1,223 +0,0 @@ -import type { User } from '@prisma/client'; -import fs from 'node:fs'; -import path from 'node:path'; - -import { hashSync } from '@documenso/lib/server-only/auth/hash'; - -import { prisma } from '..'; -import { - DocumentDataType, - DocumentStatus, - FieldType, - Prisma, - ReadStatus, - SendStatus, - SigningStatus, -} from '../client'; - -const PULL_REQUEST_NUMBER = 711; -const EMAIL_DOMAIN = `pr-${PULL_REQUEST_NUMBER}.documenso.com`; - -export const TEST_USERS = [ - { - name: 'Sender 1', - email: `sender1@${EMAIL_DOMAIN}`, - password: 'Password123', - }, - { - name: 'Sender 2', - email: `sender2@${EMAIL_DOMAIN}`, - password: 'Password123', - }, - { - name: 'Sender 3', - email: `sender3@${EMAIL_DOMAIN}`, - password: 'Password123', - }, -] as const; - -const examplePdf = fs - .readFileSync(path.join(__dirname, '../../../assets/example.pdf')) - .toString('base64'); - -export const seedDatabase = async () => { - const users = await Promise.all( - TEST_USERS.map(async (u) => - prisma.user.create({ - data: { - name: u.name, - email: u.email, - password: hashSync(u.password), - emailVerified: new Date(), - url: u.email, - }, - }), - ), - ); - - const [user1, user2, user3] = users; - - await createDraftDocument(user1, [user2, user3]); - await createPendingDocument(user1, [user2, user3]); - await createCompletedDocument(user1, [user2, user3]); -}; - -const createDraftDocument = async (sender: User, recipients: User[]) => { - const documentData = await prisma.documentData.create({ - data: { - type: DocumentDataType.BYTES_64, - data: examplePdf, - initialData: examplePdf, - }, - }); - - const document = await prisma.document.create({ - data: { - title: `[${PULL_REQUEST_NUMBER}] Document 1 - Draft`, - status: DocumentStatus.DRAFT, - documentDataId: documentData.id, - userId: sender.id, - }, - }); - - for (const recipient of recipients) { - const index = recipients.indexOf(recipient); - - await prisma.recipient.create({ - data: { - email: String(recipient.email), - name: String(recipient.name), - token: `draft-token-${index}`, - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.NOT_SENT, - signingStatus: SigningStatus.NOT_SIGNED, - signedAt: new Date(), - Document: { - connect: { - id: document.id, - }, - }, - Field: { - create: { - page: 1, - type: FieldType.NAME, - inserted: true, - customText: String(recipient.name), - positionX: new Prisma.Decimal(1), - positionY: new Prisma.Decimal(1), - width: new Prisma.Decimal(1), - height: new Prisma.Decimal(1), - documentId: document.id, - }, - }, - }, - }); - } -}; - -const createPendingDocument = async (sender: User, recipients: User[]) => { - const documentData = await prisma.documentData.create({ - data: { - type: DocumentDataType.BYTES_64, - data: examplePdf, - initialData: examplePdf, - }, - }); - - const document = await prisma.document.create({ - data: { - title: `[${PULL_REQUEST_NUMBER}] Document 1 - Pending`, - status: DocumentStatus.PENDING, - documentDataId: documentData.id, - userId: sender.id, - }, - }); - - for (const recipient of recipients) { - const index = recipients.indexOf(recipient); - - await prisma.recipient.create({ - data: { - email: String(recipient.email), - name: String(recipient.name), - token: `pending-token-${index}`, - readStatus: ReadStatus.OPENED, - sendStatus: SendStatus.SENT, - signingStatus: SigningStatus.SIGNED, - signedAt: new Date(), - Document: { - connect: { - id: document.id, - }, - }, - Field: { - create: { - page: 1, - type: FieldType.NAME, - inserted: true, - customText: String(recipient.name), - positionX: new Prisma.Decimal(1), - positionY: new Prisma.Decimal(1), - width: new Prisma.Decimal(1), - height: new Prisma.Decimal(1), - documentId: document.id, - }, - }, - }, - }); - } -}; - -const createCompletedDocument = async (sender: User, recipients: User[]) => { - const documentData = await prisma.documentData.create({ - data: { - type: DocumentDataType.BYTES_64, - data: examplePdf, - initialData: examplePdf, - }, - }); - - const document = await prisma.document.create({ - data: { - title: `[${PULL_REQUEST_NUMBER}] Document 1 - Completed`, - status: DocumentStatus.COMPLETED, - documentDataId: documentData.id, - completedAt: new Date(), - userId: sender.id, - }, - }); - - for (const recipient of recipients) { - const index = recipients.indexOf(recipient); - - await prisma.recipient.create({ - data: { - email: String(recipient.email), - name: String(recipient.name), - token: `completed-token-${index}`, - readStatus: ReadStatus.OPENED, - sendStatus: SendStatus.SENT, - signingStatus: SigningStatus.SIGNED, - signedAt: new Date(), - Document: { - connect: { - id: document.id, - }, - }, - Field: { - create: { - page: 1, - type: FieldType.NAME, - inserted: true, - customText: String(recipient.name), - positionX: new Prisma.Decimal(1), - positionY: new Prisma.Decimal(1), - width: new Prisma.Decimal(1), - height: new Prisma.Decimal(1), - documentId: document.id, - }, - }, - }, - }); - } -}; diff --git a/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts b/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts deleted file mode 100644 index 0fe27b703..000000000 --- a/packages/prisma/seed/pr-713-add-document-search-to-command-menu.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { User } from '@prisma/client'; -import fs from 'node:fs'; -import path from 'node:path'; - -import { hashSync } from '@documenso/lib/server-only/auth/hash'; - -import { prisma } from '..'; -import { - DocumentDataType, - DocumentStatus, - FieldType, - Prisma, - ReadStatus, - SendStatus, - SigningStatus, -} from '../client'; - -// -// https://github.com/documenso/documenso/pull/713 -// - -const PULL_REQUEST_NUMBER = 713; - -const EMAIL_DOMAIN = `pr-${PULL_REQUEST_NUMBER}.documenso.com`; - -export const TEST_USERS = [ - { - name: 'User 1', - email: `user1@${EMAIL_DOMAIN}`, - password: 'Password123', - }, - { - name: 'User 2', - email: `user2@${EMAIL_DOMAIN}`, - password: 'Password123', - }, -] as const; - -const examplePdf = fs - .readFileSync(path.join(__dirname, '../../../assets/example.pdf')) - .toString('base64'); - -export const seedDatabase = async () => { - const users = await Promise.all( - TEST_USERS.map(async (u) => - prisma.user.create({ - data: { - name: u.name, - email: u.email, - password: hashSync(u.password), - emailVerified: new Date(), - url: u.email, - }, - }), - ), - ); - - const [user1, user2] = users; - - await createSentDocument(user1, [user2]); - await createReceivedDocument(user2, [user1]); -}; - -const createSentDocument = async (sender: User, recipients: User[]) => { - const documentData = await prisma.documentData.create({ - data: { - type: DocumentDataType.BYTES_64, - data: examplePdf, - initialData: examplePdf, - }, - }); - - const document = await prisma.document.create({ - data: { - title: `[${PULL_REQUEST_NUMBER}] Document - Sent`, - status: DocumentStatus.PENDING, - documentDataId: documentData.id, - userId: sender.id, - }, - }); - - for (const recipient of recipients) { - const index = recipients.indexOf(recipient); - - await prisma.recipient.create({ - data: { - email: String(recipient.email), - name: String(recipient.name), - token: `sent-token-${index}`, - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.SENT, - signingStatus: SigningStatus.NOT_SIGNED, - signedAt: new Date(), - Document: { - connect: { - id: document.id, - }, - }, - Field: { - create: { - page: 1, - type: FieldType.NAME, - inserted: true, - customText: String(recipient.name), - positionX: new Prisma.Decimal(1), - positionY: new Prisma.Decimal(1), - width: new Prisma.Decimal(1), - height: new Prisma.Decimal(1), - documentId: document.id, - }, - }, - }, - }); - } -}; - -const createReceivedDocument = async (sender: User, recipients: User[]) => { - const documentData = await prisma.documentData.create({ - data: { - type: DocumentDataType.BYTES_64, - data: examplePdf, - initialData: examplePdf, - }, - }); - - const document = await prisma.document.create({ - data: { - title: `[${PULL_REQUEST_NUMBER}] Document - Received`, - status: DocumentStatus.PENDING, - documentDataId: documentData.id, - userId: sender.id, - }, - }); - - for (const recipient of recipients) { - const index = recipients.indexOf(recipient); - - await prisma.recipient.create({ - data: { - email: String(recipient.email), - name: String(recipient.name), - token: `received-token-${index}`, - readStatus: ReadStatus.NOT_OPENED, - sendStatus: SendStatus.SENT, - signingStatus: SigningStatus.NOT_SIGNED, - signedAt: new Date(), - Document: { - connect: { - id: document.id, - }, - }, - Field: { - create: { - page: 1, - type: FieldType.NAME, - inserted: true, - customText: String(recipient.name), - positionX: new Prisma.Decimal(1), - positionY: new Prisma.Decimal(1), - width: new Prisma.Decimal(1), - height: new Prisma.Decimal(1), - documentId: document.id, - }, - }, - }, - }); - } -}; diff --git a/packages/prisma/seed/teams.ts b/packages/prisma/seed/teams.ts index 99b0df8d5..aaae866d0 100644 --- a/packages/prisma/seed/teams.ts +++ b/packages/prisma/seed/teams.ts @@ -1,8 +1,11 @@ +import { customAlphabet } from 'nanoid'; + import { prisma } from '..'; import { TeamMemberInviteStatus, TeamMemberRole } from '../client'; import { seedUser } from './users'; const EMAIL_DOMAIN = `test.documenso.com`; +const nanoid = customAlphabet('1234567890abcdef', 10); type SeedTeamOptions = { createTeamMembers?: number; @@ -13,7 +16,7 @@ export const seedTeam = async ({ createTeamMembers = 0, createTeamEmail, }: SeedTeamOptions = {}) => { - const teamUrl = `team-${Date.now()}`; + const teamUrl = `team-${nanoid()}`; const teamEmail = createTeamEmail === true ? `${teamUrl}@${EMAIL_DOMAIN}` : createTeamEmail; const teamOwner = await seedUser({ diff --git a/packages/prisma/seed/users.ts b/packages/prisma/seed/users.ts index fd8706fea..9f7f80a71 100644 --- a/packages/prisma/seed/users.ts +++ b/packages/prisma/seed/users.ts @@ -1,3 +1,5 @@ +import { customAlphabet } from 'nanoid'; + import { hashSync } from '@documenso/lib/server-only/auth/hash'; import { prisma } from '..'; @@ -11,12 +13,22 @@ type SeedUserOptions = { verified?: boolean; }; +const nanoid = customAlphabet('1234567890abcdef', 10); + export const seedUser = async ({ - name = `user-${Date.now()}`, - email = `user-${Date.now()}@test.documenso.com`, + name, + email, password = 'password', verified = true, }: SeedUserOptions = {}) => { + if (!name) { + name = nanoid(); + } + + if (!email) { + email = `${nanoid()}@test.documenso.com`; + } + return await prisma.user.create({ data: { name,