PCF Controls — Automated Testing

Roger Hill
6 min readApr 3, 2024

--

Following some recent experiences, I wanted to write a piece about automated testing as it applies to PCF Controls (which are a feature of the Microsoft Dynamics 365/Power platform environment)

This started with what seemed at first glance a fairly simple requirement in a user story:

As a user I want to be able to enter an date and time for an incident, but the time must be entered explicitly and not be set to a default value.

For those not familiar with Dynamics 365 model driven applications, these do offer the possibility of adding date/time fields to a form. However, with the out of the box date time controls, when a date is entered into the date/time field it will default the time to value of 8am. This clearly does not meet the customer requirement.

A variety of solutions were offered by project members — entering the field as text and then parsing the date time in a JavaScript webresource, plugin, orm workflow, for example. None of these were particularly elegant. The use of a PCF contol seemed indicated. This would allow us to directly control the validity of what was entered, and the date time field would only become valid once an explicit time value had been entered.

Creating the control

The first step was to use VSCode to create a new pcf field control in an empty folder.

pac pcf init --namespace RBH --name SpecificDateTime --template field --run-npm-install

This creates most of the files required for your PCF control. The ones of most interest to us are index.ts and ControlManifest.Input.xml

Index.ts will contain the implementation of our custom date time control, but first we need to deal with the ControlManifest.Input.xml file.

When a model driven app places a control on a form, it needs to know the type of the field that it will represent. The default is to create a text field called, “sampleProperty”. We will change this to a date time field as below:

<property name="SpecificDateTimeField" display-name-key="Date Time" description-key="Field mapped to custom control" 
of-type="DateAndTime.DateAndTime" usage="bound" required="true" />

At this point typing “npm start watch” will display your control in a browser sandbox (but it will be empty as we have not implemented anything yet)

Next step is to create an implementation to display this control. I won’t include the full code here, but you will find it here on github

        this._container = document.createElement("div");
....

this.dateInputElement = document.createElement("input");
this.dateInputElement.setAttribute("type", "date");
this.dateInputElement.addEventListener("input", this._dateRefreshed);
....

this.timeInputElement = document.createElement("input");
this.timeInputElement.setAttribute("type", "time");
this.timeInputElement.addEventListener("input", this._timeRefreshed);
....

this._container.appendChild(this.dateInputElement);
this._container.appendChild(this.timeInputElement);
container.appendChild(this._container);

The essential parts of the control setup are that we create date and time input controls, with event listeners on any inputs. We combine the two in a “div” element and then append this to the container element provided by the model driven app environment.

Put it all together, add some css to style the controls (or clone the above repository and type “npm update”) and you should see the following when you type “npm start watch”.

The control can then be deployed to a Dynamics CRM environment and added to a form.

Note that we have added both the out of the box and also our new PCF date controls to the form, to allow us to compare the behaviour of both controls.

Unit Testing the control

The next question is to ask how we test this control. Obviously, we can do this manually, either within the sandbox, or when deployed into a browser.
But if you think that not writing unit tests is a good idea see my article on 6 reasons for not writing unit tests

As a user, I would like to be able to type in the date and time in as frictionless a manner as possible. Given that the user base is in the UK, I would like them to be able to just focus on the field and type in “230320241234” and have it set 23 March 2024 with a date of 12:34.
Manual testing shows that the deployed control does not work this way. So where do we start?

We’ll use the jest test runner in vscode, and add some tests. Start by adding he following packages to our project.

npm install --save-dev jest
npm install --save-dev ts-jest
npm install --save-dev @testing-library/jest-dom
npm install --save-dev @types/jest
npm install --save-dev eslint-plugin-jest-dom
npm install --save-dev eslint-plugin-testing-library
npm install --save-dev @testing-library/jest-dom
npm install --save-dev jest-environment-jsdom
npm install --save-dev jest-mock-extended

This should be sufficient to get our initial test up and running: -

test(  'Hello World', () => {

})

This passes with flying colours!

PS C:\Work\Medium\SpecificDateTimeControl> jest
PASS SpecificDateTime/tests/SpecificDatePicker.test.ts
√ Hello World (2 ms)

Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.508 s
Ran all test suites.
PS C:\Work\Medium\SpecificDateTimeControl>

Now we have our test framework up and running — we need to add some tests that actually do something.
What we would like to do is set up our control, type some text into it and then check that the control has the expcted value.

Let’s extend our test framework to create an instance of the PCF control. We will use testing library (https://testing-library.com/) to provide an implementation of the DOM for use in testing.

import { SpecificDateTime } from '../index';
import mock from "jest-mock-extended/lib/Mock";
import { IInputs } from "../generated/ManifestTypes";
import {getByTestId} from '@testing-library/dom';
import '@testing-library/jest-dom';
import userEvent from '@testing-library/user-event';

test( 'First test ', () => {
// Allow us to enter text into the controls.
userEvent.setup();

// Set up dependencies of the PCF control init function
const datePicker = new SpecificDateTime();
const context = mock<ComponentFramework.Context<IInputs>>();
const outputChanged = jest.fn(); // mock<() => void>();
const state = mock<ComponentFramework.Dictionary>();

document.body.innerHTML = '<div data-testid="container"></div>';
const container = getByTestId( document.body, "container") as HTMLDivElement;

// Initialize the PCF control
datePicker.init(context, outputChanged,state, container);

// Update the control with a new value (null)
context.parameters.SpecificDateTimeField = mock<IInputs["SpecificDateTimeField"]>();
context.parameters.SpecificDateTimeField.raw = null;

// Test continues...
}

We’ll add a number of tests here
1) Make sure that the control returns a value of undefined initially.
2) Focus on the date control and type in a date.
4) Check that the control value is still undefined.
5) Type in a time
6) Check that the control now has the expected value.

// Check that control value really is undefined.
expect( datePicker.getOutputs().SpecificDateTimeField).toBeUndefined();

// Now type in the date value and check that the date is still undefined.
userEvent.click(getByTestId(container,"date"));
userEvent.type(getByTestId(container,"date"), "23042024");
expect( datePicker.getOutputs().SpecificDateTimeField).toBeUndefined();

// Type in the time value and check that the control now has a defined value.
userEvent.click(getByTestId(container,"time"));
userEvent.type(getByTestId(container,"time"), "1234");
expect( datePicker.getOutputs().SpecificDateTimeField).toEqual("2024-04-23t12:34");

This appears to be fine, but it does not actually work — the value returned in the last line is “undefined”. Setting breakpoints in the date and time controls input events show that they are not actually being hit.

There are a couple of points to note:

  • userEvent.click/.type and the “getByTestId” functions to not execute synchronously — rather they return a Promise which needs to be “awaited”.
  • The testing library does not support any specific locale, any dates that are input using “userEvent.type()” have to be in ISO 8601 format (“2024–04–23”). Similarly the time has be to in a locale neutral format (12:34).

Once we realise this, we can see that it is not possible to implement these unit tests with the current DOM testing libraries.However, there are still some useful unit tests that we can create:

  • Ensure that the control rejects an invalid year that is below the minumum date (2022)
  • When the date field is cleared we need to make sure that the control value is set to undefined.

Once the limitations of the testing library were understood some tests could be successfully implemented — see the github project for the final result.

Testing locale dependent user input

In the next part of this article I will discuss how to carry out tests of locale dependent aspects of this control. This will use playwright to interact with a live model driven app and using headless browser sessions to simulate user input into our date time control, and to then examine the result.

--

--

Roger Hill

Working as a Dynamics 365 CRM consultant in London with extensive experience in all aspects of development for Microsoft Dynamics CRM.