Sunday, December 4, 2011

Announcing tableSnapshots: A ColdFusion Tool For Preserving and Reverting Database Table Data

Several months ago, I was doing some functional testing on a web application that was taking forever. Each run of the test resulted in changes to the data in several tables, and in order to reset the test I had to go into the tables and undo the changes. It occurred to me that it would be nice to have a tool or script that could preserve the current table data and then later write that version of the data back to the tables.

That marked the beginning of the tableSnapshots tool.

Creating the initial version of the tool just to suit my own needs was quick and easy compared to the work I've put into the tool since then to make it worth sharing, but I'm pretty happy with the result.  It's essentially a small web application that you can drop into any folder on your ColdFusion-enabled (ColdFusion 8 or 9) web server. Once you update the configuration settings in its Application.cfc file with the name of your datasource, the tool enables you to:

  • Take a "snapshot" of one or more of your database tables, storing all of the current table data as JSON data in a text file (NOTE: this tool will not work on tables with binary data fields).

  • Replace the current data in a table with the data stored in a snapshot, creating a backup snapshot of the data being replaced in the process.

  • View the data in a snapshot or backup snapshot in a jQuery-enhanced table that allows you to sort and filter the data.

  • Delete older snapshot and backup snapshot files.

All of this functionality is available via a web interface.  Creating a snapshot is as easy as clicking on the "Create" menu option, selecting the tables from your datasource that you want to generate snapshots from an HTML table (enhanced with the jQuery Datatables plugin), and clicking a button.  You reload the data from one or more snapshot files in the same fashion (though of course reloading the data takes longer), and you can view the data stored in any snapshot or backup file in tabular format.  The tool even makes it easy to delete snapshot and backup files you no longer need so you don't have to manually delete them from your file system.

The tool serializes the table data using the functions provided in the JSONUtil project because the "strictMapping" option provided by JSONUtil avoids the data conversion issues that can occur with the native ColdFusion JSON functions (such as strings being converted to numbers and "true"/"false" string values being converted to "yes/no").  However, it uses the native JSON functions for deserializing the data because they seem to perform better when working with large data sets.

This tool was built and tested on ColdFusion 9.0.1. It should work on Adobe ColdFusion 8, and perhaps other CFML engines as well, but it has not been tested in those environments. It relies on the <cfdbinfo> tag to acquire the data types of the table fields it processes, so access to that tag is a requirement.

The current version supports Apache Derby, MySQL, and Oracle:  there is one .cfc file in the tool for each of these databases that tells tableSnapshots how to map the data stored in JSON format to the relevant table fields during the snapshot reload process. So adding support for additional databases simply means writing similar .cfc files and placing them in the main tool directory. Further details about how tableSnapshots works and what configuration options are available are described in the "Technical Details" page within the application itself.

As I said at the beginning, the original purpose of tableSnapshots was to make it easy to rollback data in multiples tables during testing.  But there are a couple of other potential uses as well:

  • You could use it to simply track changes to data over time (since each snapshot file is timestamped).

  • If for some reason you don't have an IDE or database client program that lets you browse table data, you can create snapshots and view the data with the snapshot view option (which gives you the ability to sort and filter the data).

  • You can use it to copy table data from one type of database server to another.  I used it for this purpose myself:  I made a snapshot of the data, created a similar table with a different name on the other datasource, renamed the snapshot file to match that different name, reconfigured and reset tableSnapshots to interact with the new datasource, and "reloaded" the snapshot data into the new table.

  • If your development database isn't being backed up as well or as often as you'd like, you could use the snapshots as your own backup.

  • You can create snapshots as data for use while offline or in mock objects. tableSnapshots comes with a mini-tool in the "tools/jsonasquery" subfolder that can be used to pull snapshot data into a query object.

I do want to be clear that tableSnapshots is meant for use as a development tool.  While there's no risk of data loss when taking a snapshot, reloading snapshots involves deleting data and no software is perfect, so make sure you take additional steps to safeguard any data that you're afraid to lose.

tableSnapshots is available for download from both RIAForge and GitHub, so have at it!

Friday, November 25, 2011

dirtyFields jQuery Plugin Updated To Fix jQuery 1.6 Compatibility Issue

A few days ago, I received an e-mail informing me that there was a bug in my dirtyFields jQuery plugin having to do with the plugin's evaluation of checkbox and radio button state (the plugin lets you track the state of form elements and highlight changes).

At first, I didn't see the problem and didn't understand the need for the code suggestion he provided because the demo file included in the plugin download worked just fine.  But then I realized that the demo was coded to use an older version of jQuery (the plugin is over 2 years old now), and once I updated the demo to use jQuery 1.7 I could see the problem.

I had coded the plugin to use the jQuery attr() function to check the selection state of checkboxes, radio buttons, and the option elements in <select> elements.  Prior to jQuery 1.6, code like myCheckbox.attr("checked") would return a Boolean value based on whether the checkbox was currently checked or not.  But that changed in jQuery 1.6: now (in 1.6 and higher), myCheckbox.attr("checked") only returns the initial value of the "checked" attribute of the element ("checked" rather than "true"), and returns undefined if the checkbox/radio button element does not possess the checked attribute at all.  The same hold true for the "selected" attribute of option elements.  In jQuery 1.6 and higher, the new prop() function is designed to return the current state of the selected attribute (this change to jQuery is in fact described on the API page for prop()).

So I updated the plugin to use the is() function in conjunction with the ":checked" and ":selected" selectors to evaluate checkbox, radio button, and option state, since that method works with both older and newer versions of jQuery.  The new version of the plugin is available via the GitHub link from its dedicated GitHub Pages site.

Monday, October 3, 2011

Two Thoughts On Today's Adobe MAX (2011) Day One Keynote

