Sencha Touch and node.js: Part 2 – Different views based on profiles and orientation, fat models and tiny controllers

Sencha Touch and node.js: Part 2 – Different views based on profiles and orientation, fat models and tiny controllers

In previous post I described how to start developing with Sencha Touch, in this post however I will show you how to create basic component like profiles, models, stores, views and controllers, of course firstly writing a test suit for each of them (with small exception). I will also cover how to mock Ext.Direct request, which will be the most tricky task.

1. Prepare profiles

When developing on mobile one of goal achievement is to provide proper look for all type of device. It’s clear that on tablets you have more space and you can show more information. On phone, in the other hand, your views should be as simple as possible to be sure that are readable enough. Sencha Touch is of course so complex framework that provide it’s own solution for this problem. To control look and behaviors on different devices we will use class called Profile. For our needs we will create two types of it, for phone and tablets.
In directory \app\profile add files Phone.js and Tablet.js. In the first one add:

Ext.define('TutsApp.profile.Phone', {
    extend: 'Ext.app.Profile',
    requires: [
    ],
    config: {
        name: 'Phone',
        views: ['Layout']
    },
    isActive: function () {
        return Ext.os.is('Phone');
    }
});

and in the second:

Ext.define('TutsApp.profile.Tablet', {
    extend: 'Ext.app.Profile',
    requires: [
    ],
    config: {
        name: 'Tablet',
        views: ['Layout']
    },
    isActive: function () {
        return Ext.os.is('Tablet');
    }
});

Classes are quite simple, they have two abstract method which you can override. First one, isActive, helps application to recognize what profile should be used. Second, launch, works exactly in the same way as launch method in Ext.application but is called before it. In config property we can set name of the profile and dependencies which should be loaded when specific profile is active. In our example we add only view, but of course we are able to add controllers and stores too. However dependencies declared in app.js will be loaded always, no matter on what device you are.
OK, we assumed that our layout view will be different for phone and different for tablet, so delete script \app\views\Main.js and add file Layout.js to \app\views\phone and to \public\app\views\tablet. Classes which will be dedicated for specific profile will be located under catalog with profile name.
Fill both of layout files with this code

Ext.define('TutsApp.view.phone.Layout', {
    extend: 'Ext.Container',
    xtype: 'phoneLayout',
    requires: [
    ],
    config: {
        html : 'Phone',
        items: [
        ]
    }
});

but remember to replace word “phone” by “tablet” in second one.
Now open app.js and replace

   views: [
        'Main'
    ],

by

    profiles: [
        'Phone',
        'Tablet'
    ],

next, find lines

        // Initialize the main view
        Ext.Viewport.add(Ext.create('TutsApp.view.Main'));

replace it by this

        // Initialize the main view
        this.loadLayout();

and in the end of class define loadLayout method

    loadLayout : function () {
        Ext.Viewport.add(Ext.create('TutsApp.view.' + TutsApp.app.getCurrentProfile().getNamespace() + '.Layout'));
    }

Remember if you debugging your application in browser you need to set proper User Agent. Otherwise, none of the profiles defined above will work.
If you use Chrome you can easily choose what type of device you want to simulate. Just open setting in Developer Console, get into Overrides, tick check-boxes Enable, Enable on DevTools startup, User Agent and pick one of the devices from list.
Refresh app again and check the results.

2. Deal with orientation

Profiles are very useful, but sometimes they are not enough. Why? Because it might be better to change views based on device orientation. Unfortunately, in this case we need to cover it by ourselves.
OK, but for the purposes of this tutorial I decide to have different views only on tablet devices, on mobile views will be always the same.
We can achieve this with two different approaches. We can build two separates layouts one for landscape, second for portrait mode and only change visibility of those layouts or we can add few line of css code and manage visibility via css class. I will show you both of this approach and you will decide which one you want to use.

a) Separate layouts for portrait and landscape

Remove file Layout.js from \app\views\tablet and create in this location folders landscape and portrait. In both of directories create files called Main.js.
Fill those scripts with code below
- for landscape:

Ext.define('TutsApp.view.tablet.landscape.Main', {
    extend: 'Ext.tab.Panel',
    xtype: 'tabletLandscapeMain',
    requires: [
    ],
    config: {
        html: "Tablet Landscape Main",
        items: []
    }
});

- for portrait:

Ext.define('TutsApp.view.tablet.portrait.Main', {
    extend: 'Ext.tab.Panel',
    xtype: 'tabletPortraitMain',
    requires: [
    ],
    config: {
        html: "Tablet Portrait Main",
        items: []
    }
});

