Skip to content

Add mock() and spyOn() test APIs#210

Merged
frostney merged 9 commits into
mainfrom
t3code/mock-and-spyon
Apr 8, 2026
Merged

Add mock() and spyOn() test APIs#210
frostney merged 9 commits into
mainfrom
t3code/mock-and-spyon

Conversation

@frostney
Copy link
Copy Markdown
Owner

@frostney frostney commented Apr 8, 2026

Summary

  • Adds standalone mock() and spyOn() test globals with call tracking, configurable implementations, return-value helpers, and restore/reset behavior.
  • Adds matcher support for mock call and return assertions such as toHaveBeenCalled, toHaveBeenCalledWith, and toHaveReturnedWith.
  • Updates docs and test guidance to describe the new testing APIs and their expected behavior.
  • Removes the old RegExp Unicode test file and updates the built-ins docs to reflect the current u-flag status.

Fixes #119

Testing

  • The change includes extensive Pascal unit coverage in units/Goccia.Builtins.TestAssertions.Test.pas for mock behavior, spies, and matcher failures.
  • JavaScript test coverage is documented in docs/testing.md under tests/language/testing/.

- add mock function implementation and matchers
- document standalone `mock()` and `spyOn()` globals
- expand Pascal coverage for call tracking and restoration
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds full mock/spy support: new TGocciaMockFunctionValue with call/result/instance tracking and control APIs, built-in mock()/spyOn() constructors, mock-aware expectation matchers, Pascal unit tests, constants for mock metadata, and documentation updates describing mock APIs and matcher behavior.

Changes

Cohort / File(s) Summary
Documentation
AGENTS.md, docs/testing.md
Documented Mock functions API and globals: mock() / spyOn(), .mock metadata fields (calls, results, instances, contexts, lastCall), control methods (mockImplementation*, mockReturnValue*, mockClear, mockReset, mockRestore, mockName, getMockName), mock matchers (including .not), deep-equality semantics, FIFO one-shot priority, and Vitest-divergence notes.
Mock Value Implementation
units/Goccia.Values.MockFunction.pas
New TGocciaMockFunctionValue class implementing call/context/result recording, one-shot queue vs permanent implementation, default return handling, throw-result semantics, lazy .mock accessor, native mock-control methods, spy creation/restore behavior, GC marking, and cleanup.
Expectation Matchers & Builtins
units/Goccia.Builtins.TestAssertions.pas
Extended TGocciaExpectationValue with mock-aware matchers (toHaveBeenCalled*, toHaveBeenCalledWith, toHaveBeenLast/NthCalledWith, toHaveReturned*, etc.) and added builtins MockFunction/SpyOn on TGocciaTestAssertions; updated uses and public declarations.
Unit Tests
units/Goccia.Builtins.TestAssertions.Test.pas
Added TTestMockAndSpyAPIs test suite exercising mock creation, implementations/once chaining and priority, return-value precedence, mockClear vs mockReset, mockRestore, mockName/getMockName, spy pass-through/override/restore, and matcher assertions (including negation and deep equality).
Constants
units/Goccia.Constants.PropertyNames.pas
Added property-name constants for mock metadata: PROP_TYPE, PROP_CALLS, PROP_RESULTS, PROP_INSTANCES, PROP_LAST_CALL.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Test Code
    participant MockFn as TGocciaMockFunctionValue
    participant Tracker as Call/Result Tracker
    participant Matcher as TGocciaExpectationValue

    Test->>MockFn: create via mock()/spyOn()
    activate MockFn
    MockFn->>MockFn: init calls/results/instances/queues
    deactivate MockFn

    Test->>MockFn: configure (mockImplementation / mockReturnValue / once)
    activate MockFn
    MockFn->>MockFn: enqueue or set behavior (one-shot FIFO or permanent)
    deactivate MockFn

    Test->>MockFn: invoke mock(args)
    activate MockFn
    MockFn->>Tracker: record args and this -> append to calls/instances
    MockFn->>MockFn: select behavior (implOnce → returnOnce → impl → default → undefined)
    MockFn->>Tracker: append result (return or throw) to results
    MockFn-->>Test: return or rethrow
    deactivate MockFn

    Test->>Matcher: expect(mockFn).toHaveBeenCalledWith(args)
    activate Matcher
    Matcher->>MockFn: verify mock type, read .mock.calls/.results
    Matcher->>Matcher: deep-equal compare expected vs recorded call/result
    Matcher-->>Test: pass or fail assertion
    deactivate Matcher
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main changes: adding mock() and spyOn() test APIs.
Description check ✅ Passed The description covers the main objectives, implementation details, and testing approach. All required sections are present with adequate completion.
Linked Issues check ✅ Passed The PR implements all coding requirements from issue #119: mock creation (mock(), spyOn()), mock configuration (mockReturnValue*, mockImplementation*, mockReset, mockRestore), and all matchers (toHaveBeenCalled*, toHaveReturned*).
Out of Scope Changes check ✅ Passed All changes are within scope for issue #119. Documentation updates, new test suite, mock/spy value type implementation, and matcher extensions are directly aligned with PR objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

Suite Timing

Suite Metric Interpreted Bytecode
Tests Total 3834 3834
Tests Passed 3793 ✅ 3834 ✅
Tests Skipped 41 0
Tests Test Duration 255.1ms 262.6ms
Tests Lex 69.9ms 48.2ms
Tests Parse 91.3ms 97.3ms
Tests Compile 59.9ms
Tests Execute 268.3ms 291.2ms
Tests Engine Total 429.4ms 496.7ms
Benchmarks Total 274 274
Benchmarks Duration 7.51min 6.37min

Measured on ubuntu-latest x64.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 8, 2026

Benchmark Results

274 benchmarks

Interpreted: 🟢 12 improved · 🔴 150 regressed · 112 unchanged · avg -1.7%
Bytecode: 🟢 23 improved · 🔴 177 regressed · 74 unchanged · avg -4.9%

