Repository: jestjs/jest (TypeScript)

Issue:
Wrong interpolation in `test.each` when the value of array contains multiple `%`
## 🐛 Bug Report

When the value of array contains multiple `%`, wrong interpolation may occur.

## To Reproduce

index.text.js

```javascript
describe("test name should to be `%d %d`", () => {
  test.each([["%d %d", 1, 2, 3]])("%s", (_, a, b, expected) => {
    expect(a + b).toBe(expected);
  });
});

describe("test name should to be `%%d`", () => {
  test.each([["%%d", 1, 2, 3]])("%s", (_, a, b, expected) => {
    expect(a + b).toBe(expected);
  });
});
```

Run `npx jest`, then you can see the following:

```console
$ npx jest
 PASS  ./index.test.js
  test name should to be `%d %d`
    ✓ %d 1 (2 ms)
  test name should to be `%%d`
    ✓ %1

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.324 s, estimated 1 s
Ran all test suites.
```

## Expected behavior

```console
$ npx jest
 PASS  ./index.test.js
  test name should to be `%d %d`
    ✓ %d %d (2 ms)
  test name should to be `%%d`
    ✓ %%d

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.324 s, estimated 1 s
Ran all test suites.
```

## Link to repl or repo (highly encouraged)

## envinfo

<!--
Run npx envinfo --preset jest
Paste the results here:
-->

```
  System:
    OS: Linux 4.19 Debian GNU/Linux 10 (buster) 10 (buster)
    CPU: (8) x64 Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz
  Binaries:
    Node: 14.16.1 - ~/.nvm/versions/node/v14.16.1/bin/node
    Yarn: 1.22.10 - ~/.nvm/versions/node/v14.16.1/bin/yarn
    npm: 7.11.1 - ~/.nvm/versions/node/v14.16.1/bin/npm
  npmPackages:
    jest: ^26.6.3 => 26.6.3
```



Hints:

Patch:
diff --git a/packages/jest-each/src/table/array.ts b/packages/jest-each/src/table/array.ts
--- a/packages/jest-each/src/table/array.ts
+++ b/packages/jest-each/src/table/array.ts
@@ -11,10 +11,11 @@ import type {Global} from '@jest/types';
 import {format as pretty} from 'pretty-format';
 import type {EachTests} from '../bind';
 
-const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp%]/g;
+const SUPPORTED_PLACEHOLDERS = /%[sdifjoOp]/g;
 const PRETTY_PLACEHOLDER = '%p';
 const INDEX_PLACEHOLDER = '%#';
 const PLACEHOLDER_PREFIX = '%';
+const ESCAPED_PLACEHOLDER_PREFIX = /%%/g;
 const JEST_EACH_PLACEHOLDER_ESCAPE = '@@__JEST_EACH_PLACEHOLDER_ESCAPE__@@';
 
 export default (title: string, arrayTable: Global.ArrayTable): EachTests =>
@@ -46,17 +47,23 @@ const formatTitle = (
         return interpolatePrettyPlaceholder(formattedTitle, normalisedValue);
 
       return util.format(formattedTitle, normalisedValue);
-    }, interpolateTitleIndex(title, rowIndex))
+    }, interpolateTitleIndex(interpolateEscapedPlaceholders(title), rowIndex))
     .replace(new RegExp(JEST_EACH_PLACEHOLDER_ESCAPE, 'g'), PLACEHOLDER_PREFIX);
 
 const normalisePlaceholderValue = (value: unknown) =>
-  typeof value === 'string' && SUPPORTED_PLACEHOLDERS.test(value)
-    ? value.replace(PLACEHOLDER_PREFIX, JEST_EACH_PLACEHOLDER_ESCAPE)
+  typeof value === 'string'
+    ? value.replace(
+        new RegExp(PLACEHOLDER_PREFIX, 'g'),
+        JEST_EACH_PLACEHOLDER_ESCAPE,
+      )
     : value;
 
 const getMatchingPlaceholders = (title: string) =>
   title.match(SUPPORTED_PLACEHOLDERS) || [];
 
+const interpolateEscapedPlaceholders = (title: string) =>
+  title.replace(ESCAPED_PLACEHOLDER_PREFIX, JEST_EACH_PLACEHOLDER_ESCAPE);
+
 const interpolateTitleIndex = (title: string, index: number) =>
   title.replace(INDEX_PLACEHOLDER, index.toString());
 


Test patch:
diff --git a/packages/jest-each/src/__tests__/array.test.ts b/packages/jest-each/src/__tests__/array.test.ts
--- a/packages/jest-each/src/__tests__/array.test.ts
+++ b/packages/jest-each/src/__tests__/array.test.ts
@@ -143,19 +143,22 @@ describe('jest-each', () => {
           ],
         ]);
         const testFunction = get(eachObject, keyPath);
-        testFunction('expected string: %s %d %s %s %d %j %s %j %d %d %#', noop);
+        testFunction(
+          'expected string: %% %%s %s %d %s %s %d %j %s %j %d %d %#',
+          noop,
+        );
 
         const globalMock = get(globalTestMocks, keyPath);
         expect(globalMock).toHaveBeenCalledTimes(2);
         expect(globalMock).toHaveBeenCalledWith(
-          `expected string: hello 1 null undefined 1.2 ${JSON.stringify({
+          `expected string: % %s hello 1 null undefined 1.2 ${JSON.stringify({
             foo: 'bar',
           })} () => {} [] Infinity NaN 0`,
           expectFunction,
           undefined,
         );
         expect(globalMock).toHaveBeenCalledWith(
-          `expected string: world 1 null undefined 1.2 ${JSON.stringify({
+          `expected string: % %s world 1 null undefined 1.2 ${JSON.stringify({
             baz: 'qux',
           })} () => {} [] Infinity NaN 1`,
           expectFunction,
@@ -400,12 +403,13 @@ describe('jest-each', () => {
         const eachObject = each.withGlobal(globalTestMocks)([
           ['hello', '%d', 10, '%s', {foo: 'bar'}],
           ['world', '%i', 1991, '%p', {foo: 'bar'}],
+          ['joe', '%d %d', 10, '%%s', {foo: 'bar'}],
         ]);
         const testFunction = get(eachObject, keyPath);
         testFunction('expected string: %s %s %d %s %p', () => {});
 
         const globalMock = get(globalTestMocks, keyPath);
-        expect(globalMock).toHaveBeenCalledTimes(2);
+        expect(globalMock).toHaveBeenCalledTimes(3);
         expect(globalMock).toHaveBeenCalledWith(
           'expected string: hello %d 10 %s {"foo": "bar"}',
           expectFunction,
@@ -416,6 +420,11 @@ describe('jest-each', () => {
           expectFunction,
           undefined,
         );
+        expect(globalMock).toHaveBeenCalledWith(
+          'expected string: joe %d %d 10 %%s {"foo": "bar"}',
+          expectFunction,
+          undefined,
+        );
       });
     });
   });