This is the early access documentation preview for Custom Views. This documentation might not be in sync with our official documentation.

Testing

In the previous sections we learned several aspects of developing your Custom View, like fetching data, building your User Interface, etc.
At this point the basic functionalities of your application are in place. It's time to write some tests to facilitate further development.

Write tests. Not too many. Mostly integration.

When it comes to testing applications, there are several approaches available. You've probably heard about the Testing Pyramid to describe the different levels of testing.

Integration tests

Writing integration tests is about putting yourself in the user's perspective and deriving test scenarios that focus on user interactions and workflows.

For instance, in the Channels page you can write a test to check that the page renders some data in a table and maybe paginate to page two.

Mocking data

One of the first things to consider when writing tests is about the test data. Of course mocking network requests is important but so is providing "realistic" test data.

In our Custom View we recommend to implement the following approaches:

Let's set things up. First, we need to configure Mock Service Worker to create a mock server.

channels.spec.jsJavaScript
import { setupServer } from 'msw/node';
const mockServer = setupServer();
afterEach(() => mockServer.resetHandlers());
beforeAll(() =>
mockServer.listen({
onUnhandledRequest: 'error',
})
);
afterAll(() => mockServer.close());

In our test we then mock all necessary API requests. If a network request is not properly mocked, Mock Service Worker will let you know.
Our Channels page sends a FetchChannels GraphQL query, therefore we need to use the graphql.query handler matching the name of the query: FetchChannels.

channels.spec.jsJavaScript
import { graphql } from 'msw';
it('should render Channels', async () => {
mockServer.use(
graphql.query('FetchChannels', (req, res, ctx) => {
return res(
ctx.data({
channels: {
// Mocked data
}
})
);
})
);
// Actual test...
});

The mocked data that we need to return should match the shape of the fields we are requesting in our GraphQL query.
In theory we can simply hardcode some random object but this does not scale well as your Custom View grows.

A better way to fulfill the data requirements is to use a Test Data model for a Channel.

channels.spec.jsJavaScript
import { graphql } from 'msw';
import { buildGraphqlList } from '@commercetools-test-data/core';
import * as ChannelMock from '@commercetools-test-data/channel';
it('should render Channels', async () => {
mockServer.use(
graphql.query('FetchChannels', (req, res, ctx) => {
const totalItems = 20;
const itemsPerPage = 5;
return res(
ctx.data({
channels: buildGraphqlList(
Array.from({ length: itemsPerPage }).map((_, index) =>
ChannelMock.random().key(`channel-key-${index}`)
),
{
name: 'Channel',
total: totalItems,
}
),
})
);
})
);
// Actual test...
});

Testing the Custom View

To test the actual Custom View, you should use the test-utils package, as it provides the necessary setup to render a Custom View within a test environment.

The main function you would want to use from that package is named renderCustomView.

We recommend writing a function, for example renderView, to encapsulate the basic test setup, so that your actual test remains as clean as possible.

channels.spec.jsJavaScript
import {
fireEvent,
screen,
renderCustomView,
} from '@commercetools-frontend/application-shell/test-utils';
import { entryPointUriPath } from '../../constants/application';
import ApplicationRoutes from '../../routes';
const renderView = (options = {}) => {
renderCustomView({
locale: options.locale || 'en',
projectKey: options.projectKey || 'my-project',
children: <ApplicationRoutes />,
});
};
it('should render Channels', async () => {
mockServer.use(/* See mock setup */);
renderView();
await screen.findByText('Channel no. 0');
});

With this simple test we implicitly have tested the following things:

  • The routes work.
  • The Channels page renders.
  • The data is fetched.
  • The data is displayed on the page.

From here you can enhance the test to do other things, especially testing the user interactions. For example:

  • You can simulate that the user paginates through the table, or use search and filters.

Let's enhance our test to paginate to page two. We need to adjust our GraphQL mock to return results based on the offset from the query variables to do this. Then, we need to interact with the pagination button to go to the next page.

channels.spec.jsJavaScript
it('should render Channels and paginate to second page', async () => {
mockServer.use(
graphql.query('FetchChannels', (req, res, ctx) => {
// Simulate a server side pagination.
const { offset } = req.variables;
const totalItems = 25; // 2 pages
const itemsPerPage = offset === 0 ? 20 : 5;
return res(
ctx.data({
channels: buildGraphqlList(
Array.from({ length: itemsPerPage }).map((_, index) =>
ChannelMock.random().key(
`channel-key-${offset === 0 ? index : 20 + index}`
)
),
{
name: 'Channel',
total: totalItems,
}
),
})
);
})
);
renderView();
// First page
await screen.findByText('Channel no. 0');
expect(screen.queryByText('Channel no. 22')).not.toBeInTheDocument();
// Go to second page
fireEvent.click(screen.getByLabelText('Next page'));
// Second page
await screen.findByText('Channel no. 22');
expect(screen.queryByText('Channel no. 0')).not.toBeInTheDocument();
});

This should give you a basic idea of how you can approach testing your Custom View.

Testing user permissions

User permissions are bound to a Project and can vary depending on the permissions assigned to the Team where the user is a member.

By default, the test-utils assigns the pre-defined permissions for the user to be able to view the Custom View. If you need to test with more permissions, explicitly provide them in your test setup. The following field can be used to assign the different granular permission values:

  • projectAllAppliedPermissions: a list of applied resource permissions the user should have for the given Project. A resource permission is an object with the following shape:

    • name: The name of the user permissions prefixed with can. For example, canView.
    • value: If true, the resource permission is applied to the user.

In our example application, we can apply this as follows:

channels.spec.jsJavaScript
import { renderCustomView } from '@commercetools-frontend/application-shell/test-utils';
import { entryPointUriPath } from '../../constants/application';
import ApplicationRoutes from '../../routes';
const renderView = (options = {}) => {
renderCustomView({
locale: options.locale || 'en',
projectKey: options.projectKey || 'my-project',
projectAllAppliedPermissions: [
{
name: 'canView',
value: true,
},
],
children: <ApplicationRoutes />,
});
};

End-to-End

To complement unit and integration tests, we recommend to also write End-to-End tests using Cypress.

Cypress is a feature-rich and developer friendly tool to write End-to-End tests and it's very easy to use for testing Custom Views.

Assuming you have already set up and installed Cypress, we recommend to install the following packages:

  • @testing-library/cypress: provides commands to select elements using React/Dom Testing Library.
  • @commercetools-frontend/cypress: provides commands specific to Custom Views.

Define a .env file in the cypress folder, containing some of the required environment variables:

cypress/.envTerminal
CYPRESS_LOGIN_USER=""
CYPRESS_LOGIN_PASSWORD=""
CYPRESS_PROJECT_KEY=""
CYPRESS_PACKAGE_NAME=""

The .env file should be git ignored. On CI, you can define environment variables within the CI job.

In the cypress/plugins/index.js file, you need to configure the task for Custom View and to load the environment variables:

cypress/plugins/index.jsJavaScript
const { customViewConfig } = require('@commercetools-frontend/cypress/task');
module.exports = (on, cypressConfig) => {
// Load the config
if (!process.env.CI) {
const path = require('path');
const envPath = path.join(__dirname, '../.env');
require('dotenv').config({ path: envPath });
}
on('task', {
customViewConfig,
});
return {
...cypressConfig,
env: {
...cypressConfig.env,
LOGIN_USER: process.env.CYPRESS_LOGIN_USER,
LOGIN_PASSWORD: process.env.CYPRESS_LOGIN_PASSWORD,
PROJECT_KEY: process.env.CYPRESS_PROJECT_KEY,
CYPRESS_PACKAGE_NAME: process.env.CYPRESS_PACKAGE_NAME,
},
};
};

In the cypress/support/commands.js you need to import the following commands:

cypress/support/commands.jsJavaScript
import '@testing-library/cypress/add-commands';
import '@commercetools-frontend/cypress/add-commands';

Finally, in the cypress.json you should set the baseUrl to http://localhost:3001.

At this point the setup is done. You can start writing your tests.

cypress/integration/channels.jsJavaScript
describe('Channels', () => {
beforeEach(() => {
cy.loginToMerchantCenterForCustomView();
});
it('should render page', () => {
cy.findByText('Open the Custom View').click();
cy.get('#custom-view-<CUSTOM_VIEW_ID>') // provide the correct ID for the Custom View iframe if you specify a `customViewId` in the configuration file
.getIframeBody()
.within(() => {
cy.findByText('Channels list').should('exist');
});
});
});