diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index a935712eb980..54137f6db19d 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -2951,15 +2951,29 @@ pub mod formatter { } } + /// The opening bracket of an array while its named properties print. `Pending`: not written. + #[derive(Clone, Copy, PartialEq, Eq)] + pub(crate) enum ArrayOpen { + /// An object, or an array whose first element wrote the bracket. + No, + Pending, + Inline, + Multiline, + } + pub(crate) struct PropertyIteratorCtx<'a, 'b, const C: bool> { pub(crate) formatter: &'a mut Formatter<'b>, pub(crate) writer: &'a mut dyn bun_io::Write, pub(crate) i: usize, pub(crate) single_line: bool, pub(crate) always_newline: bool, + pub(crate) array_open: ArrayOpen, pub(crate) parent: JSValue, } + // Lives in the frame of every nested `print_object` and `print_array`. + const _: () = assert!(core::mem::size_of::>() == 48); + impl<'a, 'b, const C: bool> PropertyIteratorCtx<'a, 'b, C> { pub(crate) fn handle_first_property( &mut self, @@ -2993,6 +3007,34 @@ pub mod formatter { Ok(()) } + /// Writes the bracket that `print_array` held back for an array with no elements. + #[cold] + #[inline(never)] + fn open_array(&mut self) { + let indent = self.formatter.indent; + let mut writer = WrappedWriter { + ctx: &mut *self.writer, + failed: false, + estimated_line_length: &mut self.formatter.estimated_line_length, + }; + let was_good_time = + self.formatter.always_newline_scope || writer.good_time_for_a_new_line(indent); + if !self.single_line && (self.formatter.ordered_properties || was_good_time) { + writer.reset_line(indent); + writer.write_all(b"[\n"); + writer.write_indent(indent); + writer.add_for_new_line(1); + self.array_open = ArrayOpen::Multiline; + } else { + writer.write_all(b"[ "); + writer.add_for_new_line(2); + self.array_open = ArrayOpen::Inline; + } + if writer.failed { + self.formatter.failed = true; + } + } + /// Prints `key:` (with quoting/symbol decoration) and the optional /// string-value colour prefix. Hoisted out of `for_each` so its /// `format_args!` temporaries are popped before the recursive @@ -3119,7 +3161,9 @@ pub mod formatter { } if ctx.i == 0 { - if ctx.handle_first_property(global_this, ctx.parent).is_err() { + if ctx.array_open == ArrayOpen::Pending { + ctx.open_array(); + } else if ctx.handle_first_property(global_this, ctx.parent).is_err() { return None; } } @@ -4285,8 +4329,12 @@ pub mod formatter { value: JSValue, js_type: jsc::JSType, ) -> JsResult<()> { - let len = value.get_length(self.global_this)?; - if len != 0 && self.depth > self.max_depth { + // `named`: no elements, and an enumerable named property to print. + let (len, named) = match value.array_length_and_named_keys() { + Some((len, named)) => (u64::from(len), named), + None => (value.get_length(self.global_this)?, false), + }; + if (len != 0 || named) && self.depth > self.max_depth { return self.print_depth_exceeded_marker::(writer_, "Array"); } // Cache once: `disable_inspect_custom` does not change inside this @@ -4307,7 +4355,7 @@ pub mod formatter { // TODO: DerivedArray does not get passed along in JSType, and it's // not clear why. - if len == 0 { + if len == 0 && !named { writer.write_all(b"[]"); writer.add_for_new_line(2); return Ok(()); @@ -4329,6 +4377,10 @@ pub mod formatter { let _qs = defer_restore!(self.quote_strings, prev_quote_strings); let mut empty_start: Option = None; 'first: { + if len == 0 { + // `open_array` writes the bracket with the first property. + break 'first; + } let element = value.get_direct_index(self.global_this, 0)?; let tag = Tag::get_advanced(element, self.global_this, tag_opts)?; @@ -4365,7 +4417,7 @@ pub mod formatter { } } - let mut i: u32 = 1; + let mut i = u32::from(len != 0); let mut nonempty_count: u32 = 1; while (i as u64) < len { @@ -4512,7 +4564,9 @@ pub mod formatter { // Hoist field reads before `formatter: self` reborrows the // whole `*self` (struct-literal field order is not eval // order in the borrow checker's eyes once `self` is moved). - let always_newline = !self.single_line + // `open_array` reads the line length of an empty array: do not reset it. + let always_newline = len != 0 + && !self.single_line && (self.always_newline_scope || self.good_time_for_a_new_line()); let single_line = self.single_line; let global_this = self.global_this; @@ -4521,6 +4575,11 @@ pub mod formatter { writer: writer_, always_newline, single_line, + array_open: if len == 0 { + ArrayOpen::Pending + } else { + ArrayOpen::No + }, parent: value, i: i as usize, }; @@ -4529,6 +4588,7 @@ pub mod formatter { (&raw mut iter).cast::(), PropertyIteratorCtx::::for_each, )?; + let array_open = iter.array_open; if self.failed { return Ok(()); } @@ -4537,6 +4597,18 @@ pub mod formatter { failed: false, estimated_line_length: &mut self.estimated_line_length, }; + match array_open { + // No property printed. + ArrayOpen::Pending => { + writer.write_all(b"[]"); + if writer.failed { + self.failed = true; + } + return Ok(()); + } + ArrayOpen::Multiline => was_good_time = true, + ArrayOpen::Inline | ArrayOpen::No => {} + } } } @@ -5432,6 +5504,7 @@ pub mod formatter { writer: writer_, always_newline, single_line, + array_open: ArrayOpen::No, parent: value, i: 0, }; diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 6486f2d70b38..ffe1dd119f6d 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -2575,6 +2575,16 @@ impl JSValue { index => Some(index as u32), } } + /// For an `Array`: its length, and whether it is empty and has an enumerable named property. + pub(crate) fn array_length_and_named_keys(self) -> Option<(u32, bool)> { + unsafe extern "C" { + safe fn Bun__JSArray__lengthAndNamedKeys(this: JSValue) -> u64; + } + match Bun__JSArray__lengthAndNamedKeys(self) { + u64::MAX => None, + bits => Some((bits as u32, bits >> 32 != 0)), + } + } /// `JSValue.getNameProperty` — the value's `.name` (function/class /// name). Empty for empty/`undefined`/`null`. pub fn get_name_property(self, global: &JSGlobalObject) -> JsResult { diff --git a/src/jsc/bindings/UtilInspect.cpp b/src/jsc/bindings/UtilInspect.cpp index 85d25bf22bfd..814106725077 100644 --- a/src/jsc/bindings/UtilInspect.cpp +++ b/src/jsc/bindings/UtilInspect.cpp @@ -73,6 +73,34 @@ extern "C" JSC::EncodedJSValue JSC__JSValue__callCustomInspectFunction( RELEASE_AND_RETURN(scope, JSValue::encode(inspectRet)); } +// The length, plus bit 32 when the array is empty and has an enumerable named key. UINT64_MAX: not an ArrayType cell. +extern "C" uint64_t Bun__JSArray__lengthAndNamedKeys(JSC::EncodedJSValue encodedValue) +{ + static constexpr uint64_t namedKeys = 1ULL << 32; + + JSValue value = JSValue::decode(encodedValue); + if (!value.isCell() || value.asCell()->type() != ArrayType) + return std::numeric_limits::max(); + + JSArray* array = uncheckedDowncast(value.asCell()); + uint64_t length = array->length(); + Structure* structure = array->structure(); + if (length || (!structure->outOfLineSize() && !structure->inlineSize())) + return length; + + VM& vm = array->vm(); + bool found = false; + structure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { + if ((entry.attributes() & PropertyAttribute::DontEnum) || PropertyName(entry.key()).isPrivateName()) + return true; + if (WTF::equal(entry.key(), "constructor"_s)) // the property walk prints no key with this text, a symbol too + return true; + found = true; + return false; + }); + return found ? namedKeys : 0; +} + // Port of V8's `internalBinding('util').getOwnNonIndexProperties(object, filter)` used by // util.inspect and the REPL completer: own keys minus array indices, without materializing // a name (or descriptor) per element the way Object.getOwnPropertyNames() on an array would. diff --git a/src/runtime/bake/client/inspect.ts b/src/runtime/bake/client/inspect.ts index 522725bb6da8..00b3e4eb5e41 100644 --- a/src/runtime/bake/client/inspect.ts +++ b/src/runtime/bake/client/inspect.ts @@ -255,9 +255,6 @@ function formatObject(ctx: InspectContext, value: any, recurseTimes: number) { * @returns {string} Formatted array */ function formatArray(ctx: InspectContext, value: any[], recurseTimes: number) { - // Special case for empty arrays - if (value.length === 0) return "[]"; - const maxLength = Math.min(ctx.maxArrayLength!, value.length); const output: string[] = []; @@ -279,12 +276,17 @@ function formatArray(ctx: InspectContext, value: any[], recurseTimes: number) { return !(Number(key) >= 0 && Number(key) < value.length && Number(key) === +key); }); - if (keys.length > 0) { - for (const key of keys) { - output.push(`${key}: ${formatValue(ctx, value[key], recurseTimes)}`); + for (const key of keys) { + try { + const desc = Object.getOwnPropertyDescriptor(value, key); + const accessor = desc?.get ? (desc.set ? "[Getter/Setter]" : "[Getter]") : desc?.set ? "[Setter]" : null; + output.push(`${key}: ${accessor ?? formatValue(ctx, value[key], recurseTimes)}`); + } catch { + output.push(`${key}: undefined`); } } + if (output.length === 0) return "[]"; return `[ ${output.join(", ")} ]`; } diff --git a/test/bake/dev/esm.test.ts b/test/bake/dev/esm.test.ts index 82f69cf6f377..3c782f9dd4fa 100644 --- a/test/bake/dev/esm.test.ts +++ b/test/bake/dev/esm.test.ts @@ -512,6 +512,47 @@ devTest("browser console forwarding strips terminal control bytes", { }, }); +// https://github.com/oven-sh/bun/issues/22569 +devTest("browser console forwarding prints the named properties of an array with no elements", { + files: { + "index.html": emptyHtmlFile({ + scripts: ["index.ts"], + }), + "index.ts": ` + console.log("array-probe", Object.assign([], { total: 7001 }), Object.assign([5], { total: 7001 }), []); + let calls = 0; + const lazy = { get: () => ++calls, enumerable: true }; + console.log("getter-probe", Object.defineProperty([], "lazy", lazy), Object.defineProperty([5], "lazy", lazy)); + console.log("getter-calls", calls); + `, + "bun.app.ts": ` + import html from "./index.html"; + export default { + static: { + "/": html, + }, + development: { + console: true, + }, + fetch(req) { + return new Response("Not Found", { status: 404 }); + }, + }; + `, + }, + htmlFiles: [], + async test(dev) { + await using c = await dev.client("/"); + await c.expectMessage("array-probe", "getter-probe", "getter-calls"); + const line = await dev.output.waitForLine(/\[browser\].*array-probe/); + expect(line.input).toContain("array-probe [ total: 7001 ] [ 5, total: 7001 ] []"); + const getters = await dev.output.waitForLine(/\[browser\].*getter-probe/); + expect(getters.input).toContain("getter-probe [ lazy: [Getter] ] [ 5, lazy: [Getter] ]"); + const calls = await dev.output.waitForLine(/\[browser\].*getter-calls/); + expect(calls.input).toContain("getter-calls 0"); + }, +}); + devTest("error report endpoint tolerates a browser url whose normalized origin is longer than the input", { framework: minimalFramework, files: { diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 8e502eb2bab0..9e964a46dc6a 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1184,6 +1184,249 @@ describe.skipIf(!isASAN)("object mutated while being formatted", () => { }); }); +// https://github.com/oven-sh/bun/issues/22569 +describe("Bun.inspect array with no elements and non-indexed properties", () => { + const keyed = (keys = { total: 7001 }) => Object.assign([], keys); + const withOneElement = keys => Object.assign([1], keys); + const pastTheDepthLimit = value => Bun.inspect({ x: value }, { depth: 0 }); + + it("prints the properties", () => { + class Rows extends Array {} + const emptied = Object.assign([1, 2], { total: 7001 }); + emptied.length = 0; + const circular = []; + circular.self = circular; + + expect({ + oneKey: Bun.inspect(keyed()), + twoKeys: Bun.inspect(keyed({ count: 7001, command: "S7002" })), + symbolKey: Bun.inspect(keyed({ [Symbol("s")]: 1 })), + subclass: Bun.inspect(Object.assign(new Rows(), { total: 7001 })), + emptied: Bun.inspect(emptied), + frozen: Bun.inspect(Object.freeze(keyed())), + besideNonEnumerable: Bun.inspect(Object.defineProperty(keyed(), "hidden", { value: 1 })), + proxy: Bun.inspect(new Proxy(keyed(), {})), + circular: Bun.inspect(circular), + inObject: Bun.inspect({ rows: keyed() }), + inArray: Bun.inspect([keyed(), [], keyed()]), + mapValue: Bun.inspect(new Map([["rows", keyed()]])), + setElement: Bun.inspect(new Set([keyed()])), + }).toEqual({ + oneKey: "[ total: 7001 ]", + twoKeys: '[ count: 7001, command: "S7002" ]', + symbolKey: "[ [Symbol(s)]: 1 ]", + subclass: "[ total: 7001 ]", + emptied: "[ total: 7001 ]", + frozen: "[ total: 7001 ]", + besideNonEnumerable: "[ total: 7001 ]", + proxy: "[ total: 7001 ]", + circular: "[ self: [Circular] ]", + inObject: "{\n rows: [ total: 7001 ],\n}", + inArray: "[\n [ total: 7001 ], [], [ total: 7001 ]\n]", + mapValue: 'Map(1) {\n "rows": [ total: 7001 ],\n}', + setElement: "Set(1) {\n [ total: 7001 ],\n}", + }); + }); + + it("does not call a getter", () => { + let calls = 0; + const array = Object.defineProperty([], "lazy", { get: () => ++calls, enumerable: true }); + expect(Bun.inspect(array)).toBe("[ lazy: [Getter] ]"); + expect(calls).toBe(0); + }); + + it("throws what the inspect.custom of a property value throws", () => { + const thrower = { + [Bun.inspect.custom]() { + throw new Error("boom"); + }, + }; + expect(() => Bun.inspect(keyed({ x: thrower }))).toThrow(new Error("boom")); + }); + + it("uses the format of an array that has elements", () => { + const meta = { meta: { a: 1 } }; + const twoKeys = { b: 2, total: 7001 }; + const longKey = Buffer.alloc(90, "k").toString(); + const manyKeys = Object.fromEntries(Array.from({ length: 24 }, (_, i) => [`key${i}`, i])); + + expect({ + objectValue: Bun.inspect(keyed(meta)), + objectValueWithOneElement: Bun.inspect(withOneElement(meta)), + sorted: Bun.inspect(keyed(twoKeys), { sorted: true }), + sortedWithOneElement: Bun.inspect(withOneElement(twoKeys), { sorted: true }), + colors: Bun.inspect(keyed(), { colors: true }), + colorsWithOneElement: Bun.inspect(withOneElement({ total: 7001 }), { colors: true }), + afterColumn80: Bun.inspect({ [longKey]: keyed() }), + afterColumn80WithOneElement: Bun.inspect({ [longKey]: withOneElement({ total: 7001 }) }), + propertiesPassColumn80: Bun.inspect(keyed(manyKeys)), + }).toEqual({ + objectValue: "[ meta: {\n a: 1,\n } ]", + objectValueWithOneElement: "[ 1, meta: {\n a: 1,\n } ]", + sorted: "[\n b: 2, total: 7001\n]", + sortedWithOneElement: "[\n 1, b: 2, total: 7001\n]", + colors: "[ \x1b[0mtotal\x1b[2m:\x1b[0m \x1b[0m\x1b[33m7001\x1b[0m ]", + colorsWithOneElement: + "[ \x1b[0m\x1b[33m1\x1b[0m\x1b[0m\x1b[2m,\x1b[0m \x1b[0mtotal\x1b[2m:\x1b[0m \x1b[0m\x1b[33m7001\x1b[0m ]", + afterColumn80: `{\n ${longKey}: [\n total: 7001\n ],\n}`, + afterColumn80WithOneElement: `{\n ${longKey}: [\n 1, total: 7001\n ],\n}`, + propertiesPassColumn80: + "[ key0: 0, key1: 1, key2: 2, key3: 3, key4: 4, key5: 5, key6: 6, key7: 7, key8: 8, key9: 9,\n" + + " key10: 10, key11: 11, key12: 12, key13: 13, key14: 14, key15: 15, key16: 16, key17: 17,\n" + + " key18: 18, key19: 19, key20: 20, key21: 21, key22: 22, key23: 23 ]", + }); + }); + + it("stays on one line in a single-line format", () => { + const meta = { meta: { a: 1 } }; + const twoKeys = { b: 2, total: 7001 }; + const longKey = Buffer.alloc(90, "k").toString(); + + expect({ + compact: Bun.inspect(keyed(meta), { compact: true }), + compactWithOneElement: Bun.inspect(withOneElement(meta), { compact: true }), + compactSorted: Bun.inspect(keyed(twoKeys), { compact: true, sorted: true }), + compactSortedWithOneElement: Bun.inspect(withOneElement(twoKeys), { compact: true, sorted: true }), + compactAfterColumn80: Bun.inspect({ [longKey]: keyed() }, { compact: true }), + tableCell: Bun.inspect.table([{ a: keyed(twoKeys) }], { sorted: true }), + }).toEqual({ + compact: "[ meta: { a: 1 } ]", + compactWithOneElement: "[ 1, meta: { a: 1 } ]", + compactSorted: "[ b: 2, total: 7001 ]", + compactSortedWithOneElement: "[ 1, b: 2, total: 7001 ]", + compactAfterColumn80: `{ ${longKey}: [ total: 7001 ] }`, + tableCell: + "┌───┬───────────────────────┐\n" + + "│ │ a │\n" + + "├───┼───────────────────────┤\n" + + "│ 0 │ [ b: 2, total: 7001 ] │\n" + + "└───┴───────────────────────┘\n", + }); + }); + + it("stops at the depth limit like an array that has elements", () => { + const meta = { meta: { a: 1 } }; + + expect({ + nested: Bun.inspect({ keyed: keyed(), one: [1], plain: [] }, { depth: 0 }), + objectValue: Bun.inspect(keyed(meta), { depth: 0 }), + objectValueWithOneElement: Bun.inspect(withOneElement(meta), { depth: 0 }), + }).toEqual({ + nested: "{\n keyed: [Array ...],\n one: [Array ...],\n plain: [],\n}", + objectValue: "[ meta: [Object ...] ]", + objectValueWithOneElement: "[ 1, meta: [Object ...] ]", + }); + }); + + it("prints [] when no property prints", () => { + const deleted = keyed(); + delete deleted.total; + // The shape of a postgres.js result: every property is non-enumerable. + class Result extends Array { + constructor() { + super(); + Object.defineProperties(this, { + count: { value: null, writable: true }, + state: { value: { pid: 1 }, writable: true }, + command: { value: null, writable: true }, + }); + } + static get [Symbol.species]() { + return Array; + } + } + class WithPrivateField extends Array { + #secret = 1; + } + + expect({ + plain: Bun.inspect([]), + deleted: Bun.inspect(deleted), + nonEnumerable: Bun.inspect(Object.defineProperty([], "hidden", { value: 1 })), + result: Bun.inspect(new Result()), + privateField: Bun.inspect(new WithPrivateField()), + arrayPrototype: Bun.inspect(Array.prototype), + args: Bun.inspect( + (function () { + return arguments; + })(), + ), + // The property walk prints no key whose text is `constructor`: a string or a symbol. + constructorKey: Bun.inspect(keyed({ constructor: 5 })), + constructorSymbol: Bun.inspect(keyed({ [Symbol("constructor")]: 1 })), + constructorKeyAndTotal: Bun.inspect(keyed({ constructor: 5, total: 7001 })), + constructorSymbolAndTotal: Bun.inspect(keyed({ [Symbol("constructor")]: 1, total: 7001 })), + }).toEqual({ + plain: "[]", + deleted: "[]", + nonEnumerable: "[]", + result: "[]", + privateField: "[]", + arrayPrototype: "[]", + args: "[]", + constructorKey: "[]", + constructorSymbol: "[]", + constructorKeyAndTotal: "[ total: 7001 ]", + constructorSymbolAndTotal: "[ total: 7001 ]", + }); + + expect({ + nonEnumerable: pastTheDepthLimit(Object.defineProperty([], "hidden", { value: 1 })), + privateField: pastTheDepthLimit(new WithPrivateField()), + constructorKey: pastTheDepthLimit(keyed({ constructor: 5 })), + constructorSymbol: pastTheDepthLimit(keyed({ [Symbol("constructor")]: 1 })), + }).toEqual({ + nonEnumerable: "{\n x: [],\n}", + privateField: "{\n x: [],\n}", + constructorKey: "{\n x: [],\n}", + constructorSymbol: "{\n x: [],\n}", + }); + + // An array that has elements hides the same keys. If it prints one, the array with no elements must too. + expect({ + nonEnumerable: Bun.inspect(Object.defineProperty([1], "hidden", { value: 1 })), + constructorKey: Bun.inspect(withOneElement({ constructor: 5 })), + constructorSymbol: Bun.inspect(withOneElement({ [Symbol("constructor")]: 1 })), + }).toEqual({ + nonEnumerable: "[ 1 ]", + constructorKey: "[ 1 ]", + constructorSymbol: "[ 1 ]", + }); + }); + + it("keeps the line breaks around arrays that have elements", () => { + const emptyArrays = count => Array.from({ length: count }, () => []); + + expect({ + longLine: Bun.inspect([1, Buffer.alloc(100, "x").toString()]), + emptyArrays: Bun.inspect(emptyArrays(21)), + emptyArraysAndKey: Bun.inspect([...emptyArrays(21), keyed()]), + }).toEqual({ + longLine: `[ 1, "${Buffer.alloc(100, "x").toString()}" ]`, + emptyArrays: "[\n [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [],\n []\n]", + emptyArraysAndKey: + "[\n [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [],\n [], [ total: 7001 ]\n]", + }); + }); + + it("prints the properties in a matcher message and not in a snapshot", () => { + let message; + try { + expect(keyed()).toBeNull(); + } catch (error) { + message = Bun.stripANSI(error.message); + } + expect(message).toBe("expect(received).toBeNull()\n\nReceived: [ total: 7001 ]\n"); + + expect(keyed()).toMatchInlineSnapshot(`[]`); + expect({ rows: keyed() }).toMatchInlineSnapshot(` + { + "rows": [], + } + `); + }); +}); + describe("depth cap applies to Map/Set/Array and Error cause chains", () => { it("Map respects max_depth", () => { let m = new Map([["leaf", 1]]); diff --git a/test/js/sql/sqlite-sql.test.ts b/test/js/sql/sqlite-sql.test.ts index 71aecee481e8..db5b50299a74 100644 --- a/test/js/sql/sqlite-sql.test.ts +++ b/test/js/sql/sqlite-sql.test.ts @@ -925,6 +925,26 @@ describe("Query Execution", () => { expect(result.command).toBe("DELETE"); }); + // https://github.com/oven-sh/bun/issues/22569 + test("a result with no rows prints count, command, lastInsertRowid and affectedRows", async () => { + await sql`CREATE TABLE printed (id INTEGER PRIMARY KEY, name TEXT)`; + const inserted = await sql`INSERT INTO printed (name) VALUES ('a')`; + const updated = await sql`UPDATE printed SET name = 'b'`; + const selected = await sql`SELECT * FROM printed WHERE id = 2`; + + expect({ + rows: [inserted.length, updated.length, selected.length], + inserted: Bun.inspect(inserted), + updated: Bun.inspect(updated), + selected: Bun.inspect(selected), + }).toEqual({ + rows: [0, 0, 0], + inserted: '[ count: 1, command: "INSERT", lastInsertRowid: 1, affectedRows: 1 ]', + updated: '[ count: 1, command: "UPDATE", lastInsertRowid: 1, affectedRows: 1 ]', + selected: '[ count: 0, command: "SELECT", lastInsertRowid: null, affectedRows: 0 ]', + }); + }); + test("SELECT with various clauses", async () => { await sql`CREATE TABLE scores (id INTEGER, player TEXT, score INTEGER, team TEXT)`; await sql`INSERT INTO scores VALUES diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt index 6b1d807fbec7..022c03a563d9 100644 --- a/test/js/web/console/console-log.expected.txt +++ b/test/js/web/console/console-log.expected.txt @@ -231,3 +231,8 @@ Hello NaN 1 Hello \5 6, Hello %i 5 6 %d 1 +[ field_1: "field_1" ] +[ 1, 2, 3, field_1: "field_1" ] +[] +[ field1: "field_1", field2: 2 ] +[ 1, 2, 3, field1: "field_1", field2: 2 ] diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js index 91c3bddd0278..a24d84ca9e77 100644 --- a/test/js/web/console/console-log.js +++ b/test/js/web/console/console-log.js @@ -270,3 +270,32 @@ console.log("Hello %%i %i", 5, 6); // doesn't go out of bounds when printing console.log("%%d", 1); + +// Test array with extra properties +{ + // empty array with extra property + const emptyArrayWithItem = []; + emptyArrayWithItem["field_1"] = "field_1"; + console.log(emptyArrayWithItem); + + // Non-empty array with extra property + const nonEmptyArrayWithItem = [1, 2, 3]; + nonEmptyArrayWithItem["field_1"] = "field_1"; + console.log(nonEmptyArrayWithItem); + + // empty array + const emptyArray = []; + console.log(emptyArray); + + // Multiple properties on empty array + const emptyArrayWithItems = []; + emptyArrayWithItems.field1 = "field_1"; + emptyArrayWithItems.field2 = 2; + console.log(emptyArrayWithItems); + + // Multiple properties on non empty array + const nonEmptyArrayWithItems = [1, 2, 3]; + nonEmptyArrayWithItems.field1 = "field_1"; + nonEmptyArrayWithItems.field2 = 2; + console.log(nonEmptyArrayWithItems); +}