Skip to main content

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.

note

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

  1. Install Visual Studio Code on Windows.
  2. Open Visual Studio Code (VSCode) and go to extensions.
  3. Search Playwright Test for VSCode and install the extension.
  4. Install Node.js.
  5. Create a folder in your file explorer and open it. This folder will be the folder for your test project.
  6. Press Alt + D to select the address bar.
  7. Type cmd and press Enter to open the terminal.
  8. In the terminal type code . and press Enter to open VSCode.
  9. In VSCode press Ctrl+~ to open up the terminal within VSCode.

Set up project dependencies

  1. 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 module and press Enter.
    • A confirmation message will pop-up, press Enter again.
  2. To install Testwise, run:

    npm i @thinkwise/testwise
  3. To set up the Testwise dependencies, run:

    npx testwise install
  4. Open the testwise.json file.

  5. Change the baseUrl and the serviceUrl to the ones applicable to your Thinkwise environment:

    • The baseUrl should match your Universal login page.
    • The serviceUrl should match the Meta server URL up until indicium.
  6. To initialize Playwright, run:

    npm init playwright
  7. The message 'Where to put your end-to-end tests?' is shown on the screen. To continue, press Enter.

  8. The message 'Add a GitHub Actions workflow? is shown on the screen. To continue, press N to select No.

  9. The message 'Install Playwright browsers (can be done manually via 'npx playwright install'? is shown on the screen. To continue, press Enter.

  10. Open the file playwright.config.ts, then change // baseURL: 'http://localhost:3000', to baseURL: `${testwiseConfig().get<string>('environmentSettings.baseUrl')}`,​​​

  11. Select the testWiseConfig part of the baseURL and press Ctrl+..

  12. Select Add import from "@thinkwise/testwise" as import option. The import will be added at the top of your code.

  13. Save your changes, by pressing Ctrl+S.

Create your first test

To create a linear test script from a record with playback functionality in Playwright:

  1. In VSCode, go to the tab Testing and select Record new. This open a new browser to start recording your actions.
  2. Go to your application.
  3. Log in and perform the actions for your test case.
  4. 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

Testwise has a lot of 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:

  1. Change the import on line 1 to:

    import { expect } from '@playwright/test';
  2. Add a new import:

    import { test } from '@thinkwise/testwise';
  3. Remove all steps part of the login process. These are all steps where the test id starts with login, for example:

    await page.getByTestId('login__username__input').click();
  4. Replace with:

    await page.logInWithCredentials('username', 'password');
  5. Go to back the tab Testing in VSCode.

  6. 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 { 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
await expect(grid.verifyNumberOfRowsInGrid(1)).resolves.not.toThrow();
});
});

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();
});
});

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

note

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 lookupSelect method performs a lookup search to filter the options and then select the option, all as a single action.
myDropdown.lookupSelect('OptionValueToSelect')
  • The selectOption method 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:

  • 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

note

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:

  1. Select the export all rows radio button and select next:
await exportComponent.exportAllRows();
  1. Select the Next button:
await exportComponent.nextStep();
  1. Select columns to export:
const columnsToSelect: string[] = ['FirstColumnName','SecondColumnName','ThirdColumnName'];
await exportComponent.selectColumns(columnsToSelect);
note

Takes in an array of strings; for every column in the list, a click action is performed on the checkbox matching the column name.

  1. Select the export format:
import { ExportFormat } from '@thinkwise/testwise';

await exportComponent.selectExportFormat(ExportFormat.CSV)

See Enumerations for more options.

  1. 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 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: getDetailTabById and getDetailTabByIndex are still available but deprecated. Use getTabById and getTabByIndex 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;
};

Was this article helpful?