diff --git a/apps/site/hooks/react-client/__tests__/useNavigationState.test.mjs b/apps/site/hooks/react-client/__tests__/useNavigationState.test.mjs
new file mode 100644
index 0000000000000..b7ab1bded1258
--- /dev/null
+++ b/apps/site/hooks/react-client/__tests__/useNavigationState.test.mjs
@@ -0,0 +1,88 @@
+import { renderHook, act } from '@testing-library/react';
+import { useRef } from 'react';
+
+import useNavigationState from '@/hooks/react-client/useNavigationState';
+import { NavigationStateContext } from '@/providers/navigationStateProvider';
+
+describe('useNavigationState', () => {
+ it('should save and restore scroll position', () => {
+ const mockElement = {
+ scrollLeft: 0,
+ scrollTop: 0,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ scroll: jest.fn(),
+ };
+
+ const mockRef = { current: mockElement };
+ const mockContextValue = {};
+
+ const wrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ renderHook(() => useNavigationState('test-id', mockRef), { wrapper });
+
+ expect(mockElement.addEventListener).toHaveBeenCalledWith(
+ 'scroll',
+ expect.any(Function),
+ { passive: true }
+ );
+
+ act(() => {
+ mockElement.scrollTop = 100;
+ mockElement.scrollLeft = 50;
+ mockElement.addEventListener.mock.calls[0][1]();
+ });
+
+ expect(mockContextValue['test-id']).toEqual({ x: 50, y: 100 });
+
+ act(() => {
+ mockElement.scrollTop = 0;
+ mockElement.scrollLeft = 0;
+ mockElement.scroll.mock.calls[0][0]();
+ });
+
+ expect(mockElement.scroll).toHaveBeenCalledWith({
+ top: 100,
+ behavior: 'instant',
+ });
+ });
+
+ it('should add and remove scroll event listener', () => {
+ const mockElement = {
+ scrollLeft: 0,
+ scrollTop: 0,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+ };
+
+ const mockRef = { current: mockElement };
+ const mockContextValue = {};
+
+ const wrapper = ({ children }) => (
+
+ {children}
+
+ );
+
+ const { unmount } = renderHook(() => useNavigationState('test-id', mockRef), {
+ wrapper,
+ });
+
+ expect(mockElement.addEventListener).toHaveBeenCalledWith(
+ 'scroll',
+ expect.any(Function),
+ { passive: true }
+ );
+
+ unmount();
+
+ expect(mockElement.removeEventListener).toHaveBeenCalledWith(
+ 'scroll',
+ expect.any(Function)
+ );
+ });
+});
diff --git a/apps/site/reducers/__tests__/releaseReducer.test.mjs b/apps/site/reducers/__tests__/releaseReducer.test.mjs
new file mode 100644
index 0000000000000..21f92e2c93ee7
--- /dev/null
+++ b/apps/site/reducers/__tests__/releaseReducer.test.mjs
@@ -0,0 +1,74 @@
+import releaseReducer, { releaseState, getActions } from '@/reducers/releaseReducer';
+
+describe('releaseReducer', () => {
+ it('should return the initial state', () => {
+ expect(releaseReducer(undefined, {})).toEqual(releaseState);
+ });
+
+ it('should handle SET_VERSION', () => {
+ const action = { type: 'SET_VERSION', payload: 'v14.17.0' };
+ const expectedState = { ...releaseState, version: 'v14.17.0' };
+ expect(releaseReducer(releaseState, action)).toEqual(expectedState);
+ });
+
+ it('should handle SET_OS', () => {
+ const action = { type: 'SET_OS', payload: 'WIN' };
+ const expectedState = { ...releaseState, os: 'WIN' };
+ expect(releaseReducer(releaseState, action)).toEqual(expectedState);
+ });
+
+ it('should handle SET_PLATFORM', () => {
+ const action = { type: 'SET_PLATFORM', payload: 'x64' };
+ const expectedState = { ...releaseState, platform: 'x64' };
+ expect(releaseReducer(releaseState, action)).toEqual(expectedState);
+ });
+
+ it('should handle SET_INSTALL_METHOD', () => {
+ const action = { type: 'SET_INSTALL_METHOD', payload: 'brew' };
+ const expectedState = { ...releaseState, installMethod: 'brew' };
+ expect(releaseReducer(releaseState, action)).toEqual(expectedState);
+ });
+
+ it('should handle SET_MANAGER', () => {
+ const action = { type: 'SET_MANAGER', payload: 'yarn' };
+ const expectedState = { ...releaseState, packageManager: 'yarn' };
+ expect(releaseReducer(releaseState, action)).toEqual(expectedState);
+ });
+});
+
+describe('getActions', () => {
+ it('should create setVersion action', () => {
+ const dispatch = jest.fn();
+ const actions = getActions(dispatch);
+ actions.setVersion('v14.17.0');
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VERSION', payload: 'v14.17.0' });
+ });
+
+ it('should create setOS action', () => {
+ const dispatch = jest.fn();
+ const actions = getActions(dispatch);
+ actions.setOS('WIN');
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_OS', payload: 'WIN' });
+ });
+
+ it('should create setPlatform action', () => {
+ const dispatch = jest.fn();
+ const actions = getActions(dispatch);
+ actions.setPlatform('x64');
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_PLATFORM', payload: 'x64' });
+ });
+
+ it('should create setInstallMethod action', () => {
+ const dispatch = jest.fn();
+ const actions = getActions(dispatch);
+ actions.setInstallMethod('brew');
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_INSTALL_METHOD', payload: 'brew' });
+ });
+
+ it('should create setPackageManager action', () => {
+ const dispatch = jest.fn();
+ const actions = getActions(dispatch);
+ actions.setPackageManager('yarn');
+ expect(dispatch).toHaveBeenCalledWith({ type: 'SET_MANAGER', payload: 'yarn' });
+ });
+});
diff --git a/apps/site/util/__tests__/assignClientContext.test.mjs b/apps/site/util/__tests__/assignClientContext.test.mjs
index 48ba4094f8643..83cc33133017a 100644
--- a/apps/site/util/__tests__/assignClientContext.test.mjs
+++ b/apps/site/util/__tests__/assignClientContext.test.mjs
@@ -25,6 +25,9 @@ describe('assignClientContext', () => {
expect(result.headings).toEqual(mockContext.headings);
expect(result.readingTime).toEqual(mockContext.readingTime);
expect(result.filename).toEqual(mockContext.filename);
+ expect(result.os).toEqual(mockContext.os);
+ expect(result.architecture).toEqual(mockContext.architecture);
+ expect(result.bitness).toEqual(mockContext.bitness);
expect(result).toEqual(mockContext);
});
@@ -42,5 +45,26 @@ describe('assignClientContext', () => {
words: 0,
});
expect(result.filename).toEqual('');
+ expect(result.os).toEqual('OTHER');
+ expect(result.architecture).toEqual('x64');
+ expect(result.bitness).toEqual(64);
+ });
+
+ it('should handle invalid inputs gracefully', () => {
+ const result = assignClientContext(null);
+
+ expect(result.frontmatter).toEqual({});
+ expect(result.pathname).toEqual('');
+ expect(result.headings).toEqual([]);
+ expect(result.readingTime).toEqual({
+ text: '',
+ minutes: 0,
+ time: 0,
+ words: 0,
+ });
+ expect(result.filename).toEqual('');
+ expect(result.os).toEqual('OTHER');
+ expect(result.architecture).toEqual('x64');
+ expect(result.bitness).toEqual(64);
});
});
diff --git a/apps/site/util/__tests__/authorUtils.test.mjs b/apps/site/util/__tests__/authorUtils.test.mjs
index 7a5a41d35db3a..12db3bb7addf2 100644
--- a/apps/site/util/__tests__/authorUtils.test.mjs
+++ b/apps/site/util/__tests__/authorUtils.test.mjs
@@ -2,6 +2,9 @@ import {
mapAuthorToCardAuthors,
getAuthorWithId,
getAuthorWithName,
+ getAuthorName,
+ getAuthorBio,
+ getAuthorImage,
} from '../authorUtils';
describe('mapAuthorToCardAuthors', () => {
@@ -96,3 +99,45 @@ describe('getAuthorWithName', () => {
]);
});
});
+
+describe('getAuthorName', () => {
+ it('should return the correct author name', () => {
+ const authorId = 'tjfontaine';
+ const result = getAuthorName(authorId);
+ expect(result).toBe('Timothy J Fontaine');
+ });
+
+ it('should return undefined for an unknown author', () => {
+ const authorId = 'unknown';
+ const result = getAuthorName(authorId);
+ expect(result).toBeUndefined();
+ });
+});
+
+describe('getAuthorBio', () => {
+ it('should return the correct author bio', () => {
+ const authorId = 'tjfontaine';
+ const result = getAuthorBio(authorId);
+ expect(result).toBeUndefined(); // Assuming no bio is provided in the authors.json
+ });
+
+ it('should return undefined for an unknown author', () => {
+ const authorId = 'unknown';
+ const result = getAuthorBio(authorId);
+ expect(result).toBeUndefined();
+ });
+});
+
+describe('getAuthorImage', () => {
+ it('should return the correct author image URL', () => {
+ const authorId = 'tjfontaine';
+ const result = getAuthorImage(authorId);
+ expect(result).toBe('https://avatars.githubusercontent.com/tjfontaine');
+ });
+
+ it('should return undefined for an unknown author', () => {
+ const authorId = 'unknown';
+ const result = getAuthorImage(authorId);
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/apps/site/util/__tests__/blogUtils.test.mjs b/apps/site/util/__tests__/blogUtils.test.mjs
new file mode 100644
index 0000000000000..a5cfb806db5e5
--- /dev/null
+++ b/apps/site/util/__tests__/blogUtils.test.mjs
@@ -0,0 +1,27 @@
+import { getBlogPosts, getBlogCategories, getBlogTags } from '@/util/blogUtils';
+
+describe('blogUtils', () => {
+ describe('getBlogPosts', () => {
+ it('should retrieve blog posts', () => {
+ const blogPosts = getBlogPosts();
+ expect(blogPosts).toBeDefined();
+ expect(Array.isArray(blogPosts)).toBe(true);
+ });
+ });
+
+ describe('getBlogCategories', () => {
+ it('should retrieve blog categories', () => {
+ const blogCategories = getBlogCategories();
+ expect(blogCategories).toBeDefined();
+ expect(Array.isArray(blogCategories)).toBe(true);
+ });
+ });
+
+ describe('getBlogTags', () => {
+ it('should retrieve blog tags', () => {
+ const blogTags = getBlogTags();
+ expect(blogTags).toBeDefined();
+ expect(Array.isArray(blogTags)).toBe(true);
+ });
+ });
+});
diff --git a/apps/site/util/__tests__/dateUtils.test.mjs b/apps/site/util/__tests__/dateUtils.test.mjs
index 5fe75341814bb..8b9059fade69e 100644
--- a/apps/site/util/__tests__/dateUtils.test.mjs
+++ b/apps/site/util/__tests__/dateUtils.test.mjs
@@ -1,4 +1,4 @@
-import { dateIsBetween } from '@/util/dateUtils';
+import { dateIsBetween, formatDate, parseDate, isValidDate } from '@/util/dateUtils';
describe('dateIsBetween', () => {
it('returns true when the current date is between start and end dates', () => {
@@ -28,3 +28,41 @@ describe('dateIsBetween', () => {
);
});
});
+
+describe('formatDate', () => {
+ it('formats a valid date correctly', () => {
+ const date = new Date('2024-02-17T00:00:00.000Z');
+ const formattedDate = formatDate(date, 'yyyy-MM-dd');
+ expect(formattedDate).toBe('2024-02-17');
+ });
+
+ it('throws an error for an invalid date', () => {
+ const invalidDate = new Date('Invalid Date');
+ expect(() => formatDate(invalidDate, 'yyyy-MM-dd')).toThrow('Invalid date');
+ });
+});
+
+describe('parseDate', () => {
+ it('parses a valid date string correctly', () => {
+ const dateString = '2024-02-17';
+ const parsedDate = parseDate(dateString, 'yyyy-MM-dd');
+ expect(parsedDate).toEqual(new Date('2024-02-17T00:00:00.000Z'));
+ });
+
+ it('throws an error for an invalid date string', () => {
+ const invalidDateString = 'Invalid Date';
+ expect(() => parseDate(invalidDateString, 'yyyy-MM-dd')).toThrow('Invalid date string');
+ });
+});
+
+describe('isValidDate', () => {
+ it('returns true for a valid date', () => {
+ const validDate = new Date('2024-02-17T00:00:00.000Z');
+ expect(isValidDate(validDate)).toBe(true);
+ });
+
+ it('returns false for an invalid date', () => {
+ const invalidDate = new Date('Invalid Date');
+ expect(isValidDate(invalidDate)).toBe(false);
+ });
+});
diff --git a/apps/site/util/__tests__/debounce.test.mjs b/apps/site/util/__tests__/debounce.test.mjs
index 401feb699b65c..e55929cc25e18 100644
--- a/apps/site/util/__tests__/debounce.test.mjs
+++ b/apps/site/util/__tests__/debounce.test.mjs
@@ -28,4 +28,33 @@ describe('debounce', () => {
expect(fn).toHaveBeenCalledWith(3);
});
+
+ it('should delay the execution of the function', () => {
+ const fn = jest.fn();
+ const debouncedFn = debounce(fn, 1000);
+
+ debouncedFn();
+
+ expect(fn).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(500);
+ expect(fn).not.toHaveBeenCalled();
+
+ jest.advanceTimersByTime(500);
+ expect(fn).toHaveBeenCalled();
+ });
+
+ it('should execute only once within the delay', () => {
+ const fn = jest.fn();
+ const debouncedFn = debounce(fn, 1000);
+
+ debouncedFn();
+ jest.advanceTimersByTime(500);
+ debouncedFn();
+ jest.advanceTimersByTime(500);
+ debouncedFn();
+ jest.advanceTimersByTime(1000);
+
+ expect(fn).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/apps/site/util/__tests__/detectOS.test.mjs b/apps/site/util/__tests__/detectOS.test.mjs
index 9e006f96144ee..8c06ba518cd8c 100644
--- a/apps/site/util/__tests__/detectOS.test.mjs
+++ b/apps/site/util/__tests__/detectOS.test.mjs
@@ -1,4 +1,4 @@
-import { detectOsInUserAgent } from '@/util/detectOS';
+import { detectOS, detectOsInUserAgent } from '@/util/detectOS';
describe('detectOsInUserAgent', () => {
it.each([
@@ -29,3 +29,37 @@ describe('detectOsInUserAgent', () => {
expect(detectOsInUserAgent(os)).toBe(expected);
});
});
+
+describe('detectOS', () => {
+ it('should detect Windows OS', () => {
+ Object.defineProperty(global.navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246',
+ configurable: true,
+ });
+ expect(detectOS()).toBe('WIN');
+ });
+
+ it('should detect Mac OS', () => {
+ Object.defineProperty(global.navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36',
+ configurable: true,
+ });
+ expect(detectOS()).toBe('MAC');
+ });
+
+ it('should detect Linux OS', () => {
+ Object.defineProperty(global.navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1',
+ configurable: true,
+ });
+ expect(detectOS()).toBe('LINUX');
+ });
+
+ it('should detect unknown OS', () => {
+ Object.defineProperty(global.navigator, 'userAgent', {
+ value: 'Mozilla/5.0 (X11; CrOS x86_64 8172.45.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.64 Safari/537.36',
+ configurable: true,
+ });
+ expect(detectOS()).toBe('OTHER');
+ });
+});
diff --git a/apps/site/util/__tests__/getHighEntropyValues.test.mjs b/apps/site/util/__tests__/getHighEntropyValues.test.mjs
new file mode 100644
index 0000000000000..7a13835578179
--- /dev/null
+++ b/apps/site/util/__tests__/getHighEntropyValues.test.mjs
@@ -0,0 +1,64 @@
+import { getHighEntropyValues } from '@/util/getHighEntropyValues';
+
+describe('getHighEntropyValues', () => {
+ it('should retrieve high entropy values', async () => {
+ const mockGetHighEntropyValues = jest.fn().mockResolvedValue({
+ bitness: '64',
+ architecture: 'x86',
+ });
+
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: {
+ getHighEntropyValues: mockGetHighEntropyValues,
+ },
+ writable: true,
+ });
+
+ const result = await getHighEntropyValues(['bitness', 'architecture']);
+
+ expect(result).toEqual({
+ bitness: '64',
+ architecture: 'x86',
+ });
+ expect(mockGetHighEntropyValues).toHaveBeenCalledWith(['bitness', 'architecture']);
+ });
+
+ it('should handle missing high entropy values gracefully', async () => {
+ const mockGetHighEntropyValues = jest.fn().mockResolvedValue({});
+
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: {
+ getHighEntropyValues: mockGetHighEntropyValues,
+ },
+ writable: true,
+ });
+
+ const result = await getHighEntropyValues(['bitness', 'architecture']);
+
+ expect(result).toEqual({
+ bitness: undefined,
+ architecture: undefined,
+ });
+ expect(mockGetHighEntropyValues).toHaveBeenCalledWith(['bitness', 'architecture']);
+ });
+
+ it('should handle absence of getHighEntropyValues method gracefully', async () => {
+ Object.defineProperty(navigator, 'userAgentData', {
+ value: {},
+ writable: true,
+ });
+
+ const result = await getHighEntropyValues(['bitness', 'architecture']);
+
+ expect(result).toEqual({
+ bitness: undefined,
+ architecture: undefined,
+ });
+ });
+
+ it('should handle invalid inputs gracefully', async () => {
+ const result = await getHighEntropyValues(null);
+
+ expect(result).toEqual({});
+ });
+});
diff --git a/apps/site/util/__tests__/getHighlighter.test.mjs b/apps/site/util/__tests__/getHighlighter.test.mjs
new file mode 100644
index 0000000000000..39776c98232c7
--- /dev/null
+++ b/apps/site/util/__tests__/getHighlighter.test.mjs
@@ -0,0 +1,33 @@
+import { highlightToHtml, highlightToHast } from '@/util/getHighlighter';
+
+describe('highlightToHtml', () => {
+ it('should return highlighted HTML for valid code and language', () => {
+ const code = 'const x = 10;';
+ const language = 'javascript';
+ const result = highlightToHtml(code, language);
+ expect(result).toContain('');
+ });
+
+ it('should handle invalid inputs gracefully', () => {
+ const code = 'const x = 10;';
+ const language = 'invalidLanguage';
+ const result = highlightToHtml(code, language);
+ expect(result).toContain('');
+ });
+});
+
+describe('highlightToHast', () => {
+ it('should return highlighted HAST for valid code and language', () => {
+ const code = 'const x = 10;';
+ const language = 'javascript';
+ const result = highlightToHast(code, language);
+ expect(result).toHaveProperty('type', 'root');
+ });
+
+ it('should handle invalid inputs gracefully', () => {
+ const code = 'const x = 10;';
+ const language = 'invalidLanguage';
+ const result = highlightToHast(code, language);
+ expect(result).toHaveProperty('type', 'root');
+ });
+});
diff --git a/apps/site/util/__tests__/getLanguageDisplayName.test.mjs b/apps/site/util/__tests__/getLanguageDisplayName.test.mjs
new file mode 100644
index 0000000000000..9bd50351d7040
--- /dev/null
+++ b/apps/site/util/__tests__/getLanguageDisplayName.test.mjs
@@ -0,0 +1,23 @@
+import { getLanguageDisplayName } from '@/util/getLanguageDisplayName';
+
+describe('getLanguageDisplayName', () => {
+ it('should return the display name for a valid language', () => {
+ const result = getLanguageDisplayName('javascript');
+ expect(result).toBe('JavaScript');
+ });
+
+ it('should return the input language if no display name is found', () => {
+ const result = getLanguageDisplayName('unknownLanguage');
+ expect(result).toBe('unknownLanguage');
+ });
+
+ it('should handle case-insensitive language input', () => {
+ const result = getLanguageDisplayName('JAVASCRIPT');
+ expect(result).toBe('JavaScript');
+ });
+
+ it('should return the display name for a valid language alias', () => {
+ const result = getLanguageDisplayName('js');
+ expect(result).toBe('JavaScript');
+ });
+});
diff --git a/apps/site/util/__tests__/getNodeApiLink.test.mjs b/apps/site/util/__tests__/getNodeApiLink.test.mjs
index 5317855c62aa7..77adf948e1b91 100644
--- a/apps/site/util/__tests__/getNodeApiLink.test.mjs
+++ b/apps/site/util/__tests__/getNodeApiLink.test.mjs
@@ -36,4 +36,13 @@ describe('getNodeApiLink', () => {
expect(result).toBe(expectedLink);
});
+
+ it('returns the correct API link for invalid versions', () => {
+ const version = 'invalid';
+ const expectedLink = `https://nodejs.org/dist/${version}/docs/api/`;
+
+ const result = getNodeApiLink(version);
+
+ expect(result).toBe(expectedLink);
+ });
});
diff --git a/apps/site/util/__tests__/getNodeDownloadUrl.test.mjs b/apps/site/util/__tests__/getNodeDownloadUrl.test.mjs
index c95fa48e66126..f11a5fb475a45 100644
--- a/apps/site/util/__tests__/getNodeDownloadUrl.test.mjs
+++ b/apps/site/util/__tests__/getNodeDownloadUrl.test.mjs
@@ -5,35 +5,80 @@ const version = 'v18.16.0';
describe('getNodeDownloadUrl', () => {
it('returns the correct download URL for Mac', () => {
const os = 'MAC';
- const bitness = 86;
+ const platform = 'x64';
const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.pkg';
- expect(getNodeDownloadUrl(version, os, bitness)).toBe(expectedUrl);
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
});
it('returns the correct download URL for Windows (32-bit)', () => {
const os = 'WIN';
- const bitness = 86;
+ const platform = 'x86';
const expectedUrl =
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x86.msi';
- expect(getNodeDownloadUrl(version, os, bitness)).toBe(expectedUrl);
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
});
it('returns the correct download URL for Windows (64-bit)', () => {
const os = 'WIN';
- const bitness = 64;
+ const platform = 'x64';
const expectedUrl =
'https://nodejs.org/dist/v18.16.0/node-v18.16.0-x64.msi';
- expect(getNodeDownloadUrl(version, os, bitness)).toBe(expectedUrl);
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
+ });
+
+ it('returns the correct download URL for Linux (ARMv7)', () => {
+ const os = 'LINUX';
+ const platform = 'armv7l';
+ const expectedUrl =
+ 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-armv7l.tar.xz';
+
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
+ });
+
+ it('returns the correct download URL for Linux (ARMv8)', () => {
+ const os = 'LINUX';
+ const platform = 'arm64';
+ const expectedUrl =
+ 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-linux-arm64.tar.xz';
+
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
+ });
+
+ it('returns the correct download URL for AIX', () => {
+ const os = 'AIX';
+ const platform = 'ppc64';
+ const expectedUrl =
+ 'https://nodejs.org/dist/v18.16.0/node-v18.16.0-aix-ppc64.tar.gz';
+
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
});
it('returns the default download URL for other operating systems', () => {
const os = 'OTHER';
- const bitness = 86;
+ const platform = 'x64';
+ const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.tar.gz';
+
+ expect(getNodeDownloadUrl(version, os, platform)).toBe(expectedUrl);
+ });
+
+ it('returns the correct download URL for source code', () => {
+ const os = 'OTHER';
+ const platform = 'x64';
+ const kind = 'source';
const expectedUrl = 'https://nodejs.org/dist/v18.16.0/node-v18.16.0.tar.gz';
- expect(getNodeDownloadUrl(version, os, bitness)).toBe(expectedUrl);
+ expect(getNodeDownloadUrl(version, os, platform, kind)).toBe(expectedUrl);
+ });
+
+ it('returns the correct download URL for invalid versions', () => {
+ const invalidVersion = 'invalid';
+ const os = 'OTHER';
+ const platform = 'x64';
+ const expectedUrl = 'https://nodejs.org/dist/invalid/node-invalid.tar.gz';
+
+ expect(getNodeDownloadUrl(invalidVersion, os, platform)).toBe(expectedUrl);
});
});
diff --git a/apps/site/util/__tests__/getNodeJsChangelog.test.mjs b/apps/site/util/__tests__/getNodeJsChangelog.test.mjs
index d225ee21179fe..8c4b7af528145 100644
--- a/apps/site/util/__tests__/getNodeJsChangelog.test.mjs
+++ b/apps/site/util/__tests__/getNodeJsChangelog.test.mjs
@@ -42,7 +42,17 @@ describe('getNodeJsChangelog', () => {
it('returns the correct changelog URL for other versions', () => {
const version = '0.12.7';
const expectedUrl =
- 'https://github.com/nodejs/node/blob/main/doc/changelogs/CHANGELOG_V012.md#0.12.7';
+ 'https://github.com/nodejs/node-v0.x-archive/blob/0.12.7/ChangeLog';
+
+ const result = getNodeJsChangelog(version);
+
+ expect(result).toBe(expectedUrl);
+ });
+
+ it('returns the correct changelog URL for invalid versions', () => {
+ const version = 'invalid';
+ const expectedUrl =
+ 'https://github.com/nodejs/node-v0.x-archive/blob/invalid/ChangeLog';
const result = getNodeJsChangelog(version);
diff --git a/apps/site/util/__tests__/getUserPlatform.test.mjs b/apps/site/util/__tests__/getUserPlatform.test.mjs
new file mode 100644
index 0000000000000..77d89aa4d0c5f
--- /dev/null
+++ b/apps/site/util/__tests__/getUserPlatform.test.mjs
@@ -0,0 +1,23 @@
+import { getUserPlatform } from '@/util/getUserPlatform';
+
+describe('getUserPlatform', () => {
+ it('should return arm64 for arm architecture and 64 bitness', () => {
+ const result = getUserPlatform('arm', '64');
+ expect(result).toBe('arm64');
+ });
+
+ it('should return x64 for 64 bitness', () => {
+ const result = getUserPlatform('', '64');
+ expect(result).toBe('x64');
+ });
+
+ it('should return x86 for 32 bitness', () => {
+ const result = getUserPlatform('', '32');
+ expect(result).toBe('x86');
+ });
+
+ it('should return x86 for unknown bitness', () => {
+ const result = getUserPlatform('', '');
+ expect(result).toBe('x86');
+ });
+});
diff --git a/apps/site/util/__tests__/gitHubUtils.test.mjs b/apps/site/util/__tests__/gitHubUtils.test.mjs
index a749a403af062..c8db04b5211ce 100644
--- a/apps/site/util/__tests__/gitHubUtils.test.mjs
+++ b/apps/site/util/__tests__/gitHubUtils.test.mjs
@@ -3,6 +3,8 @@ import {
createGitHubSlugger,
getGitHubBlobUrl,
getGitHubApiDocsUrl,
+ getGitHubUser,
+ getGitHubRepo,
} from '@/util/gitHubUtils';
describe('GitHub utils', () => {
@@ -30,4 +32,48 @@ describe('GitHub utils', () => {
'https://api.github.com/repos/nodejs/node/contents/doc/api?ref=assert';
expect(result).toBe(expected);
});
+
+ it('getGitHubUser returns the correct user data', async () => {
+ const mockUser = { login: 'octocat', id: 1 };
+ global.fetch = jest.fn(() =>
+ Promise.resolve({
+ json: () => Promise.resolve(mockUser),
+ })
+ );
+
+ const result = await getGitHubUser('octocat');
+ expect(result).toEqual(mockUser);
+ });
+
+ it('getGitHubRepo returns the correct repo data', async () => {
+ const mockRepo = { name: 'nodejs.org', id: 1 };
+ global.fetch = jest.fn(() =>
+ Promise.resolve({
+ json: () => Promise.resolve(mockRepo),
+ })
+ );
+
+ const result = await getGitHubRepo('nodejs/nodejs.org');
+ expect(result).toEqual(mockRepo);
+ });
+
+ it('getGitHubUser handles errors correctly', async () => {
+ global.fetch = jest.fn(() =>
+ Promise.reject(new Error('Failed to fetch'))
+ );
+
+ await expect(getGitHubUser('invalid-user')).rejects.toThrow(
+ 'Failed to fetch'
+ );
+ });
+
+ it('getGitHubRepo handles errors correctly', async () => {
+ global.fetch = jest.fn(() =>
+ Promise.reject(new Error('Failed to fetch'))
+ );
+
+ await expect(getGitHubRepo('invalid-repo')).rejects.toThrow(
+ 'Failed to fetch'
+ );
+ });
});
diff --git a/apps/site/util/__tests__/hexToRGBA.test.mjs b/apps/site/util/__tests__/hexToRGBA.test.mjs
index 2e7ca9e00b3d2..39c591d3e2a7f 100644
--- a/apps/site/util/__tests__/hexToRGBA.test.mjs
+++ b/apps/site/util/__tests__/hexToRGBA.test.mjs
@@ -14,4 +14,25 @@ describe('hexToRGBA', () => {
expect(rgbaColor).toBe('rgba(255, 255, 255, 0.5)');
});
+
+ it('should convert a hex color to an rgba color with default alpha', () => {
+ const hexColor = '#ff5733';
+ const rgbaColor = hexToRGBA(hexColor);
+
+ expect(rgbaColor).toBe('rgba(255, 87, 51, 0.9)');
+ });
+
+ it('should handle invalid hex color and return rgba(0, 0, 0, alpha)', () => {
+ const hexColor = '#zzzzzz';
+ const rgbaColor = hexToRGBA(hexColor, 0.5);
+
+ expect(rgbaColor).toBe('rgba(0, 0, 0, 0.5)');
+ });
+
+ it('should handle empty hex color and return rgba(0, 0, 0, alpha)', () => {
+ const hexColor = '';
+ const rgbaColor = hexToRGBA(hexColor, 0.5);
+
+ expect(rgbaColor).toBe('rgba(0, 0, 0, 0.5)');
+ });
});
diff --git a/apps/site/util/__tests__/imageUtils.test.mjs b/apps/site/util/__tests__/imageUtils.test.mjs
index 3e2ec00245ca1..b6d151a66e741 100644
--- a/apps/site/util/__tests__/imageUtils.test.mjs
+++ b/apps/site/util/__tests__/imageUtils.test.mjs
@@ -1,4 +1,4 @@
-import { isSvgImage } from '@/util/imageUtils';
+import { isSvgImage, resizeImage, cropImage } from '@/util/imageUtils';
describe('isSvgImage', () => {
const testCases = [
@@ -41,3 +41,47 @@ describe('isSvgImage', () => {
});
});
});
+
+describe('resizeImage', () => {
+ it('should resize the image correctly', () => {
+ const image = new Image();
+ image.src = 'https://nodejs.org/image.png';
+ const width = 100;
+ const height = 100;
+
+ const resizedImage = resizeImage(image, width, height);
+
+ expect(resizedImage.width).toBe(width);
+ expect(resizedImage.height).toBe(height);
+ });
+
+ it('should throw an error for invalid inputs', () => {
+ expect(() => resizeImage(null, 100, 100)).toThrow();
+ expect(() => resizeImage(new Image(), -100, 100)).toThrow();
+ expect(() => resizeImage(new Image(), 100, -100)).toThrow();
+ });
+});
+
+describe('cropImage', () => {
+ it('should crop the image correctly', () => {
+ const image = new Image();
+ image.src = 'https://nodejs.org/image.png';
+ const x = 10;
+ const y = 10;
+ const width = 50;
+ const height = 50;
+
+ const croppedImage = cropImage(image, x, y, width, height);
+
+ expect(croppedImage.width).toBe(width);
+ expect(croppedImage.height).toBe(height);
+ });
+
+ it('should throw an error for invalid inputs', () => {
+ expect(() => cropImage(null, 10, 10, 50, 50)).toThrow();
+ expect(() => cropImage(new Image(), -10, 10, 50, 50)).toThrow();
+ expect(() => cropImage(new Image(), 10, -10, 50, 50)).toThrow();
+ expect(() => cropImage(new Image(), 10, 10, -50, 50)).toThrow();
+ expect(() => cropImage(new Image(), 10, 10, 50, -50)).toThrow();
+ });
+});
diff --git a/apps/site/util/__tests__/stringUtils.test.mjs b/apps/site/util/__tests__/stringUtils.test.mjs
index 27e65f1b0355e..f4f9e4219ffe9 100644
--- a/apps/site/util/__tests__/stringUtils.test.mjs
+++ b/apps/site/util/__tests__/stringUtils.test.mjs
@@ -2,6 +2,9 @@ import {
getAcronymFromString,
parseRichTextIntoPlainText,
dashToCamelCase,
+ capitalize,
+ camelCase,
+ kebabCase,
} from '@/util/stringUtils';
describe('String utils', () => {
@@ -72,4 +75,28 @@ describe('String utils', () => {
it('dashToCamelCase returns correct camelCase with numbers', () => {
expect(dashToCamelCase('foo-123-bar')).toBe('foo123Bar');
});
+
+ it('capitalize returns correct capitalized string', () => {
+ expect(capitalize('hello world')).toBe('Hello world');
+ });
+
+ it('camelCase returns correct camelCase string', () => {
+ expect(camelCase('Hello World')).toBe('helloWorld');
+ });
+
+ it('kebabCase returns correct kebab-case string', () => {
+ expect(kebabCase('Hello World')).toBe('hello-world');
+ });
+
+ it('capitalize handles empty string', () => {
+ expect(capitalize('')).toBe('');
+ });
+
+ it('camelCase handles empty string', () => {
+ expect(camelCase('')).toBe('');
+ });
+
+ it('kebabCase handles empty string', () => {
+ expect(kebabCase('')).toBe('');
+ });
});