@conorheffron/ironoc-frontend 9.1.4 → 9.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/App.test.js +44 -1
- package/src/AppNavbar.js +10 -5
- package/src/components/Donate.js +53 -0
- package/src/components/RepoIssues.js +7 -2
- package/src/components/__tests__/Donate.test.js +32 -4
- package/src/components/__tests__/RepoIssues.test.js +37 -1
- package/src/index.test.js +54 -0
- package/src/reportWebVitals.test.js +60 -0
- package/src/utils/activityTracker.js +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conorheffron/ironoc-frontend",
|
|
3
|
-
"version": "9.1.
|
|
3
|
+
"version": "9.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@graphiql/plugin-explorer": "^5.1.1",
|
|
14
14
|
"@graphiql/react": "^0.37.1",
|
|
15
15
|
"@testing-library/user-event": "^14.6.1",
|
|
16
|
-
"axios": "^1.
|
|
16
|
+
"axios": "^1.15.2",
|
|
17
17
|
"bootstrap": "5.3",
|
|
18
18
|
"graphql": "^16.11.0",
|
|
19
19
|
"history": "^5.3.0",
|
package/src/App.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
3
|
import '@testing-library/jest-dom';
|
|
4
4
|
import Home from './components/Home';
|
|
5
5
|
import App from './App';
|
|
@@ -9,6 +9,11 @@ import AppNavBar from './AppNavbar';
|
|
|
9
9
|
import About from './components/About';
|
|
10
10
|
import Footer from './Footer';
|
|
11
11
|
import { Router, useLocation, MemoryRouter } from 'react-router';
|
|
12
|
+
import { trackClickOut } from './utils/activityTracker';
|
|
13
|
+
|
|
14
|
+
jest.mock('./utils/activityTracker', () => ({
|
|
15
|
+
trackClickOut: jest.fn(),
|
|
16
|
+
}));
|
|
12
17
|
|
|
13
18
|
describe('AppNavBar', () => {
|
|
14
19
|
test('renders AppNavBar component correctly', () => {
|
|
@@ -65,6 +70,27 @@ describe('AppNavBar', () => {
|
|
|
65
70
|
expect(homeLink).toBeInTheDocument();
|
|
66
71
|
expect(homeLink.closest('a')).toHaveAttribute('href', '/');
|
|
67
72
|
});
|
|
73
|
+
|
|
74
|
+
test('toggles collapsed navbar state when toggler is clicked', async () => {
|
|
75
|
+
const { container } = render(<AppNavBar />);
|
|
76
|
+
const toggler = container.querySelector('.navbar-toggler');
|
|
77
|
+
const collapse = container.querySelector('.navbar-collapse');
|
|
78
|
+
|
|
79
|
+
expect(toggler).toBeInTheDocument();
|
|
80
|
+
expect(collapse).toBeInTheDocument();
|
|
81
|
+
expect(collapse).not.toHaveClass('show');
|
|
82
|
+
fireEvent.click(toggler);
|
|
83
|
+
await waitFor(() => expect(collapse).toHaveClass('show'));
|
|
84
|
+
fireEvent.click(toggler);
|
|
85
|
+
await waitFor(() => expect(collapse).not.toHaveClass('show'));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('tracks click-outs for GitHub project links', () => {
|
|
89
|
+
render(<AppNavBar />);
|
|
90
|
+
const githubProjectLink = screen.getByText('iRonoc-DB');
|
|
91
|
+
fireEvent.click(githubProjectLink);
|
|
92
|
+
expect(trackClickOut).toHaveBeenCalledWith('github', 'https://github.com/conorheffron/ironoc-db');
|
|
93
|
+
});
|
|
68
94
|
});
|
|
69
95
|
|
|
70
96
|
describe('Footer Component', () => {
|
|
@@ -109,4 +135,21 @@ describe('Footer Component', () => {
|
|
|
109
135
|
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
110
136
|
await waitFor(() => expect(screen.getByText('© 2025 by Conor Heffron |')).toBeInTheDocument());
|
|
111
137
|
});
|
|
138
|
+
|
|
139
|
+
test('handles non-ok fetch responses', async () => {
|
|
140
|
+
const textMock = jest.fn(() => Promise.resolve('unexpected-version'));
|
|
141
|
+
fetch.mockImplementationOnce(() =>
|
|
142
|
+
Promise.resolve({
|
|
143
|
+
ok: false,
|
|
144
|
+
text: textMock,
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
render(<Footer />);
|
|
149
|
+
await waitFor(() => expect(fetch).toHaveBeenCalledTimes(1));
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(textMock).not.toHaveBeenCalled();
|
|
152
|
+
expect(screen.queryByText('unexpected-version')).not.toBeInTheDocument();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
112
155
|
});
|
package/src/AppNavbar.js
CHANGED
|
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
|
|
2
2
|
import "bootstrap/dist/css/bootstrap.min.css";
|
|
3
3
|
import {Navbar, NavbarText, Container, Collapse, NavbarBrand, NavbarToggler, Nav, UncontrolledDropdown, DropdownToggle, DropdownItem, DropdownMenu } from "reactstrap";
|
|
4
4
|
import logo from './img/robot-logo.png';
|
|
5
|
+
import { trackClickOut } from './utils/activityTracker';
|
|
5
6
|
import "@fontsource/montserrat/700.css";
|
|
6
7
|
import "@fontsource/open-sans/400-italic.css";
|
|
7
8
|
|
|
@@ -34,6 +35,10 @@ class AppNavBar extends Component {
|
|
|
34
35
|
this.setState({ isOpen: !this.state.isOpen });
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
handleGitHubClick = (target) => {
|
|
39
|
+
trackClickOut('github', target);
|
|
40
|
+
};
|
|
41
|
+
|
|
37
42
|
render() {
|
|
38
43
|
const { color, light, dark, fixed, container, expand, className, isOpen } = this.state;
|
|
39
44
|
|
|
@@ -76,12 +81,12 @@ class AppNavBar extends Component {
|
|
|
76
81
|
<UncontrolledDropdown inNavbar nav>
|
|
77
82
|
<DropdownToggle caret nav>GitHub Projects</DropdownToggle>
|
|
78
83
|
<DropdownMenu end>
|
|
79
|
-
<DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc">iRonoc</DropdownItem>
|
|
84
|
+
<DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/ironoc')}>iRonoc</DropdownItem>
|
|
80
85
|
<DropdownItem divider />
|
|
81
|
-
<DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-db">iRonoc-DB</DropdownItem>
|
|
82
|
-
<DropdownItem target="_blank" href="https://github.com/conorheffron/booking-sys">Booking System Sample</DropdownItem>
|
|
83
|
-
<DropdownItem target="_blank" href="https://github.com/conorheffron/nba-stats">NBA Stats Analysis</DropdownItem>
|
|
84
|
-
<DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-pytest">PyTest GitHub Client Package</DropdownItem>
|
|
86
|
+
<DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-db" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/ironoc-db')}>iRonoc-DB</DropdownItem>
|
|
87
|
+
<DropdownItem target="_blank" href="https://github.com/conorheffron/booking-sys" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/booking-sys')}>Booking System Sample</DropdownItem>
|
|
88
|
+
<DropdownItem target="_blank" href="https://github.com/conorheffron/nba-stats" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/nba-stats')}>NBA Stats Analysis</DropdownItem>
|
|
89
|
+
<DropdownItem target="_blank" href="https://github.com/conorheffron/ironoc-pytest" onClick={() => this.handleGitHubClick('https://github.com/conorheffron/ironoc-pytest')}>PyTest GitHub Client Package</DropdownItem>
|
|
85
90
|
</DropdownMenu>
|
|
86
91
|
</UncontrolledDropdown>
|
|
87
92
|
<UncontrolledDropdown inNavbar nav>
|
package/src/components/Donate.js
CHANGED
|
@@ -8,6 +8,7 @@ import AppNavbar from '../AppNavbar';
|
|
|
8
8
|
import red from '../img/red-bg.png';
|
|
9
9
|
import { Container } from 'reactstrap';
|
|
10
10
|
import LoadingSpinner from '.././LoadingSpinner';
|
|
11
|
+
import { trackClickOut } from '../utils/activityTracker';
|
|
11
12
|
|
|
12
13
|
// Define the GraphQL query to fetch donate items
|
|
13
14
|
export const GET_DONATE_ITEMS = gql`
|
|
@@ -78,6 +79,58 @@ class Donate extends Component {
|
|
|
78
79
|
);
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
return (
|
|
83
|
+
<div className="App">
|
|
84
|
+
<AppNavbar />
|
|
85
|
+
<Container>
|
|
86
|
+
<Carousel className="App-header">
|
|
87
|
+
{donateItems.map((item, index) => (
|
|
88
|
+
<Carousel.Item key={index} interval={500}>
|
|
89
|
+
<img className="d-block w-100" src={red} alt={item.alt} />
|
|
90
|
+
|
|
91
|
+
<Carousel.Caption>
|
|
92
|
+
<h1 className="mb-3">
|
|
93
|
+
<span style={{ textDecoration: 'underline' }}>{item.name}</span>
|
|
94
|
+
</h1>
|
|
95
|
+
|
|
96
|
+
<p>
|
|
97
|
+
<b>Contact & Help by Phone: </b>
|
|
98
|
+
<span dangerouslySetInnerHTML={{ __html: item.phone }} />
|
|
99
|
+
</p>
|
|
100
|
+
|
|
101
|
+
<p>
|
|
102
|
+
<b>Home page: </b>
|
|
103
|
+
<a
|
|
104
|
+
href={item.link}
|
|
105
|
+
target="_blank"
|
|
106
|
+
rel="noreferrer"
|
|
107
|
+
onClick={() => trackClickOut('charity', item.link)}
|
|
108
|
+
>
|
|
109
|
+
{item.link}
|
|
110
|
+
</a>
|
|
111
|
+
</p>
|
|
112
|
+
|
|
113
|
+
<p className="overview-text">
|
|
114
|
+
<b>Overview:</b> Founded in {item.founded}, {item.overview}
|
|
115
|
+
</p>
|
|
116
|
+
|
|
117
|
+
<p>
|
|
118
|
+
<a
|
|
119
|
+
href={item.donate}
|
|
120
|
+
target="_blank"
|
|
121
|
+
rel="noreferrer"
|
|
122
|
+
onClick={() => trackClickOut('charity', item.donate)}
|
|
123
|
+
>
|
|
124
|
+
Donate here
|
|
125
|
+
</a>
|
|
126
|
+
</p>
|
|
127
|
+
</Carousel.Caption>
|
|
128
|
+
</Carousel.Item>
|
|
129
|
+
))}
|
|
130
|
+
</Carousel>
|
|
131
|
+
</Container>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
81
134
|
return (
|
|
82
135
|
<div className="App">
|
|
83
136
|
<AppNavbar />
|
|
@@ -6,7 +6,6 @@ import { useParams, useNavigate } from 'react-router';
|
|
|
6
6
|
import {
|
|
7
7
|
MaterialReactTable,
|
|
8
8
|
useMaterialReactTable,
|
|
9
|
-
type MRT_ColumnDef,
|
|
10
9
|
} from 'material-react-table';
|
|
11
10
|
import { darken, lighten, useTheme } from '@mui/material';
|
|
12
11
|
|
|
@@ -126,7 +125,13 @@ const RepoIssues = () => {
|
|
|
126
125
|
data: repoIssueList,
|
|
127
126
|
enableFacetedValues: true,
|
|
128
127
|
enableStickyHeader: true,
|
|
129
|
-
initialState: {
|
|
128
|
+
initialState: {
|
|
129
|
+
showColumnFilters: true,
|
|
130
|
+
columnVisibility: {
|
|
131
|
+
state: false,
|
|
132
|
+
body: false,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
130
135
|
muiTablePaperProps: {
|
|
131
136
|
elevation: 0,
|
|
132
137
|
sx: {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { render, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
3
3
|
import { MockedProvider } from '@apollo/client/testing';
|
|
4
4
|
import Donate, { GET_DONATE_ITEMS } from '../Donate';
|
|
5
5
|
import '@testing-library/jest-dom';
|
|
6
|
+
import { trackClickOut } from '../../utils/activityTracker';
|
|
7
|
+
|
|
8
|
+
jest.mock('../../utils/activityTracker', () => ({
|
|
9
|
+
trackClickOut: jest.fn(),
|
|
10
|
+
}));
|
|
6
11
|
|
|
7
12
|
// Mock data for testing
|
|
8
13
|
const mockDonateItems = [
|
|
@@ -57,7 +62,7 @@ const errorMocks = [
|
|
|
57
62
|
describe('Donate Component', () => {
|
|
58
63
|
it('renders loading spinner initially', () => {
|
|
59
64
|
render(
|
|
60
|
-
<MockedProvider mocks={mocks}
|
|
65
|
+
<MockedProvider mocks={mocks}>
|
|
61
66
|
<Donate />
|
|
62
67
|
</MockedProvider>
|
|
63
68
|
);
|
|
@@ -68,7 +73,7 @@ describe('Donate Component', () => {
|
|
|
68
73
|
|
|
69
74
|
it('renders error message when GraphQL query fails', async () => {
|
|
70
75
|
render(
|
|
71
|
-
<MockedProvider mocks={errorMocks}
|
|
76
|
+
<MockedProvider mocks={errorMocks}>
|
|
72
77
|
<Donate />
|
|
73
78
|
</MockedProvider>
|
|
74
79
|
);
|
|
@@ -81,7 +86,7 @@ describe('Donate Component', () => {
|
|
|
81
86
|
|
|
82
87
|
it('renders donate items after successful fetch', async () => {
|
|
83
88
|
render(
|
|
84
|
-
<MockedProvider mocks={mocks}
|
|
89
|
+
<MockedProvider mocks={mocks}>
|
|
85
90
|
<Donate />
|
|
86
91
|
</MockedProvider>
|
|
87
92
|
);
|
|
@@ -94,4 +99,27 @@ describe('Donate Component', () => {
|
|
|
94
99
|
});
|
|
95
100
|
});
|
|
96
101
|
});
|
|
102
|
+
|
|
103
|
+
it('tracks charity click-outs for donate and homepage links', async () => {
|
|
104
|
+
render(
|
|
105
|
+
<MockedProvider mocks={mocks}>
|
|
106
|
+
<Donate />
|
|
107
|
+
</MockedProvider>
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await waitFor(() => {
|
|
111
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const donateLink = document.querySelector('a[href="https://example.com/donate1"]');
|
|
115
|
+
const homepageLink = document.querySelector('a[href="https://example.com/home1"]');
|
|
116
|
+
expect(donateLink).toBeInTheDocument();
|
|
117
|
+
expect(homepageLink).toBeInTheDocument();
|
|
118
|
+
|
|
119
|
+
fireEvent.click(donateLink);
|
|
120
|
+
fireEvent.click(homepageLink);
|
|
121
|
+
|
|
122
|
+
expect(trackClickOut).toHaveBeenCalledWith('charity', 'https://example.com/donate1');
|
|
123
|
+
expect(trackClickOut).toHaveBeenCalledWith('charity', 'https://example.com/home1');
|
|
124
|
+
});
|
|
97
125
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import RepoIssues from '../RepoIssues';
|
|
4
|
+
import { useMaterialReactTable } from 'material-react-table';
|
|
4
5
|
|
|
5
6
|
// Mock react-router
|
|
6
7
|
jest.mock('react-router', () => ({
|
|
@@ -10,7 +11,11 @@ jest.mock('react-router', () => ({
|
|
|
10
11
|
|
|
11
12
|
// Mock react-bootstrap
|
|
12
13
|
jest.mock('react-bootstrap', () => ({
|
|
13
|
-
Container: ({ children, ...props }) =>
|
|
14
|
+
Container: ({ children, fluid, ...props }) => (
|
|
15
|
+
<div data-testid="container" data-fluid={fluid ? 'true' : undefined} {...props}>
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
14
19
|
InputGroup: ({ children, ...props }) => <div data-testid="input-group" {...props}>{children}</div>,
|
|
15
20
|
Form: {
|
|
16
21
|
Control: ({ ...props }) => <input data-testid="form-control" {...props} />,
|
|
@@ -50,14 +55,23 @@ import { useParams, useNavigate } from 'react-router';
|
|
|
50
55
|
|
|
51
56
|
describe('RepoIssues', () => {
|
|
52
57
|
const mockNavigate = jest.fn();
|
|
58
|
+
const mockIssuesResponse = [];
|
|
53
59
|
|
|
54
60
|
beforeEach(() => {
|
|
55
61
|
jest.clearAllMocks();
|
|
56
62
|
useNavigate.mockReturnValue(mockNavigate);
|
|
63
|
+
global.fetch = jest.fn(() =>
|
|
64
|
+
Promise.resolve({
|
|
65
|
+
json: () => Promise.resolve(mockIssuesResponse),
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
window.fetch = global.fetch;
|
|
57
69
|
});
|
|
58
70
|
|
|
59
71
|
it('shows loading spinner and navbar initially', () => {
|
|
60
72
|
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
73
|
+
global.fetch = jest.fn(() => new Promise(() => {}));
|
|
74
|
+
window.fetch = global.fetch;
|
|
61
75
|
render(<RepoIssues />);
|
|
62
76
|
expect(screen.getByTestId('navbar')).toBeInTheDocument();
|
|
63
77
|
expect(screen.getByTestId('spinner')).toBeInTheDocument();
|
|
@@ -78,6 +92,7 @@ describe('RepoIssues', () => {
|
|
|
78
92
|
]),
|
|
79
93
|
})
|
|
80
94
|
);
|
|
95
|
+
window.fetch = global.fetch;
|
|
81
96
|
render(<RepoIssues />);
|
|
82
97
|
await waitFor(() =>
|
|
83
98
|
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
@@ -85,9 +100,30 @@ describe('RepoIssues', () => {
|
|
|
85
100
|
expect(screen.getByTestId('mrt-table')).toBeInTheDocument();
|
|
86
101
|
});
|
|
87
102
|
|
|
103
|
+
it('hides state and description columns by default', async () => {
|
|
104
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
105
|
+
render(<RepoIssues />);
|
|
106
|
+
await waitFor(() =>
|
|
107
|
+
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(useMaterialReactTable).toHaveBeenCalledWith(
|
|
111
|
+
expect.objectContaining({
|
|
112
|
+
initialState: expect.objectContaining({
|
|
113
|
+
showColumnFilters: true,
|
|
114
|
+
columnVisibility: {
|
|
115
|
+
state: false,
|
|
116
|
+
body: false,
|
|
117
|
+
},
|
|
118
|
+
}),
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
88
123
|
it('navigates on form submit', async () => {
|
|
89
124
|
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
90
125
|
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));
|
|
126
|
+
window.fetch = global.fetch;
|
|
91
127
|
render(<RepoIssues />);
|
|
92
128
|
await waitFor(() => expect(screen.queryByTestId('spinner')).not.toBeInTheDocument());
|
|
93
129
|
const input = screen.getByTestId('form-control');
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const mockRender = jest.fn();
|
|
2
|
+
const mockCreateRoot = jest.fn(() => ({
|
|
3
|
+
render: mockRender,
|
|
4
|
+
}));
|
|
5
|
+
|
|
6
|
+
const elementContainsClassName = (node, className) => {
|
|
7
|
+
if (!node) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (Array.isArray(node)) {
|
|
12
|
+
return node.some((child) => elementContainsClassName(child, className));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof node !== 'object') {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nodeClassName = node.props && typeof node.props.className === 'string'
|
|
20
|
+
? node.props.className.split(/\s+/)
|
|
21
|
+
: [];
|
|
22
|
+
|
|
23
|
+
if (nodeClassName.includes(className)) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return elementContainsClassName(node.props && node.props.children, className);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('index entrypoint', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.resetModules();
|
|
33
|
+
mockCreateRoot.mockReset();
|
|
34
|
+
mockCreateRoot.mockImplementation(() => ({ render: mockRender }));
|
|
35
|
+
mockRender.mockReset();
|
|
36
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('renders application wrapper into root element', () => {
|
|
40
|
+
jest.isolateModules(() => {
|
|
41
|
+
jest.doMock('react-dom/client', () => ({
|
|
42
|
+
createRoot: mockCreateRoot,
|
|
43
|
+
}));
|
|
44
|
+
jest.doMock('./App', () => () => null);
|
|
45
|
+
jest.doMock('./AppNavbar', () => () => null);
|
|
46
|
+
jest.doMock('./Footer', () => () => null);
|
|
47
|
+
require('./index');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(mockCreateRoot).toHaveBeenCalledWith(document.getElementById('root'));
|
|
51
|
+
expect(mockRender).toHaveBeenCalledTimes(1);
|
|
52
|
+
expect(elementContainsClassName(mockRender.mock.calls[0][0], 'app-wrapper')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
describe('reportWebVitals', () => {
|
|
2
|
+
test('does nothing when callback is not provided', async () => {
|
|
3
|
+
const getCLSMock = jest.fn();
|
|
4
|
+
const getFIDMock = jest.fn();
|
|
5
|
+
const getFCPMock = jest.fn();
|
|
6
|
+
const getLCPMock = jest.fn();
|
|
7
|
+
const getTTFBMock = jest.fn();
|
|
8
|
+
|
|
9
|
+
jest.resetModules();
|
|
10
|
+
jest.isolateModules(() => {
|
|
11
|
+
jest.doMock('web-vitals', () => ({
|
|
12
|
+
getCLS: getCLSMock,
|
|
13
|
+
getFID: getFIDMock,
|
|
14
|
+
getFCP: getFCPMock,
|
|
15
|
+
getLCP: getLCPMock,
|
|
16
|
+
getTTFB: getTTFBMock,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const reportWebVitals = require('./reportWebVitals').default;
|
|
20
|
+
reportWebVitals();
|
|
21
|
+
});
|
|
22
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
23
|
+
|
|
24
|
+
expect(getCLSMock).not.toHaveBeenCalled();
|
|
25
|
+
expect(getFIDMock).not.toHaveBeenCalled();
|
|
26
|
+
expect(getFCPMock).not.toHaveBeenCalled();
|
|
27
|
+
expect(getLCPMock).not.toHaveBeenCalled();
|
|
28
|
+
expect(getTTFBMock).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('forwards callback to web-vitals metrics', async () => {
|
|
32
|
+
const getCLSMock = jest.fn();
|
|
33
|
+
const getFIDMock = jest.fn();
|
|
34
|
+
const getFCPMock = jest.fn();
|
|
35
|
+
const getLCPMock = jest.fn();
|
|
36
|
+
const getTTFBMock = jest.fn();
|
|
37
|
+
const onPerfEntry = function onPerfEntry() {};
|
|
38
|
+
|
|
39
|
+
jest.resetModules();
|
|
40
|
+
jest.isolateModules(() => {
|
|
41
|
+
jest.doMock('web-vitals', () => ({
|
|
42
|
+
getCLS: getCLSMock,
|
|
43
|
+
getFID: getFIDMock,
|
|
44
|
+
getFCP: getFCPMock,
|
|
45
|
+
getLCP: getLCPMock,
|
|
46
|
+
getTTFB: getTTFBMock,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const reportWebVitals = require('./reportWebVitals').default;
|
|
50
|
+
reportWebVitals(onPerfEntry);
|
|
51
|
+
});
|
|
52
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
53
|
+
|
|
54
|
+
expect(getCLSMock).toHaveBeenCalledWith(onPerfEntry);
|
|
55
|
+
expect(getFIDMock).toHaveBeenCalledWith(onPerfEntry);
|
|
56
|
+
expect(getFCPMock).toHaveBeenCalledWith(onPerfEntry);
|
|
57
|
+
expect(getLCPMock).toHaveBeenCalledWith(onPerfEntry);
|
|
58
|
+
expect(getTTFBMock).toHaveBeenCalledWith(onPerfEntry);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const trackClickOut = (category, target) => {
|
|
2
|
+
if (!category || !target) {
|
|
3
|
+
return;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const payload = JSON.stringify({ category, target });
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
|
|
10
|
+
const blob = new Blob([payload], { type: 'application/json' });
|
|
11
|
+
navigator.sendBeacon('/api/activity/click-out', blob);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fetch('/api/activity/click-out', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: payload,
|
|
19
|
+
keepalive: true
|
|
20
|
+
}).catch(() => null);
|
|
21
|
+
} catch {
|
|
22
|
+
// no-op to avoid blocking navigation
|
|
23
|
+
}
|
|
24
|
+
};
|