I'm not going to try and summarize the keynote:  you can either watch the replay at http://www.max.adobe.com/online/ or read a brief synopsis at http://max.adobe.com/news/2011/keynote_day1.html.

My first thought is actually about some news that wasn't part of the keynote, but was announced on the Internet during the keynote:  Adobe's acquisition of PhoneGapPhoneGap is an application framework that allows developers to build mobile apps using web technologies (HTML, CSS, and Javascript) and the PhoneGap API (to access particular device functions like geolocation, the camera, internal storage, etc) and then package and deploy those apps such that they work on most mobile operating systems (Android, IOS, Blackberry, and a few others).

What struck me about that news was that it means Adobe now provides two ways for app developers to develop cross-platform apps.  If a developer is already familiar with Flash and Flex development, they can build mobile apps using Flash Builder and Adobe AIR.  If the developer is more skilled with web design and programming, they can build apps using PhoneGap.  In both cases, the end result is an app that runs natively on your device of choice (IOS, Android, whatever).

First of all, it's yet another example of Adobe's willingness to support both Flash and HTML5 as development platforms going forward.  A number of tech analysts contend that Adobe's development of new tools that play nice with HTML5 is a capitulation of some kind to the way the industry is moving.  I don't see it that way: Adobe may be synonymous with Flash, but it's never been all about Flash with them.  They have always supplied the tools that the digital and web creation markets have demanded, just as any sane company would, regardless of what their internal preferences might be.

The other thing about the PhoneGap acquisition is that it will expand the number of developers using Adobe tools and technologies to create mobile apps for both Android and IOS.  And now suddenly Adobe potentially a big player in the smartphone and tablet space, not by release their own mobile OS or mobile hardware, but by providing the tools to any developer who wants to write on any and all of these platforms.  To be clear, at the moment there are certain kinds of apps or app functions that a developer would want or need to develop in native OS code, but as the Adobe tools provide ways to overcome those limitations (such as the ability to include native code extensions in Flash/AIR apps), the number of reasons for writing an app in native OS code (and hence writing a second version for deployment on another platform) are going to become fewer and fewer.

My second observation regarding the keynote concerns the suite of touch-optimized mobile apps - the Adobe Touch Apps - coming in November to Android devices and later to IOS devices.  While some of the apps are geared more towards visual designers, the Proto wireframing app is something I could use in the design phase of my web and mobile application work, and, well, who wouldn't want a touch-optimized version of Photoshop to use to mess around with their photos?

And that's when the thought occurred to me:  here were some apps that I could use to be productive with a tablet.  I'm an application developer who writes code.  I have access to two tablets and I don't use either of them to write code or generate digital content of any kind.  For me, tablets have always been digital consumption devices (something Amazon seems to recognize with their new Kindle Fire tablet) for viewing video, looking at pictures, or reading web pages or books, and kinda superfluous given I can do all that and much more with a laptop (to be clear, I do love my Nook Color as an e-reader, but I rarely use it to surf the web or anything else).

But the apps Adobe showed off today made me feel like I would in fact want to use them on a tablet, because they were designed specifically with tablets and touch-interaction in mind, not as desktop apps shrunk into a mobile form factor.  That to me is a step in the right direction, and it's apps like those that are going to grow the tablet market by making the tablet experience more compelling.

Monday, June 20, 2011

An Overview of buzztouch, A Mobile App CMS for Non-Programmers

The other day I stumbled on a Make Use Of article about buzztouch, a "iPhone, iPad, Android app builder and content management system."  Since we're researching options for creating cross-platform mobile applications, and the article indictated that it was worth checking out, I decided to sign up and give buzztouch a spin.

buzztouch is designed to let non-programmers build a mobile app on the buzztouch website using a collection of web-based forms.  The app-building interface prompts you to create certain items, like a launcher icon for the app, a banner image for the home screen, and an "info" page for the app, but you choose what other content you wish to provide from a drop-down list of different components.  One component lets you create a screen of formatted text using CKEditor, while another lets you create an app screen using HTML, CSS, and Javascript.  There are components for pulling in remote content such as web pages, YouTube pages, streaming audio and video files, and RSS feeds.  One component lets you pinpoint one or more locations on a Google Map with brief location information that a user of the app can then use to get driving directions to those locations from their current position.  Even though I was casually playing with the system, I was able to create an app using several of these components in under two hours.

Once you've created the app, you can then download the native source code for the app:  you're provided with separated source code downloads for IOS and for Android.  The source code comes in a .zip file, which also contains a readme file with instructions on how to compile the source code for that particular platform and create the native app.  It only took me a few minutes to create an Android app from the source code, and it only took me that long because I had to provide a Google Maps API key to enable the mapping component I'd added (the readme provided exact instructions on the changes I need to make it the code to use the key).  I had my IOS developer colleague compile the IOS version of the app, and it was also smooth sailing for him.  The resulting app on the Android platform looked a little rough simply because of the generic design elements, but the IOS version looked pitch-perfect.

The first time you access any component or screen in the mobile app, there is a pause as the app retrieves the content or content instructions from buzztouch then caches it for local use.  This is where the content management system aspect of buzztouch kicks in:  any changes you make to the app on the buzztouch site get pushed to (or pulled by, not sure which) the mobile app.  So the app owner doesn't need to recompile and update the app through the App Store or the Android Market in order to make content changes.  If the mobile device doesn't have a network connection, then the cached content is used.

So what's my overall take on buzztouch?  I think that if you need to build a mostly informational mobile app, something that serves the same purpose as a mobile-optimized website, then buzztouch is a compelling option because of how easy it is to create the app and then update the content.  Even if your long-term strategy is to redesign an existing "desktop"-optimized website to be mobile-friendly as well, a buzztouch app could fill the void until that goal was reached.

