Add tests to formbricks-js (#399)

* init: jest for formbricks/js

* test: formbricks init

* test: formbricks set attributes

* test: formbricks updated attributes

* test: formbricks track, refresh, route change

* test: formbricks logout

* chore: use strict checking & replace let w const

* chore: destructure variables

* feat: test coverage visibility

* updated: pnpm lock file

* feat: tests now use a mock API

* fix: mock actual formbricks survey response and not empty placeholders

* rename: unit test for clarity

* chore: destructure setting attributes into individual tests

* feat: mock console logger for cleaner cli during tests

* add top level test script for turbo

* update babel config to fix errors in formbricks-js build

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Shubham Palriwala
2023-06-30 19:59:38 +05:30
committed by GitHub
parent 888d10434a
commit d67858e2ea
15 changed files with 1972 additions and 1065 deletions

View File

@@ -23,7 +23,8 @@
"generate": "turbo run generate",
"lint": "turbo run lint",
"release": "turbo run build --filter=react^... && changeset publish",
"nuke": "rm -r node_modules; for d in **/node_modules; do echo $d; rm -r $d; done"
"nuke": "rm -r node_modules; for d in **/node_modules; do echo $d; rm -r $d; done",
"test": "turbo run test"
},
"devDependencies": {
"@changesets/cli": "^2.26.1",

View File

@@ -0,0 +1,6 @@
module.exports = (api) => {
return {
presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"],
plugins: [["@babel/plugin-transform-react-jsx", { runtime: api.env("test") ? "automatic" : "classic" }]],
};
};

View File

@@ -42,36 +42,46 @@
]
},
"author": "Formbricks <hola@formbricks.com>",
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/preset-typescript": "^7.22.5",
"@formbricks/api": "workspace:*",
"@formbricks/types": "workspace:*",
"@types/enzyme": "^3.10.13",
"@types/jest": "^29.5.2",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"autoprefixer": "^10.4.14",
"babel-jest": "^29.5.0",
"cross-env": "^7.0.3",
"enzyme": "^3.11.0",
"enzyme-adapter-preact-pure": "^4.1.0",
"eslint": "^8.42.0",
"eslint-config-formbricks": "workspace:*",
"eslint-config-preact": "^1.3.0",
"isomorphic-fetch": "^3.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"jest-fetch-mock": "^3.0.3",
"jest-preset-preact": "^4.0.5",
"microbundle": "^0.15.1",
"postcss": "^8.4.24",
"preact": "10.15.1",
"preact-cli": "^3.4.5",
"preact-render-to-string": "^6.1.0",
"regenerator-runtime": "^0.13.11",
"rimraf": "^5.0.1",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.3"
},
"jest": {
"preset": "jest-preset-preact",
"transformIgnorePatterns": [
"!node_modules/"
],
"setupFiles": [
"<rootDir>/tests/__mocks__/setupTests.js"
]
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/tests/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/tests/__mocks__/styleMock.js"
}
}
}

View File

