diff options
Diffstat (limited to 'testing/web-platform/tests/dom')
10 files changed, 1241 insertions, 27 deletions
diff --git a/testing/web-platform/tests/dom/abort/WEB_FEATURES.yml b/testing/web-platform/tests/dom/abort/WEB_FEATURES.yml new file mode 100644 index 0000000000..169de93ae9 --- /dev/null +++ b/testing/web-platform/tests/dom/abort/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: aborting + files: "**" diff --git a/testing/web-platform/tests/dom/events/event-global.html b/testing/web-platform/tests/dom/events/event-global.html index 3e8d25ecb5..f70606fb65 100644 --- a/testing/web-platform/tests/dom/events/event-global.html +++ b/testing/web-platform/tests/dom/events/event-global.html @@ -114,4 +114,14 @@ async_test(t => { target.dispatchEvent(new Event("click")); }, "window.event is set to the current event, which is the event passed to dispatch"); + +async_test(t => { + let target = new XMLHttpRequest(); + + target.onload = t.step_func_done(e => { + assert_equals(e, window.event); + }); + + target.dispatchEvent(new Event("load")); +}, "window.event is set to the current event, which is the event passed to dispatch (2)"); </script> diff --git a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html index 5e3af7966e..99a281480f 100644 --- a/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html +++ b/testing/web-platform/tests/dom/events/scrolling/scrollend-event-fires-on-visual-viewport.html @@ -20,28 +20,48 @@ </style> <div class="large"></div> <script> - window.onload = () => { - promise_test(async () => { - await waitForCompositorCommit(); - - await pinchZoomIn(); - assert_greater_than(visualViewport.scale, 1, "page should be zoomed in."); - + window.onload = async () => { + async function pan_viewport_test(add_event_listener_func) { const preScrollVisualViewportOffsetTop = visualViewport.offsetTop; const preScrollWindowScrollOffset = window.scrollY; - const scrollend_promise = new Promise((resolve) => { - visualViewport.addEventListener("scrollend", resolve); - }); + + const scrollend_promise = add_event_listener_func(); const scrollAmount = 50; await touchScrollInTarget(scrollAmount, document.documentElement, "up"); await scrollend_promise; - assert_less_than(visualViewport.offsetTop, preScrollVisualViewportOffsetTop, + assert_less_than(visualViewport.offsetTop, + preScrollVisualViewportOffsetTop, `visualViewport should be scrolled.`); assert_equals(window.scrollY, preScrollWindowScrollOffset, "the window should not scroll."); - }, "scrollend fires when visual viewport is panned."); + // No need to undo scroll; subsequent test has room to scroll further. + } + + await waitForCompositorCommit(); + await pinchZoomIn(); + assert_greater_than(visualViewport.scale, 1, "page should be zoomed in."); + + promise_test(async (t) => { + await pan_viewport_test(() => { + return new Promise((resolve) => { + visualViewport.addEventListener("scrollend", resolve, { once: true}); + }); + }); + }, "scrollend listener added via addEventlistener fires when the visual " + + "viewport is panned."); + + promise_test(async (t) => { + await pan_viewport_test((t) => { + return new Promise((resolve) => { + visualViewport.onscrollend = () => { + visualViewport.onscrollend = undefined; + resolve(); + } + }); + }); + }, "visualviewport.onscrollend fires when the visual viewport is panned."); } </script> </body> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html index a9b7ba633e..fa4a987751 100644 --- a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/Node-appendChild-script-and-default-style-meta-from-fragment.tentative.html @@ -7,29 +7,53 @@ <div id="div">hello</div> <script> let scriptRan = false; -let computedStyleDuringInsertion = null; +let computedStyleInPreScript = null; +let computedStyleInPostScript = null; test(() => { const div = document.getElementById("div"); + + // 1. Gets inserted *before* the `<meta>` tag. Cannot observe the meta tag's + // effect, because this script runs before the meta tag's post-insertion steps + // run, and the meta tag's post-insertion steps is where the default style + // sheet actually changes. + const preScript = document.createElement("script"); + preScript.textContent = ` + computedStyleInPreScript = getComputedStyle(div).display; + scriptRan = true; + `; + + // 2. The `<meta>` tag itself. const meta = document.createElement("meta"); meta.httpEquiv = "default-style"; meta.content = "alternative"; - const script = document.createElement("script"); - script.textContent = ` - computedStyleDuringInsertion = getComputedStyle(div).display; + + // 3. Gets inserted *after* the `<meta>` tag. Observes the meta tag's effect, + // because this script runs after the meta tag's post-insertion steps, which + // has the script-observable change to the default style sheet. + const postScript = document.createElement("script"); + postScript.textContent = ` + computedStyleInPostScript = getComputedStyle(div).display; scriptRan = true; `; + const df = document.createDocumentFragment(); - df.appendChild(script); - df.appendChild(meta); - assert_equals(getComputedStyle(div).display, "block", "div has block display"); + df.append(preScript, meta, postScript); + + assert_equals(getComputedStyle(div).display, "block", + "div still has block display before meta insertion"); assert_false(scriptRan, "script has not run before insertion"); + document.head.appendChild(df); assert_true(scriptRan, "script has run after insertion"); - assert_equals(computedStyleDuringInsertion, "none", - "display: none; style was applied during DOM insertion, before " + - "later-inserted script runs"); + assert_equals(computedStyleInPreScript, "block", + "display: none; style was NOT applied during DOM insertion steps, " + + "before earlier-inserted script post-insertion steps run"); + assert_equals(computedStyleInPostScript, "none", + "display: none; style WAS applied during DOM post-insertion steps, " + + "before later-inserted script runs"); assert_equals(getComputedStyle(div).display, "none", "style remains display: none; after insertion"); + }, "Inserting <meta> that uses alternate stylesheets, applies the style " + - "during DOM insertion, and before script runs as a result of any atomic insertions"); + "during DOM post-insertion steps"); </script> diff --git a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js index 4c8cd85cbf..fdca02dcda 100644 --- a/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js +++ b/testing/web-platform/tests/dom/nodes/insertion-removing-steps/blur-event.window.js @@ -12,8 +12,17 @@ test(() => { const button = document.body.appendChild(document.createElement('button')); button.focus(); - let blurCalled = false; - button.onblur = e => blurCalled = true; + let blur_called = false; + let focus_out_called = false; + let focus_called = false; + + button.onblur = () => { blur_called = true; } + button.onfocusout = () => { focus_out_called = true; } + document.body.addEventListener("focus", + () => { focus_called = true; }, {capture: true}); button.remove(); - assert_false(blurCalled, "Blur event was not fired"); -}, "<button> element does not fire blur event upon DOM removal"); + + assert_false(blur_called, "Blur event was not fired"); + assert_false(focus_out_called, "FocusOut event was not fired"); + assert_false(focus_called, "Focus was not fired"); +}, "<button> element does not fire blur/focusout events upon DOM removal"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js new file mode 100644 index 0000000000..7c99066dc2 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-first.any.js @@ -0,0 +1,114 @@ +promise_test(async () => { + const results = []; + + const source = new Observable(subscriber => { + subscriber.addTeardown(() => results.push('teardown')); + subscriber.next(1); + results.push(subscriber.active ? 'active' : 'inactive'); + results.push(subscriber.signal.aborted ? 'aborted' : 'not aborted') + + // Ignored. + subscriber.next(2); + subscriber.complete(); + }); + + const value = await source.first(); + + assert_array_equals(results, ['teardown', 'inactive', 'aborted']); + assert_equals(value, 1, + "Promise resolves with the first value from the source Observable"); +}, "first(): Promise resolves with the first value from the source Observable"); + +promise_test(async () => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + let rejection; + try { + await source.first(); + } catch (e) { + rejection = e; + } + + assert_equals(rejection, error, "Promise rejects with source Observable error"); +}, "first(): Promise rejects with the error emitted from the source Observable"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.complete(); + }); + + let rejection; + try { + await source.first(); + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof RangeError, + "Upon complete(), first() Promise rejects with RangeError"); + assert_equals(rejection.message, "No values in Observable"); +}, "first(): Promise rejects with RangeError when source Observable " + + "completes without emitting any values"); + +promise_test(async () => { + const source = new Observable(subscriber => {}); + + const controller = new AbortController(); + const promise = source.first({ signal: controller.signal }); + + controller.abort(); + + let rejection; + try { + await promise; + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof DOMException, + "Promise rejects with a DOMException for abortion"); + assert_equals(rejection.name, "AbortError", + "Rejected with 'AbortError' DOMException"); + assert_equals(rejection.message, "signal is aborted without reason"); +}, "first(): Aborting a signal rejects the Promise with an AbortError DOMException"); + +promise_test(async () => { + const results = []; + + const source = new Observable(subscriber => { + results.push("source subscribe"); + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.signal.addEventListener("abort", () => results.push("source abort")); + results.push("before source next 1"); + subscriber.next(1); + results.push("after source next 1"); + }); + + results.push("calling first"); + const promise = source.first(); + + assert_array_equals(results, [ + "calling first", + "source subscribe", + "before source next 1", + "source teardown", + "source abort", + "after source next 1" + ], "Array values after first() is called"); + + const firstValue = await promise; + results.push(`first resolved with: ${firstValue}`); + + assert_array_equals(results, [ + "calling first", + "source subscribe", + "before source next 1", + "source teardown", + "source abort", + "after source next 1", + "first resolved with: 1", + ], "Array values after Promise is awaited"); +}, "first(): Lifecycle"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js new file mode 100644 index 0000000000..7cbfa6cb60 --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-flatMap.any.js @@ -0,0 +1,315 @@ +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + let projectionCalls = 0; + + const results = []; + + const flattened = source.flatMap(value => { + projectionCalls++; + return new Observable((subscriber) => { + subscriber.next(value * 10); + subscriber.next(value * 100); + subscriber.complete(); + }); + }); + + assert_true(flattened instanceof Observable, "flatMap() returns an Observable"); + assert_equals(projectionCalls, 0, + "Projection is not called until subscription starts"); + + flattened.subscribe({ + next: v => results.push(v), + error: () => results.push("error"), + complete: () => results.push("complete"), + }); + + assert_equals(projectionCalls, 3, + "Mapper is called three times, once for each source Observable value"); + assert_array_equals(results, [10, 100, 20, 200, 30, 300, "complete"], + "flatMap() results are correct"); +}, "flatMap(): Flattens simple source Observable properly"); + +test(() => { + const error = new Error("error"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.error(error); + subscriber.next(3); + }); + + const flattened = source.flatMap(value => { + return new Observable(subscriber => { + subscriber.next(value * 10); + subscriber.next(value * 100); + subscriber.complete(); + }); + }); + + const results = []; + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [10, 100, 20, 200, error], + "Source error is passed through to the flatMap() Observable"); +}, "flatMap(): Returned Observable passes through source Observable errors"); + +test(() => { + const results = []; + const error = new Error("error"); + const source = new Observable(subscriber => { + subscriber.next(1); + results.push(subscriber.active ? "active" : "inactive"); + subscriber.next(2); + results.push(subscriber.active ? "active" : "inactive"); + subscriber.next(3); + subscriber.complete(); + }); + + const flattened = source.flatMap((value) => { + return new Observable((subscriber) => { + subscriber.next(value * 10); + subscriber.next(value * 100); + if (value === 2) { + subscriber.error(error); + } else { + subscriber.complete(); + } + }); + }); + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [10, 100, "active", 20, 200, error, "inactive"], + "Inner subscription error gets surfaced"); +}, "flatMap(): Outer Subscription synchronously becomes inactive when an " + + "'inner' Observable emits an error"); + +test(() => { + const results = []; + const error = new Error("error"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + results.push(subscriber.active ? "active" : "inactive"); + subscriber.complete(); + }); + + const flattened = source.flatMap(value => { + if (value === 3) { + throw error; + } + return new Observable(subscriber => { + subscriber.next(value * 10); + subscriber.next(value * 100); + subscriber.complete(); + }); + }); + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [10, 100, 20, 200, error, "inactive"], + "Inner subscriber thrown error gets surfaced"); +}, "flatMap(): Outer Subscription synchronously becomes inactive when an " + + "'inner' Observable throws an error"); + +test(() => { + const source = createTestSubject(); + const inner1 = createTestSubject(); + const inner2 = createTestSubject(); + + const flattened = source.flatMap(value => { + if (value === 1) { + return inner1; + } + + return inner2; + }); + + const results = []; + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, []); + + source.next(1); + assert_equals(inner1.subscriberCount(), 1, "inner1 gets subscribed to"); + + source.next(2); + assert_equals(inner2.subscriberCount(), 0, + "inner2 is queued, not subscribed to until inner1 completes"); + + assert_array_equals(results, []); + + inner1.next(100); + inner1.next(101); + + assert_array_equals(results, [100, 101]); + + inner1.complete(); + assert_equals(inner1.subscriberCount(), 0, + "inner1 becomes inactive once it completes"); + assert_equals(inner2.subscriberCount(), 1, + "inner2 gets un-queued and subscribed to once inner1 completes"); + + inner2.next(200); + inner2.next(201); + assert_array_equals(results, [100, 101, 200, 201]); + + inner2.complete(); + assert_equals(inner2.subscriberCount(), 0, + "inner2 becomes inactive once it completes"); + assert_equals(source.subscriberCount(), 1, + "source is not unsubscribed from yet, since it has not completed"); + assert_array_equals(results, [100, 101, 200, 201]); + + source.complete(); + assert_equals(source.subscriberCount(), 0, + "source unsubscribed from after it completes"); + + assert_array_equals(results, [100, 101, 200, 201, "complete"]); +}, "flatMap(): result Observable does not complete until source and inner " + + "Observables all complete"); + +test(() => { + const source = createTestSubject(); + const inner1 = createTestSubject(); + const inner2 = createTestSubject(); + + const flattened = source.flatMap(value => { + if (value === 1) { + return inner1; + } + + return inner2; + }); + + const results = []; + + flattened.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, []); + + source.next(1); + source.next(2); + assert_equals(inner1.subscriberCount(), 1, "inner1 gets subscribed to"); + assert_equals(inner2.subscriberCount(), 0, + "inner2 is queued, not subscribed to until inner1 completes"); + + assert_array_equals(results, []); + + // Before `inner1` pushes any values, we first complete the source Observable. + // This will not fire completion of the Observable returned from `flatMap()`, + // because there are two values (corresponding to inner Observables) that are + // queued to the inner queue that need to be processed first. Once the last + // one of *those* completes (i.e., `inner2.complete()` further down), then the + // returned Observable can finally complete. + source.complete(); + assert_equals(source.subscriberCount(), 0, + "source becomes inactive once it completes"); + + inner1.next(100); + inner1.next(101); + + assert_array_equals(results, [100, 101]); + + inner1.complete(); + assert_array_equals(results, [100, 101], + "Outer completion not triggered after inner1 completes"); + assert_equals(inner2.subscriberCount(), 1, + "inner2 gets un-queued and subscribed after inner1 completes"); + + inner2.next(200); + inner2.next(201); + assert_array_equals(results, [100, 101, 200, 201]); + + inner2.complete(); + assert_equals(inner2.subscriberCount(), 0, + "inner2 becomes inactive once it completes"); + assert_array_equals(results, [100, 101, 200, 201, "complete"]); +}, "flatMap(): result Observable does not complete after source Observable " + + "completes while there are still queued inner Observables to process " + + "Observables all complete"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + const result = source.flatMap(() => inner); + + const ac = new AbortController(); + + result.subscribe({}, { signal: ac.signal, }); + + source.next(1); + + assert_equals(inner.subscriberCount(), 1, + "inner Observable subscribed to once source emits it"); + + ac.abort(); + + assert_equals(source.subscriberCount(), 0, + "source unsubscribed from, once outer signal is aborted"); + + assert_equals(inner.subscriberCount(), 0, + "inner Observable unsubscribed from once the outer Observable is " + + "subscribed from, as a result of the outer signal being aborted"); +}, "flatMap(): source and inner active Observables are both unsubscribed " + + "from once the outer subscription signal is aborted"); + +// A helper function to create an Observable that can be externally controlled +// and examined for testing purposes. +function createTestSubject() { + const subscribers = new Set(); + const subject = new Observable(subscriber => { + subscribers.add(subscriber); + subscriber.addTeardown(() => subscribers.delete(subscriber)); + }); + + subject.next = value => { + for (const subscriber of Array.from(subscribers)) { + subscriber.next(value); + } + }; + subject.error = error => { + for (const subscriber of Array.from(subscribers)) { + subscriber.error(error); + } + }; + subject.complete = () => { + for (const subscriber of Array.from(subscribers)) { + subscriber.complete(); + } + }; + subject.subscriberCount = () => { + return subscribers.size; + }; + + return subject; +} diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js new file mode 100644 index 0000000000..80408ddced --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js @@ -0,0 +1,354 @@ +// Because we test that the global error handler is called at various times. +setup({allow_uncaught_exception: true}); + +test(() => { + assert_equals(typeof Observable.from, "function", + "Observable.from() is a function"); +}, "from(): Observable.from() is a function"); + +test(() => { + assert_throws_js(TypeError, () => Observable.from(10), + "Number cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from(true), + "Boolean cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from("String"), + "String cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from({a: 10}), + "Object cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from(Symbol.iterator), + "Bare Symbol.iterator cannot convert to an Observable"); + assert_throws_js(TypeError, () => Observable.from(Promise), + "Promise constructor cannot convert to an Observable"); +}, "from(): Failed conversions"); + +test(() => { + const target = new EventTarget(); + const observable = target.on('custom'); + const from_observable = Observable.from(observable); + assert_equals(observable, from_observable); +}, "from(): Given an observable, it returns that exact observable"); + +test(() => { + let completeCalled = false; + const results = []; + const array = [1, 2, 3, 'a', new Date(), 15, [12]]; + const observable = Observable.from(array); + observable.subscribe({ + next: v => results.push(v), + error: e => assert_unreached('error is not called'), + complete: () => completeCalled = true + }); + + assert_array_equals(results, array); + assert_true(completeCalled); +}, "from(): Given an array"); + +test(() => { + const iterable = { + [Symbol.iterator]() { + let n = 0; + return { + next() { + n++; + if (n <= 3) { + return { value: n, done: false }; + } + return { value: undefined, done: true }; + }, + }; + }, + }; + + const observable = Observable.from(iterable); + + assert_true(observable instanceof Observable, "Observable.from() returns an Observable"); + + const results = []; + + observable.subscribe({ + next: (value) => results.push(value), + error: () => assert_unreached("should not error"), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, "complete"], + "Subscription pushes iterable values out to Observable"); + + // A second subscription should restart iteration. + observable.subscribe({ + next: (value) => results.push(value), + error: () => assert_unreached("should not error"), + complete: () => results.push("complete2"), + }); + + assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete2"], + "Subscribing again causes another fresh iteration on an un-exhausted iterable"); +}, "from(): Iterable converts to Observable"); + +// The result of the @@iterator method of the converted object is called: +// 1. Once on conversion (to test that the value is an iterable). +// 2. Once on subscription, to re-pull the iterator implementation from the +// raw JS object that the Observable owns once synchronous iteration is +// about to begin. +test(() => { + let numTimesSymbolIteratorCalled = 0; + let numTimesNextCalled = 0; + + const iterable = { + [Symbol.iterator]() { + numTimesSymbolIteratorCalled++; + return { + next() { + numTimesNextCalled++; + return {value: undefined, done: true}; + } + }; + } + }; + + const observable = Observable.from(iterable); + + assert_equals(numTimesSymbolIteratorCalled, 1, + "Observable.from(iterable) invokes the @@iterator method getter once"); + assert_equals(numTimesNextCalled, 0, + "Iterator next() is not called until subscription"); + + // Override iterable's `[Symbol.iterator]` protocol with an error-throwing + // function. We assert that on subscription, this method (the new `@@iterator` + // implementation), is called because only the raw JS object gets stored in + // the Observable that results in conversion. This raw value must get + // re-converted to an iterable once iteration is about to start. + const customError = new Error('@@iterator override error'); + iterable[Symbol.iterator] = () => { + throw customError; + }; + + let thrownError = null; + observable.subscribe({ + error: e => thrownError = e, + }); + + assert_equals(thrownError, customError, + "Error thrown from next() is passed to the error() handler"); + + assert_equals(numTimesSymbolIteratorCalled, 1, + "Subscription re-invokes @@iterator method, which now is a different " + + "method that does *not* increment our assertion value"); + assert_equals(numTimesNextCalled, 0, "Iterator next() is never called"); +}, "from(): [Symbol.iterator] side-effects (one observable)"); + +// Similar to the above test, but with more Observables! +test(() => { + let numTimesSymbolIteratorCalled = 0; + let numTimesNextCalled = 0; + + const iterable = { + [Symbol.iterator]() { + numTimesSymbolIteratorCalled++; + return { + next() { + numTimesNextCalled++; + return {value: undefined, done: true}; + } + }; + } + }; + + const obs1 = Observable.from(iterable); + const obs2 = Observable.from(iterable); + const obs3 = Observable.from(iterable); + const obs4 = Observable.from(obs3); + + assert_equals(numTimesSymbolIteratorCalled, 3, "Observable.from(iterable) invokes the iterator method getter once"); + assert_equals(numTimesNextCalled, 0, "Iterator next() is not called until subscription"); + + iterable[Symbol.iterator] = () => { + throw new Error('Symbol.iterator override error'); + }; + + let errorCount = 0; + + const observer = {error: e => errorCount++}; + obs1.subscribe(observer); + obs2.subscribe(observer); + obs3.subscribe(observer); + obs4.subscribe(observer); + assert_equals(errorCount, 4, + "Error-throwing `@@iterator` implementation is called once per " + + "subscription"); + + assert_equals(numTimesSymbolIteratorCalled, 3, + "Subscription re-invokes the iterator method getter once"); + assert_equals(numTimesNextCalled, 0, "Iterator next() is never called"); +}, "from(): [Symbol.iterator] side-effects (many observables)"); + +test(() => { + const customError = new Error('@@iterator next() error'); + const iterable = { + [Symbol.iterator]() { + return { + next() { + throw customError; + } + }; + } + }; + + let thrownError = null; + Observable.from(iterable).subscribe({ + error: e => thrownError = e, + }); + + assert_equals(thrownError, customError, + "Error thrown from next() is passed to the error() handler"); +}, "from(): [Symbol.iterator] next() throws error"); + +promise_test(async () => { + const promise = Promise.resolve('value'); + const observable = Observable.from(promise); + + assert_true(observable instanceof Observable, "Converts to Observable"); + + const results = []; + + observable.subscribe({ + next: (value) => results.push(value), + error: () => assert_unreached("error() is not called"), + complete: () => results.push("complete()"), + }); + + assert_array_equals(results, [], "Observable does not emit synchronously"); + + await promise; + + assert_array_equals(results, ["value", "complete()"], "Observable emits and completes after Promise resolves"); +}, "from(): Converts Promise to Observable"); + +promise_test(async t => { + let unhandledRejectionHandlerCalled = false; + const unhandledRejectionHandler = () => { + unhandledRejectionHandlerCalled = true; + }; + + self.addEventListener("unhandledrejection", unhandledRejectionHandler); + t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler)); + + const promise = Promise.reject("reason"); + const observable = Observable.from(promise); + + assert_true(observable instanceof Observable, "Converts to Observable"); + + const results = []; + + observable.subscribe({ + next: (value) => assert_unreached("next() not called"), + error: (error) => results.push(error), + complete: () => assert_unreached("complete() not called"), + }); + + assert_array_equals(results, [], "Observable does not emit synchronously"); + + let catchBlockEntered = false; + try { + await promise; + } catch { + catchBlockEntered = true; + } + + assert_true(catchBlockEntered, "Catch block entered"); + assert_false(unhandledRejectionHandlerCalled, "No unhandledrejection event"); + assert_array_equals(results, ["reason"], + "Observable emits error() after Promise rejects"); +}, "from(): Converts rejected Promise to Observable. No " + + "`unhandledrejection` event when error is handled by subscription"); + +promise_test(async t => { + let unhandledRejectionHandlerCalled = false; + const unhandledRejectionHandler = () => { + unhandledRejectionHandlerCalled = true; + }; + + self.addEventListener("unhandledrejection", unhandledRejectionHandler); + t.add_cleanup(() => self.removeEventListener("unhandledrejection", unhandledRejectionHandler)); + + let errorReported = null; + self.addEventListener("error", e => errorReported = e, { once: true }); + + let catchBlockEntered = false; + try { + const promise = Promise.reject("custom reason"); + const observable = Observable.from(promise); + + observable.subscribe(); + await promise; + } catch { + catchBlockEntered = true; + } + + assert_true(catchBlockEntered, "Catch block entered"); + assert_false(unhandledRejectionHandlerCalled, + "No unhandledrejection event, because error got reported to global"); + assert_not_equals(errorReported, null, "Error was reported to the global"); + + assert_true(errorReported.message.includes("custom reason"), + "Error message matches"); + assert_equals(errorReported.lineno, 0, "Error lineno is 0"); + assert_equals(errorReported.colno, 0, "Error lineno is 0"); + assert_equals(errorReported.error, "custom reason", + "Error object is equivalent"); +}, "from(): Rejections not handled by subscription are reported to the " + + "global, and still not sent as an unhandledrejection event"); + +test(() => { + const results = []; + const observable = new Observable(subscriber => { + subscriber.next('from Observable'); + subscriber.complete(); + }); + + observable[Symbol.iterator] = () => { + results.push('Symbol.iterator() called'); + return { + next() { + return {value: 'from @@iterator', done: true}; + } + }; + }; + + Observable.from(observable).subscribe({ + next: v => results.push(v), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["from Observable", "complete"]); +}, "from(): Observable that implements @@iterator protocol gets converted " + + "as an Observable, not iterator"); + +test(() => { + const results = []; + const promise = new Promise(resolve => { + resolve('from Promise'); + }); + + promise[Symbol.iterator] = () => { + let done = false; + return { + next() { + if (!done) { + done = true; + return {value: 'from @@iterator', done: false}; + } else { + return {value: undefined, done: true}; + } + } + }; + }; + + Observable.from(promise).subscribe({ + next: v => results.push(v), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["from @@iterator", "complete"]); +}, "from(): Promise that implements @@iterator protocol gets converted as " + + "an iterable, not Promise"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js new file mode 100644 index 0000000000..cd39a3700a --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-last.any.js @@ -0,0 +1,113 @@ +promise_test(async () => { + const source = new Observable(subscriber => { + // Never exposed to the `last()` promise. + subscriber.next(1); + + subscriber.next(2); + subscriber.complete(); + }); + + const value = await source.last(); + + assert_equals(value, 2); +}, "last(): Promise resolves to last value"); + +promise_test(async () => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + let rejection = null; + try { + await source.last(); + } catch (e) { + rejection = e; + } + + assert_equals(rejection, error); +}, "last(): Promise rejects with emitted error"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.complete(); + }); + + let rejection = null; + try { + await source.last(); + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof RangeError, + "Promise rejects with RangeError"); + assert_equals(rejection.message, "No values in Observable"); +}, "last(): Promise rejects with RangeError when source Observable " + + "completes without emitting any values"); + +promise_test(async () => { + const source = new Observable(subscriber => {}); + + const controller = new AbortController(); + const promise = source.last({ signal: controller.signal }); + + controller.abort(); + + let rejection = null; + try { + await promise; + } catch (e) { + rejection = e; + } + + assert_true(rejection instanceof DOMException, + "Promise rejects with a DOMException for abortion"); + assert_equals(rejection.name, "AbortError", + "Rejected with 'AbortError' DOMException"); + assert_equals(rejection.message, "signal is aborted without reason"); +}, "last(): Aborting a signal rejects the Promise with an AbortError DOMException"); + +promise_test(async () => { + const results = []; + const source = new Observable(subscriber => { + results.push("source subscribe"); + subscriber.addTeardown(() => results.push("source teardown")); + subscriber.signal.addEventListener("abort", () => results.push("source abort")); + results.push("before source next 1"); + subscriber.next(1); + results.push("after source next 1"); + results.push("before source complete"); + subscriber.complete(); + results.push("after source complete"); + }); + + results.push("calling last"); + const promise = source.last(); + + assert_array_equals(results, [ + "calling last", + "source subscribe", + "before source next 1", + "after source next 1", + "before source complete", + "source teardown", + "source abort", + "after source complete", + ], "Array values after last() is called"); + + const lastValue = await promise; + results.push(`last resolved with: ${lastValue}`); + + assert_array_equals(results, [ + "calling last", + "source subscribe", + "before source next 1", + "after source next 1", + "before source complete", + "source teardown", + "source abort", + "after source complete", + "last resolved with: 1", + ], "Array values after Promise is awaited"); +}, "last(): Lifecycle"); diff --git a/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js b/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js new file mode 100644 index 0000000000..836a39a68e --- /dev/null +++ b/testing/web-platform/tests/dom/observable/tentative/observable-switchMap.any.js @@ -0,0 +1,252 @@ +test(() => { + const source = createTestSubject(); + const inner1 = createTestSubject(); + const inner2 = createTestSubject(); + + const result = source.switchMap((value, index) => { + if (value === 1) { + return inner1; + } + if (value === 2) { + return inner2; + } + throw new Error("invalid "); + }); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_equals(source.subscriberCount(), 1, + "source observable is subscribed to"); + + source.next(1); + assert_equals(inner1.subscriberCount(), 1, + "inner1 observable is subscribed to"); + + inner1.next("1a"); + assert_array_equals(results, ["1a"]); + + inner1.next("1b"); + assert_array_equals(results, ["1a", "1b"]); + + source.next(2); + assert_equals(inner1.subscriberCount(), 0, + "inner1 observable is unsubscribed from"); + assert_equals(inner2.subscriberCount(), 1, + "inner2 observable is subscribed to"); + + inner2.next("2a"); + assert_array_equals(results, ["1a", "1b", "2a"]); + + inner2.next("2b"); + assert_array_equals(results, ["1a", "1b", "2a", "2b"]); + + inner2.complete(); + assert_array_equals(results, ["1a", "1b", "2a", "2b"]); + + source.complete(); + assert_array_equals(results, ["1a", "1b", "2a", "2b", "complete"]); +}, "switchMap(): result subscribes to one inner observable at a time, " + + "unsubscribing from the previous active one when a new one replaces it"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + + const result = source.switchMap(() => inner); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_equals(source.subscriberCount(), 1, + "source observable is subscribed to"); + assert_equals(inner.subscriberCount(), 0, + "inner observable is not subscribed to"); + + source.next(1); + assert_equals(inner.subscriberCount(), 1, + "inner observable is subscribed to"); + + inner.next("a"); + assert_array_equals(results, ["a"]); + + inner.next("b"); + assert_array_equals(results, ["a", "b"]); + + source.complete(); + assert_array_equals(results, ["a", "b"], + "Result observable does not complete when source observable completes, " + + "because inner is still active"); + + inner.next("c"); + assert_array_equals(results, ["a", "b", "c"]); + + inner.complete(); + assert_array_equals(results, ["a", "b", "c", "complete"], + "Result observable completes when inner observable completes, because " + + "source is already complete"); +}, "switchMap(): result does not complete when the source observable " + + "completes, if the inner observable is still active"); + +test(() => { + const source = createTestSubject(); + + const e = new Error('thrown from mapper'); + const result = source.switchMap(() => { + throw e; + }); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + assert_equals(source.subscriberCount(), 1, + "source observable is subscribed to"); + + source.next(1); + assert_array_equals(results, [e]); + assert_equals(source.subscriberCount(), 0, + "source observable is unsubscribed from"); +}, "switchMap(): result emits an error if Mapper callback throws an error"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + + const result = source.switchMap(() => inner); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + source.next(1); + inner.next("a"); + assert_array_equals(results, ["a"]); + + const e = new Error('error from source'); + source.error(e); + assert_array_equals(results, ["a", e], + "switchMap result emits an error if the source emits an error"); + assert_equals(inner.subscriberCount(), 0, + "inner observable is unsubscribed from"); + assert_equals(source.subscriberCount(), 0, + "source observable is unsubscribed from"); +}, "switchMap(): result emits an error if the source observable emits an " + + "error"); + +test(() => { + const source = createTestSubject(); + const inner = createTestSubject(); + + const result = source.switchMap(() => inner); + + const results = []; + + result.subscribe({ + next: v => results.push(v), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + source.next(1); + inner.next("a"); + assert_array_equals(results, ["a"]); + + const e = new Error("error from inner"); + inner.error(e); + assert_array_equals(results, ["a", e], + "result emits an error if the inner observable emits an error"); + assert_equals(inner.subscriberCount(), 0, + "inner observable is unsubscribed from"); + assert_equals(source.subscriberCount(), 0, + "source observable is unsubscribed from"); +}, "switchMap(): result emits an error if the inner observable emits an error"); + +test(() => { + const results = []; + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.addTeardown(() => { + results.push('source teardown'); + }); + subscriber.signal.onabort = e => { + results.push('source onabort'); + }; + }); + + const inner = new Observable(subscriber => { + subscriber.addTeardown(() => { + results.push('inner teardown'); + }); + subscriber.signal.onabort = () => { + results.push('inner onabort'); + }; + }); + + const result = source.switchMap(() => inner); + + const ac = new AbortController(); + result.subscribe({ + next: v => results.push(v), + error: e => results.error(e), + complete: () => results.complete("complete"), + }, {signal: ac.signal}); + + ac.abort(); + assert_array_equals(results, [ + "source teardown", + "source onabort", + "inner teardown", + "inner onabort", + ], "Unsubscription order is correct"); +}, "switchMap(): should unsubscribe in the correct order when user aborts " + + "the subscription"); + +// A helper function to create an Observable that can be externally controlled +// and examined for testing purposes. +function createTestSubject() { + const subscribers = new Set(); + const subject = new Observable(subscriber => { + subscribers.add(subscriber); + subscriber.addTeardown(() => subscribers.delete(subscriber)); + }); + + subject.next = value => { + for (const subscriber of Array.from(subscribers)) { + subscriber.next(value); + } + }; + subject.error = error => { + for (const subscriber of Array.from(subscribers)) { + subscriber.error(error); + } + }; + subject.complete = () => { + for (const subscriber of Array.from(subscribers)) { + subscriber.complete(); + } + }; + subject.subscriberCount = () => { + return subscribers.size; + }; + + return subject; +} |