admin管理员组

文章数量:1279057

I've been trying to render certain template related to a state and ponent according to this article

In my project running under dev-server it all works fine and when I execute $state.go("home") the ponent template is loaded how I expect but when I do this in a testing environment, this doesn't work.

Before, in testing, when I use the "old way" using "template" instead "ponent" with ui-router, execute $rootScope.$digest() was enough for add the template inside the <div ui-view></div> but using this new way this doesn't work anymore.

What am I doing wrong?

Edit: I've been trying to deeply understand the problem and I see that the problem is related to the HTTP request that was done. Maybe it's related to the way that my promise resolves on the resolve callback using async/await. Please check the Service:

Service

export class TodoService {
    constructor($http, BASE_URL) {
        this.http = $http;
        this.url = `${BASE_URL}/todos`
    }
    async getTodos() {
        const apiResponse = await this.http.get(this.url)
        return apiResponse.data.todos
    }
}

I've been trying to render certain template related to a state and ponent according to this article

In my project running under dev-server it all works fine and when I execute $state.go("home") the ponent template is loaded how I expect but when I do this in a testing environment, this doesn't work.

Before, in testing, when I use the "old way" using "template" instead "ponent" with ui-router, execute $rootScope.$digest() was enough for add the template inside the <div ui-view></div> but using this new way this doesn't work anymore.

What am I doing wrong?

Edit: I've been trying to deeply understand the problem and I see that the problem is related to the HTTP request that was done. Maybe it's related to the way that my promise resolves on the resolve callback using async/await. Please check the Service:

Service

export class TodoService {
    constructor($http, BASE_URL) {
        this.http = $http;
        this.url = `${BASE_URL}/todos`
    }
    async getTodos() {
        const apiResponse = await this.http.get(this.url)
        return apiResponse.data.todos
    }
}

Router

import '@uirouter/angularjs'

export function routes($stateProvider, $locationProvider) {
    $locationProvider.html5Mode({
        enabled: true,
        requireBase: false,
        rewriteLinks: true,
    })

    $stateProvider
        .state("home", {
            url: "/",
            ponent: "todoList",
            resolve: {
                todosList: TodoService => TodoService.getTodos()
            }
        })
}

Test

import { routes } from "routes"
import { TodoListComponent } from "ponents/todoList.ponent"
import { TodoService } from "services/todo.service"

describe("TodoListComponent rendering and interaction on '/' base path", () => {
    let ponentDOMelement
    let stateService

    beforeAll(() => {
        angular
            .module("Test", [
                "ui.router"
            ])
            .config(routes)
            .constant("BASE_URL", "http://localhost:5000/api")
            .ponent("todoList", TodoListComponent)
            .service("TodoService", TodoService)
            //I enable this for better logs about the problem
            .run(['$rootScope','$trace', function($rootScope, $trace) {
               $trace.enable("TRANSITION")
             }])
    })
    beforeEach(angular.mock.module("Test"))

    beforeEach(inject(($rootScope, $pile, $state, $httpBackend) => {
        //build the scene
        //1st render the root element of scene: We needs a router view for load the base path
        let scope = $rootScope.$new()
        ponentDOMelement = angular.element("<div ui-view></div>")

        $pile(ponentDOMelement)(scope)
        scope.$digest()
        
         document.body.appendChild(ponentDOMelement[0]) //This is a hack for jsdom before the $rootScope.$digest() call
        //2nd let's create a fake server for intercept the http requests and fake the responses
        const todosResponse = require(`${__dirname}/../../stubs/todos_get.json`)
        $httpBackend
            .whenGET(/.+\/todos/)
            .respond((method, url, data, headers, params) => {
                return [200, todosResponse]
            })

        //3rd Let's generate the basic scenario: Go at home state ("/" path)
        $state.go("home")
        $rootScope.$digest()
        $httpBackend.flush()
    }))

    it("Should be render a list", () => {
        console.log("HTML rendered")
        console.log(document.querySelectorAll("html")[0].outerHTML)
    })
})

The HTML result that not rendering

<html>
<head>
<style type="text/css">
@charset "UTF-8";[ng\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate) {
  display:none !important;
}
ng\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{
  position:absolute;
}
</style>
</head>
<body><!-- uiView: -->
</body>
</html>

Also, I traced the stateChange before the HTML:

