-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add unit tests for error states in critical workflows (#1118)
* chore(travis): run the frontend tests * chore(amplify): run the frontend tests Make sure to set CI=true explicitly as Amplify doesn't set it by default. CI=true will run all the tests exactly once and exit. Also compile translations before running the tests. * test(app): add an initial render test for App.tsx Just a simple render test to ensure that the test infrastructure works fine. * fix(amplify): fix test configuration * fix(travis): run frontend tests only after building This makes more sense as compilation errors are more important than test errors. This also matches the Amplify build configuration. * feat(app.test): add checks for the footer; improve docs * feat(dashboard): write initial email campaign test * feat(dashboard): add initial SMS campaign workflow test Also refactor common API endpoints shared with the Email campaign test into a separate array. * feat(dashboard): test the flow for actually creating a new campaign * feat(dashboard): add test for telegram campaign creation workflow Also add assertions for testing the credential dropdown for SMS campaigns. * fix(email): select the correct channel button when creating email campaign Previously, we were selecting the SMS channel button, not the email button. * feat(dashboard): colocate APIs; implement initial campaign management * feat(dashboard): add happy path test for protected email campaign * chore(test): rename dashboard integration tests for clarity * fix(dashboard): fake timers for protected email integration test * fix: use fake timers for dashboard tests * feat(test-utils): implement template sanitization * feat(sms): implement some unit tests for sms templates Of particular interest is the unit test for submitting invalid templates. * feat(telegram): add unit tests for telegram templates Of particular interest is the unit test for invalid template submissions. * feat(email): add unit test for invalid subject * fix(test-utils/api): extract params using the template client * fix(test-utils/api): store the sanitized email subject and body * feat(test-utils/api): add template var checks for protected emails * feat: add unit tests for saving invalid protected email templates * feat: add a simple render test for EmailRecipients * feat: simulate the validation of invalid credentials * feat: add unit test for invalid SMS credentials * chore: add comment headers for setup and teardown sections * feat: add unit test for validating an invalid Telegram credential * fix: query credential label options by role * feat: add unit tests for protected email error states * fix: fix types after rebasing on updated API mocks * fix: add missing check for the custom from address dropdown * feat: mock API responses for invalid CSV files * refactor: move CSV file constants to test-utils * refactor: move protected email integration test to a separate file * chore: add tests for uploading invalid CSV files * chore: fix rebase conflicts * fix: fill invalid CSV files with invalid data * feat: add test for uploading invalid CSV file for protected email
- Loading branch information
Showing
18 changed files
with
1,848 additions
and
334 deletions.
There are no files selected for viewing
112 changes: 112 additions & 0 deletions
112
frontend/src/components/dashboard/create/email/tests/EmailRecipients.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import React from 'react' | ||
import userEvent from '@testing-library/user-event' | ||
import { | ||
screen, | ||
mockCommonApis, | ||
server, | ||
render, | ||
Campaign, | ||
USER_EMAIL, | ||
DEFAULT_FROM, | ||
INVALID_EMAIL_CSV_FILE, | ||
} from 'test-utils' | ||
import CampaignContextProvider from 'contexts/campaign.context' | ||
import FinishLaterModalContextProvider from 'contexts/finish-later.modal.context' | ||
import { Route } from 'react-router-dom' | ||
import EmailRecipients from '../EmailRecipients' | ||
import { EmailCampaign } from 'classes' | ||
|
||
const TEST_EMAIL_CAMPAIGN: Campaign = { | ||
id: 1, | ||
name: 'Test email campaign', | ||
type: 'EMAIL', | ||
created_at: new Date(), | ||
valid: false, | ||
protect: false, | ||
demo_message_limit: null, | ||
csv_filename: null, | ||
is_csv_processing: false, | ||
num_recipients: null, | ||
job_queue: [], | ||
halted: false, | ||
email_templates: { | ||
body: 'Test body', | ||
subject: 'Test subject', | ||
params: [], | ||
reply_to: USER_EMAIL, | ||
from: DEFAULT_FROM, | ||
}, | ||
has_credential: false, | ||
} | ||
|
||
function mockApis() { | ||
const { handlers } = mockCommonApis({ | ||
curUserId: 1, // Start authenticated | ||
|
||
// Start with an email campaign with a saved template | ||
campaigns: [{ ...TEST_EMAIL_CAMPAIGN }], | ||
}) | ||
return handlers | ||
} | ||
|
||
function renderRecipients() { | ||
const setActiveStep = jest.fn() | ||
|
||
render( | ||
<Route path="/campaigns/:id"> | ||
<CampaignContextProvider | ||
initialCampaign={new EmailCampaign({ ...TEST_EMAIL_CAMPAIGN })} | ||
> | ||
<FinishLaterModalContextProvider> | ||
<EmailRecipients setActiveStep={setActiveStep} /> | ||
</FinishLaterModalContextProvider> | ||
</CampaignContextProvider> | ||
</Route>, | ||
{ | ||
router: { initialIndex: 0, initialEntries: ['/campaigns/1'] }, | ||
} | ||
) | ||
} | ||
|
||
test('displays the necessary elements', async () => { | ||
// Setup | ||
server.use(...mockApis()) | ||
renderRecipients() | ||
|
||
// Wait for the component to fully load | ||
const uploadButton = await screen.findByRole('button', { | ||
name: /upload file/i, | ||
}) | ||
|
||
/** | ||
* Assert that the following elements are present: | ||
* 1. "Upload File" button | ||
* 2. "Download a sample .csv file" button | ||
*/ | ||
expect(uploadButton).toBeInTheDocument() | ||
expect( | ||
screen.getByRole('button', { name: /download a sample/i }) | ||
).toBeInTheDocument() | ||
}) | ||
|
||
test('displays an error message after uploading an invalid recipients list', async () => { | ||
// Setup | ||
server.use(...mockApis()) | ||
renderRecipients() | ||
|
||
// Wait for the component to fully load | ||
const fileUploadInput = (await screen.findByLabelText( | ||
/upload file/i | ||
)) as HTMLInputElement | ||
|
||
// Upload the file | ||
// Note: we cannot select files via the file picker | ||
userEvent.upload(fileUploadInput, INVALID_EMAIL_CSV_FILE) | ||
expect(fileUploadInput?.files).toHaveLength(1) | ||
expect(fileUploadInput?.files?.[0]).toBe(INVALID_EMAIL_CSV_FILE) | ||
|
||
// Assert that an error message is displayed | ||
expect( | ||
await screen.findByText(/error: invalid recipient file/i) | ||
).toBeInTheDocument() | ||
}) |
264 changes: 264 additions & 0 deletions
264
frontend/src/components/dashboard/create/email/tests/EmailTemplate.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,264 @@ | ||
import React from 'react' | ||
import { | ||
Campaign, | ||
screen, | ||
mockCommonApis, | ||
server, | ||
render, | ||
fireEvent, | ||
} from 'test-utils' | ||
import CampaignContextProvider from 'contexts/campaign.context' | ||
import FinishLaterModalContextProvider from 'contexts/finish-later.modal.context' | ||
import userEvent from '@testing-library/user-event' | ||
import { Route } from 'react-router-dom' | ||
import EmailTemplate from '../EmailTemplate' | ||
|
||
function mockApis(protect: boolean) { | ||
const campaign: Campaign = { | ||
id: 1, | ||
name: 'Test email campaign', | ||
type: 'EMAIL', | ||
created_at: new Date(), | ||
valid: false, | ||
protect, | ||
demo_message_limit: null, | ||
csv_filename: null, | ||
is_csv_processing: false, | ||
num_recipients: null, | ||
job_queue: [], | ||
halted: false, | ||
has_credential: false, | ||
} | ||
const { handlers } = mockCommonApis({ | ||
// Start with a freshly created email campaign | ||
campaigns: [campaign], | ||
}) | ||
return handlers | ||
} | ||
|
||
function renderTemplatePage() { | ||
const setActiveStep = jest.fn() | ||
|
||
render( | ||
<Route path="/campaigns/:id"> | ||
<CampaignContextProvider> | ||
<FinishLaterModalContextProvider> | ||
<EmailTemplate setActiveStep={setActiveStep} /> | ||
</FinishLaterModalContextProvider> | ||
</CampaignContextProvider> | ||
</Route>, | ||
{ | ||
router: { initialIndex: 0, initialEntries: ['/campaigns/1'] }, | ||
} | ||
) | ||
} | ||
|
||
test('displays the necessary elements', async () => { | ||
// Setup | ||
server.use(...mockApis(false)) | ||
renderTemplatePage() | ||
|
||
// Wait for the component to fully load | ||
const heading = await screen.findByRole('heading', { | ||
name: /create email message/i, | ||
}) | ||
|
||
/** | ||
* Assert that the following elements are present: | ||
* 1. "Create email message" heading | ||
* 2. From address dropdown | ||
* 3. Subject textbox | ||
* 4. Message textbox | ||
* 5. Reply-to textbox | ||
* 6. Next button | ||
*/ | ||
expect(heading).toBeInTheDocument() | ||
expect( | ||
screen.getByRole('listbox', { | ||
name: /custom from/i, | ||
}) | ||
).toBeInTheDocument() | ||
expect( | ||
screen.getByRole('textbox', { | ||
name: /subject/i, | ||
}) | ||
).toBeInTheDocument() | ||
expect( | ||
screen.getByRole('textbox', { name: /rdw-editor/i }) | ||
).toBeInTheDocument() | ||
expect( | ||
screen.getByRole('textbox', { | ||
name: /replies/i, | ||
}) | ||
) | ||
expect(screen.getByRole('button', { name: /next/i })).toBeInTheDocument() | ||
}) | ||
|
||
test('displays an error if the subject is empty after sanitization', async () => { | ||
// Setup | ||
jest.spyOn(console, 'error').mockImplementation(() => { | ||
// Do nothing. Mock console.error to silence expected errors | ||
// due to submitting invalid templates to the API | ||
}) | ||
server.use(...mockApis(false)) | ||
renderTemplatePage() | ||
|
||
// Wait for the component to fully load | ||
const subjectTextbox = await screen.findByRole('textbox', { | ||
name: /subject/i, | ||
}) | ||
const nextButton = screen.getByRole('button', { name: /next/i }) | ||
|
||
// Test against various empty templates | ||
const TEST_TEMPLATES = ['<hehe>', '<script>'] | ||
for (const template of TEST_TEMPLATES) { | ||
// Type the template text into the textbox | ||
userEvent.clear(subjectTextbox) | ||
userEvent.type(subjectTextbox, template) | ||
|
||
// Click the next button to submit the template | ||
userEvent.click(nextButton) | ||
|
||
// Assert that an error message is shown | ||
expect( | ||
await screen.findByText(/message template is invalid/i) | ||
).toBeInTheDocument() | ||
} | ||
|
||
// Teardown | ||
jest.restoreAllMocks() | ||
}) | ||
|
||
describe('protected email', () => { | ||
test('displays an error if the subject contains extraneous invalid params', async () => { | ||
// Setup | ||
jest.spyOn(console, 'error').mockImplementation(() => { | ||
// Do nothing. Mock console.error to silence expected errors | ||
// due to submitting invalid templates to the API | ||
}) | ||
server.use(...mockApis(true)) | ||
renderTemplatePage() | ||
|
||
// Wait for the component to fully load | ||
expect(await screen.findByText(/donotreply/i)).toBeInTheDocument() | ||
const subjectTextbox = screen.getByRole('textbox', { | ||
name: /subject/i, | ||
}) | ||
const nextButton = screen.getByRole('button', { name: /next/i }) | ||
|
||
// Test against various templates with extraneous invalid params | ||
const TEST_TEMPLATES = [ | ||
'test {{invalidparam}}', | ||
'{{anotherInvalidParam}} in a subject', | ||
] | ||
for (const template of TEST_TEMPLATES) { | ||
// Type the template text into the textbox | ||
userEvent.clear(subjectTextbox) | ||
userEvent.paste(subjectTextbox, template) | ||
|
||
// Click the next button to submit the template | ||
userEvent.click(nextButton) | ||
|
||
// Assert that an error message is shown | ||
expect( | ||
await screen.findByText(/only these keywords are allowed/i) | ||
).toBeInTheDocument() | ||
} | ||
|
||
// Teardown | ||
jest.restoreAllMocks() | ||
}) | ||
|
||
test('displays an error if the body contains extraneous invalid params', async () => { | ||
// Setup | ||
jest.spyOn(console, 'error').mockImplementation(() => { | ||
// Do nothing. Mock console.error to silence unexpected errors | ||
// due to submitting invalid templates to the API | ||
}) | ||
server.use(...mockApis(true)) | ||
renderTemplatePage() | ||
|
||
// Wait for the component to fully load | ||
expect(await screen.findByText(/donotreply/i)).toBeInTheDocument() | ||
const subjectTextbox = screen.getByRole('textbox', { | ||
name: /subject/i, | ||
}) | ||
const messageTextbox = screen.getByRole('textbox', { | ||
name: /rdw-editor/i, | ||
}) | ||
const nextButton = screen.getByRole('button', { name: /next/i }) | ||
|
||
// Make the subject non-empty | ||
userEvent.paste(subjectTextbox, 'filler subject') | ||
|
||
// Test against various templates with extraneous invalid params | ||
const TEST_TEMPLATES = [ | ||
'a body with {{protectedlink}}, {{recipient}} and {{more}}', | ||
'a body with {{protectedlink}} and {{unwanted}} params', | ||
] | ||
for (const template of TEST_TEMPLATES) { | ||
// Type the template text into the textbox | ||
fireEvent.paste(messageTextbox, { | ||
clipboardData: { | ||
getData: () => template, | ||
}, | ||
}) | ||
|
||
// Click the next button to submit the template | ||
userEvent.click(nextButton) | ||
|
||
// Assert that an error message is shown | ||
expect( | ||
await screen.findByText(/only these keywords are allowed/i) | ||
).toBeInTheDocument() | ||
} | ||
|
||
jest.restoreAllMocks() | ||
}) | ||
|
||
test('displays an error if the body does not have required params', async () => { | ||
// Setup | ||
jest.spyOn(console, 'error').mockImplementation(() => { | ||
// Do nothing. Mock console.error to silence unexpected errors | ||
// due to submitting invalid templates to the API | ||
}) | ||
server.use(...mockApis(true)) | ||
renderTemplatePage() | ||
|
||
// Wait for the component to fully load | ||
expect(await screen.findByText(/donotreply/i)).toBeInTheDocument() | ||
const subjectTextbox = screen.getByRole('textbox', { | ||
name: /subject/i, | ||
}) | ||
const messageTextbox = screen.getByRole('textbox', { | ||
name: /rdw-editor/i, | ||
}) | ||
const nextButton = screen.getByRole('button', { name: /next/i }) | ||
|
||
// Make the subject non-empty | ||
userEvent.paste(subjectTextbox, 'filler subject') | ||
|
||
// Test against various templates with extraneous invalid params | ||
const TEST_TEMPLATES = [ | ||
'a body without protectedlink', | ||
'a body with {{recipient}} but no protectedlink', | ||
] | ||
for (const template of TEST_TEMPLATES) { | ||
// Type the template text into the textbox | ||
fireEvent.paste(messageTextbox, { | ||
clipboardData: { | ||
getData: () => template, | ||
}, | ||
}) | ||
|
||
// Click the next button to submit the template | ||
userEvent.click(nextButton) | ||
|
||
// Assert that an error message is shown | ||
expect(await screen.findByText(/missing keywords/i)).toBeInTheDocument() | ||
} | ||
|
||
// Teardown | ||
jest.restoreAllMocks() | ||
}) | ||
}) |
Oops, something went wrong.