Files
ISA-Frontend/docs/guidelines/testing.md
2025-04-28 15:36:03 +00:00

33 KiB

Testing Guidelines

Table of Contents

  1. Introduction
  2. Core Testing Principles
  3. Testing Tools
  4. Best Practices
  5. Example Test Structures
  6. End-to-End (E2E) Testing Attributes (data-what, data-which)
  7. Running Tests (Nx)
  8. References

Introduction

This document outlines the guidelines and best practices for writing unit tests in this Angular project. Consistent and effective testing ensures code quality, maintainability, and reliability.

Key Requirements:

  • Test files must end with .spec.ts.
  • Use Jest as the primary test runner.
  • Utilize Spectator for testing Components, Directives, and Services.
  • Employ ng-mocks for mocking complex dependencies like child components.

Core Testing Principles

Adhere to these fundamental principles for writing effective unit tests.

Arrange-Act-Assert (AAA) Pattern

Structure your tests using the AAA pattern for clarity and consistency:

  1. Arrange: Set up the test environment. Initialize objects, mock dependencies, and prepare inputs.
  2. Act: Execute the code being tested. Call the function or method under scrutiny.
  3. Assert: Verify the outcome. Check if the actual result matches the expected result.

Isolation: Mocking Dependencies

Unit tests should focus on a single unit of code in isolation.

  • Mock External Dependencies: Services, APIs, or other modules the unit interacts with should be mocked to prevent side effects and external failures from impacting the test. Spectator's mocks array in factory options simplifies this.
  • Mock Child Components: When testing a component, mock its child components using Spectator's overrideComponents to ensure you are only testing the parent component's logic and template, not the children's implementation details.

Error Case Testing

Go beyond the "happy path". Ensure your tests cover:

  • Edge cases
  • Invalid inputs
  • Error scenarios and how the unit handles them

Clarity and Isolation

  • Clear Names: Write descriptive describe and it block names that clearly state what is being tested and the expected outcome.
  • Independent Tests: Ensure tests do not depend on each other. Each test should set up its own state and clean up afterward. Avoid relying on the execution order of tests.

Testing Tools

Jest: The Test Runner

Jest is the testing framework used for running tests, providing features like test discovery, assertion functions (expect), and mocking capabilities (jest.fn(), jest.spyOn()).

Spectator: Simplifying Angular Tests

Spectator significantly reduces boilerplate and simplifies testing Angular components, directives, and services.

Spectator Overview

  • Less Boilerplate: Streamlines TestBed configuration.
  • Easy DOM Querying: Provides intuitive methods (query, queryAll, byText, etc.) to find elements.
  • Clean Event API: Simplifies triggering user interactions (click, typeInElement, keyboard).
  • Custom Matchers: Offers Jest matchers (toExist, toHaveText, toBeDisabled, etc.) for more readable assertions.

Core Factory Methods

Spectator provides factory functions to set up the testing environment:

  • createComponentFactory: For testing components in isolation.
  • createHostFactory: For testing components within a host template (useful for inputs/outputs).
  • createDirectiveFactory: For testing directives.
  • createServiceFactory: For testing services.
  • createHttpFactory: For testing services that interact with HttpClient.

Example Usage (Component):

import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { MyComponent } from './my-component.component';
import { SomeService } from '../some.service';

const createComponent = createComponentFactory({
  component: MyComponent,
  mocks: [SomeService], // Automatically mocks SomeService
  detectChanges: false, // Control initial change detection
});

let spectator: Spectator<MyComponent>;
beforeEach(() => spectator = createComponent());

Querying Elements

Use Spectator's query methods to find elements for interaction or assertion:

// CSS selectors
const button = spectator.query('button.submit');
const inputs = spectator.queryAll('input');
// By component/directive type
const child = spectator.query(ChildComponent);
// By text, label, placeholder, etc.
const submitBtn = spectator.query(byText('Submit'));
const usernameInput = spectator.query(byLabel('Username'));

Working with Inputs/Outputs