For phone we only need to rename file stored in \public\app\views\phone from Layout to Main and remember to match the contents of the file to its new name.
Right, but we still need file which take care about main layout, which will be shared between both profiles. So, create file Layout.js in \app\views and put in it this code:

Ext.define('TutsApp.view.Layout', {
    extend: 'Ext.Container',
    xtype: 'layout',
    config: {
        items: [
        ]
    },
    // based on the passed parameters add layers for both 
    // orientation or add one universal layer
    initLayout: function (dualLayout, device, orientation) {
        if (Ext.Array.contains(dualLayout, device)) {
            this.add([
                {
                    xtype: device + 'PortraitMain',
                    itemId: 'portrait',
                    hidden: orientation !== 'portrait'
                },
                {
                    xtype: device + 'LandscapeMain',
                    itemId: 'landscape',
                    hidden: orientation !== 'landscape'
                }
            ]);
        } else {
            this.add({
                xtype: device + 'Main'
            });
        }
        this.device = device;
        this.orientation = orientation;
        this.dualLayout = dualLayout;
    },
    // changed visibility of layer 
    changeOrientation: function (orientation, width, height) {
        if (Ext.Array.contains(this.dualLayout, this.device)) {
            this.down('#' + this.orientation).setHidden(true);
            this.down('#' + orientation).setHidden(false);
            this.orientation = orientation;
        }
    }
});

OK, last part of this task, open \app.js and modify it to something similar to this code:

..........................
    views: [
        'Layout'
    ]
..........................
    launch: function () {
        // Destroy the #appLoadingIndicator element
        Ext.fly('appLoadingIndicator').destroy();
        // Initialize the main view
        this.loadLayout();
        // take care on orientation change event
        Ext.Viewport.on('orientationchange', this.changeLayoutOrientation);
    },
    onUpdated: function () {
        Ext.Msg.confirm(
            "Application Update",
            "This application has just successfully been updated to the latest version. Reload now?",
            function (buttonId) {
                if (buttonId === 'yes') {
                    window.location.reload();
                }
            }
        );
    },
    // run layout
    loadLayout: function () {
        var layout = Ext.create('TutsApp.view.Layout'),
            profile;
        try {
            profile = this.getCurrentProfile().getNamespace();
        } catch (e) {
            if (Ext.Viewport.getOrientation() === 'portrait') {
                profile = Ext.Viewport.getWindowWidth() < 768 ? 'phone' : 'tablet';
            } else {
                profile = Ext.Viewport.getWindowWidth() < 1024 ? 'phone' : 'tablet';
            }
            Ext.Msg.alert(
                "Application Error",
                "Are you use desktop mode? Please disable it and reload application."
            );
            
        }
        layout.initLayout(['tablet'], profile, Ext.Viewport.getOrientation());
        Ext.Viewport.add(layout);
    },
    /**
     * @param viewport
     * @param orientation
     * @param width
     * @param height
     * change layout base on orientation
     *
     */
    changeLayoutOrientation: function (viewport, orientation, width, height) {
        viewport.down('layout').changeOrientation(orientation, width, height);
    }
..........................

In the end update dependencies in your tablet profile class:

..........................
        views: [
            'portrait.Main',
            'landscape.Main'
        ]
..........................

b) Managing visibility of components via CSS

Rename files \app\views\tablet\Layout.js and \app\views\phone\Layout.js to \same\path\Main.js (in file change class name and xtype and remember to update dependencies in profile classes too). Open \app\views\tablet\Main.js and past this

Ext.define('TutsApp.view.tablet.Main', {
    extend: 'Ext.Container',
    xtype: 'tabletMain',
    config: {
        layout: {
            type: 'hbox'
        },
        items: [
            {
                xtype: 'container',
                layout: 'fit',
                flex: 1,
                html : 'Main'
            },
            {
                xtype: 'container',
                layout: 'fit',
                width: 200,
                html: 'Visible only on landscape',
                // this component will be visible 
                // only in landscape mode
                cls: 'landscape'
            }
        ]
    }
});

Create file Layout.js in \app\views and fill it with this code

