Files
bun.sh/test/js/bun/util/wrapAnsi.npm.test.ts
SUZUKI Sosuke 44df912d37 Add Bun.wrapAnsi() for text wrapping with ANSI escape code preservation (#26061)
## Summary

Adds `Bun.wrapAnsi()`, a native implementation of the popular
[wrap-ansi](https://www.npmjs.com/package/wrap-ansi) npm package for
wrapping text with ANSI escape codes.

## API

```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;              // default: false - Break words longer than columns
  wordWrap?: boolean;          // default: true - Wrap at word boundaries
  trim?: boolean;              // default: true - Trim leading/trailing whitespace
  ambiguousIsNarrow?: boolean; // default: true - Treat ambiguous-width chars as narrow
}
```

## Features

- Wraps text to fit within specified column width
- Preserves ANSI escape codes (SGR colors/styles)
- Supports OSC 8 hyperlinks
- Respects Unicode display widths (full-width characters, emoji)
- Normalizes `\r\n` to `\n`

## Implementation Details

The implementation closes and reopens ANSI codes around line breaks for
robust terminal compatibility. This differs slightly from the npm
package in edge cases but produces visually equivalent output.

### Behavioral Differences from npm wrap-ansi

1. **ANSI code preservation**: Bun always maintains complete ANSI escape
sequences. The npm version can output malformed codes (missing ESC
character) in certain edge cases with `wordWrap: false, trim: false`.

2. **Newline ANSI handling**: Bun closes and reopens ANSI codes around
newlines for robustness. The npm version sometimes keeps them spanning
across newlines. The visual output is equivalent.

## Tests

- 27 custom tests covering basic functionality, ANSI codes, Unicode, and
options
- 23 tests ported from the npm package (MIT licensed, credited in file
header)
- All 50 tests pass

## Benchmark

<!-- Benchmark results will be added -->
```
$ cd /Users/sosuke/code/bun/bench && ../build/release/bun snippets/wrap-ansi.js
clk: ~3.82 GHz
cpu: Apple M4 Max
runtime: bun 1.3.7 (arm64-darwin)

benchmark                    avg (min … max) p75   p99    (min … top 1%)
-------------------------------------------- -------------------------------
Short text (45 chars) - npm    25.81 µs/iter  21.71 µs  █
                      (16.79 µs … 447.38 µs) 110.96 µs ▆█▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
Short text (45 chars) - Bun   685.55 ns/iter 667.00 ns    █
                       (459.00 ns … 2.16 ms)   1.42 µs ▁▁▁█▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

summary
  Short text (45 chars) - Bun
   37.65x faster than Short text (45 chars) - npm

-------------------------------------------- -------------------------------
Medium text (810 chars) - npm 568.12 µs/iter 578.00 µs  ▄▅█▆▆▃
                     (525.25 µs … 944.71 µs) 700.75 µs ▄██████▆▅▄▃▃▂▂▂▁▁▁▁▁▁
Medium text (810 chars) - Bun  11.22 µs/iter  11.28 µs                     █
                       (11.04 µs … 11.46 µs)  11.33 µs █▁▁▁██▁█▁▁▁▁█▁█▁▁█▁▁█

summary
  Medium text (810 chars) - Bun
   50.62x faster than Medium text (810 chars) - npm

-------------------------------------------- -------------------------------
Long text (8100 chars) - npm    7.66 ms/iter   7.76 ms     ▂▂▅█   ▅
                         (7.31 ms … 8.10 ms)   8.06 ms ▃▃▄▃█████▇▇███▃▆▆▆▄▁▃
Long text (8100 chars) - Bun  112.14 µs/iter 113.50 µs        █
                     (102.50 µs … 146.04 µs) 124.92 µs ▁▁▁▁▁▁██▇▅█▃▂▂▂▂▁▁▁▁▁

summary
  Long text (8100 chars) - Bun
   68.27x faster than Long text (8100 chars) - npm

-------------------------------------------- -------------------------------
Colored short - npm            28.46 µs/iter  28.56 µs              █
                       (27.90 µs … 29.34 µs)  28.93 µs ▆▁▆▁▁▆▁▁▆▆▆▁▆█▁▁▁▁▁▁▆
Colored short - Bun           861.64 ns/iter 867.54 ns         ▂  ▇█▄▂
                     (839.68 ns … 891.12 ns) 882.04 ns ▃▅▄▅▆▆▇▆██▇████▆▃▅▅▅▂

summary
  Colored short - Bun
   33.03x faster than Colored short - npm

-------------------------------------------- -------------------------------
Colored medium - npm          557.84 µs/iter 562.63 µs      ▂▃█▄
                     (508.08 µs … 911.92 µs) 637.96 µs ▁▁▁▂▄█████▅▂▂▁▁▁▁▁▁▁▁
Colored medium - Bun           14.91 µs/iter  14.94 µs ██  ████ ██ █      ██
                       (14.77 µs … 15.17 µs)  15.06 µs ██▁▁████▁██▁█▁▁▁▁▁▁██

summary
  Colored medium - Bun
   37.41x faster than Colored medium - npm

-------------------------------------------- -------------------------------
Colored long - npm              7.84 ms/iter   7.90 ms       █  ▅
                         (7.53 ms … 8.38 ms)   8.19 ms ▂▂▂▄▃▆██▇██▇▃▂▃▃▃▄▆▂▂
Colored long - Bun            176.73 µs/iter 175.42 µs       █
                       (162.50 µs … 1.37 ms) 204.46 µs ▁▁▂▄▇██▅▂▂▂▁▁▁▁▁▁▁▁▁▁

summary
  Colored long - Bun
   44.37x faster than Colored long - npm

-------------------------------------------- -------------------------------
Hard wrap long - npm            8.05 ms/iter   8.12 ms       ▃ ▇█
                         (7.67 ms … 8.53 ms)   8.50 ms ▄▁▁▁▃▄█████▄▃▂▆▄▃▂▂▂▂
Hard wrap long - Bun          111.85 µs/iter 112.33 µs         ▇█
                     (101.42 µs … 145.42 µs) 123.88 µs ▁▁▁▁▁▁▁████▄▃▂▂▂▁▁▁▁▁

summary
  Hard wrap long - Bun
   72.01x faster than Hard wrap long - npm

-------------------------------------------- -------------------------------
Hard wrap colored - npm         8.82 ms/iter   8.92 ms   ▆ ██
                         (8.55 ms … 9.47 ms)   9.32 ms ▆▆████▆▆▄▆█▄▆▄▄▁▃▁▃▄▃
Hard wrap colored - Bun       174.38 µs/iter 175.54 µs   █ ▂
                     (165.75 µs … 210.25 µs) 199.50 µs ▁▃█▆███▃▂▃▂▂▂▂▂▁▁▁▁▁▁

summary
  Hard wrap colored - Bun
   50.56x faster than Hard wrap colored - npm

-------------------------------------------- -------------------------------
Japanese (full-width) - npm    51.00 µs/iter  52.67 µs    █▂   █▄
                      (40.71 µs … 344.88 µs)  66.13 µs ▁▁▃██▄▃▅██▇▄▃▄▃▂▂▁▁▁▁
Japanese (full-width) - Bun     7.46 µs/iter   7.46 µs       █
                        (6.50 µs … 34.92 µs)   9.38 µs ▁▁▁▁▁██▆▂▁▂▁▁▁▁▁▁▁▁▁▁

summary
  Japanese (full-width) - Bun
   6.84x faster than Japanese (full-width) - npm

-------------------------------------------- -------------------------------
Emoji text - npm              173.63 µs/iter 222.17 µs   █
                     (129.42 µs … 527.25 µs) 249.58 µs ▁▃█▆▃▃▃▁▁▁▁▁▁▁▂▄▆▄▂▂▁
Emoji text - Bun                9.42 µs/iter   9.47 µs           ██
                         (9.32 µs … 9.52 µs)   9.50 µs █▁▁███▁▁█▁██▁▁▁▁██▁▁█

summary
  Emoji text - Bun
   18.44x faster than Emoji text - npm

-------------------------------------------- -------------------------------
Hyperlink (OSC 8) - npm       208.00 µs/iter 254.25 µs   █
                     (169.58 µs … 542.17 µs) 281.00 µs ▁▇█▃▃▂▂▂▁▁▁▁▁▁▁▃▃▅▃▂▁
Hyperlink (OSC 8) - Bun         6.00 µs/iter   6.06 µs      █           ▄
                         (5.88 µs … 6.11 µs)   6.10 µs ▅▅▅▁▅█▅▁▅▁█▁▁▅▅▅▅█▅▁█

summary
  Hyperlink (OSC 8) - Bun
   34.69x faster than Hyperlink (OSC 8) - npm

-------------------------------------------- -------------------------------
No trim long - npm              8.32 ms/iter   8.38 ms  █▇
                        (7.61 ms … 13.67 ms)  11.74 ms ▃████▄▂▃▂▂▃▁▁▁▁▁▁▁▁▁▂
No trim long - Bun             93.92 µs/iter  94.42 µs           █▂
                      (82.75 µs … 162.38 µs) 103.83 µs ▁▁▁▁▁▁▁▁▄███▄▃▂▂▁▁▁▁▁

summary
  No trim long - Bun
   88.62x faster than No trim long - npm
```

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-16 16:12:23 -08:00

256 lines
12 KiB
TypeScript

/**
* Tests ported from wrap-ansi npm package
* https://github.com/chalk/wrap-ansi
*
* MIT License
*
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute,
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
* BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { expect, test } from "bun:test";
// ANSI color helpers (equivalent to chalk with level 1)
const red = (s: string) => `\u001B[31m${s}\u001B[39m`;
const green = (s: string) => `\u001B[32m${s}\u001B[39m`;
const blue = (s: string) => `\u001B[34m${s}\u001B[39m`;
const bgGreen = (s: string) => `\u001B[42m${s}\u001B[49m`;
const bgRed = (s: string) => `\u001B[41m${s}\u001B[49m`;
const black = (s: string) => `\u001B[30m${s}\u001B[39m`;
// Helper functions
const stripAnsi = (s: string) => s.replace(/\u001B\[[0-9;]*m|\u001B\]8;;[^\u0007]*\u0007/g, "");
const hasAnsi = (s: string) => /\u001B\[[0-9;]*m/.test(s);
// Fixtures
const fixture =
"The quick brown " + red("fox jumped over ") + "the lazy " + green("dog and then ran away with the unicorn.");
const fixture2 = "12345678\n901234567890";
const fixture3 = "12345678\n901234567890 12345";
const fixture4 = "12345678\n";
const fixture5 = "12345678\n ";
// When "hard" is false
test("wraps string at 20 characters", () => {
const result = Bun.wrapAnsi(fixture, 20);
expect(result).toBe(
"The quick brown \u001B[31mfox\u001B[39m\n\u001B[31mjumped over \u001B[39mthe lazy\n\u001B[32mdog and then ran\u001B[39m\n\u001B[32maway with the\u001B[39m\n\u001B[32municorn.\u001B[39m",
);
expect(
stripAnsi(result)
.split("\n")
.every(line => line.length <= 20),
).toBe(true);
});
test("wraps string at 30 characters", () => {
const result = Bun.wrapAnsi(fixture, 30);
expect(result).toBe(
"The quick brown \u001B[31mfox jumped\u001B[39m\n\u001B[31mover \u001B[39mthe lazy \u001B[32mdog and then ran\u001B[39m\n\u001B[32maway with the unicorn.\u001B[39m",
);
expect(
stripAnsi(result)
.split("\n")
.every(line => line.length <= 30),
).toBe(true);
});
test('does not break strings longer than "cols" characters', () => {
const result = Bun.wrapAnsi(fixture, 5, { hard: false });
expect(result).toBe(
"The\nquick\nbrown\n\u001B[31mfox\u001B[39m\n\u001B[31mjumped\u001B[39m\n\u001B[31mover\u001B[39m\n\u001B[31m\u001B[39mthe\nlazy\n\u001B[32mdog\u001B[39m\n\u001B[32mand\u001B[39m\n\u001B[32mthen\u001B[39m\n\u001B[32mran\u001B[39m\n\u001B[32maway\u001B[39m\n\u001B[32mwith\u001B[39m\n\u001B[32mthe\u001B[39m\n\u001B[32municorn.\u001B[39m",
);
expect(
stripAnsi(result)
.split("\n")
.some(line => line.length > 5),
).toBe(true);
});
test("handles colored string that wraps on to multiple lines", () => {
const result = Bun.wrapAnsi(green("hello world") + " hey!", 5, { hard: false });
const lines = result.split("\n");
expect(hasAnsi(lines[0])).toBe(true);
expect(hasAnsi(lines[1])).toBe(true);
expect(hasAnsi(lines[2])).toBe(false);
});
test('does not prepend newline if first string is greater than "cols"', () => {
const result = Bun.wrapAnsi(green("hello") + "-world", 5, { hard: false });
expect(result.split("\n").length).toBe(1);
});
// When "hard" is true
test('breaks strings longer than "cols" characters', () => {
const result = Bun.wrapAnsi(fixture, 5, { hard: true });
expect(result).toBe(
"The\nquick\nbrown\n\u001B[31mfox j\u001B[39m\n\u001B[31mumped\u001B[39m\n\u001B[31mover\u001B[39m\n\u001B[31m\u001B[39mthe\nlazy\n\u001B[32mdog\u001B[39m\n\u001B[32mand\u001B[39m\n\u001B[32mthen\u001B[39m\n\u001B[32mran\u001B[39m\n\u001B[32maway\u001B[39m\n\u001B[32mwith\u001B[39m\n\u001B[32mthe\u001B[39m\n\u001B[32munico\u001B[39m\n\u001B[32mrn.\u001B[39m",
);
expect(
stripAnsi(result)
.split("\n")
.every(line => line.length <= 5),
).toBe(true);
});
test("removes last row if it contained only ansi escape codes", () => {
const result = Bun.wrapAnsi(green("helloworld"), 2, { hard: true });
expect(
stripAnsi(result)
.split("\n")
.every(x => x.length === 2),
).toBe(true);
});
test("does not prepend newline if first word is split", () => {
const result = Bun.wrapAnsi(green("hello") + "world", 5, { hard: true });
expect(result.split("\n").length).toBe(2);
});
test("takes into account line returns inside input", () => {
expect(Bun.wrapAnsi(fixture2, 10, { hard: true })).toBe("12345678\n9012345678\n90");
});
test("word wrapping", () => {
expect(Bun.wrapAnsi(fixture3, 15)).toBe("12345678\n901234567890\n12345");
});
test("no word-wrapping", () => {
const result = Bun.wrapAnsi(fixture3, 15, { wordWrap: false });
expect(result).toBe("12345678\n901234567890 12\n345");
const result2 = Bun.wrapAnsi(fixture3, 5, { wordWrap: false });
expect(result2).toBe("12345\n678\n90123\n45678\n90 12\n345");
const result3 = Bun.wrapAnsi(fixture5, 5, { wordWrap: false });
expect(result3).toBe("12345\n678\n");
const result4 = Bun.wrapAnsi(fixture, 5, { wordWrap: false });
expect(result4).toBe(
"The q\nuick\nbrown\n\u001B[31mfox j\u001B[39m\n\u001B[31mumped\u001B[39m\n\u001B[31mover\u001B[39m\n\u001B[31m\u001B[39mthe l\nazy \u001B[32md\u001B[39m\n\u001B[32mog an\u001B[39m\n\u001B[32md the\u001B[39m\n\u001B[32mn ran\u001B[39m\n\u001B[32maway\u001B[39m\n\u001B[32mwith\u001B[39m\n\u001B[32mthe u\u001B[39m\n\u001B[32mnicor\u001B[39m\n\u001B[32mn.\u001B[39m",
);
});
test("no word-wrapping and no trimming", () => {
const result = Bun.wrapAnsi(fixture3, 13, { wordWrap: false, trim: false });
expect(result).toBe("12345678\n901234567890 \n12345");
const result2 = Bun.wrapAnsi(fixture4, 5, { wordWrap: false, trim: false });
expect(result2).toBe("12345\n678\n");
const result3 = Bun.wrapAnsi(fixture5, 5, { wordWrap: false, trim: false });
expect(result3).toBe("12345\n678\n ");
// NOTE: The NPM test expects malformed ANSI codes (e.g., "[31mjumpe[39m" without ESC character)
// when ANSI escape sequences get character-wrapped across lines. Our implementation
// correctly preserves complete ANSI escape sequences. The visual output is equivalent.
const result4 = Bun.wrapAnsi(fixture, 5, { wordWrap: false, trim: false });
expect(result4).toBe(
"The q\nuick \nbrown\n \u001B[31mfox \u001B[39m\n\u001B[31mjumpe\u001B[39m\n\u001B[31md ove\u001B[39m\n\u001B[31mr \u001B[39mthe\n lazy\n \u001B[32mdog \u001B[39m\n\u001B[32mand t\u001B[39m\n\u001B[32mhen r\u001B[39m\n\u001B[32man aw\u001B[39m\n\u001B[32may wi\u001B[39m\n\u001B[32mth th\u001B[39m\n\u001B[32me uni\u001B[39m\n\u001B[32mcorn.\u001B[39m",
);
});
test("supports fullwidth characters", () => {
expect(Bun.wrapAnsi("안녕하세", 4, { hard: true })).toBe("안녕\n하세");
});
test("supports unicode surrogate pairs", () => {
expect(Bun.wrapAnsi("a\uD83C\uDE00bc", 2, { hard: true })).toBe("a\n\uD83C\uDE00\nbc");
expect(Bun.wrapAnsi("a\uD83C\uDE00bc\uD83C\uDE00d\uD83C\uDE00", 2, { hard: true })).toBe(
"a\n\uD83C\uDE00\nbc\n\uD83C\uDE00\nd\n\uD83C\uDE00",
);
});
test("#23, properly wraps whitespace with no trimming", () => {
expect(Bun.wrapAnsi(" ", 2, { trim: false })).toBe(" \n ");
expect(Bun.wrapAnsi(" ", 2, { trim: false, hard: true })).toBe(" \n ");
});
test("#24, trims leading and trailing whitespace only on actual wrapped lines and only with trimming", () => {
expect(Bun.wrapAnsi(" foo bar ", 3)).toBe("foo\nbar");
expect(Bun.wrapAnsi(" foo bar ", 6)).toBe("foo\nbar");
expect(Bun.wrapAnsi(" foo bar ", 42)).toBe("foo bar");
expect(Bun.wrapAnsi(" foo bar ", 42, { trim: false })).toBe(" foo bar ");
});
test("#24, trims leading and trailing whitespace inside a color block only on actual wrapped lines and only with trimming", () => {
// NOTE: Bun's implementation closes and reopens ANSI codes around newlines for robustness.
// The visual output is equivalent: both lines appear in blue.
// NPM: "\u001B[34mfoo\nbar\u001B[39m"
// Bun: "\u001B[34mfoo\u001B[39m\n\u001B[34mbar\u001B[39m"
const result = Bun.wrapAnsi(blue(" foo bar "), 6);
expect(result).toBe("\u001B[34mfoo\u001B[39m\n\u001B[34mbar\u001B[39m");
expect(Bun.wrapAnsi(blue(" foo bar "), 42)).toBe(blue("foo bar"));
expect(Bun.wrapAnsi(blue(" foo bar "), 42, { trim: false })).toBe(blue(" foo bar "));
});
test("#25, properly wraps whitespace between words with no trimming", () => {
expect(Bun.wrapAnsi("foo bar", 3)).toBe("foo\nbar");
expect(Bun.wrapAnsi("foo bar", 3, { hard: true })).toBe("foo\nbar");
expect(Bun.wrapAnsi("foo bar", 3, { trim: false })).toBe("foo\n \nbar");
expect(Bun.wrapAnsi("foo bar", 3, { trim: false, hard: true })).toBe("foo\n \nbar");
});
test("#26, does not multiplicate leading spaces with no trimming", () => {
expect(Bun.wrapAnsi(" a ", 10, { trim: false })).toBe(" a ");
expect(Bun.wrapAnsi(" a ", 10, { trim: false })).toBe(" a ");
});
test("#27, does not remove spaces in line with ansi escapes when no trimming", () => {
expect(Bun.wrapAnsi(bgGreen(` ${black("OK")} `), 100, { trim: false })).toBe(bgGreen(` ${black("OK")} `));
expect(Bun.wrapAnsi(bgGreen(` ${black("OK")} `), 100, { trim: false })).toBe(bgGreen(` ${black("OK")} `));
expect(Bun.wrapAnsi(bgGreen(" hello "), 10, { hard: true, trim: false })).toBe(bgGreen(" hello "));
});
test("#35, wraps hyperlinks, preserving clickability in supporting terminals", () => {
const result1 = Bun.wrapAnsi(
"Check out \u001B]8;;https://www.example.com\u0007my website\u001B]8;;\u0007, it is \u001B]8;;https://www.example.com\u0007supercalifragilisticexpialidocious\u001B]8;;\u0007.",
16,
{ hard: true },
);
expect(result1).toBe(
"Check out \u001B]8;;https://www.example.com\u0007my\u001B]8;;\u0007\n\u001B]8;;https://www.example.com\u0007website\u001B]8;;\u0007, it is\n\u001B]8;;https://www.example.com\u0007supercalifragili\u001B]8;;\u0007\n\u001B]8;;https://www.example.com\u0007sticexpialidocio\u001B]8;;\u0007\n\u001B]8;;https://www.example.com\u0007us\u001B]8;;\u0007.",
);
const result2 = Bun.wrapAnsi(
`Check out \u001B]8;;https://www.example.com\u0007my \uD83C\uDE00 ${bgGreen("website")}\u001B]8;;\u0007, it ${bgRed("is \u001B]8;;https://www.example.com\u0007super\uD83C\uDE00califragilisticexpialidocious\u001B]8;;\u0007")}.`,
16,
{ hard: true },
);
expect(result2).toBe(
"Check out \u001B]8;;https://www.example.com\u0007my 🈀\u001B]8;;\u0007\n\u001B]8;;https://www.example.com\u0007\u001B[42mwebsite\u001B[49m\u001B]8;;\u0007, it \u001B[41mis\u001B[49m\n\u001B[41m\u001B]8;;https://www.example.com\u0007super🈀califragi\u001B]8;;\u0007\u001B[49m\n\u001B[41m\u001B]8;;https://www.example.com\u0007listicexpialidoc\u001B]8;;\u0007\u001B[49m\n\u001B[41m\u001B]8;;https://www.example.com\u0007ious\u001B]8;;\u0007\u001B[49m.",
);
});
test("covers non-SGR/non-hyperlink ansi escapes", () => {
expect(Bun.wrapAnsi("Hello, \u001B[1D World!", 8)).toBe("Hello,\u001B[1D\nWorld!");
expect(Bun.wrapAnsi("Hello, \u001B[1D World!", 8, { trim: false })).toBe("Hello, \u001B[1D \nWorld!");
});
test("#39, normalizes newlines", () => {
expect(Bun.wrapAnsi("foobar\r\nfoobar\r\nfoobar\nfoobar", 3, { hard: true })).toBe(
"foo\nbar\nfoo\nbar\nfoo\nbar\nfoo\nbar",
);
expect(Bun.wrapAnsi("foo bar\r\nfoo bar\r\nfoo bar\nfoo bar", 3)).toBe("foo\nbar\nfoo\nbar\nfoo\nbar\nfoo\nbar");
});