From 0bd73b436373fe1b623a73f374c5259c86c9d3c8 Mon Sep 17 00:00:00 2001 From: pfg Date: Fri, 25 Jul 2025 22:22:04 -0700 Subject: [PATCH] Fix toIncludeRepeated (#21366) Fixes #12276: toIncludeRepeated should check for the exact repeat count not >= This is a breaking change because some people may be relying on the existing behaviour. Should it be feature-flagged for 1.3? --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- src/bun.js/test/expect.zig | 6 ++--- test/js/bun/test/jest-extended.test.js | 6 ++--- test/regression/issue/issue-12276.test.ts | 27 +++++++++++++++++++++++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 test/regression/issue/issue-12276.test.ts diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 7fdbaa504e..735e42f2c4 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -3841,10 +3841,8 @@ pub const Expect = struct { return globalThis.throw("toIncludeRepeated() requires the first argument to be a non-empty string", .{}); } - if (countAsNum == 0) - pass = !strings.contains(expectStringAsStr, subStringAsStr) - else - pass = std.mem.containsAtLeast(u8, expectStringAsStr, countAsNum, subStringAsStr); + const actual_count = std.mem.count(u8, expectStringAsStr, subStringAsStr); + pass = actual_count == countAsNum; if (not) pass = !pass; if (pass) return .js_undefined; diff --git a/test/js/bun/test/jest-extended.test.js b/test/js/bun/test/jest-extended.test.js index 7602122aaa..4cf2a29e7a 100644 --- a/test/js/bun/test/jest-extended.test.js +++ b/test/js/bun/test/jest-extended.test.js @@ -601,13 +601,13 @@ describe("jest-extended", () => { expect("abc").not.toIncludeRepeated("d", 1); // Any other number - expect("abc abc abc").toIncludeRepeated("abc", 1); - expect("abc abc abc").toIncludeRepeated("abc", 2); + expect("abc abc abc").not.toIncludeRepeated("abc", 1); + expect("abc abc abc").not.toIncludeRepeated("abc", 2); expect("abc abc abc").toIncludeRepeated("abc", 3); expect("abc abc abc").not.toIncludeRepeated("abc", 4); // Emojis/Unicode - expect("😘πŸ₯³πŸ˜€πŸ˜˜πŸ₯³").toIncludeRepeated("😘", 1); + expect("😘πŸ₯³πŸ˜€πŸ˜˜πŸ₯³").toIncludeRepeated("😘", 2); expect("😘πŸ₯³πŸ˜€πŸ˜˜πŸ₯³").toIncludeRepeated("πŸ₯³", 2); expect("😘πŸ₯³πŸ˜€πŸ˜˜πŸ₯³").not.toIncludeRepeated("😘", 3); expect("😘πŸ₯³πŸ˜€πŸ˜˜πŸ₯³").not.toIncludeRepeated("πŸ˜Άβ€πŸŒ«οΈ", 1); diff --git a/test/regression/issue/issue-12276.test.ts b/test/regression/issue/issue-12276.test.ts new file mode 100644 index 0000000000..781a255383 --- /dev/null +++ b/test/regression/issue/issue-12276.test.ts @@ -0,0 +1,27 @@ +import { expect, test } from "bun:test"; + +// https://github.com/oven-sh/bun/issues/12276 +test("toIncludeRepeated should check for exact count, not at least count", () => { + // The bug: toIncludeRepeated was checking if string contains AT LEAST n occurrences + // Instead of EXACTLY n occurrences + + // These should pass - exact match + expect("hello hello world").toIncludeRepeated("hello", 2); + expect("hello world").toIncludeRepeated("hello", 1); + expect("world").toIncludeRepeated("hello", 0); + + // These should pass - not exact match with .not + expect("hello hello world").not.toIncludeRepeated("hello", 1); + expect("hello hello world").not.toIncludeRepeated("hello", 3); + expect("hello hello hello").not.toIncludeRepeated("hello", 2); + + // Additional test cases + expect("abc abc abc").toIncludeRepeated("abc", 3); + expect("abc abc abc").not.toIncludeRepeated("abc", 1); + expect("abc abc abc").not.toIncludeRepeated("abc", 2); + expect("abc abc abc").not.toIncludeRepeated("abc", 4); + + // Edge cases - std.mem.count doesn't count overlapping occurrences + expect("aaa").toIncludeRepeated("aa", 1); // "aa" appears once (non-overlapping) + expect("aaaa").toIncludeRepeated("aa", 2); // "aa" appears twice (non-overlapping) +});