- 1. Factory classes
- 2. Page objects
- 3. Test scenarios
- 3.1. Use underscore separator for test folders naming
- 3.2. Use folders for test suite organization
- 3.3. Use one spec file per one test
- 3.4. Before block should not contain common test data preparation
- 3.5. Test block should only contain test steps_and_assertions
- 3.6. Before block should not contain test steps
- 3.7. Before block should be placed inside the Describe block
- 4. Assertions
- 5. Files naming
- 6. Class methods with conditional logic
The FactoryItem
class should be extended for each new Entity like User, Course, Event, StudentGroup class.
For the tables which contains additional information for the Item it's recommended to create a link
method.
// ✅ recommended
export class User extends FactoryItem {
public constructor(
apiClient: SeedAPI,
options: Options,
) {
super(apiClient);
...
}
async linkToCourse(
courseId: number,
status: CourseUserStatus,
): Promise<void> {
await this.api.seedItemIfNotExist(
FactoryItemType.CourseUsers,
{
courseId,
userId: this.id,
status,
},
);
}
}
Please define locators in the page object classes and components using the below example. There could be different approaches, but we agreed to follow a single style within the organization.
// ❌ not recommended
export class SignInPage extends BasePage{
private readonly emailField: Locator;
constructor(page: Page) {
super(page);
this.emailField = page.getByTestId('sign-in-user-email');
}
}
// ✅ recommended
export class SignInPage extends BasePage{
private readonly emailField = this.page.getByTestId('sign-in-user-email');
constructor(page: Page) {
super(page);
}
}
Use the parametrized ROUTES to define the parametrized page object URLs.
// ❌ not recommended
interface Options {
chatId: number;
}
export class ChatPage extends LoggedInBasePage {
constructor(
page: Page,
options: Options,
) {
super(page);
this.url = `${ROUTES.chat}\\${options.chatId}`;
}
}
// ✅ recommended
interface Options {
chatId: number;
}
export class ChatPage extends LoggedInBasePage {
constructor(
page: Page,
options: Options,
) {
super(page);
this.url = ROUTES.chat(options.chatId).index;
}
}
Do not repeat the code for page elements. If you defined some common elements from the header, footer, sideBar, or some popups that appear on several pages - make sure to define them in the corresponding component class.
Do not use dynamic values in allure step text becuase that way new test case will be added to the Allure TestOps on each test re-run.
// ❌ not recommended
async typeCourseName(name: string): Promise<void> {
await test.step(`Type course name ${name}`, async () => {
await this.courseDropwDown.type(name);
});
}
// ✅ recommended
async typeCourseName(name: string): Promise<void> {
await test.step('Type course name', async () => {
await this.courseDropwDown.type(name);
});
}
Use fill()
instead of type()
method, as type()
is deprecated in playwright.
// ❌ not recommended
await this.textField.type(text);
// ✅ recommended
await this.textField.fill(text);
Use the underscore separator for test folder names. Never use spaces in the folders and file names.
// ❌ not recommended
- tests
- e2e
- Admin Tools
- Homework-Review-Plugin
// ✅ recommended
- tests
- e2e
- Admin_Tools
- Homework_Review_Plugin
Do not add test spec files from different suites to one folder. Create separate folder for each test suite.
// ❌ not recommended
- tests
- e2e
- LMS_Editor
- Courses
- shouldBeCreatedWithOnlyRequiredFields.spec.ts
- shouldBeCreatedWithAllFields.spec.ts
- shouldEditOnlyRequiredFields.spec.ts
- shouldEditAllFields.spec.ts
// ✅ recommended
- tests
- e2e
- LMS_Editor
- Courses
- New_course
- shouldBeCreatedWithOnlyRequiredFields.spec.ts
- shouldBeCreatedWithAllFields.spec.ts
- Edit_course
- shouldEditOnlyRequiredFields.spec.ts
- shouldEditAllFields.spec.ts
Do not add several tests into one spec file. Each spec file should normally have one e2e test scenario. The exception can be done for the parametrized tests.
// ❌ not recommended
test.describe(`New application form`, () => {
test('should allow to open the page')
test('should allow to be submitted')
test('should show the success message')
}
)
// ✅ recommended
test.describe(`New application form`, () => {
test('should allow to successfully submit the application by new user')
}
)
The before test block in the test spec file should generally contain only calling the fixtures and defining the shortcut constants for better test readability. In case one need to define some additional data preparation (for example seeding some data) - one need to do this in a separate methods and fixtures which can be re-used in other tests.
// ❌ not recommended
test.beforeEach((
{
page,
newProfessionUA,
techCheckTopicInNewCourse,
},
) => {
const techCheckQuestion
= new TechCheckQuestion(
seedAPI,
{ techCheckTopic: techCheckTopicInNewCourse },
);
await techCheckQuestion.createWithEnTranslate();
courseName = newProfessionUA.postpaid.nameShortCode;
questionEn = techCheckQuestion.enTranslation;
questionEditorPage = new QuestionsEditorPage(page);
});
// ✅ recommended
test.beforeEach((
{
page,
newProfessionUA,
techCheckQuestionInTopicInNewCourse,
},
) => {
courseName = newProfessionUA.postpaid.nameShortCode;
questionEn = techCheckQuestionInTopicInNewCourse.enTranslation;
questionEditorPage = new QuestionsEditorPage(page);
});
Test block should contain only test-step or test-assertion methods - these are page object methods wrapped with allure steps and clearly describing user performed step or assertion. Do not use page locators or locator action methods directly in the test spec file - add page-object method instead.
// ❌ not recommended
test('should allow to successfully create course',
async () => {
...
await createCoursePage.courseField.fill(name);
...
await createCoursePage.waitForFlashMessage('Course_succesfully_created');
});
// ✅ recommended
export class CreateCoursePage {
async fillCourseName(name: string): Promise<void> {
await test.step(`Fill the course name`, async () => {
await this.courseField.fill(name);
});
}
async assertCourseCreatedMessage(): Promise<void> {
await test.step(`Assert course created message`, async () => {
await this.waitForFlashMessage('Course_succesfully_created');
});
}
}
test('should allow to successfully create course',
async () => {
...
await createCoursePage.fillCourseName(name);
...
await createCoursePage.assertCourseCreatedMessage();
});
Please do not add the test steps to the before block. All test steps should be added to the test block.
// ❌ not recommended
test.beforeEach( async ({ page }) => {
signInPage = new SignInPage(page);
forgotPasswordPage = new ForgotPasswordPage(page);
await signInPage.visit();
});
test('should redirect to sign in page after submitting form',
async ({}) => {
await signInPage.clickResetPasswordLink();
await forgotPasswordPage.assertOpened();
...
});
// ✅ recommended
test.beforeEach(({ page }) => {
signInPage = new SignInPage(page);
forgotPasswordPage = new ForgotPasswordPage(page);
});
test('should redirect to sign in page after submitting form',
async ({ }) => {
await signInPage.visit();
await signInPage.clickResetPasswordLink();
await forgotPasswordPage.assertOpened();
...
});
Please place beforeEach
block inside the Describe
block for more readability.
// ❌ not recommended
let chatPage: ChatPage;
test.beforeEach((
{
page,
chat,
},
) => {
chatPage = new ChatPage(page, { chatId: chat.id });
});
test.describe('Chat page', () => {
test('should provide the ability to send messge',
async ({ priority }) => {
priority.critical();
await chatPage.visit();
});
});
// ✅ recommended
test.describe('Chat page', () => {
let chatPage: ChatPage;
test.beforeEach((
{
page,
chat,
},
) => {
chatPage = new ChatPage(page, { chatId: chat.id });
});
test('should provide the ability to send messge',
async ({ priority }) => {
priority.critical();
await chatPage.visit();
});
});
Create a separate method for each test assertion. This allows to read all the test steps at one glance and allows to automatically import clear test case steps to allure.
// ❌ not recommended
export class QuestionsEditorPage {
async assertQuestionAdded(question: string): Promise<void> {
await test.step(`Assert question added`, async () => {
await this.assertFlashMessage('editor.question_successfully_created');
await expect(this.questionInList.getByText(question)).toBeVisible();
});
}
}
// ✅ recommended
export class QuestionsEditorPage {
async assertQuestionCreatedSuccessMessage(): Promise<void> {
await this.assertFlashMessage('editor.question_successfully_created');
}
async assertQuestionIsPresentInTheList(question: string): Promise<void> {
await test.step(`Assert question is present in the list`, async () => {
await expect(this.questionInList.getByText(question)).toBeVisible();
});
}
}
Always use expect for assertions.
// ❌ not recommended
export class QuestionsEditorPage {
async assertQuestionIsPresentInTheList(question: string): Promise<void> {
await test.step(`Assert question is present in the list`, async () => {
await this.questionInList.getByText(question).isVisible();
});
}
}
// ✅ recommended
export class QuestionsEditorPage {
async assertQuestionIsPresentInTheList(question: string): Promise<void> {
await test.step(`Assert question is present in the list`, async () => {
await expect(this.questionInList.getByText(question)).toBeVisible();
});
}
}
Split methods for waitings and assertions. If the method is called 'assert' it should contain expect. If the method contains only waitForDisplayed, name it as 'waitFor...'.
// ❌ not recommended
export class QuestionsEditorPage {
async assertQuestionIsPresentInTheList(question: string): Promise<void> {
await step(`Assert question is present in the list`, async () => {
await this.questionInList(question).waitForDisplayed();
});
}
async assertQuestionIsPresentInTheList(question: string): Promise<void> {
await step(`Assert question is present in the list`, async () => {
await this.questionInList(question).waitForDisplayed();
await expect(await this.questionInList(question).isDisplayed()).toBeTruthy();
});
}
}
// ✅ recommended
export class QuestionsEditorPage {
async waitForQuestionIsPresentInTheList(question: string): Promise<void> {
await step(`Wait for question is present in the list`, async () => {
await this.questionInList(question).waitForDisplayed();
});
}
async assertQuestionIsPresentInTheList(question: string): Promise<void> {
await step(`Assert question is present in the list`, async () => {
await expect(
await this.questionInList(question).isDisplayed(),
).toBeTruthy();
});
}
}
To make file name shorter, try to avoid using the should
word at the beginning:
shouldBeAbleToEditOwnUserProfile
>editOwnUserProfile
shouldBeAbleToChangeTheCourse
>courseChanging
shouldUpdateUsername
>updateUsername
Use ternary operator for conditional logic instead of if-else statement because it's more concise and readable.
// ❌ not recommended
export class CourseViewPage extends LMSEditorBasePage {
async assertModuleListItemIsVisible(name?: string): Promise<void> {
await test.step('Assert module list item is visible', async () => {
if (name) {
const moduleListItem = this.listItem.filter({hasText: name});
await expect(moduleListItem).toBeVisible();
return;
}
await expect(this.listItem).toBeVisible();
});
}
}
// ✅ recommended
export class CourseViewPage extends LMSEditorBasePage {
async assertModuleListItemIsVisible(name?: string): Promise<void> {
await test.step('Assert module list item is visible', async () => {
const moduleListItem = name
? this.listItem.filter({hasText: name})
: this.listItem;
await expect(moduleListItem).toBeVisible();
});
}
}