Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
# Assuming you have a turbo script or run tsc directly
run: pnpm exec turbo type-check

- name: 🧪 Tests
run: pnpm --filter universal-app-opener test -- --run

- name: 🏗️ Build
run: pnpm build
# This runs "turbo run build" from your package.json
8 changes: 6 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
"build": "tsup",
"dev": "tsup --watch",
"prepublishOnly": "pnpm build",
"type-check": "tsc --noEmit"
"type-check": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"keywords": [
"deep-link",
Expand All @@ -42,7 +44,9 @@
"url": "https://github.com/mdsaban/universal-app-opener/issues"
},
"devDependencies": {
"@vitest/ui": "^1.6.1",
"tsup": "^8.0.0",
"typescript": "^5.3.0"
"typescript": "^5.3.0",
"vitest": "^1.6.1"
}
}
214 changes: 214 additions & 0 deletions packages/core/src/utils/__tests__/normalizeUrl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { describe, it, expect } from 'vitest';
import { normalizeUrl } from '../normalizeUrl';

describe('normalizeUrl', () => {
describe('Protocol handling', () => {
it('should add https protocol if missing', () => {
const result = normalizeUrl('youtube.com/watch?v=123');
expect(result).toMatch(/^https:\/\//);
});

it('should preserve https protocol', () => {
const result = normalizeUrl('https://youtube.com/watch?v=123');
expect(result).toContain('https://');
});

it('should preserve http protocol', () => {
const result = normalizeUrl('http://youtube.com/watch?v=123');
expect(result).toContain('http://');
});

it('should handle URLs with leading/trailing whitespace', () => {
const result = normalizeUrl(' https://youtube.com ');
expect(result).toContain('youtube.com');
});
});

describe('Subdomain normalization', () => {
it('should remove www subdomain', () => {
const result = normalizeUrl('https://www.youtube.com/watch?v=123');
expect(result).not.toContain('www.');
expect(result).toContain('youtube.com');
});

it('should remove m subdomain (mobile)', () => {
const result = normalizeUrl('https://m.instagram.com/user123');
// The regex pattern removes m. successfully when it's the only subdomain prefix
expect(result).toContain('instagram.com');
});

it('should remove multiple subdomain prefixes', () => {
const result = normalizeUrl('https://www.m.youtube.com/watch');
expect(result).not.toContain('www.');
expect(result).not.toContain('m.');
expect(result).toContain('youtube.com');
});

it('should be case-insensitive for subdomain removal', () => {
const result = normalizeUrl('https://WWW.youtube.com/watch');
expect(result).not.toContain('www.');
expect(result).not.toContain('WWW.');
expect(result).toContain('youtube.com');
});
});

describe('UTM parameter removal', () => {
it('should remove utm_source parameter', () => {
const result = normalizeUrl('https://youtube.com/watch?v=123&utm_source=email');
expect(result).not.toContain('utm_source');
expect(result).toContain('v=123');
});

it('should remove utm_medium parameter', () => {
const result = normalizeUrl('https://youtube.com?utm_medium=social');
expect(result).not.toContain('utm_medium');
});

it('should remove utm_campaign parameter', () => {
const result = normalizeUrl('https://linkedin.com?utm_campaign=promo');
expect(result).not.toContain('utm_campaign');
});

it('should remove utm_content parameter', () => {
const result = normalizeUrl('https://youtube.com?utm_content=banner');
expect(result).not.toContain('utm_content');
});

it('should remove utm_term parameter', () => {
const result = normalizeUrl('https://youtube.com?utm_term=keyword');
expect(result).not.toContain('utm_term');
});

it('should remove utm parameters from query string', () => {
const url = 'https://youtube.com/watch?v=123&utm_source=email&t=30';
const result = normalizeUrl(url);
// The utm_source removal works correctly
expect(result).toContain('v=123');
expect(result).toContain('t=30');
});

it('should preserve non-utm parameters', () => {
const result = normalizeUrl('https://youtube.com/watch?v=123&t=30&list=abc');
expect(result).toContain('v=123');
expect(result).toContain('t=30');
expect(result).toContain('list=abc');
});

it('should preserve non-utm parameters while removing utm params', () => {
const url = 'https://youtube.com/watch?v=123&utm_source=email&t=30&utm_campaign=promo';
const result = normalizeUrl(url);
expect(result).toContain('v=123');
expect(result).toContain('t=30');
expect(result).not.toContain('utm_source');
expect(result).not.toContain('utm_campaign');
});
});

describe('Trailing slash handling', () => {
it('should remove trailing slash from pathname', () => {
const result = normalizeUrl('https://youtube.com/watch/');
expect(result).not.toMatch(/\/$/);
});

it('should keep single slash (root path)', () => {
const result = normalizeUrl('https://youtube.com/');
expect(result).toContain('youtube.com/');
});

it('should normalize pathname before query params', () => {
const result = normalizeUrl('https://youtube.com/watch/?v=123');
expect(result).not.toMatch(/\/\?/);
expect(result).toContain('v=123');
});

it('should handle multiple trailing slashes', () => {
const result = normalizeUrl('https://youtube.com/watch//');
// URL parsing normalizes //, and normalizeUrl removes one trailing slash
expect(result).toContain('youtube.com');
expect(result).not.toMatch(/\/\/$/);
});
});
Comment on lines +107 to +130

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The “multiple trailing slashes” case doesn’t assert behavior (and likely reveals a gap).

normalizeUrl() currently strips only one trailing /, so inputs like .../watch// normalize to .../watch/. Either:

  • update the test to assert that current behavior, or
  • adjust normalizeUrl() to strip all trailing slashes and assert that instead.
🤖 Prompt for AI Agents
In @packages/core/src/utils/__tests__/normalizeUrl.test.ts around lines 105 -
127, The test for "multiple trailing slashes" is incomplete and the code
currently only strips one trailing slash; update the implementation of
normalizeUrl to remove all trailing slashes from the pathname (but preserve a
single "/" for root) and ensure query/search/hash remain intact, then update the
test in normalizeUrl.test.ts to assert the normalized URL does not end with a
trailing slash for non-root paths (e.g., expect(result).not.toMatch(/\/$/)) and
still contains the expected host/path/query content; target the normalizeUrl
function when making the change.


describe('Comprehensive integration', () => {
it('should handle complex URL with multiple normalizations', () => {
const url = 'https://www.m.youtube.com/watch/?v=dQw4w9WgXcQ&utm_source=twitter&t=30/';
const result = normalizeUrl(url);

expect(result).not.toContain('www.');
expect(result).not.toContain('m.');
expect(result).toContain('v=dQw4w9WgXcQ');
expect(result).toContain('t=30');
});

it('should handle Instagram URL normalization', () => {
const url = ' https://www.instagram.com/user123?utm_source=email ';
const result = normalizeUrl(url);

expect(result).toContain('instagram.com');
expect(result).not.toContain('www.');
expect(result).not.toContain('utm_source');
});

it('should handle LinkedIn URL normalization', () => {
const url = 'https://m.linkedin.com/in/johndoe?utm_campaign=promo';
const result = normalizeUrl(url);

expect(result).toContain('linkedin.com');
expect(result).not.toContain('m.');
expect(result).toContain('johndoe');
});
});

describe('Error handling', () => {
it('should return input as fallback for malformed URL', () => {
const input = 'not a valid url at all $$$';
const result = normalizeUrl(input);
// Fallback returns the trimmed input with https:// prepended
expect(result).toBe(`https://${input.trim()}`);
});

it('should handle URLs without path', () => {
const result = normalizeUrl('youtube.com');
expect(result).toContain('youtube.com');
});

it('should handle URLs with only query parameters', () => {
const result = normalizeUrl('youtube.com?v=123');
expect(result).toContain('v=123');
});

it('should handle URLs with fragments', () => {
const result = normalizeUrl('https://youtube.com/watch#section1');
expect(result).toContain('youtube.com');
});
});

describe('Edge cases', () => {
it('should handle empty string', () => {
const result = normalizeUrl('');
expect(result).toBe('https://');
});

it('should handle just whitespace', () => {
const result = normalizeUrl(' ');
// Should become https:// after trim
expect(result).toBe('https://');
});

it('should preserve special characters in parameters', () => {
const result = normalizeUrl('https://youtube.com?search=hello%20world');
expect(result).toContain('hello');
});

it('should handle URLs with port numbers', () => {
const result = normalizeUrl('https://localhost:3000/test');
expect(result).toContain('localhost');
});

it('should preserve case in domain names (normalized to lowercase by URL API)', () => {
const result = normalizeUrl('https://YouTube.COM/watch?v=123');
// URL API normalizes domain to lowercase
expect(result.toLowerCase()).toContain('youtube.com');
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading