@next-box/link-extra 0.1.0 → 0.1.2

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.
@@ -0,0 +1,159 @@
1
+ /** @jest-environment @repodog/jest-config/FailOnJsdomConsoleEnvironment.mjs */
2
+
3
+ import { afterEach, describe, expect, it, jest } from '@jest/globals';
4
+ import { type ComponentProps, type ComponentType, type ReactElement, act } from 'react';
5
+ import { createRoot } from 'react-dom/client';
6
+ import { Link } from './Link.tsx';
7
+ import { LinkProvider } from './LinkProvider.tsx';
8
+ import { shouldUrlAndLocationMatch } from './helpers.ts';
9
+ import { type NextLinkComponent, type OwnLinkProps } from './types.ts';
10
+
11
+ const NextLinkMock: NextLinkComponent = ({ children, href }) => {
12
+ const nextHref = typeof href === 'string' ? href : (href.pathname ?? '');
13
+
14
+ return <div data-next-href={nextHref}>{children}</div>;
15
+ };
16
+
17
+ const OwnLink: ComponentType<OwnLinkProps> = ({ children, ...props }) => <a {...props}>{children}</a>;
18
+
19
+ const Loading = () => <span data-testid="loading">Loading</span>;
20
+
21
+ Object.defineProperty(globalThis, 'IS_REACT_ACT_ENVIRONMENT', { configurable: true, value: true });
22
+
23
+ const mount = (element: ReactElement) => {
24
+ const container = document.createElement('div');
25
+ const root = createRoot(container);
26
+
27
+ act(() => {
28
+ root.render(element);
29
+ });
30
+
31
+ return {
32
+ container,
33
+ unmount: () => {
34
+ act(() => {
35
+ root.unmount();
36
+ });
37
+ },
38
+ };
39
+ };
40
+
41
+ const createLink = (props: ComponentProps<typeof Link>) => (
42
+ <LinkProvider
43
+ LoadingComponent={Loading}
44
+ NextLink={NextLinkMock}
45
+ OwnLink={OwnLink}
46
+ loadingTimeout={100}
47
+ pathname="/current"
48
+ >
49
+ <Link {...props} />
50
+ </LinkProvider>
51
+ );
52
+
53
+ afterEach(() => {
54
+ jest.useRealTimers();
55
+ history.replaceState(undefined, '', '/');
56
+ });
57
+
58
+ describe('Link integration', () => {
59
+ it('When a link is clicked and navigation exceeds the timeout, then it displays the loading component', () => {
60
+ // Arrange
61
+ history.replaceState(undefined, '', '/current');
62
+ Object.defineProperty(globalThis, 'scroll', { configurable: true, value: jest.fn() });
63
+ jest.useFakeTimers();
64
+
65
+ const { container, unmount } = mount(createLink({ children: 'Next page', href: '/next' }));
66
+ const anchor = container.querySelector('a');
67
+
68
+ // Act
69
+ act(() => {
70
+ anchor?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
71
+ jest.advanceTimersByTime(100);
72
+ });
73
+
74
+ // Assert
75
+ expect(container.querySelector('[data-testid="loading"]')?.textContent).toBe('Loading');
76
+ expect(container.querySelector<HTMLElement>('[data-next-href]')?.dataset.nextHref).toMatch(/^\/next\?y=/);
77
+
78
+ unmount();
79
+ });
80
+
81
+ it('When a click is prevented, then it does not display the loading component', () => {
82
+ // Arrange
83
+ history.replaceState(undefined, '', '/current');
84
+ Object.defineProperty(globalThis, 'scroll', { configurable: true, value: jest.fn() });
85
+ jest.useFakeTimers();
86
+
87
+ const { container, unmount } = mount(
88
+ createLink({
89
+ children: 'Prevented navigation',
90
+ href: '/next',
91
+ onClick: event => {
92
+ event.preventDefault();
93
+ },
94
+ }),
95
+ );
96
+
97
+ const anchor = container.querySelector('a');
98
+
99
+ // Act
100
+ act(() => {
101
+ anchor?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
102
+ jest.advanceTimersByTime(100);
103
+ });
104
+
105
+ // Assert
106
+ expect(container.querySelector('[data-testid="loading"]')).toBeNull();
107
+
108
+ unmount();
109
+ });
110
+
111
+ it('When router cache control is disabled, then it passes the original href to NextLink', () => {
112
+ // Arrange
113
+ history.replaceState(undefined, '', '/current');
114
+ Object.defineProperty(globalThis, 'scroll', { configurable: true, value: jest.fn() });
115
+
116
+ const { container, unmount } = mount(
117
+ createLink({ children: 'Current page', disableRouterCache: false, href: '/current' }),
118
+ );
119
+
120
+ // Act
121
+ const href = container.querySelector<HTMLElement>('[data-next-href]')?.dataset.nextHref;
122
+
123
+ // Assert
124
+ expect(href).toBe('/current');
125
+
126
+ unmount();
127
+ });
128
+
129
+ it('When a pending navigation timer reaches an unmounted provider, then it has been cleared', () => {
130
+ // Arrange
131
+ history.replaceState(undefined, '', '/current');
132
+ Object.defineProperty(globalThis, 'scroll', { configurable: true, value: jest.fn() });
133
+ jest.useFakeTimers();
134
+
135
+ const { container, unmount } = mount(createLink({ children: 'Next page', href: '/next' }));
136
+ const anchor = container.querySelector('a');
137
+
138
+ act(() => {
139
+ anchor?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
140
+ });
141
+
142
+ // Act
143
+ unmount();
144
+
145
+ // Assert
146
+ expect(jest.getTimerCount()).toBe(0);
147
+ });
148
+
149
+ it('When query parameters are reordered, then the URL matches the current location', () => {
150
+ // Arrange
151
+ history.replaceState(undefined, '', '/current?alpha=one&bravo=two&y=cache');
152
+
153
+ // Act
154
+ const isMatch = shouldUrlAndLocationMatch('/current?bravo=two&alpha=one&y=other');
155
+
156
+ // Assert
157
+ expect(isMatch).toBe(true);
158
+ });
159
+ });