Easily set inputs and subscribe to outputs:

// Set single input
spectator.setInput('user', { name: 'Test' });
// Set multiple inputs
spectator.setInput({ user: { name: 'Test' }, isAdmin: true });
// Host component input
// spectator.setHostInput('propName', value); // If using createHostFactory

// Subscribe to output
const outputSpy = jest.fn();
spectator.output('userLoggedIn').subscribe(outputSpy);
spectator.click('button.login');
expect(outputSpy).toHaveBeenCalled();

Event Triggering

Simulate user interactions:

spectator.click('.login-button');
spectator.typeInElement('test user', 'input[name="username"]');
spectator.keyboard.pressEnter('input[name="password"]');
spectator.selectOption(spectator.query('select'), 'Option 2');

Custom Matchers

Improve assertion readability:

expect('.error-message').not.toExist();
expect('h1.title').toHaveText('Welcome');
expect('input.email').toHaveValue('test@example.com');
expect('button.submit').toBeDisabled();
expect('.parent').toHaveDescendant('.child');

ng-mocks: Advanced Mocking

ng-mocks excels at mocking complex Angular artifacts like Components, Directives, Pipes, and Modules, further enhancing test isolation. Note: While ng-mocks offers powerful features like MockComponent and MockDirective, in a standalone component architecture, the primary way to replace dependencies (including child components or directives) is often through Spectator's overrideComponents feature, as shown in the examples below. ng-mocks might still be useful for features like MockProvider or MockInstance.

ng-mocks Overview

  • Deep Mocking: Easily mocks entire modules or specific declarations/providers.
  • Boilerplate Reduction: Simplifies complex TestBed setups.
  • Helper Utilities: Provides functions for interactions (ngMocks.change, ngMocks.click) and querying (ngMocks.find).

Key APIs

  • MockBuilder: Fluent API to configure TestBed, specifying what to keep real and what to mock.
  • MockRender: Enhanced TestBed.createComponent that respects lifecycle hooks and simplifies input/output binding.
  • MockComponent, MockDirective, MockPipe, MockProvider: Functions to create shallow mocks of specific artifacts. (Note: For standalone, prefer overrideComponents for components/directives).
  • MockInstance: Allows customizing mock behavior before initialization (e.g., setting up spies).

When to Use ng-mocks

  • Mocking specific services or pipes using MockProvider when more control is needed than Spectator's mocks array offers.
  • When needing fine-grained control over mock behavior using MockInstance.
  • Testing components that rely heavily on content projection (ng-content).
  • Caution: Avoid MockComponent and MockDirective for standalone components/directives; use Spectator's overrideComponents instead.

Integration with Spectator

Spectator and ng-mocks can work together. Use Spectator for the primary test setup and interaction/assertion. Use Spectator's overrideComponents to replace standalone child components/directives. Use ng-mocks' MockProvider or MockInstance when needed for services or advanced mocking scenarios.

// Example using Spectator's overrideComponents for a standalone child
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ParentComponent } from './parent.component';
import { ChildComponent } from './child.component';
import { MockChildComponent } from './mock-child.component'; // A standalone mock component
import { ComplexService } from './complex.service';
import { MockProvider } from 'ng-mocks'; // ng-mocks can still be used for providers

const createComponent = createComponentFactory({
  component: ParentComponent, // Assume ParentComponent imports ChildComponent
  overrideComponents: [
    [
      ParentComponent, // Target the component whose imports need overriding
      {
        remove: { imports: [ChildComponent] }, // Remove the real standalone component
        add: { imports: [MockChildComponent] }, // Add the mock standalone component
      },
    ],
  ],
  providers: [
    MockProvider(ComplexService, { // Mock service using ng-mocks' MockProvider
      getData: () => of(['mocked data']),
    }),
  ],
});

// ... rest of the test using Spectator API ...

Best Practices

General

  • Focus: Each test (it block) should ideally test one specific behavior or scenario.
  • Readability: Use clear variable names and structure tests logically (AAA).
  • Avoid Logic: Do not put complex logic within tests. Keep them simple and direct.
  • No Implementation Details: Test the public API or observable behavior, not private methods or internal state.

