@@ -128,5 +128,169 @@ public async Task AssignedPropertyIsReportedConsistently()
128128 var result = await "(function () { var d = document.createElement('div'); d.custom = 1; return d.hasOwnProperty('custom') + ',' + Object.getOwnPropertyNames(d).join(); })()" . EvalScriptAsync ( ) ;
129129 Assert . AreEqual ( "true,custom" , result ) ;
130130 }
131+
132+ // Reading an index, testing it for existence and enumerating it are three different
133+ // questions the engine asks, and the node answers each of them from a different
134+ // method. They have to agree.
135+ [ Test ]
136+ public async Task IndexedEntryIsReportedAsAnOwnPropertyAndReads ( )
137+ {
138+ var result = await "(function () { var c = document.getElementsByTagName('script'); return c.hasOwnProperty(0) + ',' + (0 in c) + ',' + c[0].nodeName; })()" . EvalScriptAsync ( ) ;
139+ Assert . AreEqual ( "true,true,SCRIPT" , result ) ;
140+ }
141+
142+ [ Test ]
143+ public async Task IndexBeyondTheEndIsNoOwnPropertyAndReadsUndefined ( )
144+ {
145+ var result = await "(function () { var c = document.getElementsByTagName('script'); return c.hasOwnProperty(5) + ',' + (5 in c) + ',' + (typeof c[5]); })()" . EvalScriptAsync ( ) ;
146+ Assert . AreEqual ( "false,false,undefined" , result ) ;
147+ }
148+
149+ // A member of an indexed collection must not be mistaken for an index. "length" is
150+ // the collection's own property rather than an inherited one, which is what the
151+ // array-like projection reports for it - a browser has it on the prototype instead.
152+ [ Test ]
153+ public async Task MemberOfAnIndexedCollectionIsNotMistakenForAnIndex ( )
154+ {
155+ var result = await "(function () { var c = document.getElementsByTagName('script'); return ('length' in c) + ',' + c.length + ',' + c.propertyIsEnumerable('length'); })()" . EvalScriptAsync ( ) ;
156+ Assert . AreEqual ( "true,1,false" , result ) ;
157+ }
158+
159+ // A DOM collection is array-like, not an array - which is exactly what a browser
160+ // reports for one.
161+ [ Test ]
162+ public async Task CollectionIsNotAnArray ( )
163+ {
164+ var result = await "Array.isArray(document.getElementsByTagName('script'))" . EvalScriptAsync ( ) ;
165+ Assert . AreEqual ( "False" , result ) ;
166+ }
167+
168+ [ Test ]
169+ public async Task CollectionKeepsItsDomIdentity ( )
170+ {
171+ var result = await "(function () { var c = document.getElementsByTagName('script'); return Object.prototype.toString.call(c) + ',' + (c instanceof HTMLCollection); })()" . EvalScriptAsync ( ) ;
172+ Assert . AreEqual ( "[object HTMLCollection],true" , result ) ;
173+ }
174+
175+ [ Test ]
176+ public async Task CollectionCanBeIterated ( )
177+ {
178+ var result = await "(function () { var n = 0; for (var s of document.getElementsByTagName('script')) { n += s.nodeName.length; } return n; })()" . EvalScriptAsync ( ) ;
179+ Assert . AreEqual ( "6" , result ) ;
180+ }
181+
182+ [ Test ]
183+ public async Task CollectionCanBeSpread ( )
184+ {
185+ var result = await "[...document.getElementsByTagName('script')].length" . EvalScriptAsync ( ) ;
186+ Assert . AreEqual ( "1" , result ) ;
187+ }
188+
189+ [ Test ]
190+ public async Task ArrayGenericsRunOverACollection ( )
191+ {
192+ var result = await "Array.prototype.map.call(document.getElementsByTagName('script'), function (e) { return e.nodeName; }).join()" . EvalScriptAsync ( ) ;
193+ Assert . AreEqual ( "SCRIPT" , result ) ;
194+ }
195+
196+ [ Test ]
197+ public async Task IndicesOfACollectionAreEnumerated ( )
198+ {
199+ var result = await "JSON.stringify(Object.keys(document.getElementsByTagName('script')))" . EvalScriptAsync ( ) ;
200+ Assert . AreEqual ( "[\" 0\" ]" , result ) ;
201+ }
202+
203+ // The platform-object shape: the projection owns its indices, so script cannot delete
204+ // one or define over it.
205+ [ Test ]
206+ public async Task IndexOfACollectionCannotBeDeleted ( )
207+ {
208+ var result = await "(function () { var c = document.getElementsByTagName('script'); return delete c[0]; })()" . EvalScriptAsync ( ) ;
209+ Assert . AreEqual ( "False" , result ) ;
210+ }
211+
212+ [ Test ]
213+ public async Task ExpandoOnACollectionIsStillPossible ( )
214+ {
215+ var result = await "(function () { var c = document.getElementsByTagName('script'); c.marker = 'kept'; return c.marker + ',' + c.hasOwnProperty('marker'); })()" . EvalScriptAsync ( ) ;
216+ Assert . AreEqual ( "kept,true" , result ) ;
217+ }
218+
219+ // Existence is answered from the collection's length alone, without producing the
220+ // element - so it has to keep agreeing with what reading it would say.
221+ [ Test ]
222+ public async Task ExistenceOfAnIndexAgreesWithReadingIt ( )
223+ {
224+ var result = await "(function () { var c = document.getElementsByTagName('script'); var r = []; for (var i = 0; i < 3; i++) { r.push((i in c) + ':' + (c[i] !== undefined)); } return r.join(); })()" . EvalScriptAsync ( ) ;
225+ Assert . AreEqual ( "true:true,false:false,false:false" , result ) ;
226+ }
227+
228+ [ Test ]
229+ public async Task NumericIndexerOfNamedNodeMapStillReadsBothWays ( )
230+ {
231+ var result = await "(function () { var d = document.createElement('div'); d.setAttribute('title', 't'); var a = d.attributes; return a.length + ',' + a[0].name + ',' + a.title.value; })()" . EvalScriptAsync ( ) ;
232+ Assert . AreEqual ( "1,title,t" , result ) ;
233+ }
234+
235+ // An element whose id happens to be numeric must not surface as an index of the
236+ // collection. The array-like projection owns every array-index key - an index past the
237+ // end is authoritatively absent - so the named entry answers only non-index names,
238+ // which is also how WebIDL resolves the collision. Every answer has to say the same
239+ // thing, the descriptor included.
240+ [ Test ]
241+ public async Task NamedEntryWithAnIndexShapedNameIsNotAnIndex ( )
242+ {
243+ var result = await "(function () { var f = document.createElement('form'); f.id = '5'; document.documentElement.appendChild(f); var c = document.forms; return (Object.getOwnPropertyDescriptor(c, '5') === undefined) + ',' + ('5' in c) + ',' + (c['5'] === undefined) + ',' + c.hasOwnProperty('5') + ',' + (c[0] === f); })()" . EvalScriptAsync ( ) ;
244+ Assert . AreEqual ( "true,false,true,false,true" , result ) ;
245+ }
246+
247+ // The guard above must not overreach: a named entry whose name is not an array index
248+ // keeps resolving, descriptor and all.
249+ [ Test ]
250+ public async Task NamedEntryWithAnOrdinaryNameStillResolves ( )
251+ {
252+ var result = await "(function () { var f = document.createElement('form'); f.id = 'login'; document.documentElement.appendChild(f); var c = document.forms; return (c.login === f) + ',' + (Object.getOwnPropertyDescriptor(c, 'login') !== undefined); })()" . EvalScriptAsync ( ) ;
253+ Assert . AreEqual ( "true,true" , result ) ;
254+ }
255+
256+ // A symbol cannot be an index, and it must not be turned into one either - the
257+ // well-known symbols are probed on every kind of object by library code.
258+ [ Test ]
259+ public async Task SymbolKeyOnAnIndexedCollectionIsNotTreatedAsAnIndex ( )
260+ {
261+ var result = await "(function () { var c = document.getElementsByTagName('script'); return c.hasOwnProperty(Symbol.toStringTag) + ',' + (typeof c[Symbol.toStringTag]); })()" . EvalScriptAsync ( ) ;
262+ Assert . AreEqual ( "false,string" , result ) ;
263+ }
264+
265+ // The node's own property set lives in the DOM, so nothing in the engine changes
266+ // when an entry appears there. A name that was absent has to start resolving on the
267+ // node from the very next read, rather than staying on whatever the prototype said.
268+ [ Test ]
269+ public async Task NamedEntryStartsResolvingOnTheNodeAsSoonAsItExists ( )
270+ {
271+ var result = await "(function () { var d = document.createElement('div'), a = d.attributes, before = typeof a.title; d.setAttribute('title', 't'); return before + ',' + a.title.value + ',' + a.hasOwnProperty('title'); })()" . EvalScriptAsync ( ) ;
272+ Assert . AreEqual ( "undefined,t,true" , result ) ;
273+ }
274+
275+ [ Test ]
276+ public async Task AccessorDefinedOnANodeByScriptIsInvokedOnRead ( )
277+ {
278+ var result = await "(function () { var d = document.createElement('div'); Object.defineProperty(d, 'marker', { get: function () { return 'from getter'; } }); return d.marker + ',' + d.hasOwnProperty('marker'); })()" . EvalScriptAsync ( ) ;
279+ Assert . AreEqual ( "from getter,true" , result ) ;
280+ }
281+
282+ [ Test ]
283+ public async Task NonEnumerablePropertyOfANodeIsSeenButNotEnumerated ( )
284+ {
285+ var result = await "(function () { var d = document.createElement('div'); Object.defineProperty(d, 'hidden2', { value: 1 }); return d.hasOwnProperty('hidden2') + ',' + d.propertyIsEnumerable('hidden2') + ',' + Object.keys(d).length; })()" . EvalScriptAsync ( ) ;
286+ Assert . AreEqual ( "true,false,0" , result ) ;
287+ }
288+
289+ [ Test ]
290+ public async Task PropertyAssignedToANodeIsCopiedAndSerialized ( )
291+ {
292+ var result = await "(function () { var d = document.createElement('div'); d.custom = 'v'; return JSON.stringify(d) + ',' + JSON.stringify(Object.assign({}, d)); })()" . EvalScriptAsync ( ) ;
293+ Assert . AreEqual ( "{\" custom\" :\" v\" },{\" custom\" :\" v\" }" , result ) ;
294+ }
131295 }
132296}
0 commit comments