Skip to content

Component under test not destroyed between tests #398

Closed
@lukaleli

Description

@lukaleli

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 angular@14.0.0 on node@16.15.1. 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!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions