Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add unit tests for error states in critical workflows #1118

Merged
merged 40 commits into from
Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f31cd08
chore(travis): run the frontend tests
zwliew Mar 23, 2021
7c68bb2
chore(amplify): run the frontend tests
zwliew Mar 24, 2021
c587fb3
test(app): add an initial render test for App.tsx
zwliew Mar 23, 2021
050b791
fix(amplify): fix test configuration
zwliew Mar 30, 2021
d755b58
fix(travis): run frontend tests only after building
zwliew Mar 30, 2021
61f23e6
feat(app.test): add checks for the footer; improve docs
zwliew Apr 7, 2021
e10507b
feat(dashboard): write initial email campaign test
zwliew Mar 25, 2021
f8d1b4e
feat(dashboard): add initial SMS campaign workflow test
zwliew Mar 30, 2021
8421eaa
feat(dashboard): test the flow for actually creating a new campaign
zwliew Mar 30, 2021
906227a
feat(dashboard): add test for telegram campaign creation workflow
zwliew Mar 31, 2021
7039d30
fix(email): select the correct channel button when creating email cam…
zwliew Apr 1, 2021
982c477
feat(dashboard): colocate APIs; implement initial campaign management
zwliew Apr 1, 2021
7707135
feat(dashboard): add happy path test for protected email campaign
zwliew Apr 6, 2021
5df18c0
chore(test): rename dashboard integration tests for clarity
zwliew Apr 8, 2021
7d974b2
fix(dashboard): fake timers for protected email integration test
zwliew Apr 8, 2021
995cec9
fix: use fake timers for dashboard tests
zwliew Apr 12, 2021
6d45220
feat(test-utils): implement template sanitization
zwliew Apr 8, 2021
bfaf022
feat(sms): implement some unit tests for sms templates
zwliew Apr 8, 2021
a19dd63
feat(telegram): add unit tests for telegram templates
zwliew Apr 8, 2021
76c6f15
feat(email): add unit test for invalid subject
zwliew Apr 8, 2021
4203098
fix(test-utils/api): extract params using the template client
zwliew Apr 12, 2021
03f4079
fix(test-utils/api): store the sanitized email subject and body
zwliew Apr 12, 2021
8e3077c
feat(test-utils/api): add template var checks for protected emails
zwliew Apr 12, 2021
de0aa7e
feat: add unit tests for saving invalid protected email templates
zwliew Apr 12, 2021
2a30995
feat: add a simple render test for EmailRecipients
zwliew Apr 12, 2021
417f586
feat: simulate the validation of invalid credentials
zwliew Apr 12, 2021
2b26014
feat: add unit test for invalid SMS credentials
zwliew Apr 12, 2021
2a17af1
chore: add comment headers for setup and teardown sections
zwliew Apr 12, 2021
f94cf21
feat: add unit test for validating an invalid Telegram credential
zwliew Apr 12, 2021
1006182
fix: query credential label options by role
zwliew Apr 12, 2021
6dbd3fa
feat: add unit tests for protected email error states
zwliew Apr 13, 2021
f39cae6
fix: fix types after rebasing on updated API mocks
zwliew Apr 19, 2021
60d50a2
fix: add missing check for the custom from address dropdown
zwliew Apr 20, 2021
74b94ea
feat: mock API responses for invalid CSV files
zwliew Apr 26, 2021
18226dc
refactor: move CSV file constants to test-utils
zwliew Apr 26, 2021
08d3aab
refactor: move protected email integration test to a separate file
zwliew Apr 26, 2021
aefeac7
chore: add tests for uploading invalid CSV files
zwliew Apr 26, 2021
b986412
chore: fix rebase conflicts
zwliew Apr 26, 2021
ffad95d
fix: fill invalid CSV files with invalid data
zwliew Apr 26, 2021
0b6b279
feat: add test for uploading invalid CSV file for protected email
zwliew Apr 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also test for the error messages output when invalid templates are provided such as {{test}, {{test 123}} {{}}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to add these in? i think the reason why I gave these examples is that they each output a different error message, but if the error messages are generated on the backend and not the frontend, then we can address this in backend tests

Copy link
Contributor Author

@zwliew zwliew Apr 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into them initially, but realised that I would need to add some extra cases to the API endpoint mocks to handle them, so I put that on hold 😅

Now that you mention it, having these in the backend tests sounds like a good idea as well! If so, I guess we can postpone adding frontend tests for those cases to later?

// 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()
Comment on lines +98 to +104
Copy link
Contributor

@miazima miazima Apr 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly the setup and teardown in these tests could be extracted to jest before/after hooks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's a bit difficult to do so, since most tests have slightly different setup/teardown phases. For example, some tests mock console.error and some use protected email campaigns.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, as for mocking the console.error, if you want to do this globally, you can feed it in a setup file in your jest.config.ts, this is something the backend tests currently implement to silence console.log messages

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I considered doing so, but decided against mocking console.error globally. When fixing test failures, I found that console.error statements provided useful information.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, the backend tests currently implement that only for console.log to not overcrowd the travis log output


// 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