arraybuffer.js — Interp: 🔴 12, 2 unch. · avg -1.7% · Bytecode: 🟢 1, 🔴 13 · avg -10.0%
Benchmark Interpreted Δ Bytecode Δ
create ArrayBuffer(0) 466,042 ops/sec [465,099..469,104] → 455,431 ops/sec [451,477..459,081] 🔴 -2.3% 568,011 ops/sec [559,248..568,626] → 489,791 ops/sec [485,632..492,779] 🔴 -13.8%
create ArrayBuffer(64) 440,832 ops/sec [426,474..445,921] → 436,339 ops/sec [432,947..436,736] ~ overlap (-1.0%) 528,496 ops/sec [527,245..532,546] → 460,931 ops/sec [453,204..465,297] 🔴 -12.8%
create ArrayBuffer(1024) 350,553 ops/sec [349,251..353,871] → 343,692 ops/sec [339,438..343,994] 🔴 -2.0% 375,132 ops/sec [373,722..375,886] → 361,252 ops/sec [356,275..363,003] 🔴 -3.7%
create ArrayBuffer(8192) 154,890 ops/sec [154,430..155,636] → 152,904 ops/sec [151,623..153,860] 🔴 -1.3% 143,102 ops/sec [142,052..145,433] → 157,873 ops/sec [155,606..158,705] 🟢 +10.3%
slice full buffer (64 bytes) 521,617 ops/sec [517,577..523,424] → 509,340 ops/sec [506,761..510,404] 🔴 -2.4% 673,774 ops/sec [667,794..674,753] → 586,184 ops/sec [576,703..589,728] 🔴 -13.0%
slice half buffer (512 of 1024 bytes) 444,448 ops/sec [444,043..446,833] → 440,314 ops/sec [436,866..443,443] 🔴 -0.9% 569,719 ops/sec [565,457..572,364] → 507,782 ops/sec [506,855..509,104] 🔴 -10.9%
slice with negative indices 434,963 ops/sec [432,507..436,339] → 429,179 ops/sec [424,453..430,165] 🔴 -1.3% 617,355 ops/sec [612,962..618,416] → 547,618 ops/sec [543,181..549,970] 🔴 -11.3%
slice empty range 494,684 ops/sec [491,468..496,647] → 487,536 ops/sec [428,115..491,298] 🔴 -1.4% 660,672 ops/sec [657,613..664,603] → 580,629 ops/sec [576,087..585,803] 🔴 -12.1%
byteLength access 1,315,473 ops/sec [1,310,655..1,317,972] → 1,282,799 ops/sec [1,259,131..1,288,080] 🔴 -2.5% 1,734,640 ops/sec [1,728,197..1,739,992] → 1,483,692 ops/sec [1,476,129..1,491,682] 🔴 -14.5%
Symbol.toStringTag access 1,002,745 ops/sec [999,650..1,016,566] → 983,703 ops/sec [974,894..988,188] 🔴 -1.9% 1,207,225 ops/sec [1,206,452..1,208,412] → 955,051 ops/sec [951,090..960,696] 🔴 -20.9%
ArrayBuffer.isView 733,830 ops/sec [732,672..736,575] → 724,756 ops/sec [720,663..729,738] 🔴 -1.2% 955,968 ops/sec [954,438..958,422] → 790,743 ops/sec [790,007..792,889] 🔴 -17.3%
clone ArrayBuffer(64) 419,611 ops/sec [415,247..420,731] → 407,366 ops/sec [402,397..409,079] 🔴 -2.9% 506,522 ops/sec [504,911..508,654] → 458,465 ops/sec [457,117..460,077] 🔴 -9.5%
clone ArrayBuffer(1024) 325,932 ops/sec [324,305..329,133] → 318,872 ops/sec [318,041..322,705] 🔴 -2.2% 361,532 ops/sec [359,538..365,347] → 353,573 ops/sec [352,240..356,823] 🔴 -2.2%
clone ArrayBuffer inside object 267,935 ops/sec [266,576..269,080] → 266,277 ops/sec [263,505..267,337] ~ overlap (-0.6%) 314,026 ops/sec [308,198..314,577] → 285,900 ops/sec [284,323..286,564] 🔴 -9.0%
arrays.js — Interp: 🟢 1, 🔴 14, 4 unch. · avg -1.5% · Bytecode: 🔴 17, 2 unch. · avg -8.8%
Benchmark Interpreted Δ Bytecode Δ
Array.from length 100 13,062 ops/sec [12,273..13,113] → 12,842 ops/sec [12,814..12,900] ~ overlap (-1.7%) 15,740 ops/sec [15,616..15,895] → 14,542 ops/sec [14,526..14,605] 🔴 -7.6%
Array.from 10 elements 225,644 ops/sec [224,572..226,105] → 220,744 ops/sec [219,324..220,826] 🔴 -2.2% 258,028 ops/sec [257,958..258,792] → 238,105 ops/sec [236,696..238,175] 🔴 -7.7%
Array.of 10 elements 275,771 ops/sec [274,650..276,657] → 274,904 ops/sec [272,077..276,989] ~ overlap (-0.3%) 326,107 ops/sec [325,760..326,816] → 301,303 ops/sec [293,245..301,659] 🔴 -7.6%
spread into new array 327,018 ops/sec [323,636..328,089] → 327,017 ops/sec [325,665..328,735] ~ overlap (-0.0%) 204,429 ops/sec [202,684..206,148] → 195,887 ops/sec [194,570..197,061] 🔴 -4.2%
map over 50 elements 25,049 ops/sec [24,872..25,177] → 24,077 ops/sec [23,904..24,139] 🔴 -3.9% 28,474 ops/sec [28,213..28,628] → 26,638 ops/sec [26,548..26,719] 🔴 -6.4%
filter over 50 elements 22,662 ops/sec [22,558..22,740] → 21,972 ops/sec [21,790..22,011] 🔴 -3.0% 25,348 ops/sec [25,279..25,610] → 22,926 ops/sec [22,858..23,033] 🔴 -9.6%
reduce sum 50 elements 25,989 ops/sec [25,939..26,046] → 25,693 ops/sec [25,563..25,894] 🔴 -1.1% 21,025 ops/sec [20,986..21,061] → 19,733 ops/sec [19,606..19,777] 🔴 -6.1%
forEach over 50 elements 22,531 ops/sec [22,424..22,624] → 22,132 ops/sec [22,038..22,232] 🔴 -1.8% 29,749 ops/sec [29,618..29,841] → 27,325 ops/sec [27,291..27,545] 🔴 -8.1%
find in 50 elements 32,237 ops/sec [32,127..32,396] → 32,296 ops/sec [32,160..32,383] ~ overlap (+0.2%) 33,868 ops/sec [33,673..33,929] → 29,888 ops/sec [29,408..30,050] 🔴 -11.8%
sort 20 elements 12,265 ops/sec [12,128..12,292] → 12,052 ops/sec [11,955..12,078] 🔴 -1.7% 14,017 ops/sec [13,968..14,079] → 13,529 ops/sec [13,509..13,549] 🔴 -3.5%
flat nested array 113,767 ops/sec [113,665..114,269] → 112,486 ops/sec [112,139..113,553] 🔴 -1.1% 138,097 ops/sec [137,722..139,202] → 127,461 ops/sec [126,492..128,043] 🔴 -7.7%
flatMap 73,214 ops/sec [72,955..73,360] → 71,908 ops/sec [71,667..72,099] 🔴 -1.8% 88,784 ops/sec [87,344..89,478] → 80,155 ops/sec [78,385..80,712] 🔴 -9.7%
map inside map (5x5) 20,527 ops/sec [20,357..20,562] → 19,858 ops/sec [19,478..19,958] 🔴 -3.3% 23,909 ops/sec [21,095..23,980] → 21,939 ops/sec [21,865..22,125] ~ overlap (-8.2%)
filter inside map (5x10) 15,596 ops/sec [15,535..15,667] → 15,149 ops/sec [15,085..15,167] 🔴 -2.9% 17,760 ops/sec [17,672..17,827] → 15,960 ops/sec [15,844..16,102] 🔴 -10.1%
reduce inside map (5x10) 19,120 ops/sec [19,056..19,143] → 18,829 ops/sec [18,684..18,916] 🔴 -1.5% 17,210 ops/sec [17,117..17,255] → 12,352 ops/sec [11,625..12,745] 🔴 -28.2%
forEach inside forEach (5x10) 16,516 ops/sec [16,457..16,576] → 16,204 ops/sec [16,198..16,208] 🔴 -1.9% 18,471 ops/sec [18,317..18,596] → 16,714 ops/sec [16,594..16,835] 🔴 -9.5%
find inside some (10x10) 13,400 ops/sec [13,299..13,421] → 13,249 ops/sec [13,233..13,289] 🔴 -1.1% 14,168 ops/sec [14,102..14,283] → 12,558 ops/sec [12,530..12,606] 🔴 -11.4%
map+filter chain nested (5x20) 5,153 ops/sec [5,126..5,185] → 5,009 ops/sec [4,995..5,032] 🔴 -2.8% 5,833 ops/sec [5,777..5,881] → 5,287 ops/sec [5,278..5,312] 🔴 -9.4%
reduce flatten (10x5) 37,535 ops/sec [37,261..37,807] → 38,889 ops/sec [38,780..38,994] 🟢 +3.6% 17,626 ops/sec [17,453..17,657] → 17,499 ops/sec [17,465..17,561] ~ overlap (-0.7%)
async-await.js — Interp: 🔴 4, 2 unch. · avg -1.7% · Bytecode: 🔴 6 · avg -9.5%
Benchmark Interpreted Δ Bytecode Δ
single await 377,213 ops/sec [375,849..379,311] → 369,693 ops/sec [367,412..369,883] 🔴 -2.0% 434,420 ops/sec [431,018..437,495] → 386,875 ops/sec [384,279..388,593] 🔴 -10.9%
multiple awaits 169,445 ops/sec [168,754..170,012] → 167,569 ops/sec [166,985..168,375] 🔴 -1.1% 196,195 ops/sec [194,550..198,243] → 171,233 ops/sec [170,885..172,333] 🔴 -12.7%
await non-Promise value 822,951 ops/sec [820,852..825,338] → 818,813 ops/sec [813,219..821,699] ~ overlap (-0.5%) 1,176,154 ops/sec [1,169,331..1,193,803] → 1,106,380 ops/sec [1,099,922..1,111,495] 🔴 -5.9%
await with try/catch 367,982 ops/sec [366,153..368,525] → 358,879 ops/sec [356,397..361,639] 🔴 -2.5% 432,670 ops/sec [426,914..444,295] → 388,981 ops/sec [386,739..390,431] 🔴 -10.1%
await Promise.all 51,385 ops/sec [50,871..51,574] → 50,610 ops/sec [50,373..50,994] ~ overlap (-1.5%) 54,633 ops/sec [54,477..54,967] → 50,198 ops/sec [49,878..50,437] 🔴 -8.1%
nested async function call 193,867 ops/sec [193,077..195,408] → 189,105 ops/sec [188,698..189,751] 🔴 -2.5% 245,931 ops/sec [243,461..248,760] → 223,655 ops/sec [221,742..224,396] 🔴 -9.1%
classes.js — Interp: 🔴 28, 3 unch. · avg -3.0% · Bytecode: 🟢 4, 🔴 12, 15 unch. · avg -3.0%
Benchmark Interpreted Δ Bytecode Δ
simple class new 143,016 ops/sec [142,090..144,683] → 136,960 ops/sec [136,219..137,755] 🔴 -4.2% 215,225 ops/sec [213,375..216,616] → 200,820 ops/sec [198,293..202,942] 🔴 -6.7%
class with defaults 110,528 ops/sec [110,099..111,231] → 109,037 ops/sec [108,160..109,078] 🔴 -1.3% 152,071 ops/sec [148,257..153,756] → 139,749 ops/sec [139,331..141,170] 🔴 -8.1%
50 instances via Array.from 5,819 ops/sec [5,795..5,842] → 5,596 ops/sec [5,550..5,605] 🔴 -3.8% 8,651 ops/sec [8,545..8,719] → 7,661 ops/sec [7,597..7,814] 🔴 -11.5%
instance method call 72,778 ops/sec [71,956..73,406] → 69,419 ops/sec [68,854..69,586] 🔴 -4.6% 100,586 ops/sec [99,975..100,822] → 89,360 ops/sec [87,821..90,436] 🔴 -11.2%
static method call 116,965 ops/sec [116,383..117,113] → 112,314 ops/sec [111,967..112,485] 🔴 -4.0% 190,752 ops/sec [189,233..196,519] → 179,725 ops/sec [176,750..180,401] 🔴 -5.8%
single-level inheritance 57,339 ops/sec [57,194..57,642] → 55,077 ops/sec [54,853..55,178] 🔴 -3.9% 77,605 ops/sec [76,612..78,467] → 73,939 ops/sec [72,175..75,069] 🔴 -4.7%
two-level inheritance 48,945 ops/sec [48,865..49,173] → 47,407 ops/sec [46,627..47,889] 🔴 -3.1% 62,851 ops/sec [61,539..65,388] → 58,665 ops/sec [56,881..59,858] 🔴 -6.7%
private field access 72,524 ops/sec [72,190..73,028] → 71,217 ops/sec [71,069..71,591] 🔴 -1.8% 97,892 ops/sec [97,269..97,989] → 87,475 ops/sec [85,938..88,105] 🔴 -10.6%
private methods 79,946 ops/sec [79,709..80,210] → 77,315 ops/sec [76,731..77,559] 🔴 -3.3% 99,789 ops/sec [98,335..100,606] → 96,432 ops/sec [95,368..96,889] 🔴 -3.4%
getter/setter access 76,961 ops/sec [76,639..77,213] → 74,331 ops/sec [74,214..74,395] 🔴 -3.4% 108,400 ops/sec [107,567..108,645] → 99,912 ops/sec [98,604..100,407] 🔴 -7.8%
class decorator (identity) 94,459 ops/sec [93,561..95,055] → 92,222 ops/sec [91,610..93,055] 🔴 -2.4% 101,895 ops/sec [101,443..102,659] → 100,284 ops/sec [96,238..102,939] ~ overlap (-1.6%)
class decorator (wrapping) 56,123 ops/sec [55,656..56,373] → 54,267 ops/sec [53,867..54,758] 🔴 -3.3% 59,492 ops/sec [56,905..63,269] → 56,178 ops/sec [55,066..59,588] ~ overlap (-5.6%)
identity method decorator 66,825 ops/sec [66,204..67,549] → 65,067 ops/sec [64,820..65,226] 🔴 -2.6% 89,779 ops/sec [80,972..97,441] → 86,104 ops/sec [78,712..92,096] ~ overlap (-4.1%)
wrapping method decorator 54,205 ops/sec [53,859..54,661] → 52,301 ops/sec [52,166..52,464] 🔴 -3.5% 60,957 ops/sec [56,189..67,443] → 58,159 ops/sec [55,146..62,957] ~ overlap (-4.6%)
stacked method decorators (x3) 36,185 ops/sec [36,091..36,219] → 35,233 ops/sec [35,007..35,645] 🔴 -2.6% 40,657 ops/sec [38,602..43,800] → 42,758 ops/sec [40,016..45,304] ~ overlap (+5.2%)
identity field decorator 73,437 ops/sec [72,649..73,968] → 71,469 ops/sec [70,435..71,975] 🔴 -2.7% 70,912 ops/sec [67,919..76,669] → 69,660 ops/sec [67,619..70,930] ~ overlap (-1.8%)
field initializer decorator 61,108 ops/sec [60,469..61,333] → 59,069 ops/sec [58,881..59,214] 🔴 -3.3% 74,990 ops/sec [66,192..81,467] → 63,778 ops/sec [56,502..64,425] 🔴 -15.0%
getter decorator (identity) 66,925 ops/sec [66,781..67,006] → 64,080 ops/sec [63,335..64,368] 🔴 -4.3% 67,280 ops/sec [66,919..68,846] → 69,408 ops/sec [66,528..72,515] ~ overlap (+3.2%)
setter decorator (identity) 55,955 ops/sec [55,748..56,200] → 54,112 ops/sec [53,822..54,505] 🔴 -3.3% 63,155 ops/sec [61,948..64,837] → 61,325 ops/sec [60,462..62,739] ~ overlap (-2.9%)
static method decorator 72,266 ops/sec [71,458..72,446] → 70,337 ops/sec [70,168..70,669] 🔴 -2.7% 72,184 ops/sec [71,593..72,454] → 77,072 ops/sec [74,620..78,780] 🟢 +6.8%
static field decorator 84,592 ops/sec [84,348..84,694] → 82,371 ops/sec [81,737..82,973] 🔴 -2.6% 82,033 ops/sec [76,767..89,297] → 77,633 ops/sec [74,550..82,925] ~ overlap (-5.4%)
private method decorator 54,683 ops/sec [54,428..54,919] → 52,908 ops/sec [52,591..53,027] 🔴 -3.2% 62,489 ops/sec [61,245..67,267] → 76,323 ops/sec [71,647..78,519] 🟢 +22.1%
private field decorator 60,989 ops/sec [60,757..61,252] → 58,687 ops/sec [58,134..58,858] 🔴 -3.8% 56,653 ops/sec [56,232..59,276] → 56,468 ops/sec [55,296..58,503] ~ overlap (-0.3%)
plain auto-accessor (no decorator) 106,070 ops/sec [103,907..108,585] → 103,144 ops/sec [101,745..105,937] ~ overlap (-2.8%) 106,200 ops/sec [94,127..120,097] → 89,682 ops/sec [89,152..92,502] 🔴 -15.6%
auto-accessor with decorator 57,031 ops/sec [55,790..58,318] → 55,140 ops/sec [54,288..56,146] ~ overlap (-3.3%) 54,264 ops/sec [50,762..57,457] → 50,099 ops/sec [46,822..54,326] ~ overlap (-7.7%)
decorator writing metadata 45,433 ops/sec [45,210..45,862] → 44,689 ops/sec [44,552..45,115] 🔴 -1.6% 45,975 ops/sec [43,740..48,376] → 45,395 ops/sec [42,326..53,625] ~ overlap (-1.3%)
static getter read 129,746 ops/sec [129,365..130,382] → 125,628 ops/sec [124,830..126,008] 🔴 -3.2% 155,811 ops/sec [155,239..159,423] → 162,235 ops/sec [160,860..163,348] 🟢 +4.1%
static getter/setter pair 100,077 ops/sec [99,354..100,814] → 97,028 ops/sec [96,366..98,069] 🔴 -3.0% 122,647 ops/sec [121,888..131,674] → 118,383 ops/sec [117,345..123,108] ~ overlap (-3.5%)
inherited static getter 76,972 ops/sec [75,701..78,044] → 76,119 ops/sec [75,681..76,858] ~ overlap (-1.1%) 87,461 ops/sec [82,813..88,779] → 90,236 ops/sec [85,243..93,794] ~ overlap (+3.2%)
inherited static setter 83,924 ops/sec [83,561..84,689] → 82,085 ops/sec [81,724..82,537] 🔴 -2.2% 96,556 ops/sec [95,311..98,471] → 99,458 ops/sec [94,278..106,310] ~ overlap (+3.0%)
inherited static getter with this binding 71,896 ops/sec [71,334..72,198] → 70,564 ops/sec [70,469..70,642] 🔴 -1.9% 82,694 ops/sec [81,997..83,401] → 86,302 ops/sec [85,063..86,857] 🟢 +4.4%
closures.js — Interp: 🔴 10, 1 unch. · avg -2.3% · Bytecode: 🔴 6, 5 unch. · avg -3.8%
Benchmark Interpreted Δ Bytecode Δ
closure over single variable 133,242 ops/sec [132,975..133,497] → 130,832 ops/sec [129,746..132,039] 🔴 -1.8% 276,267 ops/sec [268,456..278,719] → 268,194 ops/sec [263,625..272,773] ~ overlap (-2.9%)
closure over multiple variables 120,480 ops/sec [119,110..120,778] → 116,352 ops/sec [115,697..116,723] 🔴 -3.4% 196,428 ops/sec [193,705..201,966] → 191,082 ops/sec [189,048..197,890] ~ overlap (-2.7%)
nested closures 127,228 ops/sec [126,780..127,981] → 123,159 ops/sec [122,070..124,001] 🔴 -3.2% 222,467 ops/sec [216,140..239,658] → 223,173 ops/sec [211,662..238,313] ~ overlap (+0.3%)
function as argument 93,877 ops/sec [92,538..94,493] → 91,741 ops/sec [91,090..92,205] 🔴 -2.3% 249,204 ops/sec [247,545..249,859] → 239,836 ops/sec [237,557..243,096] 🔴 -3.8%
function returning function 118,168 ops/sec [116,359..119,097] → 115,413 ops/sec [114,558..116,276] 🔴 -2.3% 282,250 ops/sec [280,987..285,183] → 276,558 ops/sec [275,240..278,137] 🔴 -2.0%
compose two functions 71,717 ops/sec [71,368..71,830] → 69,923 ops/sec [69,103..70,435] 🔴 -2.5% 147,844 ops/sec [146,329..148,572] → 138,768 ops/sec [136,841..139,607] 🔴 -6.1%
fn.call 152,479 ops/sec [152,022..153,190] → 150,501 ops/sec [148,418..151,420] 🔴 -1.3% 233,715 ops/sec [232,076..235,671] → 211,721 ops/sec [210,514..212,344] 🔴 -9.4%
fn.apply 111,716 ops/sec [110,530..112,366] → 111,218 ops/sec [109,958..111,927] ~ overlap (-0.4%) 154,630 ops/sec [136,652..155,953] → 153,880 ops/sec [153,205..156,166] ~ overlap (-0.5%)
fn.bind 144,216 ops/sec [143,654..144,598] → 140,622 ops/sec [139,961..141,395] 🔴 -2.5% 282,276 ops/sec [264,643..284,497] → 274,077 ops/sec [273,209..275,630] ~ overlap (-2.9%)
recursive sum to 50 11,989 ops/sec [11,943..12,064] → 11,697 ops/sec [11,607..11,789] 🔴 -2.4% 24,603 ops/sec [24,522..24,659] → 23,792 ops/sec [23,725..23,837] 🔴 -3.3%
recursive tree traversal 20,699 ops/sec [20,528..20,838] → 20,067 ops/sec [19,930..20,342] 🔴 -3.1% 37,551 ops/sec [37,277..37,732] → 34,328 ops/sec [33,912..34,699] 🔴 -8.6%
collections.js — Interp: 🔴 11, 1 unch. · avg -7.3% · Bytecode: 🟢 1, 🔴 10, 1 unch. · avg -9.8%
Benchmark Interpreted Δ Bytecode Δ
add 50 elements 7,327 ops/sec [7,300..7,379] → 6,805 ops/sec [6,741..6,829] 🔴 -7.1% 7,818 ops/sec [7,664..7,900] → 6,925 ops/sec [6,862..6,958] 🔴 -11.4%
has lookup (50 elements) 95,858 ops/sec [94,861..96,395] → 87,505 ops/sec [87,359..87,716] 🔴 -8.7% 107,496 ops/sec [106,650..108,099] → 95,300 ops/sec [95,062..95,550] 🔴 -11.3%
delete elements 50,804 ops/sec [50,336..51,468] → 45,636 ops/sec [45,500..45,827] 🔴 -10.2% 54,210 ops/sec [54,168..54,404] → 47,033 ops/sec [46,943..47,289] 🔴 -13.2%
forEach iteration 15,643 ops/sec [15,523..15,796] → 15,213 ops/sec [15,057..15,262] 🔴 -2.8% 16,379 ops/sec [16,303..16,474] → 14,439 ops/sec [14,367..14,491] 🔴 -11.8%
spread to array 30,047 ops/sec [29,858..30,275] → 28,543 ops/sec [28,269..28,783] 🔴 -5.0% 207,357 ops/sec [201,052..215,434] → 208,022 ops/sec [204,824..214,511] ~ overlap (+0.3%)
deduplicate array 39,790 ops/sec [39,584..39,938] → 37,415 ops/sec [37,124..37,504] 🔴 -6.0% 74,743 ops/sec [74,417..75,589] → 66,208 ops/sec [65,834..66,521] 🔴 -11.4%
set 50 entries 5,503 ops/sec [5,475..5,564] → 4,985 ops/sec [4,955..5,054] 🔴 -9.4% 6,058 ops/sec [6,031..6,104] → 5,426 ops/sec [5,380..5,431] 🔴 -10.4%
get lookup (50 entries) 94,566 ops/sec [94,270..94,937] → 84,028 ops/sec [83,908..84,507] 🔴 -11.1% 104,952 ops/sec [104,474..105,109] → 88,195 ops/sec [88,084..88,222] 🔴 -16.0%
has check 139,275 ops/sec [138,879..139,675] → 123,745 ops/sec [123,610..123,876] 🔴 -11.2% 160,565 ops/sec [160,269..161,108] → 134,958 ops/sec [134,270..135,060] 🔴 -15.9%
delete entries 50,326 ops/sec [50,155..50,389] → 44,496 ops/sec [43,927..44,608] 🔴 -11.6% 52,716 ops/sec [52,569..52,910] → 45,368 ops/sec [45,177..45,463] 🔴 -13.9%
forEach iteration 15,593 ops/sec [15,478..15,723] → 15,391 ops/sec [15,347..15,497] ~ overlap (-1.3%) 16,941 ops/sec [16,847..17,014] → 15,168 ops/sec [15,124..15,218] 🔴 -10.5%
keys/values/entries 8,227 ops/sec [8,161..8,271] → 7,911 ops/sec [7,855..7,951] 🔴 -3.8% 24,158 ops/sec [23,835..24,846] → 25,975 ops/sec [25,665..26,439] 🟢 +7.5%
destructuring.js — Interp: 🟢 1, 🔴 10, 11 unch. · avg -1.3% · Bytecode: 🟢 2, 🔴 12, 8 unch. · avg -2.6%
Benchmark Interpreted Δ Bytecode Δ
simple array destructuring 379,614 ops/sec [378,839..379,875] → 380,724 ops/sec [378,125..383,615] ~ overlap (+0.3%) 255,234 ops/sec [250,597..257,017] → 260,645 ops/sec [257,591..263,344] 🟢 +2.1%
with rest element 242,824 ops/sec [240,140..244,020] → 244,799 ops/sec [244,246..246,232] 🟢 +0.8% 213,107 ops/sec [211,437..215,666] → 207,458 ops/sec [204,722..208,105] 🔴 -2.7%
with defaults 392,458 ops/sec [388,347..395,642] → 376,399 ops/sec [373,378..377,748] 🔴 -4.1% 293,392 ops/sec [290,151..295,516] → 294,365 ops/sec [290,498..299,268] ~ overlap (+0.3%)
skip elements 398,118 ops/sec [392,670..399,998] → 401,609 ops/sec [398,535..403,354] ~ overlap (+0.9%) 294,056 ops/sec [290,888..296,809] → 288,010 ops/sec [286,422..290,010] 🔴 -2.1%
nested array destructuring 166,619 ops/sec [165,829..168,710] → 166,385 ops/sec [164,315..166,780] ~ overlap (-0.1%) 83,586 ops/sec [83,244..83,967] → 87,414 ops/sec [86,386..89,010] 🟢 +4.6%
swap variables 499,551 ops/sec [497,100..502,558] → 502,356 ops/sec [500,632..503,803] ~ overlap (+0.6%) 332,911 ops/sec [331,209..338,191] → 334,098 ops/sec [329,392..339,307] ~ overlap (+0.4%)
simple object destructuring 304,173 ops/sec [301,523..306,926] → 308,067 ops/sec [306,736..309,056] ~ overlap (+1.3%) 428,894 ops/sec [411,750..432,913] → 409,886 ops/sec [408,460..411,569] 🔴 -4.4%
with defaults 346,628 ops/sec [345,602..350,620] → 339,673 ops/sec [335,807..342,145] 🔴 -2.0% 584,459 ops/sec [580,564..591,750] → 540,525 ops/sec [536,630..546,589] 🔴 -7.5%
with renaming 330,170 ops/sec [328,534..330,901] → 326,347 ops/sec [322,889..328,742] ~ overlap (-1.2%) 454,657 ops/sec [453,167..455,491] → 434,055 ops/sec [432,141..438,403] 🔴 -4.5%
nested object destructuring 149,892 ops/sec [148,001..150,621] → 148,218 ops/sec [146,433..149,520] ~ overlap (-1.1%) 203,386 ops/sec [201,115..205,106] → 194,050 ops/sec [192,465..194,616] 🔴 -4.6%
rest properties 186,920 ops/sec [183,377..187,518] → 184,675 ops/sec [182,721..186,107] ~ overlap (-1.2%) 188,710 ops/sec [188,289..191,862] → 183,647 ops/sec [182,433..186,294] 🔴 -2.7%
object parameter 94,500 ops/sec [93,793..94,850] → 93,528 ops/sec [92,961..93,611] 🔴 -1.0% 150,026 ops/sec [148,579..151,560] → 141,395 ops/sec [139,050..144,245] 🔴 -5.8%
array parameter 122,921 ops/sec [120,210..123,435] → 120,158 ops/sec [119,424..121,303] ~ overlap (-2.2%) 107,058 ops/sec [105,182..108,578] → 107,658 ops/sec [105,614..108,700] ~ overlap (+0.6%)
mixed destructuring in map 33,300 ops/sec [33,156..33,438] → 32,942 ops/sec [32,551..33,210] ~ overlap (-1.1%) 36,523 ops/sec [36,289..36,612] → 33,720 ops/sec [33,559..33,986] 🔴 -7.7%
forEach with array destructuring 63,736 ops/sec [63,306..64,040] → 61,929 ops/sec [61,828..62,423] 🔴 -2.8% 41,718 ops/sec [41,083..42,456] → 41,983 ops/sec [40,711..42,259] ~ overlap (+0.6%)
map with array destructuring 63,414 ops/sec [62,916..63,867] → 61,829 ops/sec [61,264..62,329] 🔴 -2.5% 41,857 ops/sec [41,567..41,950] → 42,308 ops/sec [41,710..42,644] ~ overlap (+1.1%)
filter with array destructuring 65,806 ops/sec [65,407..66,025] → 63,940 ops/sec [63,539..64,411] 🔴 -2.8% 46,582 ops/sec [45,994..47,097] → 46,473 ops/sec [46,210..46,782] ~ overlap (-0.2%)
reduce with array destructuring 72,047 ops/sec [71,396..72,621] → 70,517 ops/sec [70,112..71,173] 🔴 -2.1% 45,677 ops/sec [45,219..46,057] → 46,515 ops/sec [46,022..46,675] ~ overlap (+1.8%)
map with object destructuring 74,031 ops/sec [73,532..74,611] → 71,515 ops/sec [71,215..71,931] 🔴 -3.4% 93,463 ops/sec [93,032..93,942] → 84,701 ops/sec [79,771..85,504] 🔴 -9.4%
map with nested destructuring 61,777 ops/sec [61,366..62,211] → 60,452 ops/sec [59,785..60,578] 🔴 -2.1% 89,052 ops/sec [88,783..89,694] → 81,011 ops/sec [75,114..81,909] 🔴 -9.0%
map with rest in destructuring 42,050 ops/sec [41,821..42,582] → 41,938 ops/sec [41,583..42,009] ~ overlap (-0.3%) 23,155 ops/sec [23,082..23,268] → 23,547 ops/sec [23,225..23,753] ~ overlap (+1.7%)
map with defaults in destructuring 56,623 ops/sec [56,222..56,929] → 55,776 ops/sec [55,515..55,929] 🔴 -1.5% 69,163 ops/sec [67,826..69,475] → 62,563 ops/sec [62,165..62,907] 🔴 -9.5%
fibonacci.js — Interp: 🔴 6, 2 unch. · avg -1.8% · Bytecode: 🔴 8 · avg -5.0%
Benchmark Interpreted Δ Bytecode Δ
recursive fib(15) 311 ops/sec [310..313] → 311 ops/sec [311..315] ~ overlap (+0.1%) 872 ops/sec [869..873] → 812 ops/sec [811..813] 🔴 -6.9%
recursive fib(20) 29 ops/sec [29..29] → 28 ops/sec [28..28] 🔴 -1.8% 78 ops/sec [77..79] → 73 ops/sec [73..73] 🔴 -6.9%
recursive fib(15) typed 318 ops/sec [313..320] → 314 ops/sec [312..316] ~ overlap (-1.1%) 893 ops/sec [890..896] → 851 ops/sec [848..855] 🔴 -4.8%
recursive fib(20) typed 29 ops/sec [29..29] → 28 ops/sec [28..28] 🔴 -2.0% 78 ops/sec [78..79] → 77 ops/sec [77..77] 🔴 -1.5%
iterative fib(20) via reduce 12,043 ops/sec [11,991..12,072] → 11,751 ops/sec [11,641..11,890] 🔴 -2.4% 17,902 ops/sec [17,768..17,936] → 17,368 ops/sec [16,846..17,466] 🔴 -3.0%
iterator fib(20) 9,640 ops/sec [9,597..9,666] → 9,436 ops/sec [9,352..9,461] 🔴 -2.1% 18,731 ops/sec [18,490..18,854] → 17,925 ops/sec [17,726..18,087] 🔴 -4.3%
iterator fib(20) via Iterator.from + take 15,481 ops/sec [15,331..15,571] → 15,199 ops/sec [15,114..15,329] 🔴 -1.8% 21,664 ops/sec [21,300..21,759] → 20,153 ops/sec [19,908..20,330] 🔴 -7.0%
iterator fib(20) last value via reduce 11,448 ops/sec [11,382..11,462] → 11,109 ops/sec [10,953..11,338] 🔴 -3.0% 14,863 ops/sec [14,679..15,286] → 14,044 ops/sec [13,970..14,209] 🔴 -5.5%
for-of.js — Interp: 🔴 2, 5 unch. · avg -0.9% · Bytecode: 🔴 6, 1 unch. · avg -5.1%
Benchmark Interpreted Δ Bytecode Δ
for...of with 10-element array 45,485 ops/sec [45,212..45,724] → 45,200 ops/sec [44,943..45,579] ~ overlap (-0.6%) 177,593 ops/sec [177,137..178,124] → 165,416 ops/sec [164,803..165,602] 🔴 -6.9%
for...of with 100-element array 5,248 ops/sec [5,162..5,265] → 5,208 ops/sec [5,178..5,240] ~ overlap (-0.8%) 19,168 ops/sec [19,038..19,354] → 17,878 ops/sec [17,865..17,953] 🔴 -6.7%
for...of with string (10 chars) 33,984 ops/sec [33,775..34,460] → 33,508 ops/sec [33,038..33,736] 🔴 -1.4% 91,780 ops/sec [90,270..92,280] → 82,962 ops/sec [82,416..83,545] 🔴 -9.6%
for...of with Set (10 elements) 46,103 ops/sec [45,675..46,250] → 45,630 ops/sec [45,136..45,845] ~ overlap (-1.0%) 165,817 ops/sec [165,429..166,486] → 152,674 ops/sec [151,848..153,197] 🔴 -7.9%
for...of with Map entries (10 entries) 29,607 ops/sec [29,422..29,969] → 29,083 ops/sec [28,883..29,185] 🔴 -1.8% 26,752 ops/sec [26,158..27,044] → 27,349 ops/sec [26,918..27,920] ~ overlap (+2.2%)
for...of with destructuring 39,077 ops/sec [38,966..39,306] → 39,057 ops/sec [38,894..39,228] ~ overlap (-0.1%) 38,145 ops/sec [37,773..38,326] → 36,749 ops/sec [36,174..37,050] 🔴 -3.7%
for-await-of with sync array 44,145 ops/sec [43,529..44,279] → 43,808 ops/sec [43,486..43,940] ~ overlap (-0.8%) 128,172 ops/sec [126,908..128,909] → 123,747 ops/sec [122,299..124,226] 🔴 -3.5%
helpers/bench-module.js — Interp: 0 · Bytecode: 0
Benchmark Interpreted Δ Bytecode Δ
iterators.js — Interp: 🔴 9, 11 unch. · avg -1.6% · Bytecode: 🔴 18, 2 unch. · avg -5.9%
Benchmark Interpreted Δ Bytecode Δ
Iterator.from({next}).toArray() — 20 elements 15,573 ops/sec [15,565..15,744] → 15,262 ops/sec [15,219..15,393] 🔴 -2.0% 20,515 ops/sec [20,285..20,723] → 19,636 ops/sec [19,488..20,092] 🔴 -4.3%
Iterator.from({next}).toArray() — 50 elements 6,747 ops/sec [6,702..6,866] → 6,535 ops/sec [6,342..6,614] 🔴 -3.1% 9,706 ops/sec [9,629..9,717] → 8,704 ops/sec [8,604..8,748] 🔴 -10.3%
spread pre-wrapped iterator — 20 elements 11,595 ops/sec [11,451..11,806] → 11,304 ops/sec [11,184..11,508] ~ overlap (-2.5%) 20,295 ops/sec [20,137..20,522] → 18,920 ops/sec [18,659..19,204] 🔴 -6.8%
Iterator.from({next}).forEach — 50 elements 4,649 ops/sec [4,633..4,663] → 4,578 ops/sec [4,509..4,603] 🔴 -1.5% 5,935 ops/sec [5,836..6,002] → 5,498 ops/sec [5,415..5,528] 🔴 -7.4%
Iterator.from({next}).reduce — 50 elements 4,680 ops/sec [4,614..4,698] → 4,576 ops/sec [4,512..4,596] 🔴 -2.2% 5,651 ops/sec [5,607..5,680] → 5,250 ops/sec [5,166..5,274] 🔴 -7.1%
wrap array iterator 161,300 ops/sec [160,675..161,503] → 159,909 ops/sec [157,824..160,939] ~ overlap (-0.9%) 177,310 ops/sec [176,563..178,069] → 172,104 ops/sec [165,765..174,732] 🔴 -2.9%
wrap plain {next()} object 10,848 ops/sec [10,602..10,945] → 10,454 ops/sec [10,228..10,514] 🔴 -3.6% 15,113 ops/sec [15,048..15,203] → 13,828 ops/sec [13,677..13,984] 🔴 -8.5%
map + toArray (50 elements) 4,605 ops/sec [4,592..4,639] → 4,610 ops/sec [4,554..4,645] ~ overlap (+0.1%) 6,364 ops/sec [6,334..6,407] → 5,866 ops/sec [5,837..5,922] 🔴 -7.8%
filter + toArray (50 elements) 4,504 ops/sec [4,458..4,592] → 4,469 ops/sec [4,448..4,477] ~ overlap (-0.8%) 5,910 ops/sec [5,851..5,953] → 5,604 ops/sec [5,580..5,645] 🔴 -5.2%
take(10) + toArray (50 element source) 28,101 ops/sec [27,761..28,337] → 27,786 ops/sec [27,663..28,116] ~ overlap (-1.1%) 38,067 ops/sec [37,731..38,249] → 35,043 ops/sec [34,610..35,434] 🔴 -7.9%
drop(40) + toArray (50 element source) 6,776 ops/sec [6,719..6,847] → 6,621 ops/sec [6,548..6,675] 🔴 -2.3% 9,157 ops/sec [9,072..9,231] → 8,649 ops/sec [8,540..8,694] 🔴 -5.5%
chained map + filter + take (100 element source) 8,251 ops/sec [8,201..8,370] → 8,201 ops/sec [8,052..8,236] ~ overlap (-0.6%) 10,608 ops/sec [10,547..10,673] → 9,765 ops/sec [9,710..9,973] 🔴 -8.0%
some + every (50 elements) 2,652 ops/sec [2,612..2,675] → 2,622 ops/sec [2,598..2,636] ~ overlap (-1.1%) 3,598 ops/sec [3,567..3,659] → 3,313 ops/sec [3,274..3,372] 🔴 -7.9%
find (50 elements) 5,778 ops/sec [5,735..5,812] → 5,717 ops/sec [5,684..5,769] ~ overlap (-1.1%) 7,403 ops/sec [7,345..7,413] → 6,941 ops/sec [6,879..6,974] 🔴 -6.2%
array.values().map().filter().toArray() 7,940 ops/sec [7,926..8,040] → 7,799 ops/sec [7,757..7,904] 🔴 -1.8% 9,972 ops/sec [9,872..10,053] → 9,132 ops/sec [9,043..9,156] 🔴 -8.4%
array.values().take(5).toArray() 211,820 ops/sec [206,640..214,849] → 208,295 ops/sec [206,352..209,398] ~ overlap (-1.7%) 234,507 ops/sec [232,784..235,581] → 229,116 ops/sec [227,413..230,964] 🔴 -2.3%
array.values().drop(45).toArray() 196,206 ops/sec [193,660..197,331] → 195,176 ops/sec [192,884..197,041] ~ overlap (-0.5%) 217,836 ops/sec [216,291..218,607] → 212,624 ops/sec [211,604..214,166] 🔴 -2.4%
map.entries() chained helpers 10,012 ops/sec [9,903..10,125] → 9,844 ops/sec [9,770..9,896] 🔴 -1.7% 5,902 ops/sec [5,890..5,990] → 5,974 ops/sec [5,916..6,054] ~ overlap (+1.2%)
set.values() chained helpers 16,815 ops/sec [16,487..16,977] → 16,655 ops/sec [16,393..16,785] ~ overlap (-1.0%) 20,125 ops/sec [19,881..20,245] → 18,495 ops/sec [18,462..18,611] 🔴 -8.1%
string iterator map + toArray 13,696 ops/sec [13,580..13,879] → 13,402 ops/sec [13,239..13,562] 🔴 -2.1% 14,345 ops/sec [14,089..14,494] → 14,127 ops/sec [13,840..14,382] ~ overlap (-1.5%)
json.js — Interp: 🔴 13, 7 unch. · avg -1.8% · Bytecode: 🔴 18, 2 unch. · avg -6.7%
Benchmark Interpreted Δ Bytecode Δ
parse simple object 149,803 ops/sec [148,321..151,491] → 146,436 ops/sec [144,604..147,733] 🔴 -2.2% 160,428 ops/sec [158,861..161,395] → 148,411 ops/sec [147,490..149,557] 🔴 -7.5%
parse nested object 95,316 ops/sec [94,375..96,017] → 94,975 ops/sec [94,211..95,385] ~ overlap (-0.4%) 96,284 ops/sec [95,221..96,691] → 96,301 ops/sec [95,538..97,089] ~ overlap (+0.0%)
parse array of objects 55,913 ops/sec [55,185..56,649] → 55,256 ops/sec [54,590..55,421] ~ overlap (-1.2%) 57,780 ops/sec [57,091..58,660] → 54,740 ops/sec [52,509..55,183] 🔴 -5.3%
parse large flat object 60,205 ops/sec [60,073..60,676] → 59,475 ops/sec [58,523..59,750] 🔴 -1.2% 59,995 ops/sec [55,825..62,218] → 59,122 ops/sec [58,213..59,752] ~ overlap (-1.5%)
parse mixed types 68,991 ops/sec [68,479..69,612] → 68,039 ops/sec [67,820..68,233] 🔴 -1.4% 73,085 ops/sec [72,183..73,758] → 68,640 ops/sec [68,022..69,090] 🔴 -6.1%
stringify simple object 153,280 ops/sec [149,987..154,440] → 148,073 ops/sec [146,115..149,042] 🔴 -3.4% 168,645 ops/sec [166,532..169,803] → 147,408 ops/sec [147,091..148,685] 🔴 -12.6%
stringify nested object 84,991 ops/sec [84,632..85,339] → 83,554 ops/sec [82,889..83,814] 🔴 -1.7% 91,531 ops/sec [90,941..91,825] → 84,047 ops/sec [83,865..84,734] 🔴 -8.2%
stringify array of objects 37,124 ops/sec [37,032..37,259] → 35,762 ops/sec [35,542..35,777] 🔴 -3.7% 41,585 ops/sec [41,485..41,624] → 36,001 ops/sec [35,780..36,311] 🔴 -13.4%
stringify mixed types 64,197 ops/sec [63,814..65,142] → 62,374 ops/sec [61,825..62,608] 🔴 -2.8% 67,548 ops/sec [67,071..68,141] → 62,418 ops/sec [62,149..62,791] 🔴 -7.6%
reviver doubles numbers 47,019 ops/sec [45,795..47,649] → 46,085 ops/sec [45,152..46,361] ~ overlap (-2.0%) 50,526 ops/sec [50,140..50,851] → 47,964 ops/sec [47,507..49,375] 🔴 -5.1%
reviver filters properties 39,957 ops/sec [39,728..39,977] → 39,431 ops/sec [39,204..39,503] 🔴 -1.3% 42,799 ops/sec [42,271..42,887] → 39,905 ops/sec [39,559..40,218] 🔴 -6.8%
reviver on nested object 50,796 ops/sec [50,425..51,128] → 49,680 ops/sec [49,434..49,761] 🔴 -2.2% 55,117 ops/sec [52,678..55,782] → 51,330 ops/sec [50,757..52,211] 🔴 -6.9%
reviver on array 31,043 ops/sec [30,713..31,379] → 30,680 ops/sec [30,343..30,946] ~ overlap (-1.2%) 31,595 ops/sec [31,499..31,717] → 30,432 ops/sec [30,093..30,807] 🔴 -3.7%
replacer function doubles numbers 40,996 ops/sec [40,459..41,201] → 39,941 ops/sec [39,730..40,310] 🔴 -2.6% 47,641 ops/sec [46,690..47,941] → 43,070 ops/sec [42,311..43,478] 🔴 -9.6%
replacer function excludes properties 55,202 ops/sec [54,642..55,777] → 53,541 ops/sec [53,231..53,714] 🔴 -3.0% 58,704 ops/sec [58,203..59,043] → 53,147 ops/sec [52,724..53,317] 🔴 -9.5%
array replacer (allowlist) 92,227 ops/sec [91,904..92,483] → 91,564 ops/sec [89,641..92,646] ~ overlap (-0.7%) 95,948 ops/sec [95,494..96,314] → 93,093 ops/sec [91,700..95,002] 🔴 -3.0%
stringify with 2-space indent 73,527 ops/sec [73,418..74,455] → 72,325 ops/sec [72,190..73,176] 🔴 -1.6% 79,361 ops/sec [78,962..79,508] → 73,328 ops/sec [73,255..73,374] 🔴 -7.6%
stringify with tab indent 74,679 ops/sec [74,299..75,277] → 73,695 ops/sec [73,213..74,464] ~ overlap (-1.3%) 79,489 ops/sec [79,125..79,914] → 73,771 ops/sec [72,887..74,089] 🔴 -7.2%
parse then stringify 44,297 ops/sec [44,179..44,636] → 43,878 ops/sec [43,449..44,030] 🔴 -0.9% 48,084 ops/sec [47,508..48,855] → 45,340 ops/sec [45,052..45,682] 🔴 -5.7%
stringify then parse 26,188 ops/sec [26,083..26,323] → 26,028 ops/sec [25,759..26,210] ~ overlap (-0.6%) 28,203 ops/sec [28,058..28,306] → 26,045 ops/sec [25,695..26,661] 🔴 -7.7%
jsx.jsx — Interp: 🟢 3, 🔴 1, 17 unch. · avg +0.2% · Bytecode: 🟢 5, 🔴 2, 14 unch. · avg +0.2%
Benchmark Interpreted Δ Bytecode Δ
simple element 211,011 ops/sec [207,983..211,999] → 216,322 ops/sec [214,335..217,320] 🟢 +2.5% 311,156 ops/sec [310,296..312,520] → 312,967 ops/sec [309,341..321,794] ~ overlap (+0.6%)
self-closing element 220,107 ops/sec [218,968..222,415] → 221,583 ops/sec [218,955..222,868] ~ overlap (+0.7%) 336,026 ops/sec [334,995..339,455] → 336,296 ops/sec [334,734..338,935] ~ overlap (+0.1%)
element with string attribute 177,578 ops/sec [175,858..178,082] → 179,998 ops/sec [177,715..181,436] ~ overlap (+1.4%) 248,956 ops/sec [248,153..249,919] → 254,660 ops/sec [252,555..258,230] 🟢 +2.3%
element with multiple attributes 155,441 ops/sec [154,557..156,101] → 155,970 ops/sec [153,356..158,970] ~ overlap (+0.3%) 194,339 ops/sec [192,438..195,362] → 195,431 ops/sec [193,724..200,772] ~ overlap (+0.6%)
element with expression attribute 167,058 ops/sec [162,455..168,136] → 168,883 ops/sec [167,618..169,548] ~ overlap (+1.1%) 240,907 ops/sec [239,837..242,170] → 245,241 ops/sec [243,606..246,525] 🟢 +1.8%
text child 211,223 ops/sec [208,567..213,594] → 208,490 ops/sec [196,220..212,507] ~ overlap (-1.3%) 309,445 ops/sec [307,061..310,786] → 311,255 ops/sec [308,924..313,611] ~ overlap (+0.6%)
expression child 205,314 ops/sec [204,016..206,516] → 205,417 ops/sec [203,669..209,872] ~ overlap (+0.1%) 306,414 ops/sec [305,808..309,437] → 313,674 ops/sec [312,483..313,957] 🟢 +2.4%
mixed text and expression 194,538 ops/sec [192,688..195,021] → 193,865 ops/sec [190,658..194,140] ~ overlap (-0.3%) 274,810 ops/sec [271,937..277,634] → 270,929 ops/sec [268,623..272,234] ~ overlap (-1.4%)
nested elements (3 levels) 81,199 ops/sec [80,654..81,452] → 80,750 ops/sec [80,394..81,204] ~ overlap (-0.6%) 118,992 ops/sec [118,460..120,010] → 118,813 ops/sec [118,391..120,784] ~ overlap (-0.2%)
sibling children 59,489 ops/sec [59,330..59,887] → 59,981 ops/sec [59,603..60,206] ~ overlap (+0.8%) 86,478 ops/sec [85,538..86,534] → 85,293 ops/sec [84,557..85,899] ~ overlap (-1.4%)
component element 150,063 ops/sec [148,788..150,755] → 148,904 ops/sec [147,568..150,740] ~ overlap (-0.8%) 212,421 ops/sec [210,858..213,039] → 219,475 ops/sec [217,302..222,121] 🟢 +3.3%
component with children 92,804 ops/sec [91,845..92,927] → 92,892 ops/sec [92,719..93,173] ~ overlap (+0.1%) 134,191 ops/sec [133,590..136,691] → 130,802 ops/sec [129,182..133,830] ~ overlap (-2.5%)
dotted component 126,368 ops/sec [125,390..126,994] → 126,120 ops/sec [122,967..126,429] ~ overlap (-0.2%) 169,513 ops/sec [168,684..170,182] → 173,110 ops/sec [170,323..173,468] 🟢 +2.1%
empty fragment 219,192 ops/sec [218,363..221,206] → 221,207 ops/sec [220,211..223,048] ~ overlap (+0.9%) 352,621 ops/sec [351,697..355,358] → 355,745 ops/sec [351,619..360,468] ~ overlap (+0.9%)
fragment with children 59,076 ops/sec [58,718..59,405] → 59,600 ops/sec [59,513..59,621] 🟢 +0.9% 88,368 ops/sec [88,152..88,821] → 87,357 ops/sec [85,318..88,108] 🔴 -1.1%
spread attributes 109,865 ops/sec [109,078..110,311] → 108,767 ops/sec [108,743..109,357] ~ overlap (-1.0%) 131,897 ops/sec [131,191..133,529] → 129,624 ops/sec [127,652..131,142] 🔴 -1.7%
spread with overrides 95,836 ops/sec [95,193..97,202] → 95,504 ops/sec [95,283..96,385] ~ overlap (-0.3%) 116,433 ops/sec [115,218..119,378] → 113,603 ops/sec [110,930..115,329] ~ overlap (-2.4%)
shorthand props 160,896 ops/sec [159,660..161,451] → 158,785 ops/sec [156,159..160,246] ~ overlap (-1.3%) 224,691 ops/sec [224,287..228,472] → 226,650 ops/sec [224,746..229,569] ~ overlap (+0.9%)
nav bar structure 27,396 ops/sec [27,083..27,822] → 28,236 ops/sec [28,212..28,428] 🟢 +3.1% 39,260 ops/sec [38,816..39,721] → 38,918 ops/sec [38,644..38,980] ~ overlap (-0.9%)
card component tree 32,007 ops/sec [31,668..32,273] → 32,031 ops/sec [31,746..32,112] ~ overlap (+0.1%) 43,320 ops/sec [43,007..43,730] → 44,133 ops/sec [43,334..44,679] ~ overlap (+1.9%)
10 list items via Array.from 14,793 ops/sec [14,681..14,861] → 14,471 ops/sec [14,410..14,555] 🔴 -2.2% 18,737 ops/sec [17,437..18,976] → 18,589 ops/sec [18,461..18,889] ~ overlap (-0.8%)
modules.js — Interp: 🔴 5, 4 unch. · avg -2.1% · Bytecode: 🔴 6, 3 unch. · avg -2.4%
Benchmark Interpreted Δ Bytecode Δ
call imported function 466,269 ops/sec [464,531..471,268] → 458,153 ops/sec [455,645..464,303] 🔴 -1.7% 667,172 ops/sec [663,579..674,866] → 619,758 ops/sec [581,185..628,256] 🔴 -7.1%
call two imported functions 260,722 ops/sec [258,832..261,565] → 253,124 ops/sec [252,063..254,032] 🔴 -2.9% 357,366 ops/sec [355,465..365,184] → 342,542 ops/sec [341,511..343,829] 🔴 -4.1%
read imported constant 1,637,985 ops/sec [1,610,881..1,648,335] → 1,633,899 ops/sec [1,631,729..1,634,846] ~ overlap (-0.2%) 3,648,180 ops/sec [3,455,683..3,661,571] → 3,661,640 ops/sec [3,658,134..3,663,671] ~ overlap (+0.4%)
read imported string 1,632,259 ops/sec [1,608,621..1,644,394] → 1,590,547 ops/sec [1,575,356..1,602,005] 🔴 -2.6% 3,700,443 ops/sec [3,687,588..3,710,813] → 3,686,350 ops/sec [3,681,104..3,688,935] ~ overlap (-0.4%)
read JSON string property 1,661,125 ops/sec [1,633,340..1,680,095] → 1,647,372 ops/sec [1,639,247..1,652,866] ~ overlap (-0.8%) 3,748,882 ops/sec [3,733,435..3,769,638] → 3,684,658 ops/sec [3,679,951..3,690,431] 🔴 -1.7%
read JSON number property 1,631,687 ops/sec [1,625,621..1,637,682] → 1,600,644 ops/sec [1,594,155..1,611,797] 🔴 -1.9% 3,735,996 ops/sec [3,702,207..3,754,940] → 3,610,355 ops/sec [3,584,458..3,622,407] 🔴 -3.4%
read JSON boolean property 1,654,932 ops/sec [1,647,055..1,667,700] → 1,640,267 ops/sec [1,618,660..1,650,476] ~ overlap (-0.9%) 3,720,078 ops/sec [3,668,717..3,731,674] → 3,680,331 ops/sec [3,660,065..3,692,238] ~ overlap (-1.1%)
read JSON array property 1,656,997 ops/sec [1,645,793..1,675,245] → 1,649,958 ops/sec [1,642,798..1,654,852] ~ overlap (-0.4%) 3,734,920 ops/sec [3,721,262..3,756,325] → 3,644,675 ops/sec [3,633,565..3,652,553] 🔴 -2.4%
read multiple JSON properties 967,310 ops/sec [961,909..968,983] → 893,203 ops/sec [887,377..900,618] 🔴 -7.7% 3,358,013 ops/sec [3,354,019..3,365,496] → 3,302,003 ops/sec [3,276,232..3,307,429] 🔴 -1.7%
numbers.js — Interp: 🟢 2, 🔴 4, 5 unch. · avg -0.5% · Bytecode: 🟢 1, 🔴 9, 1 unch. · avg -12.2%
Benchmark Interpreted Δ Bytecode Δ
integer arithmetic 514,423 ops/sec [512,420..516,831] → 501,295 ops/sec [492,021..505,961] 🔴 -2.6% 1,054,143 ops/sec [1,051,600..1,057,139] → 1,068,411 ops/sec [1,064,298..1,072,952] 🟢 +1.4%
floating point arithmetic 597,979 ops/sec [590,377..602,947] → 568,493 ops/sec [563,535..574,764] 🔴 -4.9% 957,518 ops/sec [950,994..962,147] → 765,737 ops/sec [762,832..779,160] 🔴 -20.0%
number coercion 181,609 ops/sec [180,618..183,483] → 181,616 ops/sec [181,153..182,512] ~ overlap (+0.0%) 252,056 ops/sec [251,449..252,607] → 210,736 ops/sec [209,635..211,683] 🔴 -16.4%
toFixed 101,303 ops/sec [100,971..101,846] → 101,879 ops/sec [101,482..102,857] ~ overlap (+0.6%) 112,142 ops/sec [110,144..112,610] → 110,405 ops/sec [109,763..111,747] ~ overlap (-1.5%)
toString 155,702 ops/sec [155,073..156,692] → 155,580 ops/sec [153,515..155,881] ~ overlap (-0.1%) 191,951 ops/sec [190,203..194,403] → 180,059 ops/sec [179,275..183,313] 🔴 -6.2%
valueOf 230,097 ops/sec [227,789..231,418] → 229,350 ops/sec [228,080..230,421] ~ overlap (-0.3%) 274,463 ops/sec [274,264..274,643] → 252,378 ops/sec [250,563..253,406] 🔴 -8.0%
toPrecision 142,958 ops/sec [141,910..143,213] → 144,989 ops/sec [144,281..145,526] 🟢 +1.4% 163,265 ops/sec [161,292..164,159] → 157,647 ops/sec [154,642..158,780] 🔴 -3.4%
Number.isNaN 314,146 ops/sec [313,491..314,806] → 310,554 ops/sec [308,645..312,570] 🔴 -1.1% 381,101 ops/sec [380,896..382,395] → 304,490 ops/sec [301,954..306,860] 🔴 -20.1%
Number.isFinite 301,994 ops/sec [300,320..303,453] → 314,531 ops/sec [312,430..316,878] 🟢 +4.2% 366,692 ops/sec [362,903..367,577] → 288,716 ops/sec [287,716..289,792] 🔴 -21.3%
Number.isInteger 321,352 ops/sec [319,162..322,103] → 312,156 ops/sec [311,008..314,389] 🔴 -2.9% 390,172 ops/sec [387,257..391,141] → 313,945 ops/sec [312,201..315,439] 🔴 -19.5%
Number.parseInt and parseFloat 247,785 ops/sec [245,312..249,260] → 247,536 ops/sec [245,876..248,422] ~ overlap (-0.1%) 294,914 ops/sec [293,755..297,237] → 239,786 ops/sec [238,761..243,307] 🔴 -18.7%
objects.js — Interp: 🟢 1, 🔴 1, 5 unch. · avg +0.4% · Bytecode: 🟢 2, 🔴 1, 4 unch. · avg -0.7%
Benchmark Interpreted Δ Bytecode Δ
create simple object 463,289 ops/sec [450,704..470,611] → 488,105 ops/sec [486,668..489,016] 🟢 +5.4% 555,534 ops/sec [546,027..566,012] → 558,715 ops/sec [554,999..563,647] ~ overlap (+0.6%)
create nested object 231,541 ops/sec [226,937..233,371] → 233,651 ops/sec [231,700..234,967] ~ overlap (+0.9%) 245,242 ops/sec [244,078..249,253] → 244,105 ops/sec [240,402..247,802] ~ overlap (-0.5%)
create 50 objects via Array.from 9,268 ops/sec [9,070..9,394] → 9,256 ops/sec [9,204..9,326] ~ overlap (-0.1%) 9,867 ops/sec [9,789..9,955] → 9,781 ops/sec [9,740..9,848] ~ overlap (-0.9%)
property read 540,675 ops/sec [538,354..545,641] → 539,279 ops/sec [529,362..541,236] ~ overlap (-0.3%) 442,788 ops/sec [420,868..448,726] → 425,289 ops/sec [423,304..426,934] ~ overlap (-4.0%)
Object.keys 280,844 ops/sec [278,676..281,843] → 281,102 ops/sec [278,312..285,294] ~ overlap (+0.1%) 315,346 ops/sec [311,958..320,019] → 302,884 ops/sec [301,054..309,683] 🔴 -4.0%
Object.entries 105,092 ops/sec [104,440..105,573] → 100,547 ops/sec [99,786..101,391] 🔴 -4.3% 105,613 ops/sec [104,653..106,505] → 108,518 ops/sec [106,645..108,945] 🟢 +2.8%
spread operator 176,722 ops/sec [175,127..178,177] → 178,295 ops/sec [177,027..179,798] ~ overlap (+0.9%) 203,488 ops/sec [202,059..203,801] → 205,779 ops/sec [204,388..207,793] 🟢 +1.1%
promises.js — Interp: 🟢 2, 🔴 3, 7 unch. · avg -0.2% · Bytecode: 🔴 6, 6 unch. · avg -2.6%
Benchmark Interpreted Δ Bytecode Δ
Promise.resolve(value) 534,972 ops/sec [528,140..538,499] → 518,573 ops/sec [515,016..520,210] 🔴 -3.1% 572,692 ops/sec [570,024..573,406] → 537,035 ops/sec [535,015..541,073] 🔴 -6.2%
new Promise(resolve => resolve(value)) 188,976 ops/sec [187,571..190,357] → 192,798 ops/sec [191,504..193,411] 🟢 +2.0% 228,557 ops/sec [228,172..228,638] → 221,760 ops/sec [217,202..222,721] 🔴 -3.0%
Promise.reject(reason) 544,024 ops/sec [537,962..545,986] → 529,389 ops/sec [528,376..533,876] 🔴 -2.7% 555,283 ops/sec [547,634..578,765] → 514,785 ops/sec [510,514..515,954] 🔴 -7.3%
resolve + then (1 handler) 177,210 ops/sec [176,454..178,425] → 174,628 ops/sec [171,097..175,959] 🔴 -1.5% 200,868 ops/sec [199,287..202,484] → 191,642 ops/sec [190,668..192,540] 🔴 -4.6%
resolve + then chain (3 deep) 69,641 ops/sec [68,313..70,014] → 69,767 ops/sec [69,350..70,199] ~ overlap (+0.2%) 76,967 ops/sec [75,094..77,532] → 75,831 ops/sec [75,497..76,044] ~ overlap (-1.5%)
resolve + then chain (10 deep) 22,003 ops/sec [21,850..22,021] → 21,785 ops/sec [21,453..22,047] ~ overlap (-1.0%) 24,711 ops/sec [24,291..24,814] → 24,409 ops/sec [24,115..24,650] ~ overlap (-1.2%)
reject + catch + then 98,265 ops/sec [97,669..98,900] → 99,493 ops/sec [98,739..99,804] ~ overlap (+1.2%) 110,339 ops/sec [109,159..111,157] → 106,754 ops/sec [106,639..106,984] 🔴 -3.2%
resolve + finally + then 83,147 ops/sec [82,284..83,392] → 83,830 ops/sec [83,400..83,946] 🟢 +0.8% 90,983 ops/sec [90,096..91,559] → 90,161 ops/sec [88,919..91,772] ~ overlap (-0.9%)
Promise.all (5 resolved) 31,072 ops/sec [30,816..31,493] → 30,976 ops/sec [30,792..31,268] ~ overlap (-0.3%) 32,647 ops/sec [32,527..33,200] → 32,741 ops/sec [32,380..33,163] ~ overlap (+0.3%)
Promise.race (5 resolved) 32,872 ops/sec [32,042..33,488] → 33,057 ops/sec [32,735..33,279] ~ overlap (+0.6%) 35,414 ops/sec [34,950..35,494] → 34,763 ops/sec [34,561..35,078] ~ overlap (-1.8%)
Promise.allSettled (5 mixed) 26,190 ops/sec [25,970..26,631] → 26,284 ops/sec [26,117..26,472] ~ overlap (+0.4%) 27,417 ops/sec [27,180..27,789] → 27,470 ops/sec [27,220..27,641] ~ overlap (+0.2%)
Promise.any (5 mixed) 30,946 ops/sec [30,757..31,174] → 31,347 ops/sec [31,064..31,534] ~ overlap (+1.3%) 33,170 ops/sec [32,991..33,262] → 32,338 ops/sec [31,987..32,527] 🔴 -2.5%
regexp.js — Interp: 🔴 1, 10 unch. · avg +0.1% · Bytecode: 🔴 6, 5 unch. · avg -4.4%
Benchmark Interpreted Δ Bytecode Δ
regex literal creation 150,229 ops/sec [149,381..152,904] → 154,081 ops/sec [152,285..154,747] ~ overlap (+2.6%) 135,952 ops/sec [135,771..136,908] → 135,368 ops/sec [134,027..135,840] ~ overlap (-0.4%)
new RegExp(pattern, flags) 130,551 ops/sec [128,715..131,427] → 130,766 ops/sec [129,429..132,001] ~ overlap (+0.2%) 134,780 ops/sec [133,595..136,742] → 133,473 ops/sec [129,750..136,436] ~ overlap (-1.0%)
RegExp(existingRegex) returns the same regex 697,567 ops/sec [695,084..698,391] → 696,234 ops/sec [693,529..698,099] ~ overlap (-0.2%) 1,031,399 ops/sec [1,030,590..1,032,074] → 898,467 ops/sec [895,523..900,540] 🔴 -12.9%
test() on a global regex 138,429 ops/sec [137,350..139,448] → 138,383 ops/sec [136,948..138,555] ~ overlap (-0.0%) 164,154 ops/sec [162,627..164,672] → 157,110 ops/sec [156,188..157,557] 🔴 -4.3%
exec() with capture groups 118,407 ops/sec [116,845..119,298] → 117,406 ops/sec [116,750..118,035] ~ overlap (-0.8%) 138,202 ops/sec [136,480..139,060] → 130,195 ops/sec [128,899..130,655] 🔴 -5.8%
toString() 469,211 ops/sec [464,161..470,894] → 458,365 ops/sec [457,277..463,315] 🔴 -2.3% 670,677 ops/sec [668,594..673,322] → 579,068 ops/sec [576,045..580,342] 🔴 -13.7%
match() with global regex 39,063 ops/sec [38,631..39,186] → 39,014 ops/sec [38,814..39,279] ~ overlap (-0.1%) 39,130 ops/sec [39,024..39,389] → 37,874 ops/sec [37,610..38,323] 🔴 -3.2%
matchAll() with capture groups 20,052 ops/sec [19,859..20,188] → 20,388 ops/sec [20,149..20,472] ~ overlap (+1.7%) 25,445 ops/sec [25,278..25,620] → 24,315 ops/sec [24,149..24,869] 🔴 -4.4%
replace() with global regex 37,355 ops/sec [37,306..37,367] → 37,401 ops/sec [37,188..37,568] ~ overlap (+0.1%) 37,003 ops/sec [32,512..37,607] → 36,492 ops/sec [36,407..36,676] ~ overlap (-1.4%)
search() with regex 77,724 ops/sec [76,884..78,493] → 77,989 ops/sec [77,478..78,754] ~ overlap (+0.3%) 74,668 ops/sec [69,917..76,228] → 72,914 ops/sec [71,444..74,878] ~ overlap (-2.3%)
split() with regex separator 39,033 ops/sec [38,731..39,345] → 38,803 ops/sec [38,225..39,084] ~ overlap (-0.6%) 37,404 ops/sec [36,947..38,139] → 37,917 ops/sec [37,415..38,113] ~ overlap (+1.4%)
strings.js — Interp: 🔴 7, 4 unch. · avg -2.7% · Bytecode: 🟢 1, 🔴 7, 3 unch. · avg -5.2%
Benchmark Interpreted Δ Bytecode Δ
string concatenation 403,700 ops/sec [402,929..404,175] → 406,583 ops/sec [403,085..408,729] ~ overlap (+0.7%) 296,505 ops/sec [281,647..330,347] → 283,015 ops/sec [279,547..284,644] ~ overlap (-4.5%)
template literal 665,996 ops/sec [665,213..668,409] → 611,707 ops/sec [603,609..616,844] 🔴 -8.2% 602,806 ops/sec [592,218..604,001] → 624,515 ops/sec [616,709..628,018] 🟢 +3.6%
string repeat 394,429 ops/sec [393,408..395,197] → 394,108 ops/sec [391,583..394,962] ~ overlap (-0.1%) 448,374 ops/sec [416,981..470,843] → 449,313 ops/sec [447,777..449,698] ~ overlap (+0.2%)
split and join 134,058 ops/sec [133,628..134,193] → 132,828 ops/sec [132,136..133,158] 🔴 -0.9% 145,370 ops/sec [139,801..150,916] → 147,998 ops/sec [145,467..150,562] ~ overlap (+1.8%)
indexOf and includes 169,550 ops/sec [169,279..169,768] → 163,726 ops/sec [161,292..165,410] 🔴 -3.4% 200,187 ops/sec [198,283..201,897] → 180,335 ops/sec [179,878..181,500] 🔴 -9.9%
toUpperCase and toLowerCase 255,170 ops/sec [252,417..257,982] → 246,727 ops/sec [240,150..248,966] 🔴 -3.3% 340,291 ops/sec [339,530..341,271] → 314,815 ops/sec [312,713..316,403] 🔴 -7.5%
slice and substring 152,890 ops/sec [150,606..153,918] → 148,892 ops/sec [147,422..153,111] ~ overlap (-2.6%) 203,423 ops/sec [201,486..204,927] → 189,010 ops/sec [184,275..189,524] 🔴 -7.1%
trim operations 183,333 ops/sec [181,972..187,796] → 179,046 ops/sec [177,436..181,409] 🔴 -2.3% 251,528 ops/sec [249,989..253,536] → 235,154 ops/sec [234,786..235,761] 🔴 -6.5%
replace and replaceAll 200,845 ops/sec [197,831..203,752] → 189,424 ops/sec [187,780..191,845] 🔴 -5.7% 240,484 ops/sec [238,841..240,940] → 213,912 ops/sec [211,472..214,387] 🔴 -11.0%
startsWith and endsWith 135,320 ops/sec [133,884..136,056] → 133,238 ops/sec [132,172..134,226] ~ overlap (-1.5%) 165,370 ops/sec [164,088..166,032] → 152,186 ops/sec [151,001..153,575] 🔴 -8.0%
padStart and padEnd 194,405 ops/sec [193,270..196,104] → 188,844 ops/sec [186,342..189,644] 🔴 -2.9% 240,178 ops/sec [237,810..243,071] → 219,592 ops/sec [216,199..222,529] 🔴 -8.6%
typed-arrays.js — Interp: 🟢 2, 🔴 9, 11 unch. · avg -1.1% · Bytecode: 🟢 6, 🔴 14, 2 unch. · avg -1.9%
Benchmark Interpreted Δ Bytecode Δ
new Int32Array(0) 336,598 ops/sec [333,875..338,484] → 326,331 ops/sec [324,459..327,620] 🔴 -3.1% 367,883 ops/sec [366,603..372,425] → 360,273 ops/sec [353,689..361,461] 🔴 -2.1%
new Int32Array(100) 300,954 ops/sec [300,091..302,592] → 296,038 ops/sec [294,059..297,532] 🔴 -1.6% 330,481 ops/sec [328,911..334,158] → 325,308 ops/sec [322,618..327,160] 🔴 -1.6%
new Int32Array(1000) 179,053 ops/sec [178,153..181,434] → 176,919 ops/sec [172,070..178,336] ~ overlap (-1.2%) 177,750 ops/sec [175,352..179,231] → 189,790 ops/sec [188,433..192,435] 🟢 +6.8%
new Float64Array(100) 270,123 ops/sec [267,301..273,095] → 265,253 ops/sec [261,130..267,280] 🔴 -1.8% 289,460 ops/sec [285,297..292,100] → 293,988 ops/sec [292,166..296,780] 🟢 +1.6%
Int32Array.from([...]) 173,706 ops/sec [173,586..174,516] → 180,353 ops/sec [179,620..181,504] 🟢 +3.8% 187,800 ops/sec [186,433..188,673] → 186,756 ops/sec [185,378..189,432] ~ overlap (-0.6%)
Int32Array.of(1, 2, 3, 4, 5) 302,788 ops/sec [299,193..304,909] → 296,478 ops/sec [292,882..299,226] ~ overlap (-2.1%) 340,762 ops/sec [332,676..341,545] → 344,966 ops/sec [343,453..347,015] 🟢 +1.2%
sequential write 100 elements 3,457 ops/sec [3,428..3,497] → 3,428 ops/sec [3,423..3,461] ~ overlap (-0.8%) 15,296 ops/sec [15,212..15,332] → 14,651 ops/sec [14,515..14,852] 🔴 -4.2%
sequential read 100 elements 3,494 ops/sec [3,473..3,532] → 3,435 ops/sec [3,417..3,512] ~ overlap (-1.7%) 10,494 ops/sec [10,330..10,556] → 9,992 ops/sec [9,955..10,213] 🔴 -4.8%
Float64Array write 100 elements 3,197 ops/sec [3,178..3,208] → 3,120 ops/sec [3,097..3,153] 🔴 -2.4% 10,814 ops/sec [10,779..10,834] → 10,505 ops/sec [10,459..10,629] 🔴 -2.9%
fill(42) 47,165 ops/sec [47,048..47,237] → 45,365 ops/sec [45,305..45,499] 🔴 -3.8% 47,209 ops/sec [46,975..47,502] → 45,764 ops/sec [45,727..45,804] 🔴 -3.1%
slice() 201,367 ops/sec [199,874..202,371] → 198,557 ops/sec [198,050..199,120] 🔴 -1.4% 209,120 ops/sec [206,800..209,788] → 215,064 ops/sec [212,889..216,961] 🟢 +2.8%
map(x => x * 2) 7,617 ops/sec [7,588..7,682] → 7,577 ops/sec [7,553..7,662] ~ overlap (-0.5%) 8,859 ops/sec [8,805..8,984] → 8,484 ops/sec [8,110..8,660] 🔴 -4.2%
filter(x => x > 50) 7,892 ops/sec [7,833..7,919] → 7,914 ops/sec [7,880..7,926] ~ overlap (+0.3%) 9,947 ops/sec [9,913..10,069] → 9,307 ops/sec [9,216..9,405] 🔴 -6.4%
reduce (sum) 7,474 ops/sec [7,411..7,608] → 7,714 ops/sec [7,648..7,763] 🟢 +3.2% 7,546 ops/sec [7,473..7,561] → 7,070 ops/sec [6,986..7,091] 🔴 -6.3%
sort() 166,278 ops/sec [165,573..166,768] → 164,639 ops/sec [164,220..165,010] 🔴 -1.0% 166,119 ops/sec [158,427..166,337] → 178,327 ops/sec [178,232..178,372] 🟢 +7.3%
indexOf() 417,858 ops/sec [414,167..419,861] → 414,451 ops/sec [411,024..415,163] ~ overlap (-0.8%) 491,179 ops/sec [484,842..493,213] → 469,301 ops/sec [468,096..474,929] 🔴 -4.5%
reverse() 320,978 ops/sec [319,808..322,431] → 304,659 ops/sec [298,047..309,575] 🔴 -5.1% 344,404 ops/sec [344,008..344,642] → 345,602 ops/sec [344,588..347,249] ~ overlap (+0.3%)
create view over existing buffer 395,728 ops/sec [394,465..397,122] → 390,709 ops/sec [388,768..392,501] 🔴 -1.3% 468,204 ops/sec [463,008..469,205] → 437,601 ops/sec [435,561..438,476] 🔴 -6.5%
subarray() 419,415 ops/sec [417,935..420,837] → 417,158 ops/sec [413,322..419,948] ~ overlap (-0.5%) 525,836 ops/sec [521,392..526,674] → 499,974 ops/sec [490,104..503,172] 🔴 -4.9%
set() from array 544,500 ops/sec [536,592..546,892] → 544,405 ops/sec [541,165..546,330] ~ overlap (-0.0%) 698,466 ops/sec [698,192..700,881] → 605,420 ops/sec [604,878..607,373] 🔴 -13.3%
for-of loop 4,966 ops/sec [4,949..4,974] → 4,901 ops/sec [4,880..4,956] ~ overlap (-1.3%) 17,038 ops/sec [16,867..17,129] → 16,630 ops/sec [16,405..16,810] 🔴 -2.4%
spread into array 17,205 ops/sec [17,134..17,320] → 17,035 ops/sec [16,865..17,173] ~ overlap (-1.0%) 85,619 ops/sec [84,993..87,154] → 90,975 ops/sec [89,782..91,667] 🟢 +6.3%

