Skip to content

Component under test not destroyed between tests #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
lukaleli opened this issue Jul 5, 2023 · 7 comments · Fixed by #419
Closed

Component under test not destroyed between tests #398

lukaleli opened this issue Jul 5, 2023 · 7 comments · Fixed by #419

Comments

@lukaleli
Copy link

lukaleli commented Jul 5, 2023

Hello,
First of all: great library!

I encountered an issue that I have problem solving and probably missing something in the setup. I searched for the solution in issues, tried different options but without success.
Looks like the component I'm testing is rendered twice. First test case works correctly, but the next one actually finds duplicated elements on the screen and looks like it's not teared down between tests.
Here is my test file:

spec.ts
const renderComponent = async () => {
  const searchStoreMock = provideMockStore(SearchStore, {
    selectors: [
      { selector: selectLoading, value: false },
      { selector: selectSearchHistory, value: [] },
      {
        selector: selectSearchResults,
        value: {
          results: []
        },
      },
      { selector: selectHasSearchResults, value: true }
    ],
  });

  const component = await render(SearchComponent, {
    imports: [
      ReactiveFormsModule,
      provideTranslationsMock(),
      MockComponent(ItemsListComponent),
    ],
    providers: [
      { provide: SearchStore, useValue: searchStoreMock },
      {
        provide: ScrollMemoryService,
        useValue: createMock(ScrollMemoryService),
      },
      { provide: VehicleStatusService, useValue: { online$: of(true) } },
      provideMock(Logger),
      provideMock(UserStore),
      provideMock(PlayerStore),
      provideMock(DetailsStore),
    ],
  });

  const inputControl = screen.getByRole('textbox');
  const searchModeSpy = subscribeSpyTo(
    component.fixture.componentInstance.isSearchMode$
  );

  return {
    component,
    searchStoreMock,
    inputControl,
    searchModeSpy,
  };
};

describe('SearchComponent', () => {
  it('should call search when provided input', async () => {
    // arrange
    const { searchStoreMock, inputControl } = await renderComponent();

    // act
    await userEvent.type(inputControl, 'test');

    // assert
    expect(inputControl).toHaveValue('test');
    await waitFor(() => expect(searchStoreMock.search).toBeCalledWith('test'));
  });

  it('should switch to dark mode when search query provided in input', async () => {
    // arrange
    const { searchStoreMock, inputControl, component, searchModeSpy } =
      await renderComponent();

    // assert
    expect(searchModeSpy.getLastValue()).toBe(false);

    // act
    await userEvent.type(inputControl, 'test');

    // assert
    expect(inputControl).toHaveValue('test');
    await waitFor(() =>
      expect(i18n(T.spotify.tabbar.lib.search)).not.toBeInTheDocument()
    );
  });
});

and it fails at the second case with following error:

