Thank you. Could not find a simple answer anywhere.
This helped me wrap Playwright into a builder pattern for async tasks.
using Microsoft.Playwright;
public class PlaywrightService
{
private readonly IPage _page;
private PlaywrightService(IPage page)
{
_page = page ?? throw new ArgumentNullException(nameof(page));
}
public static PlaywrightService Create(IPage page) => new PlaywrightService(page);
public PlaywrightServiceBuilder NewBuilder() => new PlaywrightServiceBuilder(_page);
public class PlaywrightServiceBuilder
{
private readonly IPage _page;
private readonly List<Func<Task>> _actions = new List<Func<Task>>();
public PlaywrightServiceBuilder(IPage page)
{
_page = page;
}
public PlaywrightServiceBuilder Navigate(string url)
{
// Wait for page load explicitly.
_actions.Add(() => _page.GotoAsync(url, new PageGotoOptions { WaitUntil = WaitUntilState.Load }));
return this;
}
public PlaywrightServiceBuilder ClickElement(string selector)
{
// Wait for the element to be available before clicking.
_actions.Add(() => _page.WaitForSelectorAsync(selector, new PageWaitForSelectorOptions { State = WaitForSelectorState.Visible }));
_actions.Add(() => _page.ClickAsync(selector));
return this;
}
// Deets
/*
It does not simulate typing, it *directly sets the value*.
It clears the field first before entering new text.
If you need to simulate real keystrokes, use ***_page.TypeAsync(selector, text)***.
If the input field is not visible or enabled, Playwright will throw an error. */
public PlaywrightServiceBuilder SetFieldValue(string selector, string text)
{
_actions.Add(() => _page.FillAsync(selector, text));
return this;
}
// As above - but as if it were actually TYPED MANUALLY
public PlaywrightServiceBuilder TypeFieldValue(string selector, string text)
{
_actions.Add(() => _page.Locator(selector).PressSequentiallyAsync(text));
return this;
}
public PlaywrightServiceBuilder GetText(string selector, Action<string> onTextReceived)
{
_actions.Add(async () =>
{
var text = await _page.InnerTextAsync(selector).ConfigureAwait(false);
onTextReceived(text);
});
return this;
}
// If element exists, run the action
public PlaywrightServiceBuilder IfElementExists(string selector, Func<Task> actionIfExists)
{
_actions.Add(async () =>
{
var element = await _page.QuerySelectorAsync(selector).ConfigureAwait(false);
if (element != null)
{
await actionIfExists().ConfigureAwait(false);
}
});
return this;
}
// If element exists with an alternative for the "else" case
public PlaywrightServiceBuilder IfElementExistsWithAlternative(string selector, Func<Task> actionIfExists, Func<Task> actionIfNotExists)
{
_actions.Add(async () =>
{
var element = await _page.QuerySelectorAsync(selector).ConfigureAwait(false);
if (element != null)
{
await actionIfExists().ConfigureAwait(false);
}
else
{
await actionIfNotExists().ConfigureAwait(false);
}
});
return this;
}
// Build and execute the accumulated tasks
public async Task ExecuteAsync()
{
foreach (var action in _actions)
{
await action().ConfigureAwait(false);
}
}
}
}
And its finally doing what I need.
await PlaywrightService.Create(page).NewBuilder()
.Navigate("https://google.com/")
.IfElementExistsWithAlternative(".gNO89b",
async () =>
{
// If #someElement exists, run this block and stop the outer chain
Console.WriteLine("Element exists! Running actions...");
},
async () =>
{
// If #someElement doesn't exist, run this block instead and continue
Console.WriteLine("Element does not exist! Continuing with other actions...");
})
.IfElementExists("#otherElement", async () =>
{
// Another conditional check for another element
Console.WriteLine("Running actions for another element...");
await page.ClickAsync("#otherElement");
})
.TypeFieldValue("textarea.gLFyf\n", "How duth I google this")
.ClickElement(".gNO89b") // This will execute only if the outer chain is not stopped
.ExecuteAsync();
Task<HomeViewModelBuilder>? So that you can chain it?Build()method? So that theWith...()only modifies the builder's state, memorizing that it needs to call the method.