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:

src/main/resources/lib/myproject/controller.ts
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:

src/jest/server/mockXP.ts
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 the starter-ts 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.
src/jest/server/controller.test.ts
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.

Mock XP was used to mock the Enonic content and portal APIs.

Next we demonstrate how to write a client-side test.


Contents

Contents