@@ -85,43 +85,124 @@ test(() => {
8585 "Subscribing again causes another fresh iteration on an un-exhausted iterable" ) ;
8686} , "from(): Iterable converts to Observable" ) ;
8787
88- // The result of the @@iterator method of the converted object is called:
89- // 1. Once on conversion (to test that the value is an iterable).
90- // 2. Once on subscription, to re-pull the iterator implementation from the
91- // raw JS object that the Observable owns once synchronous iteration is
92- // about to begin.
88+ // This test, and the variants below it, test the web-observable side-effects of
89+ // converting an iterable object to an Observable. Specifically, it tracks
90+ // exactly when the %Symbol.iterator% method is *retrieved* from the object,
91+ // invoked, and what its error-throwing side-effects are.
92+ //
93+ // Even more specifically, we assert that the %Symbol.iterator% method is
94+ // retrieved a single time when converting to an Observable, and then again when
95+ // subscribing to the converted Observable. This makes it possible for the
96+ // %Symbol.iterator% method getter to change return values in between conversion
97+ // and subscription. See https://github.com/WICG/observable/issues/127 for
98+ // related discussion.
9399test ( ( ) => {
94- let numTimesSymbolIteratorCalled = 0 ;
95- let numTimesNextCalled = 0 ;
100+ const results = [ ] ;
96101
97102 const iterable = {
98- [ Symbol . iterator ] ( ) {
99- numTimesSymbolIteratorCalled ++ ;
103+ get [ Symbol . iterator ] ( ) {
104+ results . push ( "[Symbol.iterator] method GETTER" ) ;
105+ return function ( ) {
106+ results . push ( "[Symbol.iterator implementation]" ) ;
107+ return {
108+ get next ( ) {
109+ results . push ( "next() method GETTER" ) ;
110+ return function ( ) {
111+ results . push ( "next() implementation" ) ;
112+ return { value : undefined , done : true } ;
113+ } ;
114+ } ,
115+ } ;
116+ } ;
117+ } ,
118+ } ;
119+
120+ const observable = Observable . from ( iterable ) ;
121+ assert_array_equals ( results , [ "[Symbol.iterator] method GETTER" ] ) ;
122+
123+ let thrownError = null ;
124+ observable . subscribe ( ) ;
125+ assert_array_equals ( results , [
126+ "[Symbol.iterator] method GETTER" ,
127+ "[Symbol.iterator] method GETTER" ,
128+ "[Symbol.iterator implementation]" ,
129+ "next() method GETTER" ,
130+ "next() implementation"
131+ ] ) ;
132+ } , "from(): [Symbol.iterator] side-effects (one observable)" ) ;
133+
134+ // This tests that once `Observable.from()` detects a non-null and non-undefined
135+ // `[Symbol.iterator]` property, we've committed to converting as an iterable.
136+ // If the value of that property is not callable, we don't silently move on to
137+ // the next conversion type — we throw a TypeError;
138+ test ( ( ) => {
139+ let results = [ ] ;
140+ const iterable = {
141+ [ Symbol . iterator ] : 10 ,
142+ } ;
143+
144+ let errorThrown = null ;
145+ try {
146+ Observable . from ( iterable ) ;
147+ } catch ( e ) {
148+ errorThrown = e ;
149+ }
150+
151+ assert_true ( errorThrown instanceof TypeError ) ;
152+ assert_equals ( errorThrown . message ,
153+ "Failed to execute 'from' on 'Observable': @@iterator must be a " +
154+ "callable." ) ;
155+ } , "from(): [Symbol.iterator] not callable" ) ;
156+
157+ test ( ( ) => {
158+ let results = [ ] ;
159+ const customError = new Error ( "@@iterator override error" ) ;
160+
161+ const iterable = {
162+ numTimesCalled : 0 ,
163+
164+ // The first time this getter is called, it returns a legitimate function
165+ // that, when called, returns an iterator. Every other time it returns an
166+ // error-throwing function that does not return an iterator.
167+ get [ Symbol . iterator ] ( ) {
168+ this . numTimesCalled ++ ;
169+ results . push ( "[Symbol.iterator] method GETTER" ) ;
170+
171+ if ( this . numTimesCalled === 1 ) {
172+ return this . validIteratorImplementation ;
173+ } else {
174+ return this . errorThrowingIteratorImplementation ;
175+ }
176+ } ,
177+
178+ validIteratorImplementation : function ( ) {
179+ results . push ( "[Symbol.iterator implementation]" ) ;
100180 return {
101- next ( ) {
102- numTimesNextCalled ++ ;
103- return { value : undefined , done : true } ;
181+ get next ( ) {
182+ results . push ( "next() method GETTER" ) ;
183+ return function ( ) {
184+ results . push ( "next() implementation" ) ;
185+ return { value : undefined , done : true } ;
186+ }
104187 }
105188 } ;
106- }
189+ } ,
190+ errorThrowingIteratorImplementation : function ( ) {
191+ results . push ( "Error-throwing [Symbol.iterator] implementation" ) ;
192+ throw customError ;
193+ } ,
107194 } ;
108195
109196 const observable = Observable . from ( iterable ) ;
110-
111- assert_equals ( numTimesSymbolIteratorCalled , 1 ,
112- "Observable.from(iterable) invokes the @@iterator method getter once" ) ;
113- assert_equals ( numTimesNextCalled , 0 ,
114- "Iterator next() is not called until subscription" ) ;
197+ assert_array_equals ( results , [
198+ "[Symbol.iterator] method GETTER" ,
199+ ] ) ;
115200
116201 // Override iterable's `[Symbol.iterator]` protocol with an error-throwing
117202 // function. We assert that on subscription, this method (the new `@@iterator`
118203 // implementation), is called because only the raw JS object gets stored in
119204 // the Observable that results in conversion. This raw value must get
120205 // re-converted to an iterable once iteration is about to start.
121- const customError = new Error ( '@@iterator override error' ) ;
122- iterable [ Symbol . iterator ] = ( ) => {
123- throw customError ;
124- } ;
125206
126207 let thrownError = null ;
127208 observable . subscribe ( {
@@ -130,56 +211,101 @@ test(() => {
130211
131212 assert_equals ( thrownError , customError ,
132213 "Error thrown from next() is passed to the error() handler" ) ;
133-
134- assert_equals ( numTimesSymbolIteratorCalled , 1 ,
135- "Subscription re-invokes @@iterator method, which now is a different " +
136- "method that does *not* increment our assertion value" ) ;
137- assert_equals ( numTimesNextCalled , 0 , "Iterator next() is never called" ) ;
138- } , "from(): [Symbol.iterator] side-effects (one observable)" ) ;
214+ assert_array_equals ( results , [
215+ // Old:
216+ "[Symbol.iterator] method GETTER" ,
217+ // New:
218+ "[Symbol.iterator] method GETTER" ,
219+ "Error-throwing [Symbol.iterator] implementation"
220+ ] ) ;
221+ } , "from(): [Symbol.iterator] is not cached" ) ;
139222
140223// Similar to the above test, but with more Observables!
141224test ( ( ) => {
225+ const results = [ ] ;
142226 let numTimesSymbolIteratorCalled = 0 ;
143227 let numTimesNextCalled = 0 ;
144228
145229 const iterable = {
146- [ Symbol . iterator ] ( ) {
147- numTimesSymbolIteratorCalled ++ ;
230+ get [ Symbol . iterator ] ( ) {
231+ results . push ( "[Symbol.iterator] method GETTER" ) ;
232+ return this . internalIteratorImplementation ;
233+ } ,
234+ set [ Symbol . iterator ] ( func ) {
235+ this . internalIteratorImplementation = func ;
236+ } ,
237+
238+ internalIteratorImplementation : function ( ) {
239+ results . push ( "[Symbol.iterator] implementation" ) ;
148240 return {
149- next ( ) {
150- numTimesNextCalled ++ ;
151- return { value : undefined , done : true } ;
152- }
241+ get next ( ) {
242+ results . push ( "next() method GETTER" ) ;
243+ return function ( ) {
244+ results . push ( "next() implementation" ) ;
245+ return { value : undefined , done : true } ;
246+ } ;
247+ } ,
153248 } ;
154- }
249+ } ,
155250 } ;
156251
157252 const obs1 = Observable . from ( iterable ) ;
158253 const obs2 = Observable . from ( iterable ) ;
159254 const obs3 = Observable . from ( iterable ) ;
160255 const obs4 = Observable . from ( obs3 ) ;
161-
162- assert_equals ( numTimesSymbolIteratorCalled , 3 , "Observable.from(iterable) invokes the iterator method getter once" ) ;
163- assert_equals ( numTimesNextCalled , 0 , "Iterator next() is not called until subscription" ) ;
256+ assert_equals ( obs3 , obs4 ) ;
257+
258+ assert_array_equals ( results , [
259+ "[Symbol.iterator] method GETTER" ,
260+ "[Symbol.iterator] method GETTER" ,
261+ "[Symbol.iterator] method GETTER" ,
262+ ] ) ;
263+
264+ obs1 . subscribe ( ) ;
265+ assert_array_equals ( results , [
266+ // Old:
267+ "[Symbol.iterator] method GETTER" ,
268+ "[Symbol.iterator] method GETTER" ,
269+ "[Symbol.iterator] method GETTER" ,
270+ // New:
271+ "[Symbol.iterator] method GETTER" ,
272+ "[Symbol.iterator] implementation" ,
273+ "next() method GETTER" ,
274+ "next() implementation" ,
275+ ] ) ;
164276
165277 iterable [ Symbol . iterator ] = ( ) => {
278+ results . push ( "Error-throwing [Symbol.iterator] implementation" ) ;
166279 throw new Error ( 'Symbol.iterator override error' ) ;
167280 } ;
168281
169282 let errorCount = 0 ;
170283
171284 const observer = { error : e => errorCount ++ } ;
172- obs1 . subscribe ( observer ) ;
173285 obs2 . subscribe ( observer ) ;
174286 obs3 . subscribe ( observer ) ;
175287 obs4 . subscribe ( observer ) ;
176- assert_equals ( errorCount , 4 ,
288+ assert_equals ( errorCount , 3 ,
177289 "Error-throwing `@@iterator` implementation is called once per " +
178290 "subscription" ) ;
179291
180- assert_equals ( numTimesSymbolIteratorCalled , 3 ,
181- "Subscription re-invokes the iterator method getter once" ) ;
182- assert_equals ( numTimesNextCalled , 0 , "Iterator next() is never called" ) ;
292+ assert_array_equals ( results , [
293+ // Old:
294+ "[Symbol.iterator] method GETTER" ,
295+ "[Symbol.iterator] method GETTER" ,
296+ "[Symbol.iterator] method GETTER" ,
297+ "[Symbol.iterator] method GETTER" ,
298+ "[Symbol.iterator] implementation" ,
299+ "next() method GETTER" ,
300+ "next() implementation" ,
301+ // New:
302+ "[Symbol.iterator] method GETTER" ,
303+ "Error-throwing [Symbol.iterator] implementation" ,
304+ "[Symbol.iterator] method GETTER" ,
305+ "Error-throwing [Symbol.iterator] implementation" ,
306+ "[Symbol.iterator] method GETTER" ,
307+ "Error-throwing [Symbol.iterator] implementation" ,
308+ ] ) ;
183309} , "from(): [Symbol.iterator] side-effects (many observables)" ) ;
184310
185311test ( ( ) => {
@@ -221,7 +347,8 @@ promise_test(async () => {
221347
222348 await promise ;
223349
224- assert_array_equals ( results , [ "value" , "complete()" ] , "Observable emits and completes after Promise resolves" ) ;
350+ assert_array_equals ( results , [ "value" , "complete()" ] ,
351+ "Observable emits and completes after Promise resolves" ) ;
225352} , "from(): Converts Promise to Observable" ) ;
226353
227354promise_test ( async t => {
@@ -352,3 +479,65 @@ test(() => {
352479 assert_array_equals ( results , [ "from @@iterator" , "complete" ] ) ;
353480} , "from(): Promise that implements @@iterator protocol gets converted as " +
354481 "an iterable, not Promise" ) ;
482+
483+ // When the [Symbol.iterator] method on a given object is undefined, we don't
484+ // try to convert the object to an Observable via the iterable protocol. The
485+ // Observable specification *also* does the same thing if the [Symbol.iterator]
486+ // method is *null*. That is, in that case we also skip the conversion via
487+ // iterable protocol, and continue to try and convert the object as another type
488+ // (in this case, a Promise).
489+ promise_test ( async ( ) => {
490+ const promise = new Promise ( resolve => resolve ( 'from Promise' ) ) ;
491+ assert_equals ( promise [ Symbol . iterator ] , undefined ) ;
492+ promise [ Symbol . iterator ] = null ;
493+ assert_equals ( promise [ Symbol . iterator ] , null ) ;
494+
495+ const value = await new Promise ( resolve => {
496+ Observable . from ( promise ) . subscribe ( value => resolve ( value ) ) ;
497+ } ) ;
498+
499+ assert_equals ( value , 'from Promise' ) ;
500+ } , "from(): Promise whose [Symbol.iterator] returns null converts as Promise" ) ;
501+
502+ // This is a more sensitive test, which asserts that even just trying to reach
503+ // for the [Symbol.iterator] method on an object whose *getter* for the
504+ // [Symbol.iterator] method throws an error, results in `Observable#from()`
505+ // rethrowing that error.
506+ test ( ( ) => {
507+ const error = new Error ( 'thrown from @@iterator getter' ) ;
508+ const obj = {
509+ get [ Symbol . iterator ] ( ) {
510+ throw error ;
511+ }
512+ }
513+
514+ try {
515+ Observable . from ( obj ) ;
516+ assert_unreached ( "from() conversion throws" ) ;
517+ } catch ( e ) {
518+ assert_equals ( e , error ) ;
519+ }
520+ } , "from(): Rethrows the error when Converting an object whose @@iterator " +
521+ "method *getter* throws an error" ) ;
522+
523+ test ( ( ) => {
524+ const obj = { } ;
525+ // Non-undefined & non-null values of the `@@iterator` property are not
526+ // allowed. Specifically they fail the the `IsCallable()` test, which fails
527+ // Observable conversion.
528+ obj [ Symbol . iterator ] = 10 ;
529+
530+ try {
531+ Observable . from ( obj ) ;
532+ assert_unreached ( "from() conversion throws" ) ;
533+ } catch ( e ) {
534+ assert_true ( e instanceof TypeError ) ;
535+ assert_equals ( e . message ,
536+ "Failed to execute 'from' on 'Observable': @@iterator must be a callable." ) ;
537+ }
538+ } , "from(): Throws 'callable' error when @@iterator property is a " +
539+ "non-callable primitive" ) ;
540+
541+ // TODO([email protected] ): Add another test like the above, but for 542+ // `[Symbol.asyncIterator] = null` falling back to `[Symbol.iterator]`
543+ // conversion.
0 commit comments