reshaped icon indicating copy to clipboard operation
reshaped copied to clipboard

Tests on PinField sometimes fail (source code)

Open vladoyoung opened this issue 1 year ago • 8 comments

Describe the bug Probably about 50% of the times, the tests from yarn test:unit fail for the PinField component. The errors have always been the same and have been occurring for the last several minor updates on the Reshaped source. Basically since the PinField was implemented.

Expected behavior The tests to stop failing.

Screenshots Consecutive screenshots as I can't fit it all in one screenshot. Pasted Graphic Pasted Graphic 1 Pasted Graphic 2

vladoyoung avatar Aug 12 '24 14:08 vladoyoung

Can you provide some additional information about the environment? Like the installed package versions in case they were changed, node version, etc? I'm trying to reproduce this error but it still passed every single time for me

blvdmitry avatar Aug 12 '24 15:08 blvdmitry

Apart from the package version change from this issue, I've not changed any other package versions.

Node version: v22.5.1 Yarn version: 1.22.22

I've managed to get to the same error in about 5 test runs. So I can't find a pattern yet.

I've switched my node version to v20.16.0 and it failed on the second try, although I don't think it's from the node version.

vladoyoung avatar Aug 13 '24 08:08 vladoyoung

Let's try debugging this remotely if you're ok with that since I still can't reproduce it locally. Does replacing the test case with the following change anything for you?

test("handles keyboard navigation", async () => {
		render(
			<Reshaped>
				<PinField name={fixtures.name} />
			</Reshaped>
		);

		const elInput = screen.getByRole<HTMLInputElement>("textbox");

		await act(() => {
			elInput.focus();
		});

		expect(elInput.selectionStart).toEqual(0);

		await userEvent.keyboard("1");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(1);
		});

		await userEvent.keyboard("234");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(3);
			expect(elInput.selectionEnd).toEqual(4);
		});

		await userEvent.keyboard(
			"{ArrowLeft}{/ArrowLeft}{ArrowLeft}{/ArrowLeft}{ArrowLeft}{/ArrowLeft}"
		);

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(0);
			expect(elInput.selectionEnd).toEqual(1);
		});

		await userEvent.keyboard("{ArrowRight}{/ArrowRight}{ArrowRight}{/ArrowRight}");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(2);
			expect(elInput.selectionEnd).toEqual(3);
		});

		expect(elInput).toHaveValue("1234");
		await userEvent.keyboard("{backspace}{/backspace}");
		expect(elInput).toHaveValue("124");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(2);
			expect(elInput.selectionEnd).toEqual(3);
		});

		// Switched to type mode
		await userEvent.keyboard("{ArrowRight}{/ArrowRight}");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(3);
		});

		// Can't move further
		await userEvent.keyboard("{ArrowRight}{/ArrowRight}");
		expect(elInput.selectionStart).toEqual(3);
	});

blvdmitry avatar Aug 13 '24 21:08 blvdmitry

Unfortunately, it still fails. This time I kept track of my tries (out of 10): it failed on the first, third, fifth, sixth.

Summary of all failing tests
 FAIL  src/components/PinField/tests/PinField.test.tsx (26.09 s)
  ● Components/PinField › handles keyboard navigation

    expect(received).toEqual(expected) // deep equality

    Expected: 2
    Received: 1

    Ignored nodes: comments, script, style
    <html
      data-rs-color-mode="light"
      data-rs-keyboard="true"
      data-rs-theme="reshaped"
      dir="ltr"
    >
      <head />
      <body>
        <div>
          <div
            class=" root  root"
          >
            <div
              class=" root  root  --position-relative --flex --direction-row --nowrap-false"
              style="--rs-view-gap-s: 2;"
            >
              <div
                class=" root  item  root --radius-small  --position-relative  unit  unit  root --border-neutral --bg-elevation-base --flex --direction-column --align-center --justify-center --nowrap-false"
                style="--rs-w-s: 9; --rs-h-s: 9;"
              >
                <div
                  class=" root --variant-body-3"
                >
                  1
                </div>
              </div>
              <div
                class=" root  item item--focused  root --radius-small  --position-relative  unit  unit  root --border-neutral --bg-elevation-base --flex --direction-column --align-center --justify-center --nowrap-false"
                style="--rs-w-s: 9; --rs-h-s: 9;"
              >
                <div
                  class=" root --variant-body-3"
                >
                  2
                </div>
              </div>
              <div
                class=" root  item  root --radius-small  --position-relative  unit  unit  root --border-neutral --bg-elevation-base --flex --direction-column --align-center --justify-center --nowrap-false"
                style="--rs-w-s: 9; --rs-h-s: 9;"
              >
                <div
                  class=" root --variant-body-3"
                >
                  3
                </div>
              </div>
              <div
                class=" root  item  root --radius-small  --position-relative  unit  unit  root --border-neutral --bg-elevation-base --flex --direction-column --align-center --justify-center --nowrap-false"
                style="--rs-w-s: 9; --rs-h-s: 9;"
              >
                <div
                  class=" root --variant-body-3"
                >
                  4
                </div>
              </div>
              <input
                autocomplete="one-time-code"
                class="input"
                inputmode="numeric"
                maxlength="4"
                name="test-name"
                pattern="\\d{4}"
                type="text"
                value="1234"
              />
            </div>
          </div>
        </div>
      </body>
    </html>

      149 |
      150 | 		await waitFor(() => {
    > 151 | 			expect(elInput.selectionStart).toEqual(2);
          | 			                               ^
      152 | 			expect(elInput.selectionEnd).toEqual(3);
      153 | 		});
      154 |

      at src/components/PinField/tests/PinField.test.tsx:151:35
      at runWithExpensiveErrorDiagnosticsDisabled (node_modules/@testing-library/dom/dist/config.js:47:12)
      at checkCallback (node_modules/@testing-library/dom/dist/wait-for.js:124:77)
      at checkRealTimersCallback (node_modules/@testing-library/dom/dist/wait-for.js:118:16)
      at Timeout.task [as _onTimeout] (node_modules/jsdom/lib/jsdom/browser/Window.js:520:19)


Test Suites: 1 failed, 78 passed, 79 total
Tests:       1 failed, 1 todo, 334 passed, 336 total
Snapshots:   72 passed, 72 total
Time:        45.489 s

I also forgot to initially mention that these fails are present on Github actions too, so it's not limited to local. Here is my pull-request.yml action:

name: "pull-request"
on:
  pull_request:
    branches:
      - main

jobs:
  test-pull-request:
    permissions: write-all
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: "20.x"
      - run: yarn
      - run: yarn lint
      - run: yarn build
      - run: yarn test:unit
      - uses: andresz1/[email protected]
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          build_script: build

By the way, ChatGPT is suggesting that we try a timeout on the waitFor on the tests like so:

await userEvent.keyboard("{ArrowRight}{/ArrowRight}");
await waitFor(() => {
    expect(elInput.selectionStart).toEqual(3);
}, { timeout: 5000 }); // Adjust timeout if necessary

vladoyoung avatar Aug 14 '24 07:08 vladoyoung

Interesting, does it help if you try to add some artificial timeout? Having it failing in github actions too makes me quite confused 😅

blvdmitry avatar Aug 14 '24 08:08 blvdmitry

I've tried that, global async timeout and a lot of other approaches. I can't manage to get it fixed!

Strangely through, running an isolated test like yarn test:unit src/components/PinField/tests/PinField.test.tsx never fails. Seems like it's failing only when running alongside the other tests.

vladoyoung avatar Aug 15 '24 10:08 vladoyoung

I got it reproduced once in my CI yesterday so hopefully it keeps failing today 😅

blvdmitry avatar Aug 15 '24 11:08 blvdmitry

Another attempt, where I try to wait for every single keypress to resolve before moving on. I'm not 100% sure this is the issue but at least it stopped reproducing locally, can you check this one?

test("handles keyboard navigation", async () => {
		render(
			<Reshaped>
				<PinField name={fixtures.name} />
			</Reshaped>
		);

		const elInput = screen.getByRole<HTMLInputElement>("textbox");

		await act(() => {
			elInput.focus();
		});

		expect(elInput.selectionStart).toEqual(0);
		expect(elInput.selectionEnd).toEqual(0);

		await userEvent.keyboard("1");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(1);
			expect(elInput.selectionEnd).toEqual(1);
		});

		await userEvent.keyboard("234");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(3);
			expect(elInput.selectionEnd).toEqual(4);
		});

		// Move back to the first character
		await userEvent.keyboard("{ArrowLeft}");
		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(2);
			expect(elInput.selectionEnd).toEqual(3);
		});
		await userEvent.keyboard("{ArrowLeft}");
		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(1);
			expect(elInput.selectionEnd).toEqual(2);
		});
		await userEvent.keyboard("{ArrowLeft}");
		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(0);
			expect(elInput.selectionEnd).toEqual(1);
		});

		// Move to the third character
		await userEvent.keyboard("{ArrowRight}");
		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(1);
			expect(elInput.selectionEnd).toEqual(2);
		});
		await userEvent.keyboard("{ArrowRight}");
		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(2);
			expect(elInput.selectionEnd).toEqual(3);
		});

		expect(elInput).toHaveValue("1234");
		await userEvent.keyboard("{backspace}");
		expect(elInput).toHaveValue("124");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(2);
			expect(elInput.selectionEnd).toEqual(3);
		});

		// Switched to type mode
		await userEvent.keyboard("{ArrowRight}");

		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(3);
			expect(elInput.selectionStart).toEqual(3);
		});

		// Can't move further
		await userEvent.keyboard("{ArrowRight}");
		await waitFor(() => {
			expect(elInput.selectionStart).toEqual(3);
			expect(elInput.selectionStart).toEqual(3);
		});
	});

