End-to-end tests
Introduction to end-to-end tests
End-to-end (E2E) testing helps you verify the complete flow of an application from the perspective of your users. It complements other quality measures such as branching and merging, code reviews, unit tests, smoke tests, and validations by providing confidence that the system works as intended in real-world scenarios. Where unit and integration tests focus on smaller components, end-to-end tests confirm that these components work together correctly
Testwise for end-to-end testing
Applications built with the Thinkwise Platform are often complex and remain in use for many years. To ensure quality, test automation is essential. It provides consistent and repeatable checks and reduces the need for manual regression testing, so testers can focus on new features and critical areas.
Testwise is a test automation solution designed specifically for Thinkwise applications. Built on Playwright, it adds prebuilt page extensions, custom controls, and components that follow Thinkwise UI patterns. With these, you can easily interact with complex screens such as grids, forms, and action bars. Testwise also includes scripts for authentication and user simulation, helping you avoid repetitive setup tasks and improving test stability.
In summary, Testwise streamlines end-to-end testing by turning common Thinkwise actions into clear, reusable methods. This allows development teams to focus on validating business logic.
Benefits of Testwise
Using Testwise as part of your Software Factory application testing and automation efforts offers several benefits:
- You do not have to maintain components yourself. If implementations change, Thinkwise maintains and update component logic.
- Development is faster with built-in, ready to use functions and helpers that you can use directly.
- In the future, model-based, auto-generated artifacts will further increase productivity.
One limitation of Testwise is the learning curve. This is because Testwise is technology-specific automation library, so it takes time and effort to become familiar with its features.
Getting started with Testwise
Refer to the Testwise 101 blog post as a quick guide. A formal guide will be added to this section once the installation process has been refined.
Set up scripts
Use the following scripts to get started with Testwise.
To install Testwise:
npx testwise install
To log in via the terminal and run a test:
npx testwise-credentials <script_name_to_run_tests>
An example of test scripts for the testwise-credentials
command:
"scripts": {
"test": "npx playwright test --ui --reporter=html --output=./playwright-report",
"debug": "npx playwright test --debug --reporter=html --output=./playwright-report",
"headless": "npx playwright test --reporter=html --output=./playwright-report"
}
Build a linear script
In this scenario, Playwright was used to record and playback functionalities to create the following linear script:
import { test, expect } from '@playwright/test';
test.describe('Sample Test Suite', () => {
test('Add time entry', async ({ page }) => {
await page.goto('https://tcp.thinkwise.app/universal/#application=tcp');
await page.getByTestId('login__username__input').click();
await page.getByTestId('login__username__input').fill('testUser');
await page.getByTestId('login__password__input').click();
await page.getByTestId('login__password__input').fill('Xyz@245');
await page.getByTestId('login__login-button').click();
await page.getByTestId('listbar__project-beheer').click();
await page.getByTestId('listbar__project-beheer__item__0').click();
await page.getByTestId('actionbar__add').click();
await page.getByTestId('form-field__project-id__control__input').click();
await page.getByTestId('form-field__project-id__control__input').fill('q');
await page.getByTestId('form-field__project-id__control__option__quality-assurance').click();
await page.getByTestId('form-field__sub-project-id__control__input').click();
await page.getByTestId('form-field__sub-project-id__control__option__automated-testing').click();
await page.getByTestId('form-field__aantal-uren__input').click();
await page.getByTestId('form-field__aantal-uren__input').fill('1');
await page.getByTestId('form-field__activiteit-id__control__input').click();
await page.getByTestId('form-field__activiteit-id__control__input').fill('Build example test');
await page.getByTestId('form-field__omschrijving__input').click();
await page.getByTestId('form-field__omschrijving__input').fill('This is my description');
await page.getByTestId('form-field__adres-id__control__input').click();
await page.getByTestId('form-field__adres-id__control__input').fill('Test Street');
await page.getByTestId('actionbar__save').click();
expect(await page.getByTestId('screen__uren-boeken-dag__grid1').getByRole('row').all).toEqual(1);
});
});
Adjust the script
After building the linear script, you can optimize it by using Testwise components and controls. This results in the following code:
import { expect } from '@playwright/test';
import { Actionbar, createLookupDropdown, Form, Grid, test } from '@thinkwise/testwise';
test.describe('Sample Test Suite', () => {
test('Add time entry', async ({ page }) => {
//Arrange
const actionbar = new Actionbar(page);
const form = new Form(page);
const grid = new Grid(page);
const projectIdControl = createLookupDropdown(page.getByTestId('form-field__project-id__control__input'));
const subProjectIdControl = createLookupDropdown(page.getByTestId('form-field__sub-project-id__control__input'));
const addressControl = createLookupDropdown(page.getByTestId('form-field__adres-id__control__input'));
await page.logInWithCredentials('testUser', 'Xyz@245');
await page.goToDeepLink('subject=datum_helper_medewerker_uren_overzicht');
// Act
await actionbar.getAddButton().click();
await projectIdControl.lookupSelect('Quality Assurance');
await subProjectIdControl.lookupSelect('Automated Testing');
await form.getFieldByColId('aantal-uren').fill('1');
await form.getFieldByColId('activiteit-id__control').fill('Build example test');
await form.getFieldByColId('omschrijving').fill('This is my description');
await addressControl.lookupSelect('Test Street');
await actionbar.getSaveButton().click();
// Assert
expect(await grid.verifyNumberOfRowsInGrid(1)).toBeTruthy();
});
});
Apply best practices
To optimize the script further, apply common test automation patterns such as the Page Object Model (POM) pattern. This approach maximizes reusability and minimizes maintenance by reducing repeating logic. This results in the following code:
import { expect } from '@playwright/test';
import { test } from '@thinkwise/testwise';
import { HoursBookingPage } from './pages/HoursBookingPage';
test.describe('Sample Test Suite', () => {
test('Add time entry', async ({ page }) => {
//Arrange
const hoursBookingPage = new HoursBookingPage(page);
await page.logInWithCredentials('testUser', 'Xyz@245');
await hoursBookingPage.goTo();
// Act
await hoursBookingPage.addNewRecord();
await hoursBookingPage.getProjectIdControl.lookupSelect('Quality Assurance');
await hoursBookingPage.getSubProjectIdControl.lookupSelect('Automated Testing');
await hoursBookingPage.inputHours('1');
await hoursBookingPage.fillActivity('Build example test');
await hoursBookingPage.addDescription('This is my description');
await hoursBookingPage.getAddressControl.lookupSelect('Test Street');
await hoursBookingPage.saveRecord();
// Assert
expect(await hoursBookingPage.Grid.verifyNumberOfRowsInGrid(1)).toBeTruthy();
});
});
Page extensions for Playwright
In this section, you can find commonly used functionalities that are built into the Playwright page object.
Deep linking
Deep linking allows you to bypass UI navigation.
The goToDeepLink()
will navigate within the current application, utilizing the configured baseUrl, to allow environment switching without tests breaking.
Examples how to use this functionality are provided.
-
To navigate to:
https://tcp.thinkwise.app/universal/#application=tcp/subject=tms_ticket
await page.goToDeepLink('subject=tms_ticket');
-
To navigate to:
https://tcp.thinkwise.app/universal/#application=tcp/subject=vw_persoon_capaciteit/subjectVariant=vw_persoon_capaciteit_medewerker_planning
await page.goToDeepLink('subject=vw_persoon_capaciteit/subjectVariant=vw_persoon_capaciteit_medewerker_planning');
The deep-link format for the Universal GUI is: https://[server]/#application=[alias]/processflow=[id]?...
.
The helper behind the goToDeepLink()
method works with this format and automatically prefixes the configured baseURL, making tests portable across environments.
For more details on the URL structure, see Create a deep link for Universal GUI.
Authentication
The login method uses environment variables. The following script sets process environment variables to use with this method.
When you run the testwise-credentials
script, executed tests will use the specified credentials provided at runtime.
This will use the specified credentials on any tests where the await page.logIn()
extension method is invoked.
If you run the following command in your terminal:
`npx testwise-credentials <project-script-name>`
You will be prompted to enter a username and password.
There are multiple authentication methods when running tests to log in or log out:
-
logIn()
- Extension method is used in combination with other features using specified login credentials.import { test } from '@thinkwise/testwise';
test.describe('Log in with credentials supplied at runtime', () => {
test.beforeEach(async ({ page }) => {
await page.logIn();
});
test('should display the main menu after login', async ({ page }) => {
// Add your test steps here
});
}); -
logInWithCredentials()
- To log in using test credentials, a username and password.import { test } from '@thinkwise/testwise';
test.describe('Log in with credentials', () => {
test.beforeEach(async ({ page }) => {
await page.logInWithCredentials('MyUsername', 'MyPassword');
});
test('should display the main menu after login', async ({ page }) => {
// Add your test steps here
});
}); -
logInWithOptions()
- Used to pass additional information, such as a different application to launch into. See Enumerations for more options.import { test, type UniversalLoginOptions } from '@thinkwise/testwise';
test.describe('Log in with options', () => {
test.beforeEach(async ({ page }) => {
const options: UniversalLoginOptions = {
username: 'MyUsername',
password: 'MyPassword',
config: {
defaultApplication: 'iam'
}
};
await page.logInWithOptions(options);
});
test('should display the main menu after login', async ({ page }) => {
// Add your test steps here
});
}); -
logOut()
- Used to log out in scenarios where logging out is required.import { test } from '@thinkwise/testwise';
test.describe('Test logout functionality', () => {
test.beforeEach(async ({ page }) => {
await page.logInWithCredentials('MyUsername', 'MyPassword');
});
test('should log me out successfully', async ({ page }) => {
await page.logOut();
// Add your verification steps here
});
});
User simulation
You need to be logged in as a user with simulation rights for this functionality to work. See User simulation for more information.
There are mutliple ways to start or stop a user simulation:
-
simulateUser()
- To simulate other users after logging in.test('should be able to simulate a different user and do stuff', async ({ page }) => {
await page.simulateUser('UsernameOfSimulatedUser');
// Do something as that user
}); -
getSimulatedUser()
- Get the username of the currently simulated user.test('should be able to get simulated users username', async ({ page }) => {
const simulatedUser = await page.getSimulatedUser();
console.log(`Simulated user is: ${simulatedUser}`);
}); -
stopSimulation()
- Stop user simulation.test('should be able to do other stuff after stopping user simulation', async ({ page }) => {
await page.simulateUser('UsernameOfSimulatedUser');
// Do something as simulated user
await page.stopSimulation();
// Do some other stuff as logged in user
});
Custom controls
Initializing these controls provides actions beyond standard Playwright actions.
Lookup dropdown
You can use this for drop-down controls, where you want to filter and select options. Ordinarily you would initialize your element objects like this:
const myDropdown = page.getByTestId('my_id');
Now, this is replaced with the following:
import { createLookupDropdown } from '@thinkwise/testwise';
myDropdown = createLookupDropdown(page.getByTestId('my_id'));
createLookupDropdown
takes an input parameter of type Locator
.
This means you would take any selector definition you would have previously used to define the element and then feed it into method.
Using the LookupDropdown
control will give you access to one additional action over and above all the actions you get with Playwright.
myDropdown.lookupSelect('OptionValueToSelect')
Components
Components are the centralized logic that serve as counterparts to components in the Software Factory. They provide access to specific actions that make end-user interaction easier.
For more information on the available screen components, see Universal screen components and Screen components.
Action bar
This component gives you access to all standard elements that come with the action bar out of the box.
To initialize the Action bar component:
import { Actionbar } from '@thinkwise/testwise';
const actionbar = new Actionbar(page);
const actionbarWithContext = new Actionbar(page, page.locator('.Toolbar_ClassID'));
When initializing the actionbar, the page object, as well as an optional locator for context/anchor point are taken in. If you do not pass a context in, then the root of your page will be set as the default context.
To use the Action bar component:
Get the button of choice and perform the desired Playwright action on it:
await actionbar.getExpandAllButton().click();
You can also interact with this same button from within the overflow menu:
await actionbar.overflowMenu.getExpandAllButton().click();
The overflow menu opens automatically; you don’t need to trigger it explicitly. The component assumes the correct state. If you attempt to interact with buttons that are not present on the Actionbar or in the overflow menu, the test will fail.
You can access all the elements on the action bar using the following methods:
getAddButton
getCancelButton
getCopyButton
getDeleteButton
getRefreshButton
getSaveButton
getUpdateButton
getExportButton
getImportButton
getExportImmediatelyButton
getMassUpdateButton
getQuickFilterButton
getFilterButton
getClearAllFiltersButton
getRestoreSortOrderButton
getSortButton
getSearchInput
getUpScreenTypeButton
getUpDetailSettingsButton
getUpManagePrefiltersButton
getUpGridSettingsButton
getCubeSortButton
getCubePivotSettingsButton
getCubeChartSettingsButton
getSaveAsCubeViewButton
getDeleteCubeViewButton
getCollapseAllButton
getExpandAllButton
Export
To use this Testwise component, you must open it through regular means. For example, you can select Export on the action bar before interacting with it.
You can use this component when you need to export your data to a file.
To initialize the Export component:
import { ExportComponent } from '@thinkwise/testwise';
const exportComponent = new ExportComponent(page);
const exportComponentWithContext = new ExportComponent(page, page.getByTestId('SomeContainerTestID'));
To use the Export component:
- Select the export all rows radio button and select next:
await exportComponent.exportAllRows();
- Select the
Next
button:
await exportComponent.nextStep();
- Select columns to export:
const columnsToSelect: string[] = ['FirstColumnName','SecondColumnName','ThirdColumnName'];
await exportComponent.selectColumns(columnsToSelect);
Takes in an array of strings; for every column in the list, a click action is performed on the checkbox matching the column name.
- Select the export format:
import { ExportFormat } from '@thinkwise/testwise';
await exportComponent.selectExportFormat(ExportFormat.CSV)
See Enumerations for more options.
- Complete export:
const exportedData : Record<string, string>[] = await exportComponent.completeExport();
// Assert
for (const [index, row] of exportedData.entries()) {
expect(row.SomeKey).toBe('Some value');
}
Form
To initialize the Form component:
import { Form } from '@thinkwise/testwise';
const form = new Form(page);
const formWithContext = new Form(page, page.getByTestId('SomeContainerTestID'));
Uses for the Form component:
- Verify edit mode:
const isInEditMode : boolean = await form.isInEditMode();
// Assert
expect(isInEditMode).toBeTruthy();
- Get field by current value:
expect(await form.getFieldByValue('Some value')).toBeEditable();
- Get field by column ID:
const column = form.getFieldByColId('Some_column_id');
- Get lookup button by column ID:
const columnLookupButton = form.getLookupButtonByColId('Some_column_id');
Grid
To initialize the Grid component:
import { Grid } from '@thinkwise/testwise';
const grid = new Grid(page);
const gridWithContext = new Grid(page, page.getByTestId('SomeContainerTestID'));
Uses for the Grid component:
- Get column by header text - Returns a Locator for the column header with the specified text:
const column = grid.getColumnHeaderByText('');
- Get row by index - Returns a Locator for the row at the given index:
const row = grid.getRowByIndex(0);
- Get cell by exact value - Returns a Locator for the cell containing the exact value:
const cell = grid.getCellByExactValue('Some value');
- Open Excel-style filter popup for a column
await grid.openExcelStyleFilterPopup('Order number');
- Get all rows matching a column value - Returns an array of Locators for rows where the specified column’s value matches
valueToMatch
:
const rows = await grid.getRowsByColValue('order_id', '12345');
- Get all unique cell values from a column - Returns an array of unique string values from the specified column:
const values = await grid.getCellValuesByColId('customer_name');
- Get all column header values - Returns an array of unique column header strings:
const headers = await grid.getColumnHeaderValues();
- Filter a column by value (Excel style)
await grid.filterByColumnValueExcelStyle('Status', 'Active');
- Get all rows in the grid:
const allRows = grid.rows();
- Verify the number of rows in the grid:
await grid.verifyNumberOfRowsInGrid(5);
- Check if the "No result" overlay is visible:
const hasNoRows = await grid.hasNoRowsOverlay();
Pop-up
To initialize the Pop-up component:
import { PopUpComponent } from '@thinkwise/testwise';
const popup = new PopUpComponent(page);
Uses for the Pop-up component:
- Confirm "Yes" action - Clicks the "Yes" button in the popup and resolves when the action is complete:
await popup.confirmYes();
- Confirm "No" action - Clicks the "No" button in the popup and resolves when the action is complete:
await popup.confirmNo();
- Get the popup content message - Returns the text content of the popup message:
const message = await popup.getContentMessage();
- Click the close button - Selects the close (X) button to dismiss the popup:
await popup.clickCloseButton();
Task bar
To initialize the Task bar component:
import { TaskBar } from '@thinkwise/testwise';
const taskBar = new TaskBar(page);
const taskBarWithContext = new TaskBar(page, page.getByTestId('SomeContainerTestID'));
Uses for the Task bar component:
- Get all task buttons:
const allTasks = taskBar.tasks();
- Get a task button by its data-testid key:
const taskById= taskBar.getTaskById('My_Task_Id');
- Get a task button by its visible label:
const task = taskBar.getTaskByLabel('My Task');
- Select a task button by its data-testid key:
await taskBar.clickById('my-task-id');
- Select a task button by its visible label:
await taskBar.clickByLabel('My Task');
Task tile
To initialize the Task tile component:
import { TaskTile } from '@thinkwise/testwise';
const taskTile = new TaskTile(page);
const taskTileWithContext = new TaskTile(page, page.getByTestId('SomeContainerTestID'));
Uses for the Task tile component:
- Get all task tile buttons:
const allTasks = taskTile.tasks();
- Get a task tile button by its data-testid key:
const task = taskTile.getTaskById('my-task-id');
- Get a task tile button by its visible label:
const task = taskTile.getTaskByLabel('My Task');
- Select a task tile button by its data-testid key:
await taskTile.clickById('my-task-id');
- Select a task tile button by its visible label:
await taskTile.clickByLabel('My Task');
Component tab
Tabs represent tabstrips in Thinkwise screens (for example, List tabs, Forms or Detail tabs). They allow navigation between sections within a subject or screen type. There are components for the Component tab and the Detail tab.
To initialize the Component tab:
import { ComponentTab } from '@thinkwise/testwise';
// Without context (defaults to the root tabstrip)
const componentTab = new ComponentTab(page);
// With context (scopes to a specific tabstrip, e.g. inside a detail)
const componentTabWithContext = new ComponentTab(page, page.locator('[data-testid="my-context"]'));
Uses for the Component tab:
- Open the List tab (defaults to tabindex = -1 for master):
await componentTab.openListTab();
- Get the locator for the List tab:
const listTab = await componentTab.getListTab();
- Open the Form tab:
await componentTab.openFormTab();
- Get the locator for the Form tab:
const formTab = await componentTab.getFormTab();
- Open a tab by its id (data-testid suffix):
await componentTab.openById('users');
- Open a tab by index:
await componentTab.openByIndex(2);
- Get the badge count for a tab:
const count = await componentTab.getBadgeCount('user-group-tags');
Returns 0
if no badge is visible.
Detail tab
To initialize the Detail tab:
import { DetailTab } from '@thinkwise/testwise';
const detailTab = new DetailTab(page);
const detailTabWithContext = new DetailTab(page, page.locator('[data-testid="detail-context"]'));
Uses for the Detail tab:
- Open a detail tab by ID:
await detailTab.openById('authorization');
- Open a detail tab by index:
await detailTab.openByIndex(1);
- Get the locator for a detail tab:
const tab = await detailTab.getTabById('users');
- Get the badge count for a detail tab:
const count = await detailTab.getBadgeCount('group-owners');
Note:
getDetailTabById
andgetDetailTabByIndex
are still available but deprecated. UsegetTabById
andgetTabByIndex
instead.
Enumerations
Enumerations are used to define a set of named constants. You can use the enumerations below in your test scripts.
Export formats:
export enum ExportFormat {
CSV = 'csv',
XLSX = 'xlsx',
XLS = 'xls'
}
Universal login options:
export type UniversalLoginOptions = {
username?: string;
password?: string;
serviceUrl?: string;
metaEndpoint?: string;
config?: UniversalConfigOptions;
};
Universal config options:
export type UniversalConfigOptions = {
barcodeScannerSymbologies?: string;
cortexEnabledSymbologies?: string[];
cortexLicense?: string;
debugMode?: boolean;
devDisableProcessFlows?: boolean;
useFormFieldBackgroundColor?: boolean;
defaultApplication?: string;
defaultPlatform?: number;
loginOptionsDisabled?: boolean;
loginOptionsHidden?: boolean;
installNotificationDisabled?: boolean;
enableDragDrop?: boolean;
installNotificationExpirationInDays?: number;
spacingMode?: string;
serviceUrl?: string;
useServiceWorker?: boolean;
};