Testing Forge Custom UI components Using Jest

I am developing a new Forge application and I’ve set about writing JEST unit tests. However, I cannot seem to get unit tests in the Custom UI to work. I get the below error message

  Cannot find module '@forge/bridge' from 'src/components/App.tsx'

The lone Custom UI example app I saw that included Jest only did simple comparison testing and did not mock any @forge services and thus avoided the issue.

So I am wondering what is the proper way to unit test Custom UI components with Jest in Jira Forge Custom UI?

Thanks!

4 Likes

@KankeIshaku did you run the “npm install” before you ran “jest”?

Yes and it seems I am not the only one having this issue. This was raised back in April and no one replied.

Can you please kindly assist?

@KankeIshaku I ran “npm install” and then ran “npm test”. Here’s my output:

➜  forge-daci-helper git:(master) npm i
added 725 packages from 438 contributors and audited 726 packages in 10.953s

20 packages are looking for funding
  run `npm fund` for details

found 48544 vulnerabilities (25850 low, 41 moderate, 22653 high)
  run `npm audit fix` to fix them, or `npm audit` for details

➜  forge-daci-helper git:(master) npm test

> forge-ui-starter-typescript-experimental@0.0.0 test /Users/dsorin/atlassian/forge-daci-helper
> jest

 PASS  src/test/AdfUtil.test.ts
 PASS  src/test/DaciChecker.test.ts

Test Suites: 2 passed, 2 total
Tests:       11 passed, 11 total
Snapshots:   0 total
Time:        1.446s, estimated 3s
Ran all test suites.

This repo doesn’t have a @forge/bridge dependency in its package.json. Did you add it?

Your example above which is from this project doesn’t use Forge Custom UI.

I created the project using the Forge Project Page Jira Custom UI template which adds @forge/bridge into the package.json in the /static/ directory by default, I didn’t add this manually. Removing it means I can’t use the invoke method.

Here is an example of an app custom UI app created by yourselves clearly showing @forge/bridge as a dependency in the package.json of the /static/src/ directory.

If the Custom UI Bridge dependency is missing, how is our app able to securely securely integrate with Atlassian products? Here is your documentation which clearly states this.

Below is the simple Jest test and output from it;

describe('HomePage', () => {
    test('basic', async () => {
        render(<App />)
    });
});

Surely you should know this?

Hi Kankelshaku,
Whilst i’ve not gotten as far as CustomUI Jest testing procedure, but am interested in doing so and also face problems with both invoke from forge/bridge and api from forge/api being in the /static directory (hence here).
Did you see the recent post regarding Resolvers for CustomUI? 27 Jul 2021 - Direct product requests, CI upgrade support, and composable routes

(You may not need to use forge bridge if you want to access Confluence/Jira endpoints, and if the getText method is just returning a String you could try the following instead of the invoke:)

//Replace the invoke('getText'..) with the following:
let myInvoke = new ExtractedInvoke();
        return await myInvoke.getText();

Then a new file, named ‘ExtractedInvoke’ would be:

export class ExtractedInvoke {
    async getText() {
               //Whatever your 'getText' was doing here..
     }
}

export default ExtractedInvoke;

This workaround means you don’t need the forge/bridge import.

1 Like

Hi @DennyMiller,

I have the same problem. but I don’t need an api call. I’m using the storageAPI and I think this need a resolver. I not really understand your solution.

I get always this error if I want test my code

Cannot find module '@forge/bridge' from 'src/stores/AppStore.ts'

Here is my resolver

import Resolver from '@forge/resolver'
import { storage } from '@forge/api'

const getRoadmapDescription = async () => {
  const result = (await storage.get('roadmap-title')) || { data: 'no' }
  console.log('result', result)
  return result
}

Is there a way to test the forge custom UI apps? I’m using the storageAPI to set configs for my app.

Thank you

1 Like

Hi @bongartz
I’ve experienced that error before, which is why I have separated my storage API code into its own file. See below for how i’ve done it:

fileWhereStorageIs.js
(If we don’t find the key, we initialize it. You could ignore the ‘if’ check and just return storage.get() value)

import {storage} from "@forge/api";
export const getFromStorageAPI = async () => {
    let foundData = await storage.get(STORAGEKEY);
    if (foundData === undefined) {
        await storage.set(STORAGEKEY, DEFAULTVALUE);
        foundData = await storage.get(STORAGEKEY);
    }
    return foundData;
};

Then in the custom Resolver, the storage API function is imported:

