DoneJS StealJS jQuery++ FuncUnit DocumentJS
3.14.1
5.0.0 4.3.0 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Reading the Docs (API Guide)
    • recipes
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • TodoMVC with StealJS
    • upgrade
      • Migrating to CanJS 3
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

ATM Guide

  • Edit on GitHub

This guide will walk you through building and testing an Automated Teller Machine (ATM) application with CanJS’s Core libraries. You’ll learn how to do test driven development (TDD) and manage complex state. It takes about 2 hours to complete.

Overview

Check out the final app:

JS Bin on jsbin.com

Notice it has tests at the bottom of the Output tab.

Setup

The easiest way to get started is to clone the following JS Bin by clicking the JS Bin button on the top left:

JS Bin on jsbin.com

The JS Bin is designed to run both the application and its tests in the OUTPUT tab. To set this up, the HTML tab:

  • Loads QUnit for its testing library. It also includes the <div id="qunit"></div> element where QUnit’s test results will be written to.

  • Loads can.all.js, which is a script that includes all of CanJS core under a single global can namespace.

    Generally speaking, you should not use the global can script, but instead you should import things directly with a module loader like StealJS, WebPack or Browserify. Read Setting Up CanJS for instructions on how to set up CanJS in a real app.

  • Includes the content for an app-template can-stache template. This template provides the title for the ATM app and uses the <atm-machine> custom can-component element that will eventually provide the ATM functionality.

The JavaScript tab is split into two sections:

  • CODE - The ATM’s models, view-models and component code will go here.
  • TESTS - The ATM’s tests will go here.

Normally, your application’s code and tests will be in separate files and loaded by different html pages, but we combine them here to fit within JS Bin’s limitations.

The CODE section renders the app-template with:

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

The TESTS section labels which module will be tested:

QUnit.module("ATM system", {});

Mock out switching between pages

In this section, we will mock out which pages will be shown as the state of the ATM changes.

