diff --git a/app/javascript/shared/helpers/joinUrl.js b/app/javascript/shared/helpers/joinUrl.js new file mode 100644 index 000000000..ea7e80a65 --- /dev/null +++ b/app/javascript/shared/helpers/joinUrl.js @@ -0,0 +1,30 @@ +/** + * Join multiple paths together with a base URL + * NOTE: This function is not designed to handle query strings or fragments + * + * @param {string} baseUrl - The base URL to join the paths to + * @param {...string} paths - The paths to join + * @returns {string} - The full URL + */ +export function joinUrl(baseUrl, ...paths) { + // remove empty undefined and null path items + // also handle if the path is just a slash or just multiple slashes only + const sanitizedPaths = paths.filter(path => { + if (!path) return false; + if (path === null && path === '') return false; + // if path is just a sequence of slashes + if (/^\/+$/.test(path)) return false; + + return true; + }); + + const fullUrl = sanitizedPaths.reduce( + (acc, path) => { + const sanitizedPath = path.replace(/^\/+|\/+$/g, ''); // Remove leading and trailing slashes from each path segment + return `${acc}/${sanitizedPath}`; // Concatenate with a single slash + }, + baseUrl.replace(/\/+$/, '') + ); + + return fullUrl; +} diff --git a/app/javascript/shared/helpers/specs/joinUrl.spec.js b/app/javascript/shared/helpers/specs/joinUrl.spec.js new file mode 100644 index 000000000..52542170a --- /dev/null +++ b/app/javascript/shared/helpers/specs/joinUrl.spec.js @@ -0,0 +1,59 @@ +import { joinUrl } from '../joinUrl'; + +describe('joinUrl', () => { + it('should correctly join base URL and multiple paths', () => { + expect(joinUrl('http://example.com', 'path1', 'path2', 'path3')).toBe( + 'http://example.com/path1/path2/path3' + ); + }); + + it('should handle trailing slashes on the base URL', () => { + expect(joinUrl('http://example.com/', 'path1', 'path2')).toBe( + 'http://example.com/path1/path2' + ); + }); + + it('should handle leading and trailing slashes on path segments', () => { + expect(joinUrl('http://example.com', '/path1/', '/path2/')).toBe( + 'http://example.com/path1/path2' + ); + }); + + it('should handle a mix of slashes in base URL and paths', () => { + expect(joinUrl('http://example.com/', '/path1/', '/path2/')).toBe( + 'http://example.com/path1/path2' + ); + }); + + it('should return the base URL if no paths are provided', () => { + expect(joinUrl('http://example.com')).toBe('http://example.com'); + }); + + it('should handle empty path segments', () => { + expect(joinUrl('http://example.com', '', 'path1', '', 'path2')).toBe( + 'http://example.com/path1/path2' + ); + }); + + it('should handle paths that are just slashes', () => { + expect(joinUrl('http://example.com', '/', '//', '/path1/', '/')).toBe( + 'http://example.com/path1' + ); + }); + + it('should handle null and undefined paths', () => { + expect( + joinUrl('http://example.com', null, 'path1', undefined, 'path2') + ).toBe('http://example.com/path1/path2'); + }); + + it('should handle paths with multiple consecutive slashes', () => { + expect(joinUrl('http://example.com', '///path1///', 'path2///')).toBe( + 'http://example.com/path1/path2' + ); + }); + + it('should handle only slashes in base URL and paths', () => { + expect(joinUrl('http://example.com/', '/', '/')).toBe('http://example.com'); + }); +});