console.log node_modules/@uirouter/core/_bundles/ui-router-core.js:1276
    Transition #0-0: Started  -> "Transition#0( ''{} -> 'home'{} )"

console.log node_modules/@uirouter/core/_bundles/ui-router-core.js:1282
    Transition #1-0: Ignored  <> "Transition#1( ''{} -> 'home'{} )"

console.log node_modules/@uirouter/core/_bundles/ui-router-core.js:1313
    Transition #1-0: <- Rejected "Transition#1( ''{} -> 'home'{} )", reason: Transition Rejection($id: 0 type: 5, message: The transition was ignored, detail: "undefined")

I see a problem in a transition but no reason was given.

========================================================================

Edit 2 Finally we found the problem but I can't figure out the real problem. I created a branch in my project for showing the problem. This it's related to async/await javascript feature:

export class TodoService {
    constructor($http, BASE_URL) {
        this.http = $http;
        this.url = `${BASE_URL}/todos`
    }
    //Interchange the ment on the getTodos method and run `npm run tdd` for see the problem:
    //When async/await doesn't used, the html associated to the resolve in the
    // "/" route that used this service, the promise was resolved that expected.
    //The idea for this branch it's research about the problem and propose a way
    //for we can use async/await on the production code and on the testing environment
    async getTodos() {
        const apiResponse = await this.http.get(this.url)
        return apiResponse.data.todos
    }
    // getTodos() {
    //     return this.http.get(this.url).then(res => res.data.todos)
    // }
}

The repository

So my new Questions are:

  • Why the way that I use the async/await functionality it's not patible with the ui-router resolve on testing environment but in production code it works?
  • Maybe it's related to $httpBackend.flush() call?

Edit 3 The issue 3522 reported in angular UI router repository

Share Improve this question edited Sep 26, 2017 at 17:52 kauffmanes 51810 silver badges29 bronze badges asked Aug 24, 2017 at 13:54 Gonzalo Pincheira ArancibiaGonzalo Pincheira Arancibia 3,6232 gold badges25 silver badges43 bronze badges 9
  • 9 Sounds like the problem is that async/await uses native promises but your ponent needs an Angular promise (like the ones returned by $http) – Bergi Commented Aug 26, 2017 at 17:00
  • I may just guess. I will just tell what I know. Async/await feature is just an experimental feature and supported in very limited amount of runtimes. Transplitter uses state machine to emulate async/await. This state machine creates simple promises instead of extended once created by $q service. Try to pare actual code which runs on different environments. – Eduard Lepner Commented Aug 26, 2017 at 19:25
  • I have noticed in your repository that you don't use babel-plugin-transform-async-to-generator. What will happen if you plug it in? – Eduard Lepner Commented Aug 26, 2017 at 19:45
  • @EduardLepner Using the "transform-regenerator" babel plugin and "stage-3" it's enough for my production code. I've been trying add babel-plugin-transform-async-to-generator but still doesn't work. About your another response, if the promise created for the runtime it's not the promise expected by $q service, why this work if i run the code using my npm start script and see this work on the browser? Maybe could be a problem related to the $httpBackend mock service because this not work only in the test environment? – Gonzalo Pincheira Arancibia Commented Aug 26, 2017 at 20:43
  • I have pulled your project and run all tests. It works on my machine. – Eduard Lepner Commented Aug 27, 2017 at 9:13
 |  Show 4 more ments

2 Answers 2

Reset to default 1

The issue is that angular expects an angular Promise that's why your then will work but your await won't, you can solve this by using a library like: https://www.npmjs./package/angular-async-await or make a construction like they're demonstrating here https://medium./@alSkachkov/using-async-await-function-in-angular-1-5-babel-6-387f7c43948c

Good luck with your problem!

This is just an educated guess based on my understanding of the way resolve-ers work and what ngMock does. In your first example, your resolve for getTodos does not return until the $http promise has resolved, at which point you pluck the value off the response and return it. However, resolve-ers expect a $q.Promise value as a sentinel to hold the rendering of the router until it resolves. In your code, depending on how it's transpiled, the await and return call likely doesn't produce the correct sentinel value, so it's treated like a synchronous response.

One way to test would be to request the resolve-er in the controller for your todolist ponent and inspect the value. I'll bet it's NOT a $q.Promise, although it might be a native Promise.

When using resolve, though, just chain the Promise by adding a then and return it. The router will handle the rest.

Or better, switch to Observables! (/me ducks ining tomatoes)

本文标签: