Conversation
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
Decision: REQUEST_CHANGES
Your test suite is strong and covers many standard behaviors, but several required edge cases are missing, which are critical blockers for full spec compliance. These gaps mean important failure modes may go untested.
Critical issues to fix (with suggestions):
-
TypeError when callback is not a function (requirement 1.13)
- Why: The spec requires throwing a TypeError if the callback is not callable.
- Where: Add a test in
src/reduce.test.js(e.g., near other error-handling tests). - How: For example:
expect(() => [1,2,3].reduce2(null)).toThrow(TypeError);- Also consider
undefined, numbers, strings, and objects as non-functions.
-
TypeError for empty array without an initial value (requirement 1.6, second bullet)
- Why: Native
reducethrows a TypeError in this case; your suite must assert the same. - Where:
src/reduce.test.jsunder empty array scenarios. - How:
expect(() => [].reduce2((a,b) => a + b)).toThrow(TypeError);
- Why: Native
-
Sparse arrays: holes must be skipped (requirement 1.7)
- Why:
reduceskips non-existent indices; your tests should ensure the callback is not called for holes. - Where: Add a dedicated describe block for sparse arrays.
- How: Use a spy and a sparse array:
const arr = [1, , 3];- Track indices seen and assert the callback was called only for indices 0 and 2, not 1.
- Why:
-
Mutation during iteration (requirement 1.9)
- Why: Behavior should match native
reducewhen elements are added/removed during iteration. - Where: New tests in
src/reduce.test.jscomparing behavior with native reduce. - How: Examples:
- Adding elements beyond initial length: inside callback push to the array and assert those new elements are not processed.
- Deleting upcoming elements: delete an index in the callback and assert it’s skipped as in native.
- Why: Behavior should match native
-
Single-element array without initial value (requirement 1.14)
- Why: Spec says the single element is returned and the callback is not called.
- Where: Near your single-element tests.
- How:
const spy = jest.fn(); const result = [42].reduce2(spy); expect(result).toBe(42); expect(spy).not.toHaveBeenCalled();
Minor improvements (optional):
- Consider tests for array-like objects only if your implementation is intended to be generic beyond arrays.
You’ve done a great job on structure and core behaviors—once these edge-case tests are added, the suite will be fully robust and spec-compliant. Keep it up!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Important
Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.
Tip
If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.
Caution
Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.
Footnotes
| array4.reduce2(nothingSpecial2); | ||
| }).not.toThrowError(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
There is no test that verifies that a TypeError is thrown when the callback is not a function, as required by the task (requirement 1.13). Consider adding a test like:
expect(() => [1,2,3].reduce2(null)).toThrow(TypeError);| array4.reduce2(nothingSpecial2); | ||
| }).not.toThrowError(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
There is no test that verifies that a TypeError is thrown when reduce is called on an empty array without an initial value (requirement 1.6, second bullet). Consider adding a test like:
expect(() => [].reduce2((a,b) => a+b)).toThrow(TypeError);| array4.reduce2(nothingSpecial2); | ||
| }).not.toThrowError(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
There is no test that verifies that holes in sparse arrays are skipped (requirement 1.7). Consider adding a test with an array like [1,,3] and checking the callback is not called for the hole.
| array4.reduce2(nothingSpecial2); | ||
| }).not.toThrowError(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
There is no test that verifies mutation during iteration (requirement 1.9). For example, adding or deleting elements during reduce should behave as in native reduce. Consider adding such tests.
| array4.reduce2(nothingSpecial2); | ||
| }).not.toThrowError(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
There is no explicit test for the behavior when reducing a single-element array without an initial value (requirement 1.14). According to the spec, the single element should be returned and the callback should not be called. Consider adding such a test.
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
Decision: Request changes — There are critical, blocking issues that break spec-equivalent behavior and required checklist items.
Critical issues to fix (blocking):
-
src/reduce.js — keys-snapshot iteration breaks spec semantics (3.6/3.7)
- Where: lines using
Object.keys(this)and loops overkeys(e.g., lines 11–13, 28–31, 36–38). - Why: The spec requires checking property existence at visit time. Using a precomputed
keysarray will still visit indices that get deleted before they’re reached, and may mis-handle ordering for array-like objects. - How to fix: Replace keys-based iteration with a numeric loop over a length snapshot. Example approach:
const O = Object(this);const len = O.length >>> 0;(Uint32 snapshot)- Validate callback type first (see item 2).
- If no initial value: scan
for (let i = 0; i < len; i++) { if (i in O) { acc = O[i]; start = i + 1; break; } }; if none found, throw TypeError. - Then iterate
for (let i = start; i < len; i++) { if (!(i in O)) continue; acc = callback(acc, O[i], i, O); }.
- Where: lines using
-
src/reduce.js — missing early callback type check
- Where: near the start of the function, before reading keys or computing indices.
- Why: The spec requires throwing a TypeError if
callbackis not callable; without an early guard, a non-function causes less controlled errors. - How to fix: Add
if (typeof callback !== 'function') { throw new TypeError('callback is not a function'); }before other logic.
-
src/reduce.js — length snapshot and empty-array checks ordering
- Where: the
doThrowErrorlogic currently usesthis.length < 1after other work. - Why: You must take a numeric length snapshot once and use it for all decisions to honor snapshot semantics; also validate callback first.
- How to fix: Move
const len = this.length >>> 0;up (after callback check), and uselenfor the empty/no-initial error path and all loops.
- Where: the
-
src/reduce.js — no-initial-value branch fragile for sparse arrays/array-likes
- Where: logic that assumes
this[keys[0]]is the first element. - Why:
keys[0]may not be the smallest numeric index < length; spec requires finding the first present index in 0..len-1. - How to fix: Use the numeric scan described in item 1 to pick the first present index as the starting accumulator.
- Where: logic that assumes
Required test additions (to ensure compliance):
- Single-element/no-initial: assert callback is NOT called (e.g.,
[x].reduce2(spy)⇒spynot called). - Single-element/with-initial: assert callback called exactly once with correct args and final value.
- Callback this-binding: in strict mode, inside callback assert
this === undefined. - Mutation during iteration: add tests that compare native
reducevsreduce2for:- Appending elements during reduction (new elements beyond initial length not processed).
- Deleting a future index before it’s visited (that index skipped).
- Modifying a future index before visit (updated value observed).
Minor/nice-to-have:
- Add comments explaining the length snapshot and
inchecks to future-proof the implementation.
You’ve covered many behaviors well (callback args, sparse indices, array-like handling). Addressing the iteration strategy, callback guard, and adding the mutation/single-element/this-binding tests will bring your solution in line with the spec and checklist. Keep going—you’re close!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Important
Fix the issues, commit the changes, and then re-request my review using the Re-request review button2 in the Reviewers tab.
Tip
If you have any comments about this review, please leave a message in the Add a comment section below and mention me with the @mateacademy-ai-mentor tag3.
I will read your message during my next review of your pull request. Please do not reply directly to my comments, as that feature is not supported yet.
Caution
Do not close the pull request until the review is complete. Closing it early can lead to unexpected behavior and disrupt the review process.
Footnotes
src/reduce.js
Outdated
| const keys = isArrayLike | ||
| ? Object.keys(this).slice(0, this.length) | ||
| : Object.keys(this); |
There was a problem hiding this comment.
Using a precomputed keys array (Object.keys(...)) for iteration means the implementation visits entries that existed at the start even if they are deleted before the iteration reaches them. The spec requires checking whether the property exists when its index is visited (i.e., deletions before the visit should cause the position to be skipped). Replace this keys-based iteration with a numeric index loop over 0..len-1 and for each index check property existence at visit time (e.g., if (!(i in this)) continue;). This change will also help enforce the "append-beyond-length not processed" semantics by using a length snapshot. See checklist items about mutation during iteration and sparse arrays (3.6, 3.7).
src/reduce.js
Outdated
| let prev = startValue; | ||
| let startIndex = 0; | ||
| let i2 = 0; | ||
| let valueIndex = +keys[i2]; |
There was a problem hiding this comment.
You compute valueIndex = +keys[i2] before validating the callback. Add an explicit check near the start of the function to ensure callback is callable: if (typeof callback !== 'function') { throw new TypeError('callback is not a function'); }. Tests expect a TypeError for non-function callbacks and an early guard makes the behaviour explicit and consistent with the spec and your test-suite.
src/reduce.js
Outdated
| const keys = isArrayLike | ||
| ? Object.keys(this).slice(0, this.length) | ||
| : Object.keys(this); | ||
| const doThrowError = this.length < 1 && startValue === undefined; |
There was a problem hiding this comment.
The doThrowError check uses this.length < 1. Move the length snapshot calculation earlier and perform the callback type check first. Also ensure this.length is treated as a numeric Uint32 (e.g., const len = this.length >>> 0;) and use len for subsequent checks to match spec snapshot semantics.
src/reduce.js
Outdated
| if (arguments.length < 2) { | ||
| startIndex = 1; | ||
| prev = this[0]; | ||
| i2 = 1; | ||
| valueIndex = +keys[i2]; // Pierwszy nie-pusty element | ||
| prev = this[keys[0]]; |
There was a problem hiding this comment.
The no-initial-value branch assumes the first element is this[keys[0]] and then iterates keys.length - 1 times. This is fragile for sparse arrays and array-like objects where numeric ordering matters and where keys[0] may not correspond to the smallest numeric index < length. Instead: (1) compute len, (2) loop for (let i = 0; i < len; i++) to find the first index i with i in this, set prev = this[i] and then continue the loop from i+1 to len-1, checking if (!(j in this)) continue; before calling the callback. This will correctly handle sparse arrays and array-like objects. Also prevents visiting deleted indices (if deleted earlier) and ensures proper starting index semantics.
src/reduce.js
Outdated
| for (let i = 0; i < keys.length - 1; i++, i2++) { | ||
| prev = callback(prev, this[valueIndex], valueIndex, this); | ||
| valueIndex = +keys[i2 + 1]; | ||
| } | ||
|
|
||
| return prev; | ||
| } | ||
|
|
||
| for (let i = startIndex; i < this.length; i++) { | ||
| prev = callback(prev, this[i], i, this); | ||
| for (let i = 0; i < keys.length; i++, i2++) { | ||
| prev = callback(prev, this[valueIndex], valueIndex, this); | ||
| valueIndex = +keys[i2 + 1]; |
There was a problem hiding this comment.
The for-loops (lines 28–31 and 36–38) iterate over keys and use valueIndex = +keys[...]. As above, switch to numeric index iteration up to the length snapshot and check property existence at visit time. This ensures correct ordering and behaviour for holes, deletions, updates, and appended elements beyond the initial length. It also simplifies determining the expected number of callback invocations for tests (dense vs sparse arrays and initial/no-initial cases).
| + `array length = 1 and 'startValue isn't included'`, () => { | ||
| const results = [ | ||
| [6].reduce2(adding), | ||
| [].reduce2(subtracting, 13), | ||
| [24].reduce2(multiplication), | ||
| [].reduce2(average, 91), | ||
| ]; | ||
| const expected = [6, 13, 24, 91]; | ||
|
|
||
| results.forEach((result, i) => { | ||
| expect(result) | ||
| .toBe(expected[i]); | ||
| }); |
There was a problem hiding this comment.
The test that builds results for single-element/no-initial and empty-with-initial (lines 344–356) asserts returned values but does not assert that the callback was NOT invoked for the single-element/no-initial case. The checklist requires that [x].reduce(callback) returns x and does not call callback. Add an explicit spy-based assertion with expect(spy).not.toHaveBeenCalled() for that scenario.
| + `array length = 1 and 'startValue isn't included'`, () => { | ||
| const results = [ | ||
| [6].reduce2(adding), | ||
| [].reduce2(subtracting, 13), | ||
| [24].reduce2(multiplication), | ||
| [].reduce2(average, 91), | ||
| ]; | ||
| const expected = [6, 13, 24, 91]; | ||
|
|
||
| results.forEach((result, i) => { | ||
| expect(result) | ||
| .toBe(expected[i]); | ||
| }); |
There was a problem hiding this comment.
There is no explicit test for the single-element-with-initial-value case that asserts the callback is invoked exactly once with the correct parameters and that the final returned value is correct. Add a test like:
const spy = jest.fn((acc, cur, idx, arr) => acc + cur); const res = [2].reduce2(spy, 10); expect(spy).toHaveBeenCalledTimes(1); expect(spy.mock.calls[0]).toEqual([10, 2, 0, [2]]); expect(res).toBe(12); This verifies checklist items about single-element behaviour (3.5).
| it(`should invoke callback function ` | ||
| + `with 'arr' argument === this `, () => { | ||
| const array1 = [1, 5, 4]; | ||
| const array2 = [5, 3, 2]; | ||
| const array3 = [2, 2.5]; | ||
| const array4 = [0, 30, 0]; | ||
|
|
||
| array1.reduce2(adding, 8); | ||
| array2.reduce2(subtracting, 20); | ||
| array3.reduce2(multiplication, 2); | ||
| array4.reduce2(average); | ||
|
|
||
| const listOfArguments = [ | ||
| adding.mock.calls, | ||
| subtracting.mock.calls, | ||
| multiplication.mock.calls, | ||
| average.mock.calls, | ||
| ]; | ||
|
|
||
| const expected = [ | ||
| [array1, array1, array1, array1], | ||
| [array2, array2, array2, array2], | ||
| [array3, array3, array3], | ||
| [array4, array4], | ||
| ]; | ||
|
|
||
| listOfArguments.forEach((argList, operationIndex) => { | ||
| argList.forEach((call, callIndex) => { | ||
| const currentVal = call[3]; | ||
|
|
||
| expect(currentVal) | ||
| .toBe(expected[operationIndex][callIndex]); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
The it block checking the arr argument (lines 213–247) ensures the fourth parameter equals the original array, but it doesn't verify the callback's this binding. Per the checklist, add a test asserting this inside a strict-mode callback is undefined (or otherwise that the callback is not called with the array bound as this). For example, const spy = jest.fn(function () { expect(this).toBeUndefined(); }); [1,2].reduce2(spy, 0);.
| describe('(Sparse array)', () => { | ||
| it('should skips non-existent indices ' | ||
| + 'e.g [5, , , 5, 4]', () => { | ||
| const arrays = [ | ||
| /* eslint-disable max-len */ | ||
| /* eslint-disable no-sparse-arrays, standard/array-bracket-even-spacing, max-len */ | ||
| [, , 2, 17], | ||
| [1, 1, , 4], | ||
| [4, 5, 3, 1, , , , ], | ||
| /* eslint-enable no-sparse-arrays, standard/array-bracket-even-spacing, max-len */ | ||
| /* eslint-enable max-len */ | ||
| ]; | ||
| const expected = [ | ||
| [3], | ||
| [1, 3], | ||
| [1, 2, 3], | ||
| ]; | ||
|
|
||
| arrays.forEach((array, arrayIndex) => { | ||
| array.reduce2(callbacks[arrayIndex]); | ||
|
|
||
| const listOfArguments = callbacks[arrayIndex].mock.calls; | ||
|
|
||
| for (let i = 0; i < listOfArguments.length; i++) { | ||
| const index = listOfArguments[i][2]; | ||
|
|
||
| expect(index) | ||
| .toBe(expected[arrayIndex][i]); | ||
| } | ||
| }); | ||
| } | ||
| ); |
There was a problem hiding this comment.
Sparse array tests (lines 447–478) verify indices passed to the callback for existing elements, which is good. However, you are missing mutation-during-iteration tests: add tests that (1) append elements during iteration and assert they are not processed (elements appended beyond the initial length should be skipped), (2) delete a future index before it is visited and assert it's not called, and (3) change a future index before it is visited and assert the modified value is observed. These scenarios ensure reduce handles per-visit property existence and agrees with native reduce.
| .toBe(expected[i]); | ||
| } | ||
| }); | ||
| }); |
There was a problem hiding this comment.
After the existing describe blocks (near the end of the file) add a new describe or it-block to compare Array.prototype.reduce and reduce2 on mutation scenarios. Because the implementation in src/reduce.js currently iterates via a keys snapshot it may not match native reduce for deletions and updates made during reduction; tests that compare results and call counts between native reduce and reduce2 will quickly reveal discrepancies (see implementation at src/reduce.js for context) .
mateacademy-ai-mentor
left a comment
There was a problem hiding this comment.
Review Summary
Educational approval: You’ve made solid progress and I’m approving this to help you continue learning, but there are a few important semantic fixes to address next.
What you did well:
- Early
typeof callback !== 'function'guard is correct and aligned with the spec (src/reduce.js lines 10–12; checklist 3.4). - The general control flow for handling initial vs. no-initial cases is on the right track.
Critical issues to fix next:
- src/reduce.js line 14 and elsewhere: Avoid snapshot copies like
const copyOfThis = { ...this }andObject.assign([], copyOfThis). These break native reduce semantics by hiding runtime mutations and passing a copy to the callback (checklist 3.1, 3.6, 3.9, 3.11). Useconst O = Object(this);thenconst len = O.length >>> 0;and perform visit-time checks withif (i in O); always passOas the 4th callback arg. - Lines 36–45: In the no-initial-value branch, scan for the first present index using
if (i in O)and, if none found, throw TypeError for the fully sparse case. Remove the unreachableelse if (i === len). - Lines 15–18 and 60–64: Remove the
isArrayLikebranching;Object(this)+ numeric length snapshot +inchecks correctly handles arrays and array-likes (checklist 3.7).
Minor test improvements:
- Update the test description to say the callback receives the original array, not a copy, and prefer
toBeovertoEqualwhen asserting the 4th argument identity.
You’re very close—cleaning up the copy-based approach and unifying around Object(this) with visit-time checks will bring your implementation in line with native semantics. Keep up the great work!
✨ Please rate this review in the Platform's notification center1. I will be grateful for your feedback! ✨
Footnotes
| if (typeof callback !== 'function') { | ||
| throw new TypeError('Callback is not a function'); | ||
| } |
There was a problem hiding this comment.
Line 10–12: Good — the early typeof callback !== 'function' guard is present and correctly throws a TypeError. This satisfies the requirement that a non-function callback causes a TypeError (checklist 3.4).
| throw new TypeError('Callback is not a function'); | ||
| } | ||
|
|
||
| const copyOfThis = { ...this }; |
There was a problem hiding this comment.
Line 14: Critical issue — const copyOfThis = { ...this }; creates a snapshot copy of the object's indexed properties. Native reduce semantics require checking property presence on the original object at visit time (so deletions or modifications done by the callback before an index is visited affect iteration). Creating a shallow copy here prevents observing runtime mutations and will cause tests that mutate/delete elements during reduction to fail. Replace this with const O = Object(this); and iterate O directly using in checks at visit time. See tests that modify/delete/append array elements during callback execution for expected native-like behaviour.
| const isArrayLike = this instanceof Object | ||
| && this.hasOwnProperty('length') | ||
| && typeof this.length === 'number' | ||
| && !Array.isArray(this); |
There was a problem hiding this comment.
Lines 15–18: The isArrayLike detection is fragile and unnecessary. Using Object(this) and const len = O.length >>> 0; plus visit-time in checks will correctly handle arrays and array-like objects. Removing the isArrayLike branching will simplify logic and better match required semantics (checklist 3.7).
| Object.defineProperty(copyOfThis, 'length', { | ||
| value: this.length, | ||
| enumerable: false, | ||
| }); | ||
|
|
||
| const len = copyOfThis.length >>> 0; |
There was a problem hiding this comment.
Lines 20–25: You define length on the copied object and then use const len = copyOfThis.length >>> 0;. The numeric length snapshot must come from the original object (O.length >>> 0). Snapshot only the numeric length; do not pre-copy the indexed elements. This preserves the required snapshot-of-length but visit-time property checks for element presence.
| if (len < 1 && !hasStartValue) { | ||
| // eslint-disable-next-line max-len | ||
| throw new TypeError(`Array is empty and doesn't have 'startValue' argument`); | ||
| } |
There was a problem hiding this comment.
Lines 28–31: The check if (len < 1 && !hasStartValue) throw TypeError is correct in principle, but ensure len was derived from Object(this).length >>> 0 (not from a copied object). Tests rely on this behaviour when an empty array has no initial value.
| if (!(i in copyOfThis)) { | ||
| continue; | ||
| } | ||
|
|
||
| prev = callback(prev, copyOfThis[i], i, copyOfThis); |
There was a problem hiding this comment.
Lines 60–64: The array-like branch similarly checks i in copyOfThis and calls callback(..., copyOfThis). Again, pass the original object O as the 4th argument and perform presence checks on O at visit time. Unify handling instead of having separate branches — Object(this) + in checks covers both arrays and array-likes.
| const { reduce } = require('./reduce'); | ||
|
|
||
| describe('reduce', () => { | ||
| describe('reduce function', () => { |
There was a problem hiding this comment.
Good: the test-suite structure (describe/it blocks) is clear and the initial setup attaches the custom implementation to Array.prototype.reduce2 in beforeAll so tests run against the provided implementation. This matches the project pattern used elsewhere and keeps tests isolated.
| }); | ||
| }); | ||
|
|
||
| it(`should invoke callback function ` |
There was a problem hiding this comment.
Minor wording issue: the test title says "with 'arr' argument = copy of this", but the test actually asserts the callback receives the original array. For clarity change the description to something like: "with 'arr' argument = the original array (this)" so the intent matches the assertion that follows.
| argList.forEach((call, callIndex) => { | ||
| const currentVal = call[3]; | ||
|
|
||
| expect(currentVal) |
There was a problem hiding this comment.
Assertion precision: here you assert the callback's 4th argument equals the original array using toEqual. The checklist requires verifying the original array reference is passed; prefer toBe (reference equality) over toEqual (deep equality) to make the intent strict and explicit. Example: expect(currentVal).toBe(expected[operationIndex][callIndex]);
| expect(indexValue) | ||
| .toBe(expected[operationIndex][callIndex][2]); | ||
|
|
||
| expect(callbackArray) |
There was a problem hiding this comment.
Same recommendation as above (in the native-vs-custom comparison): you use toEqual to compare callbackArray to the expected callback argument captured from native reduce. Use toBe if you want to assert the same object reference is being passed through the callback in your custom implementation. If the expected array was captured (from native reduce) as the same object reference, toBe will be stricter and clearer about intent.
No description provided.