Skip to content

Instantly share code, notes, and snippets.

@nipafx
Last active August 21, 2018 14:00

Revisions

  1. Nicolai Parlog renamed this gist Aug 21, 2018. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  2. Nicolai Parlog created this gist Aug 9, 2018.
    11 changes: 11 additions & 0 deletions GridDemoTest.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,11 @@
    @SeleniumGridConfiguration(url = "http://0.0.0.0:4444")
    class GridDemoTest {

    @SeleniumGridTest
    fun demo(driver: WebDriver) {
    driver.get("https://blog.codefx.org")
    val homeUrl = driver.findElement(By.className("img-hyperlink")).getAttribute("href")
    assertThat(homeUrl).contains("blog.codefx.org")
    }

    }
    90 changes: 90 additions & 0 deletions SeleniumGrid.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,90 @@
    class SeleniumGrid {

    // a line looks like this:
    // <p>capabilities: Capabilities {applicationName: , browserName: chrome, [...] }</p>
    private static final Pattern CAPABILITIES_LINE = Pattern.compile("<p>[^{]*\\{(.*?)}</p>");

    private static final ConcurrentMap<String, ResolvedGrid> GRIDS = new ConcurrentHashMap<>();

    public static Stream<SeleniumNode> nodes(String url, TestReporter reporter) {
    return GRIDS
    .computeIfAbsent(url, u -> resolve(u, reporter))
    .nodes().stream();
    }

    private static ResolvedGrid resolve(String url, TestReporter reporter) {
    try {
    URL hubUrl = new URL(url + "/wd/hub");
    String consoleUrl = url + "/grid/console";
    List<SeleniumNode> nodes = parseCapabilities(consoleUrl)
    .peek(capabilities -> reporter.publishEntry("node", capabilities.toString()))
    .map(capabilities -> new SeleniumNode(hubUrl, capabilities))
    .collect(toList());
    return ResolvedGrid.resolvedToNodes(nodes);
    } catch (Exception ex) {
    return ResolvedGrid.resolvedToError(ex);
    }
    }

    private static Stream<Capabilities> parseCapabilities(String consoleUrl) throws IOException {
    Document doc = Jsoup.connect(consoleUrl).get();
    Elements leftColumn = doc.select("#left-column > div > div.content > div[type=\"config\"]");
    Elements rightColumn = doc.select("#right-column > div > div.content > div[type=\"config\"]");
    return concat(leftColumn.stream(), rightColumn.stream())
    .map(SeleniumGrid::parseCapabilities);
    }

    private static Capabilities parseCapabilities(Element element) {
    MutableCapabilities capabilities = new MutableCapabilities();

    String capabilitiesLine = element.select("p").stream()
    .filter(p -> p.toString().contains("capabilities"))
    .findFirst()
    .orElseThrow(() -> new IllegalStateException(
    "Selenium node has no capabilities\n" + element.toString()))
    .toString();

    // e.g. "<p>capabilities: Capabilities {applicationName: , browserName: chrome, [...] }</p>"
    Matcher matcher = CAPABILITIES_LINE.matcher(capabilitiesLine);
    if (!matcher.find())
    throw new IllegalStateException("Selenium node has malformed capabilities: \"" + capabilitiesLine + "\"");
    // e.g. "capabilities: Capabilities {applicationName: , browserName: chrome, [...]"
    stream(matcher.group(1).split(","))
    .map(String::trim)
    // e.g. "applicationName: " or "browserName: chrome"
    .map(pair -> pair.split(":"))
    // some pairs have a key, but no value - remove them
    .filter(pair -> pair.length == 2)
    .forEach(pair -> capabilities.setCapability(pair[0].trim(), pair[1].trim()));

    return capabilities;
    }

    private static class ResolvedGrid {

    private final Optional<Exception> error;
    private final List<SeleniumNode> nodes;

    private ResolvedGrid(
    Optional<Exception> error, List<SeleniumNode> nodes) {
    this.error = requireNonNull(error);
    this.nodes = requireNonNull(nodes);
    }

    static ResolvedGrid resolvedToNodes(List<SeleniumNode> nodes) {
    return new ResolvedGrid(empty(), nodes);
    }

    static ResolvedGrid resolvedToError(Exception error) {
    return new ResolvedGrid(of(error), emptyList());
    }

    List<SeleniumNode> nodes() throws IllegalArgumentException {
    if (error.isPresent())
    throw new IllegalArgumentException("Grid resolution failed", error.get());
    return nodes;
    }

    }

    }
    8 changes: 8 additions & 0 deletions SeleniumGridConfiguration.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,8 @@
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
    @ExtendWith(SeleniumGridExtension.class)
    public @interface SeleniumGridConfiguration {

    String url();

    }
    105 changes: 105 additions & 0 deletions SeleniumGridExtension.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,105 @@
    public class SeleniumGridExtension implements TestTemplateInvocationContextProvider {

    public static final String SELENIUM_GRID_URL = "selenium.grid.url";

    @Override
    public boolean supportsTestTemplate(ExtensionContext context) {
    return stream(context.getRequiredTestMethod().getParameters())
    .anyMatch(SeleniumGridExtension::isWebDriver);
    }

    @Override
    public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
    String url = SeleniumGridUrlFinder.find(context);
    return SeleniumGrid
    .nodes(url, context::publishReportEntry)
    .map(SeleniumGridInvocationContext::new);
    }

    private static boolean isWebDriver(Parameter parameter) {
    return WebDriver.class.isAssignableFrom(parameter.getType());
    }

    private static class SeleniumGridUrlFinder {

    private final ExtensionContext context;

    private SeleniumGridUrlFinder(ExtensionContext context) {
    this.context = requireNonNull(context);
    }

    public static String find(ExtensionContext context) {
    return new SeleniumGridUrlFinder(context).find();
    }

    private String find() {
    return Stream.of(findOnParameter(), findOnMethod(), findOnClass(), findInConfiguration())
    .flatMap(optional -> optional.map(Stream::of).orElseGet(Stream::empty))
    .findFirst()
    .orElseThrow(() -> new IllegalStateException("No Selenium grid URL defined"));
    }

    private Optional<String> findOnParameter() {
    return stream(context.getRequiredTestMethod().getParameters())
    .filter(SeleniumGridExtension::isWebDriver)
    .flatMap(this::findOnElement)
    .findFirst();
    }

    private Optional<String> findOnMethod() {
    return findOnElement(context.getRequiredTestMethod()).findFirst();
    }

    private Optional<String> findOnClass() {
    return findOnElement(context.getRequiredTestClass()).findFirst();
    }

    private Optional<String> findInConfiguration() {
    return context.getConfigurationParameter(SELENIUM_GRID_URL);
    }

    private Stream<String> findOnElement(AnnotatedElement element) {
    return AnnotationSupport
    .findAnnotation(element, SeleniumGridConfiguration.class)
    .map(SeleniumGridConfiguration::url)
    .map(Stream::of)
    .orElse(Stream.empty());
    }

    }

    private static class SeleniumGridInvocationContext implements TestTemplateInvocationContext, ParameterResolver {

    private final SeleniumNode node;

    SeleniumGridInvocationContext(SeleniumNode node) {
    this.node = requireNonNull(node);
    }

    @Override
    public String getDisplayName(int invocationIndex) {
    return node.name();
    }

    @Override
    public List<Extension> getAdditionalExtensions() {
    return singletonList(this);
    }

    @Override
    public boolean supportsParameter(
    ParameterContext parameterContext, ExtensionContext extensionContext)
    throws ParameterResolutionException {
    return isWebDriver(parameterContext.getParameter());
    }

    @Override
    public Object resolveParameter(
    ParameterContext parameterContext, ExtensionContext extensionContext)
    throws ParameterResolutionException {
    return node.connect();
    }

    }

    }
    5 changes: 5 additions & 0 deletions SeleniumGridTest.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    @TestTemplate
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
    @ExtendWith(SeleniumGridExtension.class)
    public @interface SeleniumGridTest { }
    19 changes: 19 additions & 0 deletions SeleniumNode.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    class SeleniumNode {

    private final URL gridUrl;
    private final Capabilities capabilities;

    public SeleniumNode(URL gridUrl, Capabilities capabilities) {
    this.gridUrl = requireNonNull(gridUrl);
    this.capabilities = requireNonNull(capabilities);
    }

    public String name() {
    return capabilities.getBrowserName() + " : " + capabilities.getVersion();
    }

    public RemoteWebDriver connect() {
    return new RemoteWebDriver(gridUrl, capabilities);
    }

    }