summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/dom/observable/tentative/observable-from.any.js
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/dom/observable/tentative/observable-from.any.js')
-rw-r--r--testing/web-platform/tests/dom/observable/tentative/observable-from.any.js354
1 files changed, 354 insertions, 0 deletions
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");