If you need to build a mobile app that's more interactive, one where the user can send and receive custom data, then buzztouch is at best an incomplete solution.  There are no buzztouch components for collecting data (though the app itself will capture GPS info from the device in order to map where the app has been used, which is both useful and slightly disturbing), but you could probably use the Custom HTML component to create a web form that transmitted data via a regular form submit or via AJAX.  And there are as yet no components that let you access device-specific functions like the camera or accelerometer.

On the other hand, if you are capable of doing some IOS or Android coding, the native code provided by buzztouch could serve as a starting point for adding additional functionality:  the buzztouch folks seem to be open to folks using the generated code to advance their learning of native app programming.

So overall, I'm impressed with what buzztouch has done so far with the direction they've taken, and I hope they continue to improve on the platform.

Friday, June 10, 2011

For CFSelenium Users: An Ant Script to Transcribe Your Tests For Multiple Browsers

The easiest way to create a CFSelenium test case is to use the Selenium IDE plugin for Firefox to compose the test, then export the test using the export formatter Bob Silverberg included in the CFSelenium project.  You end up with a test that can be run against Firefox but can be further customized to your needs.

Of course the beauty of CFSelenium is that it lets you run those tests against browsers other than Firefox, to conduct cross-browser testing.  But who wants to create multiple copies of the original tests, or edit the test files whenever you want to check them against another browser?

Not this guy.

So I went ahead and created an Ant script that will take the CFSelenium tests generated for Firefox and transcribe them to work for other browsers I wanted to test against.  The script includes configuration properties that allow you to customize the browser settings to your environment.  For example, I needed the script to be able to rewrite my Internet Explorer tests to run IE on a virtual machine rather than on localhost.  I also took what I learned about capturing screenshots using CFSelenium and added a nice screenshot management feature to the script:  if you add one or more captureScreenshot or captureEntirePageScreenshot statements to your tests, the script will make sure you're using the correct statement for the particular browser, will rewrite the statement to place the screenshot files in a particular directory, and will add code that will incrementally name the screenshot images so you can take and preserve multiple screenshots for one or more tests.

I've put this Ant script up on GitHub at https://github.com/bcswartz/CFSelenium-Cross-Browser-Converter-Ant-Script.  The Ant file itself provides a lot of guidance on how to configure and use the script and some hints on using CFSelenium in general.  Hopefully it's usable even by folks who aren't completely comfortable with Ant.  I myself am still something of an Ant noob:  I'm sure there are more elegant ways of writing this script, and Ant gurus are welcome to fork the GitHub repo and make improvements.

Tuesday, May 24, 2011

CFSelenium News: No Need To Start Selenium Server Separately Anymore

In my earlier blog post on how to install and run CFSelenium, my steps included directions on copying the .jar file for the Selenium-RC/Selenium Server application to an easily accessible location and then starting your CFSelenium testing session by executing that .jar file from the command line.

I'm happy to report that anyone using CFSelenium in conjunction with ColdFusion 8 or 9 can skip those particular steps.  Marc Esher (of MXUnit fame) recently contributed code to the CFSelenium project that enables CFSelenium to start the Selenium Server (if it's not already running) anytime you run a test, and stops the server once the test is complete.  This code is now part of the latest version of CFSelenium that can be download via the CFSelenium GitHub page.

Unfortunately, when I revised the tag-based, CF 7/8-friendly versions of the CFSelenium files to incorporate Marc's code and ran it against CF 7, I found that CF 7 apparently could not start and stop the Selenium Server via the new code.  So ColdFusion 7 users will still have to start Selenium Server from the command line, but otherwise CFSelenium still works in CF 7.

Monday, May 23, 2011

Integrating Mura with the Jasig Central Authentication Services (CAS)

I've already posted most of this information on the Mura forums, but I wanted to post it here as well for anyone looking for information on this topic.

With the blessings of my boss, I've been playing around with the ColdFusion-powered Mura content management system to see if it might be a good CMS option for us.  From what I've seen so far, Mura is a well-thought out CMS system that is both powerful and easy to use, which is a difficult balancing act.

One of the things my boss wanted me to investigate was whether or not we could tie Mura in with our single sign-on solution, which is CAS (Central Authentication Service), a Jasig project originally created at Yale.  When a user tries to access a page or a site that requires them to authenticate, they are redirected to the CAS server and enters their LDAP-based user id and password on the CAS login page.  If they successfully authenticate, the CAS server redirects them back to the original site and stores a token as a cookie in the user's browser.  If the user visits a different website secured by CAS, the cookie allows them access to that site without the need to log in again.

Mura provides a plugin architecture that allows developers to intercept certain Mura events and run their own code.  A number of Mura shops have created plugins that intercept the Mura login events in order to tie in with their in-house directory and authentication servers, but those login events are triggered by the admin and user login forms built into Mura.  I couldn't go that route:  I needed to circumvent and replace the Mura login form(s) with the CAS login form and have Mura log in the user based on the credential information returned by CAS.

After some digging into the code and some trial-and-error, I came up with a way to authenticate Mura administrators via our CAS system.  First, I created admin user accounts in Mura where the username matched the username/user identifier returned by a successful CAS login.  Then I created a Mura plugin that would fire with the OnGlobalRequestStart event with the following event handler:

 

 

...The code in this plugin will only execute if the user navigates to /admin/index.cfm (without any URL query parameters) and the "mura" struct in the session scope contains an empty userId key.  Without these conditionals, the login code will run at times when you don't want it to, like when the user is trying to log out or when saving page edits (apparently saving content changes involves navigating to the admin/index.cfm page without URL query parameters).

The NetId is the user identifier returned by CAS.  If it's not found in the session scope, the action is redirected to adminCASLogin.cfm, a variation on the CAS authentication script posted on the CAS website (https://wiki.jasig.org/display/CASC/ColdFusion+client+script).  The user enters their username and password on our CAS login form page, and if the log in is successful the adminCASLogin.cfm page copies the NetId to the session scope and uses cflocation to go to admin/index.cfm.

