From 6c58521cfd4e39789aa1c431da54e72ed7beab40 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:51:09 +0900 Subject: [PATCH] fix: truncate fractional slice and splice indices in the array methods plugin --- __tests__/base.js | 40 ++++++++++++++++++++++++++++++++----- src/plugins/arrayMethods.ts | 7 +++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/__tests__/base.js b/__tests__/base.js index 208a1149..0e6e60be 100644 --- a/__tests__/base.js +++ b/__tests__/base.js @@ -60,7 +60,7 @@ function runBaseTest( useListener, useArrayMethods = false ) { - const listener = useListener ? function() {} : undefined + const listener = useListener ? function () {} : undefined const {produce, produceWithPatches} = createPatchedImmer({ autoFreeze, @@ -73,7 +73,7 @@ function runBaseTest( const immer = new Immer(options) const {produce} = immer - immer.produce = function(...args) { + immer.produce = function (...args) { return typeof args[1] === "function" && args.length < 3 ? produce(...args, listener) : produce(...args) @@ -642,6 +642,17 @@ function runBaseTest( }) expect(result.items[1].value).toBe(20) }) + + test("splice with a fractional start inserting a draft reference", () => { + const base = {items: [{id: 1}, {id: 2}], other: {x: 1}} + const result = produce(base, draft => { + // native splice truncates the start index to 1 + draft.items.splice(1.5, 0, {ref: draft.other}) + draft.other.x = 2 + }) + expect(result.items).toEqual([{id: 1}, {ref: {x: 2}}, {id: 2}]) + expect(isDraft(result.items[1].ref)).toBe(false) + }) }) describe("combined operations", () => { @@ -1063,6 +1074,25 @@ function runBaseTest( // Verify result is a copy expect(result.items[3]).not.toBe(base.items[3]) }) + + test("slice converts non-integer indices like the native method", () => { + const base = createTestData() + const cases = [ + [0, 2.5], + [1.5], + [-2.5], + [NaN], + [undefined, -1.5], + [NaN, NaN] + ] + produce(base, draft => { + for (const args of cases) { + expect(draft.items.slice(...args).map(item => item.id)).toEqual( + base.items.slice(...args).map(item => item.id) + ) + } + }) + }) }) describe("flatMap()", () => { @@ -3415,13 +3445,13 @@ function runBaseTest( it("'this' should not be bound anymore - 1", () => { const base = {x: 3} - const next1 = produce(base, function() { + const next1 = produce(base, function () { expect(this).toBe(undefined) }) }) it("'this' should not be bound anymore - 2", () => { - const incrementor = produce(function() { + const incrementor = produce(function () { expect(this).toBe(undefined) }) incrementor() @@ -3430,7 +3460,7 @@ function runBaseTest( it("should be possible to use dynamic bound this", () => { const world = { counter: {count: 1}, - inc: produce(function(draft) { + inc: produce(function (draft) { expect(this).toBe(world) draft.counter.count = this.counter.count + 1 }) diff --git a/src/plugins/arrayMethods.ts b/src/plugins/arrayMethods.ts index 0310e237..d43c4781 100644 --- a/src/plugins/arrayMethods.ts +++ b/src/plugins/arrayMethods.ts @@ -196,7 +196,14 @@ export function enableArrayMethods() { state.baseRefs_ = new Set(state.base_) } + /** + * Resolves a `slice`/`splice` index argument the way the native methods do: + * the value is converted to an integer first (fractions are truncated and + * `NaN` becomes 0), then negative values count back from the end and the + * result is clamped to `[0, length]`. + */ function normalizeSliceIndex(index: number, length: number): number { + index = Math.trunc(Number(index)) || 0 if (index < 0) { return Math.max(length + index, 0) }