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 in the Universal UI. 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.
Testwise is designed for applications using the Universal UI. If you have applications that use a different UI, such as the Windows GUI, we suggest migrating to the Universal UI to benefit from Testwise and other modern features. For more information, see Transition to the Universal UI.
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.
- Test 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 a technology-specific automation library, so it takes time and effort to become familiar with its features.
Set up Testwise
Prerequisites
- Install Visual Studio Code on Windows.
- Open Visual Studio Code (VSCode) and go to extensions.
- Search Playwright Test for VSCode and install the extension.
- Install Node.js.
- Create a folder in your file explorer and open it. This folder will be the folder for your test project.
- Press Alt + D to select the address bar.
- Type
cmdand press Enter to open the terminal. - In the terminal type
code .and press Enter to open VSCode. - In VSCode press Ctrl+~ to open up the terminal within VSCode.
Set up project dependencies
-
To create a new NPM project run the following command:
npm init- Press Enter until you see the following line in the terminal:
type: (commonjs). - Enter
moduleand press Enter. - A confirmation message will pop-up, press Enter again.
- Press Enter until you see the following line in the terminal:
-
To install Testwise, run:
npm i @thinkwise/testwise -
Open the
testwise.jsonfile. -
Replace the example values inside
environmentSettingswith valid details:{
"environmentSettings": {
"baseUrl": "https://<your-environment>",
"serviceUrl": "https://<your-environment>/service",
"metaEndpoint": "iam",
"authUser": "<username>",
"authUserPassword": "<password>",
"guiApplAlias": "<application-alias>"
}
}note- Use a test user instead of personal login details.
- The baseUrl should match your Universal login page.
- The serviceUrl should match the Meta server URL up until
indicium.
Initialize Playwright
To initialize Playwright, run:
npm init playwright
- The message 'Where to put your end-to-end tests?' is shown on the screen. To continue, press Enter.
- The message 'Add a GitHub Actions workflow? is shown on the screen. To continue, press N to select No.
- The message 'Install Playwright browsers (can be done manually via 'npx playwright install'? is shown on the screen. To continue, press Enter.
- Open the file
playwright.config.ts, then change// baseURL: 'http://localhost:3000',tobaseURL: `${testwiseConfig().get<string>('environmentSettings.baseUrl')}`, - Select the
testWiseConfigpart of the baseURL and press Ctrl+.. - Select
Add import from "@thinkwise/testwise"as import option. The import will be added at the top of your code. - Save your changes, by pressing Ctrl+S.
Sync the application model to Testwise
Synchronizing the application model to Testwise will generate Subject page objects. You can directly import and use these in your tests or extend in your own pages, in addition to all Thinkwise components.
To sync the application model, run:
npx testwise sync
The following phases take place:
Phase 1: Cleanup and Initial Compilation
Phase 2: Build Model Data
Phase 3: Refine Model Data
Phase 4: Generate Artifacts
Phase 5: Final Compilation
Phase 6: Backup
The end of the sync is clearly indicated:
Sync process completed successfully!
In model synchronization:
- Page objects will be generated for the subjects the authUser is authorized for in IAM. This can take a couple of minutes depending on your application size.
- If your
testwise.jsonconfiguration is already in place before installing this version, the model and artifacts are generated automatically during installation. - If you install first and add/update configuration afterwards, you will need to run the sync command above to retrieve all updates.
Introducing Generated Artifacts
The sync step resulted in auto-generated page objects in Testwise. You can directly import and use these page objects in your tests or extend in your own pages. The page objects are essentially a combination of a Subject and Screen type as configured in the Software Factory. Use these to ensure your test scripts are simpler, shorter, and less maintenance-prone in the future. You can find an example below to help you get started.
Naming convention
There are several naming conventions:
- Subject name is converted from snake case to Pascal case
- Screen type is one of:
- Main
- Detail
- Popup
- Zoom
Examples:
| Model subject | Variant | Screen | Generated name |
|---|---|---|---|
category | - | Main | CategoryMain |
category | - | Detail | CategoryDetail |
category | Admin | Main | CategoryAdminMain |
Create your first test
To create a linear test script from a record with playback functionality in Playwright:
- In VSCode, go to the tab Testing and select Record new. This open a new browser to start recording your actions.
- Go to your application.
- Log in and perform the actions for your test case.
- Close the browser to stop the recording.
Example: linear script with record and playback function
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);
});
});
Convert the script with Testwise
Apply common test automation patterns in the optimized script. For example, the AAA pattern and Page Object Model (POM) pattern. This approach maximizes reusability and minimizes maintenance by reducing repeating logic.
Testwise has functionality to replace and convert steps in the recording. To be able to use this functionality you need to import the test fixture from Testwise.
To import the test fixture, follow these steps:
-
Change the import on line 1 to:
import { expect } from '@playwright/test'; -
Add a new import:
import { test } from '@thinkwise/testwise'; -
Remove all steps part of the login process where the test id starts with login and replace with:
await page.logInWithCredentials('username', 'password'); -
In Testwise, screens are exposed as typed subjects on
page.subject. Introduce the subject once:const hourBookingPage = await page.subject.HourBookingMain; -
Replace
page.getByTestIdwith the Subject structure:Playwright (old) Testwise (new) page.getByTestId('actionbar__add')hourBookingPage.toolbarXXXX.getAddButton()page.getByTestId('form-field__x__input')hourBookingPage.form1.getFieldByColId('x')lookup option click dropdown.lookupSelect('value')grid selector hourBookingPage.grid1 -
Refactor to act as components, not selectors:
Instead of:
await page.getByTestId('form-field__aantal-uren__input').fill('1');You always convert to:
await hourBookingPage.form1.getFieldByColId('aantal-uren').fill('1'); -
Go back to the Testing tab in VSCode.
-
Run your test again by selecting the test case and selecting Run test. If your test passes, you are successfully linked to the Testwise library.
Example: optimized script
import { expect } from '@playwright/test';
import { test } from '@thinkwise/testwise';
test.describe('Sample Test Suite', () => {
test('Add time entry', async ({ page }) => {
//Arrange
const hourBookingPage = await page.subject.HourBookingMain;
await page.logInWithCredentials('testUser', 'Xyz@245');
await page.goToDeepLink('subject=datum_helper_medewerker_uren_overzicht');
// Act
await hourBookingPage.toolbar503DB1BD.getAddButton().click();
await hourBookingPage.form1.projectIdDropdown.lookupSelect('Quality Assurance');
await hourBookingPage.form1.subProjectIdDropdown.lookupSelect('Automated Testing');
await hourBookingPage.form1.getFieldByColId('aantal-uren').fill('1');
await hourBookingPage.form1.getFieldByColId('activiteit-id__control').fill('Build example test');
await hourBookingPage.form1.getFieldByColId('omschrijving').fill('This is my description');
await hourBookingPage.form1.addressDropdown.lookupSelect('Test Street');
await hourBookingPage.toolbar503DB1BD.getSaveButton().click();
// Assert
await expect(hourBookingPage.grid2.verifyNumberOfRecordsInGrid(1)).resolves.not.toThrow();
});
});
Scripts
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"
}
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() navigates 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_ticketawait page.goToDeepLink('subject=tms_ticket'); -
To navigate to:
https://tcp.thinkwise.app/universal/#application=tcp/subject=vw_persoon_capaciteit/subjectVariant=vw_persoon_capaciteit_medewerker_planningawait 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 use the specified credentials provided at runtime.
This uses 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 are 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 multiple 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 gives you access to one additional action over and above all the actions you get with Playwright.
- The
lookupSelectmethod performs a lookup search to filter the options and then select the option, all as a single action.
myDropdown.lookupSelect('OptionValueToSelect')
- The
selectOptionmethod replaces the Playwright implementation for this control type specifically. It expands the dropdown, and then selects the specified option, all as a single action.
myDropdown.selectOption('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 action bar, 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 is 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 action bar or in the overflow menu, the test will fail.
You can access all the elements on the action bar using the following methods:
getAddButtongetCancelButtongetCopyButtongetDeleteButtongetRefreshButtongetSaveButtongetUpdateButtongetExportButtongetImportButtongetExportImmediatelyButtongetMassUpdateButtongetQuickFilterButtongetFilterButtongetClearAllFiltersButtongetRestoreSortOrderButtongetSortButtongetSearchInputgetUpScreenTypeButtongetUpDetailSettingsButtongetUpManagePrefiltersButtongetUpGridSettingsButtongetCubeSortButtongetCubePivotSettingsButtongetCubeChartSettingsButtongetSaveAsCubeViewButtongetDeleteCubeViewButtongetCollapseAllButtongetExpandAllButton
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
Nextbutton:
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 records in the grid:
await grid.verifyNumberOfRecordsInGrid(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 tab strips 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:
getDetailTabByIdandgetDetailTabByIndexare still available but deprecated. UsegetTabByIdandgetTabByIndexinstead.
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;
};