Add page supplier, including support for multi-typed suppliers (#31313)

Closes #30611

Closes #30097

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-07-16 12:36:51 +02:00 committed by GitHub
parent 9b39498085
commit 1fb5db0fcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 229 additions and 46 deletions

View file

@ -0,0 +1,35 @@
package org.keycloak.test.base;
import org.junit.jupiter.api.Test;
import org.keycloak.test.framework.KeycloakIntegrationTest;
import org.keycloak.test.framework.page.LoginPage;
import org.keycloak.test.framework.page.TestPage;
import org.keycloak.test.framework.page.WelcomePage;
@KeycloakIntegrationTest
public class PagesTest {
@TestPage
WelcomePage welcomePage;
@TestPage
LoginPage loginPage;
@Test
public void testLoginFromWelcome() {
welcomePage.navigateTo();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
loginPage.fillLogin("admin", "admin");
loginPage.submit();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -5,6 +5,7 @@ import org.keycloak.test.framework.TestAdminClient;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.server.KeycloakTestServer;
@ -33,7 +34,7 @@ public class KeycloakAdminClientSupplier implements Supplier<Keycloak, TestAdmin
}
@Override
public boolean compatible(InstanceWrapper<Keycloak, TestAdminClient> a, InstanceWrapper<Keycloak, TestAdminClient> b) {
public boolean compatible(InstanceWrapper<Keycloak, TestAdminClient> a, RequestedInstance<Keycloak, TestAdminClient> b) {
return true;
}

View file

@ -4,6 +4,7 @@ import org.keycloak.test.framework.KeycloakTestDatabase;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
public abstract class AbstractDatabaseSupplier implements Supplier<TestDatabase, KeycloakTestDatabase> {
@ -26,7 +27,7 @@ public abstract class AbstractDatabaseSupplier implements Supplier<TestDatabase,
}
@Override
public boolean compatible(InstanceWrapper<TestDatabase, KeycloakTestDatabase> a, InstanceWrapper<TestDatabase, KeycloakTestDatabase> b) {
public boolean compatible(InstanceWrapper<TestDatabase, KeycloakTestDatabase> a, RequestedInstance<TestDatabase, KeycloakTestDatabase> b) {
return true;
}

View file

@ -23,7 +23,7 @@ public class Registry {
private ExtensionContext currentContext;
private final List<Supplier<?, ?>> suppliers = new LinkedList<>();
private final List<InstanceWrapper<?, ?>> deployedInstances = new LinkedList<>();
private final List<InstanceWrapper<?, ?>> requestedInstances = new LinkedList<>();
private final List<RequestedInstance<?, ?>> requestedInstances = new LinkedList<>();
public Registry() {
loadSuppliers();
@ -52,9 +52,9 @@ public class Registry {
return (T) dependency.getValue();
}
dependency = getRequestedInstance(typeClass);
if (dependency != null) {
dependency = dependency.getSupplier().getValue(this, dependency.getAnnotation());
RequestedInstance requestedDependency = getRequestedInstance(typeClass);
if (requestedDependency != null) {
dependency = requestedDependency.getSupplier().getValue(this, requestedDependency.getAnnotation(), typeClass);
dependency.registerDependency(dependent);
deployedInstances.add(dependency);
@ -69,8 +69,8 @@ public class Registry {
Optional<Supplier<?, ?>> supplied = suppliers.stream().filter(s -> s.getValueType().equals(typeClass)).findFirst();
if (supplied.isPresent()) {
Supplier<?, ?> supplier = supplied.get();
dependency = supplier.getValue(this, null);
Supplier<T, ?> supplier = (Supplier<T, ?>) supplied.get();
dependency = supplier.getValue(this, null, typeClass);
deployedInstances.add(dependency);
if (LOGGER.isTraceEnabled()) {
@ -87,13 +87,13 @@ public class Registry {
public void beforeEach(Object testInstance) {
Class testClass = testInstance.getClass();
InstanceWrapper requestedServerInstance = createInstanceWrapper(testClass.getAnnotations());
RequestedInstance requestedServerInstance = createRequestedInstance(testClass.getAnnotations(), null);
requestedInstances.add(requestedServerInstance);
for (Field f : testClass.getDeclaredFields()) {
InstanceWrapper instanceWrapper = createInstanceWrapper(f.getAnnotations());
if (instanceWrapper != null) {
requestedInstances.add(instanceWrapper);
RequestedInstance requestedInstance = createRequestedInstance(f.getAnnotations(), f.getType());
if (requestedInstance != null) {
requestedInstances.add(requestedInstance);
}
}
@ -102,10 +102,10 @@ public class Registry {
requestedInstances.stream().map(r -> r.getSupplier().getClass().getSimpleName()).collect(Collectors.joining(", ")));
}
Iterator<InstanceWrapper<?, ?>> itr = requestedInstances.iterator();
Iterator<RequestedInstance<?, ?>> itr = requestedInstances.iterator();
while (itr.hasNext()) {
InstanceWrapper<?, ?> requestedInstance = itr.next();
InstanceWrapper deployedInstance = getDeployedInstance(requestedInstance.getSupplier());
RequestedInstance<?, ?> requestedInstance = itr.next();
InstanceWrapper deployedInstance = getDeployedInstance(requestedInstance);
if (deployedInstance != null) {
if (deployedInstance.getSupplier().compatible(deployedInstance, requestedInstance)) {
if (LOGGER.isTraceEnabled()) {
@ -127,9 +127,9 @@ public class Registry {
itr = requestedInstances.iterator();
while (itr.hasNext()) {
InstanceWrapper requestedInstance = itr.next();
RequestedInstance requestedInstance = itr.next();
InstanceWrapper instance = requestedInstance.getSupplier().getValue(this, requestedInstance.getAnnotation());
InstanceWrapper instance = requestedInstance.getSupplier().getValue(this, requestedInstance.getAnnotation(), requestedInstance.getValueType());
if (LOGGER.isTraceEnabled()) {
LOGGER.tracev("Created instance: {0}",
@ -142,7 +142,7 @@ public class Registry {
}
for (Field f : testInstance.getClass().getDeclaredFields()) {
InstanceWrapper<?, ?> instance = getDeployedInstance(f.getAnnotations());
InstanceWrapper<?, ?> instance = getDeployedInstance(f.getType(), f.getAnnotations());
try {
f.setAccessible(true);
f.set(testInstance, instance.getValue());
@ -170,21 +170,22 @@ public class Registry {
destroy.forEach(this::destroy);
}
private InstanceWrapper<?, ?> createInstanceWrapper(Annotation[] annotations) {
private RequestedInstance<?, ?> createRequestedInstance(Annotation[] annotations, Class<?> valueType) {
for (Annotation a : annotations) {
for (Supplier s : suppliers) {
if (s.getAnnotationClass().equals(a.annotationType())) {
return new InstanceWrapper(s, a);
return new RequestedInstance(s, a, valueType);
}
}
}
return null;
}
private InstanceWrapper<?, ?> getDeployedInstance(Annotation[] annotations) {
private InstanceWrapper<?, ?> getDeployedInstance(Class<?> valueType, Annotation[] annotations) {
for (Annotation a : annotations) {
for (InstanceWrapper<?, ?> i : deployedInstances) {
if (i.getSupplier().getAnnotationClass().equals(a.annotationType())) {
Supplier<?, ?> supplier = i.getSupplier();
if (supplier.getAnnotationClass().equals(a.annotationType()) && valueType.isAssignableFrom(i.getValue().getClass())) {
return i;
}
}
@ -197,7 +198,7 @@ public class Registry {
if (removed) {
Set<InstanceWrapper> dependencies = instanceWrapper.getDependencies();
dependencies.forEach(this::destroy);
instanceWrapper.getSupplier().close(instanceWrapper.getValue());
instanceWrapper.getSupplier().close(instanceWrapper);
if (LOGGER.isTraceEnabled()) {
LOGGER.tracev("Closed instance: {0}",
@ -206,8 +207,8 @@ public class Registry {
}
}
private InstanceWrapper getDeployedInstance(Supplier supplier) {
return deployedInstances.stream().filter(i -> i.getSupplier().equals(supplier)).findFirst().orElse(null);
private InstanceWrapper getDeployedInstance(RequestedInstance requestedInstance) {
return deployedInstances.stream().filter(i -> i.getValue().equals(requestedInstance.getValueType())).findFirst().orElse(null);
}
private void loadSuppliers() {
@ -260,7 +261,7 @@ public class Registry {
return deployedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).findFirst().orElse(null);
}
private InstanceWrapper getRequestedInstance(Class typeClass) {
private RequestedInstance getRequestedInstance(Class typeClass) {
return requestedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).findFirst().orElse(null);
}

View file

@ -0,0 +1,28 @@
package org.keycloak.test.framework.injection;
import java.lang.annotation.Annotation;
public class RequestedInstance<T, A extends Annotation> {
private final Supplier<T, A> supplier;
private final A annotation;
private final Class<? extends T> valueType;
public RequestedInstance(Supplier<T, A> supplier, A annotation, Class<? extends T> valueType) {
this.supplier = supplier;
this.annotation = annotation;
this.valueType = valueType;
}
public Supplier<T, A> getSupplier() {
return supplier;
}
public A getAnnotation() {
return annotation;
}
public Class<? extends T> getValueType() {
return valueType;
}
}

View file

@ -10,9 +10,18 @@ public interface Supplier<T, S extends Annotation> {
InstanceWrapper<T, S> getValue(Registry registry, S annotation);
boolean compatible(InstanceWrapper<T, S> a, InstanceWrapper<T, S> b);
default InstanceWrapper<T, S> getValue(Registry registry, S annotation, Class<? extends T> valueType) {
return getValue(registry, annotation);
}
default void close(T instance) {}
boolean compatible(InstanceWrapper<T, S> a, RequestedInstance<T, S> b);
default void close(T value) {
}
default void close(InstanceWrapper<T, S> instanceWrapper) {
close(instanceWrapper.getValue());
}
default String getAlias() {
return getClass().getSimpleName();

View file

@ -0,0 +1,15 @@
package org.keycloak.test.framework.page;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.PageFactory;
public class AbstractPage {
protected final WebDriver driver;
public AbstractPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
}

View file

@ -0,0 +1,31 @@
package org.keycloak.test.framework.page;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
public class LoginPage extends AbstractPage {
@FindBy(id = "username")
private WebElement usernameInput;
@FindBy(id = "password")
private WebElement passwordInput;
@FindBy(css = "[type=submit]")
private WebElement submitButton;
public LoginPage(WebDriver driver) {
super(driver);
}
public void fillLogin(String username, String password) {
usernameInput.sendKeys(username);
passwordInput.sendKeys(password);
}
public void submit() {
submitButton.click();
}
}

View file

@ -0,0 +1,50 @@
package org.keycloak.test.framework.page;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.openqa.selenium.WebDriver;
import java.lang.reflect.Constructor;
public class PageSupplier implements Supplier<AbstractPage, TestPage> {
@Override
public Class<TestPage> getAnnotationClass() {
return TestPage.class;
}
@Override
public Class<AbstractPage> getValueType() {
return AbstractPage.class;
}
public InstanceWrapper<AbstractPage, TestPage> getValue(Registry registry, TestPage annotation) {
throw new UnsupportedOperationException();
}
@Override
public InstanceWrapper<AbstractPage, TestPage> getValue(Registry registry, TestPage annotation, Class<? extends AbstractPage> valueType) {
InstanceWrapper<AbstractPage, TestPage> instanceWrapper = new InstanceWrapper<>(this, annotation);
WebDriver webDriver = registry.getDependency(WebDriver.class, instanceWrapper);
AbstractPage page = createPage(webDriver, valueType);
instanceWrapper.setValue(page, LifeCycle.CLASS);
return instanceWrapper;
}
@Override
public boolean compatible(InstanceWrapper<AbstractPage, TestPage> a, RequestedInstance<AbstractPage, TestPage> b) {
return true;
}
private <S extends AbstractPage> S createPage(WebDriver webDriver, Class<S> valueType) {
try {
Constructor<S> constructor = valueType.getDeclaredConstructor(WebDriver.class);
return constructor.newInstance(webDriver);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,11 @@
package org.keycloak.test.framework.page;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface TestPage {
}

View file

@ -6,9 +6,7 @@ import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class WelcomePage {
private final WebDriver driver;
public class WelcomePage extends AbstractPage {
@FindBy(id = "username")
private WebElement usernameInput;
@ -26,8 +24,7 @@ public class WelcomePage {
private WebElement pageAlert;
public WelcomePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
super(driver);
}
public void navigateTo() {

View file

@ -8,6 +8,7 @@ import org.keycloak.test.framework.TestClient;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierHelpers;
@ -56,9 +57,8 @@ public class ClientSupplier implements Supplier<ClientResource, TestClient> {
}
@Override
public boolean compatible(InstanceWrapper<ClientResource, TestClient> a, InstanceWrapper<ClientResource, TestClient> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config()) &&
a.getNote(CLIENT_UUID_KEY, String.class).equals(b.getNote(CLIENT_UUID_KEY, String.class));
public boolean compatible(InstanceWrapper<ClientResource, TestClient> a, RequestedInstance<ClientResource, TestClient> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config());
}
@Override

View file

@ -7,6 +7,7 @@ import org.keycloak.test.framework.TestRealm;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierHelpers;
@ -51,9 +52,8 @@ public class RealmSupplier implements Supplier<RealmResource, TestRealm> {
}
@Override
public boolean compatible(InstanceWrapper<RealmResource, TestRealm> a, InstanceWrapper<RealmResource, TestRealm> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config()) &&
a.getNote(REALM_NAME_KEY, String.class).equals(b.getNote(REALM_NAME_KEY, String.class));
public boolean compatible(InstanceWrapper<RealmResource, TestRealm> a, RequestedInstance<RealmResource, TestRealm> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config());
}
@Override

View file

@ -8,6 +8,7 @@ import org.keycloak.test.framework.TestUser;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierHelpers;
@ -56,9 +57,8 @@ public class UserSupplier implements Supplier<UserResource, TestUser> {
}
@Override
public boolean compatible(InstanceWrapper<UserResource, TestUser> a, InstanceWrapper<UserResource, TestUser> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config()) &&
a.getNote(USER_UUID_KEY, String.class).equals(b.getNote(USER_UUID_KEY, String.class));
public boolean compatible(InstanceWrapper<UserResource, TestUser> a, RequestedInstance<UserResource, TestUser> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config());
}
@Override

View file

@ -5,6 +5,7 @@ import org.keycloak.test.framework.database.TestDatabase;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierHelpers;
@ -45,7 +46,7 @@ public abstract class AbstractKeycloakTestServerSupplier implements Supplier<Key
}
@Override
public boolean compatible(InstanceWrapper<KeycloakTestServer, KeycloakIntegrationTest> a, InstanceWrapper<KeycloakTestServer, KeycloakIntegrationTest> b) {
public boolean compatible(InstanceWrapper<KeycloakTestServer, KeycloakIntegrationTest> a, RequestedInstance<KeycloakTestServer, KeycloakIntegrationTest> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config());
}

View file

@ -3,6 +3,7 @@ package org.keycloak.test.framework.webdriver;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
@ -26,7 +27,7 @@ public class ChromeWebDriverSupplier implements Supplier<WebDriver, TestWebDrive
}
@Override
public boolean compatible(InstanceWrapper<WebDriver, TestWebDriver> a, InstanceWrapper<WebDriver, TestWebDriver> b) {
public boolean compatible(InstanceWrapper<WebDriver, TestWebDriver> a, RequestedInstance<WebDriver, TestWebDriver> b) {
return true;
}

View file

@ -3,6 +3,7 @@ package org.keycloak.test.framework.webdriver;
import org.keycloak.test.framework.injection.InstanceWrapper;
import org.keycloak.test.framework.injection.LifeCycle;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
@ -26,7 +27,7 @@ public class FirefoxWebDriverSupplier implements Supplier<WebDriver, TestWebDriv
}
@Override
public boolean compatible(InstanceWrapper<WebDriver, TestWebDriver> a, InstanceWrapper<WebDriver, TestWebDriver> b) {
public boolean compatible(InstanceWrapper<WebDriver, TestWebDriver> a, RequestedInstance<WebDriver, TestWebDriver> b) {
return true;
}

View file

@ -9,4 +9,5 @@ org.keycloak.test.framework.webdriver.ChromeWebDriverSupplier
org.keycloak.test.framework.webdriver.FirefoxWebDriverSupplier
org.keycloak.test.framework.database.DevMemDatabaseSupplier
org.keycloak.test.framework.database.DevFileDatabaseSupplier
org.keycloak.test.framework.database.PostgresDatabaseSupplier
org.keycloak.test.framework.database.PostgresDatabaseSupplier
org.keycloak.test.framework.page.PageSupplier