diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index aeaa9e459..95443a122 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -10,7 +10,4 @@
-
-
-
-
+
\ No newline at end of file
diff --git a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java
index 6a00c5c4b..75fa00300 100644
--- a/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java
+++ b/allure-java-commons-test/src/main/java/io/qameta/allure/test/AllureTestCommonsUtils.java
@@ -41,6 +41,11 @@
*/
public final class AllureTestCommonsUtils {
+ private static final String DOT = ".";
+ private static final String JSON_EXTENSION = "json";
+ private static final String JSON_TYPE = "application/json";
+ private static final String TEXT_EXTENSION = "txt";
+ private static final String TEXT_TYPE = "text/plain";
private static final ObjectWriter WRITER = JsonMapper
.builder()
.configure(USE_WRAPPER_NAME_AS_PROPERTY_NAME, true)
@@ -65,7 +70,9 @@ public static void attach(final AllureResults allureResults) {
try {
Allure.addAttachment(
testResult.getUuid() + AllureConstants.TEST_RESULT_FILE_SUFFIX,
- WRITER.writeValueAsString(testResult)
+ JSON_TYPE,
+ WRITER.writeValueAsString(testResult),
+ JSON_EXTENSION
);
} catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
@@ -76,7 +83,9 @@ public static void attach(final AllureResults allureResults) {
try {
Allure.addAttachment(
container.getUuid() + AllureConstants.TEST_RESULT_CONTAINER_FILE_SUFFIX,
- WRITER.writeValueAsString(container)
+ JSON_TYPE,
+ WRITER.writeValueAsString(container),
+ JSON_EXTENSION
);
} catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
@@ -86,11 +95,31 @@ public static void attach(final AllureResults allureResults) {
allureResults.getAttachments().forEach((fileName, body) -> Allure
.addAttachment(
fileName,
- new ByteArrayInputStream(body)
+ type(fileName),
+ new ByteArrayInputStream(body),
+ extension(fileName)
)
);
}
+ private static String type(final String fileName) {
+ if (fileName.endsWith(DOT + JSON_EXTENSION)) {
+ return JSON_TYPE;
+ }
+ if (fileName.endsWith(DOT + TEXT_EXTENSION)) {
+ return TEXT_TYPE;
+ }
+ return null;
+ }
+
+ private static String extension(final String fileName) {
+ final int index = fileName.lastIndexOf('.');
+ if (index < 0 || index == fileName.length() - 1) {
+ return null;
+ }
+ return fileName.substring(index + 1);
+ }
+
/**
* Parameter mode serializer.
*/
diff --git a/allure-selenium-bidi/build.gradle.kts b/allure-selenium-bidi/build.gradle.kts
new file mode 100644
index 000000000..392d791ef
--- /dev/null
+++ b/allure-selenium-bidi/build.gradle.kts
@@ -0,0 +1,47 @@
+description = "Allure Selenium WebDriver BiDi Integration"
+
+val agent: Configuration by configurations.creating
+
+val seleniumVersion = "4.23.0"
+val testcontainersVersion = "1.21.4"
+
+dependencies {
+ agent("org.aspectj:aspectjweaver")
+ api(project(":allure-java-commons"))
+ compileOnly("org.seleniumhq.selenium:selenium-java:$seleniumVersion")
+ testAnnotationProcessor(project(":allure-descriptions-javadoc"))
+ testImplementation("org.seleniumhq.selenium:selenium-java:$seleniumVersion")
+ testImplementation("org.assertj:assertj-core")
+ testImplementation("org.junit.jupiter:junit-jupiter-api")
+ testImplementation("org.slf4j:slf4j-simple")
+ testImplementation("org.testcontainers:junit-jupiter:$testcontainersVersion")
+ testImplementation("org.testcontainers:testcontainers:$testcontainersVersion")
+ testImplementation(project(":allure-assertj"))
+ testImplementation(project(":allure-java-commons-test"))
+ testImplementation(project(":allure-junit-platform"))
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+}
+
+tasks {
+ compileJava {
+ options.release.set(17)
+ }
+ compileTestJava {
+ options.release.set(17)
+ }
+ jar {
+ manifest {
+ attributes(
+ mapOf(
+ "Automatic-Module-Name" to "io.qameta.allure.seleniumbidi"
+ )
+ )
+ }
+ }
+ test {
+ useJUnitPlatform()
+ jvmArgs("-javaagent:${agent.singleFile}")
+ systemProperty("allure.model.indentOutput", "true")
+ systemProperty("org.slf4j.simpleLogger.defaultLogLevel", "warn")
+ }
+}
diff --git a/allure-selenium-bidi/src/main/java/io/qameta/allure/seleniumbidi/AllureWebDriverBiDi.java b/allure-selenium-bidi/src/main/java/io/qameta/allure/seleniumbidi/AllureWebDriverBiDi.java
new file mode 100644
index 000000000..264d44862
--- /dev/null
+++ b/allure-selenium-bidi/src/main/java/io/qameta/allure/seleniumbidi/AllureWebDriverBiDi.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2016-2026 Qameta Software Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.qameta.allure.seleniumbidi;
+
+import io.qameta.allure.Allure;
+import io.qameta.allure.AllureLifecycle;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.events.EventFiringDecorator;
+import org.openqa.selenium.support.events.WebDriverListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Selenium WebDriver BiDi listener that captures browser log and network events
+ * as aggregated Allure attachments.
+ */
+@SuppressWarnings("unused")
+public class AllureWebDriverBiDi implements WebDriverListener, AutoCloseable {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AllureWebDriverBiDi.class);
+
+ private final AllureLifecycle lifecycle;
+ private final BiDiSessionFactory sessionFactory;
+ private final BiDiConfiguration configuration = new BiDiConfiguration();
+ private final Map sessions = new IdentityHashMap<>();
+ private final Lock sessionsLock = new ReentrantLock();
+
+ public AllureWebDriverBiDi() {
+ this(Allure.getLifecycle(), new SeleniumBiDiSessionFactory());
+ }
+
+ AllureWebDriverBiDi(final AllureLifecycle lifecycle,
+ final BiDiSessionFactory sessionFactory) {
+ this.lifecycle = lifecycle;
+ this.sessionFactory = sessionFactory;
+ }
+
+ public T decorate(final T driver) {
+ return new EventFiringDecorator(this).decorate(driver);
+ }
+
+ public AllureWebDriverBiDi logs(final boolean enabled) {
+ configuration.setLogsEnabled(enabled);
+ return this;
+ }
+
+ public AllureWebDriverBiDi network(final boolean enabled) {
+ configuration.setNetworkEnabled(enabled);
+ return this;
+ }
+
+ public AllureWebDriverBiDi maxLogEntries(final int maxLogEntries) {
+ configuration.setMaxLogEntries(maxLogEntries);
+ return this;
+ }
+
+ public AllureWebDriverBiDi maxNetworkEvents(final int maxNetworkEvents) {
+ configuration.setMaxNetworkEvents(maxNetworkEvents);
+ return this;
+ }
+
+ public AllureWebDriverBiDi redactHeaders(final String... headerNames) {
+ configuration.redactHeaders(headerNames);
+ return this;
+ }
+
+ @Override
+ public void beforeAnyWebDriverCall(final WebDriver driver,
+ final Method method,
+ final Object[] args) {
+ captureActiveAllureContext(driver);
+ }
+
+ @Override
+ public void afterAnyWebDriverCall(final WebDriver driver,
+ final Method method,
+ final Object[] args,
+ final Object result) {
+ captureActiveAllureContext(driver);
+ }
+
+ @Override
+ public void beforeQuit(final WebDriver driver) {
+ closeSession(driver);
+ }
+
+ @Override
+ public void afterQuit(final WebDriver driver) {
+ closeSession(driver);
+ }
+
+ @Override
+ public void close() {
+ final List activeSessions = new ArrayList<>();
+ sessionsLock.lock();
+ try {
+ activeSessions.addAll(sessions.values());
+ sessions.clear();
+ } finally {
+ sessionsLock.unlock();
+ }
+ activeSessions.forEach(this::safeFlushAndClose);
+ }
+
+ private void captureActiveAllureContext(final WebDriver driver) {
+ if (!configuration.isAnyEnabled()) {
+ return;
+ }
+
+ if (!lifecycle.getCurrentTestCaseOrStep().isPresent()) {
+ return;
+ }
+
+ getOrCreateSession(driver);
+ }
+
+ private BiDiSessionState getOrCreateSession(final WebDriver driver) {
+ sessionsLock.lock();
+ try {
+ final BiDiSessionState existing = sessions.get(driver);
+ if (existing != null) {
+ return existing;
+ }
+
+ try {
+ final BiDiSessionState created = BiDiSessionState.start(driver, configuration, sessionFactory);
+ if (created != null) {
+ sessions.put(driver, created);
+ }
+ return created;
+ } catch (RuntimeException e) {
+ LOGGER.debug("Could not start WebDriver BiDi capture", e);
+ return null;
+ }
+ } finally {
+ sessionsLock.unlock();
+ }
+ }
+
+ private void closeSession(final WebDriver driver) {
+ final BiDiSessionState state;
+ sessionsLock.lock();
+ try {
+ state = sessions.remove(driver);
+ } finally {
+ sessionsLock.unlock();
+ }
+ safeFlushAndClose(state);
+ }
+
+ private void safeFlushAndClose(final BiDiSessionState state) {
+ if (state == null) {
+ return;
+ }
+ try {
+ state.flushAndClose(lifecycle);
+ } catch (RuntimeException e) {
+ LOGGER.debug("Could not flush WebDriver BiDi capture", e);
+ }
+ }
+}
diff --git a/allure-selenium-bidi/src/main/java/io/qameta/allure/seleniumbidi/BiDiAttachmentStorage.java b/allure-selenium-bidi/src/main/java/io/qameta/allure/seleniumbidi/BiDiAttachmentStorage.java
new file mode 100644
index 000000000..251bb2989
--- /dev/null
+++ b/allure-selenium-bidi/src/main/java/io/qameta/allure/seleniumbidi/BiDiAttachmentStorage.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2016-2026 Qameta Software Inc
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.qameta.allure.seleniumbidi;
+
+import io.qameta.allure.AllureLifecycle;
+import org.openqa.selenium.json.Json;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+final class BiDiAttachmentStorage {
+
+ static final String LOG_ATTACHMENT_NAME = "WebDriver BiDi logs";
+ static final String NETWORK_ATTACHMENT_NAME = "WebDriver BiDi network";
+
+ private static final String JSON_TYPE = "application/json";
+ private static final String JSON_EXTENSION = "json";
+ private static final Json JSON = new Json();
+
+ private final List