Search code examples
c#selenium-webdriver

Test fixture (Selenium) for populating a slow-loading XML list doesn't work


I am writing Se tests for an Ext JS 3.x website. This is a very old framework from Sencha. It is not HTML 5 compliant and deeply nests elements that need to be tested.

One case is an object with 70 dates. In debug, the tests will run. In Run mode, there isn't time for the list to populate and by the time its elements are needed, the error 'Sequence contains no elements' is thrown.

Incorporating all the code needed to drill into this list into its own browser test fixture still falls prey to the same issue.

Here is the HTML for the list, showing the detail into the first element of the list:

<?xml version="1.0" encoding="utf-8" ?>
<div id="programmaticWindow" class=" x-window x-window-plain" style="position: absolute; z-index: 9003; visibility: visible; left: 278px; top: 246px; width: 600px; display: block;">
  <div class="x-window-tl">
    <div class="x-window-tr">
      <div class="x-window-tc">
        <div class="x-window-header x-unselectable x-window-draggable" id="ext-gen286">
          <div class="x-tool x-tool-close" id="ext-gen292">&nbsp;</div>
          <span class="x-window-header-text" id="ext-gen298">Programmatic Dates</span>
        </div>
      </div>
    </div>
  </div>
  <div class="x-window-bwrap" id="ext-gen287">
    <div class="x-window-ml">
      <div class="x-window-mr">
        <div class="x-window-mc" id="ext-gen291">
          <div class="x-window-body x-border-layout-ct" id="ext-gen288" style="width: 588px; height: 345px;">
            <div id="ext-comp-1217" class=" x-panel x-panel-noborder x-border-panel" style="padding: 5px; width: 190px; left: 0px; top: 0px;">
              <div class="x-panel-bwrap" id="ext-gen304">
                <div class="x-panel-body x-panel-body-noheader x-panel-body-noborder" id="ext-gen305" style="width: 190px; height: 335px;">
                  <div id="ext-comp-1214" class=" x-panel x-panel-noborder x-grid-panel" style="width: 190px;">
                    <div class="x-panel-bwrap" id="ext-gen308">
                      <div class="x-panel-body x-panel-body-noheader x-panel-body-noborder" id="ext-gen309" style="height: 335px; width: 190px;">
                        <div class="x-grid3" hidefocus="true" id="ext-gen310" style="width: 190px; height: 335px;">
                          <div class="x-grid3-viewport" id="ext-gen311">
                            <div class="x-grid3-header" id="ext-gen312">
                              <div class="x-grid3-header-inner" id="ext-gen314" style="width: 190px;">
                                <div class="x-grid3-header-offset" style="width:190px;">
                                  <table border="0" cellspacing="0" cellpadding="0" style="width: 174px;">
                                    <thead>
                                      <tr class="x-grid3-hd-row">
                                        <td class="x-grid3-hd x-grid3-cell x-grid3-td-FormattedDate x-grid3-cell-first " style="width: 100px;">
                                          <div class="x-grid3-hd-inner x-grid3-hd-FormattedDate" unselectable="on" style="">
                                            Programmatic Date<img alt="" class="x-grid3-sort-icon" src="/pdpdev/scripts/ext/resources/images/default/s.gif">
                                          </div>
                                        </td>
                                        <td class="x-grid3-hd x-grid3-cell x-grid3-td-isActiveDate x-grid3-cell-last" style="width: 74px;text-align: center;">
                                          <div class="x-grid3-hd-inner x-grid3-hd-isActiveDate" unselectable="on" style="">
                                            Active<img alt="" class="x-grid3-sort-icon" src="/pdpdev/scripts/ext/resources/images/default/s.gif">
                                          </div>
                                        </td>
                                      </tr>
                                    </thead>
                                  </table>
                                </div>
                              </div>
                              <div class="x-clear"></div>
                            </div>
                            <div class="x-grid3-scroller" id="ext-gen313" style="width: 190px; height: 311px;">
                              <div class="x-grid3-body" style="width:174px;" id="ext-gen315">
                                <div class="x-grid3-row x-grid3-row-first" style="width:174px;">
                                  <table class="x-grid3-row-table" border="0" cellspacing="0" cellpadding="0" style="width:174px;">
                                    <tbody>
                                      <tr>
                                        <td class="x-grid3-col x-grid3-cell x-grid3-td-FormattedDate x-grid3-cell-first " style="width: 100px;" tabindex="0">
                                          <div class="x-grid3-cell-inner x-grid3-col-FormattedDate" unselectable="on">11/21/2025</div>
                                        </td>
                                        <td class="x-grid3-col x-grid3-cell x-grid3-td-isActiveDate x-grid3-cell-last " style="width: 74px;text-align: center;" tabindex="0">
                                          <div class="x-grid3-cell-inner x-grid3-col-isActiveDate" unselectable="on">
                                            <input type="checkbox" disabled="disabled" class="noauto" checked="checked">
                                          </div>
                                        </td>
                                      </tr>
                                    </tbody>
                                  </table>
                                </div>

Here are screens shot of the dialog window, with HTML containing the list:

List of Div Containers Containing Divs for One Date

There are 70 containing divs, using the x-grid3-row class element to group them.

The div containing elements (70) must be drilled into in a for-each loop to get if they're checked or not, and the date value.

Use cases for this dialog depend on access to the list itself. A date must be selected for it to be shown in the Programmatic Date textbox, for example.