Measured on ubuntu-latest x64. Benchmark ranges compare cached main-branch min/max ops/sec with the PR run; overlapping ranges are treated as unchanged noise. Percentage deltas are secondary context.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
units/Goccia.Builtins.TestAssertions.Test.pas (2)

2907-2916: Test mock(impl) through the exported helper, not the raw constructor.

This case builds TGocciaMockFunctionValue directly, so it never proves that the mock() global forwards its optional implementation argument correctly. A bug in the registration layer would still leave this test green.

🧪 Suggested test adjustment
-  M := TGocciaMockFunctionValue.Create(
-    TGocciaNativeFunctionValue.Create(CallbackReturn42, 'impl', 0));
+  M := CreateMockViaGlobal(
+    TGocciaNativeFunctionValue.Create(CallbackReturn42, 'impl', 0));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.Test.pas` around lines 2907 - 2916, The
test TestMockWithImplementation currently constructs TGocciaMockFunctionValue
directly (using TGocciaNativeFunctionValue and CallbackReturn42), which doesn't
verify that the exported mock() helper forwards an implementation; update the
test to call the exported mock(impl) helper with the implementation (e.g.,
CallbackReturn42) and assign its result to M, then use CallMockFunction(M, [])
and Expect on the returned value—replace direct TGocciaMockFunctionValue.Create
usage with the mock(...) helper call so the registration/forwarding path is
exercised.

3280-3387: Exercise spies through the patched object property at least once.

Every spy assertion in this block calls Spy directly, so the suite never verifies the behavior spyOn() is supposed to preserve: invoking obj.method(...) with the original receiver. A regression that drops call-site this or records the wrong instance would still pass.

Based on learnings: shorthand methods receive call-site this, and this binding is resolved via virtual dispatch on TGocciaFunctionValue.BindThis.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.Test.pas` around lines 3280 - 3387,
These tests call Spy directly instead of exercising the patched object property,
so they don't verify call-site "this" binding or that obj.method(...) routes
through the spy; modify each affected test (TestSpyOnPassesThrough,
TestSpyOnTracksCalls, TestSpyOnMockImplementationOverrides,
TestSpyOnMockRestoreRestoresOriginal, TestSpyOnMockReturnValueOverrides) to
invoke the spy via the object's property (get the method from Obj with
Obj.GetProperty('method') or call the method through Obj rather than calling Spy
directly) and pass Obj as the receiver so BindThis on TGocciaFunctionValue is
exercised; ensure CallMockFunction is used on the property/value retrieved from
Obj (or otherwise simulate calling obj.method(...)) so the spy records the
correct instance and "this".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@AGENTS.md`:
- Line 527: Remove guidance that points matcher coverage to JavaScript E2E
tests: update the description for tests/language/testing to keep mock.js and
spyOn.js but remove or relocate any mention of expect-toMatch.js and instead
state that TGocciaExpectationValue matchers are tested in
units/Goccia.Builtins.TestAssertions.Test.pas; ensure references to
expect-toMatch.js are deleted or moved and explicitly mention
TGocciaExpectationValue and units/Goccia.Builtins.TestAssertions.Test.pas so
contributors know where matcher coverage lives.

In `@docs/testing.md`:
- Around line 380-384: The example declares const fn twice which causes a
runtime error; update the snippet to use distinct bindings or separate examples
so each mock() call is shown independently — e.g., keep the first as const fn =
mock(); and rename the second to const fnWithImpl = mock((x) => x * 2); or split
into two code blocks demonstrating mock() and mock((x)=>x*2) so the functions
(mock, fn, fnWithImpl) are uniquely identified and the example runs.

In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 3098-3108: The spyOn implementation currently permits spying on
any existing property; before calling TGocciaMockFunctionValue.CreateSpy check
that ExistingValue is actually callable: require ExistingValue to be a
TGocciaFunctionBase (or an explicit accessor type your runtime uses for
getter/setter properties), and if not call ThrowError('spyOn: cannot spy on
non-callable property "' + MethodName + '"', 0, 0) and return
TGocciaUndefinedLiteralValue.UndefinedValue; update the guard around
ExistingValue (the value returned by Target.GetProperty) so only
callable/allowed accessor types proceed to CreateSpy.

In `@units/Goccia.Values.MockFunction.pas`:
- Around line 131-145: The spy currently only saves the resolved value
(FSpyOriginalValue) and re-applies it with DefineProperty, which destroys
accessor/non-enumerable flags and turns inherited methods into own data
properties; update Create (the constructor code that sets
FSpyTarget/FSpyMethodName/FSpyOriginalValue) to also capture and store the
original property descriptor and whether it was an own property (e.g., add
fields like FSpyOriginalDescriptor and FSpyWasOwnProperty), and then change
mockRestore() to restore by re-applying the saved descriptor when
FSpyWasOwnProperty is true or by deleting the shadowing own property when it was
inherited so the original inherited descriptor remains intact (use the existing
ATarget.DefineProperty and any available GetOwnPropertyDescriptor/GetProperty
helpers to obtain the full descriptor and ownership info).
- Around line 281-294: The catch in the implementation branches (around
FImplementationOnce usage and the call to TGocciaFunctionBase.Call) only handles
TGocciaThrowValue so native/runtime failures (TGocciaError and other Exception)
are not recorded into FResults; update the try/except blocks in both places (the
block using FImplementationOnce and the similar block at 308-319) to catch
TGocciaThrowValue as before but also catch TGocciaError and generic Exception,
and for each caught exception append a CreateResultEntry(RESULT_TYPE_THROW,
<appropriate value>) to FResults (use the exception's Value for
TGocciaThrowValue/TGocciaError and a converted error value or message for
generic Exception) before re-raising so FResults stays aligned with FCalls.
- Around line 199-247: The temporary TGocciaValue instances created in
CreateResultEntry, BuildCallsArray, BuildResultsArray, BuildInstancesArray and
BuildMockObject must be protected from GC while being constructed and before
they are attached to their parent objects: wrap creations in AddTempRoot(...)
and RemoveTempRoot(...) calls (or add the temp container as a temp root) so each
temporary (e.g. the TGocciaObjectValue returned by CreateResultEntry, each
element added to Result.Elements in Build*Array, and LastCall in
BuildMockObject) is registered with AddTempRoot immediately after creation and
only unregistered with RemoveTempRoot after the value has been AssignProperty'd
or added to the final object/array; ensure every code path (including early
returns) removes the temp root to avoid leaks.

---

Nitpick comments:
In `@units/Goccia.Builtins.TestAssertions.Test.pas`:
- Around line 2907-2916: The test TestMockWithImplementation currently
constructs TGocciaMockFunctionValue directly (using TGocciaNativeFunctionValue
and CallbackReturn42), which doesn't verify that the exported mock() helper
forwards an implementation; update the test to call the exported mock(impl)
helper with the implementation (e.g., CallbackReturn42) and assign its result to
M, then use CallMockFunction(M, []) and Expect on the returned value—replace
direct TGocciaMockFunctionValue.Create usage with the mock(...) helper call so
the registration/forwarding path is exercised.
- Around line 3280-3387: These tests call Spy directly instead of exercising the
patched object property, so they don't verify call-site "this" binding or that
obj.method(...) routes through the spy; modify each affected test
(TestSpyOnPassesThrough, TestSpyOnTracksCalls,
TestSpyOnMockImplementationOverrides, TestSpyOnMockRestoreRestoresOriginal,
TestSpyOnMockReturnValueOverrides) to invoke the spy via the object's property
(get the method from Obj with Obj.GetProperty('method') or call the method
through Obj rather than calling Spy directly) and pass Obj as the receiver so
BindThis on TGocciaFunctionValue is exercised; ensure CallMockFunction is used
on the property/value retrieved from Obj (or otherwise simulate calling
obj.method(...)) so the spy records the correct instance and "this".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f864d3c1-3e69-42fa-bb84-5ebf5dd7f6dd

📥 Commits

Reviewing files that changed from the base of the PR and between a4bbae5 and afd9307.

📒 Files selected for processing (5)
  • AGENTS.md
  • docs/testing.md
  • units/Goccia.Builtins.TestAssertions.Test.pas
  • units/Goccia.Builtins.TestAssertions.pas
  • units/Goccia.Values.MockFunction.pas

Comment thread AGENTS.md Outdated
Comment thread docs/testing.md
Comment thread units/Goccia.Builtins.TestAssertions.pas
Comment thread units/Goccia.Values.MockFunction.pas
Comment thread units/Goccia.Values.MockFunction.pas Outdated
Comment thread units/Goccia.Values.MockFunction.pas Outdated
- Reject non-callable spy targets with clear error message
- Preserve original property descriptor in spyOn for proper mockRestore
- Temp-root metadata objects during construction to prevent GC sweep
- Catch all exception types in Call to keep FResults aligned with FCalls
- Fix duplicate const fn in docs/testing.md example
- Fix CLAUDE.md test directory guidance for matcher coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

♻️ Duplicate comments (4)
units/Goccia.Values.MockFunction.pas (3)

143-152: ⚠️ Potential issue | 🟠 Major

Restore the saved descriptor by kind, not as a data property.

Line 152 installs the spy with a generic writable/enumerable data descriptor, and Lines 503-505 restore it the same way. Own non-enumerable methods change shape while the spy is active, and accessor properties still cannot round-trip correctly on mockRestore().

Also applies to: 501-505


221-246: ⚠️ Potential issue | 🟠 Major

Temp-root the .mock arrays until they are attached.

BuildCallsArray, BuildResultsArray, and BuildInstancesArray return fresh TGocciaArrayValues that are only in Pascal temporaries when Lines 256-258 assign them onto Result. If GC runs during that descriptor allocation window, those arrays are still unreachable.

Based on learnings: Values held only by Pascal code (not in any GocciaScript scope) must be protected with AddTempRoot/RemoveTempRoot for the duration they are needed.

Also applies to: 256-258

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.MockFunction.pas` around lines 221 - 246,
BuildCallsArray, BuildResultsArray and BuildInstancesArray create
TGocciaArrayValue instances that can be collected before they are attached; wrap
the newly created array with AddTempRoot immediately after creation and ensure
the matching RemoveTempRoot is called in the caller right after the array is
assigned/attached to its owner (the place that currently assigns these returned
arrays onto the descriptor/result). Use AddTempRoot(Result) in each of
BuildCallsArray/BuildResultsArray/BuildInstancesArray right after
TGocciaArrayValue.Create and remove the temp root with RemoveTempRoot at the
attachment site so the arrays are protected during the allocation window.