Now that the NetID exists in session, the plugin queries the tUsers table (the Mura database table containing, well, user records) for an admin account with a username that matches the NetId and gets back the Mura userId for that person, and provides that userId along with other values to the loginManager's loginByUserID() function.  The admin user ends up in the administrative Dashboard, logged in and ready to go.

The one drawback to this method is that when a user logs out, they are presented with the normal Mura admin login form, which can be a little confusing.  But without knowing the password to their Mura user record, they would still have to log in via CAS.

I wrote a similar plugin for requiring CAS authentication to access a particular Mura site (an "internal" site only viable to authorized personnel).  In this case, the event handler executes in response to the OnSiteRequestStart event:

 

 

...in this case, I had to create my own session variable (session.userId) for denoting if the user had already authenticated via CAS.  The siteCASlogin.cfm file is pretty much the same as the adminCASLogin.cfm file in the first example except that when it completes the CAS authentication it uses cflocation to return to the index.cfm page of the site (rather than admin/index.cfm).

If the user successfully authenticates via CAS and has any sort of Mura user account, they will be granted access to the site, and if they are a Mura admin they will be logged in so they can edit the site if they wish.

Sunday, April 3, 2011

Quick Tip: Controlling Execution Speed in CFSelenium

When you use Selenium IDE to play back the actions you've recorded on a web page (the clicks, the text input, etc.), you can control the waiting period between the execution of each command using the slider control on the left end of the tool bar:

With CFSelenium, the way that you control the time period between executions is with the setSpeed(string milliseconds) function.  Once you set the execution speed with this function, the time period between executions will remain set at that length until you use setSpeed again.  So you could set the speed once within the setUp() function of your MXUnit test case...

public void function setUp() {
    browserUrl = "http://local.test";
    selenium = new CFSelenium.selenium(browserUrl, "localhost", 4444, "*firefox");
    selenium.start();
    selenium.setTimeout(30000);
    selenium.setSpeed("1000");
}

 

...or you could alter the speed within a test case function...

public void function testFormValidation() {
    selenium.open("blah/index.cfm?event=addEditUser&userId=12");
    selenium.setSpeed("3000");
    //Now Selenium will wait 3 seconds between each of the following command statements
    selenium.type("firstName", "John");
    selenium.type("lastName", "White");
    //Setting the speed back down to 1 second
    selenium.setSpeed("1000");
    ....
}

 

Knowing how to change the speed in CFSelenium is crucial if you're testing any sort of operation that might take a second or two to update the page, like an AJAX call that populates a drop-down box:  if the next command in your test requires that drop-down box to be populated, but CFSelenium tries to run that command before the AJAX call completes, either your test will fail or you'll get an error message from Selenium.  The setSpeed function lets you slow down the execution speed so your AJAX calls have time to do their work before the next step in the test.

Monday, March 28, 2011

Taking Screenshots with CFSelenium

In addition to doing browser behavior testing, there are four functions in CFSelenium that you can use to capture screenshots.  The first two are:

captureScreenshotToString(): captures a shot of your current monitor display in .png format and returns the image data as a base-64 encoded string. **

captureScreenshot(string filename): captures a shot of your current monitor display in .png format and writes the image data to the filename passed in as the argument.  The filename parameter must be a full path ending in a filename, like "C:\Data\Screenshots\screenshot1.png." **

 

**These two functions work for all supported browsers, but because they take an image of the entire monitor screen, the browser window launched by *CFSelenium may be obscured by other windows on your monitor, and sometimes CFSelenium (or more specifically the Selenium Server) places the browser window in the lower half of the monitor, cutting off the view of the lower half of the window.

The other two screenshot functions capture only an image of the web page in the browser launched by CFSelenium, and they capture that page in its entirety: even if the page is long enough to scroll for several screens, all of that content will be captured.  While that can make for some very elongated images, it does capture all parts of the page.  But unlike the first two functions, these can only be used with certain browsers:

 

captureEntirePageScreenshotToString(): captures the entire web page in .png format and returns the image data as a base-64 encoded string.  Only works when used with Firefox.

captureEntirePageScreenshot(string filename, string kwargs): captures the entire web page in .png format and writes the image data to the filename passed in as the first argument.  The filename parameter must be a full path ending in a filename, like "C:\Data\Screenshots\screenshot1.png." The second argument is a string that modifies the way the screenshot is captured.  At the moment, it can only be used to alter the background of the HTML document object of the page.  An empty string can be passed as the argument, like so:

selenium.captureEntirePageScreenshot("C:\Data\Screenshots\screenshot1.png","");

 

The captureEntirePageScreenshot works when used with Firefox.  You call also use it with Internet Explorer with a helper program called snapsIE if you do the following:

  1. Go to http://snapsie.sourceforge.net/ and click on the "snapsie-0.2" link to download the file. The file is a compressed .tar.gz file, so you will need a program that can extract the files from that package (Mac OS X can open it as if it were a .zip file).

  2. Ignore the first of the bullet points on the http://snapsie.sourceforge.net/ page but follow the rest of them to register the snapsIE .dll file.

  3. The last of the bullet points alludes to the fact that the security restrictions regarding ActiveX controls can block the use of snapsIE. In experimenting with it, I ended up making the following changes to Internet Explorer's security settings under "Internet Options":

    • Automatic prompting for ActiveX controls: Disable

    • Run ActiveX controls and plug-ins: Enable

    • Script ActiveX controls marked safe for scripting: Enable

    (Even after making those changes, the first time I had CFSelenium try to capture a acreenshot with IE, I had to watch the test run and click on the prompt at the top of the web page and tell IE to allow the snapsIE script to run on all sites.)

  4. When you instantiate the CFSelenium object to run the test that will take the screenshots, make sure you use "*iexploreproxy" as the browser string instead of the regular "*iexplore"

 

