From 2b876ce52b5f1c173aadff97092e9099329c01d8 Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Thu, 29 Oct 2026 08:19:52 +0530 Subject: [PATCH] fix(precondition): restore cached session on the captured origin Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 + .../precondition/SessionCache.java | 26 ++++++- .../precondition/SessionCacheLiveTest.java | 74 +++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 src/test/java/com/seleniumboot/precondition/SessionCacheLiveTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 73b0ffe..f38cec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ### Unreleased +- **Fix: `@PreCondition` session restore on a fresh browser** — the cached cookies and localStorage were silently dropped when the next test started on `about:blank`, so only the first test per thread was logged in. Restore now navigates to the origin the session was captured on first. + - **Trace shows browser console errors** — when `browser.captureConsoleErrors: true`, the JS errors collected for a test now appear in its trace file under "Console Errors", next to the steps and failure details, so a shared trace includes what the browser logged. No change when the option is off. (#130) - **Network mocking on Firefox via WebDriver BiDi** — `networkMock().stub(...)` now uses WebDriver BiDi network interception under the hood when supported by the driver (including Firefox and BiDi-enabled drivers), falling back to Chrome DevTools Protocol (CDP) for Chromium-based drivers. The existing stubbing API (`returnJson`, `returnBody`, `returnStatus`, `delay`, `clear`) works identically across Chrome and Firefox. (#196) diff --git a/src/main/java/com/seleniumboot/precondition/SessionCache.java b/src/main/java/com/seleniumboot/precondition/SessionCache.java index 48daac4..3778fba 100644 --- a/src/main/java/com/seleniumboot/precondition/SessionCache.java +++ b/src/main/java/com/seleniumboot/precondition/SessionCache.java @@ -20,10 +20,12 @@ final class SessionCache { static final class SavedSession { final Set cookies; final Map localStorage; + final String origin; // scheme://host[:port] the session was captured on - SavedSession(Set cookies, Map localStorage) { + SavedSession(Set cookies, Map localStorage, String origin) { this.cookies = cookies; this.localStorage = localStorage; + this.origin = origin; } boolean isEmpty() { @@ -40,17 +42,23 @@ private SessionCache() {} static void store(String conditionName, WebDriver driver) { Set cookies = driver.manage().getCookies(); Map localStorage = captureLocalStorage(driver); - cache.get().put(conditionName, new SavedSession(cookies, localStorage)); + cache.get().put(conditionName, new SavedSession(cookies, localStorage, originOf(driver.getCurrentUrl()))); } /** * Restores cookies and localStorage from cache into the current browser session. - * Navigates to the current URL to apply cookies before restoring localStorage. + * A browser can only accept cookies and localStorage for the origin it is on, and a fresh + * session starts on {@code about:blank}, so it first navigates to the origin the session + * was captured on. */ static void restore(String conditionName, WebDriver driver) { SavedSession session = cache.get().get(conditionName); if (session == null) return; + if (session.origin != null && !session.origin.equals(originOf(driver.getCurrentUrl()))) { + driver.get(session.origin); + } + driver.manage().deleteAllCookies(); for (Cookie cookie : session.cookies) { try { driver.manage().addCookie(cookie); } catch (Exception ignored) {} @@ -105,6 +113,18 @@ private static void restoreLocalStorage(WebDriver driver, Map it } catch (Exception ignored) {} } + /** Returns scheme://host[:port] of an http(s) URL, or null for anything else (about:blank, data:). */ + private static String originOf(String url) { + try { + java.net.URI u = java.net.URI.create(url); + if (u.getScheme() == null || u.getHost() == null + || !(u.getScheme().equals("http") || u.getScheme().equals("https"))) return null; + return u.getScheme() + "://" + u.getHost() + (u.getPort() == -1 ? "" : ":" + u.getPort()); + } catch (Exception e) { + return null; + } + } + private static String jsString(String value) { return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"; } diff --git a/src/test/java/com/seleniumboot/precondition/SessionCacheLiveTest.java b/src/test/java/com/seleniumboot/precondition/SessionCacheLiveTest.java new file mode 100644 index 0000000..b585d17 --- /dev/null +++ b/src/test/java/com/seleniumboot/precondition/SessionCacheLiveTest.java @@ -0,0 +1,74 @@ +package com.seleniumboot.precondition; + +import com.sun.net.httpserver.HttpServer; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.testng.SkipException; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; + +import static org.testng.Assert.assertNotNull; + +/** + * Live check that a cached session is restored into a browser that is not on the app origin + * (a fresh driver starts on about:blank, where {@code addCookie} is rejected). + * Skips when Chrome is not available. + */ +public class SessionCacheLiveTest { + + private HttpServer server; + private String base; + + @BeforeClass + public void startServer() throws Exception { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", ex -> { + byte[] body = "ok".getBytes(StandardCharsets.UTF_8); + ex.getResponseHeaders().set("Content-Type", "text/html"); + if (ex.getRequestURI().getPath().equals("/login")) { + ex.getResponseHeaders().add("Set-Cookie", "sso=abc123; Path=/"); + } + ex.sendResponseHeaders(200, body.length); + ex.getResponseBody().write(body); + ex.close(); + }); + server.start(); + base = "http://127.0.0.1:" + server.getAddress().getPort(); + } + + @AfterClass(alwaysRun = true) + public void stopServer() { + if (server != null) server.stop(0); + } + + @Test + public void restore_fromAboutBlank_restoresCookies() { + WebDriver driver; + try { + ChromeOptions o = new ChromeOptions(); + o.addArguments("--headless=new", "--no-sandbox"); + driver = new ChromeDriver(o); + } catch (Throwable t) { + throw new SkipException("Chrome not available: " + t.getMessage()); + } + try { + driver.get(base + "/login"); + SessionCache.store("sso", driver); + + driver.manage().deleteAllCookies(); + driver.get("about:blank"); // where a fresh session sits before the test + SessionCache.restore("sso", driver); + + driver.get(base + "/"); + assertNotNull(driver.manage().getCookieNamed("sso"), + "cookie cached by the provider should be restored for the next test"); + } finally { + driver.quit(); + } + } +}