Ext.define('TutsApp.view.Layout', {
    extend: 'Ext.Container',
    xtype: 'layout',
    config: {
        layout: 'fit',
        items: [
        ]
    },
    initLayout: function (device) {
        var body = document.getElementsByTagName('body')[0],
            css = document.createElement('style'),
            style = document.createTextNode(
                "@media screen and (orientation:portrait) {" +
                    "/* Portrait styles */" +
                    "#" + body.id + " .landscape { display:none !important; }" +
                    "}" +
                    "/* Landscape */" +
                    "@media screen and (orientation:landscape) {" +
                    "#" + body.id + " .portrait { display:none !important; }" +
                    "}"
            );
        css.appendChild(style);
        body.insertBefore(css, null);
        this.add({
            xtype: device + 'Main'
        });
    }
});

Now modify \app.js

..........................
    views: [
        'Layout'
    ],
..........................
    launch: function () {
        // Destroy the #appLoadingIndicator element
        Ext.fly('appLoadingIndicator').destroy();
        // Initialize the main view
        this.loadLayout();
    },
..........................
    loadLayout: function () {
        var layout = Ext.create('TutsApp.view.Layout'),
            profile;
        try {
            profile = this.getCurrentProfile().getNamespace();
        } catch (e) {
            if (Ext.Viewport.getOrientation() === 'portrait') {
                profile = Ext.Viewport.getWindowWidth() < 768 ? 'phone' : 'tablet';
            } else {
                profile = Ext.Viewport.getWindowWidth() < 1024 ? 'phone' : 'tablet';
            }
            Ext.Msg.alert(
                "Application Error",
                "Are you use desktop mode? Please disable it and reload application."
            );
        }
        layout.initLayout(profile);
        Ext.Viewport.add(layout);
    }
..........................

That should be enough, now you can test what we done. One advice, to test tablet functionality you need to set User Agent to Chrome – Android Tablet in devTools. If you set User Agent as iPad it still be recognized as a iPhone.

2c

3. Write your first model…

The reason why we still didn’t wrote a bunch of unit test is because I usually don’t do that for views. Of course it’s possible but more comfortable will be to cover it via integration test. So, let’s dive straight into the model. For our purpose we determine that

  • we will create model for user
  • with fields: id, firstName, lastName, email, password, createdAt
  • field firstName is required and should have also at least 5 characters
  • field lastName is required
  • field email is required and should match to [a-z0-9!#$%&'*+/=?^_{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])? pattern
  • field password is required and should have also at least 7 characters
  • field createdAt has default value (current time in timestamp)

OK, now we are ready to start coding. We will do that step by step, first, we will write unit test and after that we will provide a solution to pass it.
On more thing, run watcher in console.

grunt test

Let’s begin from basic assumption, in \app-test\specs create file userModelSpec.js and add something like that:

describe("TutsApp.model.User", function () {
    Ext.require('TutsApp.model.User');
    // check if model was loaded
    it('exists', function() {
        var model = Ext.create('TutsApp.model.User');
        expect(model.$className).toEqual('TutsApp.model.User');
    });
    // and check if passed data was correctly assigned
    it('has data', function () {
        var model = Ext.create('TutsApp.model.User', {
            firstName: 'Name',
            lastName : 'Last',
            email: 'test@example.com',
            password: '123qwe'
        });
        expect(model.get('firstName')).toEqual('Name');
        expect(model.get('lastName')).toEqual('Last');
        expect(model.get('email')).toEqual('test@example.com');
        expect(model.get('password')).toEqual('123qwe');
    });
});

If you check console you should notice that something bad happen, red color isn’t a good sign, right? Lucky, we can fix it quickly. Firstly, remove \app-test\specs\testSpec.js, next, create another new file, in \app\model directory add User.js and define class for user object:

Ext.define('TutsApp.model.User', {
    extend: 'Ext.data.Model',
    config: {
        fields: [
            {
                name: 'firstName'
            },
            {
                name: 'lastName'
            },
            {
                name: 'email'
            },
            {
                name: 'password'
            },
            {
                name: 'id'
            },
            {
                name: 'createdAt'
            }
        ]
    }
});

Save it and check console again. Yep, earlier error disappeared and we got a message, highlighted on green, that test was perform successfully. Great, so it’s time for next task.
\app-test\specs\UserModelSpecs.js

..........................
    it('generate default values', function (){
        var model = Ext.create('TutsApp.model.User', {});
        var date = new Date(model.get('createdAt')),
            currentDate = new Date();
        expect(date.getUTCFullYear()).toEqual(currentDate.getUTCFullYear());
    });
..........................

OK, another assumption, we need to check if default value was generated. To cover this modify \app\model\User.js

..........................
            {
                name: 'createdAt',
                defaultValue : +new Date(),
                convert : function (value){
                    return new Date(value);
                }
            }
..........................

Check console. No error? You are ready for part number three.
\app-test\specs\UserModelSpecs.js

..........................
    it('requires first name and first name need to be longer than 5 characters', function () {
        // we create empty model
        var model = Ext.create('TutsApp.model.User', {}),
            validator = model.validate();
        // so validation should be falsy
        expect(validator.isValid()).toBeFalsy();
        // as we had two valiadtors they results will be stored in array
        expect(validator.getByField('firstName')[0].getMessage()).toEqual('must be present');
        expect(validator.getByField('firstName')[1].getMessage()).toEqual('is the wrong length');
    });

    it('requires last name', function () {
        var model = Ext.create('TutsApp.model.User', {}),
            validator = model.validate();
        expect(validator.isValid()).toBeFalsy();
        expect(validator.getByField('lastName')[0].getMessage()).toEqual('must be present');
    });

    it('requires email which match to regexp', function () {
        var model = Ext.create('TutsApp.model.User', {
                firstName: 'Name5',
                lastName: 'Last',
                email: 'test@examplecom',
                password: '123qwe7'
            }),
            validator = model.validate();
        expect(validator.isValid()).toBeFalsy();
        expect(validator.getByField('email')[0].getMessage()).toEqual('is the wrong format');
    });

    it('requires password and password need to be longer than 7 characters', function () {
        var model = Ext.create('TutsApp.model.User', {}),
            validator = model.validate();
        expect(validator.isValid()).toBeFalsy();
        expect(validator.getByField('password')[0].getMessage()).toEqual('must be present');
        expect(validator.getByField('password')[1].getMessage()).toEqual('is the wrong length');
    });

    it('is valid', function () {
        var model = Ext.create('TutsApp.model.User', {
            firstName: 'Name5',
            lastName: 'Last',
            email: 'test@example.com',
            password: '123qwe7'
        });
        expect(model.validate().isValid()).toBeTruthy();
    });
..........................

\app\model\User.js

..........................
            {
                name: 'createdAt',
                defaultValue: +new Date(),
                convert: function (value) {
                    return new Date(value);
                }
            }
        ],
        validations: [
            {type: 'presence', field: 'firstName'},
            {type: 'length', field: 'firstName', min: 5},
            {type: 'presence', field: 'lastName'},
            {type: 'presence', field: 'password'},
            {type: 'length', field: 'password', min: 7},
            {type: 'presence', field: 'email'},
            {type: 'format', field: 'email', matcher: /[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/}
        ]
    }
});

