diff --git a/config/setupTests.js b/config/setupTests.js
index 4b4ca6987..4242bda40 100644
--- a/config/setupTests.js
+++ b/config/setupTests.js
@@ -6,6 +6,7 @@ import 'babel-polyfill';
import '@testing-library/jest-dom';
import { expect } from '@jest/globals';
import * as matchers from '@testing-library/jest-dom/dist/matchers';
+import MutationObserver from 'mutation-observer';
// ensure the expect is picked up from jest not cypress
global.expect = expect;
@@ -13,12 +14,8 @@ global.expect = expect;
global.expect.extend(matchers);
configure({ adapter: new Adapter() });
global.SVGPathElement = function () {};
-
-global.MutationObserver = class {
- constructor(callback) {}
- disconnect() {}
- observe(element, initObject) {}
-};
+// real MutationObserver polyfill for JSDOM
+global.MutationObserver = MutationObserver;
global.window.insights = {
...(window.insights || {}),
diff --git a/package-lock.json b/package-lock.json
index 08d08522d..0561eab1c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -99,6 +99,7 @@
"jest-canvas-mock": "^2.4.0",
"jest-environment-jsdom": "^29.6.2",
"lerna": "^5.6.2",
+ "mutation-observer": "^1.0.3",
"node-sass-package-importer": "^5.3.2",
"prettier": "^2.7.1",
"redux-mock-store": "^1.5.4",
@@ -30255,6 +30256,12 @@
"mustache": "bin/mustache"
}
},
+ "node_modules/mutation-observer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
+ "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==",
+ "dev": true
+ },
"node_modules/mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
@@ -41989,10 +41996,12 @@
"version": "1.0.0",
"license": "Apache-2.0",
"devDependencies": {
+ "@redhat-cloud-services/types": "^1.0.3",
"@types/react": "^18.0.0",
"glob": "10.3.3"
},
"peerDependencies": {
+ "@scalprum/react-core": "^0.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.0.0"
@@ -49296,6 +49305,7 @@
"@redhat-cloud-services/chrome": {
"version": "file:packages/chrome",
"requires": {
+ "@redhat-cloud-services/types": "^1.0.3",
"@types/react": "^18.0.0",
"glob": "10.3.3"
}
@@ -66018,6 +66028,12 @@
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="
},
+ "mutation-observer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/mutation-observer/-/mutation-observer-1.0.3.tgz",
+ "integrity": "sha512-M/O/4rF2h776hV7qGMZUH3utZLO/jK7p8rnNgGkjKUw8zCGjRQPxB8z6+5l8+VjRUQ3dNYu4vjqXYLr+U8ZVNA==",
+ "dev": true
+ },
"mute-stream": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
diff --git a/package.json b/package.json
index bc722f281..c8e8ce861 100644
--- a/package.json
+++ b/package.json
@@ -107,6 +107,7 @@
"jest-canvas-mock": "^2.4.0",
"jest-environment-jsdom": "^29.6.2",
"lerna": "^5.6.2",
+ "mutation-observer": "^1.0.3",
"node-sass-package-importer": "^5.3.2",
"prettier": "^2.7.1",
"redux-mock-store": "^1.5.4",
diff --git a/packages/chrome/package.json b/packages/chrome/package.json
index 7535a8a13..5d7e3af84 100644
--- a/packages/chrome/package.json
+++ b/packages/chrome/package.json
@@ -26,11 +26,13 @@
},
"homepage": "https://github.com/RedHatInsights/frontend-components/tree/master/packages/chrome#readme",
"peerDependencies": {
+ "@scalprum/react-core": "^0.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.0.0"
},
"devDependencies": {
+ "@redhat-cloud-services/types": "^1.0.3",
"@types/react": "^18.0.0",
"glob": "10.3.3"
},
diff --git a/packages/chrome/src/ChromeProvider/ChromeProvider.test.tsx b/packages/chrome/src/ChromeProvider/ChromeProvider.test.tsx
index d75fe431e..7e98aa5e7 100644
--- a/packages/chrome/src/ChromeProvider/ChromeProvider.test.tsx
+++ b/packages/chrome/src/ChromeProvider/ChromeProvider.test.tsx
@@ -1,7 +1,9 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { Link, MemoryRouter, Route, Routes } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
+import { ScalprumContext } from '@scalprum/react-core';
+import { PluginStore } from '@openshift/dynamic-plugin-sdk';
import ChromeProvider from './ChromeProvider';
import * as fetch from '../utils/fetch';
@@ -31,17 +33,41 @@ describe('ChromeProvider', () => {
expect(getSpy).toHaveBeenCalledWith('/api/chrome-service/v1/user');
});
- test('should post new data on pathname change', async () => {
+ test('should post new data on title change', async () => {
getSpy.mockResolvedValueOnce([]);
postSpy.mockResolvedValue(['/', '/bar']);
+ const DocumentMutator = () => {
+ useEffect(() => {
+ document.title = 'Foo title';
+ }, []);
+ return null;
+ };
+ // mock the initial document title
+ document.title = 'Initial title';
await act(async () => {
render(
-
-
- /foo/bar}>
-
-
-
+ ({
+ bundleTitle: 'bundle-title',
+ }),
+ },
+ },
+ }}
+ >
+
+
+ }>
+ /foo/bar}>
+
+
+
+
);
});
// change location
@@ -54,7 +80,11 @@ describe('ChromeProvider', () => {
await flushPromises();
});
expect(postSpy).toHaveBeenCalledTimes(2);
- expect(postSpy).toHaveBeenLastCalledWith('/api/chrome-service/v1/last-visited', { pathname: '/foo/bar', title: '', bundle: 'bundle-title' });
+ expect(postSpy).toHaveBeenLastCalledWith('/api/chrome-service/v1/last-visited', {
+ pathname: '/foo/bar',
+ title: 'Foo title',
+ bundle: 'bundle-title',
+ });
});
test('should not update state on mount if received error response', async () => {
@@ -66,7 +96,22 @@ describe('ChromeProvider', () => {
await act(async () => {
await render(
-
+ ({
+ bundleTitle: 'bundle-title',
+ }),
+ },
+ },
+ }}
+ >
+
+
);
});
diff --git a/packages/chrome/src/ChromeProvider/ChromeProvider.tsx b/packages/chrome/src/ChromeProvider/ChromeProvider.tsx
index b576a6236..7cef26273 100644
--- a/packages/chrome/src/ChromeProvider/ChromeProvider.tsx
+++ b/packages/chrome/src/ChromeProvider/ChromeProvider.tsx
@@ -1,4 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
+import { useScalprum } from '@scalprum/react-core';
+import { ChromeAPI } from '@redhat-cloud-services/types';
import { useLocation } from 'react-router-dom';
import { ChromeContext } from '../ChromeContext';
@@ -7,20 +9,61 @@ import { IDENTITY_URL, LAST_VISITED_URL, get, post } from '../utils/fetch';
const getUserIdentity = () => get(IDENTITY_URL);
-const useLastPageVisitedUploader = (providerState: ReturnType, bundle = '') => {
+const useLastPageVisitedUploader = (providerState: ReturnType) => {
+ const scalprum = useScalprum<{ initialized: boolean; api: { chrome: ChromeAPI } }>();
const { pathname } = useLocation();
+ const postData = async (pathname: string, title: string, bundle: string) => {
+ try {
+ const data = await post(LAST_VISITED_URL, {
+ pathname,
+ title,
+ bundle,
+ });
+ providerState.setLastVisited(data);
+ } catch (error) {
+ console.error('Unable to update last visited pages!', error);
+ }
+ };
useEffect(() => {
- post(LAST_VISITED_URL, {
- pathname,
- title: document.title,
- bundle,
- })
- .then((data) => providerState.setLastVisited(data))
- .catch((error) => console.error('Unable to update last visited pages!', error));
+ let titleObserver: MutationObserver | undefined;
+ let prevTitle: string | null;
+ const titleTarget = document.querySelector('title');
+ if (titleTarget) {
+ prevTitle = titleTarget.textContent;
+ // initial api call on mount
+ postData(pathname, prevTitle ?? '', scalprum.api.chrome.getBundleData().bundleTitle);
+ /**
+ * Use Mutation observer to trigger the updates.
+ * Using the observer will ensure the last visited pages gets updated on document title change rather than just location change.
+ * The chrome service uses pathname as identifier and updates title according.
+ * Multiple calls with the same pathname and different title will ensure that the latest correct title is assigned to a pathname. *
+ * */
+ titleObserver = new MutationObserver((mutations) => {
+ // grab text from the title element
+ const currentTitle = mutations[0]?.target.textContent;
+ // trigger only if the titles are different
+ if (typeof currentTitle === 'string' && currentTitle !== prevTitle) {
+ try {
+ prevTitle = currentTitle;
+ postData(pathname, currentTitle, scalprum.api.chrome.getBundleData().bundleTitle);
+ } catch (error) {
+ // catch sync errors
+ console.error('Unable to update last visited pages!', error);
+ }
+ }
+ });
+ titleObserver.observe(titleTarget, {
+ // observe only the children
+ childList: true,
+ });
+ }
+ return () => {
+ titleObserver?.disconnect();
+ };
}, [pathname]);
};
-const ChromeProvider: React.FC> = ({ children, bundle }) => {
+const ChromeProvider: React.FC = ({ children }) => {
const isMounted = useRef(false);
const [initialRequest, setInitialRequest] = useState(false);
const providerState = useRef>();
@@ -28,7 +71,7 @@ const ChromeProvider: React.FC> = (
providerState.current = chromeState();
}
- useLastPageVisitedUploader(providerState.current, bundle);
+ useLastPageVisitedUploader(providerState.current);
useEffect(() => {
isMounted.current = true;