-
Notifications
You must be signed in to change notification settings - Fork 33
Web API
Xrm.WebApi was introduced with Dynamics 365 version 9:
Provides properties and methods to use Web API to create and manage records and execute Web API actions and functions in Customer Engagement.
It enables developers to interact with the Web Api using shorthand code. For example, prior to version 9, one would write the following to create an account:
var req = new XMLHttpRequest();
req.open("POST", encodeURI(clientURL + "/api/data/v8.1/accounts"), true);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
if (this.readyState == 4 /* complete */) {
req.onreadystatechange = null;
if (this.status == 204) {
var accountUri = this.getResponseHeader("OData-EntityId");
console.log("Created account with URI: " + accountUri);
} else {
var error = JSON.parse(this.response).error;
console.log(error.message);
}
}
};
req.send(JSON.stringify({ name: "Sample account" }));
Now, using Xrm.WebApi this can be rewritten as:
Xrm.WebApi.createRecord("account", { name: "Sample account" });
XrmMockGenerator.initialise()
initialises an empty Xrm.WebAPI object. Calls to its methods such as createRecord
throw a not implemented error.
The current recommended approach is to therefore stub any API methods being called in the code under test, and force their return values. This allows you to:
- control your test's expected behaviour
- prevent direct calls to the Dynamics database via
XMLHttpRequest
or similar
This example demonstrates a basic client-side script running on a Contact form in Dynamics. When the form is loaded, the script:
- gets the Id of the Contact's Parent Contact via
Xrm.Page.getAttribute.getValue
- retrieves the Parent Contact's name via
Xrm.WebApi.retrieveRecord
- sets the Contact's description to
"My parent is called {parent contact's name}"
viaXrm.Page.getAttribute.setValue
This example uses Sinon.JS to help create Web Api stubs.
Standalone and test framework agnostic JavaScript test spies, stubs and mocks (pronounced "sigh-non", named after Sinon, the warrior).
contact.ts
export default class Contact {
public static onLoad(): Promise<void> {
return Promise.resolve(this.describeParent());
}
private static async describeParent(): Promise<void> {
const parentsName = await this.getParentsName();
return Promise.resolve(Xrm.Page.getAttribute("description").setValue("My parent is called " + parentsName));
}
private static getParentsName(): Promise<string> {
const parentId = Xrm.Page.getAttribute("primarycontactid").getValue()[0].id;
return new Promise((resolve, reject) => {
Xrm.WebApi.retrieveRecord("contact", parentId, "?$select=firstname").then((result) => {
resolve(result.firstname);
}).catch((error) => {
reject(error);
});
});
}
}
contact.test.ts
import * as sinon from "sinon";
import Contact from "../src/contact";
import { XrmMockGenerator } from "xrm-mock";
describe("Contact", () => {
beforeEach(() => {
XrmMockGenerator.initialise();
XrmMockGenerator.Attribute.createString("description");
XrmMockGenerator.Attribute.createLookup("primarycontactid", {
entityType: "contact",
id: "{00000000-0000-0000-0000-000000000001}",
name: "Bob",
});
});
it("should set description to parent contact's firstname", () => {
const stub = sinon.stub(Xrm.WebApi, "retrieveRecord").resolves({
firstname: "Bob",
});
return Contact.onLoad().then(() => {
let description = Xrm.Page.getAttribute("description").getValue();
expect(description).toBe("My parent is called Bob"); // Pass
});
});
});
Code explanation and walkthrough:
Import sinon
. package.json should also contain @types/sinon
as a dependency.
import * as sinon from "sinon";
Import Contact
module. This module is compiled to JavaScript and added to the Contact form in Dynamics.
import Contact from "../src/contact";
Import XrmMockGenerator
to run tests.
import { XrmMockGenerator } from "xrm-mock";
Initialise a global Xrm
object and create required attributes to the fake Xrm object.
XrmMockGenerator.initialise();
XrmMockGenerator.Attribute.createString("description");
XrmMockGenerator.Attribute.createLookup("primarycontactid", {
entityType: "contact",
id: "{00000000-0000-0000-0000-000000000001}",
name: "Bob",
});
Stub the retrieveRecord
method. Instead of calling xrm-mock's default implementation (which throws an error), our test will call this stub which resolves to return a JSON object.
const stub = sinon.stub(Xrm.WebApi, "retrieveRecord").resolves({
firstname: "Bob",
});
Invoke Contact.onLoad
and assert the expect outcome (that the description attribute's value has been updated).
return Contact.onLoad().then(() => {
let description = Xrm.Page.getAttribute("description").getValue();
expect(description).toBe("My parent is called Bob");
});
This example demonstrates a basic client-side script running on a Contact form in Dynamics. When the form is loaded, the script:
- gets the Id of the Contact's Parent Contact via
Xrm.Page.getAttribute.getValue
- retrieves the Parent Contact's name via
Xrm.WebApi.retrieveRecord
- sets the Contact's description to
"My parent is called {parent contact's name}"
viaXrm.Page.getAttribute.setValue
This example uses Vitest to help create Web Api stubs.
Vite-native testing framework, that includes powerful mocks for funtion and API calls.
contact.ts
export default class Contact {
public static onLoad(): Promise<void> {
return Promise.resolve(this.describeParent());
}
private static async describeParent(): Promise<void> {
const parentsName = await this.getParentsName();
return Promise.resolve(Xrm.Page.getAttribute("description").setValue("My parent is called " + parentsName));
}
private static getParentsName(): Promise<string> {
const parentId = Xrm.Page.getAttribute("primarycontactid").getValue()[0].id;
return new Promise((resolve, reject) => {
Xrm.WebApi.retrieveRecord("contact", parentId, "?$select=firstname").then((result) => {
resolve(result.firstname);
}).catch((error) => {
reject(error);
});
});
}
}
contact.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import Contact from "../src/contact";
import { XrmMockGenerator } from "xrm-mock";
describe("Contact", () => {
beforeEach(() => {
XrmMockGenerator.initialise();
XrmMockGenerator.Attribute.createString("description");
XrmMockGenerator.Attribute.createLookup("primarycontactid", {
entityType: "contact",
id: "{00000000-0000-0000-0000-000000000001}",
name: "Bob",
});
});
it("should set description to parent contact's firstname", () => {
// Mock WebApi retrieveRecord to return the first name of the contact
vi.spyOn(Xrm.WebApi, "retrieveRecord").mockResolvedValue({
firstname: "Bob",
});
return Contact.onLoad().then(() => {
let description = Xrm.Page.getAttribute("description").getValue();
expect(description).toBe("My parent is called Bob"); // Pass
});
});
});
Code explanation and walkthrough:
Import the relevant vitest modules
. The project should also include a vitest.config.ts.
import { describe, it, expect, beforeEach, vi } from "vitest";
Import Contact
module. This module is compiled to JavaScript and added to the Contact form in Dynamics.
import Contact from "../src/contact";
Import XrmMockGenerator
to run tests.
import { XrmMockGenerator } from "xrm-mock";
Initialise a global Xrm
object and create required attributes to the fake Xrm object.
XrmMockGenerator.initialise();
XrmMockGenerator.Attribute.createString("description");
XrmMockGenerator.Attribute.createLookup("primarycontactid", {
entityType: "contact",
id: "{00000000-0000-0000-0000-000000000001}",
name: "Bob",
});
Stub the retrieveRecord
method. Instead of calling xrm-mock's default implementation (which throws an error), our test will call this stub which resolves to return a JSON object.
// Mock WebApi retrieveRecord to return the first name of the contact
vi.spyOn(Xrm.WebApi, "retrieveRecord").mockResolvedValue({
firstname: "Bob",
});
Invoke Contact.onLoad
and assert the expect outcome (that the description attribute's value has been updated).
return Contact.onLoad().then(() => {
let description = Xrm.Page.getAttribute("description").getValue();
expect(description).toBe("My parent is called Bob");
});
Contributor: maichopra