@@ -12,6 +12,10 @@ const errorHandler = ErrorHandler.getInstance();
export const checkPageUrl = async (): Promise<Result<void, InvalidMatchTypeError | NetworkError>> => {
logger.debug(`Checking page url: ${window.location.href}`);
const { settings } = config.get();
if (settings?.noCodeEvents === undefined) {
return okVoid();
}
const pageUrlEvents: Event[] = settings?.noCodeEvents.filter((e) => e.noCodeConfig?.type === "pageUrl");
if (pageUrlEvents.length === 0) {
@@ -105,7 +109,7 @@ export const checkClickMatch = (event: MouseEvent) => {
trackEvent(e.name).then((res) => {
match(
res,
(_value) => {},
(_value) => { },
(err) => {
errorHandler.handle(err);
}
@@ -120,7 +124,7 @@ export const checkClickMatch = (event: MouseEvent) => {
trackEvent(e.name).then((res) => {
match(
res,
(_value) => {},
(_value) => { },
(err) => {
errorHandler.handle(err);
}

View File

@@ -0,0 +1,254 @@
import fetchMock from "jest-fetch-mock";
import { constants } from "../constants"
const { environmentId, apiHost, sessionId, expiryTime,
surveyId, questionOneId, questionTwoId,
choiceOneId, choiceTwoId, choiceThreeId,
initialPersonUid, initialUserId, initialUserEmail, newPersonUid,
eventIdForRouteChange, updatedUserEmail, customAttributeKey,
customAttributeValue, eventIdForEventTracking, userIdAttributeId,
userInitialEmailAttributeId, userCustomAttrAttributeId,
userUpdatedEmailAttributeId } = constants
export const mockInitResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
apiHost,
environmentId,
person: {
id: initialPersonUid,
environmentId,
attributes: []
},
session: {
"id": sessionId,
expiresAt: expiryTime
},
settings: {
surveys: [
{
id: surveyId,
questions: [
{
id: questionOneId,
type: "multipleChoiceSingle",
choices: [
{
id: choiceOneId,
label: "Not at all disappointed"
},
{
id: choiceTwoId,
label: "Somewhat disappointed"
},
{
id: choiceThreeId,
label: "Very disappointed"
}
],
headline: "How disappointed would you be if you could no longer use Test-Formbricks?",
required: true,
subheader: "Please select one of the following options:"
},
{
id: questionTwoId,
type: "openText",
headline: "How can we improve Test-Formbricks for you?",
required: true,
subheader: "Please be as specific as possible."
}
],
triggers: [],
thankYouCard: {
enabled: true,
headline: "Thank you!",
subheader: "We appreciate your feedback."
},
autoClose: null,
delay: 0
}],
noCodeEvents: [],
brandColor: "#20b398",
formbricksSignature: true,
placement: "bottomRight",
darkOverlay: false,
clickOutsideClose: true
}
}));
}
export const mockSetUserIdResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
apiHost,
environmentId,
settings: {
surveys: [],
noCodeEvents: [],
},
person: {
id: initialPersonUid,
environmentId,
attributes: [
{
id: userIdAttributeId,
value: initialUserId,
attributeClass: {
id: environmentId,
name: "userId"
}
}
]
}
}));
}
export const mockSetEmailIdResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
apiHost,
environmentId,
settings: {
surveys: [],
noCodeEvents: [],
},
person: {
id: initialPersonUid,
environmentId,
attributes: [
{
id: userIdAttributeId,
value: initialUserId,
attributeClass: {
id: environmentId,
name: "userId"
}
},
{
id: userInitialEmailAttributeId,
value: initialUserEmail,
attributeClass: {
id: environmentId,
name: "email"
}
}
]
},
}));
}
export const mockSetCustomAttributeResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
apiHost,
environmentId,
settings: {
surveys: [],
noCodeEvents: [],
},
person: {
id: initialPersonUid,
environmentId,
attributes: [
{
id: userIdAttributeId,
value: initialUserId,
attributeClass: {
id: environmentId,
name: "userId"
}
},
{
id: userInitialEmailAttributeId,
value: initialUserEmail,
attributeClass: {
id: environmentId,
name: "email"
}
},
{
id: userCustomAttrAttributeId,
value: customAttributeValue,
attributeClass: {
id: environmentId,
name: customAttributeKey
}
}
]
},
}));
}
export const mockUpdateEmailResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
apiHost,
environmentId,
settings: {
surveys: [],
noCodeEvents: [],
},
person: {
id: initialPersonUid,
environmentId,
attributes: [
{
id: userIdAttributeId,
value: initialUserId,
attributeClass: {
id: environmentId,
name: "userId"
}
},
{
id: userUpdatedEmailAttributeId,
value: updatedUserEmail,
attributeClass: {
id: environmentId,
name: "email"
}
},
{
id: userCustomAttrAttributeId,
value: customAttributeValue,
attributeClass: {
id: environmentId,
name: customAttributeKey
}
}
]
},
}));
}
export const mockEventTrackResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
id: eventIdForEventTracking,
}));
console.log('Formbricks: Event "Button Clicked" tracked')
}
export const mockRefreshResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({}));
console.log('Settings refreshed')
}
export const mockRegisterRouteChangeResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
id: eventIdForRouteChange,
}));
console.log('Checking page url: http://localhost/');
}
export const mockLogoutResponse = () => {
fetchMock.mockResponseOnce(JSON.stringify({
settings: {
surveys: [],
noCodeEvents: [],
},
person: {
id: newPersonUid,
environmentId,
attributes: []
},
session: {},
}));
console.log('Resetting person. Getting new person, session and settings from backend');
}

View File

@@ -0,0 +1 @@
module.exports = 'placeholer-to-not-mock-all-files-as-js';

View File

@@ -1,6 +1,10 @@
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-preact-pure";
/** @type {import('jest').Config} */
const config = {
verbose: true,
testEnvironment: "jsdom"
};
configure({
adapter: new Adapter()
});
import fetchMock from "jest-fetch-mock";
fetchMock.enableMocks();
module.exports = config;

