Skip to content

Commit 0393878

Browse files
authored
Merge pull request #1921 from Hyperkid123/fix-last-visited-titles
Fix visited pages title values.
2 parents 3502726 + 2df4221 commit 0393878

File tree

6 files changed

+130
-26
lines changed

6 files changed

+130
-26
lines changed

config/setupTests.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,16 @@ import 'babel-polyfill';
66
import '@testing-library/jest-dom';
77
import { expect } from '@jest/globals';
88
import * as matchers from '@testing-library/jest-dom/dist/matchers';
9+
import MutationObserver from 'mutation-observer';
910

1011
// ensure the expect is picked up from jest not cypress
1112
global.expect = expect;
1213
// extends with RTL
1314
global.expect.extend(matchers);
1415
configure({ adapter: new Adapter() });
1516
global.SVGPathElement = function () {};
16-
17-
global.MutationObserver = class {
18-
constructor(callback) {}
19-
disconnect() {}
20-
observe(element, initObject) {}
21-
};
17+
// real MutationObserver polyfill for JSDOM
18+
global.MutationObserver = MutationObserver;
2219

2320
global.window.insights = {
2421
...(window.insights || {}),

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"jest-canvas-mock": "^2.4.0",
108108
"jest-environment-jsdom": "^29.6.2",
109109
"lerna": "^5.6.2",
110+
"mutation-observer": "^1.0.3",
110111
"node-sass-package-importer": "^5.3.2",
111112
"prettier": "^2.7.1",
112113
"redux-mock-store": "^1.5.4",

packages/chrome/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@
2626
},
2727
"homepage": "https://github.com/RedHatInsights/frontend-components/tree/master/packages/chrome#readme",
2828
"peerDependencies": {
29+
"@scalprum/react-core": "^0.5.1",
2930
"react": "^18.2.0",
3031
"react-dom": "^18.2.0",
3132
"react-router-dom": "^6.0.0"
3233
},
3334
"devDependencies": {
35+
"@redhat-cloud-services/types": "^1.0.3",
3436
"@types/react": "^18.0.0",
3537
"glob": "10.3.3"
3638
},

packages/chrome/src/ChromeProvider/ChromeProvider.test.tsx

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import React from 'react';
1+
import React, { useEffect } from 'react';
22
import { Link, MemoryRouter, Route, Routes } from 'react-router-dom';
33
import { render, screen } from '@testing-library/react';
44
import { act } from 'react-dom/test-utils';
5+
import { ScalprumContext } from '@scalprum/react-core';
6+
import { PluginStore } from '@openshift/dynamic-plugin-sdk';
57

68
import ChromeProvider from './ChromeProvider';
79
import * as fetch from '../utils/fetch';
@@ -31,17 +33,41 @@ describe('ChromeProvider', () => {
3133
expect(getSpy).toHaveBeenCalledWith('/api/chrome-service/v1/user');
3234
});
3335

