Merge lp:~jcsackett/juju-gui/search-fixes into lp:juju-gui/experimental

Proposed by j.c.sackett
Status: Merged
Merged at revision: 607
Proposed branch: lp:~jcsackett/juju-gui/search-fixes
Merge into: lp:juju-gui/experimental
Diff against target: 739 lines (+271/-131)
6 files modified
app/subapps/browser/browser.js (+135/-122)
app/subapps/browser/views/editorial.js (+4/-7)
app/subapps/browser/views/search.js (+48/-0)
test/test_browser_app.js (+60/-0)
test/test_browser_editorial.js (+0/-2)
test/test_browser_search_view.js (+24/-0)
To merge this branch: bzr merge lp:~jcsackett/juju-gui/search-fixes
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+160706@code.launchpad.net

Description of the change

Fixes several problems with search

-Querystring was eaten on viewmode changes or when a search url was pasted
into browser.
-Editorial and Search could have collisions in fullscreen.
-Charm add button didn't work on search results charm tokens.

Note: this reproduced (and now has merged) work in https://code.launchpad.net/~rharding/juju-gui/charm-details-1172050

https://codereview.appspot.com/8940043/

To post a comment you must log in.
Revision history for this message
j.c.sackett (jcsackett) wrote :

Reviewers: mp+160706_code.launchpad.net,

Message:
Please take a look.

Description:
Fixes several problems with search

-Querystring was eaten on viewmode changes or when a search url was
pasted
into browser.
-Editorial and Search could have collisions in fullscreen.
-Charm add button didn't work on search results charm tokens.

https://code.launchpad.net/~jcsackett/juju-gui/search-fixes/+merge/160706

(do not edit description out of merge proposal)

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

Affected files:
   A [revision details]
   M app/subapps/browser/browser.js
   M app/subapps/browser/views/editorial.js
   M app/subapps/browser/views/search.js
   M test/test_browser_app.js
   M test/test_browser_editorial.js
   M test/test_browser_search_view.js

Revision history for this message
j.c.sackett (jcsackett) wrote :
Revision history for this message
Jeff Pihach (hatch) wrote :

LGTM thanks, with trivial

https://codereview.appspot.com/8940043/diff/3001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/8940043/diff/3001/app/subapps/browser/browser.js#newcode24
app/subapps/browser/browser.js:24: * Show or hide the details panel.
We decided against the *'s in a Friday call.

https://codereview.appspot.com/8940043/diff/3001/test/test_browser_app.js
File test/test_browser_app.js (right):

