Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 81 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,95 @@ public new HeaderAssertions ExpectV2() => new(this);

# Playwright.ReactUI.Controls.Extensions

Библиотека предоставляет набор расширений к **Playwright.ReactUI.Controls**
Библиотека предоставляет набор расширений к **Playwright.ReactUI.Controls**, а также атрибуты для автозаполнения контролов в PageObjects и PageElements

### Как использовать

**Примеры для компонента [Input](https://tech.skbkontur.ru/kontur-ui/?path=/docs/react-ui_input-data-input--docs):**
**Примеры расширений для компонента [Input](https://tech.skbkontur.ru/kontur-ui/?path=/docs/react-ui_input-data-input--docs):**

+ `await input.AppendTextAsync("newValue").ConfigureAwait(false);` - добавление значения `newValue` к уже существующему в Input
+ `await input.WaitToBeVisibleAsync().ConfigureAwait(false);` - ожидание видимости компонента на странице
+ `await input.WaitToHaveValueAsync("TODO").ConfigureAwait(false);` - ожидание значения `TODO` в Input'e

**AutoFillControlsAttribute**

Чтобы воспользоваться атрибутом **AutoFillControls** необходимо следующее:
- Страница (PageObject) должна наследоваться от **PageBase**. Если у вас есть свой базовый класс страницы, то он должен наследовать PageBase
- Составной / сложный компонент (PageElement), т.е. контрол, который состоит из нескольких контролов (см. пример ниже), должен наследоваться от **CompoundControlBase**, а не **ControlBase**
- На PageObject / PageElement навесить атрибут [AutoFillControls]

Для заполнения самих контролов существует несколько атрибутов:
- **RootByTid** - ищет контрол по переданному data-tid'у
- **RootByLocator** - ищет контрол по переданному селектору (css / xpath)
- **ChildByTid** - ищет элемент списка по переданному data-tid'у; используется **только** для инициализации **ControlList** совместно с **RootByTid / RootByLocator**
- **ChildByLocator** - ищет элемент списка по переданному селектору (css / xpath); используется **только** для инициализации **ControlList** совместно с **RootByTid / RootByLocator**

Библиотека предоставляет возможность создать свой атрибут для заполнения контролов. Для этого надо реализовать **IRootLocatorAttribute** и(или) **IChildLocatorAttribute** (см. пример ниже)
**Если появится желание реализовать свой атрибут, то лучше сначало прийти в меня. Возможно ваш атрибут лучше поместить в библиотеку**

Если никакой атрибут заполнения контрола не указан, то **AutoFillControls** будет искать контрол по data-tid'у имени свойства

**Примеры использования AutoFillControlsAttribute**

```
// PageObject
[AutoFillControls]
public class TestPage : PageBase
{
public TestPage(IPage page)
: base(page)
{
}

// Контрол инициализируется с data-tid'ом Compound
public Compound Compound { get; init; }

// Контрол инициализируется с data-tid'ом LinkId
[RootByTid("LinkId")]
public Link Link { get; init; }

// Контрол инициализируется с локатором LocatorId (здесь может быть css / xpath)
[RootByLocator("LocatorId")]
public Input Input { get; init; }
}

// PageElement
[AutoFillControls]
public class Compound : CompoundControlBase
{
public Compound(ILocator rootLocator)
: base(rootLocator)
{
Button = new Button(rootLocator.GetByText("ButtonId"));
}

// Контрол инициализируется в конструкторе и не будет автозаполняться
[SkipAutoFillControl]
public Button Button { get; init; }

// Для создания списка необходимо указать Root* и Child* атрибуты. Child атрибут должен быть обязательно указан
[RootByTid("RootList")]
[ChildByLocator("ChildItem")]
public ControlList<Label> List { get; init; }
}
```

**Пример реализации IRootLocatorAttribute**

```
// Ищет контрол по GetByText
[AttributeUsage(AttributeTargets.Property)]
public class RootByTextAttribute : Attribute, IRootLocatorAttribute
{
public RootByTextAttribute(string selector) => Selector = selector;
public string Selector { get; }

public ILocator Resolve(ILocator locator) => locator.GetByText(Selector);

public ILocator Resolve(IPage page) => page.GetByText(Selector);
}
```

# Минимальные требования

+ netstandard2.0 / NET6
Expand Down
80 changes: 79 additions & 1 deletion readme/README-Controls.Extensions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Playwright.ReactUI.Controls.Extensions

Библиотека предоставляет набор расширений к **Playwright.ReactUI.Controls**
Библиотека предоставляет набор расширений к **Playwright.ReactUI.Controls**, а также атрибуты для автозаполнения контролов в PageObjects и PageElements

### Как использовать

Expand All @@ -10,6 +10,84 @@
+ `await input.WaitToBeVisibleAsync().ConfigureAwait(false);` - ожидание видимости компонента на странице
+ `await input.WaitToHaveValueAsync("TODO").ConfigureAwait(false);` - ожидание значения `TODO` в Input'e

**AutoFillControlsAttribute**

Чтобы воспользоваться атрибутом **AutoFillControls** необходимо следующее:
- Страница (PageObject) должна наследоваться от **PageBase**. Если у вас есть свой базовый класс страницы, то он должен наследовать **PageBase**
- Составной / сложный компонент (PageElement), т.е. контрол, который состоит из нескольких контролов (см. пример ниже), должен наследоваться от **CompoundControlBase**, а не **ControlBase**
- На PageObject / PageElement навесить атрибут [AutoFillControls]

Для заполнения самих контролов существует несколько атрибутов:
- **RootByTid** - ищет контрол по переданному data-tid'у
- **RootByLocator** - ищет контрол по переданному селектору (css / xpath)
- **ChildByTid** - ищет элемент списка по переданному data-tid'у; используется **только** для инициализации **ControlList** совместно с **RootByTid / RootByLocator**
- **ChildByLocator** - ищет элемент списка по переданному селектору (css / xpath); используется **только** для инициализации **ControlList** совместно с **RootByTid / RootByLocator**

Библиотека предоставляет возможность создать свой атрибут для заполнения контролов. Для этого надо реализовать **IRootLocatorAttribute** и(или) **IChildLocatorAttribute** (см. пример ниже)
**Если появится желание реализовать свой атрибут, то лучше сначало прийти в меня. Возможно ваш атрибут лучше поместить в библиотеку**

Если никакой атрибут заполнения контрола не указан, то **AutoFillControls** будет искать контрол по data-tid'у имени свойства

**Примеры использования AutoFillControlsAttribute**

```
// PageObject
[AutoFillControls]
public class TestPage : PageBase
{
public TestPage(IPage page)
: base(page)
{
}

// Контрол инициализируется с data-tid'ом Compound
public Compound Compound { get; init; }

// Контрол инициализируется с data-tid'ом LinkId
[RootByTid("LinkId")]
public Link Link { get; init; }

// Контрол инициализируется с локатором LocatorId (здесь может быть css / xpath)
[RootByLocator("LocatorId")]
public Input Input { get; init; }
}

// PageElement
[AutoFillControls]
public class Compound : CompoundControlBase
{
public Compound(ILocator rootLocator)
: base(rootLocator)
{
Button = new Button(rootLocator.GetByText("ButtonId"));
}

// Контрол инициализируется в конструкторе и не будет автозаполняться
[SkipAutoFillControl]
public Button Button { get; init; }

// Для создания списка необходимо указать Root* и Child* атрибуты. Child атрибут должен быть обязательно указан
[RootByTid("RootList")]
[ChildByLocator("ChildItem")]
public ControlList<Label> List { get; init; }
}
```

**Пример реализации IRootLocatorAttribute**

```
// Ищет контрол по GetByText
[AttributeUsage(AttributeTargets.Property)]
public class RootByTextAttribute : Attribute, IRootLocatorAttribute
{
public RootByTextAttribute(string selector) => Selector = selector;
public string Selector { get; }

public ILocator Resolve(ILocator locator) => locator.GetByText(Selector);

public ILocator Resolve(IPage page) => page.GetByText(Selector);
}
```

# Минимальные требования

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

[AttributeUsage(AttributeTargets.Class)]
public class AutoFillControlsAttribute : Attribute
{
private const BindingFlags bindingFlags = BindingFlags.Instance |
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.GetProperty |
BindingFlags.SetProperty;

public void OnInit(PageBase pageInstance)
=> InitControl(pageInstance);

public void OnInit(CompoundControlBase compoundControlInstance)
=> InitControl(compoundControlInstance);

private static void InitControl(object instance)
{
var type = instance.GetType();

foreach (var property in type.GetProperties(bindingFlags))
{
if (property.IsDefined(typeof(SkipAutoFillControlAttribute)) ||
!typeof(ControlBase).IsAssignableFrom(property.PropertyType))
{
continue;
}

var rootLocator = GetRootLocator(property, instance);

var value = property.PropertyType.IsGenericType &&
property.PropertyType.GetGenericTypeDefinition() == typeof(ControlList<>)
? CreateControlList(property, rootLocator, instance)
: Activator.CreateInstance(property.PropertyType, rootLocator)
?? throw new ArgumentException($"Значение для '{property.Name}' равно null");

SetProperty(property, instance, value);
}
}

private static ILocator GetRootLocator(PropertyInfo property, object instance)
{
var locatorAttribute = property
.GetCustomAttributes()
.OfType<IRootLocatorAttribute>()
.FirstOrDefault();

return instance switch
{
CompoundControlBase compound => locatorAttribute?.Resolve(compound.RootLocator) ??
compound.RootLocator.GetByTestId(property.Name),
PageBase page => locatorAttribute?.Resolve(page.Page) ?? page.Page.GetByTestId(property.Name),
_ => throw new NotSupportedException(
$"Неизвестный тип контейнера. Ожидался '{nameof(CompoundControlBase)}' или '{nameof(PageBase)}'")
};
}

private static object CreateControlList(PropertyInfo property, ILocator rootLocator, object instance)
{
var locatorAttribute = property
.GetCustomAttributes()
.OfType<IChildLocatorAttribute>()
.FirstOrDefault()
?? throw new ArgumentException(
$"Не задан '{nameof(ChildByTidAttribute)}' для '{property.Name}' в '{instance}'"
);

var itemType = property.PropertyType.GetGenericArguments().First();
var listType = typeof(ControlList<>).MakeGenericType(itemType);
var factoryType = typeof(Func<,>).MakeGenericType(typeof(ILocator), itemType);
var factoryMethod = typeof(AutoFillControlsAttribute)
.GetMethod(nameof(CreateControl), BindingFlags.Static | BindingFlags.NonPublic)!
.MakeGenericMethod(itemType);

var itemFactory = Delegate.CreateDelegate(factoryType, factoryMethod);

return Activator.CreateInstance(
listType,
rootLocator,
(Func<ILocator, ILocator>)(locator => locatorAttribute.Resolve(locator)),
itemFactory
) ?? throw new InvalidOperationException(
$"Не удалось создать '{listType}'. Убедись, что верно указал значения атрибутов. " +
$"Или используй '{nameof(SkipAutoFillControlAttribute)}' и инициализируй контрол в конструкторе"
);
}

private static T CreateControl<T>(ILocator locator) where T : ControlBase
=> (T)(Activator.CreateInstance(typeof(T), locator)
?? throw new InvalidOperationException(
$"Не удалось создать экземпляр '{typeof(T)}'. " +
"Убедись, что у него есть публичный конструктор с параметром ILocator. " +
$"Или используй '{nameof(SkipAutoFillControlAttribute)}' и инициализируй контрол в конструкторе"
)
);

private static void SetProperty(PropertyInfo property, object instance, object value)
{
try
{
property.SetValue(instance, value);
}
catch (Exception e)
{
throw new ArgumentException(
$"Не удалось установить значение для '{property.Name}' в '{instance}'. Возможно, отсутствует сеттер",
e
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

[AttributeUsage(AttributeTargets.Property)]
public class ChildByLocatorAttribute : Attribute, IChildLocatorAttribute
{
public ChildByLocatorAttribute(string selector) => Selector = selector;
public string Selector { get; }

public ILocator Resolve(ILocator locator) => locator.Locator(Selector);
}
13 changes: 13 additions & 0 deletions src/Controls.Extensions/AutoFill/Attributes/ChildByTidAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

[AttributeUsage(AttributeTargets.Property)]
public class ChildByTidAttribute : Attribute, IChildLocatorAttribute
{
public ChildByTidAttribute(string selector) => Selector = selector;
public string Selector { get; }

public ILocator Resolve(ILocator locator) => locator.GetByTestId(Selector);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

public interface IChildLocatorAttribute
{
string Selector { get; }
ILocator Resolve(ILocator locator);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

public interface IRootLocatorAttribute
{
string Selector { get; }
ILocator Resolve(ILocator locator);
ILocator Resolve(IPage page);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

[AttributeUsage(AttributeTargets.Property)]
public class RootByLocatorAttribute : Attribute, IRootLocatorAttribute
{
public RootByLocatorAttribute(string selector) => Selector = selector;
public string Selector { get; }

public ILocator Resolve(ILocator locator) => locator.Locator(Selector);

public ILocator Resolve(IPage page) => page.Locator(Selector);
}
14 changes: 14 additions & 0 deletions src/Controls.Extensions/AutoFill/Attributes/RootByTidAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
using Microsoft.Playwright;

namespace Playwright.ReactUI.Controls.Extensions.AutoFill.Attributes;

[AttributeUsage(AttributeTargets.Property)]
public class RootByTidAttribute : Attribute, IRootLocatorAttribute
{
public RootByTidAttribute(string selector) => Selector = selector;
public string Selector { get; }

public ILocator Resolve(ILocator locator) => locator.GetByTestId(Selector);
public ILocator Resolve(IPage page) => page.GetByTestId(Selector);
}
Loading
Loading