diff --git a/src/node_url_pattern.cc b/src/node_url_pattern.cc index 2294ee43f0a08d..8077ed1c2211f1 100644 --- a/src/node_url_pattern.cc +++ b/src/node_url_pattern.cc @@ -202,7 +202,11 @@ void URLPattern::New(const FunctionCallbackInfo& args) { // - new URLPattern(input, baseURL) // - new URLPattern(input, options) // - new URLPattern(input, baseURL, options) - if (args[0]->IsString()) { + // Per WebIDL, null/undefined for a union type including a dictionary + // uses the default value (empty init). + if (args[0]->IsNullOrUndefined()) { + init = ada::url_pattern_init{}; + } else if (args[0]->IsString()) { BufferValue input_buffer(env->isolate(), args[0]); CHECK_NOT_NULL(*input_buffer); input = input_buffer.ToString(); @@ -217,41 +221,56 @@ void URLPattern::New(const FunctionCallbackInfo& args) { return; } - // The next argument can be baseURL or options. - if (args.Length() > 1) { + // Per WebIDL overload resolution: + // With 3+ args, it's always overload 1: (input, baseURL, options) + // With 2 args, if arg1 is string → overload 1 (baseURL), + // else → overload 2 (options) + if (args.Length() >= 3) { + // arg1 is baseURL. Per WebIDL, null/undefined are stringified for + // USVString ("null"/"undefined"), which will be rejected as invalid + // URLs by ada downstream. if (args[1]->IsString()) { BufferValue base_url_buffer(env->isolate(), args[1]); CHECK_NOT_NULL(*base_url_buffer); base_url = base_url_buffer.ToString(); - } else if (args[1]->IsObject()) { - CHECK(!options.has_value()); - options = URLPatternOptions::FromJsObject(env, args[1].As()); - if (!options) { - // If options does not have a value, we assume an error was - // thrown and scheduled on the isolate. Return early to - // propagate it. - return; - } + } else if (args[1]->IsNull()) { + base_url = std::string("null"); + } else if (args[1]->IsUndefined()) { + base_url = std::string("undefined"); } else { THROW_ERR_INVALID_ARG_TYPE(env, - "second argument must be a string or object"); + "second argument must be a string"); return; } - // Only remaining argument can be options. - if (args.Length() > 2) { + // arg2 is options. Per WebIDL, null/undefined for a dictionary + // uses the default value (empty dict). + if (!args[2]->IsNullOrUndefined()) { if (!args[2]->IsObject()) { THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object"); return; } CHECK(!options.has_value()); options = URLPatternOptions::FromJsObject(env, args[2].As()); - if (!options) { - // If options does not have a value, we assume an error was - // thrown and scheduled on the isolate. Return early to - // propagate it. - return; - } + if (!options) return; + } + } else if (args.Length() == 2) { + // Overload resolution: string → overload 1 (baseURL), + // else → overload 2 (options). + if (args[1]->IsString()) { + BufferValue base_url_buffer(env->isolate(), args[1]); + CHECK_NOT_NULL(*base_url_buffer); + base_url = base_url_buffer.ToString(); + } else if (args[1]->IsNullOrUndefined()) { + // Overload 2, options uses default → skip + } else if (args[1]->IsObject()) { + CHECK(!options.has_value()); + options = URLPatternOptions::FromJsObject(env, args[1].As()); + if (!options) return; + } else { + THROW_ERR_INVALID_ARG_TYPE(env, + "second argument must be a string or object"); + return; } } @@ -493,11 +512,8 @@ URLPattern::URLPatternOptions::FromJsObject(Environment* env, Local ignore_case; if (obj->Get(env->context(), env->ignore_case_string()) .ToLocal(&ignore_case)) { - if (!ignore_case->IsBoolean()) { - THROW_ERR_INVALID_ARG_TYPE(env, "options.ignoreCase must be a boolean"); - return std::nullopt; - } - options.ignore_case = ignore_case->IsTrue(); + // Per WebIDL, boolean dictionary members are coerced (not type-checked). + options.ignore_case = ignore_case->BooleanValue(env->isolate()); } else { // If ToLocal returns false, the assumption is that getting the // ignore_case_string threw an error, let's propagate that now @@ -564,7 +580,7 @@ void URLPattern::Exec(const FunctionCallbackInfo& args) { ada::url_pattern_input input; std::optional baseURL{}; std::string input_base; - if (args.Length() == 0) { + if (args.Length() == 0 || args[0]->IsNullOrUndefined()) { input = ada::url_pattern_init{}; } else if (args[0]->IsString()) { Utf8Value input_value(env->isolate(), args[0].As()); @@ -580,13 +596,16 @@ void URLPattern::Exec(const FunctionCallbackInfo& args) { return; } - if (args.Length() > 1) { - if (!args[1]->IsString()) { + if (args.Length() > 1 && !args[1]->IsUndefined()) { + if (args[1]->IsNull()) { + baseURL = std::string("null"); + } else if (args[1]->IsString()) { + Utf8Value base_url_value(env->isolate(), args[1].As()); + baseURL = base_url_value.ToStringView(); + } else { THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); return; } - Utf8Value base_url_value(env->isolate(), args[1].As()); - baseURL = base_url_value.ToStringView(); } Local result; @@ -607,7 +626,7 @@ void URLPattern::Test(const FunctionCallbackInfo& args) { ada::url_pattern_input input; std::optional baseURL{}; std::string input_base; - if (args.Length() == 0) { + if (args.Length() == 0 || args[0]->IsNullOrUndefined()) { input = ada::url_pattern_init{}; } else if (args[0]->IsString()) { Utf8Value input_value(env->isolate(), args[0].As()); @@ -623,13 +642,16 @@ void URLPattern::Test(const FunctionCallbackInfo& args) { return; } - if (args.Length() > 1) { - if (!args[1]->IsString()) { + if (args.Length() > 1 && !args[1]->IsUndefined()) { + if (args[1]->IsNull()) { + baseURL = std::string("null"); + } else if (args[1]->IsString()) { + Utf8Value base_url_value(env->isolate(), args[1].As()); + baseURL = base_url_value.ToStringView(); + } else { THROW_ERR_INVALID_ARG_TYPE(env, "baseURL must be a string"); return; } - Utf8Value base_url_value(env->isolate(), args[1].As()); - baseURL = base_url_value.ToStringView(); } std::optional baseURL_opt = diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index c587a8d021fd40..337944bf9c1c72 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -29,7 +29,7 @@ Last update: - resources: https://github.com/web-platform-tests/wpt/tree/6a2f322376/resources - streams: https://github.com/web-platform-tests/wpt/tree/bc9dcbbf1a/streams - url: https://github.com/web-platform-tests/wpt/tree/7a3645b79a/url -- urlpattern: https://github.com/web-platform-tests/wpt/tree/a2e15ad405/urlpattern +- urlpattern: https://github.com/web-platform-tests/wpt/tree/f07c03cbed/urlpattern - user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing - wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/65a2134d50/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi diff --git a/test/fixtures/wpt/urlpattern/resources/urlpatterntestdata.json b/test/fixtures/wpt/urlpattern/resources/urlpatterntestdata.json index 783fc3bdcf66a5..2ae0a49631e759 100644 --- a/test/fixtures/wpt/urlpattern/resources/urlpatterntestdata.json +++ b/test/fixtures/wpt/urlpattern/resources/urlpatterntestdata.json @@ -3118,5 +3118,41 @@ "expected_match": { "hostname": { "input": "localhost", "groups": { "domain" : "localhost"} } } + }, + { + "pattern": ["((?R)):"], + "expected_obj": "error" + }, + { + "pattern": ["(\\H):"], + "expected_obj": "error" + }, + { + "pattern": [ + {"pathname": "/:foo((?a))"} + ], + "inputs": [ + {"pathname": "/a"} + ], + "expected_match": { + "pathname": { + "input": "/a", + "groups": {"foo": "a"} + } + } + }, + { + "pattern": [ + {"pathname": "/foo/(bar(?baz))"} + ], + "inputs": [ + {"pathname": "/foo/barbaz"} + ], + "expected_match": { + "pathname": { + "input": "/foo/barbaz", + "groups": {"0": "barbaz"} + } + } } ] diff --git a/test/fixtures/wpt/urlpattern/urlpattern-constructor.html b/test/fixtures/wpt/urlpattern/urlpattern-constructor.any.js similarity index 73% rename from test/fixtures/wpt/urlpattern/urlpattern-constructor.html rename to test/fixtures/wpt/urlpattern/urlpattern-constructor.any.js index 2d83b7dc0325b4..9290911d4b1c77 100644 --- a/test/fixtures/wpt/urlpattern/urlpattern-constructor.html +++ b/test/fixtures/wpt/urlpattern/urlpattern-constructor.any.js @@ -1,7 +1,4 @@ - - - - diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 7c7f8ce122dff8..d627791ed27e0c 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -76,7 +76,7 @@ "path": "url" }, "urlpattern": { - "commit": "a2e15ad40518c30c4e7f649584dbda699a40d531", + "commit": "f07c03cbede41ba677c3d26fd17ff3e02ba26783", "path": "urlpattern" }, "user-timing": { diff --git a/test/parallel/test-urlpattern-types.js b/test/parallel/test-urlpattern-types.js index 2459f0149e2d91..6d3f213453b4ca 100644 --- a/test/parallel/test-urlpattern-types.js +++ b/test/parallel/test-urlpattern-types.js @@ -23,9 +23,19 @@ assert.throws(() => new URLPattern({}, '', 1), { code: 'ERR_INVALID_ARG_TYPE', }); -assert.throws(() => new URLPattern({}, { ignoreCase: '' }), { - code: 'ERR_INVALID_ARG_TYPE', -}); +// Per WebIDL, ignoreCase is coerced to boolean (not type-checked). +{ + const p = new URLPattern({}, { ignoreCase: '' }); + assert.strictEqual(p.protocol, '*'); +} +{ + const p = new URLPattern({}, { ignoreCase: undefined }); + assert.strictEqual(p.protocol, '*'); +} +{ + const p = new URLPattern({}, {}); + assert.strictEqual(p.protocol, '*'); +} const pattern = new URLPattern(); @@ -44,3 +54,89 @@ assert.throws(() => pattern.test(1), { assert.throws(() => pattern.test('', 1), { code: 'ERR_INVALID_ARG_TYPE', }); + +// Per WebIDL, undefined/null for a URLPatternInput (union including dictionary) +// uses the default value (empty URLPatternInit {}). + +// Constructor: undefined input should be treated as empty init. +{ + const p = new URLPattern(undefined); + assert.strictEqual(p.protocol, '*'); + assert.strictEqual(p.hostname, '*'); +} + +// Constructor: null input should be treated as empty init (union, dict branch). +{ + const p = new URLPattern(null); + assert.strictEqual(p.protocol, '*'); + assert.strictEqual(p.hostname, '*'); +} + +// Constructor: 2-arg with undefined/null uses overload 2 (options defaults). +{ + const p1 = new URLPattern(undefined, undefined); + assert.strictEqual(p1.protocol, '*'); + const p2 = new URLPattern(null, null); + assert.strictEqual(p2.protocol, '*'); + const p3 = new URLPattern({}, null); + assert.strictEqual(p3.protocol, '*'); + const p4 = new URLPattern('https://example.com', null); + assert.strictEqual(p4.hostname, 'example.com'); + const p5 = new URLPattern('https://example.com', undefined); + assert.strictEqual(p5.hostname, 'example.com'); +} + +// Constructor: valid input with undefined/null options. +{ + const p = new URLPattern({ pathname: '/foo' }, undefined); + assert.strictEqual(p.pathname, '/foo'); +} + +// Constructor: 3-arg with null/undefined baseURL is stringified per WebIDL, +// rejected as invalid URL by the parser. +assert.throws(() => new URLPattern('https://example.com', null, null)); +assert.throws(() => new URLPattern('https://example.com', undefined, undefined)); + +// Constructor: 3-arg with valid baseURL and null options uses defaults. +{ + const p = new URLPattern('https://example.com', 'https://example.com', null); + assert.strictEqual(p.hostname, 'example.com'); + const p2 = new URLPattern('https://example.com', 'https://example.com', undefined); + assert.strictEqual(p2.hostname, 'example.com'); +} + +// exec() and test(): undefined input should be treated as empty init. +{ + const p = new URLPattern(); + assert.strictEqual(p.test(undefined), true); + assert.strictEqual(p.test(undefined, undefined), true); + assert.notStrictEqual(p.exec(undefined), null); + assert.notStrictEqual(p.exec(undefined, undefined), null); +} + +// exec() and test(): null input should be treated as empty init. +{ + const p = new URLPattern(); + assert.strictEqual(p.test(null), true); + assert.notStrictEqual(p.exec(null), null); +} + +// exec() and test(): null for baseURL is stringified to "null" per WebIDL. +// With string input, "null" is not a valid base URL so match fails silently. +// With dict input, providing baseURL with a dict throws per spec. +{ + const p = new URLPattern(); + // String input + null baseURL: no throw, match returns null (false). + assert.strictEqual(p.test('https://example.com', null), false); + assert.strictEqual(p.exec('https://example.com', null), null); + // Dict input + null baseURL: throws (baseURL not allowed with dict input). + assert.throws(() => p.test(null, null)); + assert.throws(() => p.exec(null, null)); +} + +// exec() and test(): valid input with undefined baseURL. +{ + const p = new URLPattern({ protocol: 'https' }); + assert.strictEqual(p.test('https://example.com', undefined), true); + assert.notStrictEqual(p.exec('https://example.com', undefined), null); +}