...The ability to take screenshots with CFSelenium lets you use CFSelenium to test the look of your web pages in other browsers in addition to testing how the page behave. And if you have a need to generate a portfolio of screenshots for a client, using the captureEntirePageScreenshot function could save you some time with that task.

Sunday, March 27, 2011

Using CFSelenium With The Most Common Browsers

When using CFSelenium to send Selenium commands to the Selenium Server, you start off by instantiating CFSelenium as an object.  If you're using the ColdFusion 9 version, you would instantiate it with a statement like this:

browserUrl= "http://www.cnn.com";
selenium = new CFSelenium.selenium(browserUrl);

 

If you're using the ColdFusion 7 or 8 version, you might instantiate it like so:

<cfset var browserUrl= "http://www.cnn.com" />
<cfset variables.selenium= CreateObject("component","CFSelenium.selenium_tags").init(browserUrl) />

 

In both cases, you're calling the init() function of the respective CFSelenium .cfc file. The init() function can take 4 arguments:

  • browserURL: (required) the URL of the website (just the address to the webroot, not to any subfolders) you want to run the Selenium commands against.

  • host: (optional) the IP address or hostname of the machine running the Selenium Server instance CFSelenium will be communicating with. The default value is "localhost"

  • port: (optional) the port number used to communicate with the Selenium Server instance. The default is 4444.

  • browserStartCommand: (optional) a string that tells Selenium which web browser on the machine running the Selenium Server instance to use. The default value is "*firefox"