34-
test('should post new data on pathname change', async () => {
36+
test('should post new data on title change', async () => {
3537
getSpy.mockResolvedValueOnce([]);
3638
postSpy.mockResolvedValue(['/', '/bar']);
39+
const DocumentMutator = () => {
40+
useEffect(() => {
41+
document.title = 'Foo title';
42+
}, []);
43+
return null;
44+
};
45+
// mock the initial document title
46+
document.title = 'Initial title';
3747
await act(async () => {
3848
render(
39-
<MemoryRouter initialEntries={['/']} initialIndex={0}>
40-
<Routes>
41-
<Route path="*" element={<Link to="/foo/bar">/foo/bar</Link>}></Route>
42-
</Routes>
43-
<ChromeProvider bundle="bundle-title" />
44-
</MemoryRouter>
49+
<ScalprumContext.Provider
50+
value={{
51+
config: {},
52+
initialized: true,
53+
pluginStore: new PluginStore(),
54+
api: {
55+
chrome: {
56+
getBundleData: () => ({
57+
bundleTitle: 'bundle-title',
58+
}),
59+
},
60+
},
61+
}}
62+
>
63+
<MemoryRouter initialEntries={['/']} initialIndex={0}>
64+
<Routes>
65+
<Route path="/foo/bar" element={<DocumentMutator />}></Route>
66+
<Route path="*" element={<Link to="/foo/bar">/foo/bar</Link>}></Route>
67+
</Routes>
68+
<ChromeProvider />
69+
</MemoryRouter>
70+
</ScalprumContext.Provider>
4571
);
4672
});
4773
// change location
@@ -54,7 +80,11 @@ describe('ChromeProvider', () => {
5480
await flushPromises();
5581
});
5682
expect(postSpy).toHaveBeenCalledTimes(2);
57-
expect(postSpy).toHaveBeenLastCalledWith('/api/chrome-service/v1/last-visited', { pathname: '/foo/bar', title: '', bundle: 'bundle-title' });
83+
expect(postSpy).toHaveBeenLastCalledWith('/api/chrome-service/v1/last-visited', {
84+
pathname: '/foo/bar',
85+
title: 'Foo title',
86+
bundle: 'bundle-title',
87+
});
5888
});
5989

6090
test('should not update state on mount if received error response', async () => {
@@ -66,7 +96,22 @@ describe('ChromeProvider', () => {
6696
await act(async () => {
6797
await render(
6898
<MemoryRouter>
69-
<ChromeProvider />
99+
<ScalprumContext.Provider
100+
value={{
101+
config: {},
102+
initialized: true,
103+
pluginStore: new PluginStore(),
104+
api: {
105+
chrome: {
106+
getBundleData: () => ({
107+
bundleTitle: 'bundle-title',
108+
}),
109+
},
110+
},
111+
}}
112+
>
113+
<ChromeProvider />
114+
</ScalprumContext.Provider>
70115
</MemoryRouter>
71116
);
72117
});

packages/chrome/src/ChromeProvider/ChromeProvider.tsx

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import React, { useEffect, useRef, useState } from 'react';
2+
import { useScalprum } from '@scalprum/react-core';
3+
import { ChromeAPI } from '@redhat-cloud-services/types';
24
import { useLocation } from 'react-router-dom';
35

46
import { ChromeContext } from '../ChromeContext';
@@ -7,28 +9,69 @@ import { IDENTITY_URL, LAST_VISITED_URL, get, post } from '../utils/fetch';
79

810
const getUserIdentity = () => get<UserIdentity>(IDENTITY_URL);
911

10-
const useLastPageVisitedUploader = (providerState: ReturnType<typeof chromeState>, bundle = '') => {
12+
const useLastPageVisitedUploader = (providerState: ReturnType<typeof chromeState>) => {
13+
const scalprum = useScalprum<{ initialized: boolean; api: { chrome: ChromeAPI } }>();
1114
const { pathname } = useLocation();
15+
const postData = async (pathname: string, title: string, bundle: string) => {
16+
try {
17+
const data = await post<LastVisitedPage[], { pathname: string; title: string; bundle: string }>(LAST_VISITED_URL, {
18+
pathname,
19+
title,
20+
bundle,
21+
});
22+
providerState.setLastVisited(data);
23+
} catch (error) {
24+
console.error('Unable to update last visited pages!', error);
25+
}
26+
};
1227
useEffect(() => {
13-
post<LastVisitedPage[], { pathname: string; title: string; bundle: string }>(LAST_VISITED_URL, {
14-
pathname,
15-
title: document.title,
16-
bundle,
17-
})
18-
.then((data) => providerState.setLastVisited(data))
19-
.catch((error) => console.error('Unable to update last visited pages!', error));
28+
let titleObserver: MutationObserver | undefined;
29+
let prevTitle: string | null;
30+
const titleTarget = document.querySelector('title');
31+
if (titleTarget) {
32+
prevTitle = titleTarget.textContent;
33+
// initial api call on mount
34+
postData(pathname, prevTitle ?? '', scalprum.api.chrome.getBundleData().bundleTitle);
35+
/**
36+
* Use Mutation observer to trigger the updates.
37+
* Using the observer will ensure the last visited pages gets updated on document title change rather than just location change.
38+
* The chrome service uses pathname as identifier and updates title according.
39+
* Multiple calls with the same pathname and different title will ensure that the latest correct title is assigned to a pathname. *
40+
* */
41+
titleObserver = new MutationObserver((mutations) => {
42+
// grab text from the title element
43+
const currentTitle = mutations[0]?.target.textContent;
44+
// trigger only if the titles are different
45+
if (typeof currentTitle === 'string' && currentTitle !== prevTitle) {
46+
try {
47+
prevTitle = currentTitle;
48+
postData(pathname, currentTitle, scalprum.api.chrome.getBundleData().bundleTitle);
49+
} catch (error) {
50+
// catch sync errors
51+
console.error('Unable to update last visited pages!', error);
52+
}
53+
}
54+
});
55+
titleObserver.observe(titleTarget, {
56+
// observe only the children
57+
childList: true,
58+
});
59+
}
60+
return () => {
61+
titleObserver?.disconnect();
62+
};
2063
}, [pathname]);
2164
};
2265

23-
const ChromeProvider: React.FC<React.PropsWithChildren<{ bundle?: string }>> = ({ children, bundle }) => {
66+
const ChromeProvider: React.FC<React.PropsWithChildren> = ({ children }) => {
2467
const isMounted = useRef(false);
2568
const [initialRequest, setInitialRequest] = useState(false);
2669
const providerState = useRef<ReturnType<typeof chromeState>>();
2770
if (!providerState.current) {
2871
providerState.current = chromeState();
2972
}
3073

31-
useLastPageVisitedUploader(providerState.current, bundle);
74+
useLastPageVisitedUploader(providerState.current);
3275

3376
useEffect(() => {
3477
isMounted.current = true;

0 commit comments

Comments
 (0)