getScheme())->toBe('https'); expect($url->getHost())->toBe('example.com'); expect($url->getPath())->toBe('/path'); }); it('parses URLs with all components', function () { $url = WhatwgUrl::parse('https://user:pass@example.com:8080/path?query=1#fragment'); expect($url->getScheme())->toBe('https'); expect($url->getUserInfo())->toBe('user:pass'); expect($url->getHost())->toBe('example.com'); expect($url->getPort())->toBe(8080); expect($url->getPath())->toBe('/path'); expect($url->getQuery())->toBe('query=1'); expect($url->getFragment())->toBe('fragment'); }); it('handles default ports correctly', function () { $url = WhatwgUrl::parse('https://example.com/path'); expect($url->getPort())->toBeNull(); }); it('parses relative URLs with base', function () { $base = WhatwgUrl::parse('https://example.com/base/path'); $url = WhatwgUrl::parse('../other', $base); expect($url->getPath())->toContain('other'); }); it('normalizes URLs according to WHATWG spec', function () { $url = WhatwgUrl::parse('https://example.com/./path/../other'); // WHATWG normalizes . and .. in paths expect($url->getPath())->toBe('/other'); }); }); describe('spec compliance', function () { it('reports WHATWG spec', function () { $url = WhatwgUrl::parse('https://example.com'); expect($url->getSpec())->toBe(UrlSpec::WHATWG); expect($url->getSpec()->isWhatwg())->toBeTrue(); expect($url->getSpec()->isRfc3986())->toBeFalse(); }); }); describe('immutable withers', function () { it('returns new instance with different scheme', function () { $original = WhatwgUrl::parse('https://example.com'); $modified = $original->withScheme('http'); expect($original->getScheme())->toBe('https'); expect($modified->getScheme())->toBe('http'); expect(spl_object_id($original))->notToBe(spl_object_id($modified)); }); it('returns new instance with different host', function () { $original = WhatwgUrl::parse('https://example.com'); $modified = $original->withHost('app.example.com'); expect($original->getHost())->toBe('example.com'); expect($modified->getHost())->toBe('app.example.com'); }); it('returns new instance with different port', function () { $original = WhatwgUrl::parse('https://example.com'); $modified = $original->withPort(8080); expect($original->getPort())->toBeNull(); expect($modified->getPort())->toBe(8080); }); it('returns new instance with different path', function () { $original = WhatwgUrl::parse('https://example.com/old'); $modified = $original->withPath('/new'); expect($original->getPath())->toBe('/old'); expect($modified->getPath())->toBe('/new'); }); it('returns new instance with different query', function () { $original = WhatwgUrl::parse('https://example.com?old=1'); $modified = $original->withQuery('new=2'); expect($original->getQuery())->toBe('old=1'); expect($modified->getQuery())->toBe('new=2'); }); it('returns new instance with different fragment', function () { $original = WhatwgUrl::parse('https://example.com#old'); $modified = $original->withFragment('new'); expect($original->getFragment())->toBe('old'); expect($modified->getFragment())->toBe('new'); }); it('returns new instance with user info', function () { $original = WhatwgUrl::parse('https://example.com'); $modified = $original->withUserInfo('user', 'pass'); expect($original->getUserInfo())->toBe(''); expect($modified->getUserInfo())->toBe('user:pass'); }); it('handles user info without password', function () { $url = WhatwgUrl::parse('https://example.com'); $modified = $url->withUserInfo('user'); expect($modified->getUserInfo())->toBe('user'); }); }); describe('serialization', function () { it('converts to string', function () { $url = WhatwgUrl::parse('https://example.com/path?query=1#fragment'); expect($url->toString())->toBe('https://example.com/path?query=1#fragment'); expect((string) $url)->toBe('https://example.com/path?query=1#fragment'); }); it('handles IDNA domains (Punycode)', function () { $url = WhatwgUrl::parse('https://例え.jp/path'); $str = $url->toString(); // WHATWG automatically Punycode-encodes expect($str)->toContain('xn--'); }); it('toAsciiString returns same as toString for WHATWG', function () { $url = WhatwgUrl::parse('https://example.com/path'); expect($url->toAsciiString())->toBe($url->toString()); }); }); describe('URL resolution', function () { it('resolves relative URLs', function () { $base = WhatwgUrl::parse('https://example.com/base/path'); $resolved = $base->resolve('relative'); expect($resolved->getHost())->toBe('example.com'); expect($resolved->getPath())->toContain('relative'); }); it('resolves absolute URLs', function () { $base = WhatwgUrl::parse('https://example.com/base'); $resolved = $base->resolve('https://other.com/path'); expect($resolved->getHost())->toBe('other.com'); expect($resolved->getPath())->toBe('/path'); }); it('resolves with path normalization', function () { $base = WhatwgUrl::parse('https://example.com/base'); $resolved = $base->resolve('./path/../other'); expect($resolved->getPath())->toBe('/other'); }); }); describe('URL comparison', function () { it('compares URLs for equality', function () { $url1 = WhatwgUrl::parse('https://example.com/path'); $url2 = WhatwgUrl::parse('https://example.com/path'); expect($url1->equals($url2))->toBeTrue(); }); it('compares URLs ignoring fragments by default', function () { $url1 = WhatwgUrl::parse('https://example.com/path#frag1'); $url2 = WhatwgUrl::parse('https://example.com/path#frag2'); expect($url1->equals($url2))->toBeTrue(); expect($url1->equals($url2, includeFragment: true))->toBeFalse(); }); it('detects different URLs', function () { $url1 = WhatwgUrl::parse('https://example.com/path1'); $url2 = WhatwgUrl::parse('https://example.com/path2'); expect($url1->equals($url2))->toBeFalse(); }); }); describe('edge cases', function () { it('handles trailing slashes', function () { $url = WhatwgUrl::parse('https://example.com/path/'); expect($url->getPath())->toBe('/path/'); }); it('handles empty path', function () { $url = WhatwgUrl::parse('https://example.com'); expect($url->getPath())->toBe('/'); }); it('handles empty query', function () { $url = WhatwgUrl::parse('https://example.com/path'); expect($url->getQuery())->toBe(''); }); it('handles empty fragment', function () { $url = WhatwgUrl::parse('https://example.com/path'); expect($url->getFragment())->toBe(''); }); it('removes port with null', function () { $url = WhatwgUrl::parse('https://example.com:8080/path'); $modified = $url->withPort(null); expect($modified->getPort())->toBeNull(); }); }); describe('browser redirect use cases', function () { it('handles query parameters for redirects', function () { $url = WhatwgUrl::parse('https://example.com/redirect'); $withParams = $url->withQuery('return_url=https://other.com&status=success'); expect($withParams->getQuery())->toContain('return_url'); expect($withParams->getQuery())->toContain('status=success'); }); it('handles fragment identifiers', function () { $url = WhatwgUrl::parse('https://example.com/page#section'); expect($url->getFragment())->toBe('section'); }); it('normalizes paths for browser compatibility', function () { $url = WhatwgUrl::parse('https://example.com/./path/../other'); expect($url->getPath())->toBe('/other'); }); }); describe('deep link use cases', function () { it('handles custom schemes', function () { $url = WhatwgUrl::parse('myapp://open/profile?user_id=123'); expect($url->getScheme())->toBe('myapp'); expect($url->getPath())->toContain('profile'); expect($url->getQuery())->toContain('user_id=123'); }); }); });