By default, CFSelenium uses Firefox to conduct the browser tests (which makes sense because most of the time you'll use the Selenium IDE Firefox plugin to create your tests). In order to have CFSelenium run a test case through a different browser, you'll have to pass a different browserStartCommand string into the init() function.  You can see a list of the different browserStartCommand strings in this post on StackOveflow:  http://stackoverflow.com/questions/2569977/list-of-selenium-rc-browser-launchers.

Sometimes it's just that simple, but sometimes it's not...and when it's not, it can be a real pain to figure out.  WIth some trial and error, I was able to get Selenium Server/CFSelenium to interact with all of the browsers I had installed on my Windows and Macintosh laptops.  Here's what I had to do it each case:

Firefox

Windows 7: The typical browser string shorthand for starting Firefox ("*firefox") only works if Selenium Server can use your operating system to find out where the browser executable file is. Because Firefox isn't documented in my Windows 7 PATH envionment value, I had to add the full path to the firefox.exe file in the browser string. So my CFSelenium instantiation statement ended up looking like this:

selenium = createObject("component","CFSelenium.selenium").init("http://www.cnn.com","localhost", 4444, "*firefox C:\Program Files\Firefox\firefox.exe");

Mac OS X: The default/regular Firefox browser string "*firefox" worked just fine.

Google Chrome

Windows 7: The regular "*googlechrome" browser string worked fine.

Mac OS X: The regular "*googlechrome" browser string worked fine.

Safari

Windows 7: Safari comes with a pop-up window blocker that is turned on by default, and while on it will prevent the Selenium Server from doing anything beyond opening the browser window. To turn off that blocker in the Windows version, click on the gear icon in the upper right corner and click on "Block Pop-Up Windows" to uncheck it. Once I did that, I was able to run Safari using the "*safariproxy" browser string (it wouldn't take the regular Safari browser string) and specifying the location of the Safari executable file (similar to what I did with Firefox).

Mac OS X: Once I turned off the pop-up blocker (by clicking on "Safari" in the menu bar and unchecking "Block Pop-Up Windows"), I was able to run Safari with the normal "*safari" browser string>.

Opera

Windows 7: Although Opera 9 was the last version of Opera officially supported for the Selenium Server, I was able to run Opera 11.01 using the "*opera" string and specifying the location of the opera.exe file.

Mac OS X: This was the hardest one to figure out. Here's what I ended up doing:

  • I had to define a proxy server in Opera that would accept the commands coming from the Selenium Server using the following steps:

    1. In the Opera menu bar, I clicked on "Opera" and then "Preferences".

    2. In the Preferences window, I clicked on "Advanced", then choose "Networks" from the column of options on the left, then clicked on the "Proxy Servers..." button.

    3. In the Proxy Servers window, I put a checkmark next to "HTTP", entered "localhost" in the text box to the right of the "HTTP" checkbox, and entered "4444" in the corresponding Port box.

  • I could not use any of the Opera-specific browser strings. I ended up having to use the "*custom" browser string and specify the location of the Opera executable...which is NOT the "Opera.app" listed in the Application folder but a file within that application package/folder:

selenium = createObject("component","CFSelenium.selenium").init("http://www.cnn.com","localhost", 4444, "*custom /Applications/Opera.app/Contents/MacOS/Opera");

Even after all that, the behavior was still a bit flaky. Sometimes Opera would start with a dialog box asking if you wanted to restore the last session (as if Opera had crashed after the last session). Sometimes Selenium wouldn't proceed past the "Speed Dial" Opera startup page: telling Opera to use an actual homepage helped but that setting change didn't seem to take effect upon the next startup of Opera but rather the startup after that.

Internet Explorer 8

Windows 7: I wasn't able to use the regular "*iexplore" browser string, but "*iexploreproxy" worked fine (and with IE being part of the OS, I didn't need to specify the path to the executable).

Mac OS X: If you're running virtual machine software (I use VMWare Fusion) on your Mac in order to run a Windows environment, you can run your CFSelenium tests against IE running in that VM by doing the following:

  • Place a copy of the Selenium Server .jar file on your VM instance of Windows just as you did when you installed CFSelenium on your Mac.

  • Find out what the IP address of your VM instance is. The easiest way to do that is to open a command-line interface in Windows (by clicking the Start button, choosing "Run...", typing "cmd" in the text box and hitting the Enter key), and typing "ipconfig -all" to get the current network statistics for the Windows instance: you want the address under "IP Address".

  • Use the command-line interface to start the Selenium Server on your Windows instance (just as you would do on your Mac).

  • Instantiate CFSelenium using the IP address of the Windows VM and "*iexplore" as the browser string. So if the IP address of your Windows VM is 172.16.9.9, the instantation statement would look like this:

selenium = createObject("component","CFSelenium.selenium").init("http://www.cnn.com","172.16.9.9", 4444, "*iexplore");

When you run CFSelenium against the Selenium Server on your Windows VM, you'll see the Selenium Server on the VM doing all the work, even though the test results will be reported back to whatever browser you're using to run the test on the Mac side.

...This was how I got all the browsers to run with CFSelenium on my computers. It may be different for your computer(s), but hopefully the information I've provided here will at least be of some help.

Brief Guide to Testing Browser Behavior with Selenium IDE and CFSelenium

The Selenium IDE Firefox plugin makes it easy to record the actions you perform on a web pages or series of web pages as a set of commands.  You simply open the Selenium IDE tool from the "Tools" menu in Firefox, make sure the Record button (the red circle in the upper right) is active, and then click and type away.  Once you've stopped recording, you can re-run the the recorded steps any number of times and even save the steps as a test case file to run again later.

Simply recording your actions as described above can be useful when you're troubleshooting a browser behavior (usually a Javascript function) that only executes after you've changed certain form fields or performed particular actions:  it saves you the time of constantly reloading the page and manually going through the actions again.  But you don't end up with an actual verifiable test:  you're verifying whether or not the page is behaving correctly by visually observing the outcome each time.  In order to use Selenium (and especially CFSelenium within the context of MXUnit) as a true testing tool, you need to add verification and assertion statements into your Selenium recordings.

Fortunately, it's easy to add such statements with Selenium IDE.  If the browser behavior results in a visible change to the page (like updating text in a form field), right-click on the changed part of the page.  At the bottom of the context menu that appears will be several Selenium commands you can choose to add to your current recording.  One or more of the commands will be a "verify" or "assert" command that you can add to your recording that basically means "return true if this page element contains this value or this text; return false if not".

Take the example below.  If the user selects "College" from the first select box on the bottom line, an AJAX call is made that populates the third select box on that line with the acronyms of the various colleges.  When I right-click on that third select box, I can select the verification command that verifies whether or not the select box contains all of the acronyms (you can see all of the text labels for the options in the select are concatenated together).  Other verfication and assertion commands for the element you selected might also be available under the "Show All Available Commands" option at the bottom of the context menu.

It's these verfications and assertions that you save in your recording that form the basis for your testing with CFSelenium and MXUnit.  When you export your Selenium recording as a CFSelenium/MXUnit test case and then run it, MXUnit is going to receive the results of the verifications/assertions from the Selenium Server and report the test case as having passed or failed based on those results.  When your CFSelenium/MXUnit test case passes in Firefox, you can then use CFSelenium to run the test case against other browsers and see if it passes or fails.  If it fails in Browser X, you can look at the test results to see which verifications/assertions failed and then figure out how to modify your page's behavior/Javascript so that it works in that Browser X...and once it works in Browser X, you'd re-run the test for Firefox and the other browsers to make sure the newly modified page still passes the tests in those browsers.

If you're running CFSelenium on ColdFusion 8 or 9 in conjunction with MXUnit, you can add MXUnit debug() statements to your test cases to retrieve additional information from the test (beyond the success or failure of the verifications or assertions).  For example, you could add the following line to your MXUnit test case to retrieve all of the text on the web page used in the test:

debug(selenium.getBodyText());

 

For more information about the debug() statement, consult the MXUnit documentation. For more information about what kinds of data CFSelenium can return from the test, look through the functions and read the hints in the relevant CFSelenium .cfc file (selenium.cfc for ColdFusion 9, selenium_tags.cfc for ColdFusion 7 and 8).

Quick Guide for Installing and Running CFSelenium

I've been playing around with CFSelenium this week in order to come up with an implementation that will let me easily replicate and run tests I've created with the Firefox Selenium IDE plugin on multiple browsers.  I've learned a few things along the way that are worth blogging about, but I thought I should start with a blog post on how to get CFSelenium up and running quickly.


Installating CFSelenium

  1. First, you need to have a computer that has either ColdFusion 7, ColdFusion 8, or ColdFusion 9 installed and running such that you can call and execute a .cfm page on the machine (example "http://localhost/myLocalSite/index.cfm"), and you'll need Firefox installed.
  2. Go to Bob Silverberg's CFSelenium GitHub site - https://github.com/bobsilverberg/CFSelenium - and click on the "Downloads" button.  In the pop-up window that appears, click one of the two buttons under "Download source" to download either a .tar.gz archive or a .zip archive, and download the file to your computer.
  3. Extract the files and folders from the downloaded file wherever you like to start with - let's say a folder called cfSeleniumFiles.
  4. Create a folder called CFSelenium within your webroot folder (usually "http" or "www").  Copy the following files and folder from your cfSeleniumFiles folder into the new CFSelenium folder (*updated in August 2012 to reflect additional files):
    • selenium.cfc
    • selenium_tags.cfc
    • server.cfc
    • CFSeleniumTestCase.cfc
    • CFSeleniumTestCase_Tags.cfc
    • test
  5. Create a folder called selenium somewhere on your hard drive that isn't too deep in your folder hierarchy. On my Windows machine, I created it at the root of the hard drive (C:/selenium), while on my Macintosh system I created it under my user account (/Users/Brian/selenium).
  6. In your cfSeleniumFiles folder, open the Selenium-RC folder and copy the .jar file (the Selenium Server file) within that directory to the selenium folder you created in the previous step.
  7. If you're running ColdFusion 8 or ColdFusion 9, you'll want to download and install the MXUnit ColdFusion unit test framework (if you're running ColdFusion 7, skip this step; you'll still be able to execute Selenium commands via CFSelenium but in a different way).  If you don't already have MXUnit installed:
    • Go to the MXUnit website - http://www.mxunit.org/ - and click the download link ("1. Download").
    • Once the .zip file is downloaded, unzip the files into an mxunit folder in your webroot (so you should now have a CFSelenium folder and mxunit folder in your webroot).
That's all you need to do to install what you need to try out CFSelenium. Now you're ready to issue commands to the Selenium Server via CFSelenium and see it in action:


Testing CFSelenium

  1. Any time you want to run Selenium tests through CFSelenium, you need to start the Selenium Server (previously called Selenium-RC):
    • Open up a command line interface. On a Mac, simply open up the Terminal application. If you're machine runs windows, click on the Start button, choose "Run" from the menu, type "cmd" in the input box that appears, and hit the Enter key.
    • Use the "cd" command to navigate to the selenium folder you created earlier (the one where you copied the .jar file).
    • Type in the following command and hit the Enter key:
         java -jar selenium-server-standalone-2.0b2.jar
    • The Selenium Server should start: it will output several lines of activity then stop. If the server does not start, you may need to install Java 1.5 or Java 1.6 on your computer:  if you go to http://www.java.com, the download link/button is front and center.
  2. Now the Selenium Server is ready to respond to commands issued through CFSelenium. What you do next depends on the version of ColdFusion you're running:
    • If you're running ColdFusion 9, run the seleniumTest.cfc MXUnit test in the CFSelenium/test/cf9 folder by calling it in the browser like so:
         http://localhost/CFSelenium/test/cf9/seleniumTest.cfc?method=runTestRemote
    • If you're running ColdFusion 8, run the seleniumTest_cf8.cfc MXUnit test in the CFSelenium/test/cf7_cf8 folder by calling it in the browser like so:
         http://localhost/CFSelenium/test/cf7_cf8/seleniumTest.cfc?method=runTestRemote
    • If you're running ColdFusion 7, run the seleniumTest_cf7.cfm file in the CFSelenium/test/cf7_cf8 folder like so (it mimics the MXUnit test for CF8 and CF9 without using MXUnit):
         http://localhost/CFSelenium/test/cf7_cf8/seleniumTest_cf7.cfm
  3. If everything works, you should observe the Selenium Server react to the commands issued by CFSelenium.  It should start up an instance of Firefox, run through the test routines, and shut down again, and you should be presented with the results of those test routines.
    • If you're on a Windows machine and the Selenium Server fails to launch Firefox, that probably means that Firefox is not defined in your system path, so the Selenium Server does not know the location of the Firefox executable and cannot start Firefox. You can fix this by doing the following:
      • Locate where your firefox.exe file is on your machine (example: C:\Program Files\firefox.exe).
      • Open the appropriate "seleniumTest" file (as listed above).
      • Do a search for " *firefox " (it should only appear once in the file). Add a space after " *firefox " and type in the path to the firefox.exe file, like so:
           *firefox C:\Program Files\firefox.exe
      • Save the file and try again: it should work this time.
  4. To shut down the Selenium Server when you're done, go back to your command window and hit Control-C on your keyboard to shut down the server.

Going Further

Now you have CFSelenium installed, know how to start the Selenium Server that receives the commands from CFSelenium, and have run through one of the included tests, hopefully you'll want to learn how to create your own tests. For that, you should download and install the Selenium IDE plugin for Firefox so that you can record your own browser behavior tests. The documentation for using Selenium IDE is quite thorough.  Pay particular attention to the "Adding Verifications and Asserts With the Context Menu" section:  verifications and asserts are key to actually doing browser behavior testing with Selenium/CFSelenium (I'll touch on that briefly in my next blog post).