CustomSettingsResolver.js:

import { getFromStorageAPI } from "./fileWhereStorageIs";

export class CustomSettingsResolver {
   constructor(resolver) {
         resolver.define('getSettings', () => {
            return getFromStorageAPI();
        });
   }
}

So you can use the ‘getSettings’ Resolver definition to retrieve the data from Storage API.

To further clarify, the custom resolver CustomSettingsResolver.js is declared in the PARENT resolver index.js (see below):

import Resolver from '@forge/resolver';
import React from 'react';

//declare your custom resolver files here 
import {CustomSettingsResolver} from "./CustomSettingsResolver";
import {BasicAuthResolver} from "./BasicAuthResolver"; //I Left in a second resolver to show you can have multiple custom resolvers 

//Pass Parent resolver into Constructor of your custom resolvers
const resolver = new Resolver();
new CustomSettingsResolver(resolver);
new BasicAuthResolver(resolver);

//You can still use parent resolver definitions
resolver.define('getText', async () => {
  return 'parentResolverTest';
});

export const resolverHandler = resolver.getDefinitions();

Hope that helps for StorageAPI.

Regarding Testing, there’s multiple methods of testing. Jest works well enough for simple testing, but if you want e2e testing for synthetic tests, there’s a few available that work with Forge apps (Codecept/Playwright/Cypress (i think cypress works with Forge, but it does have trouble with IFrames)

Thank you @DennyMiller,

but this changing doesn’t change something by my jest error :frowning:

Cannot find module '@forge/bridge' from 'src/components/Nav.tsx'

For me is clear because all your code is only for the src folder, but my problem is inside the static folder.

Do you have an example how to mock the forge/bridge module?

Thank you

Hey @bongartz, I’ve just been able successfully mock @forge/bridge in my Custom UI app (for my use case, to use view.getContext).

I put the following file at static/<APP_NAME>/src/__mocks__/@forge/bridge/index.ts:

import { Context } from "../path/to/definition/for/context";

let context: Context | null = null;

export function __setContext(newContext: Context) {
  context = newContext;
}

export const view = {
  getContext: () => context,
};

and then I can use the mock in a test like so:

import { getIssueKeyFromContext } from "./getIssueKeyFromContext";

test("gets issue key", async () => {
  // Arrange
  require("@forge/bridge").__setContext({
    extension: { type: "jira:issueAction", issue: { key: "KEY-123" } },
  });

  // Act
  const key = await getIssueKeyFromContext();

  // Assert
  expect(key).toBe("KEY-123");
});

Hope this is helpful.

1 Like

Thanks @joshp!

In a similar manner I made a simple mock for the invoke function with a couple helper functions to define and reset the registered functions.

For example:
__mocks__/@forge/bridge.js (this could be in /bridge/index.js as you showed).

/**
 * mocks for the @forge/bridge module
 */

var callbacks = {}

export async function invoke(fname, payload) {
  return callbacks[fname](payload)
}

export function __define(fname, cb) {
  callbacks[fname] = cb
}

export function __reset() {
  callbacks = {}
}

and to test in bridge.test.js

/**
 * These tests verify that the resolver mock works as expected
 */

import { invoke, __define, __reset } from '@forge/bridge'
import { beforeEach, expect } from '@jest/globals'
jest.mock('@forge/bridge')

describe('bridge api', () => {
  beforeEach(() => {
    __reset()
  })

  test('invokes a defined function', async () => {
    const testerCb = jest.fn().mockResolvedValue('helloWorld')
    __define('tester', testerCb)
    const payload = { name: 'joe', year: 1942 }
    const result = await invoke('tester', payload)
    expect(testerCb).toHaveBeenCalledWith(payload)
    expect(result).toEqual('helloWorld')
  })

  test('handles multiple functions', async () => {
    const f1 = jest.fn().mockResolvedValue('f1Value')
    __define('f1', f1)

    const f2 = jest.fn().mockResolvedValue('f2Value')
    __define('f2', f2)

    const payload = { name: 'joe', year: 1942 }
    const result1 = await invoke('f1', payload)
    expect(f1).toHaveBeenCalledWith(payload)
    expect(result1).toEqual('f1Value')

    const result2 = await invoke('f2', payload)
    expect(f2).toHaveBeenCalledWith(payload)
    expect(result2).toEqual('f2Value')
  })
})
2 Likes

Ah didn’t realise that you can put the mock straight in __mocks__/@forge/bridge.js without the index.ts - thanks @jeffryan!