View File

@@ -0,0 +1 @@
module.exports = {};

View File

@@ -0,0 +1,59 @@
const generateUserId = () => {
const min = 1000;
const max = 9999;
const randomNum = Math.floor(Math.random() * (max - min + 1) + min);
return randomNum.toString();
}
const generateEmailId = () => {
const domain = "formbricks.test";
const randomString = Math.random().toString(36).substring(2);
const emailId = `${randomString}@${domain}`;
return emailId;
};
const generateRandomString = () => {
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
const maxLength = 8;
let randomString = "";
for (let i = 0; i < maxLength; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
randomString += characters.charAt(randomIndex);
}
return randomString;
};
const getOneDayExpiryTime = () => {
var ms = new Date().getTime();
var oneDayMs = 24 * 60 * 60 * 1000; // Number of milliseconds in one day
var expiryOfOneDay = ms + oneDayMs
return expiryOfOneDay;
}
export const constants = {
environmentId: "mockedEnvironmentId",
apiHost: "mockedApiHost",
sessionId: generateRandomString(),
expiryTime: getOneDayExpiryTime(),
surveyId: generateRandomString(),
questionOneId: generateRandomString(),
questionTwoId: generateRandomString(),
choiceOneId: generateRandomString(),
choiceTwoId: generateRandomString(),
choiceThreeId: generateRandomString(),
choiceFourId: generateRandomString(),
initialPersonUid: generateRandomString(),
newPersonUid: generateRandomString(),
initialUserId: generateUserId(),
initialUserEmail: generateEmailId(),
updatedUserEmail: generateEmailId(),
customAttributeKey: generateRandomString(),
customAttributeValue: generateRandomString(),
userIdAttributeId: generateRandomString(),
userInitialEmailAttributeId: generateRandomString(),
userCustomAttrAttributeId: generateRandomString(),
userUpdatedEmailAttributeId: generateRandomString(),
eventIdForEventTracking: generateRandomString(),
eventIdForRouteChange: generateRandomString()
} as const;

View File

@@ -1,3 +0,0 @@
// Enable enzyme adapter's integration with TypeScript
// See: https://github.com/preactjs/enzyme-adapter-preact-pure#usage-with-typescript
/// <reference types="enzyme-adapter-preact-pure" />

View File

