Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Updated the statechart framework to provide routing support

  • Loading branch information...
commit c6f258a922f0ac80e7a461012bb0cbd2715d4f46 1 parent 1680a73
@mlcohen mlcohen authored
Showing with 1,445 additions and 54 deletions.
  1. +1 −1  Buildfile
  2. +12 −0 apps/statechart_routing/Buildfile
  3. +11 −0 apps/statechart_routing/controllers/login_controller.js
  4. +7 −0 apps/statechart_routing/controllers/main_controller.js
  5. +17 −0 apps/statechart_routing/controllers/statechart_controller.js
  6. +25 −0 apps/statechart_routing/core.js
  7. +15 −0 apps/statechart_routing/main.js
  8. +18 −0 apps/statechart_routing/resources/_theme.css
  9. +14 −0 apps/statechart_routing/resources/bar_page.js
  10. +14 −0 apps/statechart_routing/resources/foo_page.js
  11. +9 −0 apps/statechart_routing/resources/loading.rhtml
  12. +61 −0 apps/statechart_routing/resources/login_page.js
  13. +46 −0 apps/statechart_routing/resources/main_page.js
  14. +76 −0 apps/statechart_routing/statechart.js
  15. +27 −0 apps/statechart_routing/theme.js
  16. +113 −0 frameworks/statechart/mixins/statechart_delegate.js
  17. +194 −10 frameworks/statechart/system/state.js
  18. +78 −0 frameworks/statechart/system/state_route_handler_context.js
  19. +52 −10 frameworks/statechart/system/statechart.js
  20. +161 −0 frameworks/statechart/tests/state/methods/route_triggered.js
  21. +5 −33 frameworks/statechart/tests/state_transitioning/history_state/standard/without_concurrent_states/context.js
  22. +213 −0 frameworks/statechart/tests/state_transitioning/routing/with_concurrent_states/basic.js
  23. +212 −0 frameworks/statechart/tests/state_transitioning/routing/without_concurrent_states/basic.js
  24. +64 −0 frameworks/statechart/tests/system/state_route_handler_context/methods/retry.js
View
2  Buildfile
@@ -40,7 +40,7 @@ config :foundation, :required => [:routing, :core_foundation, :datetime, :'
config :datastore, :required => [:runtime, :datetime]
config :desktop, :required => [:foundation]
config :media, :required => [:desktop]
-config :statechart, :required => [:core_foundation], :test_required => [:core_foundation, :desktop]
+config :statechart, :required => [:core_foundation], :test_required => [:core_foundation, :desktop, :routing]
config :ajax, :required => [:runtime, :core_foundation]
config :"experimental/split_view", :test_required => [:desktop]
View
12 apps/statechart_routing/Buildfile
@@ -0,0 +1,12 @@
+# ==========================================================================
+# Project: Test
+# Copyright: @2011 My Company, Inc.
+# ==========================================================================
+
+# This is your Buildfile for your app, Test. This tells SproutCore
+# how to build your app. These settings override those in your project
+# Buildfile, which contains default settings for all apps in your project.
+
+# It is better to add :required targets here than in the global Buildfile.
+config :test, :required => :sproutcore
+
View
11 apps/statechart_routing/controllers/login_controller.js
@@ -0,0 +1,11 @@
+/*globals Test */
+
+Test.loginController = SC.Object.create({
+
+ username: null,
+
+ password: null,
+
+ loggedIn: NO
+
+});
View
7 apps/statechart_routing/controllers/main_controller.js
@@ -0,0 +1,7 @@
+/*globals Test */
+
+Test.mainController = SC.Object.create({
+
+ mode: null
+
+});
View
17 apps/statechart_routing/controllers/statechart_controller.js
@@ -0,0 +1,17 @@
+/*globals Test */
+
+Test.statechartController = SC.Object.create(SC.StatechartDelegate, {
+
+ lastRouteContext: null,
+
+ statechartShouldStateHandleTriggeredRoute: function(statechart, state, context) {
+ return Test.loginController.get('loggedIn');
+ },
+
+ statechartStateCancelledHandlingTriggeredRoute: function(statechart, state, context) {
+ this.set('lastRouteContext', context);
+ SC.routes.set('location', null);
+ statechart.gotoState('loggedOutState');
+ }
+
+});
View
25 apps/statechart_routing/core.js
@@ -0,0 +1,25 @@
+// ==========================================================================
+// Project: Test
+// Copyright: @2011 My Company, Inc.
+// ==========================================================================
+/*globals Test */
+
+/** @namespace
+
+ My cool new app. Describe your application.
+
+ @extends SC.Object
+*/
+Test = SC.Application.create(
+ /** @scope Test.prototype */ {
+
+ NAMESPACE: 'Test',
+ VERSION: '0.1.0',
+
+ store: SC.Store.create().from(SC.Record.fixtures),
+
+ MODE_FOO: 0,
+
+ MODE_BAR: 1
+
+}) ;
View
15 apps/statechart_routing/main.js
@@ -0,0 +1,15 @@
+// ==========================================================================
+// Project: Test
+// Copyright: @2011 My Company, Inc.
+// ==========================================================================
+/*globals Test */
+
+Test.main = function main() {
+
+ var sc = Test.statechart;
+ SC.RootResponder.responder.set('defaultResponder', sc);
+ sc.initStatechart();
+
+} ;
+
+function main() { Test.main(); }
View
18 apps/statechart_routing/resources/_theme.css
@@ -0,0 +1,18 @@
+/*
+ This defines the global $theme variable for use inside your CSS.
+ The $theme variable holds the CSS class names for your theme.
+
+ You can then theme your app by using CSS like this:
+
+ $theme.button {
+ color: blue;
+ @include slices('white-button.png', $left: 3, $right: 3);
+ }
+
+ Any _theme.css file is prepended to all files inside its directory,
+ and any subdirectories that don't define their own _theme.css file.
+
+ This allows you to give different directories different values for
+ $theme if you wish.
+*/
+$theme: '.ace.test';
View
14 apps/statechart_routing/resources/bar_page.js
@@ -0,0 +1,14 @@
+/*globals Test */
+
+Test.barPage = SC.Page.create({
+
+ mainView: SC.View.design({
+ layout: { top: 0, bottom: 0, left: 0, right: 0 },
+ childViews: 'labelView'.w(),
+ labelView: SC.LabelView.design({
+ layout: { centerX: 0, centerY: 0, height: 24, width: 100 },
+ value: 'In Bar Mode'
+ })
+ })
+
+});
View
14 apps/statechart_routing/resources/foo_page.js
@@ -0,0 +1,14 @@
+/*globals Test */
+
+Test.fooPage = SC.Page.create({
+
+ mainView: SC.View.design({
+ layout: { top: 0, bottom: 0, left: 0, right: 0 },
+ childViews: 'labelView'.w(),
+ labelView: SC.LabelView.design({
+ layout: { centerX: 0, centerY: 0, height: 24, width: 100 },
+ value: 'In Foo Mode'
+ })
+ })
+
+});
View
9 apps/statechart_routing/resources/loading.rhtml
@@ -0,0 +1,9 @@
+<% content_for :loading do %>
+<% # Any HTML in this file will be visible on screen while your page loads
+ # its application JavaScript. SproutCore applications are optimized for
+ # caching and startup very fast, so your users will often only see this
+ # content for a brief moment on their first app load, if at all.
+%>
+<p class="loading">Loading...<p>
+
+<% end %>
View
61 apps/statechart_routing/resources/login_page.js
@@ -0,0 +1,61 @@
+/*globals Test */
+
+Test.loginPage = SC.Page.create({
+
+ mainPane: SC.MainPane.design({
+
+ childViews: 'mainView'.w(),
+
+ mainView: SC.View.design({
+
+ childViews: 'headerView usernameView passwordView footerView'.w(),
+
+ layout: { centerX: 0, centerY: 0, width: 300, height: 150 },
+
+ headerView: SC.LabelView.design({
+ layout: { top: 0, left: 0, right: 0, height: 30 },
+ value: 'Login',
+ textAlign: SC.ALIGN_CENTER
+ }),
+
+ usernameView: SC.View.design({
+ childViews: 'labelView textfieldView'.w(),
+ layout: { top: 30, left: 0, right: 0, height: 30 },
+ labelView: SC.LabelView.design({
+ layout: { centerX: 0, left: 0, height: 24, width: 80 },
+ value: "Username:"
+ }),
+ textfieldView: SC.TextFieldView.design({
+ layout: { centerX: 0, left: 90, right: 0, height: 24 },
+ valueBinding: 'Test.loginController.username'
+ })
+ }),
+
+ passwordView: SC.View.design({
+ childViews: 'labelView textfieldView'.w(),
+ layout: { top: 60, left: 0, right: 0, height: 30 },
+ labelView: SC.LabelView.design({
+ layout: { centerX: 0, left: 0, height: 24, width: 80 },
+ value: "Password:"
+ }),
+ textfieldView: SC.TextFieldView.design({
+ layout: { centerX: 0, left: 90, right: 0, height: 24 },
+ valueBinding: 'Test.loginController.password'
+ })
+ }),
+
+ footerView: SC.View.design({
+ childViews: 'buttonView'.w(),
+ layout: { top: 120, left: 0, right: 0, bottom: 0 },
+ buttonView: SC.ButtonView.design({
+ layout: { height: 24, width: 80, right: 0, bottom: 0 },
+ title: 'Login',
+ action: 'login'
+ })
+ })
+
+ })
+
+ })
+
+});
View
46 apps/statechart_routing/resources/main_page.js
@@ -0,0 +1,46 @@
+// ==========================================================================
+// Project: Test - mainPage
+// Copyright: @2011 My Company, Inc.
+// ==========================================================================
+/*globals Test */
+
+Test.mainPage = SC.Page.create({
+
+ fooView: SC.outlet('Test.fooPage.mainView'),
+
+ barView: SC.outlet('Test.barPage.mainView'),
+
+ emptyView: SC.View.design(),
+
+ mainPane: SC.MainPane.design({
+
+ childViews: 'headerView containerView'.w(),
+
+ headerView: SC.View.design({
+ layout: { top: 0, left: 0, right: 0, height: 50 },
+ childViews: 'switchModeView'.w(),
+ switchModeView: SC.SegmentedView.design({
+ layout: { centerX: 0, centerY: 0, height: 24, width: 100 },
+ items: [
+ { title: 'Foo', value: Test.MODE_FOO, action: 'switchToFooMode' },
+ { title: 'Bar', value: Test.MODE_BAR, action: 'switchToBarMode' }
+ ],
+ itemTitleKey: 'title',
+ itemValueKey: 'value',
+ itemActionKey: 'action',
+ valueBinding: SC.Binding.oneWay('Test.mainController.mode')
+ })
+ }),
+
+ containerView: SC.ContainerView.design({
+ layout: { top: 50, left: 0, right: 0, bottom: 0 },
+ contentViewBinding: SC.Binding.transform(function(mode) {
+ if (mode === Test.MODE_FOO) return Test.fooPage.get('mainView');
+ if (mode === Test.MODE_BAR) return Test.barPage.get('mainView');
+ return Test.mainPage.get('emptyView');
+ }).oneWay('Test.mainController.mode')
+ })
+
+ })
+
+});
View
76 apps/statechart_routing/statechart.js
@@ -0,0 +1,76 @@
+/*globals Test */
+
+sc_require('controllers/statechart_controller');
+
+Test.statechart = SC.Statechart.create({
+
+ trace: YES,
+
+ initialState: 'loggedOutState',
+
+ delegate: Test.statechartController,
+
+ loggedOutState: SC.State.design({
+
+ enterState: function() {
+ Test.loginPage.get('mainPane').append();
+ },
+
+ exitState: function() {
+ Test.loginPage.get('mainPane').remove();
+ },
+
+ login: function() {
+ Test.loginController.set('loggedIn', YES);
+ var del = this.get('statechartDelegate');
+ var ctx = del.get('lastRouteContext');
+ if (ctx) {
+ ctx.retry();
+ } else {
+ this.gotoState('loggedInState');
+ }
+ }
+
+ }),
+
+ loggedInState: SC.State.design({
+
+ enterState: function() {
+ Test.mainPage.get('mainPane').append();
+ },
+
+ switchToFooMode: function() {
+ this.gotoState('fooState');
+ },
+
+ switchToBarMode: function() {
+ this.gotoState('barState');
+ },
+
+ initialSubstate: 'fooState',
+
+ fooState: SC.State.design({
+
+ representRoute: 'foo',
+
+ enterState: function() {
+ this.set('location', 'foo');
+ Test.mainController.set('mode', Test.MODE_FOO);
+ }
+
+ }),
+
+ barState: SC.State.design({
+
+ representRoute: 'bar/:id',
+
+ enterState: function() {
+ this.set('location', 'bar/4?blah=xml');
+ Test.mainController.set('mode', Test.MODE_BAR);
+ }
+
+ })
+
+ })
+
+});
View
27 apps/statechart_routing/theme.js
@@ -0,0 +1,27 @@
+// ==========================================================================
+// Project: Test
+// Copyright: @2011 My Company, Inc.
+// ==========================================================================
+/*globals Test */
+
+// This is the theme that defines how your app renders.
+//
+// Your app is given its own theme so it is easier and less
+// messy for you to override specific things just for your
+// app.
+//
+// You don't have to create the whole theme on your own, though:
+// your app's theme is based on SproutCore's Ace theme.
+//
+// NOTE: if you want to change the theme this one is based on, don't
+// forget to change the :css_theme property in your buildfile.
+Test.Theme = SC.AceTheme.create({
+ name: 'test'
+});
+
+// SproutCore needs to know that your app's theme exists
+SC.Theme.addTheme(Test.Theme);
+
+// Setting it as the default theme makes every pane SproutCore
+// creates default to this theme unless otherwise specified.
+SC.defaultTheme = 'test';
View
113 frameworks/statechart/mixins/statechart_delegate.js
@@ -0,0 +1,113 @@
+// ==========================================================================
+// Project: SC.Statechart - A Statechart Framework for SproutCore
+// Copyright: ©2010, 2011 Michael Cohen, and contributors.
+// Portions @2011 Apple Inc. All rights reserved.
+// License: Licensed under MIT license (see license.js)
+// ==========================================================================
+
+/*globals SC */
+
+/**
+ @class
+
+ Apply to objects that are to represent a delegate for a SC.Statechart object.
+ When assigned to a statechart, the statechart and its associated states will
+ use the delegate in order to make various decisions.
+
+ @see SC.Statechart#delegate
+
+ @author Michael Cohen
+*/
+
+SC.StatechartDelegate = /** @scope SC.StatechartDelegate.prototype */ {
+
+ // Walk like a duck
+ isStatechartDelegate: YES,
+
+ // Route Handling Management
+
+ /**
+ Called to update the application's current location.
+
+ The location provided is dependent upon the application's underlying
+ routing mechanism.
+
+ @param {SC.StatechartManager} statechart the statechart
+ @param {String|Hash} location the new location
+ @param {SC.State} state the state requesting the location update
+ */
+ statechartUpdateLocationForState: function(statechart, location, state) {
+ SC.routes.set('location', location);
+ },
+
+ /**
+ Called to acquire the application's current location.
+
+ @param {SC.StatechartManager} statechart the statechart
+ @param {SC.State} state the state requesting the location
+ @returns {String} the location
+ */
+ statechartAcquireLocationForState: function(statechart, state) {
+ return SC.routes.get('location');
+ },
+
+ /**
+ Used to bind a state's handler to a route. When the application's location
+ matches the given route, the state's handler is to be invoked.
+
+ The statechart and states remain completely independent of how the underlying
+ routing mechanism works thereby providing a looser coupling and more flexibility
+ in how routing is to work. Given this flexiblity, it is important that a route
+ assigned (using the {@link SC.State#representRoute} property) to a state strictly
+ conforms to the underlying routing mechanism's criteria in order for the given
+ handler to be properly invoked.
+
+ By default the {@link SC.routes} mechanism is used to bind the state's handler with
+ the given route.
+
+ @param {SC.StatechartManager} statechart the statechart
+ @param {SC.State} state the state to bind the route to
+ @param {String|Hash} route the route that is to be bound to the state
+ @param {Function|String} handler the method on the state to be invoked when the route
+ gets triggered.
+
+ @see SC.State#representRoute
+ */
+ statechartBindStateToRoute: function(statechart, state, route, handler) {
+ SC.routes.add(route, state, handler);
+ },
+
+ /**
+ Invoked by a state that has been notified to handle a triggered route. The state
+ asks if it should go ahead an actually handle the triggered route. If no then
+ the state's handler will no longer continue and finish by calling this delegate's
+ `statechartStateCancelledHandlingTriggeredRoute` method. If yes then the state will
+ continue with handling the triggered route.
+
+ By default `YES` is returned.
+
+ @param {SC.StatechartManager} statechart the statechart
+ @param {SC.State} state the state making the request
+ @param {SC.StateRouteHandlerContext} routeContext contextual information about the handling
+ of a route
+
+ @see #statechartStateCancelledHandlingTriggeredRoute
+ */
+ statechartShouldStateHandleTriggeredRoute: function(statechart, state, context) {
+ return YES;
+ },
+
+ /**
+ Invoked by a state that has been informed by the delegate to not handle a triggered route.
+ Used this for any additional clean up or processing that you may wish to perform.
+
+ @param {SC.StatechartManager} statechart the statechart
+ @param {SC.State} state the state making the request
+ @param {SC.StateRouteHandlerContext} routeContext contextual information about the handling
+ of a route
+
+ @see #statechartShouldStateHandleTriggeredRoute
+ */
+ statechartStateCancelledHandlingTriggeredRoute: function(statechart, state, context) { }
+
+};
View
204 frameworks/statechart/system/state.js
@@ -110,6 +110,25 @@ SC.State = SC.Object.extend(
*/
enteredSubstates: null,
+ /**
+ Can optionally assign what route this state is to represent.
+
+ If assigned then this state will be notified to handle the route when triggered
+ any time the app's location changes and matches this state's assigned route.
+ The handler invoked is this state's {@link #routeTriggered} method.
+
+ The value assigned to this property is dependent on the underlying routing
+ mechanism used by the application. The default routing mechanism is to use
+ SC.routes.
+
+ @property {String|Hash}
+
+ @see #routeTriggered
+ @see #location
+ @see SC.StatechartDelegate
+ */
+ representRoute: null,
+
/**
Indicates if this state should trace actions. Useful for debugging
purposes. Managed by the statechart.
@@ -139,6 +158,49 @@ SC.State = SC.Object.extend(
return owner ? owner : sc;
}.property().cacheable(),
+ /**
+ Returns the statechart's assigned delegate. A statechart delegate is one
+ that adheres to the {@link SC.StatechartDelegate} mixin.
+
+ @property {SC.Object}
+
+ @see SC.StatechartDelegate
+ */
+ statechartDelegate: function() {
+ return this.getPath('statechart.statechartDelegate');
+ }.property().cacheable(),
+
+ /**
+ A volatile property used to get and set the app's current location.
+
+ This computed property defers to the the statechart's delegate to
+ actually update and acquire the app's location.
+
+ Note: Binding for this pariticular case is discouraged since in most
+ cases we need the location value immediately. If we were to use
+ bindings then the location value wouldn't be updated until at least
+ the end of one run loop. It is also advised that the delegate not
+ have its `statechartUpdateLocationForState` and
+ `statechartAcquireLocationForState` methods implemented where bindings
+ are used since they will inadvertenly stall the location value from
+ propogating immediately.
+
+ @property {String}
+
+ @see SC.StatechartDelegate#statechartUpdateLocationForState
+ @see SC.StatechartDelegate#statechartAcquireLocationForState
+ */
+ location: function(key, value) {
+ var sc = this.get('statechart'),
+ del = this.get('statechartDelegate');
+
+ if (value !== undefined) {
+ del.statechartUpdateLocationForState(sc, value, this);
+ }
+
+ return del.statechartAcquireLocationForState(sc, this);
+ }.property().idempotent(),
+
init: function() {
sc_super();
@@ -148,6 +210,8 @@ SC.State = SC.Object.extend(
this._registeredStateObserveHandlers = {};
this._registeredSubstatePaths = {};
this._registeredSubstates = [];
+ this._isEnteringState = NO;
+ this._isExitingState = NO;
// Setting up observes this way is faster then using .observes,
// which adds a noticable increase in initialization time.
@@ -177,7 +241,7 @@ SC.State = SC.Object.extend(
substates.forEach(function(state) {
state.destroy();
});
- }
+ }
this._teardownAllStateObserveHandlers();
@@ -209,6 +273,7 @@ SC.State = SC.Object.extend(
if (this.get('stateIsInitialized')) return;
this._registerWithParentStates();
+ this._setupRouteHandling();
var key = null,
value = null,
@@ -291,6 +356,105 @@ SC.State = SC.Object.extend(
this.set('stateIsInitialized', YES);
},
+ /** @private
+
+ Used to bind this state with a route this state is to represent if a route has been assigned.
+
+ When invoked, the method will delegate the actual binding strategy to the statechart delegate
+ via the delegate's {@link SC.StatechartDelegate#statechartBindStateToRoute} method.
+
+ Note that a state cannot be bound to a route if this state is a concurrent state.
+
+ @see #representRoute
+ @see SC.StatechartDelegate#statechartBindStateToRoute
+ */
+ _setupRouteHandling: function() {
+ var route = this.get('representRoute'),
+ sc = this.get('statechart'),
+ del = this.get('statechartDelegate');
+
+ if (!route) return;
+
+ if (this.get('isConcurrentState')) {
+ this.stateLogError("State %@ cannot handle route '%@' since state is concurrent".fmt(this, route));
+ return;
+ }
+
+ del.statechartBindStateToRoute(sc, this, route, this.routeTriggered);
+ },
+
+ /**
+ Main handler that gets triggered whenever the app's location matches this state's assigned
+ route.
+
+ When invoked the handler will first refer to the statechart delegate to determine if it
+ should actually handle the route via the delegate's
+ {@see SC.StatechartDelegate#statechartShouldStateHandleTriggeredRoute} method. If the
+ delegate allows the handling of the route then the state will continue on with handling
+ the triggered route by calling the state's {@link #handleTriggeredRoute} method, otherwise
+ the state will cancel the handling and inform the delegate through the delegate's
+ {@see SC.StatechartDelegate#statechartStateCancelledHandlingRoute} method.
+
+ The handler will create a state route context ({@link SC.StateRouteContext}) object
+ that packages information about what is being currently handled. This context object gets
+ passed along to the delegate's invoked methods as well as the state transition process.
+
+ Note that this method is not intended to be directly called or overridden.
+
+ @see #representRoute
+ @see SC.StatechartDelegate#statechartShouldStateHandleRoute
+ @see SC.StatechartDelegate#statechartStateCancelledHandlingRoute
+ @see #createStateRouteHandlerContext
+ @see #handleTriggeredRoute
+ */
+ routeTriggered: function(params) {
+ if (this._isEnteringState) return;
+
+ var sc = this.get('statechart'),
+ del = this.get('statechartDelegate'),
+ loc = this.get('location');
+
+ var attr = {
+ state: this,
+ location: loc,
+ params: params,
+ handler: this.routeTriggered
+ };
+
+ var context = this.createStateRouteHandlerContext(attr);
+
+ if (del.statechartShouldStateHandleTriggeredRoute(sc, this, context)) {
+ if (this.get('trace') && loc) {
+ this.stateLogTrace("will handle route '%@'".fmt(loc));
+ }
+ this.handleTriggeredRoute(context);
+ } else {
+ del.statechartStateCancelledHandlingTriggeredRoute(sc, this, context);
+ }
+ },
+
+ /**
+ Constructs a new instance of a state routing context object.
+
+ @param {Hash} attr attributes to apply to the constructed object
+ @return {SC.StateRouteContext}
+
+ @see #handleRoute
+ */
+ createStateRouteHandlerContext: function(attr) {
+ return SC.StateRouteHandlerContext.create(attr);
+ },
+
+ /**
+ Invoked by this state's {@link #routeTriggered} method if the state is
+ actually allowed to handle the triggered route.
+
+ By default the method invokes a state transition to this state.
+ */
+ handleTriggeredRoute: function(context) {
+ this.gotoState(this, context);
+ },
+
/** @private */
_addEmptyInitialSubstateIfNeeded: function() {
var initialSubstate = this.get('initialSubstate'),
@@ -649,8 +813,9 @@ SC.State = SC.Object.extend(
used to find a state relative to this state based on rules of the {@link #getState} method.
@param value {SC.State|String} the state to go to
- @param [context] {Hash} context object that will be supplied to all states that are
- exited and entered during the state transition process
+ @param [context] {Hash|Object} context object that will be supplied to all states that are
+ exited and entered during the state transition process. Context can not be an instance of
+ SC.State.
*/
gotoState: function(value, context) {
var state = this.getState(value);
@@ -688,8 +853,8 @@ SC.State = SC.Object.extend(
@param value {SC.State|String} the state whose history state to go to
@param [recusive] {Boolean} indicates whether to follow history states recusively starting
from the given state
- @param [context] {Hash} context object that will be supplied to all states that are exited
- entered during the state transition process
+ @param [context] {Hash|Object} context object that will be supplied to all states that are exited
+ entered during the state transition process. Context can not be an instance of SC.State.
*/
gotoHistoryState: function(value, recursive, context) {
var state = this.getState(value);
@@ -983,7 +1148,16 @@ SC.State = SC.Object.extend(
When the enterState method is called, an optional context value may be supplied if
one was provided to the gotoState method.
- @param context {Hash} Optional value if one was supplied to gotoState when invoked
+ In the case that the context being supplied is a state context object
+ ({@link SC.StateRouteHandlerContext}), an optional `enterStateByRoute` method can be invoked
+ on this state if the state has implemented the method. If `enterStateByRoute` is
+ not part of this state then the `enterState` method will be invoked by default. The
+ `enterStateByRoute` is simply a convenience method that helps removes checks to
+ determine if the context provide is a state route context object.
+
+ @param {Hash} [context] value if one was supplied to gotoState when invoked
+
+ @see #representRoute
*/
enterState: function(context) { },
@@ -993,9 +1167,12 @@ SC.State = SC.Object.extend(
Note: This is intended to be used by the owning statechart but it can be overridden if
you need to do something special.
+ @param {Hash} [context] value if one was supplied to gotoState when invoked
@see #enterState
*/
- stateWillBecomeEntered: function() { },
+ stateWillBecomeEntered: function(context) {
+ this._isEnteringState = YES;
+ },
/**
Notification called just after enterState is invoked.
@@ -1003,10 +1180,12 @@ SC.State = SC.Object.extend(
Note: This is intended to be used by the owning statechart but it can be overridden if
you need to do something special.
+ @param context {Hash} Optional value if one was supplied to gotoState when invoked
@see #enterState
*/
- stateDidBecomeEntered: function() {
+ stateDidBecomeEntered: function(context) {
this._setupAllStateObserveHandlers();
+ this._isEnteringState = NO;
},
/**
@@ -1039,9 +1218,11 @@ SC.State = SC.Object.extend(
Note: This is intended to be used by the owning statechart but it can be overridden
if you need to do something special.
+ @param context {Hash} Optional value if one was supplied to gotoState when invoked
@see #exitState
*/
- stateWillBecomeExited: function() {
+ stateWillBecomeExited: function(context) {
+ this._isExitingState = YES;
this._teardownAllStateObserveHandlers();
},
@@ -1051,9 +1232,12 @@ SC.State = SC.Object.extend(
Note: This is intended to be used by the owning statechart but it can be overridden
if you need to do something special.
+ @param context {Hash} Optional value if one was supplied to gotoState when invoked
@see #exitState
*/
- stateDidBecomeExited: function() { },
+ stateDidBecomeExited: function(context) {
+ this._isExitingState = NO;
+ },
/** @private
View
78 frameworks/statechart/system/state_route_handler_context.js
@@ -0,0 +1,78 @@
+// ==========================================================================
+// Project: SC.Statechart - A Statechart Framework for SproutCore
+// Copyright: ©2010, 2011 Michael Cohen, and contributors.
+// Portions @2011 Apple Inc. All rights reserved.
+// License: Licensed under MIT license (see license.js)
+// ==========================================================================
+
+/*globals SC */
+
+/**
+ @class
+
+ Represents contextual information for whenever a state handles a triggered
+ route. In additional to retaining contextual information, you can also
+ use the object to retry trigging the state's route handler. Useful in cases
+ where you need to defer the handling of the route for a later time.
+
+ @see SC.State
+
+ @extends SC.Object
+ @author Michael Cohen
+*/
+SC.StateRouteHandlerContext = SC.Object.extend(
+ /** @scope SC.StateRouteContext.prototype */{
+
+ /**
+ The state that constructed this context object.
+
+ @property {SC.State}
+ */
+ state: null,
+
+ /**
+ The location that caused the state's route to be
+ triggered.
+
+ @property {String}
+ */
+ location: null,
+
+ /**
+ The parameters that were supplied to the state's
+ handler when the state's route was triggered.
+
+ @property {Hash}
+ */
+ params: null,
+
+ /**
+ The handler that got invoked when the state's
+ route was triggered. This can either be a reference
+ to the actual method or a name of the method.
+
+ @property {Function|String}
+ */
+ handler: null,
+
+ /**
+ Used to retry invoking the state's handler for when
+ the state's route gets triggered. When called this will
+ essentially perform the same call as when the handler
+ was originally triggered on state.
+ */
+ retry: function() {
+ var state = this.get('state'),
+ params = this.get('params'),
+ handler = this.get('handler');
+
+ if (SC.typeOf(handler) === SC.T_STRING) {
+ handler = state[handler];
+ }
+
+ if (SC.typeOf(handler) === SC.T_FUNCTION) {
+ handler.apply(state, [params]);
+ }
+ }
+
+});
View
62 frameworks/statechart/system/statechart.js
@@ -8,6 +8,7 @@
/*globals SC */
sc_require('system/state');
+sc_require('mixins/statechart_delegate');
/**
@class
@@ -315,6 +316,29 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
*/
suppressStatechartWarnings: NO,
+ /**
+ A statechart delegate used by the statechart and the states that the statechart
+ manages. The value assigned must adhere to the {@link SC.StatechartDelegate} mixin.
+
+ @property {SC.Object}
+
+ @see SC.StatechartDelegate
+ */
+ delegate: null,
+
+ /**
+ Computed property that returns an objects that adheres to the
+ {@link SC.StatechartDelegate} mixin. If the {@link #delegate} is not
+ assigned then this object is the default value returned.
+
+ @see SC.StatechartDelegate
+ @see #delegate
+ */
+ statechartDelegate: function() {
+ var del = this.get('delegate');
+ return this.delegateFor('isStatechartDelegate', del);
+ }.property('delegate'),
+
initMixin: function() {
if (this.get('autoInitStatechart')) {
this.initStatechart();
@@ -648,8 +672,7 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
// Collected all the state transition actions to be performed. Now execute them.
this._gotoStateActions = gotoStateActions;
- this._executeGotoStateActions(state, this._gotoStateActions, null, context);
- this._gotoStateActions = null;
+ this._executeGotoStateActions(state, gotoStateActions, null, context);
},
/**
@@ -731,10 +754,14 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
this.statechartLogTrace("END gotoState: %@".fmt(gotoState));
}
- // Okay. We're done with the current state transition. Make sure to unlock the
- // gotoState and let other pending state transitions execute.
+ this._cleanupStateTransition();
+ },
+
+ /** @private */
+ _cleanupStateTransition: function() {
this._currentGotoStateAction = null;
this._gotoStateSuspendedPoint = null;
+ this._gotoStateActions = null;
this._gotoStateLocked = NO;
this._flushPendingStateTransition();
},
@@ -763,9 +790,9 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
state.set('currentSubstates', []);
- state.stateWillBecomeExited();
+ state.stateWillBecomeExited(context);
var result = this.exitState(state, context);
- state.stateDidBecomeExited();
+ state.stateDidBecomeExited(context);
if (this.get('monitorIsActive')) this.get('monitor').pushExitedState(state);
@@ -808,9 +835,9 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
if (this.get('allowStatechartTracing')) this.statechartLogTrace("--> entering state: %@".fmt(state));
- state.stateWillBecomeEntered();
+ state.stateWillBecomeEntered(context);
var result = this.enterState(state, context);
- state.stateDidBecomeEntered();
+ state.stateDidBecomeEntered(context);
if (this.get('monitorIsActive')) this.get('monitor').pushEnteredState(state);
@@ -823,11 +850,21 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
Called during the state transition process whenever the gotoState method is
invoked.
+ If the context provided is a state route context object
+ ({@link SC.StateRouteContext}), then if the given state has a enterStateByRoute
+ method, that method will be invoked, otherwise the state's enterState method
+ will be invoked by default. The state route context object will be supplied to
+ both enter methods in either case.
+
@param state {SC.State} the state whose enterState method is to be invoked
@param context {Hash} a context hash object to provide the enterState method
*/
enterState: function(state, context) {
- return state.enterState(context);
+ if (state.enterStateByRoute && SC.kindOf(context, SC.StateRouteHandlerContext)) {
+ return state.enterStateByRoute(context);
+ } else {
+ return state.enterState(context);
+ }
},
/**
@@ -1372,7 +1409,10 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
processedArgs.useHistory = value;
break;
case SC.T_HASH:
- processedArgs.context = value;
+ case SC.T_OBJECT:
+ if (!SC.kindOf(value, SC.State)) {
+ processedArgs.context = value;
+ }
break;
default:
processedArgs.fromCurrentState = value;
@@ -1642,6 +1682,8 @@ SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
};
+SC.mixin(SC.StatechartManager, SC.StatechartDelegate, SC.DelegateSupport);
+
/**
The default name given to a statechart's root state
*/
View
161 frameworks/statechart/tests/state/methods/route_triggered.js
@@ -0,0 +1,161 @@
+// ==========================================================================
+// SC Unit Test
+// ==========================================================================
+/*globals SC */
+
+var sc, del, foo;
+
+module("SC.State: routeTriggered method Tests", {
+
+ setup: function() {
+
+ del = SC.Object.create(SC.StatechartDelegate, {
+
+ info: {},
+
+ returnValue: YES,
+
+ statechartShouldStateHandleTriggeredRoute: function(statechart, state, context) {
+ this.info.statechartShouldStateHandleTriggeredRoute = {
+ statechart: statechart,
+ state: state,
+ context: context
+ };
+
+ return this.get('returnValue');
+ },
+
+ statechartStateCancelledHandlingTriggeredRoute: function(statechart, state, context) {
+ this.info.statechartStateCancelledHandlingTriggeredRoute = {
+ statechart: statechart,
+ state: state,
+ context: context
+ };
+ }
+
+ });
+
+ sc = SC.Statechart.create({
+
+ initialState: 'foo',
+
+ delegate: del,
+
+ foo: SC.State.design({
+
+ info: {},
+
+ location: 'foo/bar',
+
+ createStateRouteHandlerContext: function(attr) {
+ this.info.createStateRouteHandlerContext = {
+ attr: attr
+ };
+ return sc_super();
+ },
+
+ handleTriggeredRoute: function(context) {
+ this.info.handleTriggeredRoute = {
+ context: context
+ };
+ }
+
+ })
+
+ });
+
+ sc.initStatechart();
+ foo = sc.getState('foo');
+ },
+
+ teardown: function() {
+ sc = del = foo = null;
+ }
+
+});
+
+test("invoke routeTriggered where delegate does allow state to handle route", function() {
+ var info, context, params = { value: 'test' };
+
+ foo.routeTriggered(params);
+
+ info = foo.info.createStateRouteHandlerContext;
+
+ ok(info, "state.createStateRouteHandlerContext should have been invoked");
+ ok(info.attr, "state.createStateRouteHandlerContext should be provided attr param");
+
+ info = foo.info.handleTriggeredRoute;
+
+ ok(info, "state.handleTriggeredRoute should have been invoked");
+
+ context = info.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state.handleTriggeredRoute should be provided a state route handler context object");
+ equals(context.get('state'), foo, "context.state should be state foo");
+ equals(context.get('location'), 'foo/bar', "context.location should be 'foo/bar'");
+ equals(context.get('params'), params, "context.params should be value passed to state.routeTriggered method");
+ equals(context.get('handler'), foo.routeTriggered, "context.handler should be reference to state.routeTriggered");
+
+ info = del.info.statechartShouldStateHandleTriggeredRoute;
+
+ ok(info, "del.statechartShouldStateHandleTriggeredRoute should have been invoked");
+ equals(info.statechart, sc, "del.statechartShouldStateHandleTriggeredRoute should be provided a statechart");
+ equals(info.state, foo, "del.statechartShouldStateHandleTriggeredRoute should be provided a state");
+
+ context = info.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state.statechartShouldStateHandleTriggeredRoute should be provided a state route handler context object");
+ equals(context.get('state'), foo, "context.state should be state foo");
+ equals(context.get('location'), 'foo/bar', "context.location should be 'foo/bar'");
+ equals(context.get('params'), params, "context.params should be value passed to state.routeTriggered method");
+ equals(context.get('handler'), foo.routeTriggered, "context.handler should be reference to state.routeTriggered");
+
+ info = del.info.statechartStateCancelledHandlingTriggeredRoute;
+
+ ok(!info, "del.statechartStateCancelledHandlingTriggeredRoute should have been invoked");
+});
+
+test("invoke routeTriggered where delegate does not allow state to handle route", function() {
+ var info, context, params = { value: 'test' };
+
+ del.set('returnValue', NO);
+ foo.routeTriggered(params);
+
+ info = foo.info.createStateRouteHandlerContext;
+
+ ok(info, "state.createStateRouteHandlerContext should have been invoked");
+ ok(info.attr, "state.createStateRouteHandlerContext should be provided attr param");
+
+ info = foo.info.handleTriggeredRoute;
+
+ ok(!info, "state.handleTriggeredRoute should have been invoked");
+
+ info = del.info.statechartShouldStateHandleTriggeredRoute;
+
+ ok(info, "del.statechartShouldStateHandleTriggeredRoute should have been invoked");
+ equals(info.statechart, sc, "del.statechartShouldStateHandleTriggeredRoute should be provided a statechart");
+ equals(info.state, foo, "del.statechartShouldStateHandleTriggeredRoute should be provided a state");
+
+ context = info.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state.statechartShouldStateHandleTriggeredRoute should be provided a state route handler context object");
+ equals(context.get('state'), foo, "context.state should be state foo");
+ equals(context.get('location'), 'foo/bar', "context.location should be 'foo/bar'");
+ equals(context.get('params'), params, "context.params should be value passed to state.routeTriggered method");
+ equals(context.get('handler'), foo.routeTriggered, "context.handler should be reference to state.routeTriggered");
+
+ info = del.info.statechartStateCancelledHandlingTriggeredRoute;
+
+ ok(info, "del.statechartStateCancelledHandlingTriggeredRoute should have been invoked");
+ equals(info.statechart, sc, "del.statechartStateCancelledHandlingTriggeredRoute should be provided a statechart");
+ equals(info.state, foo, "del.statechartStateCancelledHandlingTriggeredRoute should be provided a state");
+
+ context = info.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state.statechartStateCancelledHandlingTriggeredRoute should be provided a state route handler context object");
+ equals(context.get('state'), foo, "context.state should be state foo");
+ equals(context.get('location'), 'foo/bar', "context.location should be 'foo/bar'");
+ equals(context.get('params'), params, "context.params should be value passed to state.routeTriggered method");
+ equals(context.get('handler'), foo.routeTriggered, "context.handler should be reference to state.routeTriggered");
+
+});
View
38 frameworks/statechart/tests/state_transitioning/history_state/standard/without_concurrent_states/context.js
@@ -96,10 +96,10 @@ test("pass no context when going to state a's history state using state", functi
stateD.gotoState('f');
stateF.gotoHistoryState('a');
equals(stateD.get('isCurrentState'), true);
- equals(stateD.get('enterStateContext'), null);
- equals(stateA.get('enterStateContext'), null);
- equals(stateB.get('exitStateContext'), null);
- equals(stateF.get('exitStateContext'), null);
+ equals(stateD.get('enterStateContext'), null, "state D's enterState method should not be passed a context value");
+ equals(stateA.get('enterStateContext'), null, "state A's enterState method should not be passed a context value");
+ equals(stateB.get('exitStateContext'), null, "state B's enterState method should not be passed a context value");
+ equals(stateF.get('exitStateContext'), null, "state F's enterState method should not be passed a context value");
});
test("pass context when going to state a's history state using statechart - gotoHistoryState('f', context)", function() {
@@ -160,32 +160,4 @@ test("pass context when going to state a's history state using state - gotoHisto
equals(stateA.get('enterStateContext'), context, 'state a should have context upon entering');
equals(stateB.get('exitStateContext'), context, 'state b should have context upon exiting');
equals(stateF.get('exitStateContext'), context, 'state f should have context upon exiting');
-});
-
-//
-// test("pass context when going to state f using state - gotoState('f', context)", function() {
-// stateC.gotoState('f', context);
-// equals(stateF.get('isCurrentState'), true);
-// equals(stateC.get('exitStateContext'), context, 'state c should have context upon exiting');
-// equals(stateA.get('exitStateContext'), context, 'state a should have context upon exiting');
-// equals(stateB.get('enterStateContext'), context, 'state b should have context upon entering');
-// equals(stateF.get('enterStateContext'), context, 'state f should have context upon entering');
-// });
-//
-// test("pass context when going to state f using statechart - gotoState('f', stateC, context) ", function() {
-// statechart.gotoState('f', stateC, context);
-// equals(stateF.get('isCurrentState'), true);
-// equals(stateC.get('exitStateContext'), context, 'state c should have context upon exiting');
-// equals(stateA.get('exitStateContext'), context, 'state a should have context upon exiting');
-// equals(stateB.get('enterStateContext'), context, 'state b should have context upon entering');
-// equals(stateF.get('enterStateContext'), context, 'state f should have context upon entering');
-// });
-//
-// test("pass context when going to state f using statechart - gotoState('f', stateC, false, context) ", function() {
-// statechart.gotoState('f', stateC, false, context);
-// equals(stateF.get('isCurrentState'), true);
-// equals(stateC.get('exitStateContext'), context, 'state c should have context upon exiting');
-// equals(stateA.get('exitStateContext'), context, 'state a should have context upon exiting');
-// equals(stateB.get('enterStateContext'), context, 'state b should have context upon entering');
-// equals(stateF.get('enterStateContext'), context, 'state f should have context upon entering');
-// });
+});
View
213 frameworks/statechart/tests/state_transitioning/routing/with_concurrent_states/basic.js
@@ -0,0 +1,213 @@
+// ==========================================================================
+// SC.Statechart Unit Test
+// ==========================================================================
+/*globals SC */
+
+var statechart, del, monitor, stateFoo, stateBar, stateA, stateB, stateX, stateY, TestState;
+
+module("SC.Statechart: Concurrent States - Trigger Routing on States Basic Tests", {
+
+ setup: function() {
+
+ del = SC.Object.create(SC.StatechartDelegate, {
+
+ location: null,
+
+ handlers: {},
+
+ statechartUpdateLocationForState: function(statechart, location, state) {
+ this.set('location', location);
+ },
+
+ statechartAcquireLocationForState: function(statechart, state) {
+ return this.get('location');
+ },
+
+ statechartBindStateToRoute: function(statechart, state, route, handler) {
+ this.handlers[route] = {
+ statechart: statechart,
+ state: state,
+ handler: handler
+ };
+ }
+
+ });
+
+ TestState = SC.State.extend({
+
+ enterState: function(context) {
+ this.info = {};
+ this.info.enterState = {
+ state: this,
+ context: context
+ };
+ }
+
+ });
+
+ statechart = SC.Statechart.create({
+
+ monitorIsActive: YES,
+
+ delegate: del,
+
+ statesAreConcurrent: YES,
+
+ foo: SC.State.design({
+
+ initialSubstate: 'a',
+
+ a: TestState.design({
+
+ representRoute: 'dog'
+
+ }),
+
+ b: TestState.design({
+
+ representRoute: 'cat'
+
+ })
+
+ }),
+
+ bar: SC.State.design({
+
+ initialSubstate: 'x',
+
+ x: TestState.design({
+
+ representRoute: 'pig'
+
+ }),
+
+ y: TestState.design({
+
+ representRoute: 'cow',
+
+ enterStateByRoute: function(context) {
+ this.info = {};
+ this.info.enterStateByRoute = {
+ context: context
+ };
+ }
+
+ })
+
+ })
+
+ });
+
+ statechart.initStatechart();
+
+ monitor = statechart.get('monitor');
+ stateFoo = statechart.getState('foo');
+ stateBar = statechart.getState('bar');
+ stateA = statechart.getState('a');
+ stateB = statechart.getState('b');
+ stateX = statechart.getState('x');
+ stateY = statechart.getState('y');
+ },
+
+ teardown: function() {
+ statechart = del = monitor = TestState = stateFoo = stateBar = stateA = stateB = stateX = stateY = null;
+ }
+
+});
+
+test("check statechart initialization", function() {
+ equals(del.get('location'), null, "del.location should be null");
+
+ var handlers = del.handlers;
+
+ ok(handlers['dog'], "delegate should have a route 'dog'");
+ equals(handlers['dog'].statechart, statechart, "route 'dog' should be bound to statechart");
+ equals(handlers['dog'].state, stateA, "route 'dog' should be bound to state A");
+ equals(handlers['dog'].handler, stateA.routeTriggered, "route 'dog' should be bound to handler stateA.routeTriggered");
+
+ ok(handlers['cat'], "delegate should have a route 'cat'");
+ equals(handlers['cat'].statechart, statechart, "route 'cat' should be bound to statechart");
+ equals(handlers['cat'].state, stateB, "route 'cat' should be bound to state B");
+ equals(handlers['cat'].handler, stateB.routeTriggered, "route 'cat' should be bound to handler stateB.routeTriggered");
+
+ ok(handlers['pig'], "delegate should have a route 'pig'");
+ equals(handlers['pig'].statechart, statechart, "route 'pig' should be bound to statechart");
+ equals(handlers['pig'].state, stateX, "route 'pig' should be bound to state X");
+ equals(handlers['pig'].handler, stateX.routeTriggered, "route 'pig' should be bound to handler stateX.routeTriggered");
+
+ ok(handlers['cow'], "delegate should have a route 'cow'");
+ equals(handlers['cow'].statechart, statechart, "route 'cow' should be bound to statechart");
+ equals(handlers['cow'].state, stateY, "route 'cow' should be bound to state Y");
+ equals(handlers['cow'].handler, stateY.routeTriggered, "route 'cow' should be bound to handler stateY.routeTriggered");
+});
+
+test("trigger state B's route", function() {
+ var params = {};
+
+ monitor.reset();
+
+ ok(stateA.get('isCurrentState'), "state A should be a current state");
+ ok(!stateB.get('isCurrentState'), "state B should not be a current state");
+ ok(stateX.get('isCurrentState'), "state X should be a current state");
+ ok(!stateY.get('isCurrentState'), "state Y should not be a current state");
+
+ del.set('location', 'cat');
+
+ stateB.routeTriggered(params);
+
+ var seq = monitor.matchSequence().begin().exited('a').entered('b').end();
+ ok(seq, 'sequence should be exited[a], entered[b]');
+
+ ok(!stateA.get('isCurrentState'), "state A should not be a current state");
+ ok(stateB.get('isCurrentState'), "state B should be a current state");
+ ok(stateX.get('isCurrentState'), "state X should be a current state");
+ ok(!stateY.get('isCurrentState'), "state Y should not be a current state");
+
+ var info = stateB.info;
+
+ ok(info.enterState, "state B's enterState should have been invoked");
+
+ var context = info.enterState.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state B's enterState method should have been provided a state route handler context object");
+ equals(context.get('state'), stateB);
+ equals(context.get('location'), 'cat');
+ equals(context.get('params'), params);
+ equals(context.get('handler'), stateB.routeTriggered);
+});
+
+test("trigger state Y's route", function() {
+ var params = {};
+
+ monitor.reset();
+
+ ok(stateA.get('isCurrentState'), "state A should be a current state");
+ ok(!stateB.get('isCurrentState'), "state B should not be a current state");
+ ok(stateX.get('isCurrentState'), "state X should be a current state");
+ ok(!stateY.get('isCurrentState'), "state Y should not be a current state");
+
+ del.set('location', 'cow');
+
+ stateY.routeTriggered(params);
+
+ var seq = monitor.matchSequence().begin().exited('x').entered('y').end();
+ ok(seq, 'sequence should be exited[x], entered[y]');
+
+ ok(stateA.get('isCurrentState'), "state A should be a current state");
+ ok(!stateB.get('isCurrentState'), "state B should not be a current state");
+ ok(!stateX.get('isCurrentState'), "state X should not be a current state");
+ ok(stateY.get('isCurrentState'), "state Y should be a current state");
+
+ var info = stateY.info;
+
+ ok(!info.enterState, "state Y's enterState should not have been invoked");
+ ok(info.enterStateByRoute, "state Y's enterStateByRoute should have been invoked");
+
+ var context = info.enterStateByRoute.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state X's enterState method should have been provided a state route handler context object");
+ equals(context.get('state'), stateY);
+ equals(context.get('location'), 'cow');
+ equals(context.get('params'), params);
+ equals(context.get('handler'), stateX.routeTriggered);
+});
View
212 frameworks/statechart/tests/state_transitioning/routing/without_concurrent_states/basic.js
@@ -0,0 +1,212 @@
+// ==========================================================================
+// SC.Statechart Unit Test
+// ==========================================================================
+/*globals SC */
+
+var statechart, del, monitor, stateA, stateB, stateC;
+
+module("SC.Statechart: No Concurrent States - Trigger Routing on States Basic Tests", {
+ setup: function() {
+
+ del = SC.Object.create(SC.StatechartDelegate, {
+
+ location: null,
+
+ handlers: {},
+
+ statechartUpdateLocationForState: function(statechart, location, state) {
+ this.set('location', location);
+ },
+
+ statechartAcquireLocationForState: function(statechart, state) {
+ return this.get('location');
+ },
+
+ statechartBindStateToRoute: function(statechart, state, route, handler) {
+ this.handlers[route] = {
+ statechart: statechart,
+ state: state,
+ handler: handler
+ };
+ }
+
+ });
+
+ statechart = SC.Statechart.create({
+
+ monitorIsActive: YES,
+
+ delegate: del,
+
+ initialState: 'a',
+
+ a: SC.State.design({
+
+ representRoute: 'foo',
+
+ info: {},
+
+ enterState: function(context) {
+ this.info.enterState = {
+ context: context
+ };
+ }
+
+ }),
+
+ b: SC.State.design({
+
+ representRoute: 'bar',
+
+ info: {},
+
+ enterState: function(context) {
+ this.info.enterState = {
+ context: context
+ };
+ }
+
+ }),
+
+ c: SC.State.design({
+
+ representRoute: 'mah',
+
+ info: {},
+
+ enterStateByRoute: function(context) {
+ this.info.enterStateByRoute = {
+ context: context
+ };
+ },
+
+ enterState: function(context) {
+ this.info.enterState = {
+ context: context
+ };
+ }
+
+ })
+
+ });
+
+ statechart.initStatechart();
+
+ monitor = statechart.get('monitor');
+ stateA = statechart.getState('a');
+ stateB = statechart.getState('b');
+ stateC = statechart.getState('c');
+ },
+
+ teardown: function() {
+ statechart = del = monitor = stateA = stateB = stateC = null;
+ }
+
+});
+
+test("check statechart initialization", function() {
+ equals(del.get('location'), null, "del.location should be null");
+
+ var handlers = del.handlers;
+
+ ok(handlers['foo'], "delegate should have a route 'foo'");
+ equals(handlers['foo'].statechart, statechart, "route 'foo' should be bound to statechart");
+ equals(handlers['foo'].state, stateA, "route 'foo' should be bound to state A");
+ equals(handlers['foo'].handler, stateA.routeTriggered, "route 'foo' should be bound to handler stateA.routeTriggered");
+
+ ok(handlers['bar'], "delegate should have a route 'bar'");
+ equals(handlers['bar'].statechart, statechart, "route 'foo' should be bound to statechart");
+ equals(handlers['bar'].state, stateB, "route 'bar' should be bound to state B");
+ equals(handlers['bar'].handler, stateB.routeTriggered, "route 'bar' should be bound to handler stateB.routeTriggered");
+
+ ok(handlers['mah'], "delegate should have a route 'mah'");
+ equals(handlers['mah'].statechart, statechart, "route 'foo' should be bound to statechart");
+ equals(handlers['mah'].state, stateC, "route 'mah' should be bound to state C");
+ equals(handlers['mah'].handler, stateC.routeTriggered, "route 'mah' should be bound to handler stateC.routeTriggered");
+});
+
+test("trigger state B's route", function() {
+ var params = {};
+
+ monitor.reset();
+
+ del.set('location', 'bar');
+
+ ok(stateA.get('isCurrentState'), "state A should be a current state");
+ ok(!stateB.get('isCurrentState'), "state B should not be a current state");
+
+ stateB.routeTriggered(params);
+
+ var seq = monitor.matchSequence().begin().exited('a').entered('b').end();
+ ok(seq, 'sequence should be exited[a], entered[b]');
+
+ ok(!stateA.get('isCurrentState'), "state A should not be a current state after route triggered");
+ ok(stateB.get('isCurrentState'), "state B should be a current state after route triggered");
+
+ var info = stateB.info;
+
+ ok(info.enterState, "state B's enterState should have been invoked");
+
+ var context = info.enterState.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state B's enterState method should have been provided a state route handler context object");
+ equals(context.get('state'), stateB);
+ equals(context.get('location'), 'bar');
+ equals(context.get('params'), params);
+ equals(context.get('handler'), stateB.routeTriggered);
+});
+
+test("trigger state C's route", function() {
+ var params = {};
+
+ monitor.reset();
+
+ del.set('location', 'mah');
+
+ ok(stateA.get('isCurrentState'), "state A should be a current state");
+ ok(!stateC.get('isCurrentState'), "state C should not be a current state");
+
+ stateC.routeTriggered(params);
+
+ var seq = monitor.matchSequence().begin().exited('a').entered('c').end();
+ ok(seq, 'sequence should be exited[a], entered[c]');
+
+ ok(!stateA.get('isCurrentState'), "state A should not be a current state after route triggered");
+ ok(stateC.get('isCurrentState'), "state C should be a current state after route triggered");
+
+ var info = stateC.info;
+
+ ok(!info.enterState, "state C's enterState should not have been invoked");
+ ok(info.enterStateByRoute, "state C's enterStateByRoute should have been invoked");
+
+ var context = info.enterStateByRoute.context;
+
+ ok(SC.kindOf(context, SC.StateRouteHandlerContext), "state C's enterState method should have been provided a state route handler context object");
+ equals(context.get('state'), stateC);
+ equals(context.get('location'), 'mah');
+ equals(context.get('params'), params);
+ equals(context.get('handler'), stateC.routeTriggered);
+});
+
+test("Go to state C without triggering state's route", function() {
+ var context = {};
+
+ monitor.reset();
+
+ ok(stateA.get('isCurrentState'), "state A should be a current state");
+ ok(!stateC.get('isCurrentState'), "state C should not be a current state");
+
+ statechart.gotoState(stateC, context);
+
+ var seq = monitor.matchSequence().begin().exited('a').entered('c').end();
+ ok(seq, 'sequence should be exited[a], entered[c]');
+
+ ok(!stateA.get('isCurrentState'), "state A should not be a current state after route triggered");
+ ok(stateC.get('isCurrentState'), "state C should be a current state after route triggered");
+
+ var info = stateC.info;
+
+ ok(info.enterState, "state C's enterState should have been invoked");
+ ok(!info.enterStateByRoute, "state C's enterStateByRoute should not have been invoked");
+ equals(info.enterState.context, context, "state C's enterState should have been passed a context value");
+});
View
64 frameworks/statechart/tests/system/state_route_handler_context/methods/retry.js
@@ -0,0 +1,64 @@
+// ==========================================================================
+// SC.State Unit Test
+// ==========================================================================
+/*globals SC externalState1 externalState2 */
+
+var state, params, context;
+
+module("SC.StateRouteHandlerContext: retry Method Tests", {
+
+ setup: function() {
+
+ params = { };
+
+ state = SC.Object.create({
+
+ info: {},
+
+ handler: function(params) {
+ this.info.handler = {
+ params: params
+ };
+ }
+
+ });
+
+ context = SC.StateRouteHandlerContext.create({
+
+ state: state,
+
+ params: params
+
+ });
+
+ },
+
+ teardown: function() {
+ params = state = context = null;
+ }
+
+});
+
+test("Invoke retry with context's handler property assigned a function value", function() {
+
+ context.set('handler', state.handler);
+ context.retry();
+
+ var info = state.info;
+
+ ok(info.handler, "state's handler method was invoked");
+ equals(info.handler.params, params, "state's handler was provided params");
+
+});
+
+test("Invoke retry with context's handler property assigned a string value", function() {
+
+ context.set('handler', 'handler');
+ context.retry();
+
+ var info = state.info;
+
+ ok(info.handler, "state's handler method was invoked");
+ equals(info.handler.params, params, "state's handler was provided params");
+
+});
Please sign in to comment.
Something went wrong with that request. Please try again.