/*jslint browser: true, white: false, devel: true */ /*global window: true, Raphael: true, $: true, _: true */ var symbiote = {}; symbiote.baseUrlFor = function(path){ return window.location.protocol + "//" + window.location.host + "/" + path; }; symbiote.UiLocator = function(){ var allViews = [], paper = new Raphael( 'ui-locator-view'), viewIndicator = { remove: _.identity }, screenshotUrl = symbiote.baseUrlFor( "screenshot" ), backdrop = null, erstaz = null; paper.canvas.setAttribute('preserveAspectRatio','xMidYMin meet'); function iPhoneErsatz(raphael){ var BACKDROP_FRAME = { x: 24, y: 120, width: 320, height: 480 }; function drawFakeDevice(backdrop){ paper.canvas.setAttribute('width','100%'); paper.canvas.setAttribute('height','100%'); paper.canvas.setAttribute("viewBox", "0 0 380 720"); // main outline of device paper.rect( 6, 6, 360, 708, 40 ).attr( { 'fill': 'black', 'stroke': 'gray', 'stroke-width': 4, }); // home button paper.circle( 180+6, 655, 34 ).attr( 'fill', '90-#303030-#101010' ); // square inside home button paper.rect( 180+6, 655, 22, 22, 5 ).attr({ 'stroke': 'gray', 'stroke-width': 2, }).translate( -11, -11 ); if( backdrop ){ // reposition backdrop within frame backdrop.attr( BACKDROP_FRAME ).toFront(); } } return { drawFakeDevice: drawFakeDevice, screenOffset: function(){ return BACKDROP_FRAME; } }; } function iPadErsatz(raphael){ var BACKDROP_FRAME = { x: 55, y: 55, width: 768, height: 1024 }; function drawFakeDevice(backdrop){ paper.canvas.setAttribute('width','100%'); paper.canvas.setAttribute('height','100%'); paper.canvas.setAttribute("viewBox", "0 0 900 1200"); // main outline of device paper.rect( 10, 10, 855, 1110, 20 ).attr( { 'fill': 'black', 'stroke': 'gray', 'stroke-width': 6 }); // home button //paper.circle( 180+6, 655, 34 ).attr( 'fill', '90-#303030-#101010' ); // square inside home button //paper.rect( 180+6, 655, 22, 22, 5 ).attr({ //'stroke': 'gray', //'stroke-width': 2, //}).translate( -11, -11 ); if( backdrop ){ // reposition backdrop within frame backdrop.attr( BACKDROP_FRAME ).toFront(); } } return { drawFakeDevice: drawFakeDevice, screenOffset: function(){ return BACKDROP_FRAME; } }; } function pointIsWithinView(point,view){ var offsetFromOrigin = { x: point.x - view.accessibilityFrame.origin.x, y: point.y - view.accessibilityFrame.origin.y }, isInHorz = offsetFromOrigin.x >= 0 && offsetFromOrigin.x <= view.accessibilityFrame.size.width, isInVert = offsetFromOrigin.y >= 0 && offsetFromOrigin.y <= view.accessibilityFrame.size.height; return isInHorz && isInVert; } function findViewsAt( point ){ return _.filter( allViews, function(view){ return pointIsWithinView(point,view); }); } function showViewLocation( view ) { var screenOffset = erstaz.screenOffset(); viewIndicator.remove(); viewIndicator = paper.rect( view.accessibilityFrame.origin.x, view.accessibilityFrame.origin.y, view.accessibilityFrame.size.width, view.accessibilityFrame.size.height ) .attr({ fill: '#aaff00', opacity: 0.8, stroke: 'black', }) .translate( screenOffset.x, screenOffset.y ); } function hideViewLocation() { viewIndicator.remove(); } function addBackdropImage(){ return paper.image(); } function updateBackdrop(){ if( !backdrop ){ return; } var cacheBusterUrl = screenshotUrl+"?"+(new Date()).getTime(); backdrop.attr( 'src', cacheBusterUrl ); } function updateDeviceFamily(deviceFamily){ if( !erstaz ) { if( deviceFamily === 'ipad' ){ erstaz = iPadErsatz(paper); }else{ erstaz = iPhoneErsatz(paper); } paper.clear(); backdrop = addBackdropImage(); updateBackdrop(); erstaz.drawFakeDevice(backdrop); } } function updateViews(views){ allViews = views; } return { showViewLocation: showViewLocation, hideViewLocation: hideViewLocation, updateBackdrop: updateBackdrop, updateViews: updateViews, updateDeviceFamily: updateDeviceFamily }; }; symbiote.LiveView = function(updateViewFn,updateHeirarchyFn){ var viewTimer,heirTimer; function stop(){ window.clearInterval(viewTimer); window.clearInterval(heirTimer); } function start(){ stop(); // stop any existing timer viewTimer = window.setInterval( function(){ updateViewFn(); }, 700 ); heirTimer = window.setInterval( function(){ updateHeirarchyFn(); }, 2000 ); } return { start: start, stop: stop }; }; $(document).ready(function() { var $domDetails = $('#dom_detail'), $domList = $('div#dom_dump > ul'), $domAccessibleDump = $('div#accessible-views'), $loading = $('#loading'), INTERESTING_PROPERTIES = ['class', 'accessibilityLabel', 'tag', 'alpha', 'isHidden'], uiLocator = symbiote.UiLocator(), liveView; function domListItemForView(view){ var $found = null; $('a',$domList).each(function(i,el){ var $el = $(el); if( $el.data('rawView') === view ){ $found = $el; return false; } }); return $found; } $("#list-tabs").tabs(); $("#inspect-tabs").tabs(); function selectViewDetailsTab(){ $("#inspect-tabs").tabs('select', 0); } function selectLocatorTab(){ $("#inspect-tabs").tabs('select', 1); } function isErrorResponse( response ){ return 'ERROR' === response.outcome; } function displayErrorResponse( response ){ alert( "Frank isn't happy: "+response.reason+"\n" + "details: "+response.details ); } function showLoadingUI() { $loading.show(); } function hideLoadingUI() { $loading.hide(); } function displayDetailsFor( view ) { console.debug( 'displaying details for:', view ); var $table = $(''); function tableRow( propertyName, propertyValue, cssClass ){ if( propertyValue === null ){ propertyValue = 'null'; }else if( typeof propertyValue === 'object' ){ propertyValue = JSON.stringify(propertyValue); } return $('').addClass(cssClass) .append( $('
').text(propertyName), $('').text(propertyValue) ) .appendTo( $table ); } _.each( INTERESTING_PROPERTIES, function(propertyName) { if( !view.hasOwnProperty(propertyName) ){ return; } var propertyValue = view[propertyName]; $table.append( tableRow( propertyName, propertyValue, 'interesting' ) ); }); _.each( _.keys(view).sort(), function(propertyName) { if( propertyName === 'subviews' ){ return; } if( _.contains( INTERESTING_PROPERTIES, propertyName ) ){ return; } // don't want to include the interesting properties twice var propertyValue = view[propertyName]; $table.append( tableRow( propertyName, propertyValue ) ); }); $domDetails.children().remove(); $table.appendTo( $domDetails ); } function treeElementSelected(){ var $this = $(this), selectedView = $this.data('rawView'); displayDetailsFor( selectedView ); selectViewDetailsTab(); $('a',$domList).removeClass('selected'); $this.addClass('selected'); } function treeElementEntered(){ var view = $(this).data('rawView'); uiLocator.showViewLocation( view ); } function treeElementLeft(){ uiLocator.hideViewLocation(); } function listItemTitleFor( rawView ) { var title = ""+rawView['class']; if( rawView.accessibilityLabel ) { return title + ": '"+rawView.accessibilityLabel+"'"; }else{ return title; } } function transformDumpedViewToListItem( rawView ) { var title = listItemTitleFor( rawView ), viewListItem = $("
  • "+title+"
  • "), subviewList = $("
      "); $('a',viewListItem).data( 'rawView', rawView ); _.each( rawView.subviews, function(subview) { subviewList.append( transformDumpedViewToListItem( subview ) ); }); viewListItem.append( subviewList ); return viewListItem; } function updateDumpView( data ) { $domList.children().remove(); $domList.append( transformDumpedViewToListItem( data ) ); $('a', $domList ).bind( 'click', treeElementSelected ); $('a', $domList ).bind( 'mouseenter', treeElementEntered ); $('a', $domList ).bind( 'mouseleave', treeElementLeft ); $domList.treeview({ collapsed: false }); } function flattenViews( rootView ) { var flattenedViews = []; function collectSubViews( view ) { flattenedViews.push( view ); _.each( view.subviews, function(subview){ collectSubViews( subview ); }); } collectSubViews( rootView, flattenedViews ); return flattenedViews; } function filterAccessibleViews( views ) { return _.filter( views, function(view){ return view.accessibilityLabel; }); } function selectorForAccessibleView( view ) { return _.template( "view:'<%=viewClass%>' marked:'<%=viewLabel%>'", { viewClass: view['class'], viewLabel: view.accessibilityLabel } ); } function sendFlashCommand( selector, engine ) { var command = { query: selector, selector_engine: engine ? engine : 'uiquery' , operation: { method_name: 'flash', arguments: [] } }; showLoadingUI(); $.ajax({ type: "POST", dataType: "json", data: JSON.stringify( command ), url: symbiote.baseUrlFor( '/map' ), success: function(data) { if( isErrorResponse( data ) ) { displayErrorResponse( data ); } }, error: function(xhr,status,error) { alert( "Error while talking to Frank: " + status ); }, complete: function(xhr,status) { hideLoadingUI(); } }); return false; } function updateAccessibleViews( views ) { var accessibleViews = filterAccessibleViews( views ), divTemplate = _.template( '' ); $domAccessibleDump.children().remove(); _.each( accessibleViews, function( view ) { var selector = selectorForAccessibleView(view), divHtml = divTemplate({ selector: selector, viewClass: view['class'], viewLabel: view.accessibilityLabel }); $(divHtml) .click( function(){ sendFlashCommand( selector ); return false; }) .appendTo( $domAccessibleDump ); }); } function guessAtDeviceFamilyBasedOnViewDump(data){ switch( data.accessibilityFrame.size.height ){ case 1024: return 'ipad'; case 480: return 'iphone'; default: console.warn( "couldn't recognize device family based on screen height of " + data.accessibilityFrame.size.height + "px" ); return 'unknown'; } } function refreshViewHeirarchy(){ showLoadingUI(); $.ajax({ type: "POST", dataType: "json", data: '["DUMMY"]', // a bug in cocoahttpserver means it can't handle POSTs without a body url: symbiote.baseUrlFor( "/dump" ), success: function(data) { console.debug( 'dump returned', data ); var deviceFamily = guessAtDeviceFamilyBasedOnViewDump( data ), allViews = flattenViews(data); console.debug( 'device appears to be an '+deviceFamily ); updateDumpView( data ); updateAccessibleViews( allViews ); uiLocator.updateDeviceFamily( deviceFamily ); uiLocator.updateViews( allViews ); }, error: function(xhr,status,error) { alert( "Error while talking to Frank: " + status ); }, complete: function(){ hideLoadingUI(); } }); } $('#dump_button').click( function(){ refreshViewHeirarchy(); uiLocator.updateBackdrop(); }); $('#flash_button').click( function(){ sendFlashCommand( $("input#query").val(), $("input#selector_engine").val() ); }); liveView = symbiote.LiveView( uiLocator.updateBackdrop, refreshViewHeirarchy ); $("#live-view button").click( function(){ $(this).toggleClass('down'); if( $(this).hasClass('down') ){ liveView.start(); $(this).text('stop Live View'); }else{ liveView.stop(); $(this).text('start Live View'); } }); //initial UI setup $('#loading').hide(); // do initial DOM dump straight after page has finished loading $('#dump_button').click(); // show locator tab by default selectLocatorTab(); });