Skip to content

feat(router): add navigate method to RouterTestingHarness - #71379

Open
brysonbw wants to merge 2 commits into
angular:mainfrom
brysonbw:feat/router-testing-harness-navigate-method-71210
Open

brysonbw wants to merge 2 commits into
angular:mainfrom
brysonbw:feat/router-testing-harness-navigate-method-71210

Conversation

@brysonbw

@brysonbw brysonbw commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Mirrors Router.navigate so tests can pass queryParams, fragment and other NavigationExtras without an extra navigation.

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Documentation content changes
  • angular.dev application / infrastructure changes
  • Other... Please describe:

What is the current behavior?

RouterTestingHarness only exposes navigateByUrl, which accepts a string. To set query params or a fragment, tests must build the URL string by hand or inject Router and perform an extra navigation

Issue Number: #71210

What is the new behavior?

Adds RouterTestingHarness.navigate(commands, extras?, requiredRoutedComponentType?), mirroring Router.navigate. It accepts NavigationExtras (e.g. queryParams, fragment), waits for the navigation and any redirects to complete, and returns the activated component. Passing a component type enforces it, as with navigateByUrl

Does this PR introduce a breaking change?

  • Yes
  • No

Other information

The new private helper runNavigation consolidates the logic shared by navigateByUrl and navigate: waiting for the navigation and redirects, running change detection, and resolving or type-checking the activated component. The two public methods only supply the router call

Existing navigateByUrl usages in the repo's tests are intentionally left unchanged

Mirrors `Router.navigate` so tests can pass `queryParams`, `fragment` and other
`NavigationExtras` without an extra navigation.
Updating RouterTestingHarness documentation to include `navigate` method
@pullapprove
pullapprove Bot requested review from MarkTechson and crisbeto October 15, 2026 12:42
@angular-robot angular-robot Bot added detected: feature PR contains a feature commit area: router labels Oct 15, 2026
@ngbot ngbot Bot added this to the Backlog milestone Oct 15, 2026
@JeanMeche
JeanMeche requested review from JeanMeche and atscott and removed request for MarkTechson and crisbeto October 15, 2026 16:01
extras: NavigationExtras | undefined,
requiredRoutedComponentType: Type<T>,
): Promise<T>;
async navigate<T>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toughts on going this route (pun intended) ?

async navigate(commands: readonly any[], extras?: NavigationExtras): Promise<null | {}>;
async navigate<T>(commands: readonly any[], requiredRoutedComponentType: Type<T>): Promise<T>;
async navigate<T>(
  commands: readonly any[],
  extras: NavigationExtras | undefined,
  requiredRoutedComponentType: Type<T>,
): Promise<T>;
async navigate<T>(
  commands: readonly any[],
  extrasOrComponent?: NavigationExtras | Type<T>,
  requiredRoutedComponentType?: Type<T>,
): Promise<T | null> {
  let extras: NavigationExtras | undefined;
  let componentType: Type<T> | undefined;
  if (typeof extrasOrComponent === 'function') {
    componentType = extrasOrComponent;
  } else {
    extras = extrasOrComponent;
    componentType = requiredRoutedComponentType;
  }
  return this.runNavigation(
    (router) => router.navigate(commands, extras),
    componentType,
  );
}

Comment on lines +294 to +303
it('should read search term and fragment passed as navigation extras', async () => {
component = await harness.navigate(
['/search'],
{queryParams: {q: 'angular'}, fragment: 'results'},
Search,
);

expect(component.searchTerm()).toBe('angular');
expect(TestBed.inject(Router).url).toBe('/search?q=angular#results');
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search component does not consume the fragment. This isn't a great illustration. Consider updating the component to actually consume this.route.fragment

@@ -83,6 +83,7 @@ it('should get the activated component directly', async () => {
### Best Practices

- **Navigate with the Harness:** Always use `harness.navigateByUrl()` to simulate navigation. This method returns a promise that resolves with the instance of the activated component.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to amend this.

Suggested change
- **Navigate with the Harness:** Always use `harness.navigateByUrl()` to simulate navigation. This method returns a promise that resolves with the instance of the activated component.
- **Navigate with the Harness:** Always use `harness.navigateByUrl()` or `harness.navigate()` to simulate navigation. This method returns a promise that resolves with the instance of the activated component.

@JeanMeche JeanMeche left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENT: Thanks for working on this! I've left a couple of inline notes on test coverage and runNavigation behavior for your consideration.

expect(harness.routeNativeElement?.innerHTML).toContain('hello world');
});

it('navigates with commands, query params and fragment', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENT: Consider expanding test coverage for navigate to cover additional cases:

  • Navigating with commands only (no extras or component type): await harness.navigate(['/a'])
  • Navigating with commands and component type (no extras): await harness.navigate(['/a'], TestCmp)
  • Navigating with extras but no component type: await harness.navigate(['/a'], { queryParams: { q: '1' } })
  • Guard rejection / cancellation: Verifying behavior when a guard cancels the navigation (return false) or redirects (return UrlTree)
  • Mismatched component type: Verifying that navigate throws the expected error when the activated component doesn't match requiredRoutedComponentType
  • Relative navigation: Verifying that { relativeTo: route } behaves as expected with the harness

* @returns The activated component instance, or `null` if the outlet was not activated.
*/
private async runNavigation<T>(
startNavigation: (router: Router) => Promise<unknown>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENT: In runNavigation, if a test performs a successful initial navigation (activating ComponentA) followed by a subsequent navigation that is rejected by a guard (returning false):

  • outlet.isActivated remains true from the previous navigation.
  • If called without requiredRoutedComponentType, this returns the stale previous component (ComponentA) rather than null.
  • If called with requiredRoutedComponentType, it throws an unexpected component error rather than indicating navigation did not activate any component.

While this behavior existed in navigateByUrl, consolidating into runNavigation reinforces it. Could we check whether startNavigation(router) succeeded, or is keeping parity with navigateByUrl intentional here?

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

Labels

area: router detected: feature PR contains a feature commit

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants