ember-test-helpers icon indicating copy to clipboard operation
ember-test-helpers copied to clipboard

visit() throws TransitionAborted error

Open simonihmig opened this issue 7 years ago • 42 comments

As discussed in Slack, I was unexpectedly seeing failing tests where a transition was aborted. ApplicationInstance#visit is throwing it here

As I said I cannot share the app, but this is the human readable test spec (we have a rather exotic test setup with ember-cli-yadda 😉):

  Scenario: Access restricted page as invited user and login
    Given there exists a user with username "testname" and password "testpw"
    And I am an invited user
    And I am on the homepage
    When I go to the user profile page
    Then I should still be on the homepage
    And I should see the login modal
    When I insert "testname" as the username
    And I insert "testpw" as the password
    And I submit the login form
    Then I should be on the user profile page

The I go to the user profile page step is throwing the exception, which is calling visit('/user/profile') internally.

I quickly assembled this little repro, that resembles what we were doing in a very minimal way: https://github.com/simonihmig/ember-visit-transition-abort

All the acceptance tests are passing, with the exception of this last one: https://github.com/simonihmig/ember-visit-transition-abort/blob/master/tests/acceptance/auth-test.js#L36-L40

simonihmig avatar Feb 28 '18 20:02 simonihmig

@rwjblue did you have a chance to look at this?

I am happy to work on a PR, if we agree what the solution should be. (this is the only remaining issue left for our app's test suite refactoring PR ;))

I am not fully aware of the inner workings, but it puzzles me that clicking on the link-to is different to using visit. (See this vs. that)

simonihmig avatar Mar 07 '18 15:03 simonihmig

@simonihmig — we've been running into this too. Just opened a PR with a failing test and a place to start: https://github.com/emberjs/ember-test-helpers/pull/373

(I'd be curious if it fixes your issue)

spencer516 avatar May 08 '18 16:05 spencer516

☝️ We're having the same issue when migrating to the new testing API.

esbanarango avatar Aug 10 '18 18:08 esbanarango

I have some transitionTos happening in my afterModel hook -- which is intentional -- but in my test visit is raising a TransitionAborted exception.

The only way for my test to pass is:

try {
  await visit(url);
} catch(e) {
  console.error(e);
}

NullVoxPopuli avatar Aug 19 '18 03:08 NullVoxPopuli

I am also experiencing this issue when migrating our test suite (Ember 2.17 & ember-cli-qunit ^4.1.1)

We are using transition.abort and transition.retry to handle route authentication at the route level as suggested on the Preventing and Retrying Transitions guide.

Based on @NullVoxPopuli guidance, our current workaround has been to implement a wrapper of the visit helper:

import { visit } from '@ember/test-helpers';

export default async function visitWithAbortedTransition(url) {

  try {
    await visit(url);
  } catch(error) {
    const { message } = error;
    if (message !== 'TransitionAborted') {
      throw error;
    }
  }

};

ppcano avatar Aug 21 '18 11:08 ppcano

@ppcano that's pretty much what I'm doing now (for re-usability)

import { visit as dangerousVisit } from '@ember/test-helpers';

async function visit(url: string) {
  try {
    await dangerousVisit(url);
  } catch (e) {
    console.error(e);
  }
}

// tests omitted

I like your explicit checking for the TransitionAborted though

NullVoxPopuli avatar Aug 21 '18 12:08 NullVoxPopuli

Also experiencing this when return transitionTo('_routeName_') inside beforeModel() hook. New test suite. Ember 3.4.1

tpetrone avatar Sep 30 '18 19:09 tpetrone

Minor addition to @ppcano's solution as the original settle is interrupted by the error.

import { settled, visit as _visit } from '@ember/test-helpers';

export async function visit(url: string) {
    try {
        await _visit(url);
    } catch (e) {
        if (e.message !== 'TransitionAborted') {
            throw e;
        }
    }

    await settled();
}

chrisseto avatar Oct 15 '18 19:10 chrisseto

Im still having this issue, in my case this happens if i add a async to my beforeModel hook. After that, every test which renders this route throws a TransitionAborted.

velrest avatar May 06 '20 14:05 velrest

What is in your beforeModel? Is it transitioning?

rwjblue avatar May 06 '20 15:05 rwjblue

Im still having this issue, in my case this happens if i add a async to my beforeModel hook. After that, every test which renders this route throws a TransitionAborted.

Make sure you're returning something from an asynchronous beforeModel, not just await.

netes avatar May 06 '20 21:05 netes

what would you return?

I regularly do this:

async beforeModel() {
  if (await some condition not met) {
    return this.transitionTo('do something');
  }
}

NullVoxPopuli avatar May 06 '20 21:05 NullVoxPopuli

Im returning nothing:

  async beforeModel() {
    if (logic) {
      await something;
    }
    if (!logic2) {
      this.replaceWith("/some/url");
    }
  }

Should i put a return before this.replaceWith? If yes, can someone explain to me why?

velrest avatar May 11 '20 09:05 velrest

Because otherwise, the beforeModel promise resolves before the replaceWith has resolved. This is commonly referred to as "floating promises" (@typescript-eslint/no-floating-promises), and will result in the AbortTransition rejection happening after the rest of the transition has moved further forward (and therefore will not be entangled with the router transition promise chain, which means the rejection happens out of band causing the reported error).

rwjblue avatar May 12 '20 20:05 rwjblue

@rwjblue Hmm in my failing test the problem occurs even with a return could there be another problem that im not aware of?

velrest avatar May 14 '20 12:05 velrest

Sadly i cant really debug it since the stack trace is just backburner calls. And stepping through the this.replaceWith was not as helpful as i thought.

velrest avatar May 14 '20 14:05 velrest

@rwjblue We are running into this as well. The difference is that we are actually returning the abort. Any idea why this is still a problem based on the following code?

  async beforeModel () {
    const user = await this.auth.user,
          hints = user.get('shownHints');

    if (!hints || !hints.includes('documents')) {
      return this.transitionTo('account.documents.intro');
    }
  }

Previously user was a preset val, now it's a promise, so we have to await. Hence the reason we are "running into it".

James1x0 avatar May 14 '20 15:05 James1x0

just upgraded from 3.5.1 to 3.18.0 and am seeing this on any routes that we redirect in async beforeModel. similar to others, I'm doing something like this.

async beforeModel(){ let name = await fetch('athing') this.registration.setHostApp(name); return this.replaceWith('intended-route'); }

removing the async will remove the TransitionAborted error in tests.

sethphillips avatar Jun 05 '20 12:06 sethphillips

Someone want to throw together a small repro repo, might be worth stepping through this again 🤔

rwjblue avatar Jun 05 '20 15:06 rwjblue

Thanks for the fast response Robert. https://github.com/sethphillips/transition-aborted-reproduction great route works find and tests pass borked route fails with TransitionAborted

sethphillips avatar Jun 05 '20 19:06 sethphillips

Awesome, I'll try to poke at the repro this week. I remember looking through @simonihmig's initial repro, but things have changed enough upstream that it seems worth digging through again...

rwjblue avatar Jun 07 '20 19:06 rwjblue

Fwiw using redirect() to redirect instead of before/after/model does not cause visit to fail.

patsy-issa avatar Jun 08 '20 12:06 patsy-issa

I have managed to reproduce the issue locally, it can be seen here when the beforeModel hook is an async function.

patsy-issa avatar Jun 08 '20 13:06 patsy-issa

I'm experiencing same issue in our codebase. I've tried to rewrite the beforeModel hook from async to returning simple promise, but it still fails.

  beforeModel() {
    return Promise.resolve()
      .then(() => {
        return this._super(...arguments);
      })
      .then(() => {
        if (this.myService.boolFlag)) {
          return this.transitionTo('other-route');
        }
      });
  }
}

ondrejsevcik avatar Jun 30 '20 11:06 ondrejsevcik

I solved it by adding the error hook and using that to transition instead. Might be sufficient for some people.

akshaisarma avatar Aug 06 '20 19:08 akshaisarma

Just to check in here, I've been trying to track down exactly what is going on and I think I have a fairly good picture. Essentially what is happening is that the secondary transition completes during the beforeModel/model/afterModel of the first one, and since the full secondary transition has completed the router's error handling throws this error instead of entangling the new transition with it like we do when there is an active transition.

Ultimately, returning anything other than the actual resulting Transition object will cause the error reported here to bubble up. This includes making any of the beforeModel/model/afterModel methods themselves async (because then you are returning a Promise not a Transition), or returning an RSVP.Promise that later resolves to a Transition (e.g. return this.store.findRecord('post', 1).then((record) => if (record.whatever) { return this.transitionTo(...); }).

I know this is what folks were reporting all along, but now I actually understand it 😩. Sorry for taking so long to dig in here. I'm trying to work up a solution using @sethphillips's reproduction.

rwjblue avatar Sep 09 '20 14:09 rwjblue

@rwjblue Do you know when this will be fixed? Approximately? Because no workaround is working for me..

SebastianPetric avatar Oct 16 '20 04:10 SebastianPetric

We're also banging our heads against this issue.

FWIW: The route seems to be loaded as expected after a non-async-visit()-then-timeout. However any subsequent assert() never fires:

visit(url);
await new Promise(resolve => setTimeout(resolve, 1000));    
console.log(currentURL() == url); // true
assert.equal(currentURL(), url); // never asserted, however the UI is fully loaded and interactive at pauseTest()

This issue makes acceptance testing unusable for us for the time being. I'd love to find a workaround, or even better be pointed towards fixing any possible bad-practice that is causing this. Or trust @rwjblue to find a patch/solution, of course.

Thanks!

johanrd avatar Oct 22 '20 13:10 johanrd

@SebastianPetric I was able to workaround this issue by wrapping my visit calls in a try-catch block for tests that specifically intend to execute the aborted transition behavior. It's not ideal, but it did temporarily unblock us.

elwayman02 avatar Oct 29 '20 00:10 elwayman02

do we have a path forward? or a list of what this is waiting on?

NullVoxPopuli avatar Jan 19 '21 20:01 NullVoxPopuli