Component Testing

  • Use createComponentFactory for most components. Use createHostFactory only when testing interaction via template bindings (inputs/outputs) is necessary.
  • Test inputs change behavior as expected.
  • Test outputs are emitted correctly upon interaction.
  • Test conditional rendering (@if, @switch) and loops (@for).
  • Verify important DOM elements are rendered correctly based on state.
  • Mock Standalone Child Components/Directives using Spectator's overrideComponents feature to isolate the component under test.

Service Testing

  • Use createServiceFactory.
  • Mock dependencies using the mocks array.
  • Test public methods, ensuring they return expected values or have the correct side effects (like calling mocked dependencies).
  • For services using HttpClient, use createHttpFactory and SpectatorHttp for easy request expectation (spectator.expectOne).

Directive Testing

  • Use createDirectiveFactory.
  • Test how the directive affects the host element or other elements based on inputs or events.
  • If a directive has standalone dependencies, use overrideDirectives (similar to overrideComponents) in the factory options.

Mocking

  • Prefer Spectator's mocks array for simple service mocking.
  • Use spectator.inject(ServiceToMock) to get the mocked instance and configure its methods (e.g., mockService.method.mockReturnValue(...)).
  • Use Spectator's overrideComponents or overrideDirectives to replace standalone component/directive dependencies with mocks.
  • Use MockProvider from ng-mocks if you need more control over service mocking than Spectator's mocks array provides, or for mocking tokens/values.

Async Operations

  • Use async/await with Promises.
  • For RxJS Observables, subscribe within the test or use helpers like waitForAsync or Jest's timer mocks if needed, though often direct subscription is sufficient. Spectator handles basic async operations well.

Performance

  • Mock heavy dependencies (e.g., HTTP calls, complex computations).
  • Keep test setup (beforeEach) lean. Use beforeAll only for setup that is truly read-only and shared across all tests in a describe block.

Debugging

  • Use spectator.debug() to print the current component's DOM structure to the console.
  • Use standard console.log statements temporarily.
  • Utilize your IDE's debugger with breakpoints.

Example Test Structures

(Note: Examples are illustrative and may need adaptation)

Basic Component Test (Spectator)

import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { MyComponent } from './my-component.component';

describe('MyComponent', () => {
  let spectator: Spectator<MyComponent>;
  const createComponent = createComponentFactory(MyComponent);

  beforeEach(() => {
    spectator = createComponent();
  });

  it('should create', () => {
    // Assert
    expect(spectator.component).toBeTruthy();
  });

  it('should display the title correctly', () => {
    // Arrange
    const testTitle = 'Test Title';
    spectator.setInput('title', testTitle); // Assuming an @Input() title
    spectator.detectChanges(); // Trigger change detection

    // Act
    const titleElement = spectator.query('h1');

    // Assert
    expect(titleElement).toHaveText(testTitle);
  });

  it('should emit output event on button click', () => {
    // Arrange
    const outputSpy = jest.fn();
    spectator.output('actionClicked').subscribe(outputSpy); // Assuming an @Output() actionClicked

    // Act
    spectator.click('button.action-button');

    // Assert
    expect(outputSpy).toHaveBeenCalledTimes(1);
    expect(outputSpy).toHaveBeenCalledWith(/* expected payload */);
  });
});

Host Component Test (Spectator + Overrides)

import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { ParentComponent } from './parent.component'; // Assumed standalone, imports ChildComponent
import { ChildComponent } from './child.component'; // Assumed standalone
import { MockChildComponent } from './mock-child.component'; // Assumed standalone mock

describe('ParentComponent in Host with Overrides', () => {
  let spectator: SpectatorHost<ParentComponent>;

  // Override the standalone child component
  const createHost = createHostFactory({
    component: ParentComponent,
    overrideComponents: [
      [
        ParentComponent,
        {
          remove: { imports: [ChildComponent] },
          add: { imports: [MockChildComponent] },
        },
      ],
    ],
    template: `
      <app-parent
        [inputData]="hostInputData"
        (outputEvent)="hostHandleOutput($event)">
      </app-parent>`,
  });

  const mockOutputHandler = jest.fn();

  beforeEach(() => {
    spectator = createHost(undefined, {
      hostProps: {
        hostInputData: { id: 1, value: 'Test Data' },
        hostHandleOutput: mockOutputHandler,
      },
    });
  });

  it('should render the parent component', () => {
    // Assert
    expect(spectator.component).toBeTruthy();
  });

  it('should render the mocked child component', () => {
    // Arrange
    const mockedChild = spectator.query(MockChildComponent); // Query the mocked child

    // Assert
    expect(mockedChild).toBeTruthy();
    // You might check properties/methods on the mock instance if needed
    // expect(mockedChild?.someMockProperty).toBe(...)
  });

  it('should handle output event from the component', () => {
    // Arrange
    const payload = { success: true };
    // Assume ParentComponent emits outputEvent when something happens.
    spectator.component.outputEvent.emit(payload);

    // Assert
    expect(mockOutputHandler).toHaveBeenCalledWith(payload);
  });
});

Service Test (Spectator)

import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { DataService } from './data.service';
import { ApiService } from './api.service';
import { of } from 'rxjs';

describe('DataService', () => {
  let spectator: SpectatorService<DataService>;
  let apiServiceMock: ApiService;

  const createService = createServiceFactory({
    service: DataService,
    mocks: [ApiService], // Mock the dependency
  });

  beforeEach(() => {
    spectator = createService();
    // Get the mocked instance provided by Spectator
    apiServiceMock = spectator.inject(ApiService);
  });

  it('should be created', () => {
    // Assert
    expect(spectator.service).toBeTruthy();
  });

  it('should fetch data using ApiService', (done) => {
    // Arrange
    const mockData = [{ id: 1, name: 'Test Item' }];
    // Configure the mock method
    apiServiceMock.fetchItems.mockReturnValue(of(mockData));

    // Act
    spectator.service.getItems().subscribe(items => {
      // Assert
      expect(items).toEqual(mockData);
      expect(apiServiceMock.fetchItems).toHaveBeenCalledTimes(1);
      done(); // Signal async test completion
    });
  });
});

HTTP Service Test (Spectator)

import { createHttpFactory, HttpMethod, SpectatorHttp } from '@ngneat/spectator/jest';
import { UserHttpService } from './user-http.service';
import { AuthService } from './auth.service'; // Assume this provides a token

describe('UserHttpService', () => {
  let spectator: SpectatorHttp<UserHttpService>;
  let authServiceMock: AuthService;

  const createHttp = createHttpFactory({
    service: UserHttpService,
    mocks: [AuthService], // Mock dependencies
  });

  beforeEach(() => {
    spectator = createHttp();
    authServiceMock = spectator.inject(AuthService);
    // Setup mock return value for token
    authServiceMock.getToken.mockReturnValue('fake-token-123');
  });

  it('should fetch users from the correct endpoint with GET', () => {
    // Arrange
    const mockUsers = [{ id: 1, name: 'User 1' }];

    // Act
    spectator.service.getUsers().subscribe(); // Call the method

    // Assert
    // Expect one request to the URL with the specified method
    const req = spectator.expectOne('/api/users', HttpMethod.GET);
    // Optionally check headers
    expect(req.request.headers.get('Authorization')).toBe('Bearer fake-token-123');
    // Respond to the request to complete the observable
    req.flush(mockUsers);
  });

  it('should send user data to the correct endpoint with POST', () => {
    // Arrange
    const newUser = { name: 'New User', email: 'new@test.com' };
    const createdUser = { id: 2, ...newUser };

    // Act
    spectator.service.createUser(newUser).subscribe();

    // Assert
    const req = spectator.expectOne('/api/users', HttpMethod.POST);
    // Check request body
    expect(req.request.body).toEqual(newUser);
    // Check headers
    expect(req.request.headers.get('Authorization')).toBe('Bearer fake-token-123');
    // Respond
    req.flush(createdUser);
  });
});

