From 2904f993be3cbbb81091f047c90b50846f2aa9f6 Mon Sep 17 00:00:00 2001
From: isaacs <i@izs.me>
Date: Mon, 6 Dec 2021 14:38:00 -0800
Subject: [PATCH] Handle invalid iterators

If an object has Symbol.iterator method, but it throws when used with
Array.from, then it is actually not an Array-like.

Detect this situation, and switch to pojo-mode for those types of
objects.

Fix: https://github.com/tapjs/node-tap/issues/791
---
 lib/format.js                         | 16 +++++++++++++---
 tap-snapshots/test/format.js.test.cjs |  4 ++++
 test/format.js                        | 13 +++++++++++++
 test/same.js                          |  2 +-
 4 files changed, 31 insertions(+), 4 deletions(-)

diff --git a/lib/format.js b/lib/format.js
index 8879c76..38df6b4 100644
--- a/lib/format.js
+++ b/lib/format.js
@@ -1,3 +1,10 @@
+const arrayFrom = obj => {
+  try {
+    return Array.from(obj)
+  } catch (e) {
+    return null
+  }
+}
 class Format {
   constructor (obj, options = {}) {
     this.options = options
@@ -51,9 +58,12 @@ class Format {
 
   get objectAsArray () {
     const value = Array.isArray(this.object) ? this.object
-      : this.isArray() ? Array.from(this.object)
+      : this.isArray() ? arrayFrom(this.object)
       : null
 
+    if (value === null)
+      this.isArray = () => false
+
     Object.defineProperty(this, 'objectAsArray', { value })
     return value
   }
@@ -207,7 +217,7 @@ class Format {
       : this.isSet() ? this.set()
       : this.isMap() ? this.map()
       : this.isBuffer() ? this.buffer()
-      : this.isArray() ? this.array()
+      : this.isArray() && this.objectAsArray ? this.array()
       // TODO streams, JSX
       : this.pojo()
 
@@ -441,7 +451,7 @@ class Format {
           }
         }
       }
-      return Array.from(own)
+      return arrayFrom(own)
     } else
       return Object.keys(obj || this.object)
   }
diff --git a/tap-snapshots/test/format.js.test.cjs b/tap-snapshots/test/format.js.test.cjs
index 3c96dd2..26f2812 100644
--- a/tap-snapshots/test/format.js.test.cjs
+++ b/tap-snapshots/test/format.js.test.cjs
@@ -3374,6 +3374,10 @@ Null Object {
 }
 `
 
+exports[`test/format.js TAP invalid iterator > must match snapshot 1`] = `
+Object {}
+`
+
 exports[`test/format.js TAP locale sorting > must match snapshot 1`] = `
 Object {
   "cat": "meow",
diff --git a/test/format.js b/test/format.js
index 6b5cf7d..a8f7592 100644
--- a/test/format.js
+++ b/test/format.js
@@ -256,3 +256,16 @@ t.test('locale sorting', t => {
   t.matchSnapshot(format(obj, { sort: true }))
   t.end()
 })
+
+t.test('invalid iterator', t => {
+  const obj = { [Symbol.iterator] () { return {} } }
+  t.matchSnapshot(format(obj))
+  const f = new Format(obj)
+  // looks like an array
+  t.equal(f.isArray(), true)
+  // until you try to format it
+  t.equal(f.print(), 'Object {}')
+  // then it realizes it's actually not
+  t.equal(f.isArray(), false)
+  t.end()
+})
diff --git a/test/same.js b/test/same.js
index a3f3fac..602d2a9 100644
--- a/test/same.js
+++ b/test/same.js
@@ -79,7 +79,7 @@ t.test('array-likes', t => {
   a.push(1,2,3)
   const b = [1, 2, 3]
   t.ok(same(t, a, b))
-  t.notEqual(a.constructor, b.constructor)
+  t.not(a.constructor, b.constructor)
 
   const args = (function () { return arguments })(1,2,3)
   const o = {[Symbol.iterator]: function*() { for (let i of a) { yield i } } }