309-319: ⚠️ Potential issue | 🟠 Major

Record thrown JS error objects in mock.results.

The generic Exception path currently stores E.Message as a string. That flattens runtime-thrown TypeError/Error objects into text, so mock.results[n].value no longer matches the actual thrown JS value for native and built-in failures.

Minimal fix
       on E: TGocciaThrowValue do
       begin
         FResults.Add(CreateResultEntry(RESULT_TYPE_THROW, E.Value));
         raise;
       end;
+      on E: TGocciaError do
+      begin
+        FResults.Add(CreateResultEntry(RESULT_TYPE_THROW, E.Value));
+        raise;
+      end;
       on E: Exception do
       begin
         FResults.Add(CreateResultEntry(RESULT_TYPE_THROW,
           TGocciaStringLiteralValue.Create(E.Message)));
         raise;
       end;
Based on learnings: All runtime errors must use error helper functions so the exception carries a proper JS Error object catchable from JavaScript `try...catch`.

Also applies to: 340-350

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.MockFunction.pas` around lines 309 - 319, The Exception
catch currently stores E.Message as a string (TGocciaStringLiteralValue.Create),
which flattens native JS Error objects; instead replace that string-wrapping
with the runtime error helper that builds a proper JS Error object and pass that
into CreateResultEntry(RESULT_TYPE_THROW, ...). Update the except on Exception
block that follows TGocciaThrowValue and the similar handlers around the 340-350
area so they use the project's error helper (the function that constructs JS
Error values) rather than TGocciaStringLiteralValue.Create(E.Message), keeping
the TGocciaThrowValue branch unchanged.
AGENTS.md (1)

527-527: ⚠️ Potential issue | 🟡 Minor

Keep matcher coverage out of the JS test-directory guidance.

This bullet still points contributors at tests/language/testing/ for matcher coverage, even though the same sentence says TGocciaExpectationValue matchers live in units/Goccia.Builtins.TestAssertions.Test.pas. That sends people back to the wrong layer.

Based on learnings: TGocciaExpectationValue matchers must NOT be tested via JavaScript end-to-end tests under tests/; the correct layer is units/Goccia.Builtins.TestAssertions.Test.pas.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@AGENTS.md` at line 527, The documentation bullet incorrectly directs matcher
coverage to the JS test directory; remove the reference to
tests/language/testing from that sentence and replace it with a clear statement
that TGocciaExpectationValue matchers are exercised in the unit test
Goccia.Builtins.TestAssertions.Test (the Pascal unit) rather than via JavaScript
end-to-end tests, ensuring the doc references the TGocciaExpectationValue
matcher symbol and the Goccia.Builtins.TestAssertions.Test unit to guide
contributors to the correct test layer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/testing.md`:
- Around line 376-390: The docs currently show global mock() and spyOn() usage
and describe mock.instances as per-call this values, which is
GocciaScript-specific and conflicts with Vitest where the equivalents are
vi.fn() and vi.spyOn() and mock.instances holds constructor instances only;
update the Mock Functions section to (1) explicitly state that mock(), spyOn(),
and the described mock.instances semantics are GocciaScript TestRunner behavior,
(2) add a short note showing Vitest equivalents (vi.fn / vi.spyOn) and mention
the differing mock.instances semantics, or (3) provide a brief setup note for
making examples cross-compatible, referencing the mock(), spyOn(), and
mock.instances symbols so readers can find and adjust the examples accordingly.

In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 1672-1703: The toHaveBeenCalled and toHaveReturned matchers
currently ignore AArgs allowing accidental arguments; update
TGocciaExpectationValue.ToHaveBeenCalled (and the analogous ToHaveReturned
implementation) to first verify that AArgs.Count = 0 and if not call
TGocciaTestAssertions(FTestAssertions).AssertionFailed with a clear message like
"toHaveBeenCalled/toHaveReturned take no arguments" and return
TGocciaUndefinedLiteralValue.UndefinedValue immediately; leave the existing
mock/return checks untouched for the zero-argument case so behavior only changes
when extra args are provided.
- Around line 1944-1946: Replace hardcoded property name strings 'type' and
'value' used when accessing mock result properties with the centralized runtime
constants in Goccia.Constants.PropertyNames: change calls like
ResultObj.GetProperty('type') and comparisons against
TGocciaStringLiteralValue.Value being 'return' or other literals to use
Goccia.Constants.PropertyNames.Type and Goccia.Constants.PropertyNames.Value (or
the exact field names in PropertyNames) and compare against the same centralized
constants instead of literal strings; update all occurrences referenced (e.g.,
the checks around ResultType := ResultObj.GetProperty(...), other
ResultObj.GetProperty(...) uses, and TGocciaStringLiteralValue.Value comparisons
in the matcher code) so every access uses PropertyNames instead of hardcoded
'type'/'value'.
- Around line 3066-3075: The MockFunction currently accepts non-function or
extra arguments and silently returns a default mock; change
TGocciaTestAssertions.MockFunction to validate arguments strictly: require
exactly one argument and that AArgs.GetElement(0) is a TGocciaFunctionBase,
otherwise raise an explicit error/exception (with a clear message) instead of
returning a nil-backed TGocciaMockFunctionValue; ensure callers still get
TGocciaMockFunctionValue when valid, and use the function/class names
TGocciaTestAssertions.MockFunction and TGocciaMockFunctionValue to locate and
update the logic.

In `@units/Goccia.Values.MockFunction.pas`:
- Around line 96-105: Replace the file-local mock property name constants
(MOCK_DEFAULT_NAME, RESULT_TYPE_RETURN, RESULT_TYPE_THROW, PROP_TYPE,
PROP_VALUE, PROP_CALLS, PROP_RESULTS, PROP_INSTANCES, PROP_LAST_CALL and the
similar constants around lines 167-198) with the shared runtime constants from
Goccia.Constants.PropertyNames; remove the duplicated string literals in
units/Goccia.Values.MockFunction.pas and reference the appropriate identifiers
exported by Goccia.Constants.PropertyNames (e.g., the mockImplementation/mock,
calls, results, value, lastCall names) wherever those local constants are used
in functions or classes in this unit so the code uses the centralized constants
instead of file-local copies.
- Around line 25-28: Replace the two separate FIFO lists FImplementationOnce and
FReturnValuesOnce with a single ordered once-queue and make the dispatch and
enqueue methods operate on that queue: introduce a single field (e.g.
FOnceQueue) that stores tagged entries for either a return-value or an
implementation; update mockReturnValueOnce and mockImplementationOnce to enqueue
tagged entries into FOnceQueue preserving call order; change the call dispatch
logic (the function that currently drains FImplementationOnce before
FReturnValuesOnce) to pop the next entry from FOnceQueue and either return the
stored value or execute the stored implementation accordingly; remove usage of
FImplementationOnce and FReturnValuesOnce and ensure FDefaultReturnValue /
FHasDefaultReturnValue remain unchanged for fallback behavior.

---

Duplicate comments:
In `@AGENTS.md`:
- Line 527: The documentation bullet incorrectly directs matcher coverage to the
JS test directory; remove the reference to tests/language/testing from that
sentence and replace it with a clear statement that TGocciaExpectationValue
matchers are exercised in the unit test Goccia.Builtins.TestAssertions.Test (the
Pascal unit) rather than via JavaScript end-to-end tests, ensuring the doc
references the TGocciaExpectationValue matcher symbol and the
Goccia.Builtins.TestAssertions.Test unit to guide contributors to the correct
test layer.

In `@units/Goccia.Values.MockFunction.pas`:
- Around line 221-246: BuildCallsArray, BuildResultsArray and
BuildInstancesArray create TGocciaArrayValue instances that can be collected
before they are attached; wrap the newly created array with AddTempRoot
immediately after creation and ensure the matching RemoveTempRoot is called in
the caller right after the array is assigned/attached to its owner (the place
that currently assigns these returned arrays onto the descriptor/result). Use
AddTempRoot(Result) in each of
BuildCallsArray/BuildResultsArray/BuildInstancesArray right after
TGocciaArrayValue.Create and remove the temp root with RemoveTempRoot at the
attachment site so the arrays are protected during the allocation window.
- Around line 309-319: The Exception catch currently stores E.Message as a
string (TGocciaStringLiteralValue.Create), which flattens native JS Error
objects; instead replace that string-wrapping with the runtime error helper that
builds a proper JS Error object and pass that into
CreateResultEntry(RESULT_TYPE_THROW, ...). Update the except on Exception block
that follows TGocciaThrowValue and the similar handlers around the 340-350 area
so they use the project's error helper (the function that constructs JS Error
values) rather than TGocciaStringLiteralValue.Create(E.Message), keeping the
TGocciaThrowValue branch unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: df305a1e-5a16-4ae2-8974-0d8d3fe87f0d

📥 Commits

Reviewing files that changed from the base of the PR and between afd9307 and a80dbb1.

📒 Files selected for processing (4)
  • AGENTS.md
  • docs/testing.md
  • units/Goccia.Builtins.TestAssertions.pas
  • units/Goccia.Values.MockFunction.pas

Comment thread docs/testing.md Outdated
Comment thread units/Goccia.Builtins.TestAssertions.pas
Comment thread units/Goccia.Builtins.TestAssertions.pas
Comment thread units/Goccia.Builtins.TestAssertions.pas Outdated
Comment thread units/Goccia.Builtins.TestAssertions.pas
Comment thread units/Goccia.Values.MockFunction.pas Outdated
Comment thread units/Goccia.Values.MockFunction.pas Outdated
frostney and others added 2 commits April 8, 2026 12:39
- Enforce zero-argument arity on toHaveBeenCalled and toHaveReturned
- Validate non-negative integer for toHaveBeenCalledTimes, toHaveReturnedTimes
- Validate positive integer index for toHaveBeenNthCalledWith, toHaveNthReturnedWith
- Document mock()/spyOn() as GocciaScript-specific in Vitest divergences table
- Clarify mock.instances semantic difference vs Vitest/Jest

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move mock property names (calls, results, instances, lastCall, type)
  into Goccia.Constants.PropertyNames; remove local duplicates
- Replace hardcoded 'type'/'value' strings in matchers with shared constants
- Merge FImplementationOnce and FReturnValuesOnce into a single FIFO queue
  so interleaved mockImplementationOnce/mockReturnValueOnce respect call order
- Fail fast when mock() receives a non-function argument

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
units/Goccia.Builtins.TestAssertions.pas (2)

1961-1964: 🛠️ Refactor suggestion | 🟠 Major

Centralize the mock-result field names.

These return matchers still hardcode 'type' and 'value'. Please route them through Goccia.Constants.PropertyNames instead of duplicating the public field names here, adding constants if needed.

As per coding guidelines, "Use runtime constants from split constant units instead of hardcoded string literals: Goccia.Constants.PropertyNames (PROP_LENGTH, PROP_NAME, etc.), Goccia.Constants.TypeNames, Goccia.Constants.ErrorNames, Goccia.Constants.ConstructorNames, Goccia.Constants.SymbolNames, and Goccia.Constants."

Also applies to: 2019-2022, 2071-2077, 2133-2139, 2202-2208

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.pas` around lines 1961 - 1964, Replace
hardcoded field names 'type' and 'value' by referencing the central constants in
Goccia.Constants.PropertyNames (e.g. use PropertyNames.PROP_TYPE and
PropertyNames.PROP_VALUE or add those constants if missing) wherever
ResultObj.GetProperty('type') and property accessors compare against
TGocciaStringLiteralValue with 'return'/'value' (symbols to change: ResultObj,
ResultType, TGocciaStringLiteralValue); apply the same replacement to the other
occurrences noted (around the blocks at the other ranges) so all mock-result
field name lookups use Goccia.Constants.PropertyNames instead of string
literals.