Directive Test (Spectator)

import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest';
import { HighlightDirective } from './highlight.directive'; // Assumed standalone
// If HighlightDirective imported another standalone directive/component:
// import { SomeDependencyDirective } from './some-dependency.directive';
// import { MockSomeDependencyDirective } from './mock-some-dependency.directive';

describe('HighlightDirective', () => {
  let spectator: SpectatorDirective<HighlightDirective>;

  const createDirective = createDirectiveFactory({
    directive: HighlightDirective,
    // Example if the directive had standalone dependencies to override:
    // overrideDirectives: [
    //   [
    //     HighlightDirective,
    //     {
    //       remove: { imports: [SomeDependencyDirective] },
    //       add: { imports: [MockSomeDependencyDirective] }
    //     }
    //   ]
    // ],
    template: `<div highlight="yellow" data-testid="highlighter">Test Content</div>`,
  });

  beforeEach(() => spectator = createDirective());

  it('should apply the initial highlight color from input', () => {
    // Assert
    expect(spectator.element).toHaveStyle({ backgroundColor: 'yellow' });
  });

  it('should change style on mouseenter', () => {
    // Act
    spectator.dispatchMouseEvent(spectator.element, 'mouseenter');
    spectator.detectChanges();

    // Assert
    expect(spectator.element).toHaveStyle({
      backgroundColor: 'yellow', // Or expected mouseenter color
      fontWeight: 'bold', // Example style change
    });
  });

  it('should revert style on mouseleave', () => {
    // Arrange
    spectator.dispatchMouseEvent(spectator.element, 'mouseenter');
    spectator.detectChanges();

    // Act
    spectator.dispatchMouseEvent(spectator.element, 'mouseleave');
    spectator.detectChanges();

    // Assert
    expect(spectator.element).toHaveStyle({
      backgroundColor: 'yellow', // Back to initial or default
      fontWeight: 'normal', // Reverted style
    });
  });
});

Standalone Component Test (Spectator)

Testing standalone components is very similar to regular components. Spectator handles the necessary setup.

import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { StandaloneButtonComponent } from './standalone-button.component';
// No need to import CommonModule etc. if they are in the component's imports array

describe('StandaloneButtonComponent', () => {
  let spectator: Spectator<StandaloneButtonComponent>;

  // Factory setup is the same
  const createComponent = createComponentFactory({
    component: StandaloneButtonComponent,
    // No 'imports' needed here if the component imports them itself
    // Mocks can still be provided if needed
    // mocks: [SomeService]
  });

  beforeEach(() => {
    spectator = createComponent();
  });

  it('should create', () => {
    // Assert
    expect(spectator.component).toBeTruthy();
  });

  it('should display label', () => {
    // Arrange
    spectator.setInput('label', 'Click Me');
    spectator.detectChanges();

    // Assert
    expect(spectator.query('button')).toHaveText('Click Me');
  });
});

Deferrable Views Test (Spectator)

Spectator provides helpers to control the state of @defer blocks.

import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { DeferredComponent } from './deferred.component'; // Assume this uses @defer