After this modification tests should be passed again.
But so far we are playing only with static data and possibility that we won’t use any Ajax request in web app is very low. So, let’s cover it right now.
To achieve this we will use Jasmine’s Ajax mocking helper (download). Attached version base on the original jasmine-ajax script but I add support for Ext.Direct which I always use in my Sencha application. But before we start, we will utilize some functions to write it only ones and use multiple times. So open \app-test\specs\helpers\specHelper.js and add

/**
 * clear all fake responses
 */
function clearResponses() {
// remove all fake responses
    jasmine.Ajax.stubs
        .reset();
}

/**
 * @param method
 * @param response
 * Add fake direct response
 */
function addDirectResponse(method, response) {
    jasmine.Ajax
        // create new stub for direct method
        .stubRequest(method)
        // and assign response
        .andReturn(response);
}

Now in \app-test\specs directory add new file called UserModelDirectSpec.js

describe("TutsApp.model.User Ext.Direct", function () {
    // Tests for Ext.Direct

    // Load all necessary files
    Ext.require([
        'Ext.direct.*',
        'TutsApp.model.User'
    ]);

    // Create new namespace for direct
    Ext.ns("ExtRemote");

    // define api
    ExtRemote.REMOTING_API = {"url": "/direct", "namespace": "ExtRemote", "type": "remoting", "actions": {
        "DXUser": [
            {"name": "read", "len": 1},
            {"name": "create", "len": 1},
            {"name": "update", "len": 1},
            {"name": "destroy", "len": 1},
            {"name": "isEmailUnique", "len": 1}
        ]}
    };

    // add default direct provider passing only api definition
    Ext.direct.Manager.addProvider(ExtRemote.REMOTING_API);

    beforeEach(function () {
        // before each test swamp global
        // xhr object with fake one
        jasmine.Ajax.useMock();
        //fake xhr will be removed automatically
    });

    it('load data', function () {
        var complete = false;
        clearResponses();
        addDirectResponse('DXUser.read', {
            status: 200,
            responseText: JSON.stringify([
                {"type": "rpc", "tid": 1, "action": "DXUser", "method": "read", "result": {"success": true, "User": [
                    {"firstName": "First", "lastName": "Name", "email": "name@gmail.com", "password": "123qwe7", "id": "5264f4934deea8e80b000001", "__v": 0, "createdAt": "2013-10-23T21:47:11.795Z"}
                ], "total": 1}}
            ])
        });
        // load model
        TutsApp.model.User.load(1, function (record) {
            expect(record.get('id')).toBeDefined();
            expect(record.get('id')).toEqual('5264f4934deea8e80b000001');
            //tell jasmine that  spec was done
            complete = true;
        });
        // finish spec only when (fake) request will be finished
        waitsFor(function () {
            return complete;
        });
    });

    it('has unique email', function () {
        var complete = false;
        clearResponses();
        addDirectResponse('DXUser.isEmailUnique', {
            status: 200,
            responseText: JSON.stringify([
                {
                    "type": "rpc",
                    "tid": 2,
                    "action": "DXUser",
                    "method": "read",
                    "result": {
                        "success": true
                    }
                }
            ])
        });
        
        TutsApp.model.User.isEmailUnique("name@gmail.com", function (response) {
            expect(response).toBeTruthy();
            complete = true;
        });
        // finish spec only when (fake) request will be finished
        waitsFor(function () {
            return complete;
        });
    });
});