blvdmitry avatar Aug 15 '24 18:08 blvdmitry

I've ran it ~20 times and none of them failed! Nice one, that's the solution!

You've probably seen this, but just dropping this warning during the tests:

 RUNS  src/components/Calendar/tests/Calendar.test.tsx
  ● Console

    console.error
      Warning: `ReactDOMTestUtils.act` is deprecated in favor of `React.act`. Import `act` from `react` instead of `react-dom/test-utils`. See https://react.dev/warnings/react-dom-test-utils for more info.
          at Component (/Users/vladoyoung/Downloads/design-system/src/hooks/tests/useRTL.test.tsx:8:30)
          at ToastProvider (/Users/vladoyoung/Downloads/design-system/src/components/Toast/ToastProvider.tsx:63:10)
          at SingletonHotkeysProvider (/Users/vladoyoung/Downloads/design-system/src/hooks/_private/useSingletonHotkeys.tsx:157:10)
          at ReshapedInner (/Users/vladoyoung/Downloads/design-system/src/components/Reshaped/Reshaped.tsx:17:10)
          at div
          at Theme (/Users/vladoyoung/Downloads/design-system/src/components/Theme/Theme.tsx:12:10)
          at GlobalColorMode (/Users/vladoyoung/Downloads/design-system/src/components/Theme/GlobalColorMode.tsx:10:10)
          at Reshaped (/Users/vladoyoung/Downloads/design-system/src/components/Reshaped/Reshaped.tsx:32:10)

       9 |
      10 | 	React.useEffect(() => {
    > 11 | 		act(() => setRTL(true));
         | 		   ^
      12 | 	}, [setRTL]);
      13 |
      14 | 	return <div>{rtl}</div>;

      at printWarning (node_modules/react-dom/cjs/react-dom-test-utils.development.js:71:30)
      at error (node_modules/react-dom/cjs/react-dom-test-utils.development.js:45:7)
      at actWithWarning (node_modules/react-dom/cjs/react-dom-test-utils.development.js:1736:7)
      at src/hooks/tests/useRTL.test.tsx:11:6
      at commitHookEffectListMount (node_modules/react-dom/cjs/react-dom.development.js:23189:26)
      at commitPassiveMountOnFiber (node_modules/react-dom/cjs/react-dom.development.js:24970:11)
      at commitPassiveMountEffects_complete (node_modules/react-dom/cjs/react-dom.development.js:24930:9)
      at commitPassiveMountEffects_begin (node_modules/react-dom/cjs/react-dom.development.js:24917:7)
      at commitPassiveMountEffects (node_modules/react-dom/cjs/react-dom.development.js:24905:3)
      at flushPassiveEffectsImpl (node_modules/react-dom/cjs/react-dom.development.js:27078:3)
      at flushPassiveEffects (node_modules/react-dom/cjs/react-dom.development.js:27023:14)
      at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:26115:3)
      at flushSyncCallbacks (node_modules/react-dom/cjs/react-dom.development.js:12042:22)
      at commitRootImpl (node_modules/react-dom/cjs/react-dom.development.js:26998:3)
      at commitRoot (node_modules/react-dom/cjs/react-dom.development.js:26721:5)
      at finishConcurrentRender (node_modules/react-dom/cjs/react-dom.development.js:26020:9)
      at performConcurrentWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:25848:7)
      at flushActQueue (node_modules/react/cjs/react.development.js:2667:24)
      at act (node_modules/react/cjs/react.development.js:2582:11)
      at node_modules/@testing-library/react/dist/act-compat.js:47:25
      at renderRoot (node_modules/@testing-library/react/dist/pure.js:180:26)
      at render (node_modules/@testing-library/react/dist/pure.js:271:10)
      at Object.<anonymous> (src/hooks/tests/useRTL.test.tsx:19:9)

vladoyoung avatar Aug 16 '24 08:08 vladoyoung

Yep, I've also fixed this one along the way 😅 Thanks for checking it out

blvdmitry avatar Aug 16 '24 08:08 blvdmitry