Real-Time Selenium Coding Challenges — 30 Solutions¶
Goal: be able to write AND explain each of these from memory in an interview.
Q1. Write a Selenium script to handle dynamic web elements whose locators change frequently.¶
Approach: Avoid brittle absolute IDs/XPaths; use stable relative XPath/CSS anchored on stable text, partial attributes (contains, starts-with), or Selenium 4 relative locators.
public WebElement findDynamic(String stableLabel) {
// Anchor on visible text that does not change, not on an auto-generated id like id_1234
By stableXpath = By.xpath("//*[contains(normalize-space(text()),'" + stableLabel + "')]");
// Partial attribute match: id="user_input_9f2a" -> match the stable prefix
By partialAttr = By.cssSelector("input[id^='user_input_']");
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
try {
return wait.until(ExpectedConditions.visibilityOfElementLocated(stableXpath));
} catch (TimeoutException e) {
return wait.until(ExpectedConditions.visibilityOfElementLocated(partialAttr));
}
}
// Selenium 4 relative locator: find input to the right of a stable label
public WebElement findByRelative(WebElement label) {
return driver.findElement(RelativeLocator.with(By.tagName("input")).toRightOf(label));
}
Say it aloud: "I anchor on the most stable thing — visible text or an attribute prefix via contains/starts-with — and fall back to Selenium 4 relative locators instead of hardcoding generated IDs."
Q2. Automate a scenario to select multiple options from a multi-select dropdown.¶
Approach: Use the Select class; assert isMultiple() first, then selectByVisibleText/selectByValue repeatedly.
public void selectMultiple(By locator, String... visibleTexts) {
Select select = new Select(driver.findElement(locator));
if (!select.isMultiple()) {
throw new IllegalStateException("Dropdown is not multi-select");
}
select.deselectAll();
for (String text : visibleTexts) {
select.selectByVisibleText(text);
}
// Verify
List<String> selected = select.getAllSelectedOptions()
.stream().map(WebElement::getText).collect(Collectors.toList());
Assert.assertEquals(selected.size(), visibleTexts.length);
}
Say it aloud: "Select handles native <select multiple> elements; I check isMultiple, deselect all, then loop and finally verify the count of selected options."
Q3. Write code to upload a file using Selenium without using AutoIT or external tools.¶
Approach: Send the absolute file path directly to the <input type='file'> element with sendKeys — no OS dialog is triggered.
public void uploadFile(By fileInputLocator, String absolutePath) {
WebElement fileInput = driver.findElement(fileInputLocator);
// If input is hidden, un-hide it so sendKeys works
((JavascriptExecutor) driver).executeScript(
"arguments[0].style.display='block'; arguments[0].style.visibility='visible';",
fileInput);
fileInput.sendKeys(absolutePath); // e.g. "/Users/rohan/docs/resume.pdf"
}
Say it aloud: "For a real <input type=file>, sendKeys with the absolute path sets the file without ever opening the native dialog, so I never need AutoIT or Robot."
Q4. Handle a JavaScript alert popup and verify its message.¶
Approach: Wait for the alert, switch to it, read getText, then accept() or dismiss().
public String handleAlert(boolean accept) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
Alert alert = wait.until(ExpectedConditions.alertIsPresent());
String message = alert.getText();
if (accept) alert.accept(); else alert.dismiss();
return message;
}
// Usage
String msg = handleAlert(true);
Assert.assertEquals(msg, "Are you sure?");
Say it aloud: "I wait for alertIsPresent, switch context to the alert, capture getText for my assertion, then accept or dismiss it."
Q5. Automate drag and drop functionality on a web page.¶
Approach: Use Actions.dragAndDrop; fall back to a JS/HTML5 drag simulation if the native action fails.
public void dragAndDrop(By source, By target) {
WebElement src = driver.findElement(source);
WebElement tgt = driver.findElement(target);
Actions actions = new Actions(driver);
actions.clickAndHold(src)
.moveToElement(tgt)
.release()
.build().perform();
// Or the shorthand: new Actions(driver).dragAndDrop(src, tgt).perform();
}
Say it aloud: "I use Actions — click-and-hold the source, move to the target, release — which is more reliable across sites than the one-shot dragAndDrop."
Q6. Scroll to a specific element on a page and verify its visibility.¶
Approach: Use JavascriptExecutor.scrollIntoView, then assert isDisplayed.
public boolean scrollToAndVerify(By locator) {
WebElement el = driver.findElement(locator);
((JavascriptExecutor) driver).executeScript(
"arguments[0].scrollIntoView({block:'center'});", el);
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
return wait.until(ExpectedConditions.visibilityOf(el)).isDisplayed();
}
Say it aloud: "scrollIntoView centers the element in the viewport, then I wait for visibilityOf and return isDisplayed to confirm it is actually on screen."
Q7. Switch between multiple browser windows and verify the page titles.¶
Approach: Capture the original handle, iterate getWindowHandles, switch and verify titles.
public void switchWindowsAndVerify(String expectedNewTitle) {
String original = driver.getWindowHandle();
for (String handle : driver.getWindowHandles()) {
if (!handle.equals(original)) {
driver.switchTo().window(handle);
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.titleIs(expectedNewTitle));
Assert.assertEquals(driver.getTitle(), expectedNewTitle);
driver.close();
}
}
driver.switchTo().window(original); // return to parent
}
Say it aloud: "I save the parent handle, loop over all handles switching to each new one to assert its title, close it, and finally switch back to the parent."
Q8. Write a method to wait explicitly for an element to be clickable.¶
Approach: WebDriverWait + ExpectedConditions.elementToBeClickable.
public WebElement waitClickable(By locator, int timeoutSeconds) {
return new WebDriverWait(driver, Duration.ofSeconds(timeoutSeconds))
.until(ExpectedConditions.elementToBeClickable(locator));
}
// Usage
waitClickable(By.id("submit"), 10).click();
Say it aloud: "elementToBeClickable waits until the element is both visible and enabled, so my click never hits a not-yet-interactable element."
Q9. Handle a frame or iframe and interact with elements inside it.¶
Approach: switchTo().frame(...) (by index/name/WebElement), interact, then switchTo().defaultContent().
public void typeInsideFrame(By frameLocator, By fieldInside, String text) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.frameToBeAvailableAndSwitchToIt(frameLocator));
driver.findElement(fieldInside).sendKeys(text);
driver.switchTo().defaultContent(); // always switch back out
}
Say it aloud: "frameToBeAvailableAndSwitchToIt both waits and switches; I interact inside, then always return to defaultContent so the next steps work."
Q10. Automate mouse hover and perform click action on a submenu.¶
Approach: Actions.moveToElement on the parent, then click the revealed submenu.
public void hoverAndClickSubmenu(By menu, By submenu) {
WebElement menuEl = driver.findElement(menu);
Actions actions = new Actions(driver);
actions.moveToElement(menuEl).perform();
WebElement subEl = new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(submenu));
actions.moveToElement(subEl).click().perform();
}
Say it aloud: "I hover the parent with moveToElement to reveal the flyout, wait for the submenu to become visible, then move and click it."
Q11. Capture and save a screenshot after a test failure automatically.¶
Approach: Use TakesScreenshot inside a TestNG ITestListener.onTestFailure for automatic capture.
public class ScreenshotListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
WebDriver driver = DriverFactory.getDriver(); // your driver accessor
File src = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
String name = result.getName() + "_" + System.currentTimeMillis() + ".png";
try {
Files.copy(src.toPath(), Paths.get("screenshots", name),
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
}
// Register via @Listeners(ScreenshotListener.class) or testng.xml
Say it aloud: "A TestNG ITestListener fires onTestFailure automatically, where I cast the driver to TakesScreenshot and save the PNG named after the failing test."
Q12. Write a reusable method to handle calendar date selection.¶
Approach: Navigate month headers with next/prev buttons until the target month/year, then click the day.
public void selectDate(String targetMonthYear, String day) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
By header = By.cssSelector(".datepicker-switch");
By next = By.cssSelector(".datepicker .next");
while (!driver.findElement(header).getText().equals(targetMonthYear)) {
driver.findElement(next).click();
}
driver.findElement(By.xpath(
"//td[contains(@class,'day') and not(contains(@class,'old')) "
+ "and not(contains(@class,'new')) and text()='" + day + "']")).click();
}
Say it aloud: "I read the calendar header and click next until it matches my target month-year, then click the day cell while excluding the greyed-out old/new days from adjacent months."
Q13. Automate pagination controls and verify data on multiple pages.¶
Approach: Loop clicking "Next" while it is enabled, collecting/asserting row data per page.
public List<String> collectAllPages(By rowLocator, By nextButton) {
List<String> allData = new ArrayList<>();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
while (true) {
wait.until(ExpectedConditions.presenceOfElementLocated(rowLocator));
driver.findElements(rowLocator).forEach(r -> allData.add(r.getText()));
WebElement next = driver.findElement(nextButton);
boolean disabled = !next.isEnabled()
|| next.getAttribute("class").contains("disabled");
if (disabled) break;
next.click();
}
return allData;
}
Say it aloud: "I harvest rows on each page, then click Next; I stop when the Next control is disabled or carries a disabled class, returning the aggregated data for verification."
Q14. Handle dynamic tables and extract specific row/column data.¶
Approach: Locate <tr>/<td> by index or match a cell's text, then read the neighbouring column.
public String getCellByRowText(By tableLocator, String rowKey, int targetColIndex) {
WebElement table = driver.findElement(tableLocator);
for (WebElement row : table.findElements(By.tagName("tr"))) {
List<WebElement> cells = row.findElements(By.tagName("td"));
if (!cells.isEmpty() && cells.get(0).getText().equals(rowKey)) {
return cells.get(targetColIndex).getText();
}
}
throw new NoSuchElementException("Row not found: " + rowKey);
}
Say it aloud: "I iterate the <tr> rows, match the key cell, then pull the target column by index — this survives dynamic row ordering because I match on content, not position."
Q15. Write a script to verify broken links on a web page.¶
Approach: Collect all <a href>, fire an HTTP HEAD/GET via HttpURLConnection, flag response codes >= 400.
public List<String> findBrokenLinks() throws Exception {
List<WebElement> links = driver.findElements(By.tagName("a"));
List<String> broken = new ArrayList<>();
for (WebElement link : links) {
String url = link.getAttribute("href");
if (url == null || url.isEmpty() || url.startsWith("javascript")) continue;
// URI.create(...).toURL() — the new URL(String) constructor is deprecated since Java 20
HttpURLConnection conn = (HttpURLConnection) java.net.URI.create(url).toURL().openConnection();
conn.setRequestMethod("HEAD");
conn.setConnectTimeout(5000);
conn.connect();
int code = conn.getResponseCode();
if (code >= 400) broken.add(url + " -> " + code);
conn.disconnect();
}
return broken;
}
Say it aloud: "I gather every href, open an HttpURLConnection with a HEAD request, and any response code 400 or above is a broken link — far faster than actually navigating each URL."
Q16. Automate a login test with data-driven inputs using Excel or CSV.¶
Approach: Read rows with Apache POI and feed them to the test via a TestNG DataProvider.
@DataProvider(name = "loginData")
public Object[][] loginData() throws IOException {
FileInputStream fis = new FileInputStream("testdata/logins.xlsx");
Workbook wb = new XSSFWorkbook(fis);
Sheet sheet = wb.getSheetAt(0);
int rows = sheet.getPhysicalNumberOfRows();
Object[][] data = new Object[rows - 1][2]; // skip header row
for (int i = 1; i < rows; i++) {
Row row = sheet.getRow(i);
data[i - 1][0] = row.getCell(0).getStringCellValue(); // username
data[i - 1][1] = row.getCell(1).getStringCellValue(); // password
}
wb.close();
return data;
}
@Test(dataProvider = "loginData")
public void loginTest(String user, String pass) {
driver.findElement(By.id("username")).sendKeys(user);
driver.findElement(By.id("password")).sendKeys(pass);
driver.findElement(By.id("login")).click();
Assert.assertTrue(driver.findElement(By.id("welcome")).isDisplayed());
}
Say it aloud: "Apache POI reads the .xlsx rows into an Object[][], which a TestNG DataProvider streams into my login test so it runs once per data row."
Q17. Implement a retry logic for flaky tests that fail intermittently.¶
Approach: Implement TestNG IRetryAnalyzer to re-run failed tests up to a max count.
public class RetryAnalyzer implements IRetryAnalyzer {
private int count = 0;
private static final int MAX = 2;
@Override
public boolean retry(ITestResult result) {
if (count < MAX) {
count++;
return true; // re-run the test
}
return false;
}
}
// Apply per test: @Test(retryAnalyzer = RetryAnalyzer.class)
// Or globally via an IAnnotationTransformer listener.
Say it aloud: "TestNG's IRetryAnalyzer returns true to re-run a failed test up to my max count — I attach it via retryAnalyzer or globally with an IAnnotationTransformer so no flaky test needs a manual retry."
Q18. Write code to simulate keyboard actions like Ctrl+C, Ctrl+V, etc.¶
Approach: Use Actions with keyDown(Keys.CONTROL) ... keyUp around the letter key.
public void copyPaste(By source, By target) {
// Use a FRESH Actions per perform() — reusing one instance can re-fire
// the earlier queued (copy) sequence when you perform() again.
// Select all + copy from source
new Actions(driver)
.click(driver.findElement(source))
.keyDown(Keys.CONTROL).sendKeys("a").sendKeys("c").keyUp(Keys.CONTROL)
.perform();
// Paste into target
new Actions(driver)
.click(driver.findElement(target))
.keyDown(Keys.CONTROL).sendKeys("v").keyUp(Keys.CONTROL)
.perform();
}
Say it aloud: "I hold Keys.CONTROL with keyDown, send the letters a, c, then v, and release with keyUp — that reproduces Ctrl+A/C/V exactly as a user would."
Q19. Automate drag-and-drop where the target location is calculated dynamically.¶
Approach: Compute the pixel offset from source to a dynamic target, then moveByOffset.
public void dragToDynamicPoint(By source, int dynamicX, int dynamicY) {
WebElement src = driver.findElement(source);
Point srcLoc = src.getLocation();
int offsetX = dynamicX - (srcLoc.getX() + src.getSize().getWidth() / 2);
int offsetY = dynamicY - (srcLoc.getY() + src.getSize().getHeight() / 2);
new Actions(driver)
.clickAndHold(src)
.moveByOffset(offsetX, offsetY)
.release()
.build().perform();
}
Say it aloud: "When the drop target is computed at runtime, I derive the X/Y offset from the source's centre to that point and drive it with clickAndHold + moveByOffset + release."
Q20. Verify tooltip text on hover of a web element.¶
Approach: Hover to trigger the tooltip, then read the title attribute or the tooltip element's text.
public String getTooltip(By element, By tooltipLocator) {
WebElement el = driver.findElement(element);
new Actions(driver).moveToElement(el).perform();
// HTML title attribute tooltip:
String titleAttr = el.getAttribute("title");
if (titleAttr != null && !titleAttr.isEmpty()) return titleAttr;
// Custom JS tooltip element that appears on hover:
return new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.visibilityOfElementLocated(tooltipLocator))
.getText();
}
Say it aloud: "For a native tooltip I just read the title attribute; for a custom CSS/JS tooltip I hover with Actions, wait for the popup element, and read its text."
Q21. Handle file download and verify the file in the local system.¶
Approach: Configure the browser to auto-download to a known dir, trigger it, then poll the filesystem.
public static WebDriver driverWithDownloadDir(String dir) {
ChromeOptions options = new ChromeOptions();
Map<String, Object> prefs = new HashMap<>();
prefs.put("download.default_directory", dir);
prefs.put("download.prompt_for_download", false);
options.setExperimentalOption("prefs", prefs);
return new ChromeDriver(options);
}
public boolean waitForDownload(String dir, String fileName, int timeoutSec)
throws InterruptedException {
File f = new File(dir, fileName);
for (int i = 0; i < timeoutSec; i++) {
if (f.exists() && f.length() > 0) return true;
Thread.sleep(1000);
}
return false;
}
Say it aloud: "I point Chrome's download dir to a known folder with prefs and disable the prompt, click download, then poll the filesystem until the file exists with non-zero size."
Q22. Write code to interact with shadow DOM elements.¶
Approach: In Selenium 4, call getShadowRoot() on the host, then query inside the returned SearchContext.
public WebElement getShadowElement(By hostLocator, By insideShadow) {
WebElement host = driver.findElement(hostLocator);
SearchContext shadowRoot = host.getShadowRoot(); // Selenium 4
return shadowRoot.findElement(insideShadow); // CSS selectors only inside shadow root
}
// Usage
getShadowElement(By.cssSelector("my-widget"),
By.cssSelector("input#field")).sendKeys("hello");
Say it aloud: "Selenium 4 exposes getShadowRoot() on the shadow host, returning a SearchContext I query with CSS selectors — no more JS shadowRoot hacks for open shadow DOM."
Q23. Handle AJAX-based loading elements and verify content changes.¶
Approach: Wait for the spinner to disappear and for the new content/text to appear.
public void waitForAjaxContent(By spinner, By content, String expectedText) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(15));
// 1. wait for loader to vanish
wait.until(ExpectedConditions.invisibilityOfElementLocated(spinner));
// 2. wait for the updated content/text
wait.until(ExpectedConditions.textToBePresentInElementLocated(content, expectedText));
// Optional: wait for jQuery AJAX calls to finish
wait.until(d -> (Boolean) ((JavascriptExecutor) d)
.executeScript("return window.jQuery != undefined && jQuery.active === 0"));
}
Say it aloud: "I wait for the loading spinner to become invisible, then for the expected text to appear, and optionally poll jQuery.active === 0 to confirm all AJAX calls have settled."
Q24. Automate login tests with CAPTCHA handling (explain approach).¶
Approach: You should NOT solve real CAPTCHAs in automation — CAPTCHA exists precisely to block bots. Handle it in the test environment instead.
// Honest guidance — no code truly "solves" a production CAPTCHA reliably/ethically.
// Preferred, real-world options in order:
// 1. DISABLE CAPTCHA in the test/staging environment (feature flag / config).
// 2. Use a TEST BYPASS TOKEN — e.g. Google reCAPTCHA test keys that always pass,
// or a backend-provided bypass header/secret for the QA env.
// 3. Whitelist the automation IP so CAPTCHA is not challenged.
// 4. (Research only, not for production suites) 3rd-party solvers such as
// 2Captcha / Anti-Captcha or OCR — slow, paid, brittle, and often against ToS.
public void loginWithCaptchaBypass(String user, String pass, String bypassToken) {
driver.findElement(By.id("username")).sendKeys(user);
driver.findElement(By.id("password")).sendKeys(pass);
// Inject the env-provided bypass token instead of solving the challenge
((JavascriptExecutor) driver).executeScript(
"document.getElementById('g-recaptcha-response').value = arguments[0];",
bypassToken);
driver.findElement(By.id("login")).click();
}
Say it aloud: "I'm honest that you cannot and should not automate real CAPTCHA. The right answer is to disable it in the test env, use reCAPTCHA test keys or a bypass token, or IP-whitelist the runner; 3rd-party solvers are research-only and often violate terms of service."
Q25. Write a script to perform right-click and select context menu options.¶
Approach: Actions.contextClick to open the menu, then click the desired option.
public void rightClickAndSelect(By element, By menuOption) {
WebElement el = driver.findElement(element);
new Actions(driver).contextClick(el).perform();
new WebDriverWait(driver, Duration.ofSeconds(10))
.until(ExpectedConditions.elementToBeClickable(menuOption))
.click();
}
Say it aloud: "contextClick fires a right-click to open the context menu, then I wait for the option to be clickable and select it."
Q26. Automate tests for responsive design by changing browser size.¶
Approach: Drive window.setSize across breakpoints and assert layout differences (e.g., hamburger menu appears on mobile).
public void checkResponsive() {
Dimension[] viewports = {
new Dimension(375, 812), // mobile
new Dimension(768, 1024), // tablet
new Dimension(1440, 900) // desktop
};
for (Dimension d : viewports) {
driver.manage().window().setSize(d);
boolean hamburgerShown = !driver.findElements(By.id("hamburger")).isEmpty()
&& driver.findElement(By.id("hamburger")).isDisplayed();
if (d.getWidth() < 768) {
Assert.assertTrue(hamburgerShown, "Hamburger should show on mobile");
} else {
Assert.assertFalse(hamburgerShown, "Full nav should show on desktop");
}
}
}
Say it aloud: "I resize the window across mobile/tablet/desktop breakpoints with setSize and assert the layout reacts — for example the hamburger menu appears only below the mobile breakpoint."
Q27. Write a custom wait method for element visibility without using WebDriverWait.¶
Approach: Poll manually in a loop with a deadline, catching NoSuchElement/visibility until timeout.
public WebElement customWaitVisible(By locator, int timeoutSec, int pollMillis)
throws InterruptedException {
long deadline = System.currentTimeMillis() + timeoutSec * 1000L;
while (System.currentTimeMillis() < deadline) {
try {
WebElement el = driver.findElement(locator);
if (el.isDisplayed()) return el;
} catch (NoSuchElementException | StaleElementReferenceException ignored) {
// not ready yet
}
Thread.sleep(pollMillis);
}
throw new TimeoutException("Element not visible within " + timeoutSec + "s: " + locator);
}
Say it aloud: "Without WebDriverWait I roll my own fluent wait — loop until a deadline, swallow NoSuchElement/StaleElement, sleep between polls, and throw TimeoutException if it never becomes visible."
Q28. Implement a method to handle stale element exceptions dynamically.¶
Approach: Re-locate and retry the action on StaleElementReferenceException instead of caching the WebElement.
public void clickWithStaleRetry(By locator, int maxAttempts) {
for (int attempt = 0; attempt < maxAttempts; attempt++) {
try {
driver.findElement(locator).click(); // re-find every attempt
return;
} catch (StaleElementReferenceException e) {
if (attempt == maxAttempts - 1) throw e;
// small backoff before re-locating
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
}
}
}
Say it aloud: "Stale means the DOM re-rendered, so I never reuse a cached element — I re-find it inside a retry loop and only rethrow after the final attempt fails."
Q29. Automate interaction with web components built using React or Angular.¶
Approach: Treat SPA re-renders as async — always re-locate, wait for state (not sleeps), and use robust selectors like data-testid.
public void interactWithSpa(String testId, String text) {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
By locator = By.cssSelector("[data-testid='" + testId + "']");
// Wait for the component to mount and be clickable (React/Angular re-render often)
WebElement el = wait.until(ExpectedConditions.elementToBeClickable(locator));
el.clear();
el.sendKeys(text);
// Wait for state-driven UI change instead of Thread.sleep
wait.until(ExpectedConditions.attributeToBe(locator, "value", text));
}
Say it aloud: "SPAs constantly re-render, so I anchor on data-testid, always re-locate through explicit waits, and assert on state changes like the value attribute rather than sleeping for the framework."
Q30. Create a data-driven test framework that reads input from JSON files.¶
Approach: Deserialize JSON with Jackson into POJOs and feed them through a TestNG DataProvider.
// login.json: [ {"username":"u1","password":"p1"}, {"username":"u2","password":"p2"} ]
public static class LoginData {
public String username;
public String password;
}
@DataProvider(name = "jsonLogins")
public Object[][] jsonLogins() throws IOException {
ObjectMapper mapper = new ObjectMapper();
LoginData[] rows = mapper.readValue(
new File("testdata/login.json"), LoginData[].class);
Object[][] data = new Object[rows.length][1];
for (int i = 0; i < rows.length; i++) {
data[i][0] = rows[i];
}
return data;
}
@Test(dataProvider = "jsonLogins")
public void loginFromJson(LoginData d) {
driver.findElement(By.id("username")).sendKeys(d.username);
driver.findElement(By.id("password")).sendKeys(d.password);
driver.findElement(By.id("login")).click();
Assert.assertTrue(driver.findElement(By.id("welcome")).isDisplayed());
}
Say it aloud: "Jackson's ObjectMapper deserializes the JSON array straight into POJOs, which I wrap into an Object[][] for a TestNG DataProvider so the test runs once per JSON record."