AgentReporting.java

package org.vaadin.addons.dramafinder.agent;

import java.lang.reflect.Field;
import java.util.Optional;

import com.microsoft.playwright.Page;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

/**
 * JUnit 5 extension that, on test failure, writes an agent-oriented report to
 * {@code target/agent-report/} (Maven) or {@code build/agent-report/} (Gradle):
 * a {@code failure.txt} with the assertion message and a trimmed stack trace, a
 * screenshot of the page at failure time, and a semantic
 * {@link ComponentSnapshot component snapshot}.
 * <p>
 * The capture runs in {@link AfterTestExecutionCallback}, i.e. <em>before</em>
 * {@code @AfterEach} closes the page, so the screenshot and snapshot reflect the
 * exact failing state.
 * <p>
 * Register it either by extending {@link VisualVerificationTest} (which already carries
 * {@code @ExtendWith(AgentReporting.class)} and implements
 * {@link AgentReportProvider}) or by annotating any Playwright test with
 * {@code @ExtendWith(AgentReporting.class)}. In the latter case the extension
 * locates the page/report by implementing {@link AgentReportProvider}, or, as a
 * fallback, by reflecting an {@link AgentReport} or {@link Page} field off the
 * test instance.
 */
public class AgentReporting implements AfterTestExecutionCallback {

    @Override
    public void afterTestExecution(ExtensionContext context) {
        Optional<Throwable> failure = context.getExecutionException();
        if (failure.isEmpty()) {
            return;
        }
        Object test = context.getTestInstance().orElse(null);
        if (test == null) {
            return;
        }
        locateReport(test).ifPresent(report -> report.captureFailure(failure.get()));
    }

    private Optional<AgentReport> locateReport(Object test) {
        if (test instanceof AgentReportProvider provider) {
            AgentReport report = provider.agentReport();
            if (report != null) {
                return Optional.of(report);
            }
            Page page = provider.agentPage();
            if (page != null) {
                return Optional.of(new AgentReport(page));
            }
        }
        return reflectReport(test);
    }

    // Fallback for @ExtendWith users who don't implement AgentReportProvider:
    // find an AgentReport field, or build one from a Page field.
    private Optional<AgentReport> reflectReport(Object test) {
        AgentReport report = firstFieldValue(test, AgentReport.class);
        if (report != null) {
            return Optional.of(report);
        }
        Page page = firstFieldValue(test, Page.class);
        if (page != null) {
            return Optional.of(new AgentReport(page));
        }
        return Optional.empty();
    }

    @SuppressWarnings("unchecked")
    private <T> T firstFieldValue(Object test, Class<T> type) {
        for (Class<?> c = test.getClass(); c != null && c != Object.class; c = c.getSuperclass()) {
            for (Field field : c.getDeclaredFields()) {
                if (type.isAssignableFrom(field.getType())) {
                    try {
                        field.setAccessible(true);
                        Object value = field.get(test);
                        if (value != null) {
                            return (T) value;
                        }
                    } catch (ReflectiveOperationException | RuntimeException ignore) {
                        // Skip inaccessible fields.
                    }
                }
            }
        }
        return null;
    }
}