Forum Discussion

chicks's avatar
chicks
Regular Contributor
13 years ago

Best practices for Web Page elements



What are the experienced users using to recognize the objects on a webpage?



I have inherited a project that is using the tree model.   This seems OK for a relatively stable website,

but seems like I will have significant problems when a new website will be developed.

Also, it hinders me from writing scripts in advance, since I will not be able to know the objects place in the hierarchy.



In an ideal world, it seems best to have a  unique identifier for all of the elements on the webpage, so I can find

an object by the identifier (presumably the ID string) and operate on it from there, or throw an error if the object does not exist.

Problems with this are:

1) not having control over whether objects have unique identifiers or not

2) possible increased execution time to stop and find every object.  Has this been a problem for anybody?



Thanks for your thoughts and comments.

7 Replies

  • tristaanogre's avatar
    tristaanogre
    Esteemed Contributor
    That is what NameMapping was designed for. :-)



    However, there are features of NameMapping that make it even easier.  There's Conditional Mode, Required Children, and Extended Find.  Depending upon what exactly it is I'm doing, I may use a combination of all three of these within a single project.  You can also use wildcards in NameMapping to handle some of the more dynamic aspects of the page.



    There are times, though, when NameMapping doesn't do it completely for me.  I still use it to create Aliases for pages and root components that I know aren't going to change that frequently.  But then I may need to find within those components additional components that may be a lot more variable and dynamic.  In those cases, rather than spending the time trying to figure out how to map them, I use FindChild or FindAllChildren.



    There's a great webinar screencast posted at http://smartbear.com/support/screencasts/testcomplete/reliable-tests-for-dynamic-objects/ that covers a lot of this.  



    Hope this gives you some direction to work in.


  • In an ideal world, it seems best to have a  unique identifier for all of the elements on the webpage, so I can find

    an object by the identifier (presumably the ID string) and operate on it
    from there, or throw an error if the object does not exist.

    Problems with this are:

    1) not having control over whether objects have unique identifiers or not

    2) possible increased execution time to stop and find every object.  Has this been a problem for anybody?


    We avoid the namemapping/alias features like the plague and use the approach you describe here exclusively.  I wrote a Jscript library for our purposes specifically to make this easier.



    Performance has never really been an issue.  Occasionally there is an issue finding unique properties to identify a control, but it is very rare.
  • chicks's avatar
    chicks
    Regular Contributor
    Robert, Bert,  thanks for your responses.



    I have taken the time to review the webinar again and found it more interesting this time around.  It seems like the namespace mapping does have numerous options for being flexible in getting a handle on an object.  I can see where the conditional mapping will help me solve my modal dialog problems (they're appearing differently on different versions of the browser).   I suppose I can mix name space mapping for the objects that require a condition and regular hierarchical identification the rest of the time.



    Robert, when do you decide to use name space mapping and when do you go with another approach?



    Bert, can you expand on your JScript library idea please?



    My co-worker is experimenting with a script to find all objects on a page and map them to shorter variable names, similar to the QTP 

    object repository concept.  As I understand it, we will go through and run this as an initial step; and then run our regular scripts.

    This seems like a combination of the Test Complete Namespace mapping (with less flexibility in object recognition) and alias concepts.



    For those of you with enough time, I've included a code snippet before showing how I'm currently referencing objects for your comments.

    I'm 'folding up' the reference by creating intermediate variables, so that the path in the check property is minimized, in this case

    currPage \ transaction panel \ 'row in the table'.   There's a picture attached.

    ========================================================



    function checkOfferDetailsInAccountOverview(whatRow,expectedOfferName,expectedReward,expectedOfferType)

    {

      // NO NAVIGATION

      var currPage =   Sys.Process("iexplore", iexploreNum).Page("*");

      // for account overview, only the most recent month is shown, we don't have an extra panel

      // for the separate months

      var transaction_panel = currPage.Panel(0).Panel("content").Panel(1).Panel(0).Form("recentactivity").Panel(0).Panel(2).Panel(1).Panel(0);



    // hover mouse not needed for script.... but makes action visible

      transaction_panel.Table(0).Cell(whatRow, 2).HoverMouse();



    // verify offer details section exists with headers

      aqObject.CheckProperty(transaction_panel.Table(0).Cell(whatRow, 2).Panel(0).Panel(2), "innerText", cmpContains, "OFFER DETAILS\r\nOFFER NAMEREWARD EARNEDOFFER TYPE");

    // verify specific offer details IS CONTAINED IN THE PANEL

      aqObject.CheckProperty(transaction_panel.Table(0).Cell(whatRow, 2).Panel(0).Panel(2), "innerText", cmpContains, expectedOfferName+expectedReward+expectedOfferType);


  • tristaanogre's avatar
    tristaanogre
    Esteemed Contributor
    In my current incarnation of a software automator, I've been using the NameMapping more often than not because the application I'm working with has much more predictable, static pages.  There's not going to be much chance that different elements will appear on the same pages with different values in this environment.



    Previously, though, working with an eCommerce web application, the main store page could have any number of different "identical" components but they would be a different set depending upon the category of products displayed... always the same page, but a different listing.  So, in that case, I'd map the Page and a couple of "static" components but then use methods like FindChild, FindAllChildren, etc, to search out the specific objects/items that I'd need for the test I was running.  Because the test itself was data driven, combined with a data driven web page, I needed that flexibility that if the data in the test said "Select 5 of Item X", I'd be able to find Item X on the page and enter a quantity of 5 in the text box corresponding to that item.



    So... I guess that would be a good rule of thumb to use in determining when to use NameMapping and when not to...  Data Driven tests combined with a Data Driven web applications means less reliance on NameMapping and more reliance on dynamic code-driven object identification.  A lesser degree of Data Driven means a greater ability to rely on NameMapping.  A greater degree of Data Driven means a greater reliance on code-driven "FindChild".
  • Hi Robert Martin,



    I there any way  to work with both the approaches like first i search my object with namemapping and Is the object not found then i automatically use the properties added in namemapping corresponding to that object to use with .Find method e.g



    Function k_clickLink(PageName,ObjectName,oRow)

    If PageName <> "" then

    a=k_ObjectExistence(PageName,ObjectName)

    Eval("Aliases.iexplore." & PageName & "." & ObjectName & ".Click")

    Log.Message ("Click operation performed on " & ObjectName)

    End if

    End Function







    Function k_ObjectExistence(PageName,ObjectName)

    Dim Page_Name, Object_Name

    Page_Name = PageName

    Object_Name = ObjectName

      For i = 1 to 5

      If Eval("Aliases.iexplore." & Page_Name & "." & Object_Name &".Exists" ) = "True" then

        Log.Message (Object_Name & " exists on the page " & Page_Name)

        exit for

        else

        Log.Message ("Checking existance of " & Object_Name & " in iteration "  & i & " on page "& Page_Name)

        delay(2000)

        Aliases.iexplore.Page_Name.Refresh

      End if

      Next  

    End Function



    Here in above example what i m doing is, I create a NameMapping file and Used extended find for all objects so that my objects comes just under my page in the hierarchy. I made a function "k_ObjectExistence" just to check the existance of object using Exists method.



    I want any solution in which if my objects doesn't gets identified on the page using Aliases my function will return any True/False value basis which I use properties of objects defined in the NameMapping to use with .Find method in the same function defined above.







    ...Thanks in advance
  • tristaanogre's avatar
    tristaanogre
    Esteemed Contributor
    Sure, you can do it that way... I wouldn't, though. If the object is not found in NameMapping/Aliases, most likely it's because either you misconfigured your namemapping so the object is not found... or the object doesn't exist.



    The combination of the two would come in as I described above where you have some static components on your pages that would be mapped but, under those, would be data-driven dynamic segments that using a "Find" function would work best.



    For the ecommerce site, there was a content well that everything up to and including the content well was static.  I could get to a lot of the links around the page, the specific navigation tools, etc.  No matter what category of products I'd be "buying", that information was static.  So, those were my candidates for NameMapping and I'd make sure I did my due diligence to keep that namemapping up to date and very specific.  When it came to selecting particular products for sale, that's when I'd use the FindChild and FindAllChildren functions.



    Now, if the problem is that there is a delay between clicking a button and when the object comes up on screen, that's a different problem to be handled with the WaitAliasChild function.  I'd click on a button to go to a category of products, I'd then use "WaitAliasChild" to make sure that the list of products would resolve.
  • Bert, can you expand on your JScript library idea please?




    Sure.  Here is a (trivial) example of some test code I would write using the script based approach:





    //USEUNIT WebForms



    function Main()

    {

      var url = "http://smartbear.com";

     

      //open up a browser and navigate to smartbear

      var ie = TestedApps.iexplore.Run();

      while(!ie.Exists)

      {

        delay(1000);

      }

      var $ = new WebForm(ie.WaitPage("*", 5000));

      $.NavigateToURL(url);

     

      //search for testcomplete

      var searchBox = $.Find([["idStr","*_queryText*"],["ObjectType","Textbox"]],10); //1

      $.SetValue(searchBox, "TestComplete"); //2

      $.Click([["idStr","*_searchButton"],["ObjectType","Button"]],10); //3

     

      $.CloseBrowserWindow();

     

    }





    Note on lines 1-3 are the most comment operations you will perform on a web page during a test, finding an item, setting a value, or clicking something.  You really don't even need line one, you could just do:





      $.SetValue([["idStr","*_queryText*"],["ObjectType","Textbox"]], "TestComplete");





    A couple of notes about it.  I find the way TestComplete's Find method works to be non-intuitive; it seems more logical to me to search for an array of paired properties like [property, value] rather than to separate the properties and the values into separate arrays like they do. You mentioned performance; on any of our pages I've never had a problem with the performance of finding our page elements this way.  We still run TestComplete7 ($2k is a pretty big sum to drop for a very small company for v8), and I've noticed this method runs slower on in TestComplete7 on Windows7 than on WindowsXP; but not enough that I worry about it on an overnight test.



    One of the big wins here is that all I really need is a Page object.  As long as you can get a Page object that you can wrap with WebForm, you should be able to find the elements on the page that you need to work with.  I haven't actually done a lot of cross browser testing, but assuming TestComplete8 delivers a FireFox Page object that works with Find and FindChild in the same way, or Chrome or whatever other browser they choose to support in the future, all that should fall out naturally.



    I wrote the library because when I started out with TestComplete in 2008, replaying the recorded scripts seemed extremely hit or miss on whether the namemapping/aliases would hold up from run to run.  That, plus we're relatively agile with respect to our web pages and the the structure of the page could change frequently enough that it was painful to rely on a particular structure.



    The heart of the library is the Find and FindChild functions in the WebForm object.  Those are mostly wrappers around TestCompletes Find and FindChild, but they take care of the above mentioned property/value oddity, and also allow for multiple attempts to find a particular object if you have a slow loading page at the point in time you run the test.



    Anyway, you can take a peek at the code; I'm attaching a sample project that contains the library as I'm using it now.  I should probably go back in at some point and clean out some unused functions or other cleanup, but if you stick to the functions I mentioned above you should be on solid ground.  Our company runs several web based services and we've converted almost all of the testing for the web apps over to TestComplete using this library.  At times I feel like I'm working around TestComplete a bit and there may be a better way to do it, but TestComplete helps out in other areas like making it easy to do database comparisons and things like that.  Below is an example of a test I wrote for one of our new pages yesterday.  You can see in certain cases it's a little bit of a mix of our library, and TestComplete methods like WaitWindow.  The findIEModalWindow function is something that should ideally be part of the WebForm or some other include that I just haven't gotten around to yet.







    function TestOilDataSubmission()

    {

      var DEFAULT_DELAY = 3000;

       var errorMsgs = ["The specified XML could not be loaded.",

                       "There was an error.  The complete message is displayed above the Oil Data box."];

     

      //close the open browsers

      oilographyForm.CloseBrowserWindow();

      while(oilographyForm.Page.Parent.Exists)

      {

        Delay(1000);

      }

     

      //Test Data

      var data =

      [

        //Test submit Sample Points  

        {

          SubmissionMode: "Sample Points",

          OilLabGUID: "D080AB0A-94F5-4C3C-A569-E9E3B1D50455",

          UniqueSubmissionCode: "12345678",

          OilDataXML: XML.OilDataSubmission_SamplePoints.Document.xml

        },

        //Test submit Oil Data

        {

          SubmissionMode: "Oil Test Data",

          OilLabGUID: "D080AB0A-94F5-4C3C-A569-E9E3B1D50455",

          UniqueSubmissionCode: "87654321",

          OilDataXML:XML.OilDataSubmission_OilDataSet.Document.xml

        }      

      ];

     

      //Open the test page

      $ = new WebForm(TestedApps.Oilography.Run().WaitPage("*", 4000));

      var url = "http://localhost/concert/oilography/oildatasubmission.aspx";

      $.NavigateToURL(url);

     

      for (var currData=0; currData < data.length; currData++)

      {



        $.SetValue([["idStr","SubmissionMode"],["ObjectType","Select"]],data[currData].SubmissionMode);

        $.SetValue([["idStr","OilLabGuid"],["ObjectType","Textbox"]], data[currData].OilLabGUID);

        $.SetValue([["idStr","UniqueSubmissionCode"],["ObjectType","Textbox"]], data[currData].UniqueSubmissionCode);

        $.SetValue([["idStr","OilDataXml"],["ObjectType","Textarea"]], data[currData].OilDataXML);

        $.Click([["idStr","SubmitButton"],["ObjectType","SubmitButton"]]);



        var modalWin = findIEModalWindow("Message from webpage");

        if (!modalWin.Exists) Log.Error("Could not locate the messagebox.")

        var responseCaption = modalWin.WaitWindow("Static","*",2,DEFAULT_DELAY).WndCaption;

        for (var i=0; i <= errorMsgs.length; i++)

        {

          if (responseCaption.contains(errorMsgs))

          {

            Log.Message(responseCaption);

            $.Click(modalWin.WaitWindow("Button","OK",1,DEFAULT_DELAY));

            Log.Message($.Find([["idStr","ErrorMsg"],["ObjectType","TextNode"]]).outerText);

            Log.Error("Sample Points could not be saved.");

            return;

          }

     

        }

        $.Click(modalWin.WaitWindow("Button","OK",1,DEFAULT_DELAY));

      }

      DBTables.OilDataSubmission_OilDataSet.Compare(true, 3);

      DBTables.OilDataSubmission_OilDataSetComments.Compare(true,3);

      DBTables.OilDataSubmission_OilDataSetStatus.Compare(true,3);

      DBTables.OilDataSubmission_OilDataSetSubmission.Compare(true,3);

      DBTables.OilDataSubmission_SamplePoint.Compare(true,3);

      DBTables.OilDataSubmission_SamplePointSchedule.Compare(true,3);



      function findIEModalWindow(caption, refresh)

      {

        if (refresh || true) Sys.Refresh();

        for (var i = 0; i < Sys.ChildCount; i++)

        {    

         p=Sys.Child(i);

         // If the process is iexplore.exe

          if (p.ProcessName=="iexplore")

           {        

            // Search for IEFrame window

            w=p.WaitChild("Window(\"#32770\", \"" + caption + "\", 1)", 100);

            if (w.Exists)

             return w;

           }

        }

      }

    }