Not so hard right? Only one thing can be confusing for us. If we didn’t make any asynchronous request why we wrote code like for asynchronous task? Ext.Direct is asynchronous itself. When we called load method we only added new transaction to the queue and the request was automatically fired after proper timeout, asynchronously of course.
OK, we need to adjust our User model now.

..........................
validations: [
            {type: 'presence', field: 'firstName'},
            {type: 'length', field: 'firstName', min: 5},
            {type: 'presence', field: 'lastName'},
            {type: 'presence', field: 'password'},
            {type: 'length', field: 'password', min: 7},
            {type: 'presence', field: 'email'},
            {type: 'format', field: 'email', matcher: /[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/}
        ],
        // add direct proxy
        proxy: {
            type: 'direct',
            api: {
                create: 'ExtRemote.DXUser.create',
                read: 'ExtRemote.DXUser.read',
                update: 'ExtRemote.DXUser.update',
                destroy: 'ExtRemote.DXUser.destroy'
            },
            reader: {
                type: 'json',
                rootProperty: 'User'
            }
        }
    },
    // add some static methods
    statics: {
        isEmailUnique : function (value, callback) {
            ExtRemote.DXUser.isEmailUnique(value, callback);
        }
    }
..........................

4. … store …

OK, if we cover model testing we should be able to test store in the similar way.
Create UserStoreDirectSpec.js in which put some test for user store.

describe("TutsApp.store.Users", function () {
    // Tests for Ext.Direct

    // Load all necessary files
    Ext.require([
        'Ext.direct.*',
        'TutsApp.store.Users'
    ]);

    // Create new namespace for direct
    Ext.ns("ExtRemote");

    // define api
    ExtRemote.REMOTING_API = {"url": "/direct", "namespace": "ExtRemote", "type": "remoting", "actions": {
        "DXUser": [
            {"name": "read", "len": 1},
            {"name": "create", "len": 1},
            {"name": "update", "len": 1},
            {"name": "destroy", "len": 1},
            {"name": "isEmailUnique", "len": 1}
        ]}
    };

    // add default direct provider passing only api definition
    Ext.direct.Manager.addProvider(ExtRemote.REMOTING_API);

    beforeEach(function () {
        // before each test swamp global
        // xhr object with fake one
        jasmine.Ajax.useMock();
        //fake xhr will be removed automatically
    });

    it('load data', function () {
        var complete = false,
            store = Ext.create('TutsApp.store.Users', {
            storeId: 'ut-Users',
            // turn off auto load to prevent
            // from triggering request before
            // we mount fake one
            autoLoad : false
        });
        clearResponses();
        addDirectResponse('DXUser.read', {
            status: 200,
            responseText: JSON.stringify([
                {"type": "rpc", "tid": Ext.data.Connection.requestId + 1, "action": "DXUser", "method": "read", "result": {"success": true, "User": [
                    {"firstName": "First", "lastName": "Name", "email": "name@gmail.com", "password": "123qwe7", "_id": "5264f4934deea8e80b000001", "__v": 0, "createdAt": "2013-10-23T21:47:11.795Z"},
                    {"firstName": "First2", "lastName": "Name", "email": "name@gmail.com", "password": "123qwe7", "_id": "5264f4934deea8e80b000002", "__v": 0, "createdAt": "2013-10-23T21:47:11.795Z"},
                    {"firstName": "First3", "lastName": "Name", "email": "name@gmail.com", "password": "123qwe7", "_id": "5264f4934deea8e80b000003", "__v": 0, "createdAt": "2013-10-23T21:47:11.795Z"}
                ], "total": 3}}
            ])
        });
        store.load({
            callback: function () {
                expect(store.getCount()).toEqual(3);
                expect(store.getAt(0).get('firstName')).toEqual("First");
                expect(store.getAt(1).get('firstName')).toEqual("First2");
                expect(store.getAt(2).get('firstName')).toEqual("First3");
                complete = true;
            }
        });
        // finish spec only when (fake) request will be finished
        waitsFor(function () {
            return complete;
        });
    });
});

