Automated Testing Approach
The following describes Build.One's best practice for creating automated tests. The features developed by Build.One are also tested in this way.
Technology stack
The following technology stack is used for the automated testing:
Testing framework: Playwright
Alternative:
Build.One provides a web UI with HTML that can be easily navigated using any modern pro-code UI automated testing frameworks.
E.g.:
Selenium
Cypress
TestCafe
WebdriverIOetc.
Test automation is possible by using low-code/no-code frameworks.
Test automation is possible for any browser-based web-page.
Programming language: Node.js and Typescript
Alternative languages offered by Playwright:
Python
Java
.NET
Build a Testing Framework
The Build.One testing framework is built according to the Page Object Model pattern following the Playwright recommendations.
Encapsulating pages and components into separate instances helps avoid code duplication and improve maintenance.
Example of the Page Object Model entity from the code used:
import { Locator, Page, expect } from '@playwright/test'; import { MainMenuComponent } from '../components/main-menu.component'; import { BasePage } from './base.page'; import { ApplicationMaintenanceFrame } from './application-maintenance-frame.component'; export class DesktopPage extends BasePage { readonly PAGE_URL = '/'; readonly buildMenuIcon: Locator; readonly mainMenu: MainMenuComponent; readonly applicationMaintenanceFrame: ApplicationMaintenanceFrame; constructor(page: Page) { super(page); this.buildMenuIcon = page.locator('[menuname=Build]'); this.mainMenu = new MainMenuComponent(page); this.applicationMaintenanceFrame = new ApplicationMaintenanceFrame(page, '[akId=devHomeDesktop-ProductMaintenanceF]'); } async goto() { await this.page.goto(this.PAGE_URL, { }); await expect(this.mainMenu.buildMenuIcon, 'User should be logged in.').toBeVisible({ timeout: 10000 }); } }
This example of the class represents the desktop page and is a child of an abstract BasePage
. BasePage
is responsible for all of the shared methods and properties.
import { Locator, Page } from '@playwright/test'; import { ConfirmModalComponent } from '../components/confirm-modal.component'; import { HeaderPanel } from '../components/header-panel.component'; import { UserProfilePopupComponent } from '../components/user-profile-options.component'; export abstract class BasePage { readonly page: Page; protected readonly pageLocator: Locator; readonly errorMessage: Locator; readonly infoMessage: Locator; readonly userProfileBtn: Locator; readonly closeBtn: Locator; readonly userProfilePopup: UserProfilePopupComponent; readonly confirmModal: ConfirmModalComponent; readonly headerPanel: HeaderPanel; constructor(page: Page, pageLocator = '[akStyle=window]') { this.page = page; this.pageLocator = page.locator(pageLocator); this.closeBtn = this.pageLocator.locator('.dhxwin_button_close'); this.errorMessage = page.locator('.dhtmlx-error'); this.infoMessage = page.locator('.dhtmlx-info'); this.userProfileBtn = page.locator('.akUserProfile'); this.userProfilePopup = new UserProfilePopupComponent(page); this.confirmModal = new ConfirmModalComponent(page); this.headerPanel = new HeaderPanel(page); } async logOut() { await this.userProfileBtn.click(); await this.userProfilePopup.logoutBtn.click(); await this.confirmModal.confirmBtn.click(); } async openRepoObject(repositoryObjectName: string, waitForLoad = true) { await this.page.keyboard.press('Control+Alt+Shift+KeyL'); await this.confirmModal.openRepositoryModalInput.fill(repositoryObjectName); // prevent race condition between clicking and waiting for container request if (waitForLoad) { await Promise.all([ this.page.waitForResponse(resp => resp.url().includes('/web/Repository/Container') && resp.status() === 200), this.confirmModal.confirmBtn.click() ]); } else await this.confirmModal.confirmBtn.click(); } async openObjectInDesigner(objectName: string) { const openData = { objectName }; await this.page.evaluate(data => { window.akioma.repository.openRepositoryObjectInDesigner(data); }, openData); await Promise.all([ this.page.waitForResponse(resp => resp.url().includes('/web/Resource/Akioma.Swat.Repository.AttributeValuesTask/GetObjectMasterDesignAttributes') && resp.status() === 200), this.page.waitForResponse(resp => resp.url().includes('/web/Resource/Consultingwerk.SmartFramework.Repository.Object.ObjectMasterBusinessEntity') && resp.status() === 200) ]); } async dragAndDrop(draggableElement: Locator, destinationElement: Locator) { await draggableElement.hover(); await this.page.mouse.down(); const box = (await destinationElement.boundingBox()) ?? null; await this.page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await destinationElement.hover({ force: true }); await this.page.mouse.up(); } }
Write a Test
Tests for the Build.One platform are based on the guide provided by the Playwright framework.
The following example of the test suite with some test cases is included in Typescript:
import { test, expect, Page } from '@playwright/test'; import { ADMIN_EMAIL, getAdminPass } from '../src/constants'; import { DesktopPage } from '../src/pages/desktop.page'; import { SignInPage } from '../src/pages/signin.page'; test.describe.configure({ mode: 'parallel' }); test.describe('Sign up page Smoke tests', async () => { test.use({ storageState: 'not-logged-in-state.json' }); let signInPage: SignInPage; let desktopPage: DesktopPage; test.beforeEach(async ({ page }) => { signInPage = new SignInPage(page); await signInPage.goto(); }); test('Should Login valid user. @smoke', async ({ page }) => { await signInPage.signIn(ADMIN_EMAIL, getAdminPass()); desktopPage = new DesktopPage(page); await expect(desktopPage.mainMenu.buildMenuIcon).toBeVisible(); }); test('Should NOT Login with invalid login data. @smoke', async ({ page }) => { await signInPage.signIn('someUser@build.one', 'dummyPass'); desktopPage = new DesktopPage(page); await expect(signInPage.errorMessage).toBeVisible(); await expect.soft(signInPage.errorMessage).toHaveText('User Authentication Failed. Please enter correct username and password and try again.'); }); });
test.describe.configure({ mode: 'parallel' });
: Declares that tests from the specified suite can be run in parallel mode. For further information see Playwright parallelism and sharding .test.use({ storageState: 'not-logged-in-state.json' });
: Playwright can use pre-defined state of the application (e.g. which user is logged in, or which cookies to set). For further information see Playwright global setup and global teardown.
Debug
Playwright offers extended test debug options for selectors and running tests. For further information see Playwright Debug Selectors.
The Trace Viewer can help with the following:
To determine the cause of the error.
To make the test replay with screenshots of each step visible, even in headless mode.
To examine recorded Playwright traces after the script has been executed.
For further information see Playwright Trace Viewer.
Build.One test helpers
We have defined some helpers that make it easier to find elements, avoid unnecessary actions and skip steps on the UI that are not part of a test.
akId
: A unique HTML attribute for automated testing.The locator can be set in the following way:
In the code, as in the example above:
page.locator('[akId="SampleGridFormForm-simpleswatbutton"]');
For the whole form or grid:
Open the repository object you want to specify the attribute
akId
for in the Object Designer.Select the object in the Preview panel.
In the Attributes panel go to the attribute
akId
and set the desired value.Click Save.
Relaunch screen in which the
akId
was set.This can help navigate the elements within the specific grid or form more easily if you need to cover an element that may not have a unique identifier.
Useful script snippets
The following snippets are provided in the Typescript.
Shortcut to launch an object
Example to open a Screen:
Press Ctrl+Alt+Shift+L (Mac OS: Control+Shift+Option+L).
The Open repository object window opens.
Enter
ScreenDesktop
.
The whole snippet would look like the following:
await this.page.keyboard.press('Control+Alt+Shift+KeyL'); await this.confirmModal.openRepositoryModalInput.fill(repositoryObjectName); await Promise.all([ this.page.waitForResponse(resp => resp.url().includes('/web/Repository/Container') && resp.status() === 200), this.confirmModal.confirmBtn.click() ]);
Open an object in the Object Designer for editing
The following script opens an object directly in the Object Designer. To skip UI interactions in the test call the script evaluation directly in the browser.
async openObjectInDesigner(objectName: string) { const openData = { objectName }; await this.page.evaluate(data => { window.akioma.repository.openRepositoryObjectInDesigner(data); }, openData); await Promise.all([ this.page.waitForResponse(resp => resp.url().includes('/web/Resource/Akioma.Swat.Repository.AttributeValuesTask/GetObjectMasterDesignAttributes') && resp.status() === 200), this.page.waitForResponse(resp => resp.url().includes('/web/Resource/Consultingwerk.SmartFramework.Repository.Object.ObjectMasterBusinessEntity') && resp.status() === 200) ]); }
Continuous Integration (CI)
Playwright, that we chose as the test automation framework provides a full scale of CI tools integration, also using Docker:
CircleCI
Azure
Jenkins
GitHub Actions
Gitlab CI
etc.
For further information see Playwright Continuous Integrations.
Reporting
Playwright provides reporting using build-in features and can export JUnit report, JSON, HTML, etc. It also offers user to create a custom reporter that can be used for the integration with test-case management systems, Slack, Jira, or anything else that has a public API.
For further information see Playwright Reporters.