jest logs
 FAIL  ***/search/search.component.spec.ts
  ● SearchComponent › should switch to dark mode when search query provided in input

    TestingLibraryElementError: Found multiple elements with the role "textbox"

    Here are the matching elements:

    Ignored nodes: comments, script, style
    <input
      class="input dark ng-untouched ng-valid ng-dirty"
      name="search"
      ng-reflect-form="[object Object]"
      placeholder="Artists, Songs or Podcasts"
      type="text"
    />

    Ignored nodes: comments, script, style
    <input
      class="input dark ng-untouched ng-dirty ng-valid"
      name="search"
      ng-reflect-form="[object Object]"
      placeholder="Artists, Songs or Podcasts"
      type="text"
    />

    (If this is intentional, then use the `*AllBy*` variant of the query (like `queryAllByText`, `getAllByText`, or `findAllByText`)).

    Ignored nodes: comments, script, style
    <body>
      <div
        class="screen-search page-container"
        id="screen-search"
        ng-version="14.0.0"
      >
        <section
          class="page-content"
        >
          <page-title
            class="hidden"
            ng-reflect-hidden="true"
            ng-reflect-title="Search"
          >
            <p
              class="h2 header"
            >
               Search

            </p>
            <div
              class="content"
            >
              <mond-profile
                class="pressable"
              >
                <mond-image
                  class="image error-loading circle"
                  ng-reflect-circle="true"
                  ng-reflect-svg-placeholder="profile"
                  ng-reflect-svg-placeholder-size="full"
                  svgplaceholder="profile"
                  svgplaceholdersize="full"
                >
                  <svg-icon
                    _nghost-a-c117=""
                    aria-hidden="true"
                    class="placeholder svg full"
                    ng-reflect-key="profile"
                    role="img"
                  />
                  <img
                    alt="cover image"
                    class="image"
                    src=""
                  />
                  <spinner
                    class="loader small"
                    ng-reflect-size="small"
                    ng-reflect-visible="false"
                    size="small"
                  />
                </mond-image>
              </mond-profile>
            </div>
          </page-title>
          <div
            class="input-container"
          >
            <div
              class="input-wrapper dark"
            >
              <input
                class="input dark ng-untouched ng-valid ng-dirty"
                name="search"
                ng-reflect-form="[object Object]"
                placeholder="Artists, Songs or Podcasts"
                type="text"
              />
            </div>
            <mond-button
              class="h4 cancel-button h5"
              role="button"
              type="button"
            >
               Cancel
            </mond-button>
          </div>
        </section>
        <section
          class="search-content"
        >
          <section
            class="page-content"
          >
            <div
              class="filters"
            >
              <mond-button
                class="h4 filter-button secondary disabled active"
                ng-reflect-active="true"
                ng-reflect-disabled="true"
                ng-reflect-secondary="true"
                role="button"
                type="button"
              >
                 Top results
              </mond-button>
              <mond-button
                class="h4 filter-button secondary disabled"
                ng-reflect-active="false"
                ng-reflect-disabled="true"
                ng-reflect-secondary="true"
                role="button"
                type="button"
              >
                 Albums
              </mond-button>
              <mond-button
                class="h4 filter-button secondary disabled"
                ng-reflect-active="false"
                ng-reflect-disabled="true"
                ng-reflect-secondary="true"
                role="button"
                type="button"
              >
                 Songs
              </mond-button>
              <mond-button
                class="h4 filter-button secondary disabled"
                ng-reflect-active="false"
                ng-reflect-disabled="true"
                ng-reflect-secondary="true"
                role="button"
                type="button"
              >
                 Playlists
              </mond-button>
              <mond-button
                class="h4 filter-button secondary disabled"
                ng-reflect-active="false"
                ng-reflect-disabled="true"
                ng-reflect-secondary="true"
                role="button"
                type="button"
              >
                 Artists
              </mond-button>
              <mond-button
                class="h4 filter-button secondary disabled"
                ng-reflect-active="false"
                ng-reflect-disabled="true"
                ng-reflect-secondary="true"
                role="button"
                type="button"
              >
                 Podcasts & Shows
              </mond-button>
            </div>
          </section>
          <div
            class="list-container"
          >
            <items-list
              class="list active"
              ng-reflect-items=""
              ng-reflect-loading="false"
              ng-reflect-scroll-memory-tag="search.top"
              ng-reflect-transformer="extended"
              ng-reflect-with-top-transparency="false"
            />
            <items-list
              class="list"
              ng-reflect-items=""
              ng-reflect-loading="false"
              ng-reflect-scroll-memory-tag="search.albums"
          ...

      70 |   });
      71 |
    > 72 |   const inputControl = screen.getByRole('textbox');
         |                               ^
      73 |   const searchModeSpy = subscribeSpyTo(
      74 |     component.fixture.componentInstance.isSearchMode$
      75 |   );

My jest setup:

jest.config.js
module.exports = {
  preset: 'jest-preset-angular',
  testMatch: ['**/(*.)+(spec).+(ts)'],
  setupFilesAfterEnv: [
    'jest-preset-angular/setup-jest',
    '@testing-library/jest-dom',
    '<rootDir>/node_modules/@hirez_io/observer-spy/dist/setup-auto-unsubscribe.js',
  ],
  setupFiles: ['jest-canvas-mock'],
  moduleNameMapper: {
    '^@/fg/(.*)$': '<rootDir>/projects/mond-fg/src/$1',
    '^@/bg/(.*)$': '<rootDir>/projects/mond-bg/src/$1',
    '^@/mond-types$': '<rootDir>/projects/mond-types',
    '^@/tools$': '<rootDir>/projects/tools',
    '^@/app-config$': '<rootDir>/projects/app-config',
    '^i18n/(.*)$': '<rootDir>/projects/i18n/$1',
  },
  collectCoverage: true,
  globalSetup: 'jest-preset-angular/global-setup',
  globals: {
    'ts-jest': {
      tsconfig: '<rootDir>/tsconfig.spec.json',
      stringifyContentPathRegex: '\\.(html|svg)$',
      isolatedModules: true,
    },
  },
  transform: {
    '^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
  },
  coverageDirectory: 'coverage',
  coverageReporters: ['json', 'lcov', 'text', 'clover', 'html'],
  transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
  testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/dist/'],
  snapshotSerializers: [
    'jest-preset-angular/build/serializers/no-ng-attributes',
    'jest-preset-angular/build/serializers/ng-snapshot',
    'jest-preset-angular/build/serializers/html-comment',
  ],
  clearMocks: true,
  restoreMocks: true,
  resetMocks: true,
};

I'm running [email protected] on [email protected]. Dependencies versions:

package.json
{
"@testing-library/angular": "^12.3.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/user-event": "^14.4.3",
"jest": "28.1.3",
"jest-preset-angular": "^12.2.6",
"ng-mocks": "^14.11.0",
"ts-jest": "28.0.5",
}

I honestly have no idea why the mocks are not reset between the tests. I would love to have at least suggestion where the issue may lie. Thanks in advance!

@timdeschryver
Copy link
Member

Hey @lukaleli thanks!
It took me a while to get to this issue, but this behavior isn't what I expect.
The component should be destroyed (and removed from the DOM) between each test.

Did you by any chance already solve this issue, I want to try to reproduce it but it seems like there's a lot going on in the snippets.

@timdeschryver
Copy link
Member

@lukaleli could you help us out by creating a reproduction?
I assumed this was because of ng-mocks, but I can't reproduce this problem (#402).

@lukaleli
Copy link
Author

lukaleli commented Jul 31, 2023

hi @timdeschryver, thanks for your response. I solved that with workaround (not ideal), because nothing else worked. I'm just clearing head and body in html document in jest setup file in beforeEach hook:

document.body.innerHTML = '';
document.head.innerHTML = '';

I just cannot pinpoint the exact problem why does it happen, because I've used your library before without such issue.

@timdeschryver
Copy link
Member

Thanks for the update @lukaleli , as mentioned that's not ideal.
I'll try to investigate this further.

@dzonatan
Copy link
Contributor

dzonatan commented Nov 13, 2023

I've just faced the same issue and the root cause for me is the fact that my component is removing the id attribute from itself, which as I understood, is used to clean up the DOM between tests.

Reproduction repo: https://github.com/dzonatan/testing-library-cleanup-repro
The issue is caused by this line:
https://github.com/dzonatan/testing-library-cleanup-repro/blob/b6e1102776b747a4fdd6834a417d8a63a162f552/src/app/app.component.ts#L8

Maybe OP had something similar in his SearchComponent.

Temporarily it's possible to fix this by wrapping the component, so the root id doesn't get lost:

await render(`<my-comp />`);

but this isn't ideal when component has lots of inputs and you want to test them, it becomes very verbose very quickly.

Any ideas @timdeschryver if we can somehow fix this from the library side?

EDIT: Just found this angular/angular#35215

@timdeschryver
Copy link
Member

Thanks @dzonatan

@dzonatan
Copy link
Contributor

I should be the one thanking for such a quick fix. :D So thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants