diff --git a/src/main/java/io/github/uchagani/stagehand/FieldDecorator.java b/src/main/java/io/github/uchagani/stagehand/FieldDecorator.java index b7ee78a..dd262f1 100644 --- a/src/main/java/io/github/uchagani/stagehand/FieldDecorator.java +++ b/src/main/java/io/github/uchagani/stagehand/FieldDecorator.java @@ -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); } diff --git a/src/main/java/io/github/uchagani/stagehand/LocatorFactory.java b/src/main/java/io/github/uchagani/stagehand/LocatorFactory.java index 00163db..f58f3f0 100644 --- a/src/main/java/io/github/uchagani/stagehand/LocatorFactory.java +++ b/src/main/java/io/github/uchagani/stagehand/LocatorFactory.java @@ -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; @@ -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]); diff --git a/src/main/java/io/github/uchagani/stagehand/PageFactory.java b/src/main/java/io/github/uchagani/stagehand/PageFactory.java index dd7095b..3541299 100644 --- a/src/main/java/io/github/uchagani/stagehand/PageFactory.java +++ b/src/main/java/io/github/uchagani/stagehand/PageFactory.java @@ -1,11 +1,18 @@ 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 create(Class pageToCreate, Page page) { @@ -13,7 +20,7 @@ public static T create(Class pageToCreate, Page 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."); @@ -22,13 +29,13 @@ public static void initElements(Object pageObject, Page page) { private static T instantiatePage(Class pageClassToProxy, Page page) { try { - if(pageClassToProxy.isAnnotationPresent(PageObject.class)) { + if (pageClassToProxy.isAnnotationPresent(PageObject.class)) { T pageObjectInstance; try { Constructor 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; @@ -41,24 +48,74 @@ private static T instantiatePage(Class 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 fieldNamesAlreadyProxied = new ArrayList<>(); + List 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 proxiedScopedFields = new ArrayList<>(); + + for (Field field : fieldsWithDependencies) { + List 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); } } } diff --git a/src/main/java/io/github/uchagani/stagehand/annotations/Under.java b/src/main/java/io/github/uchagani/stagehand/annotations/Under.java new file mode 100644 index 0000000..82e05c5 --- /dev/null +++ b/src/main/java/io/github/uchagani/stagehand/annotations/Under.java @@ -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(); +} diff --git a/src/test/java/io/github/uchagani/stagehand/pages/PageWithNestedIframe.java b/src/test/java/io/github/uchagani/stagehand/pages/PageWithNestedIframe.java index 6d60f8c..2c70a0d 100644 --- a/src/test/java/io/github/uchagani/stagehand/pages/PageWithNestedIframe.java +++ b/src/test/java/io/github/uchagani/stagehand/pages/PageWithNestedIframe.java @@ -6,6 +6,8 @@ @PageObject(frame = {"#parentIframe", "#childIframe"}) public class PageWithNestedIframe { + @Find("#child-iframe-paragraph") public Locator paragraph; + } diff --git a/src/test/java/io/github/uchagani/stagehand/pages/PageWithoutConstructor.java b/src/test/java/io/github/uchagani/stagehand/pages/PageWithoutConstructor.java index 2e08fd8..1fd6174 100644 --- a/src/test/java/io/github/uchagani/stagehand/pages/PageWithoutConstructor.java +++ b/src/test/java/io/github/uchagani/stagehand/pages/PageWithoutConstructor.java @@ -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 { @@ -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; } diff --git a/src/test/java/io/github/uchagani/stagehand/tests/HTMLConstants.java b/src/test/java/io/github/uchagani/stagehand/tests/HTMLConstants.java index d3338b5..0f5acb9 100644 --- a/src/test/java/io/github/uchagani/stagehand/tests/HTMLConstants.java +++ b/src/test/java/io/github/uchagani/stagehand/tests/HTMLConstants.java @@ -1,6 +1,6 @@ package io.github.uchagani.stagehand.tests; public class HTMLConstants { - public static final String SIMPLE_HTML = "

Header Text

This is a paragraph.

"; + public static final String SIMPLE_HTML = "

Header Text

This is a paragraph.

"; public static final String IFRAME_HTML = "\">"; } diff --git a/src/test/java/io/github/uchagani/stagehand/tests/PageFactoryTests.java b/src/test/java/io/github/uchagani/stagehand/tests/PageFactoryTests.java index dec7271..7b63160 100644 --- a/src/test/java/io/github/uchagani/stagehand/tests/PageFactoryTests.java +++ b/src/test/java/io/github/uchagani/stagehand/tests/PageFactoryTests.java @@ -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();