Welcome back
In the previous article, you filled inputs, toggled checkboxes, chose radio buttons, and picked options from a real HTML <select>. Those controls were already on the page when it loaded. You changed their values, but the page did not invent a new suggestion list, swap a table’s rows, or reveal a hidden field.
This lesson moves into controls that behave differently. A list of suggestions can appear only after you type. A table can hold many rows, and the row you need might not be the first one. A page of results can swap its rows for a new set when you click “next.” A field can exist in the page’s code while staying invisible until a button reveals it.
None of this needs new Selenium classes. You already know findElement(), findElements(), click(), sendKeys(), getText(), getAttribute(), and isDisplayed(). This lesson teaches you where to point them when the page keeps changing shape.
The examples use two TestKru pages:
What makes a component dynamic
In the previous examples, the controls were already present when the page loaded. Their values could change, but the page did not generate suggestions, replace table rows, or reveal another control.
A dynamic component changes its content or its visibility while the page keeps running, without loading a new URL. Typing, clicking Next, or revealing a field can rewrite part of the current page. Selenium does not need to know how that JavaScript works. It only needs to look at the page again, after your action, and read whatever is there now.
Four dynamic behaviors show up constantly in real applications, and this lesson covers all four:
| Dynamic behavior | What the browser does | Selenium method that proves it |
|---|---|---|
| Auto-suggestion list | Shows or filters options after you type | Read the <li> options, then getAttribute("value") on the input |
| Data table | Holds many rows, and the one you need is not always first | Loop over <tr> elements and match the row by its text |
| Paginated table | Replaces its rows with a new set when you click “next” | Compare row text, and read the page status text |
| Hidden field | Exists in the page’s code but is not visible until an action reveals it | isDisplayed() before and after the action |
The same discipline from the previous lesson still applies: find the control, check its state when that state matters, perform the action, read the result, and assert it. Dynamic components just ask you to be more careful about which state you are reading, because that state can change more than once.
Choose from auto-suggestions
Open TestKru Dynamic Components. The browser tab title is Dynamic Components. The first practice area is a city search box built from a plain text input and a hidden list, not a real HTML <select>.
An auto-suggestion list shows a short set of matching options while the user is still typing. You have seen this pattern in search boxes on many websites. Here, the input has the id city, and its placeholder text reads Start typing a city. A placeholder is only a hint shown in an empty field, not the field’s value.
The suggestions live in an element with id city-suggestions, which starts hidden. Clicking into city focuses it, and focusing tells the page’s JavaScript to show every available city. Typing then narrows that list to cities whose name starts with what you typed, ignoring upper and lower case. Typing Pu leaves only Pune in the list.
Do not reach for the Select class here. Select only works with a real <select> element and its <option> children, the same helper you used for the Java, Python, and testing-tool dropdowns in the previous lesson. This city list is built from a <ul> and <li> items. Those tags also carry a role attribute (listbox on the list, option on each item). role is a plain HTML attribute, like id or class, so you can match it with the same kind of CSS selector you already know. Selenium can still find and click those items with ordinary locators. You simply do not wrap them in Select.
Type a prefix and read the filtered list
Click the input, then type a short prefix:
WebElement cityInput = driver.findElement(By.id("city"));
cityInput.click();
cityInput.sendKeys("Pu");
List<WebElement> suggestions = driver.findElements(
By.cssSelector("#city-suggestions li[role='option']"));
Assert.assertEquals(suggestions.size(), 1);
Assert.assertEquals(suggestions.get(0).getText(), "Pune");
Clicking first opens the full list. Typing Pu then filters it down. Reading suggestions.size() proves that only one city matched, which is a simple but effective check that filtering worked instead of just hoping it did.
Click an option and confirm the result
A suggestion is only useful once the user picks one. Click the matching option, then read what changed:
suggestions.get(0).click();
Assert.assertEquals(cityInput.getAttribute("value"), "Pune");
Assert.assertEquals(
driver.findElement(By.id("selected-city")).getText(),
"Selected city: Pune");
Clicking the option does two things: it copies the chosen city into city, and it hides the suggestion list again. A separate element, selected-city, then reports the confirmed choice as plain text. Reading both the input’s value and that confirmation text gives two independent proofs that the click worked, since a list item’s click handler is custom page code, not a built-in browser behavior.
Read a table by rows and cells
Open TestKru Tables. The page title is Tables. The main practice table has the id employeeTable, with column headings Name, Age, Department, and Salary across five employee rows.
A well-built HTML table keeps its heading row inside a <thead> and its data rows inside a <tbody>. Each data row is a <tr> element, and each cell is a <td> element. Selenium reads a table by walking that same structure: find the table, find its rows, then find the cells inside the row you care about.
Locate the table and its body rows
Scope your search to the table first, then to its body rows, so you never accidentally grab rows from a different table on a busy page:
List<WebElement> employeeRows = driver.findElements(
By.cssSelector("#employeeTable tbody tr"));
Assert.assertEquals(employeeRows.size(), 5);
findElements() returns a List<WebElement>, Java’s way of holding many items in order. The CSS selector #employeeTable tbody tr reads as “every tr inside the tbody inside employeeTable.” That scoping is the habit to keep: search inside the table, not across the whole page.
Find a row by a value that matters
The second employee is Jane Doe, but a real table test should not depend on knowing her row number in advance. Rows can be sorted, filtered, or added to in a real application, so search for the row by a value that matters instead:
WebElement janeRow = null;
for (WebElement row : employeeRows) {
if (row.getText().contains("Jane Doe")) {
janeRow = row;
break;
}
}
Assert.assertNotNull(janeRow);
This is a plain Java for loop over the rows, checking each row’s full text for the name you are looking for. row.getText() reads everything visible inside that <tr>, so a simple contains() check is enough to identify the right row. Assert.assertNotNull(janeRow) confirms the loop actually found a match instead of silently finishing with nothing.
Once you have the correct row, read the specific cell you need:
List<WebElement> janeCells = janeRow.findElements(By.tagName("td"));
Assert.assertEquals(janeCells.get(2).getText(), "Marketing");
Assert.assertEquals(janeCells.get(3).getText(), "$65,000");
Calling findElements() on janeRow instead of on driver searches only inside that row. The columns are Name, Age, Department, and Salary, in that order, so index 0 is the name and index 3 is the salary, since Java lists count from zero.
TestKru also gives several cells a fixed id, such as emp2Salary. Reading driver.findElement(By.id("emp2Salary")).getText() would return the same salary today, but only because you already know Jane Doe is the second row. The row-by-meaning pattern you just practiced keeps working even when that position changes.
A table whose rows can grow
The same page also has a second table, dynamicTable, plus an addRowBtn button that appends a row while the page keeps running. You will not build that add-row flow here. After new rows appear, the same habit still applies: scope to the table, then re-read its body rows with findElements().
Move through paginated results
Return to TestKru Dynamic Components for the second practice area. Below the city search sits an orders table with the id orders. Its data rows live in ordersRows. A small label with the id page-status reports the current page, for example Page 1 of 2. This table shows only a few results at a time and gives you previous and next buttons to move between pages. That pattern is called pagination. It exists so a page does not have to show every row at once.
Pagination is a good stress test for a beginner habit: proving a change happened, not just performing the click. Clicking Next is easy. Confirming that the table actually moved forward, and did not stay on the same page, is the part worth practicing.
Before clicking anything, locate the table, then read the first page:
WebElement ordersTable = driver.findElement(By.id("orders"));
Assert.assertEquals(
driver.findElement(By.id("page-status")).getText(),
"Page 1 of 2");
String page1Text = ordersTable.findElement(By.id("ordersRows")).getText();
Assert.assertTrue(page1Text.contains("A-101"));
Assert.assertTrue(page1Text.contains("A-102"));
WebElement previousButton = driver.findElement(By.id("previous"));
WebElement nextButton = driver.findElement(By.id("next"));
Assert.assertFalse(previousButton.isEnabled());
Assert.assertTrue(nextButton.isEnabled());
Locating orders first, then reading ordersRows inside it, keeps the same table-scoping habit you used for employeeTable. Checking ordersRows.getText() with contains() confirms both order numbers are present without caring about their exact order inside that text block. isEnabled() reports whether Selenium considers the button enabled. On this page, a disabled Previous button will not accept a click. An enabled button can still fail later if something covers it, so use isEnabled() together with the row text and the page status, not as the only proof. On page one, previous is disabled and next is enabled.
Click next and prove page 2
Now move forward and check every one of those same facts again:
nextButton.click();
Assert.assertEquals(
driver.findElement(By.id("page-status")).getText(),
"Page 2 of 2");
String page2Text = ordersTable.findElement(By.id("ordersRows")).getText();
Assert.assertTrue(page2Text.contains("B-202"));
Assert.assertTrue(page2Text.contains("B-203"));
Assert.assertFalse(driver.findElement(By.id("next")).isEnabled());
Assert.assertTrue(driver.findElement(By.id("previous")).isEnabled());
Notice the pattern: the page status text changed, the row content changed, and the button states flipped. Checking only one of those three would still pass if something odd happened on the page, such as the label changing without the rows actually updating.
This page updates instantly because its data already lives in the browser as plain JavaScript. A real application often has to ask a server for the next page, which takes a moment to arrive. That kind of delay is a synchronization problem, and Module 4 of this course covers it with explicit and fluent waits. For now, focus on reading the result correctly once it appears.
The last practice area on the Dynamic Components page has a button labeled Reveal field with the id reveal, and a text input with the id secret. That input’s placeholder reads Type codekru here.
You already learned to check whether an element is visible in the earlier lesson on element states, using isDisplayed(). This section puts that method to real use. The secret field is present in the page’s HTML the entire time, but its CSS starts it with display: none, so findElement() can locate it while isDisplayed() reports false. Existing in the page’s code and being visible on screen are two separate facts. Clicking reveal changes the field’s inline style to display: block, which is what actually makes it appear and become usable.
Check isDisplayed() before typing
Confirm the starting state before doing anything else:
WebElement secretField = driver.findElement(By.id("secret"));
Assert.assertFalse(secretField.isDisplayed());
findElement() succeeds here even though the field is invisible. Selenium searches the page’s HTML structure, not only what is currently painted on screen, so a hidden element is still findable. isDisplayed() reports Selenium’s displayed state. On this page, it returns false before the field is revealed and true afterward.
Click reveal and type into the field
Now perform the action that changes the field’s visibility, and check the field again before typing into it:
driver.findElement(By.id("reveal")).click();
Assert.assertTrue(secretField.isDisplayed());
secretField.sendKeys("codekru");
Assert.assertEquals(secretField.getAttribute("value"), "codekru");
Reusing the same secretField variable after the click works because clicking reveal does not replace the element, it only changes a CSS property on it. Checking isDisplayed() again, now expecting true, proves the button did its job before you trust the field enough to type into it. Typing into a still-hidden element would throw an exception, since a real browser will not let the user type into something they cannot see.
Put the four patterns together
The complete class below performs all four dynamic patterns in one TestNG test: choosing from auto-suggestions, reading a table by meaning, moving through pagination, and revealing a hidden field. It assumes your existing Maven project already has Selenium 4 and TestNG configured from earlier lessons in this module.
Save the class
Create this file:
src/test/java/com/codekru/tests/DynamicComponentsPracticeTest.java
If your project uses a different base package, adjust the package line to match it.
The complete example
package com.codekru.tests;
import java.util.List;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class DynamicComponentsPracticeTest {
private WebDriver driver;
@BeforeMethod
public void setUp() {
driver = new ChromeDriver();
driver.manage().window().maximize();
}
@Test
public void shouldHandleSuggestionsTablePaginationAndHiddenField() {
driver.get("https://testkru.com/Elements/Tables");
Assert.assertEquals(driver.getTitle(), "Tables");
List<WebElement> employeeRows = driver.findElements(
By.cssSelector("#employeeTable tbody tr"));
Assert.assertEquals(employeeRows.size(), 5);
WebElement janeRow = null;
for (WebElement row : employeeRows) {
if (row.getText().contains("Jane Doe")) {
janeRow = row;
break;
}
}
Assert.assertNotNull(janeRow);
List<WebElement> janeCells = janeRow.findElements(By.tagName("td"));
Assert.assertEquals(janeCells.get(3).getText(), "$65,000");
driver.get("https://testkru.com/Elements/DynamicComponents");
Assert.assertEquals(driver.getTitle(), "Dynamic Components");
WebElement cityInput = driver.findElement(By.id("city"));
cityInput.click();
cityInput.sendKeys("Pu");
List<WebElement> suggestions = driver.findElements(
By.cssSelector("#city-suggestions li[role='option']"));
Assert.assertEquals(suggestions.size(), 1);
Assert.assertEquals(suggestions.get(0).getText(), "Pune");
suggestions.get(0).click();
Assert.assertEquals(cityInput.getAttribute("value"), "Pune");
Assert.assertEquals(
driver.findElement(By.id("selected-city")).getText(),
"Selected city: Pune");
WebElement ordersTable = driver.findElement(By.id("orders"));
Assert.assertEquals(
driver.findElement(By.id("page-status")).getText(),
"Page 1 of 2");
String page1Text = ordersTable.findElement(By.id("ordersRows")).getText();
Assert.assertTrue(page1Text.contains("A-101"));
Assert.assertTrue(page1Text.contains("A-102"));
WebElement previousButton = driver.findElement(By.id("previous"));
WebElement nextButton = driver.findElement(By.id("next"));
Assert.assertFalse(previousButton.isEnabled());
Assert.assertTrue(nextButton.isEnabled());
nextButton.click();
Assert.assertEquals(
driver.findElement(By.id("page-status")).getText(),
"Page 2 of 2");
String page2Text = ordersTable.findElement(By.id("ordersRows")).getText();
Assert.assertTrue(page2Text.contains("B-202"));
Assert.assertTrue(page2Text.contains("B-203"));
Assert.assertFalse(driver.findElement(By.id("next")).isEnabled());
Assert.assertTrue(driver.findElement(By.id("previous")).isEnabled());
WebElement secretField = driver.findElement(By.id("secret"));
Assert.assertFalse(secretField.isDisplayed());
driver.findElement(By.id("reveal")).click();
Assert.assertTrue(secretField.isDisplayed());
secretField.sendKeys("codekru");
Assert.assertEquals(secretField.getAttribute("value"), "codekru");
}
@AfterMethod
public void tearDown() {
if (driver != null) {
driver.quit();
}
}
}
Understand each part
The imports bring in Selenium’s core classes, Java’s List, and TestNG’s annotations and Assert. setUp() starts Chrome and maximizes the window, and tearDown() closes it afterward, guarded by a null check in case Chrome never started.
The test opens Tables first, confirms the title, then loops through employeeTable‘s rows until it finds the one containing “Jane Doe.” It reads that row’s fourth cell, index 3, and confirms the salary is $65,000.
It then moves to Dynamic Components and confirms that title too. It clicks city to open the full suggestion list, types Pu to filter it, confirms exactly one match, clicks that match, and confirms both the input’s new value and the separate confirmation text.
The pagination block locates the orders table, reads the starting page status and row content, confirms previous is disabled and next is enabled, clicks next, then repeats the same three checks for page two with the button states flipped.
Finally, the hidden-field block confirms secret starts invisible, clicks reveal, confirms secret becomes visible, then types into it and confirms the typed value was accepted. Only after the field is proven visible does the test call sendKeys().
Run it and check the result
- Save the class in the path shown above.
- Open
DynamicComponentsPracticeTest.javain IntelliJ. - Right-click
shouldHandleSuggestionsTablePaginationAndHiddenFieldand run it as a TestNG test. - You can also run it through the TestNG Maven setup already configured in your project.
Chrome should open the Tables page, then the Dynamic Components page. You should see the city list appear and narrow to Pune, the orders table swap its two rows after the “next” click, and the secret field appear only after the “Reveal field” button is clicked. Every assertion should pass, and the browser should close on its own.
If a step fails, check which assertion reported the failure first. A mismatched salary usually means the wrong cell index. A missing suggestion usually means the prefix did not match a city that starts with those letters. A wrong page status usually means the click on Next did not register before you read the table again.
Common beginner mistakes
- Wrapping the city list in
Select.city-suggestionsis a<ul>and<li>structure, not a real<select>.Selectonly works with genuine<select>and<option>elements. - Choosing a suggestion by position instead of text. Prefer the option whose visible text is
Pune, not “the first item.” List order can change when the data or the typed prefix changes. - Assuming the row you need is always first. Search for a table row by a value that matters, such as a name, instead of hardcoding a row number that could change later.
- Miscounting cell index. Table columns are indexed from zero, so the fourth column, Salary, is index
3, not index4. - Reading a table cell by fixed id and calling it a general solution. An id like
emp2Salaryonly works because you already know the row position. It is a shortcut for this page, not the row-by-meaning skill this lesson teaches. - Checking only the page status text after pagination. Also confirm the row content changed and the button
isEnabled()states flipped. All three together catch problems one check alone would miss. - Typing into a hidden field. Check
isDisplayed()first. A real browser will not let a user type into something invisible, and Selenium raises an exception if you try. - Confusing “found by
findElement()” with “displayed.” Selenium can locate a hidden element in the page’s HTML. UseisDisplayed()to check Selenium’s displayed state before you type or click. - Reaching for
Thread.sleep()to “wait” for these components. These TestKru demos update instantly with synchronous JavaScript, so there is no delay to wait for. Real waiting strategies for slower pages are covered in Module 4. - Forgetting that
getText()reads the whole row. CallinggetText()on a<tr>returns every visible cell’s text joined together, useful for a quickcontains()check, but not for reading one specific cell.
Practice assignment
Practice reading one table cell by first finding the row that matters:
Continue practicing: Get Specific Table Cell Value
Do not look for a completed solution. Locate the table, find the matching row, and assert the cell value yourself.
For extra table practice, continue with Read Table Headers, Count Table Rows and Columns, and Iterate All Table Data.
After Module 4 covers waits, continue with Handle Dynamic Elements (Appearing Content). That exercise waits for timed content to appear. It is not the reveal-button flow from this lesson.
You can browse every exercise in the Selenium learning catalog.
What is next
You can now handle four component types that behave differently from the fixed controls in the previous lesson: a suggestion list that appears after typing, a table you search by meaning instead of position, a paginated table you prove with both content and button state, and a field that is present but invisible until revealed.
These TestKru pages updated instantly because their data already lives in the browser. Many real applications are slower: a suggestion list may wait for a network response, and a table may take a moment to load its next page. The next lesson, Why Selenium Tests Fail: Timing, Dynamic Pages, and Stale Elements, explains exactly why tests break when a page has not finished changing yet, setting up the explicit and fluent waits you will learn right after it.