q2ebanking / boa-constrictor

Boa Constrictor is a C# implementation of the Screenplay Pattern. Its primary use case is Web UI and REST API test automation. Boa Constrictor helps you make better interactions for better automation!
https://q2ebanking.github.io/boa-constrictor/
Other
118 stars 40 forks source link

Explore Playwright #71

Open pl-aknight opened 3 years ago

pl-aknight commented 3 years ago

Playwright has a C# implementation. Perhaps we should add Screenplay interactions for it. I heard it's much faster than Selenium WebDriver.

https://playwright.dev/ https://github.com/microsoft/playwright-sharp

pl-aknight commented 3 years ago

Ideas for parallel testing: https://twitter.com/SabotageAndi/status/1359890598031994880?s=20

AutomationPanda commented 3 years ago

It looks like Playwright for .NET is entirely async. This means we'd need to make Boa Constrictor async as well. Ohhhh boy...

thePantz commented 1 year ago

I was exploring this a while back. Here's an ability I wrote if it's at all helpful

/// <summary>
/// Enables the Actor to use a Web browser via Playwright
/// </summary>
public class BrowseTheWebSynchronously : IAbility
{
    private IBrowserContext currentContext;
    private IPage currentPage;

    /// <summary>
    /// Private constructor.
    /// (Use the static methods for public construction.)
    /// </summary>
    /// <param name="browser">The Playwright browser instance</param>
    private BrowseTheWebSynchronously(IPlaywright playwright, IBrowser browser)
    {
        Playwright = playwright;
        Browser = browser;
        Pages = new List<IPage>();
    }

    public IBrowser Browser { get; }
    public IPage CurrentPage { get; }
    public IList<IPage> Pages { get; }
    public IPlaywright Playwright { get; }

    /// <summary>
    /// Supply a pre-defined Plawright browser to use
    /// </summary>
    /// <param name="playwright">The Playwright instance</param>
    /// <param name="browser">The Playwright browser instance.</param>
    /// <returns>An instance of <see cref="BrowseTheWebSynchronously"/></returns>
    public static BrowseTheWebSynchronously Using(IPlaywright playwright, IBrowser browser)
    {
        return new BrowseTheWebSynchronously(playwright, browser);
    }

    /// <summary>
    /// Use a synchronous Chromium (i.e. Chrome, Edge, Opera, etc.) browser.
    /// </summary>
    /// <returns>An instance of <see cref="BrowseTheWebSynchronously"/> configured to use Chromium</returns>
    public static async Task<BrowseTheWebSynchronously> UsingChromium()
    {
        using var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
        await using var browser = await playwright.Chromium.LaunchAsync();
        return new BrowseTheWebSynchronously(playwright, browser);
    }

    /// <summary>
    /// Use a synchronous Firefox browser
    /// </summary>
    /// <returns>An instance of <see cref="BrowseTheWebSynchronously"/> configured to use firefox</returns>
    public static async Task<BrowseTheWebSynchronously> UsingFirefox()
    {
        using var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
        await using var browser = await playwright.Firefox.LaunchAsync();
        return new BrowseTheWebSynchronously(playwright, browser);
    }
    /// <summary>
    /// Use a synchronous WebKit (i.e. Safari, etc.) browser.
    /// </summary>
    /// <returns>An instance of <see cref="BrowseTheWebSynchronously"/> configured to use Webkit</returns>
    public static async Task<BrowseTheWebSynchronously> UsingWebkit()
    {
        using var playwright = await Microsoft.Playwright.Playwright.CreateAsync();
        await using var browser = await playwright.Chromium.LaunchAsync();
        return new BrowseTheWebSynchronously(playwright, browser);
    }

    public async Task<IPage> CurrentPageAsync()
    {
        if (currentPage == null)
        {
            await using var context = await GetBrowserContextAsync();
            currentPage = await context.NewPageAsync();
        }

        return currentPage;
    }

    /// <summary>
    /// Returns a description of this Ability
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"browse the web with playwright using {Browser}";
    }

    private async Task<IBrowserContext> GetBrowserContextAsync()
    {
        if (currentContext == null)
        {
            currentContext = await Browser.NewContextAsync();
        }

        return currentContext;
    }
}

I got stuck because at the time Boa didn't have any support for async calls... but now we do! :)

thePantz commented 1 year ago

Credit where credit is due, I took inspiration from https://github.com/ScreenPyHQ/screenpy_playwright

thePantz commented 7 months ago

I made a proof of concept here: https://github.com/thePantz/boa-constrictor/tree/feature/playwright-support/Boa.Constrictor.Playwright

pl-shernandez commented 7 months ago

@thePantz I'm excited about this proof of concept. What do you think the biggest challenges would be in making it have parity with the .Selenium package?

thePantz commented 7 months ago

@pl-shernandez there needs to be a mechanism similar to WebLocator to resolve locators and give them friendly names.

Playwright has a lot more options when it comes to locating elements compared to selenium https://playwright.dev/dotnet/docs/locators

I'm not sure what the best approach for this would be... unlike Selenium and it's By object, the method used to locate is driven by playwrights IPage.

I was thinking something like this:

    public class PlaywrightLocator
    {
        public PlaywrightLocator(string name, Func<IPage, ILocator> locatorFunc)
        {
            Name = name;
            LocatorFunc = locatorFunc;
        }

        public string Name { get; set; }
        public Func<IPage, ILocator> LocatorFunc { get; set; }

        public static PlaywrightLocator L(string name, Func<IPage, ILocator> locatorFunc)
        {
            return new PlaywrightLocator(name, locatorFunc);
        }
    }

This would just store the friendly name and a function invoked with an IPage, that returns an ILocator. This allows us to make simple locator properties, similar to the .Selenium package without sacrificing flexibility

 public static PlaywrightLocator SearchButton => 
    L("Search Button", page => page.GetByRole(AriaRole.Button, new PageGetByRoleOptions { Name = "search" }));

Then the locator gets resolved in the task/question

    public class Click : AbstractPageTask
    {
        private readonly PlaywrightLocator Locator;

        private Click(PlaywrightLocator locator)
        {
            Locator = locator;
        }

        public static Click On(PlaywrightLocator locator) => new Click(locator);

        public override async Task PerformAsAsync(IActor actor, IPage page)
        {
            var locator = Locator.LocatorFunc.Invoke(page);
            await locator.ClickAsync();
        }

        public override string ToString() => $"click on {Locator.Name}";

    }

This could probably be cleaned up a bit but you get the idea... Haven't had time to test this out but it's the best I've come up with so far... Open to suggestions, I'm certainly not a playwright expert.


Apart from the above, I noticed that Boas Wait functions do not support async questions. This should be simple enough but is required since everything is async in Playwright...

We also need to write documentation

bobbhatti commented 6 months ago

has there been any progress on this? if the proof of concept usable, and if so can we clone it and build it ourselves?

thePantz commented 1 month ago

Hi @bobbhatti,

I've slowly been adding to my PoC. I only do this in my free time... Maybe if Q2 wants to hire me I could make this go faster haha

Happy to accept any help/feedback others are willing to offer