diff --git a/ext/webidl/00_webidl.js b/ext/webidl/00_webidl.js index 6ce102f669cfb1..995fc648bb18a2 100644 --- a/ext/webidl/00_webidl.js +++ b/ext/webidl/00_webidl.js @@ -998,7 +998,7 @@ function createAsyncIterableConverter(converter) { context = undefined, opts = EMPTY_OPTS, ) { - if (type(V) !== "Object") { + if (V === undefined || V === null) { throw makeException( TypeError, "can not be converted to async iterable.", @@ -1007,12 +1007,16 @@ function createAsyncIterableConverter(converter) { ); } + // Follows the GetIterator(obj, async) abstract operation: the @@asyncIterator + // and @@iterator methods are looked up with GetMethod semantics, which treats + // both `undefined` and `null` as an absent method, and which performs ToObject + // on `V` (so primitives such as strings are accepted). let isAsync = true; let method = V[SymbolAsyncIterator]; - if (method === undefined) { + if (method === undefined || method === null) { method = V[SymbolIterator]; - if (method === undefined) { + if (method === undefined || method === null) { throw makeException( TypeError, "is not iterable.", diff --git a/tests/unit/streams_test.ts b/tests/unit/streams_test.ts index d08a79dd3fb504..c2a5d155dc1f67 100644 --- a/tests/unit/streams_test.ts +++ b/tests/unit/streams_test.ts @@ -1,10 +1,5 @@ // Copyright 2018-2026 the Deno authors. MIT license. -import { - assertEquals, - assertRejects, - assertThrows, - fail, -} from "./test_util.ts"; +import { assertEquals, assertRejects, fail } from "./test_util.ts"; // `resourceForReadableStream` is registered on the internals object only when // `ext:deno_web/06_streams.js` first evaluates, which is now lazy. Touch the @@ -578,13 +573,16 @@ Deno.test(async function decompressionStreamInvalidGzipStillReported() { ); }); -Deno.test(function readableStreamFromWithStringThrows() { - assertThrows( - // @ts-expect-error: primitives are not acceptable - () => ReadableStream.from("string"), - TypeError, - "Failed to execute 'ReadableStream.from': Argument 1 can not be converted to async iterable.", - ); +Deno.test(async function readableStreamFromString() { + // Per the Streams spec, ReadableStream.from() accepts any iterable, including + // strings, which are iterated by code point. The TS type requires an object, + // so a string primitive is rejected statically even though it works at runtime. + // @ts-expect-error: string is iterable at runtime but the type requires an object + const stream = ReadableStream.from("ab"); + const reader = stream.getReader(); + assertEquals(await reader.read(), { value: "a", done: false }); + assertEquals(await reader.read(), { value: "b", done: false }); + assertEquals((await reader.read()).done, true); }); Deno.test(async function readableStreamFromWithStringThrows() { diff --git a/tests/wpt/runner/expectations/streams.json b/tests/wpt/runner/expectations/streams.json index 82a04140477983..9a9762aac8671a 100644 --- a/tests/wpt/runner/expectations/streams.json +++ b/tests/wpt/runner/expectations/streams.json @@ -115,18 +115,8 @@ "templated.any.worker.html": true, "async-iterator.any.worker.html": true, "cross-realm-crash.window.html": false, - "from.any.html": { - "expectedFailures": [ - "ReadableStream.from ignores a null @@asyncIterator", - "ReadableStream.from accepts a string" - ] - }, - "from.any.worker.html": { - "expectedFailures": [ - "ReadableStream.from ignores a null @@asyncIterator", - "ReadableStream.from accepts a string" - ] - }, + "from.any.html": true, + "from.any.worker.html": true, "crashtests": { "garbage-collection.any.html": true, "garbage-collection.any.worker.html": true