Remember, when testing direct request one rule is very important, property tid (transition id) from the response must match to the same property in the request. So, the best solution for this is to use static property requestId from Ext.data.Connection class.
Time to resolve all errors, in \app\store\ create Users.js and fill it with:

Ext.define('TutsApp.store.Users', {
    extend: 'Ext.data.Store',

    requires: [
        'TutsApp.model.User'
    ],

    config: {
        autoLoad: true,
        autoSync: false,
        model: 'TutsApp.model.User',
        storeId: 'UsersStore',
        proxy: {
            type: 'direct',
            api: {
                create: 'ExtRemote.DXUser.create',
                read: 'ExtRemote.DXUser.read',
                update: 'ExtRemote.DXUser.update',
                destroy: 'ExtRemote.DXUser.destroy'
            },
            reader: {
                type: 'json',
                rootProperty: 'User'
            }
        }
    }
});

5. … and controller

Testing controllers also can be a tricky task. If we following the current standard we should know that most of the logic should be located in the models or in the separate classes which contains only logic. Controller should be only a connector between those classes and views. So, basically the best place to cover controller are integration tests where we check how components interact which each other. If we really want to make unit test for controllers, we should only cover this part which can be isolated from rest of the application.
Here is simple example:
\app-test\specs\UserControllerSpec.js

describe("controller TutsApp.controller.User", function () {
    var app,
        ctl;

    // load dependencies
    Ext.require([
        'TutsApp.controller.User',
        'TutsApp.store.Users'
    ]);

    beforeEach(function () {
        var appStart = false;
        // create app
        app = Ext.create('Ext.app.Application', {
            launch: function () {
                appStart = true;
            }});
        ctl = Ext.create('TutsApp.controller.User',{
            application : app
        });

        //w8 on application launch
        waitsFor(function () {
            return appStart;
        });
    });

    afterEach(function () {
        // destroy app after each test
        app.destroy();
    });

    it('on refresh button tap load store', function() {
        // create fake store
        var users = Ext.create('TutsApp.store.Users', {
            autoLoad : false
        });
        // add spy on it
        spyOn(users, 'load');
        // mock getListStore function 
        spyOn(ctl, 'getListStore').andCallFake(function() {
            // and return our fake store in it
            return users;
        });

        // call function which is 
        // callback for tap tap event occur
        ctl.onRefresh();

        // expect that load was triggered
        expect(users.load).toHaveBeenCalled();
    });
});

As you can see, instead of firing event (which in the other hand will force on us to load and initialize view) we only call event callback.
OK, so how our controller will looks:
\app\controller\phone\User.js

Ext.define('TutsApp.controller.User', {
    extend: 'Ext.app.Controller',
    config: {
        stores: [
            'Users'
        ],
        refs: {
            refreshButton: '[itemId=list-refresh]',
            list: '[itemId=user-list]'
        },
        control: {
            refreshButton: {
                tap: 'onRefresh'
            }
        }
    },
    launch: function () {
        this.callParent(arguments);
    },
    getListStore: function () {
        return this.getList().getStore();
    },
    onRefresh: function () {
        this.getListStore().load();
    }
});

6. Conclusion

Huh, it was a looong post, we cover so much stuff, from basic model testing to playing with mocks. We also learned how to create profiles, which is very helpful if we want to develop an app on the different devices. And of course, how to build different views in case of orientation.
In the next post from the series, we will cover backend things like setup a RESTful server. And I will also show you how to prepare it to work with Ext.Direct. So if you are interested, please subscribe our blog on G+.

Comments:

Leave a Reply

Please fill all required fields


Drag circle to the rectangle on the right!

Send