Skip to content

Commit

Permalink
Scoped locators (#5)
Browse files Browse the repository at this point in the history
* Added Under annotation

* Added ability to proxy dependent field

* Added implementation for scoped fields

* Added tests
  • Loading branch information
uchagani authored Feb 10, 2022
1 parent dd32bd3 commit 3692db1
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 29 deletions.
13 changes: 3 additions & 10 deletions src/main/java/io/github/uchagani/stagehand/FieldDecorator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,9 @@ public FieldDecorator(Page page) {
this.locatorFactory = new LocatorFactory(page);
}

public Object decorate(ClassLoader loader, Field field) {
if (!Locator.class.isAssignableFrom(field.getType())) {
return null;
}

if (!field.isAnnotationPresent(Find.class)) {
return null;
}

Locator locator = locatorFactory.createLocator(field);
public Object decorate(Field field, Object pageObjectInstance) {
Locator locator = locatorFactory.createLocator(field, pageObjectInstance);
ClassLoader loader = pageObjectInstance.getClass().getClassLoader();
return proxyForLocator(loader, locator);
}

Expand Down
18 changes: 16 additions & 2 deletions src/main/java/io/github/uchagani/stagehand/LocatorFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.microsoft.playwright.Page;
import io.github.uchagani.stagehand.annotations.Find;
import io.github.uchagani.stagehand.annotations.PageObject;
import io.github.uchagani.stagehand.annotations.Under;

import java.lang.reflect.Field;

Expand All @@ -15,13 +16,26 @@ public LocatorFactory(Page page) {
this.page = page;
}

public Locator createLocator(Field field) {
public Locator createLocator(Field field, Object pageObjectInstance) {
Class<?> clazz = field.getDeclaringClass();
PageObject pageAnnotation = clazz.getAnnotation(PageObject.class);
Find findAnnotation = field.getAnnotation(Find.class);
Under underAnnotation = field.getAnnotation(Under.class);

if (pageAnnotation.frame().length == 0) {
return page.locator(findAnnotation.value());
if (underAnnotation == null) {
return page.locator(findAnnotation.value());
}

Locator locator;
try {
Field depField = clazz.getField(underAnnotation.value());
locator = (Locator) depField.get(pageObjectInstance);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e.getCause());
}

return locator.locator(findAnnotation.value());
}

FrameLocator frameLocator = page.frameLocator(pageAnnotation.frame()[0]);
Expand Down
89 changes: 73 additions & 16 deletions src/main/java/io/github/uchagani/stagehand/PageFactory.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package io.github.uchagani.stagehand;

import com.microsoft.playwright.Locator;
import com.microsoft.playwright.Page;
import io.github.uchagani.stagehand.annotations.Find;
import io.github.uchagani.stagehand.annotations.PageObject;
import io.github.uchagani.stagehand.annotations.Under;
import io.github.uchagani.stagehand.exeptions.MissingPageObjectAnnotation;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class PageFactory {
public static <T> T create(Class<T> pageToCreate, Page page) {
return instantiatePage(pageToCreate, page);
}

public static void initElements(Object pageObject, Page page) {
if(pageObject.getClass().isAnnotationPresent(PageObject.class)) {
if (pageObject.getClass().isAnnotationPresent(PageObject.class)) {
initElements(new FieldDecorator(page), pageObject);
} else {
throw new MissingPageObjectAnnotation("Only pages marked with @PageObject can can be initialized by the PageFactory.");
Expand All @@ -22,13 +29,13 @@ public static void initElements(Object pageObject, Page page) {

private static <T> T instantiatePage(Class<T> pageClassToProxy, Page page) {
try {
if(pageClassToProxy.isAnnotationPresent(PageObject.class)) {
if (pageClassToProxy.isAnnotationPresent(PageObject.class)) {
T pageObjectInstance;
try {
Constructor<T> constructor = pageClassToProxy.getConstructor(Page.class);
pageObjectInstance = constructor.newInstance(page);
} catch (NoSuchMethodException e) {
pageObjectInstance = pageClassToProxy.getDeclaredConstructor().newInstance();
pageObjectInstance = pageClassToProxy.getDeclaredConstructor().newInstance();
}
initElements(new FieldDecorator(page), pageObjectInstance);
return pageObjectInstance;
Expand All @@ -41,24 +48,74 @@ private static <T> T instantiatePage(Class<T> pageClassToProxy, Page page) {
}

private static void initElements(FieldDecorator decorator, Object pageObjectInstance) {
Class<?> proxyIn = pageObjectInstance.getClass();
while (proxyIn != Object.class) {
proxyFields(decorator, pageObjectInstance, proxyIn);
proxyIn = proxyIn.getSuperclass();
Class<?> pageObjectClass = pageObjectInstance.getClass();
while (pageObjectClass != Object.class) {
proxyFields(decorator, pageObjectInstance, pageObjectClass);
pageObjectClass = pageObjectClass.getSuperclass();
}
}

private static void proxyFields(FieldDecorator decorator, Object page, Class<?> proxyIn) {
Field[] fields = proxyIn.getDeclaredFields();
private static void proxyFields(FieldDecorator decorator, Object pageObjectInstance, Class<?> pageObjectClass) {
Field[] fields = pageObjectClass.getDeclaredFields();
List<String> fieldNamesAlreadyProxied = new ArrayList<>();
List<Field> fieldsWithDependencies = new ArrayList<>();

for (Field field : fields) {
Object value = decorator.decorate(page.getClass().getClassLoader(), field);
if (value != null) {
try {
field.setAccessible(true);
field.set(page, value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
if (isProxyable(field)) {
if (hasDependencies(field)) {
fieldsWithDependencies.add(field);
continue;
}
proxyField(decorator, field, pageObjectInstance);
fieldNamesAlreadyProxied.add(field.getName());
}
}

int sizeBefore;
while (!fieldsWithDependencies.isEmpty()) {
sizeBefore = fieldsWithDependencies.size();
List<Field> proxiedScopedFields = new ArrayList<>();

for (Field field : fieldsWithDependencies) {
List<String> dependencyNames = Collections.singletonList(field.getAnnotation(Under.class).value());

if (fieldNamesAlreadyProxied.containsAll(dependencyNames)) {
proxyField(decorator, field, pageObjectInstance);
fieldNamesAlreadyProxied.add(field.getName());
proxiedScopedFields.add(field);
}
}

for (Field proxied : proxiedScopedFields) {
fieldsWithDependencies.remove(proxied);
}

if (sizeBefore == fieldsWithDependencies.size()) {
throw new RuntimeException("Unable to find dependencies for the following Fields:");
}
}
}

private static boolean hasDependencies(Field field) {
return field.isAnnotationPresent(Under.class);
}

private static boolean isProxyable(Field field) {
if (!Locator.class.isAssignableFrom(field.getType())) {
return false;
}

return field.isAnnotationPresent(Find.class);
}

private static void proxyField(FieldDecorator decorator, Field field, Object pageObjectInstance) {
Object value = decorator.decorate(field, pageObjectInstance);
if (value != null) {
try {
field.setAccessible(true);
field.set(pageObjectInstance, value);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/github/uchagani/stagehand/annotations/Under.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.github.uchagani.stagehand.annotations;

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 Under {
String value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

@PageObject(frame = {"#parentIframe", "#childIframe"})
public class PageWithNestedIframe {

@Find("#child-iframe-paragraph")
public Locator paragraph;

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.microsoft.playwright.Locator;
import io.github.uchagani.stagehand.annotations.Find;
import io.github.uchagani.stagehand.annotations.PageObject;
import io.github.uchagani.stagehand.annotations.Under;

@PageObject
public class PageWithoutConstructor {
Expand All @@ -12,4 +13,25 @@ public class PageWithoutConstructor {
@Find(".paragraph")
public Locator paragraph;

@Find("#firstNameFormDiv")
public Locator firstNameFormDiv;

@Find("form")
@Under("firstNameFormDiv")
public Locator firstNameForm;

@Find("input")
@Under("firstNameForm")
public Locator firstNameInput;

@Find("#lastNameFormDiv")
public Locator lastNameFormDiv;

@Find("form")
@Under("lastNameFormDiv")
public Locator lastNameForm;

@Find("input")
@Under("lastNameForm")
public Locator lastNameInput;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.uchagani.stagehand.tests;

public class HTMLConstants {
public static final String SIMPLE_HTML = "<!DOCTYPE html><h2 id=headerId>Header Text</h2><p class=paragraph>This is a paragraph.</p>";
public static final String SIMPLE_HTML = "<!DOCTYPE html><h2 id=headerId>Header Text</h2><p class=paragraph>This is a paragraph.<div id=firstNameFormDiv><form><input placeholder=\"First Name\"></form></div><div id=lastNameFormDiv><form><input placeholder=\"Last Name\"></form><input placeholder=City></div>";
public static final String IFRAME_HTML = "<!DOCTYPE html><iframe height=500 width=800 id=parentIframe srcdoc=\"<p id=iframe-paragraph>Hello from inside an iframe!</p><iframe id=childIframe height=500 width=800 srcdoc='<p id=child-iframe-paragraph>Child iFrame paragraph</p>' </iframe>\"></iframe>";
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,23 @@ public void create_canInitializeFields_inClassWithAConstructor() {
assertThat(homePage.paragraph.textContent()).isEqualTo("This is a paragraph.");
}

@Test
public void create_canInitialize_dependentFields() {
PageWithoutConstructor homePage = PageFactory.create(PageWithoutConstructor.class, page);

assertThat(homePage.firstNameInput.getAttribute("placeholder")).isEqualTo("First Name");
assertThat(homePage.lastNameInput.getAttribute("placeholder")).isEqualTo("Last Name");
}

@Test
public void initElements_canInitialize_dependentFields() {
PageWithoutConstructor homePage = new PageWithoutConstructor();
PageFactory.initElements(homePage, page);

assertThat(homePage.firstNameInput.getAttribute("placeholder")).isEqualTo("First Name");
assertThat(homePage.lastNameInput.getAttribute("placeholder")).isEqualTo("Last Name");
}

@Test
public void initElements_canInitializeFieldsMarkedWithFindAnnotation() {
PageWithoutConstructor homePage = new PageWithoutConstructor();
Expand Down

0 comments on commit 3692db1

Please sign in to comment.