describe('DeferredComponent', () => {
  let spectator: Spectator<DeferredComponent>;

  const createComponent = createComponentFactory({
    component: DeferredComponent,
  });

  beforeEach(() => {
    spectator = createComponent();
  });

  it('should initially show placeholder content', () => {
    // Act: Render the placeholder state (often the default)
    spectator.deferBlock().renderPlaceholder(); // Or renderIdle()

    // Assert
    expect(spectator.query('.placeholder-content')).toExist();
    expect(spectator.query('.deferred-content')).not.toExist();
    expect(spectator.query('.loading-indicator')).not.toExist();
  });

  it('should show loading state', () => {
    // Act: Render the loading state
    spectator.deferBlock().renderLoading();

    // Assert
    expect(spectator.query('.loading-indicator')).toExist();
    expect(spectator.query('.placeholder-content')).not.toExist();
    expect(spectator.query('.deferred-content')).not.toExist();
  });

  it('should render deferred content when completed', () => {
    // Act: Render the completed state
    spectator.deferBlock().renderComplete();

    // Assert
    expect(spectator.query('.deferred-content')).toExist();
    expect(spectator.query('.placeholder-content')).not.toExist();
    expect(spectator.query('.loading-indicator')).not.toExist();
  });

  it('should handle multiple defer blocks by index', () => {
    // Act: Render the second defer block (index 1)
    spectator.deferBlock(1).renderComplete();

    // Assert
    expect(spectator.query('.second-deferred-content')).toExist();
  });
});

ng-mocks Example

Illustrates using MockBuilder and MockRender. Note: This example uses MockBuilder which might be less common with Spectator and standalone components, but MockInstance and MockProvider remain useful. Prefer Spectator's factories and overrideComponents where possible.

import { MockBuilder, MockInstance, MockRender, ngMocks, MockProvider } from 'ng-mocks';
import { ProfileComponent } from './profile.component'; // Assume standalone
import { AuthService } from './auth.service';
import { ReactiveFormsModule } from '@angular/forms'; // Needed if ProfileComponent imports it

describe('ProfileComponent (with ng-mocks helpers)', () => {
  // Reset customizations after each test
  MockInstance.scope();

  // Configure TestBed using MockBuilder - less common with Spectator, but possible
  beforeEach(() => {
    // Keep ProfileComponent real, mock AuthService, keep ReactiveFormsModule if needed
    return MockBuilder(ProfileComponent)
      .keep(ReactiveFormsModule) // Keep if ProfileComponent imports it
      .provide(MockProvider(AuthService)); // Use MockProvider for the service
  });

  it('should display user email from AuthService', () => {
    // Arrange: Configure the mock instance *before* rendering
    const mockEmail = 'test@example.com';
    // Use MockInstance to configure the globally mocked AuthService
    MockInstance(AuthService, 'getUserEmail', jest.fn().mockReturnValue(mockEmail));

    // Act: Render the component using MockRender
    const fixture = MockRender(ProfileComponent);

    // Assert: Use ngMocks helpers or fixture directly
    const emailDisplay = ngMocks.find(fixture, '.user-email');
    expect(emailDisplay.nativeElement.textContent).toContain(mockEmail);
    // Verify the mock was called (optional)
    expect(MockInstance(AuthService).getUserEmail).toHaveBeenCalled();
  });

  it('should call AuthService.updateProfile on form submit', () => {
    // Arrange
    // Setup spy on the globally mocked AuthService instance
    const updateSpy = MockInstance(AuthService, 'updateProfile', jest.fn());
    const fixture = MockRender(ProfileComponent);

    // Act: Simulate form input and submission
    ngMocks.change(ngMocks.find(fixture, 'input[name="firstName"]'), 'NewName');
    ngMocks.click(ngMocks.find(fixture, 'button[type="submit"]'));

    // Assert
    expect(updateSpy).toHaveBeenCalledWith(expect.objectContaining({ firstName: 'NewName' }));
  });
});

End-to-End (E2E) Testing Attributes (data-what, data-which)

In end-to-end (E2E) testing of web applications, data-what and data-which are custom HTML data attributes used to enhance testability. They help identify elements or components in the DOM for testing purposes, making it easier for testing frameworks (like Selenium, Cypress, or WebdriverIO) to locate and interact with specific UI elements.

Purpose of data-what and data-which

  • Identification: These attributes provide a stable, test-specific way to tag elements, avoiding reliance on brittle selectors like CSS classes, IDs, or XPath, which may change due to styling or structural updates.
  • Semantics: They can describe the purpose (data-what) or specific instance/context (data-which) of an element, improving clarity in test scripts.
  • Maintainability: By using custom attributes, developers can decouple test logic from presentation, reducing the risk of tests breaking when the UI is refactored.

