Contributions to the Headless Chrome .NET API ππ§ͺ
Puppeteer Sharp Contributions offers extensions to the Puppeteer Sharp API. It provides a convenient way to write readable and robust browser tests in .NET
Puppeteer Sharp is a .NET port of the official Node.JS Puppeteer API.
Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.
- Introduction
- PuppeteerSharp.Contrib.Extensions
- PuppeteerSharp.Contrib.Should
- PuppeteerSharp.Contrib.PageObjects
- Samples
- Unsafe
- Upgrading
- Attribution
PuppeteerSharp is a great library to automate the Chrome browser in .NET / C#.
Puppeteer Sharp Contributions consists of a few libraries that helps you write browser automation tests:
PuppeteerSharp.Contrib.ExtensionsPuppeteerSharp.Contrib.ShouldPuppeteerSharp.Contrib.PageObjects
These libraries contains extension methods to the Puppeteer Sharp API and they are test framework agnostic.
PuppeteerSharp.Contrib.Extensions is a library with convenient extension methods for writing browser tests with the Puppeteer Sharp API.
π README: PuppeteerSharp.Contrib.Extensions.md
PuppeteerSharp.Contrib.Should is a should assertion library for the Puppeteer Sharp API.
π README: PuppeteerSharp.Contrib.Should.md
PuppeteerSharp.Contrib.PageObjects is a library for writing browser tests using the page object pattern with the Puppeteer Sharp API.
π README: PuppeteerSharp.Contrib.PageObjects.md
Sample projects are located in the samples folder.
Examples are written with these test frameworks:
- Machine.Specifications
- MSTest
- NUnit
- SpecFlow
- Xunit
This is an example with NUnit and PuppeteerSharp.Contrib.Should:
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using PuppeteerSharp.Contrib.Extensions;
using PuppeteerSharp.Contrib.Should;
namespace PuppeteerSharp.Contrib.Sample
{
public class PuppeteerSharpRepoTests
{
private Browser Browser { get; set; }
[SetUp]
public async Task SetUp()
{
await new BrowserFetcher().DownloadAsync();
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
}
[TearDown]
public async Task TearDown()
{
await Browser.CloseAsync();
}
[Test]
public async Task Should_be_first_search_result_on_GitHub()
{
var page = await Browser.NewPageAsync();
await page.GoToAsync("https://github.com/");
var h1 = await page.QuerySelectorAsync("h1");
await h1.ShouldHaveContentAsync("Where the world builds software");
var input = await page.QuerySelectorAsync("input.header-search-input");
if (await input.IsHiddenAsync()) await page.ClickAsync(".octicon-three-bars");
await page.TypeAsync("input.header-search-input", "Puppeteer Sharp");
await page.Keyboard.PressAsync("Enter");
await page.WaitForNavigationAsync();
var repositories = await page.QuerySelectorAllAsync(".repo-list-item");
Assert.IsNotEmpty(repositories);
var repository = repositories.First();
await repository.ShouldHaveContentAsync("hardkoded/puppeteer-sharp");
var text = await repository.QuerySelectorAsync("p");
await text.ShouldHaveContentAsync("Headless Chrome .NET API");
var link = await repository.QuerySelectorAsync("a");
await link.ClickAsync();
await page.WaitForNavigationAsync();
h1 = await page.QuerySelectorAsync("article > h1");
await h1.ShouldHaveContentAsync("Puppeteer Sharp");
Assert.AreEqual("https://github.com/hardkoded/puppeteer-sharp", page.Url);
}
[Test]
public async Task Should_have_successful_build_status()
{
var page = await Browser.NewPageAsync();
await page.GoToAsync("https://github.com/hardkoded/puppeteer-sharp");
var build = await page.QuerySelectorAsync("img[alt='Build status']");
await build.ClickAsync();
await page.WaitForNavigationAsync(new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
var success = await page.QuerySelectorAsync(".project-build.project-build-status.success");
success.ShouldExist();
}
[Test]
public async Task Should_be_up_to_date_with_the_Puppeteer_version()
{
var page = await Browser.NewPageAsync();
await page.GoToAsync("https://github.com/hardkoded/puppeteer-sharp");
var puppeteerSharpVersion = await GetLatestReleaseVersion();
await page.GoToAsync("https://github.com/puppeteer/puppeteer");
var puppeteerVersion = await GetLatestReleaseVersion();
Assert.AreEqual(puppeteerVersion, puppeteerSharpVersion);
async Task<string> GetLatestReleaseVersion()
{
var latest = await page.QuerySelectorWithContentAsync("a[href*='releases'] span", @"v\d\.\d\.\d");
return await latest.TextContentAsync();
}
}
}
}This is an example with NUnit and PuppeteerSharp.Contrib.PageObjects:
using System.Threading.Tasks;
using PuppeteerSharp.Contrib.Extensions;
using PuppeteerSharp.Contrib.PageObjects;
using PuppeteerSharp.Input;
namespace PuppeteerSharp.Contrib.Sample
{
public class GitHubStartPage : PageObject
{
[Selector("h1")]
public virtual Task<ElementHandle> Heading { get; }
[Selector(".HeaderMenu")]
public virtual Task<GitHubHeaderMenu> HeaderMenu { get; }
}
public class GitHubHeaderMenu : ElementObject
{
[Selector("input.header-search-input")]
public virtual Task<ElementHandle> SearchInput { get; }
public async Task<GitHubSearchPage> Search(string text)
{
var input = await SearchInput;
if (await input.IsHiddenAsync()) await Page.ClickAsync(".octicon-three-bars");
await input.TypeAsync(text);
await input.PressAsync(Key.Enter);
return await Page.WaitForNavigationAsync<GitHubSearchPage>();
}
}
public class GitHubSearchPage : PageObject
{
[Selector(".repo-list-item")]
public virtual Task<GitHubRepoListItem[]> RepoListItems { get; }
}
public class GitHubRepoListItem : ElementObject
{
[Selector("a")]
public virtual Task<ElementHandle> Link { get; }
[Selector("p")]
public virtual Task<ElementHandle> Text { get; }
}
public class GitHubRepoPage : PageObject
{
[Selector("article > h1")]
public virtual Task<ElementHandle> Heading { get; }
[Selector("img[alt='Build status']")]
public virtual Task<ElementHandle> BuildStatusBadge { get; }
public async Task<string> GetLatestReleaseVersion()
{
var latest = await Page.QuerySelectorWithContentAsync("a[href*='releases'] span", @"v\d\.\d\.\d");
return await latest.TextContentAsync();
}
}
public class AppVeyorBuildPage : PageObject
{
public async Task<bool> Success()
{
var success = await Page.QuerySelectorAsync(".project-build.project-build-status.success");
return success.Exists();
}
}
}using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using PuppeteerSharp.Contrib.PageObjects;
using PuppeteerSharp.Contrib.Should;
namespace PuppeteerSharp.Contrib.Sample
{
public class PuppeteerSharpRepoPageObjectTests
{
private Browser Browser { get; set; }
[SetUp]
public async Task SetUp()
{
await new BrowserFetcher().DownloadAsync();
Browser = await Puppeteer.LaunchAsync(new LaunchOptions
{
Headless = true
});
}
[TearDown]
public async Task TearDown()
{
await Browser.CloseAsync();
}
[Test]
public async Task Should_be_first_search_result_on_GitHub()
{
var page = await Browser.NewPageAsync();
var startPage = await page.GoToAsync<GitHubStartPage>("https://github.com/");
var heading = await startPage.Heading;
await heading.ShouldHaveContentAsync("Where the world builds software");
var headerMenu = await startPage.HeaderMenu;
var searchPage = await headerMenu.Search("Puppeteer Sharp");
var repositories = await searchPage.RepoListItems;
Assert.IsNotEmpty(repositories);
var repository = repositories.First();
var text = await repository.Text;
await text.ShouldHaveContentAsync("Headless Chrome .NET API");
var link = await repository.Link;
await link.ShouldHaveContentAsync("hardkoded/puppeteer-sharp");
await link.ClickAsync();
var repoPage = await page.WaitForNavigationAsync<GitHubRepoPage>();
heading = await repoPage.Heading;
await heading.ShouldHaveContentAsync("Puppeteer Sharp");
Assert.AreEqual("https://github.com/hardkoded/puppeteer-sharp", repoPage.Page.Url);
}
[Test]
public async Task Should_have_successful_build_status()
{
var page = await Browser.NewPageAsync();
var repoPage = await page.GoToAsync<GitHubRepoPage>("https://github.com/hardkoded/puppeteer-sharp");
var badge = await repoPage.BuildStatusBadge;
await badge.ClickAsync();
var buildPage = await page.WaitForNavigationAsync<AppVeyorBuildPage>(new NavigationOptions { WaitUntil = new[] { WaitUntilNavigation.Networkidle0 } });
var success = await buildPage.Success();
Assert.True(success);
}
[Test]
public async Task Should_be_up_to_date_with_the_Puppeteer_version()
{
var page = await Browser.NewPageAsync();
var repoPage = await page.GoToAsync<GitHubRepoPage>("https://github.com/hardkoded/puppeteer-sharp");
var puppeteerSharpVersion = await repoPage.GetLatestReleaseVersion();
repoPage = await page.GoToAsync<GitHubRepoPage>("https://github.com/puppeteer/puppeteer");
var puppeteerVersion = await repoPage.GetLatestReleaseVersion();
Assert.AreEqual(puppeteerVersion, puppeteerSharpVersion);
}
}
}Two packages contains sync over async versions of the extension methods:
PuppeteerSharp.Contrib.Extensions.UnsafePuppeteerSharp.Contrib.Should.Unsafe
β These packages are no longer maintained and will not be published in new versions.
π README: PuppeteerSharp.Contrib.Extensions.Unsafe.md
π README: PuppeteerSharp.Contrib.Should.Unsafe.md
β¬οΈ Upgrading from version
1.0.0to2.0.0
If you use the sync methods from PuppeteerSharp.Contrib.Extensions or PuppeteerSharp.Contrib.Should, please install:
PuppeteerSharp.Contrib.Extensions.UnsafePuppeteerSharp.Contrib.Should.Unsafe
β¬οΈ Upgrading from version
2.0.0to4.0.0
If you use the sync methods from PuppeteerSharp.Contrib.Extensions.Unsafe or PuppeteerSharp.Contrib.Should.Unsafe, please rewrite your code to use async versions.
Then uninstall:
PuppeteerSharp.Contrib.Extensions.UnsafePuppeteerSharp.Contrib.Should.Unsafe
Puppeteer Sharp Contributions is standing on the shoulders of giants.
It would not exist without https://github.com/hardkoded/puppeteer-sharp and https://github.com/puppeteer/puppeteer
Inspiration and experience has been drawn from the previous usage of https://github.com/featurist/coypu and https://github.com/stirno/FluentAutomation