3096-3105: ⚠️ Potential issue | 🟠 Major

Fail fast on invalid mock() arguments.

mock(123) and mock(fn, extra) still quietly create a default undefined-return mock here. That hides setup mistakes and weakens the resulting test failures.

🩹 Suggested fix
 function TGocciaTestAssertions.MockFunction(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
 var
   Impl: TGocciaValue;
 begin
-  if (AArgs.Length > 0) and (AArgs.GetElement(0) is TGocciaFunctionBase) then
-    Impl := AArgs.GetElement(0)
-  else
-    Impl := nil;
+  if AArgs.Length > 1 then
+    ThrowError('mock expects at most one implementation argument', 0, 0);
+
+  if AArgs.Length = 1 then
+  begin
+    if not (AArgs.GetElement(0) is TGocciaFunctionBase) then
+      ThrowError('mock expects an optional function implementation', 0, 0);
+    Impl := AArgs.GetElement(0);
+  end
+  else
+    Impl := nil;
 
   Result := TGocciaMockFunctionValue.Create(Impl);
 end;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.pas` around lines 3096 - 3105, The
MockFunction currently accepts any input and silently returns a default mock;
update TGocciaTestAssertions.MockFunction to validate arguments: require exactly
one argument and that AArgs.GetElement(0) is a TGocciaFunctionBase, and if not
raise a clear exception (e.g. EArgumentException or a test-assertion specific
exception) with a message like "mock expects a single function argument"; keep
using TGocciaMockFunctionValue.Create(Impl) when validation passes. This
enforces fail-fast behavior for invalid usages such as mock(123) or mock(fn,
extra).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 2175-2179: The current arity check for toHaveNthReturnedWith only
enforces a minimum (AArgs.Length < 2) allowing extra trailing args; replace this
with an exact-arity validation using TGocciaArgumentValidator from
Goccia.Arguments.Validator.pas to require exactly 2 arguments before proceeding
(instead of the current TGocciaTestAssertions.ThrowError branch), so that calls
like toHaveNthReturnedWith(1,42,extra) are rejected; ensure the validator raises
or triggers the same error flow used elsewhere so the function still returns
TGocciaUndefinedLiteralValue.UndefinedValue on failure.
- Around line 1726-1731: The guard blocks in the matchers
(toHaveBeenCalledTimes, toHaveBeenNthCalledWith, toHaveReturnedTimes,
toHaveNthReturnedWith) currently reject NaN/Infinity/negative/non-integers but
not values > High(Integer), which can cause RRangeError or silent wrap when
assigning Trunc(NumVal.Value) to an Integer; add an upper-bound check
(NumVal.Value > High(Integer)) to those guards before assigning
ExpectedTimes/Index and call TGocciaTestAssertions.ThrowError with an
appropriate message ("toHaveBeenCalledTimes expects a non-negative integer" for
counts, and "toHaveBeenNthCalledWith/toHaveNthReturnedWith expects a positive
integer index" for 1-based indexes) so any out-of-range values are rejected
early.

---

Duplicate comments:
In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 1961-1964: Replace hardcoded field names 'type' and 'value' by
referencing the central constants in Goccia.Constants.PropertyNames (e.g. use
PropertyNames.PROP_TYPE and PropertyNames.PROP_VALUE or add those constants if
missing) wherever ResultObj.GetProperty('type') and property accessors compare
against TGocciaStringLiteralValue with 'return'/'value' (symbols to change:
ResultObj, ResultType, TGocciaStringLiteralValue); apply the same replacement to
the other occurrences noted (around the blocks at the other ranges) so all
mock-result field name lookups use Goccia.Constants.PropertyNames instead of
string literals.
- Around line 3096-3105: The MockFunction currently accepts any input and
silently returns a default mock; update TGocciaTestAssertions.MockFunction to
validate arguments: require exactly one argument and that AArgs.GetElement(0) is
a TGocciaFunctionBase, and if not raise a clear exception (e.g.
EArgumentException or a test-assertion specific exception) with a message like
"mock expects a single function argument"; keep using
TGocciaMockFunctionValue.Create(Impl) when validation passes. This enforces
fail-fast behavior for invalid usages such as mock(123) or mock(fn, extra).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cf6a2088-7db4-4937-b75b-ed39ff6fef67

📥 Commits

Reviewing files that changed from the base of the PR and between a80dbb1 and 784bcb5.

📒 Files selected for processing (2)
  • docs/testing.md
  • units/Goccia.Builtins.TestAssertions.pas

Comment thread units/Goccia.Builtins.TestAssertions.pas
Comment thread units/Goccia.Builtins.TestAssertions.pas Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
units/Goccia.Builtins.TestAssertions.pas (3)

3096-3112: ⚠️ Potential issue | 🟡 Minor

Reject extra mock() arguments instead of dropping them.

Lines 3100-3108 only validate the first argument. mock(fn, extra) currently creates a valid mock and silently discards the trailing argument, which makes mistyped tests harder to diagnose.

🩹 Minimal fix
 begin
+  if AArgs.Length > 1 then
+  begin
+    ThrowError('mock() expects at most one function argument', 0, 0);
+    Exit(TGocciaUndefinedLiteralValue.UndefinedValue);
+  end;
+
   if AArgs.Length > 0 then
   begin
     if not (AArgs.GetElement(0) is TGocciaFunctionBase) then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.pas` around lines 3096 - 3112,
TGocciaTestAssertions.MockFunction currently only validates the first argument
and silently ignores any extras; change the validation to reject calls with more
than one argument by checking AArgs.Length > 1 and calling ThrowError (e.g.,
"mock() expects at most one argument or a function") then Exit with
TGocciaUndefinedLiteralValue.UndefinedValue; keep the existing function-type
check for the first element (AArgs.GetElement(0) is TGocciaFunctionBase) and
only use Impl := AArgs.GetElement(0) when exactly one valid argument is present.

1726-1731: ⚠️ Potential issue | 🟠 Major

Reject oversized matcher numbers before Trunc.

Lines 1726-1731, 1891-1896, and 2008-2013 still allow large finite values through. Trunc(...) returns an Int64; assigning that into these Integer locals can raise ERangeError with range checks enabled or wrap silently without them. Add a NumVal.Value > High(Integer) guard before truncating in each matcher.

Also applies to: 1891-1896, 2008-2013

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.pas` around lines 1726 - 1731, The
matcher currently truncates NumVal.Value into ExpectedTimes (an Integer) without
guarding for oversized finite values, which can raise ERangeError or wrap;
before calling Trunc for the toHaveBeenCalledTimes matcher (where NumVal,
ExpectedTimes and TGocciaTestAssertions.ThrowError are used) add a check that
NumVal.Value <= High(Integer) and if not call ThrowError('toHaveBeenCalledTimes
expects a non-negative integer', 0, 0); apply the same pre-Trunc High(Integer)
guard to the other two matcher sites that use NumVal/ExpectedTimes (the other
occurrences around the same logic) so large finite values are rejected before
Trunc.

2175-2188: ⚠️ Potential issue | 🟠 Major

Tighten toHaveNthReturnedWith argument validation.

Lines 2175-2188 still have two gaps: trailing arguments are silently ignored, and oversized finite indexes still reach Trunc. expect(fn).toHaveNthReturnedWith(1, 42, extra) currently degrades into a weaker assertion, and huge indexes can still overflow the Integer assignment.

🩹 Minimal fix
-  if AArgs.Length < 2 then
+  if AArgs.Length <> 2 then
   begin
     TGocciaTestAssertions(FTestAssertions).ThrowError(
-      'toHaveNthReturnedWith requires at least 2 arguments (call index and expected value)', 0, 0);
+      'toHaveNthReturnedWith requires exactly 2 arguments (call index and expected value)', 0, 0);
     Exit(TGocciaUndefinedLiteralValue.UndefinedValue);
   end;
@@
   NumVal := AArgs.GetElement(0).ToNumberLiteral;
   if NumVal.IsNaN or NumVal.IsInfinity or NumVal.IsNegativeInfinity or
-     (NumVal.Value < 1) or (Frac(NumVal.Value) <> 0) then
+     (NumVal.Value < 1) or (Frac(NumVal.Value) <> 0) or
+     (NumVal.Value > High(Integer)) then
     TGocciaTestAssertions(FTestAssertions).ThrowError(
       'toHaveNthReturnedWith expects a positive integer index', 0, 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.pas` around lines 2175 - 2188, The
current validation for toHaveNthReturnedWith lets extra arguments pass and
allows very large finite indexes to be truncated into N causing overflow; after
the existing checks add a guard that AArgs.Length must be exactly 2 (throw via
TGocciaTestAssertions.ThrowError if >2) so trailing arguments produce an error,
and validate NumVal.Value is <= High(Integer) (and >= 1 is already checked)
before calling N := Trunc(NumVal.Value); use the same ThrowError call for
out‑of‑range indexes so oversized but finite indexes are rejected instead of
being truncated.
units/Goccia.Values.MockFunction.pas (2)

137-147: ⚠️ Potential issue | 🟠 Major

Preserve the original descriptor shape for spies.

Line 146 always installs the spy as a writable/enumerable data property, and Lines 510–512 restore another synthesized data descriptor from FSpyOriginalValue. That still flattens own accessors into plain values and loses the original getter/setter behavior plus descriptor shape. Since you already capture FSpyOriginalDescriptor, reapply that descriptor (or an exact clone of it) instead of rebuilding TGocciaPropertyDescriptorData.

Also applies to: 508-515

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.MockFunction.pas` around lines 137 - 147, The spy
currently replaces methods by always installing a synthesized data descriptor
via ATarget.DefineProperty(..., TGocciaPropertyDescriptorData.Create(...)) which
flattens accessors and loses the original descriptor; instead, when restoring or
installing the spy, preserve and reuse the captured FSpyOriginalDescriptor (or a
deep clone of it) if FSpyWasOwnProperty is true, applying that descriptor back
to ATarget (and when installing the spy keep the original descriptor's
attributes like getter/setter/flags rather than forcing
pfWritable/pfEnumerable/pfConfigurable); update the code paths that set the spy
(near FSpyOriginalDescriptor/FSpyWasOwnProperty and the restore logic around
FSpyOriginalValue) to reapply the original descriptor shape via
ATarget.DefineProperty using the original/cloned descriptor when present.

159-193: 🛠️ Refactor suggestion | 🟠 Major

Move the remaining mock API property names into Goccia.Constants.PropertyNames.

This unit still hardcodes JS-visible property keys like mockImplementation, mockReturnValueOnce, mockReset, and mock even though it already imports the shared property-name constants unit. Centralizing these keeps the mock API, matchers, and builtins from drifting again. As per coding guidelines, use Goccia.Constants.PropertyNames constants (PROP_LENGTH, PROP_NAME, PROP_CONSTRUCTOR, etc.) instead of hardcoded string literals for property names.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Values.MockFunction.pas`:
- Around line 233-240: The code currently conflates constructor-created
instances and plain call receivers by using FInstances for both; create a
separate list field (e.g., FContexts: TList) to track plain call receivers and
leave FInstances to only store objects created by the constructor, update the
code that currently appends AThisValue to FInstances to append to FContexts
instead, ensure constructor path pushes newly constructed objects into
FInstances, and optionally add a BuildContextsArray method mirroring
BuildInstancesArray to expose mock.contexts while leaving BuildInstancesArray
unchanged to return only constructor-created instances.

---

Duplicate comments:
In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 3096-3112: TGocciaTestAssertions.MockFunction currently only
validates the first argument and silently ignores any extras; change the
validation to reject calls with more than one argument by checking AArgs.Length
> 1 and calling ThrowError (e.g., "mock() expects at most one argument or a
function") then Exit with TGocciaUndefinedLiteralValue.UndefinedValue; keep the
existing function-type check for the first element (AArgs.GetElement(0) is
TGocciaFunctionBase) and only use Impl := AArgs.GetElement(0) when exactly one
valid argument is present.
- Around line 1726-1731: The matcher currently truncates NumVal.Value into
ExpectedTimes (an Integer) without guarding for oversized finite values, which
can raise ERangeError or wrap; before calling Trunc for the
toHaveBeenCalledTimes matcher (where NumVal, ExpectedTimes and
TGocciaTestAssertions.ThrowError are used) add a check that NumVal.Value <=
High(Integer) and if not call ThrowError('toHaveBeenCalledTimes expects a
non-negative integer', 0, 0); apply the same pre-Trunc High(Integer) guard to
the other two matcher sites that use NumVal/ExpectedTimes (the other occurrences
around the same logic) so large finite values are rejected before Trunc.
- Around line 2175-2188: The current validation for toHaveNthReturnedWith lets
extra arguments pass and allows very large finite indexes to be truncated into N
causing overflow; after the existing checks add a guard that AArgs.Length must
be exactly 2 (throw via TGocciaTestAssertions.ThrowError if >2) so trailing
arguments produce an error, and validate NumVal.Value is <= High(Integer) (and
>= 1 is already checked) before calling N := Trunc(NumVal.Value); use the same
ThrowError call for out‑of‑range indexes so oversized but finite indexes are
rejected instead of being truncated.

In `@units/Goccia.Values.MockFunction.pas`:
- Around line 137-147: The spy currently replaces methods by always installing a
synthesized data descriptor via ATarget.DefineProperty(...,
TGocciaPropertyDescriptorData.Create(...)) which flattens accessors and loses
the original descriptor; instead, when restoring or installing the spy, preserve
and reuse the captured FSpyOriginalDescriptor (or a deep clone of it) if
FSpyWasOwnProperty is true, applying that descriptor back to ATarget (and when
installing the spy keep the original descriptor's attributes like
getter/setter/flags rather than forcing pfWritable/pfEnumerable/pfConfigurable);
update the code paths that set the spy (near
FSpyOriginalDescriptor/FSpyWasOwnProperty and the restore logic around
FSpyOriginalValue) to reapply the original descriptor shape via
ATarget.DefineProperty using the original/cloned descriptor when present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: edb8d657-4f93-43af-9e3c-57846373e95c

📥 Commits

Reviewing files that changed from the base of the PR and between 784bcb5 and 0a02857.

📒 Files selected for processing (3)
  • units/Goccia.Builtins.TestAssertions.pas
  • units/Goccia.Constants.PropertyNames.pas
  • units/Goccia.Values.MockFunction.pas

Comment thread units/Goccia.Values.MockFunction.pas Outdated
…ntexts

- Add High(Integer) bound check before Trunc in all count/index matchers
  to prevent range-check errors on large values
- Use RequireExactly for toHaveNthReturnedWith instead of minimum arity
- Rename FInstances to FContexts (tracks all-call this values as mock.contexts);
  mock.instances is now an empty array per Vitest/Jest semantics

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
units/Goccia.Builtins.TestAssertions.pas (1)

3092-3108: ⚠️ Potential issue | 🟡 Minor

Still reject trailing mock() arguments.

Line 3096 only validates the first argument. mock(fn, extra) still creates a usable mock, which makes call-site mistakes much harder to spot.

🩹 Proposed fix
 function TGocciaTestAssertions.MockFunction(const AArgs: TGocciaArgumentsCollection; const AThisValue: TGocciaValue): TGocciaValue;
 var
   Impl: TGocciaValue;
 begin
+  if AArgs.Length > 1 then
+  begin
+    ThrowError('mock() expects at most one implementation argument', 0, 0);
+    Exit(TGocciaUndefinedLiteralValue.UndefinedValue);
+  end;
+
   if AArgs.Length > 0 then
   begin
     if not (AArgs.GetElement(0) is TGocciaFunctionBase) then
     begin
       ThrowError('mock() expects a function argument or no arguments', 0, 0);

As per coding guidelines, units/Goccia.Builtins.*.pas: use TGocciaArgumentValidator from Goccia.Arguments.Validator.pas for built-in argument validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Builtins.TestAssertions.pas` around lines 3092 - 3108, The
MockFunction currently only validates the first argument allowing extra trailing
arguments; update TGocciaTestAssertions.MockFunction to use
TGocciaArgumentValidator (from Goccia.Arguments.Validator.pas) to enforce an
exact arity of 0 or 1 and that the single argument, if present, is a
TGocciaFunctionBase, and throw the existing ThrowError when validation fails;
locate the function TGocciaTestAssertions.MockFunction and replace the manual
AArgs.Length check with a TGocciaArgumentValidator call that enforces the
allowed signatures and then proceed to assign Impl and return
TGocciaMockFunctionValue as before.
units/Goccia.Values.MockFunction.pas (2)

138-147: ⚠️ Potential issue | 🔴 Critical

Persist an owned copy of the original descriptor before installing the spy.

GetOwnPropertyDescriptor() returns the descriptor object owned by ATarget, and DefineProperty() frees that object when the spy is installed. That makes FSpyOriginalDescriptor dangling by the time mockRestore() reads Flags. The restore path also rehydrates everything as a data descriptor from FSpyOriginalValue, so own accessors are still restored as plain data properties. Save the descriptor kind and fields (or an owned clone) before redefining the property, and restore that saved descriptor verbatim.

Also applies to: 509-513

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.MockFunction.pas` around lines 138 - 147,
FSpyOriginalDescriptor is assigned the descriptor returned by
ATarget.GetOwnPropertyDescriptor but that object is owned by ATarget and will be
freed when ATarget.DefineProperty is called, leaving FSpyOriginalDescriptor
dangling and losing accessor metadata; modify the spy install path (around
FSpyOriginalDescriptor := ATarget.GetOwnPropertyDescriptor(AMethodName) and
before ATarget.DefineProperty) to create and store an owned copy/clone of the
descriptor (or at minimum copy its Kind, Getter, Setter, Value, and Flags into
fields) so mockRestore can verbatim reapply the original descriptor (including
accessor vs data kind) using those saved fields rather than relying on the
original object or only FSpyOriginalValue.

161-192: 🛠️ Refactor suggestion | 🟠 Major

Use shared property-name constants for the remaining mock API strings.

This unit still hardcodes JS-visible property names like 'mockImplementation', 'mockReset', 'mock', and 'contexts' even though it already imports Goccia.Constants.PropertyNames. Please add constants for the remaining names and reuse them here so the testing surface stays centralized.

As per coding guidelines: Use Goccia.Constants.PropertyNames constants (PROP_LENGTH, PROP_NAME, PROP_CONSTRUCTOR, PROP_PROTOTYPE, etc.) instead of hardcoded string literals for property names.

Also applies to: 253-253

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@units/Goccia.Values.MockFunction.pas` around lines 161 - 192, Replace the
hardcoded JS-visible property name string literals used in the DefineProperty
calls (e.g., 'mockImplementation', 'mockImplementationOnce', 'mockReturnValue',
'mockReturnValueOnce', 'mockReset', 'mockClear', 'mockRestore', 'mockName',
'getMockName', 'mock' and any other remaining literals such as 'contexts') with
the centralized constants from Goccia.Constants.PropertyNames; update the
DefineProperty and DefinePropertyAccessor calls that create
TGocciaPropertyDescriptorData/TGocciaPropertyDescriptorAccessor and
TGocciaNativeFunctionValue (functions DoMockImplementation,
DoMockImplementationOnce, DoMockReturnValue, DoMockReturnValueOnce, DoMockReset,
DoMockClear, DoMockRestore, DoMockName, DoGetMockName, DoGetMockProperty) to use
those PROPERTY_* constants instead of literal strings so the JS-visible names
are reused from PropertyNames.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 3134-3144: ExistingValue may be nil for missing slots, which can
cause the "is not a function" branch to fire instead of the "non-existent
property" error; update the spyOn lookup to detect missing properties before the
non-callable check by testing for a nil/nullable value returned from
Target.GetProperty (e.g., if ExistingValue = nil then ThrowError('spyOn: cannot
spy on non-existent property "' + MethodName + '"', 0, 0) and
Exit(TGocciaUndefinedLiteralValue.UndefinedValue)); keep the existing
TGocciaUndefinedLiteralValue and TGocciaFunctionBase checks intact but ensure
the nil check occurs immediately after retrieving ExistingValue so ThrowError is
raised for truly missing slots.

In `@units/Goccia.Values.MockFunction.pas`:
- Around line 308-319: The current exception handler records
non-TGocciaThrowValue exceptions as TGocciaStringLiteralValue(E.Message), which
loses original JS Error objects; add a specific "on E: TGocciaError do" branch
before the generic "on E: Exception do" in the same try/except blocks that call
FResults.Add(CreateResultEntry(RESULT_TYPE_THROW, ...)) so you record E.Value
(preserving the original error object) for TGocciaError cases; apply this same
change to the other similar handler later in the file to keep
mock.results[*].value as the original error objects.

---

Duplicate comments:
In `@units/Goccia.Builtins.TestAssertions.pas`:
- Around line 3092-3108: The MockFunction currently only validates the first
argument allowing extra trailing arguments; update
TGocciaTestAssertions.MockFunction to use TGocciaArgumentValidator (from
Goccia.Arguments.Validator.pas) to enforce an exact arity of 0 or 1 and that the
single argument, if present, is a TGocciaFunctionBase, and throw the existing
ThrowError when validation fails; locate the function
TGocciaTestAssertions.MockFunction and replace the manual AArgs.Length check
with a TGocciaArgumentValidator call that enforces the allowed signatures and
then proceed to assign Impl and return TGocciaMockFunctionValue as before.

In `@units/Goccia.Values.MockFunction.pas`:
- Around line 138-147: FSpyOriginalDescriptor is assigned the descriptor
returned by ATarget.GetOwnPropertyDescriptor but that object is owned by ATarget
and will be freed when ATarget.DefineProperty is called, leaving
FSpyOriginalDescriptor dangling and losing accessor metadata; modify the spy
install path (around FSpyOriginalDescriptor :=
ATarget.GetOwnPropertyDescriptor(AMethodName) and before ATarget.DefineProperty)
to create and store an owned copy/clone of the descriptor (or at minimum copy
its Kind, Getter, Setter, Value, and Flags into fields) so mockRestore can
verbatim reapply the original descriptor (including accessor vs data kind) using
those saved fields rather than relying on the original object or only
FSpyOriginalValue.
- Around line 161-192: Replace the hardcoded JS-visible property name string
literals used in the DefineProperty calls (e.g., 'mockImplementation',
'mockImplementationOnce', 'mockReturnValue', 'mockReturnValueOnce', 'mockReset',
'mockClear', 'mockRestore', 'mockName', 'getMockName', 'mock' and any other
remaining literals such as 'contexts') with the centralized constants from
Goccia.Constants.PropertyNames; update the DefineProperty and
DefinePropertyAccessor calls that create
TGocciaPropertyDescriptorData/TGocciaPropertyDescriptorAccessor and
TGocciaNativeFunctionValue (functions DoMockImplementation,
DoMockImplementationOnce, DoMockReturnValue, DoMockReturnValueOnce, DoMockReset,
DoMockClear, DoMockRestore, DoMockName, DoGetMockName, DoGetMockProperty) to use
those PROPERTY_* constants instead of literal strings so the JS-visible names
are reused from PropertyNames.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5e03974b-a94c-427e-a062-5302245b3978

📥 Commits

Reviewing files that changed from the base of the PR and between 0a02857 and 31da7c2.

📒 Files selected for processing (3)
  • docs/testing.md
  • units/Goccia.Builtins.TestAssertions.pas
  • units/Goccia.Values.MockFunction.pas

Comment thread units/Goccia.Builtins.TestAssertions.pas Outdated
Comment thread units/Goccia.Values.MockFunction.pas
frostney and others added 2 commits April 8, 2026 13:56
- Add explicit TGocciaError handler in mock Call dispatch to preserve
  the original JS error object via CreateErrorObject instead of degrading
  to E.Message string
- Use HasProperty before GetProperty in spyOn to distinguish missing
  properties from non-callable ones with the correct error message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@frostney frostney merged commit be84303 into main Apr 8, 2026
9 checks passed
@frostney frostney deleted the t3code/mock-and-spyon branch April 8, 2026 13:25
@frostney frostney added the new feature New feature or request label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vitest API Phase 3: Mock infrastructure (vi.fn, vi.spyOn, mock matchers)

1 participant