Mock XP
Contents
In this chapter we introduce Mock XP - an NPM library to mock Enonic XP APIs and state.
Introduction
When it comes to mocking more complex things (such as interaction with Content and Node APIs) Enonic has an NPM module called @enonic/mock-xp
that can help you with some of the heavy lifting.
Install Mock XP
Run the following command to install it:
npm install --save-dev @enonic/mock-xp
Source code
Let’s create the following controller file:
import type {Content} from '/lib/xp/portal';
import type {Request, Response} from '/index.d';
import {getContent, imageUrl, assetUrl} from '/lib/xp/portal';
declare type ContentWithPhotos = Content<{
photos?: string|string[]
}>;
export function get(_request: Request): Response {
const content = getContent<ContentWithPhotos>();
const {data, displayName} = content;
const photoId = Array.isArray(data.photos) ? data.photos[0] : data.photos;
const imageSrc = photoId
? imageUrl({
id: photoId,
scale: "width(500)",
})
: null;
const img = imageSrc ? `<img src="${imageSrc}"/>` : '';
const styleHref = assetUrl({path: 'styles.css'});
return {
body: `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${displayName || 'Preview of content without display name'}</title>
<link rel="stylesheet" type="text/css" href="${styleHref}"/>
</head>
<body>
<h1>${displayName || 'Display name missing'}</h1>
${img}
<h3>This is a sample preview</h3>
Use live integrations with your front-end, or just a mockup - like this :-)
</body>
</html>`
};
};
Tests
In this test mock-xp
is used to set up a project repo, a person folder, a couple of images, and a couple content items (of type person
) which reference the images.
In addition, mock-xp’s Log
is used to get nice colorful logging. In order for libPortal.getContent()
to work, request
property is set on the libPortal
object.
For the test file to not become overly large, it’s been split it into smaller parts:
import type {
assetUrl as assetUrlType,
getContent as getContentType,
imageUrl as imageUrlType,
} from '@enonic-types/lib-portal';
import type {Log} from './global';
import {
App,
LibContent,
LibPortal,
Server
} from '@enonic/mock-xp';
import {jest} from '@jest/globals';
export {
Request,
} from '@enonic/mock-xp';
const APP_KEY = 'com.example.myproject';
const PROJECT_NAME = 'intro';
export const server = new Server({
loglevel: 'debug'
}).createProject({
projectName: PROJECT_NAME
}).setContext({
projectName: PROJECT_NAME
});
// Avoid type errors below.
// eslint-disable-next-line @typescript-eslint/no-namespace
declare module globalThis {
let log: Log
}
globalThis.log = server.log as Log;
const app = new App({
key: APP_KEY
});
export const libContent = new LibContent({
server
});
export const libPortal = new LibPortal({
app,
server
});
jest.mock('/lib/xp/portal', () => {
return {
assetUrl: jest.fn<typeof assetUrlType>((params) => libPortal.assetUrl(params)),
getContent: jest.fn<typeof getContentType>(() => libPortal.getContent()),
imageUrl: jest.fn<typeof imageUrlType>((params) => libPortal.imageUrl(params)),
}
}, { virtual: true });
This test uses two image files, lea-seydoux.jpg and jeffrey-wright.jpg , which are not included in this project template. You can use any image files you have available. Just make sure either to update the test to point to the correct files or to create the files in the correct location. |
import type {ByteSource} from '@enonic-types/core';
import {
describe,
expect,
test as it
} from '@jest/globals';
import {
Request,
libContent,
libPortal,
server,
} from './mockXP';
import {readFileSync} from 'fs';
import {join} from 'path';
const IMAGE_FILENAME_1 = 'Lea-Seydoux.jpg';
const IMAGE_FILENAME_2 = 'Jeffrey-Wright-hp.jpg';
//──────────────────────────────────────────────────────────────────────────────
// Test data
//──────────────────────────────────────────────────────────────────────────────
const personFolder = libContent.create({
contentType: 'base:folder',
data: {},
name: 'persons',
parentPath: '/',
});
const leaSeydouxJpg = libContent.createMedia({
data: readFileSync(join(__dirname, '..', IMAGE_FILENAME_1)) as unknown as ByteSource,
name: IMAGE_FILENAME_1,
parentPath: personFolder._path,
mimeType: 'image/jpeg',
focalX: 0.5,
focalY: 0.5,
});
libContent.create({
contentType: 'com.example.myproject:person',
data: {
bio: "French actress Léa Seydoux was born in 1985 in Paris, France, to Valérie Schlumberger, a philanthropist, and Henri Seydoux, a businessman.",
dateofbirth: "1985-07-01",
photos: leaSeydouxJpg._id,
},
name: 'lea-seydoux',
displayName: 'Léa Seydoux',
parentPath: personFolder._path,
});
const jeffreyWrightHpJpg = libContent.createMedia({
data: readFileSync(join(__dirname, '..', IMAGE_FILENAME_2)) as unknown as ByteSource,
name: IMAGE_FILENAME_2,
parentPath: personFolder._path,
mimeType: 'image/jpeg',
focalX: 0.5,
focalY: 0.5,
});
libContent.create({
contentType: 'com.example.myproject:person',
data: {
bio: "Born and raised in Washington DC, Jeffrey Wright graduated from Amherst College in 1987. Although he studied Political Science while at Amherst, Wright left the school with a love for acting. Shortly after graduating he won an acting scholarship to NYU, but dropped out after only two months to pursue acting full-time.",
dateofbirth: "1965-12-07",
photos: jeffreyWrightHpJpg._id,
},
name: 'jeffrey-wright',
displayName: 'Jeffrey Wright',
parentPath: personFolder._path,
});
//──────────────────────────────────────────────────────────────────────────────
// Test suite
//──────────────────────────────────────────────────────────────────────────────
describe('preview', () => {
it('is able to render a page for Léa Seydoux', () => {
libPortal.request = new Request({
repositoryId: server.context.repository,
path: '/admin/site/preview/intro/draft/persons/lea-seydoux'
});
import('/lib/myproject/controller').then(({get}) => {
const response = get(libPortal.request);
expect(response).toEqual({
body: `<!DOCTYPE html>
<html>
<head>
<meta charset=\"utf-8\">
<title>Léa Seydoux</title>
<link rel=\"stylesheet\" type=\"text/css\" href=\"/admin/site/preview/intro/draft/persons/lea-seydoux/_/asset/com.example.myproject:0123456789abcdef/styles.css\"/>
</head>
<body>
<h1>Léa Seydoux</h1>
<img src=\"/admin/site/preview/intro/draft/persons/lea-seydoux/_/image/00000000-0000-4000-8000-000000000006:0123456789abcdef/width-500/Lea-Seydoux.jpg\"/>
<h3>This is a sample preview</h3>
Use live integrations with your front-end, or just a mockup - like this :-)
</body>
</html>`
});
});
}); // Léa Seydoux
it('is able to render a page for Jeffrey Wright', () => {
libPortal.request = new Request({
repositoryId: server.context.repository,
path: '/admin/site/preview/intro/draft/persons/jeffrey-wright'
});
import('/lib/myproject/controller').then(({get}) => {
const response = get(libPortal.request);
expect(response).toEqual({
body: `<!DOCTYPE html>
<html>
<head>
<meta charset=\"utf-8\">
<title>Jeffrey Wright</title>
<link rel=\"stylesheet\" type=\"text/css\" href=\"/admin/site/preview/intro/draft/persons/jeffrey-wright/_/asset/com.example.myproject:0123456789abcdef/styles.css\"/>
</head>
<body>
<h1>Jeffrey Wright</h1>
<img src=\"/admin/site/preview/intro/draft/persons/jeffrey-wright/_/image/00000000-0000-4000-8000-000000000010:0123456789abcdef/width-500/Jeffrey-Wright-hp.jpg\"/>
<h3>This is a sample preview</h3>
Use live integrations with your front-end, or just a mockup - like this :-)
</body>
</html>`
});
});
}); // Jeffrey Wright
}); // describe preview
Output
Running the test produces the glorious result:
npm run cov src/jest/server/controller.test.ts
> tutorial-jest@1.0.0 test
> jest --no-cache --coverage src/jest/server/controller.test.ts
PASS SERVER src/jest/server/controller.test.ts
controller
✓ is able to render a page for Léa Seydoux (26 ms)
✓ is able to render a page for Jeffrey Wright
------------------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------------|---------|----------|---------|---------|-------------------
All files | 100 | 54.54 | 100 | 100 |
jest/server | 100 | 100 | 100 | 100 |
mockXP.ts | 100 | 100 | 100 | 100 |
main/resources/lib/myproject | 100 | 16.66 | 100 | 100 |
controller.ts | 100 | 16.66 | 100 | 100 | 16-34
------------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.839 s
Ran all test suites matching /src\/jest\/server\/controller.test.ts/i.
Summary
This chapter demonstrated how to write tests using Jest in combination with Mock XP.
Next we demonstrate how to write a client-side test.