Update the HTML tab to:

  • Switch between different pages of the application as the ATM view-model’s state property changes with {{#switch(expression)}}.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the JavaScript tab to:

  • Create the ATM view-model with a state property initialized to readingCard with can-define/map/map.
  • Create an <atm-machine> custom element with can-component.
// ========================================
// CODE
// ========================================

var ATM = can.DefineMap.extend({
    state: {type: "string", value: "readingCard"}
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {});

When complete, you should see the “Reading Card” title.

This step includes all the potential pages the state property can transition between:

  • readingCard
  • readingPin
  • choosingTransaction
  • pickingAccount
  • depositInfo
  • withdrawalInfo
  • successfulTransaction
  • printingReceipt

Each of those states are present in the following state diagram:

We’ll build out these pages once we build the Card and Transaction sub-models that will make building the ATM view model easier.

Card tests

In this section, we will:

  • Design an API for an ATM Card
  • Write out tests for the card.

An ATM Card will take a card number and pin. It will start out as having a state of "unverified". It will have a verify method that will change the state to "verifying", and if the response is successful, state will change to "verified".

Update the JavaScript tab to:

  • Make the fake data request delay 1ms by setting delay to 1 before every test and restoring it to 2s after every test runs.
  • Write a test that creates a valid card, calls .verify(), and asserts the state is "verified".
  • Write a test that creates an invalid card, calls .verify(), and asserts the state is "invalid".
// ========================================
// CODE
// ========================================

var ATM = can.DefineMap.extend({
    state: {type: "string", value: "readingCard"}
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

When complete, you should have a breaking test. Now let’s make it pass.

Card model

In this section, we will:

  • Implement the Card model so that all the tests pass.

Update the JavaScript tab to:

  • Simulate the /verifyCard with can-fixture. It will return a successful response if the request body has a number and pin, or a 400 if not.
  • Use can-define/map/map to define the Card model, including:
    • a number and a pin property.
    • a state property initialized to unverified that is not part of the card’s serialized data.
    • a verify method that posts the card’s data to /verifyCard and updates the state accordingly.
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var ATM = can.DefineMap.extend({
    state: {type: "string", value: "readingCard"}
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

When complete, all tests should pass.

In this step, you implemented a Card model that encapsulates the behavior of its own state.

Deposit test

In this section, we will:

  • Design an API retrieving Accounts.
  • Design an API for a Deposit type.
  • Write out tests for the Deposit type.

An Account will have an id, name, and balance. We’ll use can-connect to add a getList method that retrieves an account given a card.

A Deposit will take a card, an amount, and an account. Deposits will start out having a state of "invalid". When the deposit has a card, amount and account, the state will change to "ready". Once the deposit is ready, the .execute() method will change the state to "executing" and then to "executed" once the transaction completes.

Update the JavaScript tab to:

  • Create a deposit with an amount and a card.
  • Check that the state is "invalid" because there is no account.
  • Use Account.getList to get the accounts for the card and:
    • set the deposit.accounts to the first account.
    • remember the starting balance.
  • Use on to listen for state changes. When state is:
    • "ready", .execute() the transaction.
    • "executed", verify the new account balance.
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var ATM = can.DefineMap.extend({
    state: {type: "string", value: "readingCard"}
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

When complete, the Deposit test should run, but error because Deposit is not defined.

Optional: Challenge yourself by writing the Withdrawal test on your own. How is it different than the Deposit test?

Transaction, Deposit, and Withdrawal models

In this section, we will:

  • Implement the Account model.
  • Implement a base Transaction model and extend it into Deposit and Withdrawal models.
  • Get the Deposit test to pass.

Update the JavaScript tab to:

  • Simulate /accounts to return Account data with can-fixture.
  • Simulate /deposit to always return a successful result.
  • Simulate /withdrawal to always return a successful result.
  • Define the Account model to:
    • have an id property.
    • have a balance property.
    • have a name property.
  • Define an Account.List type with can-define/list/list.
  • Connect Account and Account.List types to the RESTful /accounts endpoint using can-connect/can/base-map/base-map.
  • Define the Transaction model to:
    • have account and card properties.
    • have executing and executed properties that track if the transaction is executing or has executed.
    • have a rejected property that stores the error given for a failed transaction.
    • have an abstract ready property that Deposit and Withdrawal will implement to return true when the transaction is in an executable state.
    • have a state property that reads other stateful properties and returns a string representation of the state.
    • have an abstract executeStart method that Deposit and Withdrawal will implement to execute the transaction and return a Promise that resolves when the transaction is complete.
    • have an abstract executeEnd method that Deposit and Withdrawal will implement to update the transactions values (typically the account balance) if the transaction is successfully completed.
    • have an execute method that calls .executeStart() and executeEnd() and keeps the stateful properties updated correctly.
  • Define the Deposit model to:
    • have an amount property.
    • implement ready to return true when the amount is greater than 0 and there’s an account and card.
    • implement executeStart to POST the deposit information to /deposit
    • implement executeEnd to update the account balance.
  • Define the Withdrawal model to behave in the same way as Deposit except that it POSTs the withdrawal information to /withdrawal.
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    state: {type: "string", value: "readingCard"}
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

When complete, the Deposit tests will pass.

Reading Card page and test

In this section, we will:

  • Allow the user to enter a card number and go to the Reading Pin page.
  • Add tests to the ATM Basics test.

Update the HTML tab to:

  • Allow a user to call cardNumber with the <input>’s value.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the JavaScript tab to:

  • Declare a card property.
  • Derive a state property that changes to "readingPin" when card is defined.
  • Add a cardNumber that creates a card with the provided number.
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,

    // derived properties
    get state(){
        if(this.card) {
            return "readingPin";
        }
        return "readingCard";
    },
    // methods
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

QUnit.asyncTest("ATM basics", function() {

    var atm = new ATM();

    equal(atm.state, "readingCard", "starts at reading card state");

    atm.cardNumber("01233456789");

    equal(atm.state, "readingPin", "moves to reading card state");

    QUnit.start();
});

When complete, you should be able to enter a card number and see the Reading Pin page.

Reading Pin page and test

In this section, we will:

  • Allow the user to enter a pin number and go to the Choosing Transaction page.
  • Add tests to the ATM Basics test.

Update the HTML tab to:

  • Call pinNumber with the <input>’s value.
  • Disable the <input> while the pin is being verified.
  • Show a loading icon while the pin is being verified.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the ATM view model in the CODE section of the JavaScript tab to:

  • Define an accountsPromise property that will contain a list of accounts for the card.
  • Define a transactions property that will contain a list of transactions for this session.
  • Update state to be in the "choosingTransaction" state when the card is verified.
  • Define a pinNumber method that updates the card’s pin, calls .verify(), and initializes the accountsPromise and transactions properties.

Update the TESTS section of the JavaScript tab to:

  • Test whether calling pinNumber moves the state to "choosingTransaction".
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,
    accountsPromise: "any",
    transactions: can.DefineList,

    // derived properties
    get state(){
        if(this.card) {
            if (this.card.state === "verified") {
                return "choosingTransaction";
            }
            return "readingPin";
        }
        return "readingCard";
    },

    // methods
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    },
    pinNumber: function(pin) {
        var card = this.card;

        card.pin = pin;
        this.transactions = new can.DefineList();
        this.accountsPromise = card.verify().then(function(card) {

            return Account.getList(card.serialize());
        });
    },
    exit: function(){
        this.set({
            card: null,
            accountsPromise: null,
            transactions: null
        });
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

QUnit.asyncTest("ATM basics", function() {

    var atm = new ATM();

    equal(atm.state, "readingCard", "starts at reading card state");

    atm.cardNumber("01233456789");

    equal(atm.state, "readingPin", "moves to reading card state");

    atm.pinNumber("1234");

    ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");

    atm.on("state", function(ev, newVal) {

        if (newVal === "choosingTransaction") {
            QUnit.ok(true, "in choosingTransaction");
            QUnit.start();
        }
    });
});

When complete, you should be able to enter a card and pin number and see the Choosing Transaction page.

Choosing Transaction page and test

In this section, we will:

  • Allow the user to pick a transaction type and go to the Picking Account page.
  • Add tests to the ATM Basics test.

Update the HTML tab to:

  • Have buttons for choosing a deposit, withdrawal, or print a receipt and exit.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="chooseDeposit()">Deposit</li>
                <li on:click="chooseWithdraw()">Withdraw</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the ATM view model in the CODE section of the JavaScript tab to:

  • Define a currentTransaction property that when set, adds the previous currentTransaction to the list of transactions.
  • Update the state property to "pickingAccount" when there is a currentTransaction.
  • Update the exit method to clear the currentTransaction property.
  • Define chooseDeposit that creates a Deposit and sets it as the currentTransaction.
  • Define chooseWithdraw that creates a Withdraw and sets it as the currentTransaction.

Update the TESTS section of the JavaScript tab to:

  • Call .chooseDeposit() and verify that the state moves to "pickingAccount".
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,
    accountsPromise: "any",
    transactions: can.DefineList,
    currentTransaction: {
        set: function(newTransaction) {
            var currentTransaction = this.currentTransaction;
            if (this.transactions && currentTransaction &&
                currentTransaction.state === "executed") {

                this.transactions.push(currentTransaction);
            }
            return newTransaction;
        }
    },

    // derived properties
    get state(){

        if (this.currentTransaction) {
            return "pickingAccount";
        }

        if(this.card) {
            if (this.card.state === "verified") {
                return "choosingTransaction";
            }
            return "readingPin";
        }
        return "readingCard";
    },

    // methods
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    },
    pinNumber: function(pin) {
        var card = this.card;

        card.pin = pin;
        this.transactions = new can.DefineList();
        this.accountsPromise = card.verify().then(function(card) {

            return Account.getList(card.serialize());
        });
    },
    exit: function(){
        this.set({
            card: null,
            accountsPromise: null,
            transactions: null,
            currentTransaction: null
        });
    },
    chooseDeposit: function() {
        this.currentTransaction = new Deposit({
            card: this.card
        });
    },
    chooseWithdraw: function() {
        this.currentTransaction = new Withdrawal({
            card: this.card
        });
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

QUnit.asyncTest("ATM basics", function() {

    var atm = new ATM();

    equal(atm.state, "readingCard", "starts at reading card state");

    atm.cardNumber("01233456789");

    equal(atm.state, "readingPin", "moves to reading card state");

    atm.pinNumber("1234");

    ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");

    atm.on("state", function(ev, newVal) {

        if (newVal === "choosingTransaction") {

            QUnit.ok(true, "in choosingTransaction");
            atm.chooseDeposit();

        } else if (newVal === "pickingAccount") {

            QUnit.ok(true, "in picking account state");
            QUnit.start();

        }
    });
});

Note: We will define printReceiptAndExit later!

Picking Account page and test

In this section, we will:

  • Allow the user to pick an account and go to either the Deposit Info or Withdrawal Info page.
  • Add tests to the ATM Basics test.

Update the HTML tab to:

  • Write out a “Loading Accounts…” message while the accounts are loading.
  • Write out the accounts when loaded.
  • Call chooseAccount() when an account is clicked.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="chooseDeposit()">Deposit</li>
                <li on:click="chooseWithdraw()">Withdraw</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
        <p>Please pick your account:</p>
        {{#if(accountsPromise.isPending)}}
            <div class='warn'>
                <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    Loading Accounts…
                </p>
            </div>
        {{else}}
            <ul>
                {{#each(accountsPromise.value)}}
                    <li on:click="chooseAccount(.)">{{name}} - ${{balance}}</li>
                {{/each}}
            </ul>
        {{/if}}
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the ATM view model in the CODE section of the JavaScript tab to:

  • Change state to check if the currentTransaction has an account and update the value to "depositInfo" or "withdrawalInfo", depending on the currentTransaction’s type.
  • Add a chooseAccount method that sets the currentTransaction’s account.

Update the TESTS section of the JavaScript tab to:

  • Call .chooseAccount() with the first account loaded.
  • Verify the state changes to "depositInfo".
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,
    accountsPromise: "any",
    transactions: can.DefineList,
    currentTransaction: {
        set: function(newTransaction) {
            var currentTransaction = this.currentTransaction;
            if (this.transactions && currentTransaction &&
                currentTransaction.state === "executed") {

                this.transactions.push(currentTransaction);
            }
            return newTransaction;
        }
    },

    // derived properties
    get state(){

        if (this.currentTransaction) {
            if (this.currentTransaction.account) {
                if (this.currentTransaction instanceof Deposit) {
                    return "depositInfo";
                } else {
                    return "withdrawalInfo";
                }
            }

            return "pickingAccount";
        }

        if(this.card) {
            if (this.card.state === "verified") {
                return "choosingTransaction";
            }
            return "readingPin";
        }
        return "readingCard";
    },

    // methods
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    },
    pinNumber: function(pin) {
        var card = this.card;

        card.pin = pin;
        this.transactions = new can.DefineList();
        this.accountsPromise = card.verify().then(function(card) {

            return Account.getList(card.serialize());
        });
    },
    exit: function(){
        this.set({
            card: null,
            accountsPromise: null,
            transactions: null,
            currentTransaction: null
        });
    },
    chooseDeposit: function() {
        this.currentTransaction = new Deposit({
            card: this.card
        });
    },
    chooseWithdraw: function() {
        this.currentTransaction = new Withdrawal({
            card: this.card
        });
    },
    chooseAccount: function(account) {
        this.currentTransaction.account = account;
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

QUnit.asyncTest("ATM basics", function() {

    var atm = new ATM();

    equal(atm.state, "readingCard", "starts at reading card state");

    atm.cardNumber("01233456789");

    equal(atm.state, "readingPin", "moves to reading card state");

    atm.pinNumber("1234");

    ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");

    atm.on("state", function(ev, newVal) {

        if (newVal === "choosingTransaction") {

            QUnit.ok(true, "in choosingTransaction");
            atm.chooseDeposit();

        } else if (newVal === "pickingAccount") {

            QUnit.ok(true, "in picking account state");
            atm.accountsPromise.then(function(accounts){
                atm.chooseAccount(accounts[0]);
            });

        } else if (newVal === "depositInfo") {

            QUnit.ok(true, "in depositInfo state");
            QUnit.start();

        }
    });
});

Deposit Info page and test

In this section, we will:

  • Allow the user to enter the amount of a deposit and go to the Successful Transaction page.
  • Add tests to the ATM Basics test.

Update the HTML tab to:

  • Ask the user how much they would like to deposit into the account.
  • Update currentTransaction.amount with an <input>’s value.
  • If the transaction is executing, show a spinner.
  • If the transaction is not executed:
    • show a Deposit button that will be active only once the transaction has a value.
    • show a cancel button that will clear this transaction.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="chooseDeposit()">Deposit</li>
                <li on:click="chooseWithdraw()">Withdraw</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
        <p>Please pick your account:</p>
        {{#if(accountsPromise.isPending)}}
            <div class='warn'>
                <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    Loading Accounts…
                </p>
            </div>
        {{else}}
            <ul>
                {{#each(accountsPromise.value)}}
                    <li on:click="chooseAccount(.)">{{name}} - ${{balance}}</li>
                {{/each}}
            </ul>
        {{/if}}
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
        <p>
            How much would you like to deposit
            into {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="deposit" value:bind="currentTransaction.amount"/>
        </p>

        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Deposit
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the ATM view model in the JavaScript tab to:

  • Change state to "successfulTransaction" if the currentTransaction was executed.
  • Add a removeTransaction method that removes the currentTransaction, which will revert state to "choosingTransaction".

Update the ATM basics test in the JavaScript tab to:

  • Add an amount to the currentTransaction.
  • Make sure the currentTransaction is ready to be executed.
  • Execute the currentTransaction and make sure that the state stays as "depositInfo" until the transaction is successful.
  • Verify the state changed to "successfulTransaction".
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,
    accountsPromise: "any",
    transactions: can.DefineList,
    currentTransaction: {
        set: function(newTransaction) {
            var currentTransaction = this.currentTransaction;
            if (this.transactions && currentTransaction &&
                currentTransaction.state === "executed") {

                this.transactions.push(currentTransaction);
            }
            return newTransaction;
        }
    },

    // derived properties
    get state(){

        if (this.currentTransaction) {
            if (this.currentTransaction.state === "executed") {
                return "successfulTransaction";
            }

            if (this.currentTransaction.account) {
                if (this.currentTransaction instanceof Deposit) {
                    return "depositInfo";
                } else {
                    return "withdrawalInfo";
                }
            }

            return "pickingAccount";
        }

        if(this.card) {
            if (this.card.state === "verified") {
                return "choosingTransaction";
            }
            return "readingPin";
        }
        return "readingCard";
    },

    // methods
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    },
    pinNumber: function(pin) {
        var card = this.card;

        card.pin = pin;
        this.transactions = new can.DefineList();
        this.accountsPromise = card.verify().then(function(card) {

            return Account.getList(card.serialize());
        });
    },
    exit: function(){
        this.set({
            card: null,
            accountsPromise: null,
            transactions: null,
            currentTransaction: null
        });
    },
    chooseDeposit: function() {
        this.currentTransaction = new Deposit({
            card: this.card
        });
    },
    chooseWithdraw: function() {
        this.currentTransaction = new Withdrawal({
            card: this.card
        });
    },
    chooseAccount: function(account) {
        this.currentTransaction.account = account;
    },
    removeTransaction: function() {
        this.currentTransaction = null;
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

QUnit.asyncTest("ATM basics", function() {

    var atm = new ATM();

    equal(atm.state, "readingCard", "starts at reading card state");

    atm.cardNumber("01233456789");

    equal(atm.state, "readingPin", "moves to reading card state");

    atm.pinNumber("1234");

    ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");

    atm.on("state", function(ev, newVal) {

        if (newVal === "choosingTransaction") {

            QUnit.ok(true, "in choosingTransaction");
            atm.chooseDeposit();

        } else if (newVal === "pickingAccount") {

            QUnit.ok(true, "in picking account state");
            atm.accountsPromise.then(function(accounts){
                atm.chooseAccount(accounts[0]);
            });

        } else if (newVal === "depositInfo") {

            QUnit.ok(true, "in depositInfo state");
            var currentTransaction = atm.currentTransaction;
            currentTransaction.amount = 120;
            QUnit.ok(currentTransaction.ready, "we are ready to execute");
            currentTransaction.execute();
            QUnit.equal(atm.state, "depositInfo", "in deposit state until successful");

        } else if (newVal === "successfulTransaction") {

            QUnit.ok(true, "in successfulTransaction state");
            QUnit.start();

        }
    });
});

When complete, you should be able to enter a deposit amount and see that the transaction was successful.

Withdrawal Info page

In this section, we will:

  • Allow the user to enter the amount of a withdrawal and go to the Successful Transaction page.

Update the HTML tab to:

  • Add a Withdraw page that works very similar to the Deposit page.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="chooseDeposit()">Deposit</li>
                <li on:click="chooseWithdraw()">Withdraw</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
        <p>Please pick your account:</p>
        {{#if(accountsPromise.isPending)}}
            <div class='warn'>
                <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    Loading Accounts…
                </p>
            </div>
        {{else}}
            <ul>
                {{#each(accountsPromise.value)}}
                    <li on:click="chooseAccount(.)">{{name}} - ${{balance}}</li>
                {{/each}}
            </ul>
        {{/if}}
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
        <p>
            How much would you like to deposit
            into {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="deposit" value:bind="currentTransaction.amount"/>
        </p>

        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Deposit
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
        <p>
            How much would you like to withdraw
            from {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="withdrawl" value:bind="currentTransaction.amount"/>
        </p>
        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Withdraw
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

When complete, you should be able to enter a withdrawal amount and see that the transaction was successful.

Optional: Challenge yourself by adding a test for the withdrawalInfo state of an atm instance. Consider the progression of states needed to make it to the withdrawalInfo state. How is it different from the ATM basics test we already have?

Transaction Successful page

In this section, we will:

  • Show the result of the transaction.

Update the HTML tab to:

  • List out the account balance.
  • Add buttons to:
    • start another transaction, or
    • print a receipt and exit the ATM (printReceiptAndExit will be implemented in the next section).
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="chooseDeposit()">Deposit</li>
                <li on:click="chooseWithdraw()">Withdraw</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
        <p>Please pick your account:</p>
        {{#if(accountsPromise.isPending)}}
            <div class='warn'>
                <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    Loading Accounts…
                </p>
            </div>
        {{else}}
            <ul>
                {{#each(accountsPromise.value)}}
                    <li on:click="chooseAccount(.)">{{name}} - ${{balance}}</li>
                {{/each}}
            </ul>
        {{/if}}
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
        <p>
            How much would you like to deposit
            into {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="deposit" value:bind="currentTransaction.amount"/>
        </p>

        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Deposit
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
        <p>
            How much would you like to withdraw
            from {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="withdrawl" value:bind="currentTransaction.amount"/>
        </p>
        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Withdraw
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
        <p>
            {{currentTransaction.account.name}} has
            ${{currentTransaction.account.balance}}.
        </p>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="removeTransaction()">Another transaction</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

When complete, you should be able to make a deposit or withdrawal, see the updated account balance, then start another transaction.

Printing Recipe page and test

In this section, we will make it possible to:

  • See a receipt of all transactions
  • Exit the ATM.

Update the HTML tab to:

  • List out all the transactions the user has completed.
  • List out the final value of all accounts.
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="CanJS 3.0 - ATM Guide - Setup">
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>

</head>
<body>

<script type='text/stache' id='atm-template'>
<div class="screen">
    <div class="screen-content">
        <div class="screen-glass">

{{#switch(state)}}
    {{#case("readingCard")}}
        <h2>Reading Card</h2>
        <p>Welcome to canATM where there are <strong>never</strong>
          fees!</p>
        </p>
        <p>
            Enter Card Number:
            <input name="card" on:enter="cardNumber(%element.value)"/>
        </p>
    {{/case}}

    {{#case("readingPin")}}
        <h2>Reading Pin</h2>
        <p>
            Enter Pin Number:
            <input name="pin" type="password"
                autofocus
                {{#is(card.state, "verifying")}}DISABLED{{/is}}
                on:enter="pinNumber(%element.value)"/>

            {{#is(card.state, "verifying")}}
                <div class='warn'>
                    <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    verifying
                    </p>
                </div>
            {{/is}}
        </p>
        <a href="javascript://" on:click="exit()">exit</a>
    {{/case}}

    {{#case("choosingTransaction")}}
        <h2>Choose Transaction</h2>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="chooseDeposit()">Deposit</li>
                <li on:click="chooseWithdraw()">Withdraw</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("pickingAccount")}}
        <h2>Pick Account</h2>
        <p>Please pick your account:</p>
        {{#if(accountsPromise.isPending)}}
            <div class='warn'>
                <p>
                    <img src="https://canjs.com/docs/images/loader.gif"/>
                    Loading Accounts…
                </p>
            </div>
        {{else}}
            <ul>
                {{#each(accountsPromise.value)}}
                    <li on:click="chooseAccount(.)">{{name}} - ${{balance}}</li>
                {{/each}}
            </ul>
        {{/if}}
    {{/case}}

    {{#case("depositInfo")}}
        <h2>Deposit</h2>
        <p>
            How much would you like to deposit
            into {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="deposit" value:bind="currentTransaction.amount"/>
        </p>

        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Deposit
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("withdrawalInfo")}}
        <h2>Withdraw</h2>
        <p>
            How much would you like to withdraw
            from {{currentTransaction.account.name}}
            (${{currentTransaction.account.balance}})?

            <input name="withdrawl" value:bind="currentTransaction.amount"/>
        </p>
        {{#eq(currentTransaction.state, "executing")}}
            <div class='warn'>
                <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                executing
                </p>
            </div>
        {{else}}
            <button on:click="currentTransaction.execute()"
                {{^eq currentTransaction.state "ready"}}DISABLED{{/eq}}>
                Withdraw
            </button>
            <a href="javascript://" on:click="removeTransaction()">cancel</a>
        {{/eq}}
    {{/case}}

    {{#case("successfulTransaction")}}
        <h2>Transaction Successful!</h2>
        <p>
            {{currentTransaction.account.name}} has
            ${{currentTransaction.account.balance}}.
        </p>
        <p>What would you like to do?</p>
        <nav>
            <ul>
                <li on:click="removeTransaction()">Another transaction</li>
                <li on:click="printReceiptAndExit()">Exit</li>
            </ul>
        </nav>
    {{/case}}

    {{#case("printingReceipt")}}
        <h2>Printing Receipt</h2>
        <h3>Transactions</h3>
        <ul>
            {{#if(transactions.length)}}
                {{#each(transactions)}}
                    <li>{{actionName(this)}} ${{amount}} {{actionPrep(this)}} {{account.name}}</li>
                {{/each}}
            {{else}}
                <li>None</li>
            {{/if}}
        </ul>
        <h3>Accounts</h3>
        <ul>
            {{#each(accountsPromise.value)}}
                <li on:click="chooseAccount(.)">{{name}} - ${{balance}}</li>
            {{/each}}
        </ul>
        <div class='warn'>
            <p>
                <img src="https://canjs.com/docs/images/loader.gif"/>
                printing
            </p>
        </div>
    {{/case}}

    {{#default}}
        <h2>Error</h2>
        <p>Invalid state - {{state}}</p>
    {{/default}}

{{/switch}}

        </div>
    </div>
</div>
</script>

<script type='text/stache' id='app-template'>
<div class="title">
    <h1>canATM</h1>
</div>
<atm-machine/>
</script>

<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/can@3/dist/global/can.all.js"></script>

<div id="qunit"></div>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.12.0.css">
<script src="https://code.jquery.com/qunit/qunit-1.12.0.js"></script>

</body>
</html>

Update the ATM view model in the JavaScript tab to:

  • Add a printingReceipt and receiptTime property.
  • Change the state to "printingReceipt" when printingReceipt is true.
  • Make .exit set printingReceipt to null.
  • Add a printReceiptAndExit method that:
    • clears the current transaction, which will add the currentTransaction to the list of transactions.
    • sets printingReceipt to true for printingReceipt time.

Update the ATM basics test in the JavaScript tab to:

  • Shorten the default receiptTime so the tests move quickly.
  • Call printReceiptAndExit and make sure that the state changes to "printingReceipt" and then to "readingCard" and ensure that sensitive information is cleared from the ATM.
// ========================================
// CODE
// ========================================

can.fixture({
    "/verifyCard": function(request, response) {
        if (!request.data || !request.data.number || !request.data.pin) {
            response(400, {});
        } else {
            return {};
        }
    },
    "/accounts": function() {
        return {
            data: [{
                balance: 100,
                id: 1,
                name: "checking"
            }, {
                balance: 10000,
                id: 2,
                name: "savings"
            }]
        };
    },
    "/deposit": function() {
        return {};
    },
    "/withdrawal": function() {
        return {};
    }
});
can.fixture.delay = 1000;

var Card = can.DefineMap.extend({
    number: "string",
    pin: "string",
    state: {
        value: "unverified",
        serialize: false
    },
    verify: function() {

        this.state = "verifying";

        var self = this;
        return can.ajax({
            type: "POST",
            url: "/verifyCard",
            data: this.serialize()
        }).then(
            function() {
                self.state = "verified";
                return self;
            },
            function() {
                self.state = "invalid";
                return self;
            });
    }
});

var Account = can.DefineMap.extend("Account", {
    id: "number",
    balance: "number",
    name: "string"
});
Account.List = can.DefineList.extend("AccountList", {
    "*": Account
});

can.connect.baseMap({
    url: "/accounts",
    Map: Account,
    List: Account.List,
    name: "accounts"
});

var Transaction = can.DefineMap.extend({
    account: Account,
    card: Card,
    executing: {
        type: "boolean",
        value: false
    },
    executed: {
        type: "boolean",
        value: false
    },
    rejected: "any",
    get ready(){
        throw new Error("Transaction::ready must be provided by extended type");
    },
    get state() {
        if (this.rejected) {
            return "rejected";
        }
        if (this.executed) {
            return "executed";
        }
        if (this.executing) {
            return "executing";
        }
        // make sure there’s an amount, account, and card
        if (this.ready) {
            return "ready";
        }
        return "invalid";
    },
    executeStart: function(){
        throw new Error("Transaction::executeStart must be provided by extended type");
    },
    executeEnd: function(){
        throw new Error("Transaction::executeEnd must be provided by extended type");
    },
    execute: function() {
        if (this.state === "ready") {

            this.executing = true;

            var def = this.executeStart(),
                self = this;

            def.then(function() {
                can.batch.start();
                self.set({
                    executing: false,
                    executed: true
                });
                self.executeEnd();
                can.batch.stop();
            }, function(reason){
                self.set({
                    executing: false,
                    executed: true,
                    rejected: reason
                });
            });
        }
    }
});

var Deposit = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/deposit",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance + this.amount;
    }
});

var Withdrawal = Transaction.extend({
    amount: "number",
    get ready() {
        return this.amount > 0 &&
            this.account &&
            this.card;
    },
    executeStart: function() {
        return can.ajax({
            type: "POST",
            url: "/withdrawal",
            data: {
                card: this.card.serialize(),
                accountId: this.account.id,
                amount: this.amount
            }
        });
    },
    executeEnd: function(data) {
        this.account.balance = this.account.balance - this.amount;
    }
});

var ATM = can.DefineMap.extend({
    // stateful properties
    card: Card,
    accountsPromise: "any",
    transactions: can.DefineList,
    currentTransaction: {
        set: function(newTransaction) {
            var currentTransaction = this.currentTransaction;
            if (this.transactions && currentTransaction &&
                currentTransaction.state === "executed") {

                this.transactions.push(currentTransaction);
            }
            return newTransaction;
        }
    },
    printingReceipt: "boolean",
    receiptTime: {
        value: 5000,
        type: "number"
    },

    // derived properties
    get state(){
        if (this.printingReceipt) {
            return "printingReceipt";
        }
        if (this.currentTransaction) {
            if (this.currentTransaction.state === "executed") {
                return "successfulTransaction";
            }

            if (this.currentTransaction.account) {
                if (this.currentTransaction instanceof Deposit) {
                    return "depositInfo";
                } else {
                    return "withdrawalInfo";
                }
            }

            return "pickingAccount";
        }

        if(this.card) {
            if (this.card.state === "verified") {
                return "choosingTransaction";
            }
            return "readingPin";
        }
        return "readingCard";
    },

    // methods
    cardNumber: function(number) {
        this.card = new Card({
            number: number
        });
    },
    pinNumber: function(pin) {
        var card = this.card;

        card.pin = pin;
        this.transactions = new can.DefineList();
        this.accountsPromise = card.verify().then(function(card) {

            return Account.getList(card.serialize());
        });
    },
    exit: function(){
        this.set({
            card: null,
            accountsPromise: null,
            transactions: null,
            currentTransaction: null,
            printingReceipt: null
        });
    },
    printReceiptAndExit: function() {
        this.currentTransaction = null;
        this.printingReceipt = true;
        var self = this;
        setTimeout(function() {
            self.exit();
        }, this.receiptTime);
    },
    chooseDeposit: function() {
        this.currentTransaction = new Deposit({
            card: this.card
        });
    },
    chooseWithdraw: function() {
        this.currentTransaction = new Withdrawal({
            card: this.card
        });
    },
    chooseAccount: function(account) {
        this.currentTransaction.account = account;
    },
    removeTransaction: function() {
        this.currentTransaction = null;
    }
});

can.Component.extend({
    tag: "atm-machine",
    view: can.stache.from("atm-template"),
    ViewModel: ATM,
});

document.body.insertBefore(
    can.stache.from("app-template")({}),
    document.body.firstChild
);

// ========================================
// TESTS
// ========================================

QUnit.module("ATM system", {
    setup: function() {
        can.fixture.delay = 1;
    },
    teardown: function() {
        can.fixture.delay = 2000;
    }
});

QUnit.asyncTest("Valid Card", function() {

    var c = new Card({
        number: "01234567890",
        pin: 1234
    });

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "verified", "card is verified");

        QUnit.start();
    });
});

QUnit.asyncTest("Invalid Card", function() {

    var c = new Card({});

    QUnit.equal(c.state, "unverified");

    c.verify();

    QUnit.equal(c.state, "verifying", "card is verifying");

    c.on("state", function(ev, newVal) {

        QUnit.equal(newVal, "invalid", "card is invalid");

        QUnit.start();
    });
});

QUnit.asyncTest("Deposit", 6, function() {

    var card = new Card({
        number: "0123456789",
        pin: "1122"
    });

    var deposit = new Deposit({
        amount: 100,
        card: card
    });

    equal(deposit.state, "invalid");

    var startingBalance;

    Account.getList(card.serialize()).then(function(accounts) {
        QUnit.ok(true, "got accounts");
        deposit.account = accounts[0];
        startingBalance = accounts[0].balance;
    });

    deposit.on("state", function(ev, newVal) {
        if (newVal === "ready") {

            QUnit.ok(true, "deposit is ready");
            deposit.execute();

        } else if (newVal === "executing") {

            QUnit.ok(true, "executing a deposit");

        } else if (newVal === "executed") {

            QUnit.ok(true, "executed a deposit");
            equal(deposit.account.balance, 100 + startingBalance);
            start();

        }
    });
});

QUnit.asyncTest("ATM basics", function() {

    var atm = new ATM();

    equal(atm.state, "readingCard", "starts at reading card state");

    atm.cardNumber("01233456789");

    equal(atm.state, "readingPin", "moves to reading card state");

    atm.pinNumber("1234");

    ok(atm.state, "readingPin", "remain in the reading pin state until verifyied");

    atm.on("state", function(ev, newVal) {

        if (newVal === "choosingTransaction") {

            QUnit.ok(true, "in choosingTransaction");
            atm.chooseDeposit();

        } else if (newVal === "pickingAccount") {

            QUnit.ok(true, "in picking account state");
            atm.accountsPromise.then(function(accounts){
                atm.chooseAccount(accounts[0]);
            });

        } else if (newVal === "depositInfo") {

            QUnit.ok(true, "in depositInfo state");
            var currentTransaction = atm.currentTransaction;
            currentTransaction.amount = 120;
            QUnit.ok(currentTransaction.ready, "we are ready to execute");
            currentTransaction.execute();
            QUnit.equal(atm.state, "depositInfo", "in deposit state until successful");

        } else if (newVal === "successfulTransaction") {

            QUnit.ok(true, "in successfulTransaction state");
            atm.receiptTime = 100;
            atm.printReceiptAndExit();

        } else if (newVal === "printingReceipt") {

            QUnit.ok(true, "in printingReceipt state");

        } else if (newVal === "readingCard") {

            QUnit.ok(true, "in readingCard state");
            QUnit.ok(!atm.card, "card is removed");
            QUnit.ok(!atm.transactions, "transactions removed");
            QUnit.start();

        }
    });
});

When complete, you have a working ATM! Cha-ching!

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 3.14.1.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news