@conorheffron/ironoc-frontend 9.1.5 → 9.1.7
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 +1 -1
- package/src/App.test.js +12 -0
- package/src/AppNavbar.js +10 -5
- package/src/components/Donate.js +53 -0
- package/src/components/RepoIssues.js +8 -2
- package/src/components/__tests__/Donate.test.js +29 -1
- package/src/components/__tests__/RepoIssues.test.js +78 -4
- package/src/utils/activityTracker.js +24 -0
package/package.json
CHANGED
package/src/App.test.js
CHANGED
|
@@ -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', () => {
|
|
@@ -79,6 +84,13 @@ describe('AppNavBar', () => {
|
|
|
79
84
|
fireEvent.click(toggler);
|
|
80
85
|
await waitFor(() => expect(collapse).not.toHaveClass('show'));
|
|
81
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
|
+
});
|
|
82
94
|
});
|
|
83
95
|
|
|
84
96
|
describe('Footer Component', () => {
|
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,14 @@ const RepoIssues = () => {
|
|
|
126
125
|
data: repoIssueList,
|
|
127
126
|
enableFacetedValues: true,
|
|
128
127
|
enableStickyHeader: true,
|
|
129
|
-
initialState: {
|
|
128
|
+
initialState: {
|
|
129
|
+
showColumnFilters: true,
|
|
130
|
+
columnFilters: [{ id: 'state', value: ['open'] }],
|
|
131
|
+
columnVisibility: {
|
|
132
|
+
state: false,
|
|
133
|
+
body: false,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
130
136
|
muiTablePaperProps: {
|
|
131
137
|
elevation: 0,
|
|
132
138
|
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 = [
|
|
@@ -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
|
});
|
|
@@ -2,6 +2,9 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
3
3
|
import RepoIssues from '../RepoIssues';
|
|
4
4
|
|
|
5
|
+
// Track useMaterialReactTable call opts for assertions (must start with 'mock' for Jest hoisting)
|
|
6
|
+
const mockUseMRTOpts = [];
|
|
7
|
+
|
|
5
8
|
// Mock react-router
|
|
6
9
|
jest.mock('react-router', () => ({
|
|
7
10
|
useParams: jest.fn(),
|
|
@@ -10,7 +13,11 @@ jest.mock('react-router', () => ({
|
|
|
10
13
|
|
|
11
14
|
// Mock react-bootstrap
|
|
12
15
|
jest.mock('react-bootstrap', () => ({
|
|
13
|
-
Container: ({ children, ...props }) =>
|
|
16
|
+
Container: ({ children, fluid, ...props }) => (
|
|
17
|
+
<div data-testid="container" data-fluid={fluid ? 'true' : undefined} {...props}>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
14
21
|
InputGroup: ({ children, ...props }) => <div data-testid="input-group" {...props}>{children}</div>,
|
|
15
22
|
Form: {
|
|
16
23
|
Control: ({ ...props }) => <input data-testid="form-control" {...props} />,
|
|
@@ -22,16 +29,31 @@ jest.mock('react-bootstrap', () => ({
|
|
|
22
29
|
jest.mock('../../AppNavbar', () => () => <div data-testid="navbar">Navbar</div>);
|
|
23
30
|
jest.mock('../../LoadingSpinner', () => () => <div data-testid="spinner">Loading...</div>);
|
|
24
31
|
|
|
25
|
-
// Mock MaterialReactTable and useMaterialReactTable
|
|
32
|
+
// Mock MaterialReactTable and useMaterialReactTable.
|
|
33
|
+
// useMaterialReactTable is a plain function (not jest.fn) so React 19 concurrent mode
|
|
34
|
+
// commits re-renders correctly. Calls are tracked via mockUseMRTOpts for assertions.
|
|
35
|
+
// columnFilters from initialState are applied to table.data so MaterialReactTable
|
|
36
|
+
// only receives the filtered rows, allowing DOM-level assertions on visibility.
|
|
26
37
|
jest.mock('material-react-table', () => ({
|
|
27
38
|
MaterialReactTable: ({ table }) => (
|
|
28
39
|
<div data-testid="mrt-table">
|
|
29
|
-
{table
|
|
40
|
+
{(table?.data ?? []).map((issue, idx) => (
|
|
30
41
|
<div key={idx} data-testid="mrt-row">{issue.title}</div>
|
|
31
42
|
))}
|
|
32
43
|
</div>
|
|
33
44
|
),
|
|
34
|
-
useMaterialReactTable:
|
|
45
|
+
useMaterialReactTable: (opts) => {
|
|
46
|
+
mockUseMRTOpts.push(opts);
|
|
47
|
+
const filters = opts?.initialState?.columnFilters ?? [];
|
|
48
|
+
let data = opts?.data ?? [];
|
|
49
|
+
filters.forEach((filter) => {
|
|
50
|
+
data = data.filter((row) => {
|
|
51
|
+
const val = row[filter.id];
|
|
52
|
+
return Array.isArray(filter.value) ? filter.value.includes(val) : val === filter.value;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
return { ...opts, data };
|
|
56
|
+
},
|
|
35
57
|
}));
|
|
36
58
|
|
|
37
59
|
// Mock @mui/material theme functions
|
|
@@ -50,14 +72,24 @@ import { useParams, useNavigate } from 'react-router';
|
|
|
50
72
|
|
|
51
73
|
describe('RepoIssues', () => {
|
|
52
74
|
const mockNavigate = jest.fn();
|
|
75
|
+
const mockIssuesResponse = [];
|
|
53
76
|
|
|
54
77
|
beforeEach(() => {
|
|
78
|
+
mockUseMRTOpts.length = 0;
|
|
55
79
|
jest.clearAllMocks();
|
|
56
80
|
useNavigate.mockReturnValue(mockNavigate);
|
|
81
|
+
global.fetch = jest.fn(() =>
|
|
82
|
+
Promise.resolve({
|
|
83
|
+
json: () => Promise.resolve(mockIssuesResponse),
|
|
84
|
+
})
|
|
85
|
+
);
|
|
86
|
+
window.fetch = global.fetch;
|
|
57
87
|
});
|
|
58
88
|
|
|
59
89
|
it('shows loading spinner and navbar initially', () => {
|
|
60
90
|
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
91
|
+
global.fetch = jest.fn(() => new Promise(() => {}));
|
|
92
|
+
window.fetch = global.fetch;
|
|
61
93
|
render(<RepoIssues />);
|
|
62
94
|
expect(screen.getByTestId('navbar')).toBeInTheDocument();
|
|
63
95
|
expect(screen.getByTestId('spinner')).toBeInTheDocument();
|
|
@@ -78,16 +110,58 @@ describe('RepoIssues', () => {
|
|
|
78
110
|
]),
|
|
79
111
|
})
|
|
80
112
|
);
|
|
113
|
+
window.fetch = global.fetch;
|
|
81
114
|
render(<RepoIssues />);
|
|
82
115
|
await waitFor(() =>
|
|
83
116
|
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
84
117
|
);
|
|
85
118
|
expect(screen.getByTestId('mrt-table')).toBeInTheDocument();
|
|
119
|
+
expect(screen.getByText('Test Issue')).toBeInTheDocument();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('defaults to open issues while hiding state and description columns', async () => {
|
|
123
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
124
|
+
render(<RepoIssues />);
|
|
125
|
+
await waitFor(() =>
|
|
126
|
+
expect(screen.queryByTestId('spinner')).not.toBeInTheDocument()
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(mockUseMRTOpts).toContainEqual(
|
|
130
|
+
expect.objectContaining({
|
|
131
|
+
initialState: expect.objectContaining({
|
|
132
|
+
showColumnFilters: true,
|
|
133
|
+
columnFilters: [{ id: 'state', value: ['open'] }],
|
|
134
|
+
columnVisibility: {
|
|
135
|
+
state: false,
|
|
136
|
+
body: false,
|
|
137
|
+
},
|
|
138
|
+
}),
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('renders open issues and excludes closed issues by default', async () => {
|
|
144
|
+
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
145
|
+
global.fetch = jest.fn(() =>
|
|
146
|
+
Promise.resolve({
|
|
147
|
+
json: () => Promise.resolve([
|
|
148
|
+
{ number: 1, state: 'open', labels: [], title: 'Open Issue', body: '' },
|
|
149
|
+
{ number: 2, state: 'closed', labels: [], title: 'Closed Issue', body: '' },
|
|
150
|
+
]),
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
window.fetch = global.fetch;
|
|
154
|
+
render(<RepoIssues />);
|
|
155
|
+
await waitFor(() =>
|
|
156
|
+
expect(screen.getByText('Open Issue')).toBeInTheDocument()
|
|
157
|
+
);
|
|
158
|
+
expect(screen.queryByText('Closed Issue')).not.toBeInTheDocument();
|
|
86
159
|
});
|
|
87
160
|
|
|
88
161
|
it('navigates on form submit', async () => {
|
|
89
162
|
useParams.mockReturnValue({ id: 'user', repo: 'repo' });
|
|
90
163
|
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([]) }));
|
|
164
|
+
window.fetch = global.fetch;
|
|
91
165
|
render(<RepoIssues />);
|
|
92
166
|
await waitFor(() => expect(screen.queryByTestId('spinner')).not.toBeInTheDocument());
|
|
93
167
|
const input = screen.getByTestId('form-control');
|
|
@@ -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
|
+
};
|