@@ -0,0 +1,180 @@
/**
* @jest-environment jsdom
*/
import formbricks from "../src/index";
import { constants } from "./constants"
import { Attribute } from "./types";
import { mockEventTrackResponse, mockInitResponse, mockLogoutResponse, mockRefreshResponse, mockRegisterRouteChangeResponse, mockSetCustomAttributeResponse, mockSetEmailIdResponse, mockSetUserIdResponse, mockUpdateEmailResponse } from "./__mocks__/apiMock";
const consoleLogMock = jest.spyOn(console, 'log').mockImplementation();
test("Test Jest", () => {
expect(1 + 9).toBe(10);
});
const { environmentId, apiHost, initialUserId, initialUserEmail, updatedUserEmail, customAttributeKey, customAttributeValue } = constants
beforeEach(() => {
fetchMock.resetMocks();
});
test("Formbricks should Initialise", async () => {
mockInitResponse()
await formbricks.init({
environmentId,
apiHost,
});
const configFromBrowser = localStorage.getItem("formbricksConfig");
expect(configFromBrowser).toBeTruthy();
if (configFromBrowser) {
const jsonSavedConfig = JSON.parse(configFromBrowser);
expect(jsonSavedConfig.environmentId).toStrictEqual(environmentId);
expect(jsonSavedConfig.apiHost).toStrictEqual(apiHost);
}
});
test("Formbricks should get the current person with no attributes", () => {
const currentState = formbricks.getPerson()
const currentStateAttributes: Array<Attribute> = currentState.attributes as Array<Attribute>;
expect(currentStateAttributes).toHaveLength(0)
})
test("Formbricks should set userId", async () => {
mockSetUserIdResponse()
await formbricks.setUserId(initialUserId)
const currentState = formbricks.getPerson()
expect(currentState.environmentId).toStrictEqual(environmentId)
const currentStateAttributes: Array<Attribute> = currentState.attributes as Array<Attribute>;
const numberOfUserAttributes = currentStateAttributes.length
expect(numberOfUserAttributes).toStrictEqual(1)
currentStateAttributes.forEach((attribute) => {
switch (attribute.attributeClass.name) {
case "userId":
expect(attribute.value).toStrictEqual(initialUserId)
break;
default:
expect(0).toStrictEqual(1)
}
})
})
test("Formbricks should set email", async () => {
mockSetEmailIdResponse()
await formbricks.setEmail(initialUserEmail)
const currentState = formbricks.getPerson()
expect(currentState.environmentId).toStrictEqual(environmentId)
const currentStateAttributes: Array<Attribute> = currentState.attributes as Array<Attribute>;
const numberOfUserAttributes = currentStateAttributes.length
expect(numberOfUserAttributes).toStrictEqual(2)
currentStateAttributes.forEach((attribute) => {
switch (attribute.attributeClass.name) {
case "userId":
expect(attribute.value).toStrictEqual(initialUserId)
break;
case "email":
expect(attribute.value).toStrictEqual(initialUserEmail)
break;
default:
expect(0).toStrictEqual(1)
}
})
})
test("Formbricks should set custom attribute", async () => {
mockSetCustomAttributeResponse()
await formbricks.setAttribute(customAttributeKey, customAttributeValue)
const currentState = formbricks.getPerson()
expect(currentState.environmentId).toStrictEqual(environmentId)
const currentStateAttributes: Array<Attribute> = currentState.attributes as Array<Attribute>;
const numberOfUserAttributes = currentStateAttributes.length
expect(numberOfUserAttributes).toStrictEqual(3)
currentStateAttributes.forEach((attribute) => {
switch (attribute.attributeClass.name) {
case "userId":
expect(attribute.value).toStrictEqual(initialUserId)
break;
case "email":
expect(attribute.value).toStrictEqual(initialUserEmail)
break;
case customAttributeKey:
expect(attribute.value).toStrictEqual(customAttributeValue)
break;
default:
expect(0).toStrictEqual(1)
}
})
})
test("Formbricks should update attribute", async () => {
mockUpdateEmailResponse()
await formbricks.setEmail(updatedUserEmail)
const currentState = formbricks.getPerson()
expect(currentState.environmentId).toStrictEqual(environmentId)
const currentStateAttributes: Array<Attribute> = currentState.attributes as Array<Attribute>;
const numberOfUserAttributes = currentStateAttributes.length
expect(numberOfUserAttributes).toStrictEqual(3)
currentStateAttributes.forEach((attribute) => {
switch (attribute.attributeClass.name) {
case "email":
expect(attribute.value).toStrictEqual(updatedUserEmail)
break;
case "userId":
expect(attribute.value).toStrictEqual(initialUserId)
break;
case customAttributeKey:
expect(attribute.value).toStrictEqual(customAttributeValue)
break;
default:
expect(0).toStrictEqual(1)
}
})
})
test("Formbricks should track event", async () => {
mockEventTrackResponse()
const mockButton = document.createElement("button");
mockButton.addEventListener("click", async () => {
await formbricks.track("Button Clicked");
});
await mockButton.click();
expect(consoleLogMock).toHaveBeenCalledWith(expect.stringMatching(/Formbricks: Event "Button Clicked" tracked/));
});
test("Formbricks should refresh", async () => {
mockRefreshResponse()
await formbricks.refresh()
expect(consoleLogMock).toHaveBeenCalledWith(expect.stringMatching(/Settings refreshed/));
})
test("Formbricks should register for route change", async () => {
mockRegisterRouteChangeResponse()
await formbricks.registerRouteChange()
expect(consoleLogMock).toHaveBeenCalledWith(expect.stringMatching(/Checking page url/));
})
test("Formbricks should logout", async () => {
mockLogoutResponse()
await formbricks.logout()
const currentState = formbricks.getPerson()
const currentStateAttributes: Array<Attribute> = currentState.attributes as Array<Attribute>;
expect(currentState.environmentId).toStrictEqual(environmentId)
expect(currentStateAttributes.length).toBe(0)
})

View File

@@ -1,17 +0,0 @@
import { h } from "preact";
import { shallow } from "enzyme";
import Hello from "../src/component";
describe("Hello logic", () => {
it("should be able to run tests", () => {
expect(1 + 2).toEqual(3);
});
});
describe("Hello Snapshot", () => {
it("should render header with content", () => {
const tree = shallow(<Hello />);
expect(tree.find("h1").text()).toBe("Hello, World!");
});
});

View File

@@ -0,0 +1,8 @@
export interface Attribute {
id: string;
value: string;
attributeClass: {
id: string;
name: string;
};
}

2458
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,9 @@
"start": {
"outputs": []
},
"test": {
"outputs": []
},
"generate": {
"dependsOn": ["^generate"]
},