diff --git a/addon/components/link/component.ts b/addon/components/link/component.ts index 2a896879..a1ef857b 100644 --- a/addon/components/link/component.ts +++ b/addon/components/link/component.ts @@ -1,3 +1,4 @@ +import { assert } from '@ember/debug'; import { action } from '@ember/object'; import { reads } from '@ember/object/computed'; import Transition from '@ember/routing/-private/transition'; @@ -57,6 +58,17 @@ export default class LinkComponent extends Component { @service private router!: RouterService; + /** + * Whether the router has been initialized. This will be false in render + * tests. + * + * @see https://github.com/buschtoens/ember-link/issues/126 + */ + private get isRouterInitialized() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Boolean((this.router as any)._router._routerMicrolib); + } + @reads('router.currentURL') private currentURL!: string; @@ -97,6 +109,7 @@ export default class LinkComponent extends Component { * attribute. */ get href(): string { + if (!this.isRouterInitialized) return ''; return this.router.urlFor(...this.routeArgs); } @@ -105,6 +118,7 @@ export default class LinkComponent extends Component { * models and query params. */ get isActive(): boolean { + if (!this.isRouterInitialized) return false; this.currentURL; // eslint-disable-line no-unused-expressions return this.router.isActive(...this.routeArgs); } @@ -114,6 +128,7 @@ export default class LinkComponent extends Component { * models, but ignoring query params. */ get isActiveWithoutQueryParams() { + if (!this.isRouterInitialized) return false; this.currentURL; // eslint-disable-line no-unused-expressions return this.router.isActive( this.args.route, @@ -130,6 +145,7 @@ export default class LinkComponent extends Component { * params. */ get isActiveWithoutModels() { + if (!this.isRouterInitialized) return false; this.currentURL; // eslint-disable-line no-unused-expressions return this.router.isActive(this.args.route); } @@ -139,8 +155,15 @@ export default class LinkComponent extends Component { */ @action transitionTo(event?: Event | unknown): Transition { + // Intentionally putting this *before* the assertion to prevent navigating + // away in case of a failed assertion. this.preventDefault(event); + assert( + 'You can only call `transitionTo`, when the router is initialized, e.g. when using `setupApplicationTest`.', + this.isRouterInitialized + ); + return this.router.transitionTo(...this.routeArgs); } @@ -150,8 +173,15 @@ export default class LinkComponent extends Component { */ @action replaceWith(event?: Event | unknown): Transition { + // Intentionally putting this *before* the assertion to prevent navigating + // away in case of a failed assertion. this.preventDefault(event); + assert( + 'You can only call `replaceWith`, when the router is initialized, e.g. when using `setupApplicationTest`.', + this.isRouterInitialized + ); + return this.router.replaceWith(...this.routeArgs); } diff --git a/tests/helpers/wait-for-error.ts b/tests/helpers/wait-for-error.ts new file mode 100644 index 00000000..b2074848 --- /dev/null +++ b/tests/helpers/wait-for-error.ts @@ -0,0 +1,47 @@ +import { waitUntil } from '@ember/test-helpers'; + +import Ember from 'ember'; + +import { on, off } from 'rsvp'; + +/** + * I would be using `ember-qunit-assert-helpers` here, but it does not work with + * async rendering. :( + * + * Adapted from https://github.com/workmanw/ember-qunit-assert-helpers/issues/18#issuecomment-390003905 + */ +export default async function waitForError( + callback: () => Promise, + options?: Parameters[1] +) { + const originalEmberListener = Ember.onerror; + const originalWindowListener = window.onerror; + + let error: Error | undefined; + Ember.onerror = uncaughtError => { + error = uncaughtError; + }; + window.onerror = ( + _message, + _source, + _lineNumber, + _columnNumber, + uncaughtError + ) => { + error = uncaughtError; + }; + on('error', Ember.onerror); + + await Promise.all([ + waitUntil(() => error, options).finally(() => { + Ember.onerror = originalEmberListener; + window.onerror = originalWindowListener; + off('error', Ember.onerror); + }), + callback() + ]); + + if (!error) throw new Error('No Error was thrown.'); + + return error; +} diff --git a/tests/integration/components/link-test.ts b/tests/integration/components/link-test.ts new file mode 100644 index 00000000..01e54d46 --- /dev/null +++ b/tests/integration/components/link-test.ts @@ -0,0 +1,52 @@ +import { render, click } from '@ember/test-helpers'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +import hbs from 'htmlbars-inline-precompile'; + +import waitForError from 'dummy/tests/helpers/wait-for-error'; + +module('Integration | Component | link', function(hooks) { + setupRenderingTest(hooks); + + // Regression for: https://github.com/buschtoens/ember-link/issues/126 + test('it renders', async function(assert) { + await render(hbs` + + + Link + + + `); + + assert.dom('[data-test-link]').hasAttribute('href', ''); + assert.dom('[data-test-link]').hasNoClass('is-active'); + }); + + test('triggering a transition has no effect', async function(assert) { + await render(hbs` + + + Link + + + `); + + const error = await waitForError(() => click('[data-test-link]')); + assert.ok(error instanceof Error); + assert.strictEqual( + error.message, + 'Assertion Failed: You can only call `transitionTo`, when the router is initialized, e.g. when using `setupApplicationTest`.' + ); + }); +});