Skip to content

Commit

Permalink
chore: make component functional (Mergifyio#38)
Browse files Browse the repository at this point in the history
Fixes: MRGFY-356
  • Loading branch information
sileht authored Mar 25, 2021
1 parent bfdebdf commit d0dd35b
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 50 deletions.
80 changes: 80 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Reset the DOM between tests is not automatic...
* https://github.com/facebook/jest/issues/1224#issuecomment-716075260
*/
const sideEffects = {
document: {
addEventListener: {
fn: document.addEventListener,
refs: [],
},
keys: Object.keys(document),
},
window: {
addEventListener: {
fn: window.addEventListener,
refs: [],
},
keys: Object.keys(window),
},
};

// Lifecycle Hooks
// -----------------------------------------------------------------------------
beforeAll(async () => {
// Spy addEventListener
['document', 'window'].forEach((obj) => {
const { fn } = sideEffects[obj].addEventListener;
const { refs } = sideEffects[obj].addEventListener;

function addEventListenerSpy(type, listener, options) {
// Store listener reference so it can be removed during reset
refs.push({ type, listener, options });
// Call original window.addEventListener
fn(type, listener, options);
}

// Add to default key array to prevent removal during reset
sideEffects[obj].keys.push('addEventListener');

// Replace addEventListener with mock
global[obj].addEventListener = addEventListenerSpy;
});
});

// Reset JSDOM. This attempts to remove side effects from tests, however it does
// not reset all changes made to globals like the window and document
// objects. Tests requiring a full JSDOM reset should be stored in separate
// files, which is only way to do a complete JSDOM reset with Jest.
beforeEach(async () => {
const rootElm = document.documentElement;

// Remove attributes on root element
[...rootElm.attributes].forEach((attr) => rootElm.removeAttribute(attr.name));

// Remove elements (faster than setting innerHTML)
while (rootElm.firstChild) {
rootElm.removeChild(rootElm.firstChild);
}

// Remove global listeners and keys
['document', 'window'].forEach((obj) => {
const { refs } = sideEffects[obj].addEventListener;

// Listeners
while (refs.length) {
const { type, listener, options } = refs.pop();
global[obj].removeEventListener(type, listener, options);
}

// Keys
Object.keys(global[obj])
.filter((key) => !sideEffects[obj].keys.includes(key))
.forEach((key) => {
delete global[obj][key];
});
});

// Restore base elements
rootElm.innerHTML = '<head></head><body></body>';
});
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"type": "git",
"url": "https://github.com/Mergifyio/react-crisp"
},
"jest": {
"setupFilesAfterEnv": ["./jest.setup.js"]
},
"eslintConfig": {
"env": {
"browser": true
Expand Down
94 changes: 51 additions & 43 deletions src/Crisp.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';

function insertScript() {
const scriptUrl = 'https://client.crisp.chat/l.js';
const scripts = document.querySelector(`script[src='${scriptUrl}']`);
if (scripts === null) {
const script = document.createElement('script');

script.src = scriptUrl;
script.async = 1;

document.head.appendChild(script);
}
import React, { useRef, useEffect } from 'react';

function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

function pushCrisp(method, parameters) {
Expand All @@ -24,52 +19,65 @@ function pushCrisp(method, parameters) {
}
}

class Crisp extends React.Component {
constructor(props) {
super(props);
function Crisp(props) {
const {
crispWebsiteId,
crispTokenId,
crispRuntimeConfig,
safeMode,
configuration,
attributes,
} = props;

const previousCrispWebsiteId = usePrevious(crispWebsiteId);
if (previousCrispWebsiteId && previousCrispWebsiteId !== crispWebsiteId) {
throw Error("crispWebsiteId can't be changed");
}
const previousCrispTokenId = usePrevious(crispTokenId);
if (previousCrispTokenId && previousCrispTokenId !== crispTokenId) {
throw Error("crispTokenId can't be changed");
}

this.configCrisp = this.configCrisp.bind(this);
const previousCrispRuntimeConfig = usePrevious(crispRuntimeConfig);
if (previousCrispRuntimeConfig && previousCrispRuntimeConfig !== crispRuntimeConfig) {
throw Error("crispRuntimeConfig can't be changed");
}

componentDidMount() {
const {
crispWebsiteId, crispTokenId, crispRuntimeConfig, safeMode,
} = this.props;
const previousSafeMode = usePrevious(safeMode);
if (previousSafeMode && previousSafeMode !== safeMode) {
throw Error("safeMode can't be changed");
}

if (global.$crisp === undefined) {
// Must be call before any other $crisp method
// https://help.crisp.chat/en/article/how-to-use-dollarcrisp-javascript-sdk-10ud15y/#1-disable-warnings-amp-errors
global.$crisp = [['safe', safeMode]];
}

global.$crisp = [];
// Custom configuration
pushCrisp('set', attributes);
pushCrisp('config', configuration);

const scriptUrl = 'https://client.crisp.chat/l.js';
const scripts = document.querySelector(`script[src='${scriptUrl}']`);
if (scripts === null) {
// CRISP_WEBSITE_ID, CRISP_TOKEN_ID and CRISP_RUNTIME_CONFIG
// must be declared before inserting the script
// https://help.crisp.chat/en/article/how-to-restore-chat-sessions-with-a-token-c32v4t/
// https://help.crisp.chat/en/article/how-to-use-crisp-with-reactjs-fe0eyz/

global.CRISP_WEBSITE_ID = crispWebsiteId;

global.CRISP_TOKEN_ID = crispTokenId;

global.CRISP_RUNTIME_CONFIG = crispRuntimeConfig;

insertScript();

// Must be call before any other $crisp method
// https://help.crisp.chat/en/article/how-to-use-dollarcrisp-javascript-sdk-10ud15y/#1-disable-warnings-amp-errors
global.$crisp.push(['safe', safeMode]);
}

componentDidUpdate() {
this.configCrisp();
}

configCrisp() {
const { configuration, attributes } = this.props;

pushCrisp('set', attributes);
pushCrisp('config', configuration);
// We are good start Crisp
const script = document.createElement('script');
script.src = scriptUrl;
script.async = 1;
document.head.appendChild(script);
}

render() {
return <></>;
}
return <></>;
}

Crisp.propTypes = {
Expand Down
16 changes: 9 additions & 7 deletions tests/Crisp.test.jsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import {
render, waitFor,
} from '@testing-library/react';

import Crisp from '../src/Crisp';

test('Crisp load', async () => {
await render(
<Crisp crispWebsiteId="foo-website-id" />,
);
await render(<Crisp crispWebsiteId="foo-website-id-load" />);

await waitFor(() => expect(global.$crisp).toBeDefined());
await waitFor(() => expect(document.querySelector('.crisp-client')).toBeDefined());
await waitFor(() => expect(global.CRISP_WEBSITE_ID).toMatch(/foo-website-id/));
await waitFor(() => expect(global.CRISP_WEBSITE_ID).toMatch(/foo-website-id-load/));
});

test('Crisp with a token ID', async () => {
const tokenId = 'foo-token-id';
await render(
<Crisp
crispWebsiteId="foo-website-id"
crispWebsiteId="foo-website-id-token-id"
crispTokenId={tokenId}
/>,
);
Expand All @@ -30,10 +30,12 @@ test('Crisp with a Runtime Config', async () => {

await render(
<Crisp
crispWebsiteId="foo-website-id"
crispWebsiteId="foo-website-id-runtime-config"
crispRuntimeConfig={runtimeConfig}
/>,
);

await waitFor(() => expect(global.$crisp).toBeDefined());
await waitFor(() => expect(global.CRISP_RUNTIME_CONFIG.session_merge).toBeTruthy());
await waitFor(() => expect(document.querySelector('.crisp-client')).toBeDefined());
});

0 comments on commit d0dd35b

Please sign in to comment.