Merge lp:~bcsaller/juju-gui/viewport into lp:juju-gui/experimental

Proposed by Benjamin Saller
Status: Merged
Merged at revision: 307
Proposed branch: lp:~bcsaller/juju-gui/viewport
Merge into: lp:juju-gui/experimental
Diff against target: 3047 lines (+1092/-1156)
27 files modified
app/app.js (+10/-6)
app/assets/javascripts/d3-components.js (+10/-7)
app/modules-debug.js (+6/-1)
app/templates/overview.handlebars (+1/-1)
app/views/charm-panel.js (+3/-2)
app/views/environment.js (+13/-13)
app/views/login.js (+1/-3)
app/views/topology/mega.js (+0/-59)
app/views/topology/panzoom.js (+71/-61)
app/views/topology/topology.js (+57/-61)
app/views/topology/viewport.js (+43/-100)
app/views/utils.js (+9/-7)
lib/views/stylesheet.less (+5/-0)
package.json (+2/-2)
test/index.html (+53/-52)
test/test_app.js (+41/-18)
test/test_app_hotkeys.js (+47/-45)
test/test_d3_components.js (+0/-1)
test/test_environment_view.js (+7/-7)
test/test_login.js (+3/-4)
test/test_model.js (+2/-1)
test/test_notifications.js (+469/-454)
test/test_notifier_widget.js (+98/-95)
test/test_panzoom.js (+15/-26)
test/test_service_config_view.js (+1/-1)
test/test_topology.js (+1/-0)
undocumented (+124/-129)
To merge this branch: bzr merge lp:~bcsaller/juju-gui/viewport
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+142138@code.launchpad.net

Description of the change

Topology Viewport module

Enable viewport resizing via module.

https://codereview.appspot.com/7071045/

To post a comment you must log in.
Revision history for this message
Benjamin Saller (bcsaller) wrote :

Reviewers: mp+142138_code.launchpad.net,

Message:
Please take a look.

Description:
Topology Viewport module

Enable viewport resizing via module.

https://code.launchpad.net/~bcsaller/juju-gui/viewport/+merge/142138

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/7071045/

Affected files:
   A [revision details]
   M app/app.js
   M app/assets/javascripts/d3-components.js
   M app/modules-debug.js
   M app/templates/overview.handlebars
   M app/views/charm-panel.js
   M app/views/environment.js
   M app/views/topology/mega.js
   M app/views/topology/panzoom.js
   M app/views/topology/topology.js
   M app/views/topology/viewport.js
   M app/views/utils.js
   M lib/views/stylesheet.less
   M package.json
   M test/index.html
   M test/test_app.js
   M test/test_app_hotkeys.js
   M test/test_d3_components.js
   M test/test_environment_view.js
   M test/test_notifications.js
   M test/test_notifier_widget.js
   M test/test_panzoom.js
   M test/test_topology.js
   M undocumented

Revision history for this message
Gary Poster (gary) wrote :

Land with changes.

Some very small suggestions below.

This test is not passing for me when I run the whole test suite. In
isolation it is fine: http://pastebin.ubuntu.com/1507226/

I saw problems with zooming but you said it was pre-existing and
something that you and Brad are working on. I'm OK with landing this if
it's not making things worse.

Thanks!

Gary

https://codereview.appspot.com/7071045/diff/1/app/app.js
File app/app.js (right):

https://codereview.appspot.com/7071045/diff/1/app/app.js#newcode754
app/app.js:754: // This alias doesn't seem to work, including refs by
hand.
Bug-worthy?

https://codereview.appspot.com/7071045/diff/1/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/7071045/diff/1/app/assets/javascripts/d3-components.js#newcode330
app/assets/javascripts/d3-components.js:330: * can trigger this after
its sure relevant elements
typo: it's

https://codereview.appspot.com/7071045/diff/1/app/views/environment.js
File app/views/environment.js (right):

https://codereview.appspot.com/7071045/diff/1/app/views/environment.js#newcode73
app/views/environment.js:73: // Bind d3 events (manually).
Why do we have to do this here? Worth a comment?

https://codereview.appspot.com/7071045/diff/1/app/views/topology/viewport.js
File app/views/topology/viewport.js (right):

https://codereview.appspot.com/7071045/diff/1/app/views/topology/viewport.js#newcode42
app/views/topology/viewport.js:42: // "afterPageSizeRecalculation" event
at the end of this function.
I suggest moving this comment to immediately before the pertinent code
("topo.fire('beforePageSizeRecalculation');")

https://codereview.appspot.com/7071045/diff/1/package.json
File package.json (right):

https://codereview.appspot.com/7071045/diff/1/package.json#newcode20
package.json:20: "yeti": ">=0.2.0",
we are not actually using this, are we? If so, I suggest we delete it.

https://codereview.appspot.com/7071045/diff/1/test/index.html
File test/index.html (right):

https://codereview.appspot.com/7071045/diff/1/test/index.html#newcode31
test/index.html:31: <!-- Tests (Alphabetical)-->
Cool, thanks :-)

https://codereview.appspot.com/7071045/

lp:~bcsaller/juju-gui/viewport updated
300. By Benjamin Saller

remove rect, it contained nothing

301. By Benjamin Saller

slider works properly, rect scales zoom events properly, mouse interaction broken

302. By Benjamin Saller

pan/zoom from mouse and slider

303. By Benjamin Saller

only use slider to zoom, its consistent this way

304. By Benjamin Saller

review feedback

305. By Benjamin Saller

lint prep

Revision history for this message
Benjamin Saller (bcsaller) wrote :
lp:~bcsaller/juju-gui/viewport updated
306. By Benjamin Saller

fix background viewport scaling

307. By Benjamin Saller

lint

Revision history for this message
Madison Scott-Clary (makyo) wrote :

We discussed the lack of scroll-wheel capability on the call (something
I apparently relied on quite a bit, judging by how I did my functional
testing - perhaps a relatively high priority on that).

However, another regression that I noticed in testing was the fact that
menus are not positioned properly, nor do they follow the service
they're attached to when zooming or panning; they remain at (0,0).
uistage shows the service menu remaining open and following the service
during both pan and zoom events, no matter the source.

I'm not comfortable having two user-visible regressions in one branch -
I'm fine with the scroll-wheel one because that one isn't necessarily
discoverable, though it's certainly high on my own personal list, but I
think this second one is a little too visibly not right. Just one minor
in code, so far, though I'll give it a more thorough looking-over later
today (it's hard to tell all that changed with these refactors).

Thanks for the branch!

https://codereview.appspot.com/7071045/diff/9001/app/assets/javascripts/d3-components.js
File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/7071045/diff/9001/app/assets/javascripts/d3-components.js#newcode203
app/assets/javascripts/d3-components.js:203: //console.debug('Handler
for', name, selector, d3.event);
Could be removed or just uncommented, unless it causes too much clutter
in the console.

https://codereview.appspot.com/7071045/diff/9001/app/views/environment.js
File app/views/environment.js (right):

https://codereview.appspot.com/7071045/diff/9001/app/views/environment.js#newcode71
app/views/environment.js:71: rendered: function() {
Good rename, I think.

https://codereview.appspot.com/7071045/diff/9001/app/views/topology/viewport.js
File app/views/topology/viewport.js (right):

https://codereview.appspot.com/7071045/diff/9001/app/views/topology/viewport.js#newcode43
app/views/topology/viewport.js:43: resized: function() {
Overall a great simplification. I like it!

https://codereview.appspot.com/7071045/

lp:~bcsaller/juju-gui/viewport updated
308. By Benjamin Saller

merge trunk

309. By Benjamin Saller

move scale/translate to be topology attribute again, used consistently and available to other modules

Revision history for this message
Benjamin Saller (bcsaller) wrote :

On 2013/01/08 18:38:47, matthew.scott wrote:
> We discussed the lack of scroll-wheel capability on the call
(something I
> apparently relied on quite a bit, judging by how I did my functional
testing -
> perhaps a relatively high priority on that).

> However, another regression that I noticed in testing was the fact
that menus
> are not positioned properly, nor do they follow the service they're
attached to
> when zooming or panning; they remain at (0,0). uistage shows the
service menu
> remaining open and following the service during both pan and zoom
events, no
> matter the source.

> I'm not comfortable having two user-visible regressions in one branch
- I'm fine
> with the scroll-wheel one because that one isn't necessarily
discoverable,
> though it's certainly high on my own personal list, but I think this
second one
> is a little too visibly not right. Just one minor in code, so far,
though I'll
> give it a more thorough looking-over later today (it's hard to tell
all that
> changed with these refactors).

> Thanks for the branch!

https://codereview.appspot.com/7071045/diff/9001/app/assets/javascripts/d3-components.js
> File app/assets/javascripts/d3-components.js (right):

https://codereview.appspot.com/7071045/diff/9001/app/assets/javascripts/d3-components.js#newcode203
> app/assets/javascripts/d3-components.js:203: //console.debug('Handler
for',
> name, selector, d3.event);
> Could be removed or just uncommented, unless it causes too much
clutter in the
> console.

https://codereview.appspot.com/7071045/diff/9001/app/views/environment.js
> File app/views/environment.js (right):

https://codereview.appspot.com/7071045/diff/9001/app/views/environment.js#newcode71
> app/views/environment.js:71: rendered: function() {
> Good rename, I think.

https://codereview.appspot.com/7071045/diff/9001/app/views/topology/viewport.js
> File app/views/topology/viewport.js (right):

https://codereview.appspot.com/7071045/diff/9001/app/views/topology/viewport.js#newcode43
> app/views/topology/viewport.js:43: resized: function() {
> Overall a great simplification. I like it!

Nice catch on the menu. I tried to better encapsulate pan/zoom but
didn't track down all the usage. Fix applied.

The debug console log was pretty chatty but I can put it back on if
people want.

I'd be more than happy to spend a little time with out trying to get the
mouse translate stuff working again but I'd rather do that in another
branch (I used it exclusively in the past).

Thanks for the review.

https://codereview.appspot.com/7071045/

lp:~bcsaller/juju-gui/viewport updated
310. By Benjamin Saller

lint

311. By Benjamin Saller

wip on test failures

312. By Benjamin Saller

wip on test failures

313. By Benjamin Saller

fixed tests to not check PanZoom._var privates (which don't exist anymore now anyway).

314. By Benjamin Saller

repair async test failure dying in afterEach (I think)

Revision history for this message
Benjamin Saller (bcsaller) wrote :

*** Submitted:

Topology Viewport module

Enable viewport resizing via module.

R=gary.poster, matthew.scott
CC=
https://codereview.appspot.com/7071045

https://codereview.appspot.com/7071045/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/app.js'
2--- app/app.js 2013-01-07 21:38:42 +0000
3+++ app/app.js 2013-01-09 16:44:42 +0000
4@@ -597,7 +597,7 @@
5
6 this.showView('environment', options, {
7 callback: function() {
8- this.views.environment.instance.postRender();
9+ this.views.environment.instance.rendered();
10 },
11 render: true});
12 },
13@@ -793,19 +793,23 @@
14
15 }, '0.5.2', {
16 requires: [
17+ 'juju-charm-models',
18+ 'juju-charm-panel',
19+ 'juju-charm-store',
20 'juju-models',
21+ 'juju-notifications',
22+ // This alias doesn't seem to work, including refs by hand.
23+ 'juju-controllers',
24 'juju-notification-controller',
25+ 'juju-env',
26 'juju-charm-models',
27 'juju-views',
28- 'juju-controllers',
29- 'juju-view-charm-search',
30+ 'juju-view-login',
31 'io',
32 'json-parse',
33 'app-base',
34 'app-transitions',
35 'base',
36 'node',
37- 'model',
38- 'juju-charm-panel',
39- 'juju-charm-store']
40+ 'model']
41 });
42
43=== modified file 'app/assets/javascripts/d3-components.js'
44--- app/assets/javascripts/d3-components.js 2012-12-20 21:53:04 +0000
45+++ app/assets/javascripts/d3-components.js 2013-01-09 16:44:42 +0000
46@@ -194,18 +194,19 @@
47
48 function _bindEvent(name, handler, container, selector, context) {
49 // Adapt between d3 events and YUI delegates.
50- var d3Adaptor = function(evt) {
51+ var d3Adapter = function(evt) {
52 var selection = d3.select(evt.currentTarget.getDOMNode()),
53 d = selection.data()[0];
54 // This is a minor violation (extension)
55 // of the interface, but suits us well.
56 d3.event = evt;
57+ //console.debug('Handler for', name, selector, d3.event);
58 return handler.call(
59 evt.currentTarget.getDOMNode(), d, context);
60 };
61
62 subscriptions.push(
63- Y.delegate(name, d3Adaptor, container, selector, context));
64+ Y.delegate(name, d3Adapter, container, selector, context));
65 }
66
67 this.unbind(modName);
68@@ -252,8 +253,7 @@
69 // (re)Register the event to bubble.
70 self.publish(name, {emitFacade: true});
71 }
72- console.debug('d3 component yui event binding', target.toString(),
73- eventPhase, name);
74+ console.debug('yui event binding', module.name, eventPhase, name);
75 subscriptions.push(
76 target[eventPhase](
77 name, callback, handler.context));
78@@ -288,7 +288,7 @@
79 * Specialized handling of events only found in d3.
80 * This is again an internal implementation detail.
81 *
82- * Its worth noting that d3 events don't use a delegate pattern
83+ * It is worth noting that d3 events don't use a delegate pattern
84 * and thus must be bound to nodes present in a selection.
85 * For this reason binding d3 events happens after render cycles.
86 *
87@@ -313,10 +313,11 @@
88 Y.each(handlers, function(handler, trigger) {
89 var adapter;
90 handler = self._normalizeHandler(handler, module);
91- // Create an adaptor
92+ // Create an adapter
93 adapter = function() {
94 var selection = d3.select(this),
95 d = selection.data()[0];
96+ console.debug('D3 Handler for', selector, trigger);
97 return handler.callback.call(this, d, handler.context);
98 };
99 d3.selectAll(selector).on(trigger, adapter);
100@@ -325,7 +326,9 @@
101 },
102
103 /**
104- * Allow d3 event rebinding after rendering.
105+ * Allow d3 event rebinding after rendering. The component
106+ * can trigger this after it is sure relevant elements
107+ * are in the bound DOM.
108 *
109 **/
110 bindAllD3Events: function() {
111
112=== modified file 'app/modules-debug.js'
113--- app/modules-debug.js 2012-12-21 20:34:38 +0000
114+++ app/modules-debug.js 2013-01-09 16:44:42 +0000
115@@ -12,6 +12,7 @@
116 filter: 'debug',
117 // Set "true" for verbose logging of YUI
118 debug: false,
119+
120 base: '/juju-ui/assets/javascripts/yui/',
121 // Use Rollups
122 combine: false,
123@@ -80,6 +81,7 @@
124 'juju-topology': {
125 fullpath: '/juju-ui/views/topology/topology.js'
126 },
127+
128 'juju-view-utils': {
129 fullpath: '/juju-ui/views/utils.js'
130 },
131@@ -122,6 +124,7 @@
132
133 'juju-views': {
134 use: [
135+ 'handlebars',
136 'd3-components',
137 'juju-templates',
138 'juju-notifications',
139@@ -163,7 +166,9 @@
140 },
141
142 'juju-controllers': {
143- use: ['juju-env', 'juju-charm-store',
144+ use: [
145+ 'juju-env',
146+ 'juju-charm-store',
147 'juju-notification-controller']
148 },
149
150
151=== modified file 'app/templates/overview.handlebars'
152--- app/templates/overview.handlebars 2012-12-17 15:39:40 +0000
153+++ app/templates/overview.handlebars 2013-01-09 16:44:42 +0000
154@@ -1,5 +1,5 @@
155 <div class="topology">
156- <div class="topology-canvas crosshatch-background">
157+ <div class="crosshatch-background topology-canvas">
158 <div class="environment-menu" id="service-menu">
159 <div class="triangle">&nbsp;</div>
160 <ul>
161
162=== modified file 'app/views/charm-panel.js'
163--- app/views/charm-panel.js 2012-12-14 20:25:16 +0000
164+++ app/views/charm-panel.js 2013-01-09 16:44:42 +0000
165@@ -1056,11 +1056,11 @@
166 setPanel({name: 'charms'});
167
168 // Update position if we resize the window.
169- Y.on('windowresize', function(e) {
170+ subscriptions.push(Y.on('windowresize', function(e) {
171 if (isPanelVisible) {
172 updatePanelPosition();
173 }
174- });
175+ }));
176
177 /**
178 * Hide the charm panel.
179@@ -1229,6 +1229,7 @@
180 requires: [
181 'view',
182 'juju-view-utils',
183+ 'juju-templates',
184 'node',
185 'handlebars',
186 'event-hover',
187
188=== modified file 'app/views/environment.js'
189--- app/views/environment.js 2012-12-20 17:28:17 +0000
190+++ app/views/environment.js 2013-01-09 16:44:42 +0000
191@@ -34,10 +34,10 @@
192
193 //If we need the initial HTML template
194 // take care of that.
195- if (!this.svg) {
196+ if (!this._rendered) {
197 EnvironmentView.superclass.render.apply(this, arguments);
198 container.setHTML(Templates.overview());
199- this.svg = container.one('.topology');
200+ this._rendered = true;
201 }
202
203 if (!topo) {
204@@ -51,6 +51,7 @@
205 // Bind all the behaviors we need as modules.
206 topo.addModule(views.MegaModule);
207 topo.addModule(views.PanZoomModule);
208+ topo.addModule(views.ViewportModule);
209 topo.addModule(views.RelationModule);
210
211 topo.addTarget(this);
212@@ -61,13 +62,15 @@
213 return this;
214 },
215
216- postRender: function() {
217- this.topo.attachContainer();
218+ /**
219+ * Render callback handler,
220+ * triggered from app when the view renders.
221+ *
222+ * @method rendered
223+ **/
224+ rendered: function() {
225 this.topo.fire('rendered');
226- // Bind d3 events (manually)
227- // this needs to be postRender and
228- // the jiggle in phases has broken
229- // the existing (from change to showView)
230+ // Bind d3 events (manually).
231 this.topo.bindAllD3Events();
232 }
233 }, {
234@@ -79,13 +82,10 @@
235 requires: ['juju-templates',
236 'juju-view-utils',
237 'juju-models',
238- 'd3',
239- 'd3-components',
240+ 'juju-topology',
241+ 'svg-layouts',
242 'base-build',
243 'handlebars-base',
244 'node',
245- 'svg-layouts',
246- 'event-resize',
247- 'slider',
248 'view']
249 });
250
251=== modified file 'app/views/login.js'
252--- app/views/login.js 2013-01-07 20:10:00 +0000
253+++ app/views/login.js 2013-01-09 16:44:42 +0000
254@@ -6,7 +6,6 @@
255 YUI.add('juju-view-login', function(Y) {
256
257 var views = Y.namespace('juju.views');
258- var Templates = views.Templates;
259
260 var LoginView = Y.Base.create('LoginView', Y.View, [views.JujuBaseView], {
261 // This is so tests can easily determine if the user was prompted.
262@@ -52,8 +51,7 @@
263 requires: [
264 'view',
265 'juju-view-utils',
266- 'node',
267- 'handlebars'
268+ 'node'
269 ]
270 });
271
272
273=== modified file 'app/views/topology/mega.js'
274--- app/views/topology/mega.js 2013-01-08 16:08:41 +0000
275+++ app/views/topology/mega.js 2013-01-09 16:44:42 +0000
276@@ -128,9 +128,6 @@
277 }
278 },
279 yui: {
280- windowresize: {
281- callback: 'setSizesFromViewport',
282- context: 'module'},
283 rendered: 'renderedHandler',
284 show: 'show',
285 hide: 'hide',
286@@ -655,9 +652,6 @@
287
288 this.update();
289
290- // Set the sizes from the viewport.
291- this.setSizesFromViewport();
292-
293 // Ensure relation labels are sized properly.
294 container.all('.rel-label').each(function(label) {
295 var width = label.one('text').getClientRect().width + 10;
296@@ -703,57 +697,6 @@
297 picker.one('.picker-expanded').removeClass('active');
298 },
299
300- /*
301- * Set the visualization size based on the viewport
302- */
303- setSizesFromViewport: function() {
304- // This event allows other page components that may unintentionally
305- // affect the page size, such as the charm panel, to get out of the
306- // way before we compute sizes. Note the
307- // "afterPageSizeRecalculation" event at the end of this function.
308- // start with some reasonable defaults
309- console.log('setSizesFromViewPort', this, arguments);
310- var topo = this.get('component'),
311- container = this.get('container'),
312- vis = topo.vis,
313- xscale = topo.xScale,
314- yscale = topo.yScale,
315- svg = container.one('svg'),
316- canvas = container.one('.topology-canvas');
317-
318- topo.fire('beforePageSizeRecalculation');
319- // Get the canvas out of the way so we can calculate the size
320- // correctly (the canvas contains the svg). We want it to be the
321- // smallest size we accept--no smaller or bigger--or else the
322- // presence or absence of scrollbars may affect our calculations
323- // incorrectly.
324- canvas.setStyles({height: 600, width: 800});
325- var dimensions = utils.getEffectiveViewportSize(true, 800, 600);
326- // Set the svg sizes.
327- svg.setAttribute('width', dimensions.width)
328- .setAttribute('height', dimensions.height);
329-
330- // Set the internal rect's size.
331- svg.one('rect')
332- .setAttribute('width', dimensions.width)
333- .setAttribute('height', dimensions.height);
334- canvas
335- .setStyle('height', dimensions.height)
336- .setStyle('width', dimensions.width);
337-
338- // Reset the scale parameters
339- topo.xScale.domain([-dimensions.width / 2, dimensions.width / 2])
340- .range([0, dimensions.width]);
341- topo.yScale.domain([-dimensions.height / 2, dimensions.height / 2])
342- .range([dimensions.height, 0]);
343-
344- topo.set('size', [dimensions.width, dimensions.height]);
345- topo.fire('afterPageSizeRecalculation');
346- },
347-
348- /*
349- * Update the location of the active service panel
350- */
351 updateServiceMenuLocation: function() {
352 var topo = this.get('component'),
353 container = this.get('container'),
354@@ -894,8 +837,6 @@
355 'd3',
356 'd3-components',
357 'juju-templates',
358- 'node',
359- 'event',
360 'juju-models',
361 'juju-env'
362 ]
363
364=== modified file 'app/views/topology/panzoom.js'
365--- app/views/topology/panzoom.js 2012-12-21 20:46:01 +0000
366+++ app/views/topology/panzoom.js 2013-01-09 16:44:42 +0000
367@@ -6,7 +6,7 @@
368 d3ns = Y.namespace('d3');
369
370 /**
371- * Handle PanZoom within the a Topology.
372+ * Handle PanZoom within a Topology.
373 *
374 * Emitted events:
375 *
376@@ -25,63 +25,67 @@
377 '#zoom-in-btn': {click: 'zoom_in'}
378 },
379 yui: {
380- zoom: {callback: 'zoomHandler'},
381- rendered: {callback: 'renderedHandler'}
382+ zoom: 'zoomHandler',
383+ rendered: 'renderedHandler'
384 }
385 },
386
387- initializer: function(options) {
388- PanZoomModule.superclass.constructor.apply(this, arguments);
389- this._translate = [0, 0];
390- this._scale = 1.0;
391- },
392-
393- // Handler for 'zoom' event.
394- zoomHandler: function(evt) {
395- var s = this.slider,
396- vis = this.get('component').vis;
397-
398- s.set('value', Math.floor(evt.scale * 100));
399- this.rescale(vis, evt);
400+ componentBound: function() {
401+ var topo = this.get('component'),
402+ options = topo.options;
403+
404+ this.toScale = d3.scale.linear()
405+ .domain([options.minZoom, options.maxZoom])
406+ .range([0.25, 2])
407+ .clamp(true);
408+ this.toSlider = d3.scale.linear()
409+ .domain([0.25, 2])
410+ .range([options.minZoom, options.maxZoom])
411+ .clamp(true);
412 },
413
414 renderSlider: function() {
415 var self = this,
416 topo = this.get('component'),
417- value = 100,
418- currentScale = topo.get('scale');
419+ options = topo.options,
420+ currentScale = topo.get('scale'),
421+ slider;
422
423 if (self.slider) {
424 return;
425 }
426- // Build a slider to control zoom level
427- if (currentScale) {
428- value = currentScale * 100;
429- }
430- var slider = new Y.Slider({
431- min: 25,
432- max: 200,
433- value: value
434+
435+ slider = new Y.Slider({
436+ min: options.minZoom,
437+ max: options.maxZoom,
438+ value: this.toSlider(currentScale)
439 });
440+ // XXX: selection to module option
441 slider.render('#slider-parent');
442 topo.recordSubscription(this,
443 slider.after('valueChange', function(evt) {
444- // Don't fire a zoom if there's a zoom event
445- // already in progress; that will run rescale
446- // for us.
447 if (d3.event && d3.event.scale &&
448 d3.event.translate) {
449 return;
450 }
451- self._fire_zoom((
452- evt.newVal - evt.prevVal) / 100);
453+ self._fire_zoom(self.toScale(evt.newVal));
454 }));
455- self.slider = slider;
456+ this.slider = slider;
457 },
458
459- update: function() {
460- PanZoomModule.superclass.update.apply(this, arguments);
461- return this;
462+ // Handler for 'zoom' event.
463+ zoomHandler: function(evt) {
464+ var slider = this.slider,
465+ topo = this.get('component'),
466+ height = topo.get('height'),
467+ width = topo.get('width'),
468+ options = topo.options;
469+
470+ if (!this.slider) {
471+ return;
472+ }
473+ slider.set('value', this.toSlider(evt.scale));
474+ this.rescale(d3.event);
475 },
476
477 /*
478@@ -89,7 +93,7 @@
479 */
480 zoom_out: function(data, context) {
481 var slider = context.slider,
482- val = slider.get('value');
483+ val = slider.get('value');
484 slider.set('value', val - 25);
485 },
486
487@@ -98,58 +102,63 @@
488 */
489 zoom_in: function(data, context) {
490 var slider = context.slider,
491- val = slider.get('value');
492+ val = slider.get('value');
493 slider.set('value', val + 25);
494 },
495
496 /*
497 * Wrapper around the actual rescale method for zoom buttons.
498 */
499- _fire_zoom: function(delta) {
500+ _fire_zoom: function(scale) {
501 var topo = this.get('component'),
502 vis = topo.vis,
503 zoom = topo.zoom,
504+ rect = topo.zoomPlane,
505+ delta,
506 evt = {};
507
508+ delta = scale - topo.get('scale');
509+
510 // Build a temporary event that rescale can use of a similar
511 // construction to d3.event.
512- evt.translate = zoom.translate();
513- evt.scale = zoom.scale() + delta;
514-
515+ evt.scale = scale;
516 // Update the scale in our zoom behavior manager to maintain state.
517- zoom.scale(evt.scale);
518-
519+ zoom.scale(Math.floor(scale));
520 // Update the translate so that we scale from the center
521 // instead of the origin.
522- var rect = vis.select('rect');
523- evt.translate[0] -= parseInt(rect.attr('width'), 10) / 2 * delta;
524- evt.translate[1] -= parseInt(rect.attr('height'), 10) / 2 * delta;
525+ evt.translate = zoom.translate();
526+ evt.translate[0] -= (parseInt(rect.attr('width'), 10) / 2) * delta;
527+ evt.translate[1] -= (parseInt(rect.attr('height'), 10) / 2) * delta;
528 zoom.translate(evt.translate);
529
530- this.rescale(vis, evt);
531+ this.rescale(evt);
532 },
533
534 /*
535 * Rescale the visualization on a zoom/pan event.
536 */
537- rescale: function(vis, evt) {
538+ rescale: function(evt) {
539 // Make sure we don't scale outside of our bounds.
540 // This check is needed because we're messing with d3's zoom
541 // behavior outside of mouse events (e.g.: with the slider),
542 // and can't trust that zoomExtent will play well.
543- var new_scale = Math.floor(evt.scale * 100),
544- topo = this.get('component');
545+ var topo = this.get('component'),
546+ options = topo.options,
547+ vis = topo.vis;
548
549- if (new_scale < 25 || new_scale > 200) {
550- evt.scale = topo.get('scale');
551+ if (!vis) {
552+ return;
553 }
554+
555+ evt.scale = this.toSlider(evt.scale) /100.0;
556+
557 // Store the current value of scale so that it can be restored later.
558- this._scale = evt.scale;
559+ topo.set('scale', evt.scale);
560 // Store the current value of translate as well, by copying the event
561 // array in order to avoid reference sharing.
562- this._translate = Y.mix(evt.translate);
563- vis.attr('transform', 'translate(' + evt.translate + ')' +
564- ' scale(' + evt.scale + ')');
565+ topo.set('translate', Y.mix(evt.translate));
566+ vis.attr('transform', 'translate(' + topo.get('translate') + ')' +
567+ ' scale(' + topo.get('scale') + ')');
568 topo.fire('rescaled');
569 },
570
571@@ -157,8 +166,8 @@
572 // Preserve zoom when the scene is updated.
573 var topo = this.get('component'),
574 changed = false,
575- currentScale = this._scale,
576- currentTranslate = this._translate;
577+ currentScale = topo.get('scale'),
578+ currentTranslate = topo.get('translate');
579
580 this.renderSlider();
581 if (currentTranslate && currentTranslate !== topo.get('translate')) {
582@@ -170,7 +179,7 @@
583 changed = true;
584 }
585 if (changed) {
586- this._fire_zoom(0);
587+ this.rescale({scale: currentScale, translate: currentTranslate});
588 }
589 }
590 }, {
591@@ -180,10 +189,11 @@
592 views.PanZoomModule = PanZoomModule;
593 }, '0.1.0', {
594 requires: [
595+ 'node',
596+ 'event',
597+ 'slider',
598 'd3',
599 'd3-components',
600- 'node',
601- 'event',
602 'juju-models',
603 'juju-env'
604 ]
605
606=== modified file 'app/views/topology/topology.js'
607--- app/views/topology/topology.js 2012-12-21 18:48:19 +0000
608+++ app/views/topology/topology.js 2013-01-09 16:44:42 +0000
609@@ -25,7 +25,12 @@
610 var Topology = Y.Base.create('Topology', d3ns.Component, [], {
611 initializer: function(options) {
612 Topology.superclass.constructor.apply(this, arguments);
613- this.options = Y.mix(options || {});
614+ this.options = Y.mix(options || {
615+ minZoom: 25,
616+ maxZoom: 200
617+ });
618+
619+ this._subscriptions = [];
620 },
621
622 /**
623@@ -57,6 +62,7 @@
624
625 renderOnce: function() {
626 var self = this,
627+ svg,
628 vis,
629 width = this.get('width'),
630 height = this.get('height'),
631@@ -70,76 +76,68 @@
632 // Take the first element.
633 this._templateRendered = true;
634
635- // Create a pan/zoom behavior manager.
636- this.xScale = d3.scale.linear()
637- .domain([-width / 2, width / 2])
638- .range([0, width]);
639- this.yScale = d3.scale.linear()
640- .domain([-height / 2, height / 2])
641- .range([height, 0]);
642-
643- // Include very basic behavior, fire
644- // yui event for anything more complex.
645- this.zoom = d3.behavior.zoom()
646- .x(this.xScale)
647- .y(this.yScale)
648- .scaleExtent([0.25, 2.0])
649- .on('zoom', function(evt) {
650- // This will add the d3 properties to the
651- // eventFacade
652- self.fire('zoom', d3.event);
653- });
654+ // These are defaults, a (Viewport) Module
655+ // can implement policy around them.
656+ this.computeScales();
657
658 // Set up the visualization with a pack layout.
659- vis = d3.select(container.getDOMNode())
660+ svg = d3.select(container.getDOMNode())
661 .selectAll('.topology-canvas')
662 .append('svg:svg')
663 .attr('pointer-events', 'all')
664 .attr('width', width)
665- .attr('height', height)
666- .append('svg:g')
667- .call(this.zoom)
668- .append('g');
669-
670- vis.append('svg:rect')
671- .attr('class', 'graph')
672- .attr('fill', 'rgba(255,255,255,0)');
673-
674+ .attr('height', height);
675+ this.svg = svg;
676+
677+ this.zoomPlane = svg.append('rect')
678+ .attr('class', 'zoom-plane')
679+ .attr('width', width)
680+ .attr('height', height)
681+ .call(this.zoom)
682+ .on('mousewheel.zoom', null)
683+ .on('DOMMouseScroll.zoom', null)
684+ .on('dblclick.zoom', null);
685+
686+ vis = svg.append('svg:g');
687 this.vis = vis;
688-
689- // Build out scale and zoom.
690- // These are defaults, a (Viewport) Module
691- // can implement policy around them.
692- this.sizeChangeHandler();
693- this.on('sizeChanged', this.sizeChangeHandler);
694-
695 Topology.superclass.renderOnce.apply(this, arguments);
696 return this;
697 },
698
699- sizeChangeHandler: function() {
700+ computeScales: function() {
701 var self = this,
702 width = this.get('width'),
703 height = this.get('height');
704
705+ if (!this.xScale) {
706+ this.xScale = d3.scale.linear();
707+ this.yScale = d3.scale.linear();
708+ this.zoom = d3.behavior.zoom();
709+ }
710 // Update the pan/zoom behavior manager.
711 this.xScale.domain([-width / 2, width / 2])
712- .range([0, width]);
713+ .range([0, width])
714+ .clamp(true)
715+ .nice();
716 this.yScale.domain([-height / 2, height / 2])
717- .range([height, 0]);
718+ .range([height, 0])
719+ .clamp(true)
720+ .nice();
721+
722 this.zoom.x(this.xScale)
723- .y(this.yScale);
724+ .y(this.yScale)
725+ .scaleExtent([this.options.minZoom, this.options.maxZoom])
726+ .on('zoom', function(evt) {self.fire('zoom', d3.event);});
727 },
728
729 /*
730- * Utility method to get a service object from the DB
731- * given a BoundingBox.
732- */
733+ * Utility method to get a service object from the DB
734+ * given a BoundingBox.
735+ */
736 serviceForBox: function(boundingBox) {
737 var db = this.get('db');
738 return db.services.getById(boundingBox.id);
739 }
740-
741 }, {
742 ATTRS: {
743 /**
744@@ -155,28 +153,26 @@
745 * A [width, height] tuple representing canvas size.
746 **/
747 size: {value: [640, 480]},
748- /**
749- * @property {Number} scale
750- **/
751+ width: {
752+ getter: function() {return this.get('size')[0];}
753+ },
754+
755+ height: {
756+ getter: function() {return this.get('size')[1];}
757+ },
758+ /*
759+ * Scale and translate are managed by an external module
760+ * (PanZoom in this case). If that module isn't
761+ * loaded nothing will modify these values.
762+ */
763 scale: {
764 getter: function() {return this.zoom.scale();},
765 setter: function(v) {this.zoom.scale(v);}
766 },
767- /**
768- * @property {Array} transform
769- **/
770+
771 translate: {
772 getter: function() {return this.zoom.translate();},
773- setter: function(v) {this.zoom.translate(v);}
774- },
775-
776- width: {
777- getter: function() {return this.get('size')[0];}
778- },
779-
780- height: {
781- getter: function() {return this.get('size')[1];}
782- }
783+ setter: function(v) {this.zoom.translate(v);}}
784 }
785
786 });
787
788=== modified file 'app/views/topology/viewport.js'
789--- app/views/topology/viewport.js 2012-12-11 03:58:03 +0000
790+++ app/views/topology/viewport.js 2013-01-09 16:44:42 +0000
791@@ -2,6 +2,7 @@
792
793 YUI.add('juju-topology-viewport', function(Y) {
794 var views = Y.namespace('juju.views'),
795+ utils = Y.namespace('juju.views.utils'),
796 models = Y.namespace('juju.models'),
797 d3ns = Y.namespace('d3');
798
799@@ -26,112 +27,54 @@
800
801 events: {
802 yui: {
803- windowresize: 'resized'
804- }
805- },
806-
807- initializer: function(options) {
808- ViewportModule.superclass.constructor.apply(this, arguments);
809- },
810-
811- render: function() {
812- var topology = this.get('component'),
813- value = 100,
814- currentScale = topology.get('scale');
815-
816- ViewportModule.superclass.render.apply(this, arguments);
817- // Build a slider to control zoom level
818- if (currentScale) {
819- value = currentScale * 100;
820- }
821- var slider = new Y.Slider({
822- min: 25,
823- max: 200,
824- value: value
825- });
826- slider.render('#slider-parent');
827- slider.after('valueChange', function(evt) {
828- // Don't fire a zoom if there's a zoom event already in progress;
829- // that will run rescale for us.
830- if (d3.event && d3.event.scale && d3.event.translate) {
831- return;
832- }
833- topology._fire_zoom((evt.newVal - evt.prevVal) / 100);
834- });
835- this.slider = slider;
836-
837- return this;
838- },
839-
840- update: function() {
841- ViewportModule.superclass.update.apply(this, arguments);
842- return this;
843- },
844-
845- /**
846- * Event handler for windowresize events.
847- *
848- * Properly scale the component to take advantage of all the space
849- * provided by the viewport.
850- *
851- * @method resized
852- **/
853- resized: function(evt) {
854- // start with some reasonable defaults
855- var topology = this.get('component'),
856- vis = topology.vis,
857+ windowresize: 'resized',
858+ rendered: 'resized'
859+ }
860+ },
861+
862+ /*
863+ * Set the visualization size based on the viewport.
864+ *
865+ * This event allows other page components that may unintentionally affect
866+ * the page size, such as the charm panel, to get out of the way before we
867+ * compute sizes. Note the "afterPageSizeRecalculation" event at the end
868+ * of this function.
869+ */
870+ resized: function() {
871+ var topo = this.get('component'),
872 container = this.get('container'),
873- viewport_height = '100%',
874- viewport_width = '100%',
875+ vis = topo.vis,
876 svg = container.one('svg'),
877- width = 800,
878- height = 600;
879-
880- if (container.get('winHeight') &&
881- Y.one('#overview-tasks') &&
882- Y.one('.navbar')) {
883- // Attempt to get the viewport height minus the navbar at top and
884- // control bar at the bottom. Use Y.one() to ensure that the
885- // container is attached first (provides some sensible defaults)
886-
887- viewport_height = container.get('winHeight') -
888- styleToNumber('#overview-tasks', 'height', 22) - //XXX
889- styleToNumber('.navbar', 'height', 87) - 1; //XXX
890-
891- // Attempt to get the viewport width from the overview-tasks bar.
892- viewport_width = styleToNumber('#viewport', 'width', 800); //XXX
893-
894- // Make sure we don't get sized any smaller than 800x600
895- viewport_height = Math.max(viewport_height, height);
896- viewport_width = Math.max(viewport_width, width);
897+ canvas = container.one('.topology-canvas'),
898+ zoomPlane = container.one('.zoom-plane');
899+
900+ if (!canvas || !svg) {
901+ return;
902 }
903- // Set the svg sizes.
904- svg.setAttribute('width', viewport_width)
905- .setAttribute('height', viewport_height);
906-
907- // Get the resulting computed sizes (in the case of 100%).
908- width = parseInt(svg.getComputedStyle('width'), 10);
909- height = parseInt(svg.getComputedStyle('height'), 10);
910-
911- // Set the internal rect's size.
912- svg.one('rect')
913- .setAttribute('width', width)
914- .setAttribute('height', height);
915- container.one('#canvas').setStyle('height', height);
916- container.one('#canvas').setStyle('width', width);
917-
918+ topo.fire('beforePageSizeRecalculation');
919+ // Get the canvas out of the way so we can calculate the size
920+ // correctly (the canvas contains the svg). We want it to be the
921+ // smallest size we accept--no smaller or bigger--or else the
922+ // presence or absence of scrollbars may affect our calculations
923+ // incorrectly.
924+ canvas.setStyles({height: 600, width: 800});
925+ var dimensions = utils.getEffectiveViewportSize(true, 800, 600);
926+ svg.setAttribute('width', dimensions.width);
927+ svg.setAttribute('height', dimensions.height);
928+ vis.attr('width', dimensions.width);
929+ vis.attr('height', dimensions.height);
930+
931+
932+ zoomPlane.setAttribute('width', dimensions.width);
933+ zoomPlane.setAttribute('height', dimensions.height);
934+ canvas.setStyles({
935+ width: dimensions.width + 'px',
936+ height: dimensions.height + 'px'});
937 // Reset the scale parameters
938- topology.xscale.domain([-width / 2, width / 2])
939- .range([0, width]);
940- topology.yscale.domain([-height / 2, height / 2])
941- .range([height, 0]);
942-
943- topology.width = width;
944- topology.height = height;
945+ topo.set('size', [dimensions.width, dimensions.height]);
946+ topo.fire('afterPageSizeRecalculation');
947 }
948
949-
950-
951 }, {
952 ATTRS: {}
953 });
954
955=== modified file 'app/views/utils.js'
956--- app/views/utils.js 2012-12-20 21:59:21 +0000
957+++ app/views/utils.js 2013-01-09 16:44:42 +0000
958@@ -118,6 +118,8 @@
959 time: noop,
960 timeEnd: noop,
961 log: noop,
962+ info: noop,
963+ error: noop,
964 debug: noop
965 };
966
967@@ -971,11 +973,11 @@
968
969 }, '0.1.0', {
970 requires: ['base-build',
971- 'handlebars',
972- 'node',
973- 'view',
974- 'panel',
975- 'json-stringify',
976- 'gallery-markdown',
977- 'datatype-date-format']
978+ 'handlebars',
979+ 'node',
980+ 'view',
981+ 'panel',
982+ 'json-stringify',
983+ 'gallery-markdown',
984+ 'datatype-date-format']
985 });
986
987=== modified file 'lib/views/stylesheet.less'
988--- lib/views/stylesheet.less 2012-12-05 15:12:45 +0000
989+++ lib/views/stylesheet.less 2013-01-09 16:44:42 +0000
990@@ -222,6 +222,11 @@
991 position: relative;
992 }
993
994+.zoom-plane {
995+ fill-opacity: 0;
996+ cursor: move;
997+}
998+
999
1000 .environment-menu {
1001 @border_radius: 20px;
1002
1003=== modified file 'package.json'
1004--- package.json 2013-01-02 12:57:09 +0000
1005+++ package.json 2013-01-09 16:44:42 +0000
1006@@ -15,9 +15,9 @@
1007 "cryptojs": ">= 2.5.3"
1008 },
1009 "devDependencies": {
1010- "d3": "2.10.x",
1011+ "d3": "<3.0.0",
1012 "yui": ">=3.7.0",
1013- "mocha": "1.5.x",
1014+ "mocha": "1.8.x",
1015 "express": "3.x",
1016 "expect.js": "0.1.2",
1017 "should": ">=1.0.0",
1018
1019=== modified file 'test/index.html'
1020--- test/index.html 2013-01-07 21:38:42 +0000
1021+++ test/index.html 2013-01-09 16:44:42 +0000
1022@@ -3,70 +3,71 @@
1023 <head>
1024 <meta charset="utf-8">
1025 <link rel="stylesheet" href="assets/mocha.css">
1026+
1027+
1028+ <!-- Load test runner/environment -->
1029+ <script src="assets/chai.js"></script>
1030+ <script src="assets/mocha.js"></script>
1031+ <script>
1032+ noLogin = true;
1033+ var assert = chai.assert,
1034+ expect = chai.expect;
1035+
1036+ var should = chai.should();
1037+ console.log('mocha setup');
1038+ mocha.setup({'ui': 'bdd', 'ignoreLeaks': false, 'timeout': 20000})
1039+ console.log('mocha setup done');
1040+ </script>
1041+
1042+ <!-- Load up YUI base, app modules, and test utils -->
1043+ <!-- Since only the tests depend on these files and the prod tests disable
1044+ the YUI loader, we have to include them manually here. -->
1045 <script src="/juju-ui/assets/modules.js"></script>
1046 <script src="/juju-ui/assets/all-yui.js"></script>
1047- <!-- Since only the tests depend on these files and the prod tests disable
1048- the YUI loader, we have to include them manually here. -->
1049 <script src="/juju-ui/assets/event-simulate.js"></script>
1050 <script src="/juju-ui/assets/node-event-simulate.js"></script>
1051- <script src="assets/chai.js"></script>
1052- <script src="assets/mocha.js"></script>
1053 <script src="utils.js"></script>
1054- <script>
1055- noLogin = true;
1056- var assert = chai.assert,
1057- expect = chai.expect
1058- should = chai.should();
1059- mocha.setup({ui: 'bdd', ignoreLeaks: false})
1060- </script>
1061-
1062- <script>
1063- YUI().use('node', 'event', function(Y) {
1064- var config = GlobalConfig;
1065- for (group in config.groups) {
1066- var group = config.groups[group];
1067- for (m in group.modules) {
1068- var resource = group.modules[m];
1069- if (!m || !resource.fullpath) {
1070- continue
1071- }
1072- resource.fullpath = resource.fullpath.replace(
1073- '/juju-ui/', '../juju-ui/', 1);
1074- // If we load modules asyncronously then the module loading may take
1075- // so long that the test definitions (and before/after calls) happen
1076- // *after* the test runner is invoked. In this case the test runner
1077- // will not know about the tests and therefore not run them.
1078- resource.async = false;
1079- }
1080- }
1081- Y.on('domready', mocha.run);
1082- });
1083- </script>
1084-
1085+
1086+
1087+ <!-- Tests (Alphabetical)-->
1088+ <script src="test_app.js"></script>
1089+ <script src="test_app_hotkeys.js"></script>
1090+ <script src="test_application_notifications.js"></script>
1091+ <script src="test_charm_collection_view.js"></script>
1092+ <script src="test_charm_configuration.js"></script>
1093+ <script src="test_charm_panel.js"></script>
1094+ <script src="test_charm_store.js"></script>
1095+ <script src="test_charm_view.js"></script>
1096+ <script src="test_console.js"></script>
1097 <script src="test_d3_components.js"></script>
1098- <script src="test_topology.js"></script>
1099- <script src="test_panzoom.js"></script>
1100+ <script src="test_environment_view.js"></script>
1101 <script src="test_env.js"></script>
1102+ <script src="test_endpoints.js"></script>
1103+ <script src="test_login.js"></script>
1104 <script src="test_model.js"></script>
1105 <script src="test_notifications.js"></script>
1106- <script src="test_app.js"></script>
1107- <script src="test_unit_view.js"></script>
1108- <script src="test_charm_collection_view.js"></script>
1109- <script src="test_charm_view.js"></script>
1110- <script src="test_environment_view.js"></script>
1111+ <script src="test_notifier_widget.js"></script>
1112+ <script src="test_panzoom.js"></script>
1113+ <script src="test_topology.js"></script>
1114 <script src="test_service_config_view.js"></script>
1115 <script src="test_service_view.js"></script>
1116+ <script src="test_unit_view.js"></script>
1117 <script src="test_utils.js"></script>
1118- <script src="test_login.js"></script>
1119- <script src="test_charm_panel.js"></script>
1120- <script src="test_charm_configuration.js"></script>
1121- <script src="test_console.js"></script>
1122- <script src="test_endpoints.js"></script>
1123- <script src="test_application_notifications.js"></script>
1124- <script src="test_charm_store.js"></script>
1125- <script src="test_app_hotkeys.js"></script>
1126- <script src="test_notifier_widget.js"></script>
1127-
1128+
1129+
1130+ <script>
1131+ YUI_config = {
1132+ async: false,
1133+ consoleEnabled: true,
1134+ delayUntil: 'domready'
1135+ };
1136+
1137+ YUI().use(['node', 'event'], function(Y) {
1138+ // Run the tests.
1139+ if (window.mochaPhantomJS) { mochaPhantomJS.run(); }
1140+ else { mocha.run(); }
1141+ });
1142+ </script>
1143
1144 </head>
1145
1146
1147=== modified file 'test/test_app.js'
1148--- test/test_app.js 2013-01-02 20:39:49 +0000
1149+++ test/test_app.js 2013-01-09 16:44:42 +0000
1150@@ -31,9 +31,18 @@
1151 return app;
1152 }
1153
1154-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
1155+(function() {
1156+
1157 describe('Application basics', function() {
1158- var app, container;
1159+ var Y, app, container;
1160+
1161+ before(function(done) {
1162+ Y = YUI(GlobalConfig).use(
1163+ ['juju-gui', 'juju-tests-utils', 'juju-view-utils'],
1164+ function(Y) {
1165+ done();
1166+ });
1167+ });
1168
1169 beforeEach(function() {
1170 container = Y.one('#main')
1171@@ -116,26 +125,32 @@
1172 });
1173
1174 });
1175-});
1176-
1177-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'], function(Y) {
1178+})();
1179+
1180+
1181+
1182+(function() {
1183
1184 describe('Application Connection State', function() {
1185- var container;
1186+ var container, Y;
1187
1188- before(function() {
1189- container = Y.Node.create('<div id="test" class="container"></div>');
1190+ before(function(done) {
1191+ Y = YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils'],
1192+ function(Y) {
1193+ container = Y.Node.create(
1194+ '<div id="test" class="container"></div>');
1195+ done();
1196+ });
1197 });
1198
1199 it('should be able to handle env connection status changes', function() {
1200 var juju = Y.namespace('juju'),
1201- conn = new (Y.namespace('juju-tests.utils')).SocketStub(),
1202+ conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
1203 env = new juju.Environment({conn: conn}),
1204 app = new Y.juju.App({env: env, container: container}),
1205 reset_called = false,
1206 noop = function() {return this;};
1207
1208-
1209 // mock the db
1210 app.db = {
1211 // mock out notifications
1212@@ -166,15 +181,23 @@
1213 });
1214
1215 });
1216-});
1217-
1218-YUI(GlobalConfig).use(['juju-models', 'juju-gui', 'datasource-local',
1219- 'juju-tests-utils', 'json-stringify'], function(Y) {
1220+})();
1221+
1222+
1223+(function() {
1224+
1225 describe('Application prefetching', function() {
1226- var models, conn, env, app, container, charm_store, data, juju;
1227+ var Y, models, conn, env, app, container, charm_store, data, juju;
1228
1229- before(function() {
1230- models = Y.namespace('juju.models');
1231+ before(function(done) {
1232+ console.log('Loading App prefetch test code');
1233+ Y = YUI(GlobalConfig).use(
1234+ ['juju-gui', 'datasource-local',
1235+ 'juju-views', 'juju-templates',
1236+ 'juju-tests-utils', 'json-stringify'], function(Y) {
1237+ models = Y.namespace('juju.models');
1238+ done();
1239+ });
1240 });
1241
1242 beforeEach(function() {
1243@@ -243,4 +266,4 @@
1244 get_endpoints_count.should.equal(2);
1245 });
1246 });
1247-});
1248+})();
1249
1250=== modified file 'test/test_app_hotkeys.js'
1251--- test/test_app_hotkeys.js 2013-01-07 20:10:00 +0000
1252+++ test/test_app_hotkeys.js 2013-01-09 16:44:42 +0000
1253@@ -1,11 +1,11 @@
1254 'use strict';
1255
1256-YUI(GlobalConfig).use(['juju-gui', 'juju-tests-utils', 'node-event-simulate'],
1257- function(Y) {
1258- describe('application hotkeys', function() {
1259- var app, container, windowNode;
1260+describe('application hotkeys', function() {
1261+ var app, container, windowNode, Y;
1262
1263- before(function(done) {
1264+ before(function(done) {
1265+ Y = YUI(GlobalConfig).use(
1266+ ['juju-gui', 'juju-tests-utils', 'node-event-simulate'], function(Y) {
1267 var env = {
1268 after: function() {},
1269 get: function() {},
1270@@ -22,43 +22,45 @@
1271 done();
1272 });
1273
1274- beforeEach(function() {
1275- container = Y.Node.create('<div/>');
1276- Y.one('#main').append(container);
1277- app.render();
1278- });
1279-
1280- afterEach(function() {
1281- container.remove(true);
1282- });
1283-
1284- it('should listen for alt-S events', function() {
1285- var searchInput = Y.Node.create('<input/>');
1286- searchInput.set('id', 'charm-search-field');
1287- container.append(searchInput);
1288- windowNode.simulate('keydown', {
1289- keyCode: 83, // "S" key.
1290- altKey: true
1291- });
1292- // Did charm-search-field get the focus?
1293- assert.equal(searchInput, Y.one(document.activeElement));
1294- });
1295-
1296- it('should listen for alt-E events', function() {
1297- var altEtriggered = false;
1298- app.on('navigateTo', function(ev) {
1299- if (ev && ev.url === '/') {
1300- altEtriggered = true;
1301- }
1302- // Avoid URL change performed by additional listeners.
1303- ev.stopImmediatePropagation();
1304- });
1305- windowNode.simulate('keydown', {
1306- keyCode: 69, // "E" key.
1307- altKey: true
1308- });
1309- assert.isTrue(altEtriggered);
1310- });
1311-
1312- });
1313- });
1314+ });
1315+
1316+ beforeEach(function() {
1317+ container = Y.Node.create('<div/>');
1318+ Y.one('#main').append(container);
1319+ app.render();
1320+ });
1321+
1322+ afterEach(function() {
1323+ container.remove(true);
1324+ });
1325+
1326+ it('should listen for alt-S events', function() {
1327+ var searchInput = Y.Node.create('<input/>');
1328+ searchInput.set('id', 'charm-search-field');
1329+ container.append(searchInput);
1330+ windowNode.simulate('keydown', {
1331+ keyCode: 83, // "S" key.
1332+ altKey: true
1333+ });
1334+ // Did charm-search-field get the focus?
1335+ assert.equal(searchInput, Y.one(document.activeElement));
1336+ });
1337+
1338+ it('should listen for alt-E events', function() {
1339+ var altEtriggered = false;
1340+ app.on('navigateTo', function(ev) {
1341+ if (ev && ev.url === '/') {
1342+ altEtriggered = true;
1343+ }
1344+ // Avoid URL change performed by additional listeners.
1345+ ev.stopImmediatePropagation();
1346+ });
1347+ windowNode.simulate('keydown', {
1348+ keyCode: 69, // "E" key.
1349+ altKey: true
1350+ });
1351+ assert.isTrue(altEtriggered);
1352+ });
1353+
1354+});
1355+
1356
1357=== modified file 'test/test_d3_components.js'
1358--- test/test_d3_components.js 2012-12-19 13:45:10 +0000
1359+++ test/test_d3_components.js 2013-01-09 16:44:42 +0000
1360@@ -32,7 +32,6 @@
1361 state.cancelled = true;
1362 }
1363 });
1364-
1365 done();
1366 });
1367 });
1368
1369=== modified file 'test/test_environment_view.js'
1370--- test/test_environment_view.js 2013-01-08 16:21:23 +0000
1371+++ test/test_environment_view.js 2013-01-09 16:44:42 +0000
1372@@ -303,12 +303,12 @@
1373 }).render();
1374 // Attach the view to the DOM so that sizes get set properly
1375 // from the viewport (only available from DOM).
1376- view.postRender();
1377+ view.rendered();
1378 var zoom_in = container.one('#zoom-in-btn'),
1379 zoom_out = container.one('#zoom-out-btn'),
1380 module = view.topo.modules.PanZoomModule,
1381 slider = module.slider,
1382- svg = container.one('svg g g');
1383+ svg = container.one('svg g');
1384
1385 zoom_in.simulate('click');
1386
1387@@ -339,13 +339,13 @@
1388 }).render();
1389 // Attach the view to the DOM so that sizes get set properly
1390 // from the viewport (only available from DOM).
1391- view.postRender();
1392+ view.rendered();
1393 var svg = Y.one('svg');
1394
1395- parseInt(svg.one('rect').getAttribute('height'), 10)
1396+ parseInt(svg.one('g').getAttribute('height'), 10)
1397 .should.equal(
1398 parseInt(svg.getComputedStyle('height'), 10));
1399- parseInt(svg.one('rect').getAttribute('width'), 10)
1400+ parseInt(svg.one('g').getAttribute('width'), 10)
1401 .should.equal(
1402 parseInt(svg.getComputedStyle('width'), 10));
1403 }
1404@@ -369,7 +369,7 @@
1405 }).render();
1406 // Attach the view to the DOM so that sizes get set properly
1407 // from the viewport (only available from DOM).
1408- view.postRender();
1409+ view.rendered();
1410 var svg = container.one('svg'),
1411 canvas = container.one('.topology');
1412 // We have to hide the canvas so it does not affect our calculations.
1413@@ -550,7 +550,7 @@
1414 db: db,
1415 env: env
1416 }).render();
1417- view.postRender();
1418+ view.rendered();
1419 var picker = container.one('.graph-list-picker'),
1420 button = picker.one('.picker-button');
1421 button.after('click', function() {
1422
1423=== modified file 'test/test_login.js'
1424--- test/test_login.js 2013-01-07 19:50:46 +0000
1425+++ test/test_login.js 2013-01-09 16:44:42 +0000
1426@@ -2,14 +2,13 @@
1427
1428 (function() {
1429
1430- var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
1431- var Y = YUI(GlobalConfig).use(requires);
1432-
1433 describe('environment login support', function() {
1434- var conn, env, utils, juju, makeLoginView, views, app;
1435+ var requires = ['node', 'juju-gui', 'juju-views', 'juju-tests-utils'];
1436+ var Y, conn, env, utils, juju, makeLoginView, views, app;
1437 var test = it; // We aren't really doing BDD so let's be more direct.
1438
1439 before(function() {
1440+ Y = YUI(GlobalConfig).use(requires);
1441 utils = Y.namespace('juju-tests.utils');
1442 juju = Y.namespace('juju');
1443 });
1444
1445=== modified file 'test/test_model.js'
1446--- test/test_model.js 2012-12-14 20:25:16 +0000
1447+++ test/test_model.js 2013-01-09 16:44:42 +0000
1448@@ -3,9 +3,10 @@
1449 describe('charm normalization', function() {
1450 var models;
1451
1452- before(function() {
1453+ before(function(done) {
1454 YUI(GlobalConfig).use('juju-models', 'juju-charm-models', function(Y) {
1455 models = Y.namespace('juju.models');
1456+ done()
1457 });
1458 });
1459
1460
1461=== modified file 'test/test_notifications.js'
1462--- test/test_notifications.js 2012-11-23 16:21:32 +0000
1463+++ test/test_notifications.js 2013-01-09 16:44:42 +0000
1464@@ -1,462 +1,477 @@
1465 'use strict';
1466
1467-YUI(GlobalConfig).use(['juju-gui', 'node-event-simulate', 'juju-tests-utils'],
1468+describe('notifications', function() {
1469+ var Y, juju, models, views;
1470+
1471+ var default_env = {
1472+ 'result': [
1473+ ['service', 'add', {
1474+ 'charm': 'cs:precise/wordpress-6',
1475+ 'id': 'wordpress',
1476+ 'exposed': false
1477+ }],
1478+ ['service', 'add', {
1479+ 'charm': 'cs:precise/mediawiki-3',
1480+ 'id': 'mediawiki',
1481+ 'exposed': false
1482+ }],
1483+ ['service', 'add', {
1484+ 'charm': 'cs:precise/mysql-6',
1485+ 'id': 'mysql'
1486+ }],
1487+ ['relation', 'add', {
1488+ 'interface': 'reversenginx',
1489+ 'scope': 'global',
1490+ 'endpoints':
1491+ [['wordpress', {'role': 'peer', 'name': 'loadbalancer'}]],
1492+ 'id': 'relation-0000000000'
1493+ }],
1494+ ['relation', 'add', {
1495+ 'interface': 'mysql',
1496+ 'scope': 'global',
1497+ 'endpoints':
1498+ [['mysql', {'role': 'server', 'name': 'db'}],
1499+ ['wordpress', {'role': 'client', 'name': 'db'}]],
1500+ 'id': 'relation-0000000001'
1501+ }],
1502+ ['machine', 'add', {
1503+ 'agent-state': 'running',
1504+ 'instance-state': 'running',
1505+ 'id': 0,
1506+ 'instance-id': 'local',
1507+ 'dns-name': 'localhost'
1508+ }],
1509+ ['unit', 'add', {
1510+ 'machine': 0,
1511+ 'agent-state': 'started',
1512+ 'public-address': '192.168.122.113',
1513+ 'id': 'wordpress/0'
1514+ }],
1515+ ['unit', 'add', {
1516+ 'machine': 0,
1517+ 'agent-state': 'error',
1518+ 'public-address': '192.168.122.222',
1519+ 'id': 'mysql/0'
1520+ }]
1521+ ],
1522+ 'op': 'delta'
1523+ };
1524+
1525+
1526+ before(function(done) {
1527+ Y = YUI(GlobalConfig).use([
1528+ 'juju-models',
1529+ 'juju-views',
1530+ 'juju-gui',
1531+ 'juju-env',
1532+ 'node-event-simulate',
1533+ 'juju-tests-utils'],
1534+
1535 function(Y) {
1536- describe('notifications', function() {
1537- var juju, models, views;
1538-
1539- var default_env = {
1540- 'result': [
1541- ['service', 'add', {
1542- 'charm': 'cs:precise/wordpress-6',
1543- 'id': 'wordpress',
1544- 'exposed': false
1545- }],
1546- ['service', 'add', {
1547- 'charm': 'cs:precise/mediawiki-3',
1548- 'id': 'mediawiki',
1549- 'exposed': false
1550- }],
1551- ['service', 'add', {
1552- 'charm': 'cs:precise/mysql-6',
1553- 'id': 'mysql'
1554- }],
1555- ['relation', 'add', {
1556- 'interface': 'reversenginx',
1557- 'scope': 'global',
1558- 'endpoints':
1559- [['wordpress', {'role': 'peer', 'name': 'loadbalancer'}]],
1560- 'id': 'relation-0000000000'
1561- }],
1562- ['relation', 'add', {
1563- 'interface': 'mysql',
1564- 'scope': 'global',
1565- 'endpoints':
1566- [['mysql', {'role': 'server', 'name': 'db'}],
1567- ['wordpress', {'role': 'client', 'name': 'db'}]],
1568- 'id': 'relation-0000000001'
1569- }],
1570- ['machine', 'add', {
1571- 'agent-state': 'running',
1572- 'instance-state': 'running',
1573- 'id': 0,
1574- 'instance-id': 'local',
1575- 'dns-name': 'localhost'
1576- }],
1577- ['unit', 'add', {
1578- 'machine': 0,
1579- 'agent-state': 'started',
1580- 'public-address': '192.168.122.113',
1581- 'id': 'wordpress/0'
1582- }],
1583- ['unit', 'add', {
1584- 'machine': 0,
1585- 'agent-state': 'error',
1586- 'public-address': '192.168.122.222',
1587- 'id': 'mysql/0'
1588- }]
1589- ],
1590- 'op': 'delta'
1591- };
1592-
1593-
1594- before(function() {
1595- juju = Y.namespace('juju');
1596- models = Y.namespace('juju.models');
1597- views = Y.namespace('juju.views');
1598- });
1599-
1600- it('must be able to make notification and lists of notifications',
1601- function() {
1602- var note1 = new models.Notification({
1603- title: 'test1',
1604- message: 'Hello'
1605- }),
1606- note2 = new models.Notification({
1607- title: 'test2',
1608- message: 'I said goodnight!'
1609- }),
1610- notifications = new models.NotificationList();
1611-
1612- notifications.add([note1, note2]);
1613- notifications.size().should.equal(2);
1614-
1615- // timestamp should be generated once
1616- var ts = note1.get('timestamp');
1617- note1.get('timestamp').should.equal(ts);
1618- // force an update so we can test ordering
1619- // fast execution can result in same timestamp
1620- note2.set('timestamp', ts + 1);
1621- note2.get('timestamp').should.be.above(ts);
1622-
1623- // defaults as expected
1624- note1.get('level').should.equal('info');
1625- note2.get('level').should.equal('info');
1626- // the sort order on the list should be by
1627- // timestamp
1628- notifications.get('title').should.eql(['test2', 'test1']);
1629- });
1630-
1631- it('must be able to render its view with sample data',
1632- function() {
1633- var note1 = new models.Notification({
1634- title: 'test1', message: 'Hello'}),
1635- note2 = new models.Notification({
1636- title: 'test2', message: 'I said goodnight!'}),
1637- notifications = new models.NotificationList(),
1638- container = Y.Node.create('<div id="test">'),
1639- env = new juju.Environment(),
1640- view = new views.NotificationsView({
1641- container: container,
1642- notifications: notifications,
1643- env: env});
1644- view.render();
1645- // Verify the expected elements appear in the view
1646- container.one('#notify-list').should.not.equal(undefined);
1647- container.destroy();
1648- });
1649-
1650- it('must be able to limit the size of notification events',
1651- function() {
1652- var note1 = new models.Notification({
1653- title: 'test1',
1654- message: 'Hello'
1655- }),
1656- note2 = new models.Notification({
1657- title: 'test2',
1658- message: 'I said goodnight!'
1659- }),
1660- note3 = new models.Notification({
1661- title: 'test3',
1662- message: 'Never remember'
1663- }),
1664- notifications = new models.NotificationList({
1665- max_size: 2
1666- });
1667-
1668- notifications.add([note1, note2]);
1669- notifications.size().should.equal(2);
1670-
1671- // Adding a new notification should pop the oldest from the list
1672- // (we exceed max_size)
1673- notifications.add(note3);
1674- notifications.size().should.equal(2);
1675- notifications.get('title').should.eql(['test3', 'test2']);
1676- });
1677-
1678- it('must be able to get notifications for a given model',
1679- function() {
1680- var m = new models.Service({id: 'mediawiki'}),
1681- note1 = new models.Notification({
1682- title: 'test1',
1683- message: 'Hello',
1684- modelId: m
1685- }),
1686- note2 = new models.Notification({
1687- title: 'test2',
1688- message: 'I said goodnight!'
1689- }),
1690- notifications = new models.NotificationList();
1691-
1692- notifications.add([note1, note2]);
1693- notifications.size().should.equal(2);
1694- notifications.getNotificationsForModel(m).should.eql(
1695- [note1]);
1696-
1697- });
1698-
1699- it('must be able to include and show object links', function() {
1700- var container = Y.Node.create('<div id="test">'),
1701- conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
1702- env = new juju.Environment({conn: conn}),
1703- app = new Y.juju.App({env: env, container: container}),
1704- db = app.db,
1705- mw = db.services.create({id: 'mediawiki',
1706- name: 'mediawiki'}),
1707- notifications = db.notifications,
1708- view = new views.NotificationsOverview({
1709- container: container,
1710- notifications: notifications,
1711- app: app,
1712- env: env}).render();
1713- // we use overview here for testing as it defaults
1714- // to showing all notices
1715-
1716- // we can use app's routing table to derive a link
1717- notifications.create({title: 'Service Down',
1718- message: 'Your service has an error',
1719- link: app.getModelURL(mw)
1720- });
1721- view.render();
1722- var link = container.one('.notice').one('a');
1723- link.getAttribute('href').should.equal(
1724- '/service/mediawiki/');
1725- link.getHTML().should.contain('View Details');
1726-
1727-
1728- // create a new notice passing the link_title
1729- notifications.create({title: 'Service Down',
1730- message: 'Your service has an error',
1731- link: app.getModelURL(mw),
1732- link_title: 'Resolve this'
1733- });
1734- view.render();
1735- link = container.one('.notice').one('a');
1736- link.getAttribute('href').should.equal(
1737- '/service/mediawiki/');
1738- link.getHTML().should.contain('Resolve this');
1739- });
1740-
1741- it('must be able to evict irrelevant notices', function() {
1742- var container = Y.Node.create(
1743- '<div id="test" class="container"></div>'),
1744- conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
1745- env = new juju.Environment({conn: conn}),
1746- app = new Y.juju.App({
1747- env: env,
1748- container: container,
1749- viewContainer: container
1750- });
1751- var environment_delta = default_env;
1752-
1753- var notifications = app.db.notifications,
1754- view = new views.NotificationsView({
1755- container: container,
1756- notifications: notifications,
1757- env: app.env}).render();
1758-
1759-
1760- app.env.dispatch_result(environment_delta);
1761-
1762-
1763- notifications.size().should.equal(7);
1764- // we have one unit in error
1765- view.getShowable().length.should.equal(1);
1766-
1767- // now fire another delta event marking that node as
1768- // started
1769- app.env.dispatch_result({result: [['unit', 'change', {
1770- 'machine': 0,
1771- 'agent-state': 'started',
1772- 'public-address': '192.168.122.222',
1773- 'id': 'mysql/0'
1774- }]], op: 'delta'});
1775- notifications.size().should.equal(8);
1776- // This should have evicted the prior notice from seen
1777- view.getShowable().length.should.equal(0);
1778- });
1779-
1780- it('must properly construct title and message based on level from ' +
1781- 'event data',
1782- function() {
1783- var container = Y.Node.create(
1784- '<div id="test" class="container"></div>'),
1785- app = new Y.juju.App({
1786- container: container,
1787- viewContainer: container
1788- });
1789- var environment_delta = {
1790- 'result': [
1791- ['service', 'add', {
1792- 'charm': 'cs:precise/wordpress-6',
1793- 'id': 'wordpress'
1794- }],
1795- ['service', 'add', {
1796- 'charm': 'cs:precise/mediawiki-3',
1797- 'id': 'mediawiki'
1798- }],
1799- ['service', 'add', {
1800- 'charm': 'cs:precise/mysql-6',
1801- 'id': 'mysql'
1802- }],
1803- ['unit', 'add', {
1804- 'agent-state': 'install-error',
1805- 'id': 'wordpress/0'
1806- }],
1807- ['unit', 'add', {
1808- 'agent-state': 'error',
1809- 'public-address': '192.168.122.222',
1810- 'id': 'mysql/0'
1811- }],
1812- ['unit', 'add', {
1813- 'public-address': '192.168.122.222',
1814- 'id': 'mysql/2'
1815- }]
1816- ],
1817- 'op': 'delta'
1818- };
1819-
1820- var notifications = app.db.notifications,
1821- view = new views.NotificationsView({
1822- container: container,
1823- notifications: notifications,
1824- app: app,
1825- env: app.env}).render();
1826-
1827- app.env.dispatch_result(environment_delta);
1828-
1829- notifications.size().should.equal(6);
1830- // we have one unit in error
1831- var showable = view.getShowable();
1832- showable.length.should.equal(2);
1833- // The first showable notification should indicate an error.
1834- showable[0].level.should.equal('error');
1835- showable[0].title.should.equal('Error with mysql/0');
1836- showable[0].message.should.equal('Agent-state = error.');
1837- // The second showable notification should also indicate an error.
1838- showable[1].level.should.equal('error');
1839- showable[1].title.should.equal('Error with wordpress/0');
1840- showable[1].message.should.equal('Agent-state = install-error.');
1841- // The first non-error notice should have an 'info' level and less
1842- // severe messaging.
1843- var notice = notifications.item(0);
1844- notice.get('level').should.equal('info');
1845- notice.get('title').should.equal('Problem with mysql/2');
1846- notice.get('message').should.equal('');
1847- });
1848-
1849-
1850- it('should open on click and close on clickoutside', function(done) {
1851- var container = Y.Node.create('<div id="test-container" ' +
1852- 'style="display: none" class="container"/>'),
1853- notifications = new models.NotificationList(),
1854- env = new juju.Environment(),
1855- view = new views.NotificationsView({
1856- container: container,
1857- notifications: notifications,
1858- env: env}).render(),
1859- indicator;
1860-
1861- Y.one('body').append(container);
1862- notifications.add({title: 'testing', 'level': 'error'});
1863- indicator = container.one('#notify-indicator');
1864-
1865- indicator.simulate('click');
1866- indicator.ancestor().hasClass('open').should.equal(true);
1867-
1868- Y.one('body').simulate('click');
1869- indicator.ancestor().hasClass('open').should.equal(false);
1870-
1871- container.remove();
1872- done();
1873- });
1874- });
1875-
1876- describe('changing notifications to words', function() {
1877- var juju;
1878-
1879- before(function() {
1880- juju = Y.namespace('juju');
1881- });
1882-
1883- it('should correctly translate notification operations into English',
1884- function() {
1885- assert.equal(juju._changeNotificationOpToWords('add'), 'created');
1886- assert.equal(juju._changeNotificationOpToWords('remove'),
1887- 'removed');
1888- assert.equal(juju._changeNotificationOpToWords('not-an-op'),
1889- 'changed');
1890- });
1891- });
1892-
1893- describe('relation notifications', function() {
1894- var juju;
1895-
1896- before(function() {
1897- juju = Y.namespace('juju');
1898- });
1899-
1900- it('should produce reasonable titles', function() {
1901- assert.equal(
1902- juju._relationNotifications.title(undefined, 'add'),
1903- 'Relation created');
1904- assert.equal(
1905- juju._relationNotifications.title(undefined, 'remove'),
1906- 'Relation removed');
1907- });
1908-
1909- it('should generate messages about two-party relations', function() {
1910- var changeData =
1911- { endpoints:
1912- [['endpoint0', {name: 'relation0'}],
1913- ['endpoint1', {name: 'relation1'}]]};
1914- assert.equal(
1915- juju._relationNotifications.message(undefined, 'add',
1916- changeData), 'Relation between endpoint0 (relation type ' +
1917- '"relation0") and endpoint1 (relation type "relation1") ' +
1918- 'was created');
1919- });
1920-
1921- it('should generate messages about one-party relations', function() {
1922- var changeData =
1923- { endpoints:
1924- [['endpoint1', {name: 'relation1'}]]};
1925- assert.equal(
1926- juju._relationNotifications.message(undefined, 'add',
1927- changeData), 'Relation with endpoint1 (relation type ' +
1928- '"relation1") was created');
1929- });
1930- });
1931-
1932- describe('notification visual feedback', function() {
1933- var env, models, notifications, notificationsView, notifierBox, views;
1934-
1935- before(function() {
1936+ juju = Y.namespace('juju');
1937+ models = Y.namespace('juju.models');
1938+ views = Y.namespace('juju.views');
1939+ done();
1940+ });
1941+ });
1942+
1943+ it('must be able to make notification and lists of notifications',
1944+ function() {
1945+ var note1 = new models.Notification({
1946+ title: 'test1',
1947+ message: 'Hello'
1948+ }),
1949+ note2 = new models.Notification({
1950+ title: 'test2',
1951+ message: 'I said goodnight!'
1952+ }),
1953+ notifications = new models.NotificationList();
1954+
1955+ notifications.add([note1, note2]);
1956+ notifications.size().should.equal(2);
1957+
1958+ // timestamp should be generated once
1959+ var ts = note1.get('timestamp');
1960+ note1.get('timestamp').should.equal(ts);
1961+ // force an update so we can test ordering
1962+ // fast execution can result in same timestamp
1963+ note2.set('timestamp', ts + 1);
1964+ note2.get('timestamp').should.be.above(ts);
1965+
1966+ // defaults as expected
1967+ note1.get('level').should.equal('info');
1968+ note2.get('level').should.equal('info');
1969+ // the sort order on the list should be by
1970+ // timestamp
1971+ notifications.get('title').should.eql(['test2', 'test1']);
1972+ });
1973+
1974+ it('must be able to render its view with sample data',
1975+ function() {
1976+ var note1 = new models.Notification({
1977+ title: 'test1', message: 'Hello'}),
1978+ note2 = new models.Notification({
1979+ title: 'test2', message: 'I said goodnight!'}),
1980+ notifications = new models.NotificationList(),
1981+ container = Y.Node.create('<div id="test">'),
1982+ env = new juju.Environment(),
1983+ view = new views.NotificationsView({
1984+ container: container,
1985+ notifications: notifications,
1986+ env: env});
1987+ view.render();
1988+ // Verify the expected elements appear in the view
1989+ container.one('#notify-list').should.not.equal(undefined);
1990+ container.destroy();
1991+ });
1992+
1993+ it('must be able to limit the size of notification events',
1994+ function() {
1995+ var note1 = new models.Notification({
1996+ title: 'test1',
1997+ message: 'Hello'
1998+ }),
1999+ note2 = new models.Notification({
2000+ title: 'test2',
2001+ message: 'I said goodnight!'
2002+ }),
2003+ note3 = new models.Notification({
2004+ title: 'test3',
2005+ message: 'Never remember'
2006+ }),
2007+ notifications = new models.NotificationList({
2008+ max_size: 2
2009+ });
2010+
2011+ notifications.add([note1, note2]);
2012+ notifications.size().should.equal(2);
2013+
2014+ // Adding a new notification should pop the oldest from the list (we
2015+ // exceed max_size)
2016+ notifications.add(note3);
2017+ notifications.size().should.equal(2);
2018+ notifications.get('title').should.eql(['test3', 'test2']);
2019+ });
2020+
2021+ it('must be able to get notifications for a given model',
2022+ function() {
2023+ var m = new models.Service({id: 'mediawiki'}),
2024+ note1 = new models.Notification({
2025+ title: 'test1',
2026+ message: 'Hello',
2027+ modelId: m
2028+ }),
2029+ note2 = new models.Notification({
2030+ title: 'test2',
2031+ message: 'I said goodnight!'
2032+ }),
2033+ notifications = new models.NotificationList();
2034+
2035+ notifications.add([note1, note2]);
2036+ notifications.size().should.equal(2);
2037+ notifications.getNotificationsForModel(m).should.eql(
2038+ [note1]);
2039+
2040+ });
2041+
2042+ it('must be able to include and show object links', function() {
2043+ var container = Y.Node.create('<div id="test">'),
2044+ conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
2045+ env = new juju.Environment({conn: conn}),
2046+ app = new Y.juju.App({env: env, container: container}),
2047+ db = app.db,
2048+ mw = db.services.create({id: 'mediawiki',
2049+ name: 'mediawiki'}),
2050+ notifications = db.notifications,
2051+ view = new views.NotificationsOverview({
2052+ container: container,
2053+ notifications: notifications,
2054+ app: app,
2055+ env: env}).render();
2056+ // we use overview here for testing as it defaults
2057+ // to showing all notices
2058+
2059+ // we can use app's routing table to derive a link
2060+ notifications.create({title: 'Service Down',
2061+ message: 'Your service has an error',
2062+ link: app.getModelURL(mw)
2063+ });
2064+ view.render();
2065+ var link = container.one('.notice').one('a');
2066+ link.getAttribute('href').should.equal(
2067+ '/service/mediawiki/');
2068+ link.getHTML().should.contain('View Details');
2069+
2070+
2071+ // create a new notice passing the link_title
2072+ notifications.create({title: 'Service Down',
2073+ message: 'Your service has an error',
2074+ link: app.getModelURL(mw),
2075+ link_title: 'Resolve this'
2076+ });
2077+ view.render();
2078+ link = container.one('.notice').one('a');
2079+ link.getAttribute('href').should.equal(
2080+ '/service/mediawiki/');
2081+ link.getHTML().should.contain('Resolve this');
2082+ });
2083+
2084+ it('must be able to evict irrelevant notices', function() {
2085+ var container = Y.Node.create(
2086+ '<div id="test" class="container"></div>'),
2087+ conn = new(Y.namespace('juju-tests.utils')).SocketStub(),
2088+ env = new juju.Environment({conn: conn}),
2089+ app = new Y.juju.App({
2090+ env: env,
2091+ container: container,
2092+ viewContainer: container
2093+ });
2094+ var environment_delta = default_env;
2095+
2096+ var notifications = app.db.notifications,
2097+ view = new views.NotificationsView({
2098+ container: container,
2099+ notifications: notifications,
2100+ env: app.env}).render();
2101+
2102+
2103+ app.env.dispatch_result(environment_delta);
2104+
2105+
2106+ notifications.size().should.equal(7);
2107+ // we have one unit in error
2108+ view.getShowable().length.should.equal(1);
2109+
2110+ // now fire another delta event marking that node as
2111+ // started
2112+ app.env.dispatch_result({result: [['unit', 'change', {
2113+ 'machine': 0,
2114+ 'agent-state': 'started',
2115+ 'public-address': '192.168.122.222',
2116+ 'id': 'mysql/0'
2117+ }]], op: 'delta'});
2118+ notifications.size().should.equal(8);
2119+ // This should have evicted the prior notice from seen
2120+ view.getShowable().length.should.equal(0);
2121+ });
2122+
2123+ it('must properly construct title and message based on level from ' +
2124+ 'event data',
2125+ function() {
2126+ var container = Y.Node.create(
2127+ '<div id="test" class="container"></div>'),
2128+ app = new Y.juju.App({
2129+ container: container,
2130+ viewContainer: container
2131+ });
2132+ var environment_delta = {
2133+ 'result': [
2134+ ['service', 'add', {
2135+ 'charm': 'cs:precise/wordpress-6',
2136+ 'id': 'wordpress'
2137+ }],
2138+ ['service', 'add', {
2139+ 'charm': 'cs:precise/mediawiki-3',
2140+ 'id': 'mediawiki'
2141+ }],
2142+ ['service', 'add', {
2143+ 'charm': 'cs:precise/mysql-6',
2144+ 'id': 'mysql'
2145+ }],
2146+ ['unit', 'add', {
2147+ 'agent-state': 'install-error',
2148+ 'id': 'wordpress/0'
2149+ }],
2150+ ['unit', 'add', {
2151+ 'agent-state': 'error',
2152+ 'public-address': '192.168.122.222',
2153+ 'id': 'mysql/0'
2154+ }],
2155+ ['unit', 'add', {
2156+ 'public-address': '192.168.122.222',
2157+ 'id': 'mysql/2'
2158+ }]
2159+ ],
2160+ 'op': 'delta'
2161+ };
2162+
2163+ var notifications = app.db.notifications,
2164+ view = new views.NotificationsView({
2165+ container: container,
2166+ notifications: notifications,
2167+ app: app,
2168+ env: app.env}).render();
2169+
2170+ app.env.dispatch_result(environment_delta);
2171+
2172+ notifications.size().should.equal(6);
2173+ // we have one unit in error
2174+ var showable = view.getShowable();
2175+ showable.length.should.equal(2);
2176+ // The first showable notification should indicate an error.
2177+ showable[0].level.should.equal('error');
2178+ showable[0].title.should.equal('Error with mysql/0');
2179+ showable[0].message.should.equal('Agent-state = error.');
2180+ // The second showable notification should also indicate an error.
2181+ showable[1].level.should.equal('error');
2182+ showable[1].title.should.equal('Error with wordpress/0');
2183+ showable[1].message.should.equal('Agent-state = install-error.');
2184+ // The first non-error notice should have an 'info' level and less
2185+ // severe messaging.
2186+ var notice = notifications.item(0);
2187+ notice.get('level').should.equal('info');
2188+ notice.get('title').should.equal('Problem with mysql/2');
2189+ notice.get('message').should.equal('');
2190+ });
2191+
2192+
2193+ it('should open on click and close on clickoutside', function(done) {
2194+ var container = Y.Node.create(
2195+ '<div id="test-container" style="display: none" class="container"/>'),
2196+ notifications = new models.NotificationList(),
2197+ env = new juju.Environment(),
2198+ view = new views.NotificationsView({
2199+ container: container,
2200+ notifications: notifications,
2201+ env: env}).render(),
2202+ indicator;
2203+
2204+ Y.one('body').append(container);
2205+ notifications.add({title: 'testing', 'level': 'error'});
2206+ indicator = container.one('#notify-indicator');
2207+
2208+ indicator.simulate('click');
2209+ indicator.ancestor().hasClass('open').should.equal(true);
2210+
2211+ Y.one('body').simulate('click');
2212+ indicator.ancestor().hasClass('open').should.equal(false);
2213+
2214+ container.remove();
2215+ done();
2216+ });
2217+
2218+});
2219+
2220+
2221+describe('changing notifications to words', function() {
2222+ var Y, juju;
2223+
2224+ before(function(done) {
2225+ Y = YUI(GlobalConfig).use(
2226+ ['juju-notification-controller'],
2227+ function(Y) {
2228+ juju = Y.namespace('juju');
2229+ done();
2230+ });
2231+ });
2232+
2233+ it('should correctly translate notification operations into English',
2234+ function() {
2235+ assert.equal(juju._changeNotificationOpToWords('add'), 'created');
2236+ assert.equal(juju._changeNotificationOpToWords('remove'), 'removed');
2237+ assert.equal(juju._changeNotificationOpToWords('not-an-op'), 'changed');
2238+ });
2239+});
2240+
2241+describe('relation notifications', function() {
2242+ var Y, juju;
2243+
2244+ before(function(done) {
2245+ Y = YUI(GlobalConfig).use(
2246+ ['juju-notification-controller'],
2247+ function(Y) {
2248+ juju = Y.namespace('juju');
2249+ done();
2250+ });
2251+ });
2252+
2253+ it('should produce reasonable titles', function() {
2254+ assert.equal(
2255+ juju._relationNotifications.title(undefined, 'add'),
2256+ 'Relation created');
2257+ assert.equal(
2258+ juju._relationNotifications.title(undefined, 'remove'),
2259+ 'Relation removed');
2260+ });
2261+
2262+ it('should generate messages about two-party relations', function() {
2263+ var changeData =
2264+ { endpoints:
2265+ [['endpoint0', {name: 'relation0'}],
2266+ ['endpoint1', {name: 'relation1'}]]};
2267+ assert.equal(
2268+ juju._relationNotifications.message(undefined, 'add', changeData),
2269+ 'Relation between endpoint0 (relation type "relation0") and ' +
2270+ 'endpoint1 (relation type "relation1") was created');
2271+ });
2272+
2273+ it('should generate messages about one-party relations', function() {
2274+ var changeData =
2275+ { endpoints:
2276+ [['endpoint1', {name: 'relation1'}]]};
2277+ assert.equal(
2278+ juju._relationNotifications.message(undefined, 'add', changeData),
2279+ 'Relation with endpoint1 (relation type "relation1") was created');
2280+ });
2281+});
2282+
2283+describe('notification visual feedback', function() {
2284+ var env, models, notifications, notificationsView, notifierBox, views, Y;
2285+
2286+ before(function(done) {
2287+ Y = YUI(GlobalConfig).use('juju-env', 'juju-models', 'juju-views',
2288+ function(Y) {
2289 var juju = Y.namespace('juju');
2290 env = new juju.Environment();
2291 models = Y.namespace('juju.models');
2292 views = Y.namespace('juju.views');
2293- });
2294-
2295- // Instantiate the notifications model list and view.
2296- // Also create the notifier box and attach it as first element of the
2297- // body.
2298- beforeEach(function() {
2299- notifications = new models.NotificationList();
2300- notificationsView = new views.NotificationsView({
2301- env: env,
2302- notifications: notifications
2303- });
2304- notifierBox = Y.Node.create('<div id="notifier-box"></div>');
2305- notifierBox.setStyle('display', 'none');
2306- Y.one('body').prepend(notifierBox);
2307- });
2308-
2309- // Destroy the notifier box created in beforeEach.
2310- afterEach(function() {
2311- notifierBox.remove();
2312- notifierBox.destroy(true);
2313- });
2314-
2315- // Assert the notifier box contains the expectedNumber of notifiers.
2316- var assertNumNotifiers = function(expectedNumber) {
2317- assert.equal(expectedNumber, notifierBox.get('children').size());
2318- };
2319-
2320- it('should appear when a new error is notified', function() {
2321- notifications.add({title: 'mytitle', level: 'error'});
2322- assertNumNotifiers(1);
2323- });
2324-
2325- it('should only appear when the DOM contains the notifier box',
2326- function() {
2327- notifierBox.remove();
2328- notifications.add({title: 'mytitle', level: 'error'});
2329- assertNumNotifiers(0);
2330- });
2331-
2332- it('should not appear when the notification is not an error',
2333- function() {
2334- notifications.add({title: 'mytitle', level: 'info'});
2335- assertNumNotifiers(0);
2336- });
2337-
2338- it('should not appear when the notification comes form delta',
2339- function() {
2340- notifications.add({title: 'mytitle', level: 'error', isDelta:
2341- true});
2342- assertNumNotifiers(0);
2343- });
2344-
2345- });
2346+ done();
2347+ });
2348+ });
2349+
2350+ // Instantiate the notifications model list and view.
2351+ // Also create the notifier box and attach it as first element of the body.
2352+ beforeEach(function() {
2353+ notifications = new models.NotificationList();
2354+ notificationsView = new views.NotificationsView({
2355+ env: env,
2356+ notifications: notifications
2357 });
2358+ notifierBox = Y.Node.create('<div id="notifier-box"></div>');
2359+ notifierBox.setStyle('display', 'none');
2360+ Y.one('body').prepend(notifierBox);
2361+ });
2362+
2363+ // Destroy the notifier box created in beforeEach.
2364+ afterEach(function() {
2365+ notifierBox.remove();
2366+ notifierBox.destroy(true);
2367+ });
2368+
2369+ // Assert the notifier box contains the expectedNumber of notifiers.
2370+ var assertNumNotifiers = function(expectedNumber) {
2371+ assert.equal(expectedNumber, notifierBox.get('children').size());
2372+ };
2373+
2374+ it('should appear when a new error is notified', function() {
2375+ notifications.add({title: 'mytitle', level: 'error'});
2376+ assertNumNotifiers(1);
2377+ });
2378+
2379+ it('should only appear when the DOM contains the notifier box', function() {
2380+ notifierBox.remove();
2381+ notifications.add({title: 'mytitle', level: 'error'});
2382+ assertNumNotifiers(0);
2383+ });
2384+
2385+ it('should not appear when the notification is not an error', function() {
2386+ notifications.add({title: 'mytitle', level: 'info'});
2387+ assertNumNotifiers(0);
2388+ });
2389+
2390+ it('should not appear when the notification comes form delta', function() {
2391+ notifications.add({title: 'mytitle', level: 'error', isDelta: true});
2392+ assertNumNotifiers(0);
2393+ });
2394+
2395+});
2396
2397=== modified file 'test/test_notifier_widget.js'
2398--- test/test_notifier_widget.js 2012-11-23 16:21:32 +0000
2399+++ test/test_notifier_widget.js 2013-01-09 16:44:42 +0000
2400@@ -1,101 +1,104 @@
2401 'use strict';
2402
2403-YUI(GlobalConfig).use(['notifier', 'node-event-simulate'], function(Y) {
2404- describe('notifier widget', function() {
2405- var Notifier, notifierBox;
2406-
2407- before(function() {
2408- Notifier = Y.namespace('juju.widgets').Notifier;
2409- });
2410-
2411- // Create the notifier box and attach it as first element of the body.
2412- beforeEach(function() {
2413- notifierBox = Y.Node.create('<div id="notifier-box"></div>');
2414- notifierBox.setStyle('display', 'none');
2415- Y.one('body').prepend(notifierBox);
2416- });
2417-
2418- // Destroy the notifier box created in beforeEach.
2419- afterEach(function() {
2420- notifierBox.remove();
2421- notifierBox.destroy(true);
2422- });
2423-
2424- // Factory rendering and returning a notifier instance.
2425- var makeNotifier = function(title, message, timeout) {
2426- var notifier = new Notifier({
2427- title: title || 'mytitle',
2428- message: message || 'mymessage',
2429- timeout: timeout || 10000
2430- });
2431- notifier.render(notifierBox);
2432- return notifier;
2433- };
2434-
2435- // Assert the notifier box contains the expectedNumber of notifiers.
2436- var assertNumNotifiers = function(expectedNumber) {
2437- assert.equal(expectedNumber, notifierBox.get('children').size());
2438- };
2439-
2440- it('should be able to display a notification', function() {
2441+
2442+describe('notifier widget', function() {
2443+ var Notifier, notifierBox, Y;
2444+
2445+ before(function(done) {
2446+ Y = YUI(GlobalConfig).use(['notifier', 'node-event-simulate'], function(Y) {
2447+ Notifier = Y.namespace('juju.widgets').Notifier;
2448+ done();
2449+ });
2450+ });
2451+
2452+ // Create the notifier box and attach it as first element of the body.
2453+ beforeEach(function() {
2454+ notifierBox = Y.Node.create('<div id="notifier-box"></div>');
2455+ notifierBox.setStyle('display', 'none');
2456+ Y.one('body').prepend(notifierBox);
2457+ });
2458+
2459+ // Destroy the notifier box created in beforeEach.
2460+ afterEach(function() {
2461+ notifierBox.remove();
2462+ notifierBox.destroy(true);
2463+ });
2464+
2465+ // Factory rendering and returning a notifier instance.
2466+ var makeNotifier = function(title, message, timeout) {
2467+ var notifier = new Notifier({
2468+ title: title || 'mytitle',
2469+ message: message || 'mymessage',
2470+ timeout: timeout || 10000
2471+ });
2472+ notifier.render(notifierBox);
2473+ return notifier;
2474+ };
2475+
2476+ // Assert the notifier box contains the expectedNumber of notifiers.
2477+ var assertNumNotifiers = function(expectedNumber) {
2478+ assert.equal(expectedNumber, notifierBox.get('children').size());
2479+ };
2480+
2481+ it('should be able to display a notification', function() {
2482+ makeNotifier();
2483+ assertNumNotifiers(1);
2484+ });
2485+
2486+ it('should display the given title and message', function() {
2487+ makeNotifier('mytitle', 'mymessage');
2488+ var notifierNode = notifierBox.one('*');
2489+ assert.equal('mytitle', notifierNode.one('h3').getContent());
2490+ assert.equal('mymessage', notifierNode.one('div').getContent());
2491+ });
2492+
2493+ it('should be able to display multiple notifications', function() {
2494+ var number = 10;
2495+ for (var i = 0; i < number; i += 1) {
2496 makeNotifier();
2497+ }
2498+ assertNumNotifiers(number);
2499+ });
2500+
2501+ it('should display new notifications on top', function() {
2502+ makeNotifier('mytitle1', 'mymessage1');
2503+ makeNotifier('mytitle2', 'mymessage2');
2504+ var notifierNode = notifierBox.one('*');
2505+ assert.equal('mytitle2', notifierNode.one('h3').getContent());
2506+ assert.equal('mymessage2', notifierNode.one('div').getContent());
2507+ });
2508+
2509+ it('should destroy notifications after N milliseconds', function(done) {
2510+ makeNotifier('mytitle', 'mymessage', 1);
2511+ // A timeout of 250 milliseconds is used so that we ensure the destroying
2512+ // animation can be completed.
2513+ setTimeout(function() {
2514+ assertNumNotifiers(0);
2515+ done();
2516+ }, 250);
2517+ });
2518+
2519+ it('should destroy notifications on click', function(done) {
2520+ makeNotifier();
2521+ notifierBox.one('*').simulate('click');
2522+ // A timeout of 250 milliseconds is used so that we ensure the destroying
2523+ // animation can be completed.
2524+ setTimeout(function() {
2525+ assertNumNotifiers(0);
2526+ done();
2527+ }, 250);
2528+ });
2529+
2530+ it('should prevent notification removal on mouse enter', function(done) {
2531+ makeNotifier('mytitle', 'mymessage', 1);
2532+ notifierBox.one('*').simulate('mouseover');
2533+ // A timeout of 250 milliseconds is used so that we ensure the node is not
2534+ // preserved by the destroying animation.
2535+ setTimeout(function() {
2536 assertNumNotifiers(1);
2537- });
2538-
2539- it('should display the given title and message', function() {
2540- makeNotifier('mytitle', 'mymessage');
2541- var notifierNode = notifierBox.one('*');
2542- assert.equal('mytitle', notifierNode.one('h3').getContent());
2543- assert.equal('mymessage', notifierNode.one('div').getContent());
2544- });
2545-
2546- it('should be able to display multiple notifications', function() {
2547- var number = 10;
2548- for (var i = 0; i < number; i += 1) {
2549- makeNotifier();
2550- }
2551- assertNumNotifiers(number);
2552- });
2553-
2554- it('should display new notifications on top', function() {
2555- makeNotifier('mytitle1', 'mymessage1');
2556- makeNotifier('mytitle2', 'mymessage2');
2557- var notifierNode = notifierBox.one('*');
2558- assert.equal('mytitle2', notifierNode.one('h3').getContent());
2559- assert.equal('mymessage2', notifierNode.one('div').getContent());
2560- });
2561-
2562- it('should destroy notifications after N milliseconds', function(done) {
2563- makeNotifier('mytitle', 'mymessage', 1);
2564- // A timeout of 250 milliseconds is used so that we ensure the destroying
2565- // animation can be completed.
2566- setTimeout(function() {
2567- assertNumNotifiers(0);
2568- done();
2569- }, 250);
2570- });
2571-
2572- it('should destroy notifications on click', function(done) {
2573- makeNotifier();
2574- notifierBox.one('*').simulate('click');
2575- // A timeout of 250 milliseconds is used so that we ensure the destroying
2576- // animation can be completed.
2577- setTimeout(function() {
2578- assertNumNotifiers(0);
2579- done();
2580- }, 250);
2581- });
2582-
2583- it('should prevent notification removal on mouse enter', function(done) {
2584- makeNotifier('mytitle', 'mymessage', 1);
2585- notifierBox.one('*').simulate('mouseover');
2586- // A timeout of 250 milliseconds is used so that we ensure the node is not
2587- // preserved by the destroying animation.
2588- setTimeout(function() {
2589- assertNumNotifiers(1);
2590- done();
2591- }, 250);
2592- });
2593-
2594+ done();
2595+ }, 250);
2596 });
2597+
2598 });
2599+
2600
2601=== modified file 'test/test_panzoom.js'
2602--- test/test_panzoom.js 2013-01-02 12:57:09 +0000
2603+++ test/test_panzoom.js 2013-01-09 16:44:42 +0000
2604@@ -26,22 +26,16 @@
2605 db = new models.Database();
2606 var view = new views.environment({container: viewContainer, db: db});
2607 view.render();
2608- view.postRender();
2609+ view.rendered();
2610 pz = view.topo.modules.PanZoomModule;
2611 topo = pz.get('component');
2612 vis = topo.vis;
2613 });
2614
2615 afterEach(function() {
2616- viewContainer.remove(true);
2617+ viewContainer && viewContainer.remove(true);
2618 });
2619
2620- it('should set initial values',
2621- function() {
2622- pz._translate.should.eql([0, 0]);
2623- pz._scale.should.equal(1.0);
2624- });
2625-
2626 // Test the zoom handler calculations.
2627 it('should handle fractional values properly in zoom scale',
2628 function() {
2629@@ -52,7 +46,7 @@
2630 rescaleCalled = true;
2631 };
2632 pz.zoomHandler(evt);
2633- pz.slider.get('value').should.equal(60);
2634+ pz.slider.get('value').should.equal(61);
2635 assert.isTrue(rescaleCalled);
2636 });
2637
2638@@ -82,54 +76,49 @@
2639
2640 // Test the zoom calculations.
2641 it('should handle fractional values within the limit for rescale',
2642- function(done) {
2643+ function() {
2644 // Floor is used so the scale will round down.
2645 var evt =
2646 { scale: 0.609,
2647- translate: 't'};
2648+ translate: [0, 0]};
2649 var rescaled = false;
2650 topo.once('rescaled', function() {
2651 rescaled = true;
2652- done();
2653 });
2654- pz.rescale(vis, evt);
2655- pz._scale.should.equal(0.609);
2656+ pz.rescale(evt);
2657+ topo.get('scale').should.equal(0.609);
2658 var expected = 'translate(' + evt.translate + ') scale(0.609)';
2659 vis.attr('transform').should.equal(expected);
2660 assert.isTrue(rescaled);
2661 });
2662
2663 it('should set an upper limit for rescale',
2664- function(done) {
2665+ function() {
2666 var evt =
2667 { scale: 2.1,
2668- translate: 'u'};
2669+ translate: [0, 0]};
2670 var rescaled = false;
2671 topo.once('rescaled', function() {
2672 rescaled = true;
2673- done();
2674 });
2675- topo.set('scale', 2.0);
2676- pz.rescale(vis, evt);
2677- pz._scale.should.equal(2.0);
2678+ pz.rescale(evt);
2679+ topo.get('scale').should.equal(2.0);
2680 var expected = 'translate(' + evt.translate + ') scale(2)';
2681 vis.attr('transform').should.equal(expected);
2682 assert.isTrue(rescaled);
2683 });
2684
2685 it('should set a lower limit for rescale',
2686- function(done) {
2687+ function() {
2688 var evt =
2689 { scale: 0.2,
2690- translate: 'v'};
2691+ translate: [0, 0]};
2692 var rescaled = false;
2693 topo.once('rescaled', function() {
2694 rescaled = true;
2695- done();
2696 });
2697- topo.set('scale', 0.25);
2698- pz.rescale(vis, evt);
2699- pz._scale.should.equal(0.25);
2700+ pz.rescale(evt);
2701+ topo.get('scale').should.equal(0.25);
2702 var expected = 'translate(' + evt.translate + ') scale(0.25)';
2703 vis.attr('transform').should.equal(expected);
2704 assert.isTrue(rescaled);
2705
2706=== modified file 'test/test_service_config_view.js'
2707--- test/test_service_config_view.js 2012-10-25 08:18:43 +0000
2708+++ test/test_service_config_view.js 2013-01-09 16:44:42 +0000
2709@@ -29,7 +29,7 @@
2710 });
2711
2712 after(function(done) {
2713- env.destroy();
2714+ env && env.destroy();
2715 done();
2716 });
2717
2718
2719=== modified file 'test/test_topology.js'
2720--- test/test_topology.js 2012-12-19 13:45:10 +0000
2721+++ test/test_topology.js 2013-01-09 16:44:42 +0000
2722@@ -81,6 +81,7 @@
2723 topo.setAttrs({container: container, db: db});
2724 topo.addModule(views.MegaModule);
2725 topo.addModule(views.PanZoomModule);
2726+ topo.addModule(views.ViewportModule);
2727 return topo;
2728 }
2729
2730
2731=== modified file 'undocumented'
2732--- undocumented 2013-01-07 21:02:21 +0000
2733+++ undocumented 2013-01-09 16:44:42 +0000
2734@@ -1,5 +1,31 @@
2735-app/app.js:95 "callback"
2736-app/app.js:552 "callback"
2737+app/app.js:599 "callback"
2738+app/app.js:100 "callback"
2739+app/store/env.js:180 "deploy"
2740+app/store/env.js:223 "status"
2741+app/store/env.js:198 "expose"
2742+app/store/env.js:219 "unexpose"
2743+app/store/env.js:175 "get_service"
2744+app/store/env.js:69 "on_open"
2745+app/store/env.js:27 "initializer"
2746+app/store/env.js:78 "on_message"
2747+app/store/env.js:253 "set_constraints"
2748+app/store/env.js:157 "remove_units"
2749+app/store/env.js:260 "resolved"
2750+app/store/env.js:227 "remove_relation"
2751+app/store/env.js:137 "_send_rpc"
2752+app/store/env.js:73 "on_close"
2753+app/store/env.js:150 "add_unit"
2754+app/store/env.js:171 "get_charm"
2755+app/store/env.js:234 "destroy_service"
2756+app/store/env.js:45 "destructor"
2757+app/store/env.js:164 "add_relation"
2758+app/store/env.js:8 "Environment"
2759+app/store/env.js:111 "dispatch_result"
2760+app/store/env.js:117 "_dispatch_event"
2761+app/store/env.js:240 "set_config"
2762+app/store/env.js:126 "_dispatch_rpc_result"
2763+app/store/env.js:268 "get_endpoints"
2764+app/store/env.js:50 "connect"
2765 app/store/charm.js:66 "_normalizeCharms"
2766 app/store/charm.js:24 "find"
2767 app/store/charm.js:11 "success"
2768@@ -14,41 +40,38 @@
2769 app/store/notifications.js:25 "message"
2770 app/store/notifications.js:129 "title"
2771 app/store/notifications.js:137 "message"
2772-app/store/env.js:64 "on_close"
2773-app/store/env.js:69 "on_message"
2774-app/store/env.js:181 "status"
2775-app/store/env.js:132 "remove_units"
2776-app/store/env.js:198 "set_config"
2777-app/store/env.js:59 "on_open"
2778-app/store/env.js:185 "remove_relation"
2779-app/store/env.js:150 "get_service"
2780-app/store/env.js:155 "deploy"
2781-app/store/env.js:146 "get_charm"
2782-app/store/env.js:218 "resolved"
2783-app/store/env.js:211 "set_constraints"
2784-app/store/env.js:192 "destroy_service"
2785-app/store/env.js:226 "get_endpoints"
2786-app/store/env.js:5 "Environment"
2787-app/store/env.js:92 "_dispatch_event"
2788-app/store/env.js:40 "connect"
2789-app/store/env.js:173 "expose"
2790-app/store/env.js:101 "_dispatch_rpc_result"
2791-app/store/env.js:112 "_send_rpc"
2792-app/store/env.js:35 "destructor"
2793-app/store/env.js:125 "add_unit"
2794-app/store/env.js:177 "unexpose"
2795-app/store/env.js:86 "dispatch_result"
2796-app/store/env.js:139 "add_relation"
2797-app/store/env.js:22 "initializer"
2798-app/views/charm.js:32 "render"
2799-app/views/charm.js:96 "_deployCallback"
2800-app/views/charm.js:61 "on_charm_data"
2801-app/views/charm.js:141 "on_search_change"
2802-app/views/charm.js:69 "on_charm_deploy"
2803-app/views/charm.js:114 "initializer"
2804-app/views/charm.js:16 "initializer"
2805-app/views/charm.js:166 "on_results_change"
2806-app/views/charm.js:122 "render"
2807+app/views/utils.js:370 "_addAlertMessage"
2808+app/views/utils.js:825 "BoxPair"
2809+app/views/utils.js:702 "scale"
2810+app/views/utils.js:227 "humanizeNumber"
2811+app/views/utils.js:137 "console"
2812+app/views/utils.js:828 "pair"
2813+app/views/utils.js:113 "noop"
2814+app/views/utils.js:617 "Box"
2815+app/views/utils.js:250 "hasSVGClass"
2816+app/views/utils.js:338 "action"
2817+app/views/utils.js:134 "noop"
2818+app/views/utils.js:658 "get"
2819+app/views/utils.js:217 "renderable_charm"
2820+app/views/utils.js:651 "set"
2821+app/views/utils.js:641 "get"
2822+app/views/utils.js:566 "isInt"
2823+app/views/utils.js:615 "BoundingBox"
2824+app/views/utils.js:570 "isFloat"
2825+app/views/utils.js:650 "get"
2826+app/views/utils.js:703 "translate"
2827+app/views/utils.js:418 "invokeCallback"
2828+app/views/utils.js:559 "toString"
2829+app/views/utils.js:295 "toggleSVGClass"
2830+app/views/utils.js:258 "addSVGClass"
2831+app/views/utils.js:659 "set"
2832+app/views/utils.js:279 "removeSVGClass"
2833+app/views/utils.js:194 "bindModelView"
2834+app/views/utils.js:165 "substitute"
2835+app/views/utils.js:644 "set"
2836+app/views/utils.js:131 "native"
2837+app/views/environment.js:24 "initializer"
2838+app/views/environment.js:31 "render"
2839 app/views/charm-panel.js:1168 "calculatePanelPosition"
2840 app/views/charm-panel.js:476 "initializer"
2841 app/views/charm-panel.js:250 "render"
2842@@ -71,6 +94,15 @@
2843 app/views/charm-panel.js:964 "setupOverlay"
2844 app/views/charm-panel.js:192 "mouseenter"
2845 app/views/charm-panel.js:1207 "getInstance"
2846+app/views/charm.js:32 "render"
2847+app/views/charm.js:96 "_deployCallback"
2848+app/views/charm.js:61 "on_charm_data"
2849+app/views/charm.js:141 "on_search_change"
2850+app/views/charm.js:69 "on_charm_deploy"
2851+app/views/charm.js:114 "initializer"
2852+app/views/charm.js:16 "initializer"
2853+app/views/charm.js:166 "on_results_change"
2854+app/views/charm.js:122 "render"
2855 app/views/notifications.js:239 "render"
2856 app/views/notifications.js:63 "notifyToggle"
2857 app/views/notifications.js:93 "notificationSelect"
2858@@ -92,36 +124,6 @@
2859 app/views/unit.js:297 "retryRelation"
2860 app/views/unit.js:118 "confirmResolved"
2861 app/views/unit.js:23 "initializer"
2862-app/views/utils.js:642 "set"
2863-app/views/utils.js:225 "humanizeNumber"
2864-app/views/utils.js:135 "console"
2865-app/views/utils.js:132 "noop"
2866-app/views/utils.js:293 "toggleSVGClass"
2867-app/views/utils.js:613 "BoundingBox"
2868-app/views/utils.js:639 "get"
2869-app/views/utils.js:336 "action"
2870-app/views/utils.js:615 "Box"
2871-app/views/utils.js:648 "get"
2872-app/views/utils.js:113 "noop"
2873-app/views/utils.js:277 "removeSVGClass"
2874-app/views/utils.js:657 "set"
2875-app/views/utils.js:649 "set"
2876-app/views/utils.js:368 "_addAlertMessage"
2877-app/views/utils.js:557 "toString"
2878-app/views/utils.js:163 "substitute"
2879-app/views/utils.js:564 "isInt"
2880-app/views/utils.js:826 "pair"
2881-app/views/utils.js:248 "hasSVGClass"
2882-app/views/utils.js:656 "get"
2883-app/views/utils.js:700 "scale"
2884-app/views/utils.js:215 "renderable_charm"
2885-app/views/utils.js:568 "isFloat"
2886-app/views/utils.js:823 "BoxPair"
2887-app/views/utils.js:416 "invokeCallback"
2888-app/views/utils.js:192 "bindModelView"
2889-app/views/utils.js:129 "native"
2890-app/views/utils.js:701 "translate"
2891-app/views/utils.js:256 "addSVGClass"
2892 app/views/service.js:488 "updateConstraints"
2893 app/views/service.js:514 "_setConstraintsCallback"
2894 app/views/service.js:846 "filterUnits"
2895@@ -151,22 +153,43 @@
2896 app/views/service.js:23 "resetUnits"
2897 app/views/service.js:230 "unexposeService"
2898 app/views/service.js:237 "_unexposeServiceCallback"
2899-app/views/environment.js:24 "initializer"
2900-app/views/environment.js:31 "render"
2901-app/views/environment.js:64 "postRender"
2902+app/views/topology/mega.js:794 "destroyService"
2903+app/views/topology/mega.js:152 "serviceClick"
2904+app/views/topology/mega.js:170 "serviceDblClick"
2905+app/views/topology/mega.js:273 "update"
2906+app/views/topology/mega.js:650 "renderedHandler"
2907+app/views/topology/mega.js:619 "show"
2908+app/views/topology/mega.js:771 "destroyServiceConfirm"
2909+app/views/topology/mega.js:195 "serviceMouseLeave"
2910+app/views/topology/mega.js:700 "updateServiceMenuLocation"
2911+app/views/topology/mega.js:683 "showGraphListPicker"
2912+app/views/topology/mega.js:177 "serviceMouseEnter"
2913+app/views/topology/mega.js:625 "hide"
2914+app/views/topology/mega.js:693 "hideGraphListPicker"
2915+app/views/topology/mega.js:742 "toggleControlPanel"
2916+app/views/topology/mega.js:803 "_destroyCallback"
2917+app/views/topology/mega.js:142 "initializer"
2918+app/views/topology/mega.js:762 "show_service"
2919+app/views/topology/mega.js:233 "updateData"
2920+app/views/topology/mega.js:631 "fade"
2921+app/views/topology/panzoom.js:94 "zoom_out"
2922+app/views/topology/panzoom.js:77 "zoomHandler"
2923+app/views/topology/panzoom.js:47 "renderSlider"
2924+app/views/topology/panzoom.js:141 "rescale"
2925+app/views/topology/panzoom.js:164 "renderedHandler"
2926+app/views/topology/panzoom.js:112 "_fire_zoom"
2927+app/views/topology/panzoom.js:103 "zoom_in"
2928+app/views/topology/panzoom.js:33 "componentBound"
2929 app/views/topology/relation.js:78 "processRelation"
2930 app/views/topology/relation.js:403 "_removeRelationCallback"
2931 app/views/topology/relation.js:325 "addRelationDragStart"
2932+app/views/topology/relation.js:460 "cancelRelationBuild"
2933 app/views/topology/relation.js:391 "removeRelation"
2934 app/views/topology/relation.js:309 "snapOutOfService"
2935-app/views/topology/relation.js:448 "cancelRelationBuild"
2936-app/views/topology/relation.js:620 "addRelationEnd"
2937-app/views/topology/relation.js:721 "subRelBlockMouseLeave"
2938-app/views/topology/relation.js:712 "subRelBlockMouseEnter"
2939+app/views/topology/relation.js:724 "subRelBlockMouseEnter"
2940+app/views/topology/relation.js:632 "addRelationEnd"
2941 app/views/topology/relation.js:350 "addRelationDrag"
2942 app/views/topology/relation.js:280 "snapToService"
2943-app/views/topology/relation.js:705 "subordinateRelationsForService"
2944-app/views/topology/relation.js:524 "addRelationStart"
2945 app/views/topology/relation.js:251 "draglineClicked"
2946 app/views/topology/relation.js:158 "drawRelationGroup"
2947 app/views/topology/relation.js:112 "updateLinks"
2948@@ -175,70 +198,34 @@
2949 app/views/topology/relation.js:55 "render"
2950 app/views/topology/relation.js:270 "addRelation"
2951 app/views/topology/relation.js:257 "addRelButtonClicked"
2952-app/views/topology/relation.js:661 "_addRelationCallback"
2953+app/views/topology/relation.js:781 "relationClick"
2954+app/views/topology/relation.js:673 "_addRelationCallback"
2955+app/views/topology/relation.js:717 "subordinateRelationsForService"
2956 app/views/topology/relation.js:372 "addRelationDragEnd"
2957 app/views/topology/relation.js:90 "processRelations"
2958-app/views/topology/relation.js:769 "relationClick"
2959 app/views/topology/relation.js:50 "initializer"
2960-app/views/topology/relation.js:536 "ambiguousAddRelationCheck"
2961+app/views/topology/relation.js:536 "addRelationStart"
2962+app/views/topology/relation.js:733 "subRelBlockMouseLeave"
2963 app/views/topology/relation.js:60 "update"
2964 app/views/topology/relation.js:221 "drawRelation"
2965 app/views/topology/relation.js:74 "renderedHandler"
2966-app/views/topology/panzoom.js:83 "update"
2967-app/views/topology/panzoom.js:40 "zoomHandler"
2968-app/views/topology/panzoom.js:100 "zoom_in"
2969-app/views/topology/panzoom.js:136 "rescale"
2970-app/views/topology/panzoom.js:109 "_fire_zoom"
2971-app/views/topology/panzoom.js:33 "initializer"
2972-app/views/topology/panzoom.js:157 "renderedHandler"
2973-app/views/topology/panzoom.js:91 "zoom_out"
2974-app/views/topology/panzoom.js:48 "renderSlider"
2975-app/views/topology/mega.js:614 "fade"
2976-app/views/topology/mega.js:200 "serviceMouseLeave"
2977-app/views/topology/mega.js:278 "update"
2978-app/views/topology/mega.js:779 "toggleControlPanel"
2979-app/views/topology/mega.js:799 "show_service"
2980-app/views/topology/mega.js:679 "hideGraphListPicker"
2981-app/views/topology/mega.js:182 "serviceMouseEnter"
2982-app/views/topology/mega.js:175 "serviceDblClick"
2983-app/views/topology/mega.js:808 "destroyServiceConfirm"
2984-app/views/topology/mega.js:147 "initializer"
2985-app/views/topology/mega.js:608 "hide"
2986-app/views/topology/mega.js:669 "showGraphListPicker"
2987-app/views/topology/mega.js:689 "setSizesFromViewport"
2988-app/views/topology/mega.js:831 "destroyService"
2989-app/views/topology/mega.js:633 "renderedHandler"
2990-app/views/topology/mega.js:737 "updateServiceMenuLocation"
2991-app/views/topology/mega.js:238 "updateData"
2992-app/views/topology/mega.js:602 "show"
2993-app/views/topology/mega.js:840 "_destroyCallback"
2994-app/views/topology/mega.js:157 "serviceClick"
2995-app/views/topology/topology.js:170 "setter"
2996-app/views/topology/topology.js:162 "getter"
2997-app/views/topology/topology.js:58 "renderOnce"
2998-app/views/topology/topology.js:120 "sizeChangeHandler"
2999-app/views/topology/topology.js:26 "initializer"
3000-app/views/topology/topology.js:178 "getter"
3001-app/views/topology/topology.js:169 "getter"
3002-app/views/topology/topology.js:163 "setter"
3003-app/views/topology/topology.js:138 "serviceForBox"
3004-app/views/topology/topology.js:174 "getter"
3005-app/views/topology/viewport.js:33 "initializer"
3006-app/views/topology/viewport.js:66 "update"
3007-app/views/topology/viewport.js:37 "render"
3008+app/views/topology/relation.js:548 "ambiguousAddRelationCheck"
3009 app/views/topology/service.js:46 "render"
3010 app/views/topology/service.js:33 "componentBound"
3011 app/views/topology/service.js:26 "initializer"
3012 app/views/topology/service.js:50 "update"
3013 app/views/topology/service.js:39 "_scaleLayout"
3014-app/models/endpoints.js:43 "add"
3015-app/models/endpoints.js:32 "convert"
3016-app/models/charm.js:155 "validator"
3017-app/models/charm.js:113 "parse"
3018-app/models/charm.js:77 "sync"
3019-app/models/charm.js:105 "failure"
3020-app/models/charm.js:48 "initializer"
3021-app/models/charm.js:129 "compare"
3022+app/views/topology/topology.js:137 "serviceForBox"
3023+app/views/topology/topology.js:175 "setter"
3024+app/views/topology/topology.js:174 "getter"
3025+app/views/topology/topology.js:107 "computeScales"
3026+app/views/topology/topology.js:157 "getter"
3027+app/views/topology/topology.js:63 "renderOnce"
3028+app/views/topology/topology.js:26 "initializer"
3029+app/views/topology/topology.js:161 "getter"
3030+app/views/topology/topology.js:169 "getter"
3031+app/views/topology/topology.js:170 "setter"
3032+app/views/topology/viewport.js:43 "resized"
3033 app/models/models.js:305 "setter"
3034 app/models/models.js:430 "getModelListByModelName"
3035 app/models/models.js:325 "add"
3036@@ -263,3 +250,11 @@
3037 app/models/models.js:345 "removeOldest"
3038 app/models/models.js:240 "has_relation_for_endpoint"
3039 app/models/models.js:231 "process_delta"
3040+app/models/endpoints.js:43 "add"
3041+app/models/endpoints.js:32 "convert"
3042+app/models/charm.js:155 "validator"
3043+app/models/charm.js:113 "parse"
3044+app/models/charm.js:77 "sync"
3045+app/models/charm.js:105 "failure"
3046+app/models/charm.js:48 "initializer"
3047+app/models/charm.js:129 "compare"

Subscribers

People subscribed via source and target branches