From there it can be activated (checked) and so on.

'Programmatic' in this sense means a date that will be used as a milestone. These are in turn tied to projects.

Solutions I've tried:

  1. Using .Take() to get just 10 of the elements. Fails, since the entire list must still be created to take any number of elements.
  2. Thread.Sleep()
  3. Wait Helpers
  4. Putting the code into its own test fixture, banking on Se getting the list populated as part of its set up.

Here is the code for attempt 4. The idea here is the Setup() method, called from the constructor, pops the ProgrammaticDatesList and this done, its value is passed into its own property which should have the full list.

This should be part of setup. But it isn't happening.

The same error as having the code directly in the [Fact] is happening, the 'Sequence contains no elements' error.

private static ReadOnlyCollection<IWebElement> ProgrammaticDatesList =>
    AdminManageProgrammaticDatesDialog.FindElement(
        By.XPath(CommonXpathStrings.adminManageProgrammaticDialogDateList))
            .FindElements(By.XPath(
                CommonXpathStrings.adminManageProgrammaticDialogDateListItems));
private static ReadOnlyCollection<IWebElement> myElements = null;

#endregion
#region Methods

public static ReadOnlyCollection<IWebElement> ReturnProgrammaticDatesList()
{
    return myElements;
}

private void SetupBrowser()
{
    EdgeDriver.Manage().Window.Size = new Size(1520, 1080);
    EdgeDriver.Navigate().GoToUrl(CommonStrings.localhostDevRoot);
    HomePageClickHereLoginAsAdmin();
    Thread.Sleep(3000);
    EdgeDriver.SwitchTo().Window(EdgeDriver.CurrentWindowHandle);
    CommonMethods.WaitElementByIdVisibleEdge(
        EdgeDriver, 3000, CommonXpathStrings.profilePageToolbarId);
    ClickMenuDropDownSignedIn();
    ClickMenuManageProgrammaticAsAdmin();
    GetProgrammaticDateList();
}

Thanks for any suggestions.

=========== Jeff, as requested, we start withthe GetProgrammaticDateList() method. I created an unused method and put the full xpath strings in it:

private void ExpandedGetProgrammaticDateList()
{
    IReadOnlyCollection<IWebElement> ProgrammaticDatesList =
    EdgeDriver.FindElement(By.Id("programmaticWindow"))
    .FindElement(
        By.XPath(@"div[contains(@class, 'x-window-bwrap')]
            /div/div/div/div/div/div/div/div/div/div/div/div[contains(@class, 'x-grid3-viewport')]
            /div[contains(@class, 'x-grid3-scroller')]
            /div[contains(@class, 'x-grid3-body')]"))
    .FindElements(By.XPath(@"div[contains(@class, 'x-grid3-row')]"));
}

The HTML for this is already included, above. Let me know if this will work. I'm here until 3:30ish, EST.


Solution

  • I don't know all of your use cases but I would do something like this...

    1. Wait for the full table to load using WebDriverWait. This may need to be tweaked if it doesn't work as written below. If we know there's always 70 dates, we can wait for that. We can also wait for JS/jQuery to finish in the background, etc. The hard part may be figuring out what specifically to wait for.
    2. Once we have the full list, loop through all of the HTML TABLE rows and grab the date (DateOnly) and checkbox state (bool) and stuff them into a dictionary with the DateOnly as the key. That allows fast lookups but may be overkill for what you need.
    3. Now that we have the full dictionary, we can do whatever tests/asserts we need on it rather than having to repeatedly go back to the table/page.

    I replaced all your chained XPaths, etc. and reduced them all to a simple CSS selector.

    NOTE: I store the dates as a DateOnly type. This makes it much easier to deal with dates if you need to add/subtract days, do comparisons, etc. If you don't need that "power", you can just replace DateOnly with string.

    The refactored method

    private Dictionary<DateTime, bool> GetProgrammaticDateList()
    {
        Dictionary<DateTime, bool> dateList = new Dictionary<DateTime, bool>();
        WebDriverWait wait = new WebDriverWait(driver, TimeSpan.FromSeconds(60)); // adjust to whatever time you need for the full table to load
        ReadOnlyCollection<IWebElement> rows = wait.Until(ExpectedConditions.VisibilityOfAllElementsLocatedBy(By.CssSelector("#programmaticWindow table.x-grid3-row-table tr")));
        foreach (IWebElement row in rows)
        {
            dateList.Add(DateTime.Parse(row.FindElement(By.CssSelector("div.x-grid3-col-FormattedDate")).Text), row.FindElement(By.CssSelector("input")).Selected);
        }
    
        return dateList;
    }
    

    The test code to call and use it. I added a few different examples of how to use the dictionary of date/bool values.

    // navigate to the URL, log in as admin, etc. goes here
    
    Dictionary<DateTime, bool> dateList = GetProgrammaticDateList();
    
    // assert some values
    Assert.AreEqual(dateList[new DateTime(2025, 11, 21)], true);
    Assert.AreEqual(dateList[new DateTime(2025, 11, 22)], false);
    
    // print all values
    foreach (KeyValuePair<DateTime, bool> date in dateList)
    {
        Console.WriteLine($"{date.Key.ToString("MM/dd/yy")}, {date.Value}");
    }
    

    NOTE: This works on the HTML you posted so I'm assuming it will work on the full HTML. If it doesn't I'll need more HTML... probably a few rows of dates to fix my locator(s).