https://codereview.appspot.com/8940043/diff/3001/test/test_browser_app.js#newcode514
test/test_browser_app.js:514: it('no querystring still searched',
function() {
non descriptive test name.

https://codereview.appspot.com/8940043/

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

LGTM with little comment change. Thanks for the branch.

https://codereview.appspot.com/8940043/diff/3001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/8940043/diff/3001/app/subapps/browser/browser.js#newcode496
app/subapps/browser/browser.js:496: // If no details in the route then
hide the div for
Minor:

// If there are no details in the route, then hide the div for

https://codereview.appspot.com/8940043/

lp:~jcsackett/juju-gui/search-fixes updated
591. By j.c.sackett

Comments conform.

592. By j.c.sackett

Comment typo fixed.

593. By j.c.sackett

More descriptive test name.

Revision history for this message
j.c.sackett (jcsackett) wrote :

*** Submitted:

Fixes several problems with search

-Querystring was eaten on viewmode changes or when a search url was
pasted
into browser.
-Editorial and Search could have collisions in fullscreen.
-Charm add button didn't work on search results charm tokens.

Note: this reproduced (and now has merged) work in
https://code.launchpad.net/~rharding/juju-gui/charm-details-1172050

R=jeff.pihach, matthew.scott
CC=
https://codereview.appspot.com/8940043

https://codereview.appspot.com/8940043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/subapps/browser/browser.js'
2--- app/subapps/browser/browser.js 2013-04-23 16:20:42 +0000
3+++ app/subapps/browser/browser.js 2013-04-24 18:15:32 +0000
4@@ -2,22 +2,20 @@
5
6
7 /**
8- * SubApp for the Browser
9- *
10- * @module juju
11- * @submodule subapps
12- *
13+ SubApp for the Browser
14+
15+ @module juju
16+ @submodule subapps
17 */
18 YUI.add('subapp-browser', function(Y) {
19 var ns = Y.namespace('juju.subapps'),
20 models = Y.namespace('juju.models');
21
22 /**
23- * Browser Sub App for the Juju Gui.
24- *
25- * @class Browser
26- * @extends {juju.SubApp}
27- *
28+ Browser Sub App for the Juju Gui.
29+
30+ @class Browser
31+ @extends {juju.SubApp}
32 */
33 ns.Browser = Y.Base.create('subapp-browser', Y.juju.SubApp, [], {
34 /**
35@@ -25,7 +23,6 @@
36
37 @method _detailsVisible
38 @param {Boolean} visible set the panel to hide or show.
39-
40 */
41 _detailsVisible: function(visible) {
42 var detailsNode = Y.one('.bws-view-data');
43@@ -45,7 +42,6 @@
44
45 @method _getStateUrl
46 @param {Object} change the values to change in the current state.
47-
48 */
49 _getStateUrl: function(change) {
50 var urlParts = ['/bws'];
51@@ -70,12 +66,11 @@
52 },
53
54 /**
55- * Generate a standard shared set of cfg all Views can expect to see.
56- *
57- * @method _getViewCfg
58- * @param {Object} cfg additional config to merge into the default view
59- * config.
60- *
61+ Generate a standard shared set of cfg all Views can expect to see.
62+
63+ @method _getViewCfg
64+ @param {Object} cfg additional config to merge into the default view
65+ config.
66 */
67 _getViewCfg: function(cfg) {
68 return Y.merge(cfg, {
69@@ -88,24 +83,23 @@
70 Create an initial subapp state for later url generation.
71
72 @method _initState
73-
74 */
75 _initState: function() {
76 this._oldState = {
77- viewmode: null,
78+ charmID: null,
79+ querystring: null,
80 search: null,
81- charmID: null
82+ viewmode: null
83 };
84 this._viewState = Y.merge(this._oldState, {});
85 },
86
87 /**
88- Determine if we should render the charm details based on the current
89- state.
90-
91- @return {Boolean} true if should show.
92-
93- */
94+ Determine if we should render the charm details based on the current
95+ state.
96+
97+ @return {Boolean} true if should show.
98+ */
99 _shouldShowCharm: function() {
100 if (
101 this._viewState.charmID &&
102@@ -121,12 +115,11 @@
103 },
104
105 /**
106- Determine if we should render the editorial content based on the current
107- state.
108-
109- @return {Boolean} true if should show.
110-
111- */
112+ Determine if we should render the editorial content based on the current
113+ state.
114+
115+ @return {Boolean} true if should show.
116+ */
117 _shouldShowEditorial: function() {
118 if (
119 !this._viewState.search &&
120@@ -139,19 +132,19 @@
121 },
122
123 /**
124- Determine if we should render the search results based on the current
125- state.
126-
127- @return {Boolean} true if should show.
128-
129- */
130+ Determine if we should render the search results based on the current
131+ state.
132+
133+ @return {Boolean} true if should show.
134+ */
135 _shouldShowSearch: function() {
136 if (
137 this._viewState.search &&
138 (
139 this._hasStateChanged('search') ||
140 this._hasStateChanged('viewmode') ||
141- this._hasStateChanged('querystring')
142+ this._hasStateChanged('querystring') ||
143+ (this._hasStateChanged('charmID') && !this._viewState.charmID)
144 )
145 ) {
146 return true;
147@@ -161,11 +154,10 @@
148 },
149
150 /**
151- Verify that a particular part of the state has changed.
152-
153- @method _hasStateChanged
154- @param {String} field the part of the state to check.
155-
156+ Verify that a particular part of the state has changed.
157+
158+ @method _hasStateChanged
159+ @param {String} field the part of the state to check.
160 */
161 _hasStateChanged: function(field) {
162 if (this._oldState[field] === this._viewState[field]) {
163@@ -176,11 +168,10 @@
164 },
165
166 /**
167- Update the oldState with the viewState now that we're done processing
168- the request.
169-
170- @method _saveState
171-
172+ Update the oldState with the viewState now that we're done processing
173+ the request.
174+
175+ @method _saveState
176 */
177 _saveState: function() {
178 this._oldState = Y.merge(
179@@ -193,17 +184,21 @@
180 be.
181
182 @method _updateState
183- @param {String} path the requested path.
184- @param {Object} params the params from the request payload.
185-
186+ @param {Object} req the request payload.
187 */
188- _updateState: function(path, params) {
189+ _updateState: function(req) {
190 // Update the viewmode. Every request has a viewmode.
191+ var path = req.path,
192+ params = req.params,
193+ query = req.query;
194+
195 this._viewState.viewmode = params.viewmode;
196
197 // Check for a charm id in the request.
198 if (params.id && params.id !== 'search') {
199- this._viewState.charmID = params.id;
200+ // Make sure we clear out any accidental matching of search/ in the
201+ // url.
202+ this._viewState.charmID = params.id.replace(/^search\//, '');
203 } else {
204 this._viewState.charmID = null;
205 }
206@@ -214,12 +209,19 @@
207 } else {
208 this._viewState.search = false;
209 }
210+
211+ // Check if there's a query string to set.
212+ if (query) {
213+ // Store it as a straight string.
214+ this._viewState.querystring = Y.QueryString.stringify(query);
215+ } else {
216+ this._viewState.querystring = null;
217+ }
218 },
219
220 /**
221- * The available Views run from this sub app.
222- * @attribute views
223- *
224+ The available Views run from this sub app.
225+ @attribute views
226 */
227 views: {
228 fullscreen: {
229@@ -236,7 +238,6 @@
230 Cleanup after ourselves on destroy.
231
232 @method destructor
233-
234 */
235 destructor: function() {
236 this._cacheCharms.destroy();
237@@ -244,11 +245,10 @@
238 },
239
240 /**
241- * General app initializer
242- *
243- * @method initializer
244- * @param {Object} cfg general init config object.
245- *
246+ General app initializer
247+
248+ @method initializer
249+ @param {Object} cfg general init config object.
250 */
251 initializer: function(cfg) {
252 // Hold onto charm data so we can pass model instances to other views when
253@@ -269,16 +269,15 @@
254 },
255
256 /**
257- * Render the charm details view
258- *
259- * @method renderCharmDetails
260- * @param {Request} req current request object.
261- * @param {Response} res current response object.
262- * @param {function} next callable for the next route in the chain.
263- *
264+ Render the charm details view
265+
266+ @method renderCharmDetails
267+ @param {Request} req current request object.
268+ @param {Response} res current response object.
269+ @param {function} next callable for the next route in the chain.
270 */
271 renderCharmDetails: function(req, res, next) {
272- var charmID = req.params.id;
273+ var charmID = this._viewState.charmID;
274 var extraCfg = {
275 charmID: charmID,
276 container: Y.Node.create('<div class="charmview"/>'),
277@@ -305,16 +304,15 @@
278 },
279
280 /**
281- Render editorial content into the parent view when required.
282-
283- The parent view is either fullscreen/sidebar which determines how the
284- editorial content is to be rendered.
285-
286- @method renderEditorial
287- @param {Request} req current request object.
288- @param {Response} res current response object.
289- @param {function} next callable for the next route in the chain.
290-
291+ Render editorial content into the parent view when required.
292+
293+ The parent view is either fullscreen/sidebar which determines how the
294+ editorial content is to be rendered.
295+
296+ @method renderEditorial
297+ @param {Request} req current request object.
298+ @param {Response} res current response object.
299+ @param {function} next callable for the next route in the chain.
300 */
301 renderEditorial: function(req, res, next) {
302 // If loading the interesting content then it's not a search going on.
303@@ -353,19 +351,19 @@
304 },
305
306 /**
307- * Render search results
308- *
309- * @method renderSearchResults
310- * @param {Request} req current request object.
311- * @param {Response} res current response object.
312- * @param {function} next callable for the next route in the chain.
313+ Render search results
314+
315+ @method renderSearchResults
316+ @param {Request} req current request object.
317+ @param {Response} res current response object.
318+ @param {function} next callable for the next route in the chain.
319 */
320 renderSearchResults: function(req, res, next) {
321 var container = this.get('container'),
322 extraCfg = {},
323 query;
324- if (this._viewState.querystring) {
325- query = Y.QueryString.parse(this._viewState.querystring);
326+ if (req.query) {
327+ query = req.query;
328 } else {
329 // If there's no querystring, we need a default "empty" search.
330 query = {text: ''};
331@@ -385,13 +383,12 @@
332 },
333
334 /**
335- * Render the fullscreen view to the client.
336- *
337- * @method fullscreen
338- * @param {Request} req current request object.
339- * @param {Response} res current response object.
340- * @param {function} next callable for the next route in the chain.
341- *
342+ Render the fullscreen view to the client.
343+
344+ @method fullscreen
345+ @param {Request} req current request object.
346+ @param {Response} res current response object.
347+ @param {function} next callable for the next route in the chain.
348 */
349 fullscreen: function(req, res, next) {
350 // If we've switched to viewmode fullscreen, we need to render it.
351@@ -402,6 +399,20 @@
352 this._fullscreen = this.showView('fullscreen', this._getViewCfg());
353 }
354
355+ // Regardless of the results below we need to clear out the old
356+ // subviews.
357+ if (this._editorial) {
358+ this._editorial.destroy();
359+ }
360+
361+ if (this._details) {
362+ this._details.destroy();
363+ }
364+
365+ if (this._search) {
366+ this._search.destroy();
367+ }
368+
369 // If we've changed the charmID or the viewmode has changed and we have
370 // a charmID, render charmDetails.
371 if (this._shouldShowCharm()) {
372@@ -411,7 +422,7 @@
373 // Render search results if search is in the url and the viewmode or
374 // the search has been changed in the state.
375 this.renderSearchResults(req, res, next);
376- } else if (!this._viewState.charmID) {
377+ } else if (!this._viewState.search && !this._viewState.charmID) {
378 // Render the editorial in fullscreen only if we don't have a charmid
379 this.renderEditorial(req, res, next);
380 }
381@@ -422,13 +433,12 @@
382 },
383
384 /**
385- * Handle the route for the sidebar view.
386- *
387- * @method sidebar
388- * @param {Request} req current request object.
389- * @param {Response} res current response object.
390- * @param {function} next callable for the next route in the chain.
391- *
392+ Handle the route for the sidebar view.
393+
394+ @method sidebar
395+ @param {Request} req current request object.
396+ @param {Response} res current response object.
397+ @param {function} next callable for the next route in the chain.
398 */
399 sidebar: function(req, res, next) {
400 // If we've switched to viewmode sidebar, we need to render it.
401@@ -440,10 +450,20 @@
402 // Render search results if search is in the url and the viewmode or the
403 // search has been changed in the state.
404 if (this._shouldShowSearch()) {
405+ // Showing search implies that other sidebar content is destroyed.
406+ if (this._editorial) {
407+ this._editorial.destroy();
408+ }
409+
410 this.renderSearchResults(req, res, next);
411 }
412
413 if (this._shouldShowEditorial()) {
414+ // Showing editorial implies that other sidebar content is destroyed.
415+ if (this._search) {
416+ this._search.destroy();
417+ }
418+
419 this.renderEditorial(req, res, next);
420 }
421
422@@ -454,7 +474,7 @@
423 this.renderCharmDetails(req, res, next);
424 }
425
426- // If the sidebar is the final part of the route, then hide the div for
427+ // If there are no details in the route then hide the div for
428 // viewing the charm details.
429 if (!this._viewState.charmID) {
430 this._detailsVisible(false);
431@@ -474,17 +494,16 @@
432 },
433
434 /**
435- Dispatch to the correct viewmode based on the route that was hit.
436+ Dispatch to the correct viewmode based on the route that was hit.
437
438 @method routeView
439 @param {Request} req current request object.
440 @param {Response} res current response object.
441 @param {function} next callable for the next route in the chain.
442-
443 */
444 routeView: function(req, res, next) {
445 // Update the state for the rest of things to figure out what to do.
446- this._updateState(req.path, req.params);
447+ this._updateState(req);
448 this[req.params.viewmode](req, res, next);
449 }
450
451@@ -494,7 +513,6 @@
452 @attribute container
453 @default '#subapp-browser'
454 @type {String}
455-
456 */
457 container: {
458 value: '#subapp-browser'
459@@ -504,17 +522,15 @@
460 @attribute store
461 @default Charmworld0
462 @type {Charmworld0}
463-
464 */
465 store: {
466 /**
467- We keep one instance of the store and will work on caching results
468- at the app level so that routes can share api calls. However, in
469- tests there's no config for talking to the api so we have to watch
470- out in test runs and allow the store to be broken.
471-
472- method store.valueFn
473-
474+ We keep one instance of the store and will work on caching results
475+ at the app level so that routes can share api calls. However, in
476+ tests there's no config for talking to the api so we have to watch
477+ out in test runs and allow the store to be broken.
478+
479+ method store.valueFn
480 */
481 valueFn: function() {
482 var url = '';
483@@ -533,7 +549,6 @@
484 @attribute routes
485 @default Array of subapp routes.
486 @type {Array}
487-
488 */
489 routes: {
490 value: [
491@@ -549,7 +564,6 @@
492 @attribute urlNamespace
493 @default 'charmstore'
494 @type {String}
495-
496 */
497 urlNamespace: {
498 value: 'charmstore'
499@@ -562,7 +576,6 @@
500 @attribute deploy
501 @default undefined
502 @type {Function}
503-
504 */
505 deploy: {}
506
507@@ -573,7 +586,7 @@
508 requires: [
509 'juju-charm-store',
510 'juju-models',
511- 'querystring-parse',
512+ 'querystring',
513 'sub-app',
514 'subapp-browser-charmview',
515 'subapp-browser-editorial',
516
517=== modified file 'app/subapps/browser/views/editorial.js'
518--- app/subapps/browser/views/editorial.js 2013-04-24 06:18:42 +0000
519+++ app/subapps/browser/views/editorial.js 2013-04-24 18:15:32 +0000
520@@ -60,17 +60,14 @@
521 var charmID = charm.getData('charmid');
522
523 // Update the UI for the active one.
524- this._updateActive(ev.currentTarget);
525+ if (!this.get('isFullscreen')) {
526+ this._updateActive(ev.currentTarget);
527+ }
528+
529 var change = {
530 charmID: charmID
531 };
532
533- if (this.get('isFullscreen')) {
534- change.viewmode = 'fullscreen';
535- } else {
536- change.viewmode = 'sidebar';
537- }
538-
539 this.fire('viewNavigate', {change: change});
540 },
541
542
543=== modified file 'app/subapps/browser/views/search.js'
544--- app/subapps/browser/views/search.js 2013-04-23 13:31:29 +0000
545+++ app/subapps/browser/views/search.js 2013-04-24 18:15:32 +0000
546@@ -17,7 +17,52 @@
547 ns.BrowserSearchView = Y.Base.create('browser-view-searchview', Y.View, [
548 views.utils.apiFailingView
549 ], {
550+ events: {
551+ '.charm-token': {
552+ click: '_handleCharmSelection'
553+ }
554+ },
555+
556 template: views.Templates.search,
557+
558+ /**
559+ When selecting a charm from the list make sure we re-route the app to
560+ the details view with that charm selected.
561+
562+ @method _handleCharmSelection
563+ @param {Event} ev the click event handler for the charm selected.
564+
565+ */
566+ _handleCharmSelection: function(ev) {
567+ ev.halt();
568+ var charm = ev.currentTarget;
569+ var charmID = charm.getData('charmid');
570+
571+ // Update the UI for the active one.
572+ if (!this.get('isFullscreen')) {
573+ this._updateActive(ev.currentTarget);
574+ }
575+ var change = {
576+ charmID: charmID
577+ };
578+ this.fire('viewNavigate', {change: change});
579+ },
580+
581+ /**
582+ Update the node in the editorial list marked as 'active'.
583+
584+ @method _updateActive
585+ @param {Node} clickTarget the charm-token clicked on to activate.
586+
587+ */
588+ _updateActive: function(clickTarget) {
589+ // Remove the active class from any nodes that have it.
590+ Y.all('.yui3-charmtoken.active').removeClass('active');
591+
592+ // Add it to the current node.
593+ clickTarget.ancestor('.yui3-charmtoken').addClass('active');
594+ },
595+
596 /**
597 * Renders the search results from the the store query.
598 *
599@@ -30,6 +75,9 @@
600 tplNode = Y.Node.create(tpl),
601 container = tplNode.one('.search-results');
602
603+ // Set the container so that our events will delegate based off of it.
604+ this.set('container', container);
605+
606 results.map(function(charm) {
607 var ct = new widgets.browser.CharmToken(charm.getAttrs());
608 ct.render(container);
609
610=== modified file 'test/test_browser_app.js'
611--- test/test_browser_app.js 2013-04-23 19:36:04 +0000
612+++ test/test_browser_app.js 2013-04-24 18:15:32 +0000
613@@ -468,6 +468,66 @@
614 assert.deepEqual(hits, expected);
615 });
616
617+ it('changing the query string dispatches correctly', function() {
618+ var req = {
619+ path: '/bws/fullscreen/search/',
620+ params: {
621+ viewmode: 'fullscreen'
622+ },
623+ query: {
624+ text: 'test'
625+ }
626+ };
627+ browser.routeView(req, undefined, function() {});
628+
629+ // Reset the hits and we should not redraw anything to update the view.
630+ resetHits();
631+ req.query.text = 'test2';
632+
633+ var expected = Y.merge(hits, {
634+ renderSearchResults: true
635+ });
636+ browser.routeView(req, undefined, function() {});
637+ assert.deepEqual(hits, expected);
638+ });
639+
640+ it('no change to query string does not redraw', function() {
641+ var req = {
642+ path: '/bws/fullscreen/search/',
643+ params: {
644+ viewmode: 'fullscreen'
645+ },
646+ query: {
647+ text: 'test'
648+ }
649+ };
650+ browser.routeView(req, undefined, function() {});
651+
652+ // Reset the hits and we should not redraw anything to update the view.
653+ resetHits();
654+
655+ var expected = Y.merge(hits);
656+ browser.routeView(req, undefined, function() {});
657+ assert.deepEqual(hits, expected);
658+ });
659+
660+ it('handles searches with no querystring', function() {
661+ var req = {
662+ path: '/bws/fullscreen/search/',
663+ params: {
664+ viewmode: 'fullscreen'
665+ }
666+ };
667+
668+ var expected = Y.merge(hits, {
669+ fullscreen: true,
670+ renderSearchResults: true
671+ });
672+
673+ browser.routeView(req, undefined, function() {});
674+ assert.deepEqual(hits, expected);
675+ });
676+
677 });
678 })();
679
680
681=== modified file 'test/test_browser_editorial.js'
682--- test/test_browser_editorial.js 2013-04-24 16:42:05 +0000
683+++ test/test_browser_editorial.js 2013-04-24 18:15:32 +0000
684@@ -106,7 +106,6 @@
685
686 view.on('viewNavigate', function(ev) {
687 ev.halt();
688- assert(ev.change.viewmode === 'fullscreen');
689 assert(ev.change.charmID === 'precise/ceph-7');
690 done();
691 });
692@@ -134,7 +133,6 @@
693
694 view.on('viewNavigate', function(ev) {
695 ev.halt();
696- assert(ev.change.viewmode === 'sidebar');
697 assert(ev.change.charmID === 'precise/ceph-7');
698 done();
699 });
700
701=== modified file 'test/test_browser_search_view.js'
702--- test/test_browser_search_view.js 2013-04-22 21:04:27 +0000
703+++ test/test_browser_search_view.js 2013-04-24 18:15:32 +0000
704@@ -12,6 +12,7 @@
705 'json',
706 'juju-charm-store',
707 'node',
708+ 'node-event-simulate',
709 'subapp-browser-searchview',
710 function(Y) {
711 done();
712@@ -75,4 +76,27 @@
713 view.render();
714 assert.equal('charms?text=', apiURL);
715 });
716+
717+ it('clicking a charm navigates for fullscreen', function(done) {
718+ view.render();
719+ view.on('viewNavigate', function(ev) {
720+ ev.halt();
721+ assert(ev.change.charmID === 'foo/bar-2');
722+ done();
723+ });
724+
725+ container.one('.charm-token').simulate('click');
726+ });
727+
728+ it('clicking a charm navigates for sidebar', function(done) {
729+ view.render();
730+ view.on('viewNavigate', function(ev) {
731+ ev.halt();
732+ assert(ev.change.charmID === 'foo/bar-2');
733+ done();
734+ });
735+
736+ container.one('.charm-token').simulate('click');
737+ });
738+
739 });

Subscribers

People subscribed via source and target branches