Skip to content

Commit

Permalink
feat: add unit tests for error states in critical workflows (#1118)
Browse files Browse the repository at this point in the history
* 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
zwliew authored Apr 27, 2021
1 parent dde8fe8 commit 201957c
Show file tree
Hide file tree
Showing 18 changed files with 1,848 additions and 334 deletions.
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()
})
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()
})
})
Loading

0 comments on commit 201957c

Please sign in to comment.