Example Usage

<button data-what="submit-button" data-which="form-login">Login</button>
  • data-what="submit-button": Indicates the element's role or type (e.g., a submit button).
  • data-which="form-login": Specifies the context or instance (e.g., the submit button for the login form).

In a test script (e.g., using WebdriverIO):

// Find the specific button
const loginButton = await $('[data-what="submit-button"][data-which="form-login"]');
await loginButton.click();

This targets the exact button, even if its class or ID changes.

Why Use Them?

  • Robustness: Unlike classes or IDs, which are often tied to styling or JavaScript, data-what and data-which are dedicated to testing, reducing the chance of accidental changes breaking tests.
  • Clarity: They make test scripts self-documenting by describing the element's purpose and context.
  • Scalability: In complex applications with multiple similar elements (e.g., multiple "Save" buttons), data-which helps differentiate them.

Best Practices

  • Use clear, consistent naming conventions (e.g., data-what="button", data-which="checkout-page").

  • Avoid overusing or duplicating attributes unnecessarily.

  • Use additional data-* attributes for dynamic identifiers: When you need to associate specific data like an item ID or name with an element for testing, add separate data-* attributes instead of encoding them into data-which. This keeps selectors clean and allows for more precise targeting.

    Example:

    <oms-feature-return-summary-item
      [returnProcess]="item"
      data-what="list-item"
      data-which="return-process-item"
      [attr.data-item-id]="item.id"
      [attr.data-item-name]="item.name"
    ></oms-feature-return-summary-item>
    

    E2E Test Query (WebdriverIO):

    // Find a specific item by ID
    const specificItem = await $('[data-what="list-item"][data-which="return-process-item"][data-item-id="12345"]');
    await specificItem.waitForExist();
    
    // Find all return process items
    const allItems = await $$('[data-what="list-item"][data-which="return-process-item"]');
    console.log(`Found ${allItems.length} items.`);
    
  • Ensure developers and testers agree on the attribute schema to maintain consistency.

  • Verify in Unit Tests: Ensure these data-* attributes are correctly applied in your component unit tests. Use Spectator's querying capabilities to check for their presence and dynamic values.

    Unit Test Snippet (Spectator):

    it('should apply correct data attributes', () => {
      // Arrange
      const testItem = { id: 'abc', name: 'Test Item' };
      spectator.setInput('returnProcess', testItem);
      spectator.detectChanges();
    
      // Act
      const element = spectator.queryHost('[data-what="list-item"]'); // Or query the specific element inside the component
    
      // Assert
      expect(element).toHaveAttribute('data-what', 'list-item');
      expect(element).toHaveAttribute('data-which', 'return-process-item');
      expect(element).toHaveAttribute('data-item-id', 'abc');
      expect(element).toHaveAttribute('data-item-name', 'Test Item');
    });
    

While data-what and data-which are not standardized HTML attributes, their use as custom data-* attributes aligns with HTML5 specifications, making them valid and widely supported. If you encounter them in a specific framework or codebase, check the project's testing documentation for their exact conventions.


Running Tests (Nx)

Use the Nx CLI to run tests efficiently within the monorepo.

Run Tests for a Specific Project

npx nx test <project-name>
# Example: npx nx test core-logging
# Example: npx nx test isa-app

Run Tests in Watch Mode

For active development, run tests continuously as files change:

npx nx test <project-name> --watch

Run Tests with Coverage Report

Generate a code coverage report:

npx nx test <project-name> --code-coverage

(Coverage reports are typically generated in the coverage/ directory)

Run a Specific Test File

Target a single test file:

npx nx test <project-name> --test-file=libs/core/utils/src/lib/my-util.spec.ts

Run Affected Tests

Optimize CI/CD by running tests only for projects affected by your code changes:

# Run tests for projects affected by changes compared to main branch
npx nx affected:test --base=main --head=HEAD

# Or run tests for uncommitted changes
npx nx affected:test

References