Included in the CFSelenium download file is a plugin for the Selenium IDE (yes, a plugin for a plugin) that adds the ability to export the Selenium IDE recordings (test cases) as CFSelenium commands wrapped in an MXUnit test case.  To install it, double-click on the cfml-formatters-1.0.xpi file in the cfSeleniumFiles/formats folder you created out of the CFSelenium download file.  Even if you're running ColdFusion 7 and can't run the plugin output directly (as MXUnit isn't supported in ColdFusion 7), the plugin still saves you the step of translating the recorded steps into CFML cfscript statements.  If you are using ColdFusion 8 or 9 and can run CFSelenium via MXUnit tests, I would suggest checking out the MXUnit documentation, especially the "Testing Basics" section, so you know a bit about how MXUnit works.

After you've recorded a few tests, you'll probably want to run those tests on other browsers.  The Selenium Server supports a number of different browsers but sometimes you have to do some tweaking to get the Selenium Server and the browser working together:  that'll be the subject of another post.

Tuesday, March 22, 2011

CFSelenium Now Compatible With ColdFusion 7 and 8

A few weeks ago, Bob Silverberg released his latest open-source project, CFSelenium.  For those who don't know about the project, a little background:  Selenium is a tool suite for testing web pages.  The most well-known member of the Selenium product family is Selenium IDE, a Firefox plugin that lets you record the actions performed on a web page (or a series of connected web pages) and the results of those actions.  You can then use the recording (stored as a series of commands) to redo those steps whenever you need to test the page or pages after making changes.  Another member of the Selenium product family is Selenium Server (formerly called Selenium-RC), which is a small, Java-based server that can run scripts comprised of Selenium commands in multiple browsers, allowing you to conduct the same kinds of tests recorded by Selenium IDE in browsers other than Firefox:  a great way to test web page functionality across multiple browsers.

In creating CFSelenium, Bob made it possible (easy, in fact) to create and run Selenium Server scripts using ColdFusion 9, and created a plugin for Selenium IDE that would output the recording statements in CFSelenium format within MXUnit test case functions.

Bob wrote CFSelenium in ColdFusion 9-compatible cfscript, and after he announced the project a few folks inquired about the possibility of having a version written in tag-based CFML.  On somewhat of a whim, I decided to take on that task, and ended up with a tag-based version of Bob's original selenium.cfc file that is compatible with both ColdFusion 7 and ColdFusion 8, as well as a few test files that run against the tag-based version.

Bob has now incorporated my tag-based version and my test files into the CFSelenium project, and we've agreed that I will maintain the tag-based version while he maintains the CF9 script verison.  You can download CFSelenium from either RIAForge (http://cfselenium.riaforge.org/) or from GitHub (https://github.com/bobsilverberg/CFSelenium).  For more about Selenium in general, I'd suggest reading Bob's blog post announcing CFSelenium as well as the Selenium website.

So if you're a developer whose responsibilities include cross-browser testing of your web pages, you should really check out CFSelenium.

Wednesday, March 16, 2011

Things To Know About Creating Multiple jQuery UI Datepicker Widgets on the Fly

The first time I created a UI where users could create new jQuery UI Datepicker widgets on the web page as needed, I thought about blogging about some of the "gotchas" I ran into trying to do that, but never got around to it.

But after today, when I had to relearn those lessons, I figured I should write something down.  :)

