Skip to content

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."

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."