Here's what I've observed about creating new (multiple) Datepicker widgets on the fly:

  • Cloning a text field that already has the Datepicker applied to it won't work.  You're better off either cloning a "plain" text input element (<input type="text">) or creating one from scratch when you need it.

  • If there's any chance that you're going to end up with multiple Datepicker widgets on the page, you need to make sure that each Datepicker-powered text element ends up with a unique value in the "id" attribute.  Otherwise, the user will click on a date on the pop-up calendar for the most recently-added Datepicker widget, but it'll change the text box value of the first Datepicker widget you added!

    One way to address this is to add a global numeric variable at the top of your Javascript block, increment it every time you want to add a new Datepicker, and combine the current value of that variable with some text and assign that as the id of the text element you're about to add as a Datepicker.

  • Do not invoke the datepicker() function (the function that turns the text field into a Datepicker widget) on the new text input element until after it has been added to the page:  clone or create your text input first, place it in the page using prepend(), append(), whatever, and then invoke the datepicker() function on it.

  • If you need to remove one of the Datepicker widgets, use the datepicker("destroy") method on it.

ColdFusion Builder 2 Extension That List Keyboard Shortcuts in an Eclipse View Panel

Last night, I posted a simple ColdFusion Builder 2 extension that, when activated, creates an Eclipse view that lists all of the defaut Builder-specific keyboard shortcuts in the current ColdFusion Builder 2 beta.  You can get it from RIAForge at http://cfbshortcutkeys.riaforge.org/.

I created this extension using Sagar Ganatra's list of CF Builder 2 keyboard shortcuts and by looking at how Ray Camden altered the VarScoper extension to display in an Eclipse view (see http://www.adobe.com/devnet/coldfusion/articles/cfb2-extensions.html).  The ability to display extension UIs in an Eclipse view (a movable windowed component, like the built-in Navigator or Outline views) rather than a modal window is a wonderful enhancement in Builder 2.

A few notes about the extension:

  • When you open the extension view, you have to click on either the Windows or Mac link to display the proper shortcut list (although the only difference in the shortcuts is that you use either the Ctrl key or the Cmd key).  If there's a way to detect the user's OS automatically, I haven't found it yet (cgi.http_user_agent wasn't useful in this regard).

  • It is a static list of the shortcut defaults as they are shipped in Builder 2.  If you change one of these shortcuts in your preferences, the change WILL NOT be reflected in the list.  I looked around to see if the default keyboard shorcuts were stored in a readable file somewhere that the extension could examine and parse, but it looks like that isn't the case:  user-defined and user-modified shortcuts are written to certain configuration files, but the default shortcuts are not exposed in that manner (if someone knows differently, speak up).

  • However, the advantage to it being a static list is that you can go ahead and customize the list in your copy of the extension.  If you pay attention to where the extension files are copied to when you install the extension, it's easy enough to go to that location, find the extension folder, and edit the index.cfm file in the extension.  It's a simple file, so it's easy to edit.

Hopefully this extension will be useful to folks who are trying to learn the new shortcuts, as you can use it to put the list right next to your editor window as you work.

UPDATE: per Sam Farmer's suggestion in the comments (thanks, Sam!), I've updated the shortcut to do OS detection using the os.name variable value in the server scope.

Monday, January 3, 2011

Small Update to My jQuery textCounting Plugin

I suspect there aren't too many users of my textCounting jQuery plugin, but if you do happen to use it, I wanted to point out a small update I made to it today.

Previously, the plugin was configured by default to look for the letter or word limit for your textarea control in an attribute called "maxLength."  That used to be a safe attribute name because browsers did not enforce the maxlength attribute on textrareas, only single-line text inputs.  But that's no longer the case with Safari:  if you put a maxlength attribute value of "100" on a textarea, Safari will prevent a user from entering more than 100 characters in that textarea.

So I changed the default attribute setting in the plugin to "maxTextLength" to avoid this issue.  To avoid this issue with Safari, you can either download the updated plugin files, or simply use the settings option in the previous version of the plugin to change the attribute name used by the plugin on a case-by-case basis.  In either case, you'll have to make sure you use "maxTextLength" instead of "maxLength" as an attribute in your textareas.