Skip to content

Commit

Permalink
Migrate addScriptTag (#2183)
Browse files Browse the repository at this point in the history
* Migrate addScriptTag

* code factor
  • Loading branch information
kblok committed Apr 13, 2023
1 parent 5460ec3 commit 2cee48d
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 114 deletions.
18 changes: 15 additions & 3 deletions lib/PuppeteerSharp.Tests/PageTests/AddScriptTagTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ public async Task ShouldWorkWithAContentAndTypeModule()
public async Task ShouldThrowAnErrorIfLoadingFromUrlFail()
{
await Page.GoToAsync(TestConstants.EmptyPage);
var exception = await Assert.ThrowsAsync<PuppeteerException>(()
var exception = await Assert.ThrowsAnyAsync<PuppeteerException>(()
=> Page.AddScriptTagAsync(new AddTagOptions { Url = "/nonexistfile.js" }));
Assert.Equal("Loading script from /nonexistfile.js failed", exception.Message);
Assert.Contains("Could not load script", exception.Message);
}

[PuppeteerTest("page.spec.ts", "Page.addScriptTag", "should work with a path")]
Expand Down Expand Up @@ -117,6 +117,18 @@ public async Task ShouldWorkWithContent()
Assert.Equal(35, await Page.EvaluateExpressionAsync<int>("__injected"));
}

[PuppeteerTest("page.spec.ts", "Page.addScriptTag", "should add id when provided")]
[PuppeteerFact]
public async Task ShouldAddIdWhenProvided()
{
await Page.GoToAsync(TestConstants.EmptyPage);
await Page.AddScriptTagAsync(new AddTagOptions { Content = "window.__injected = 1;", Id= "one" });
await Page.AddScriptTagAsync(new AddTagOptions { Url = "/injectedfile.js", Id = "two" });

Assert.NotNull(await Page.QuerySelectorAsync("#one"));
Assert.NotNull(await Page.QuerySelectorAsync("#two"));
}

[PuppeteerTest("page.spec.ts", "Page.addScriptTag", "should throw when added with content to the CSP page")]
[PuppeteerFact(Skip = "@see https://github.com/GoogleChrome/puppeteer/issues/4840")]
public async Task ShouldThrowWhenAddedWithContentToTheCSPPage()
Expand All @@ -135,7 +147,7 @@ public async Task ShouldThrowWhenAddedWithContentToTheCSPPage()
public async Task ShouldThrowWhenAddedWithURLToTheCSPPage()
{
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
var exception = await Assert.ThrowsAsync<PuppeteerException>(
var exception = await Assert.ThrowsAnyAsync<PuppeteerException>(
() => Page.AddScriptTagAsync(new AddTagOptions
{
Url = TestConstants.CrossProcessUrl + "/injectedfile.js"
Expand Down
11 changes: 8 additions & 3 deletions lib/PuppeteerSharp/AddTagOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,19 @@ public class AddTagOptions
public string Path { get; set; }

/// <summary>
/// Raw JavaScript content to be injected into frame.
/// JavaScript to be injected into the frame.
/// </summary>
public string Content { get; set; }

/// <summary>
/// Script type. Use <c>module</c> in order to load a Javascript ES6 module.
/// Script type. Use <c>module</c> in order to load a Javascript ES2015 module.
/// </summary>
/// <seealso href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"/>
public string Type { get; set; }
public string Type { get; set; } = "text/javascript";

/// <summary>
/// Id of the script.
/// </summary>
public object Id { get; set; }
}
}
44 changes: 20 additions & 24 deletions lib/PuppeteerSharp/AriaQueryHandlerFactory.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Linq;
using PuppeteerSharp.Messaging;
using PuppeteerSharp.PageAccessibility;
using static PuppeteerSharp.Messaging.AccessibilityGetFullAXTreeResponse;

namespace PuppeteerSharp
Expand All @@ -22,69 +18,69 @@ internal class AriaQueryHandlerFactory

internal static InternalQueryHandler Create()
{
Func<IElementHandle, string, Task<object>> queryOneId = async (IElementHandle element, string selector) =>
async Task<object> QueryOneId(IElementHandle element, string selector)
{
var queryOptions = ParseAriaSelector(selector);
var res = await QueryAXTreeAsync(((ElementHandle)element).Client, element, queryOptions.Name, queryOptions.Role).ConfigureAwait(false);

return res.FirstOrDefault()?.BackendDOMNodeId;
};
}

Func<IElementHandle, string, Task<IElementHandle>> queryOne = async (IElementHandle element, string selector) =>
async Task<IElementHandle> QueryOne(IElementHandle element, string selector)
{
var id = await queryOneId(element, selector).ConfigureAwait(false);
var id = await QueryOneId(element, selector).ConfigureAwait(false);
if (id == null)
{
return null;
}

return await ((ElementHandle)element).Frame.SecondaryWorld.AdoptBackendNodeAsync(id).ConfigureAwait(false);
};
}

Func<IElementHandle, string, WaitForSelectorOptions, Task<IElementHandle>> waitFor = async (IElementHandle root, string selector, WaitForSelectorOptions options) =>
async Task<IElementHandle> WaitFor(IElementHandle root, string selector, WaitForSelectorOptions options)
{
var frame = ((ElementHandle)root).Frame;
var element = await frame.SecondaryWorld.AdoptHandleAsync(root).ConfigureAwait(false);
var element = (await frame.SecondaryWorld.AdoptHandleAsync(root).ConfigureAwait(false)) as IElementHandle;

Func<string, Task<IElementHandle>> func = (string selector) => queryOne(element, selector);
Task<IElementHandle> Func(string selector) => QueryOne(element, selector);

var binding = new PageBinding()
{
Name = "ariaQuerySelector",
Function = (Delegate)func,
Function = (Func<string, Task<IElementHandle>>)Func,
};

return await frame.SecondaryWorld.WaitForSelectorInPageAsync(
@"(_, selector) => globalThis.ariaQuerySelector(selector)",
selector,
options,
new[] { binding }).ConfigureAwait(false);
};
}

Func<IElementHandle, string, Task<IElementHandle[]>> queryAll = async (IElementHandle element, string selector) =>
async Task<IElementHandle[]> QueryAll(IElementHandle element, string selector)
{
var exeCtx = (ExecutionContext)element.ExecutionContext;
var world = exeCtx.World;
var ariaSelector = ParseAriaSelector(selector);
var res = await QueryAXTreeAsync(exeCtx.Client, element, ariaSelector.Name, ariaSelector.Role).ConfigureAwait(false);
var elements = await Task.WhenAll(res.Select(axNode => world.AdoptBackendNodeAsync(axNode.BackendDOMNodeId))).ConfigureAwait(false);
return elements.ToArray();
};
}

Func<IElementHandle, string, Task<IJSHandle>> queryAllArray = async (IElementHandle element, string selector) =>
async Task<IJSHandle> QueryAllArray(IElementHandle element, string selector)
{
var elementHandles = await queryAll(element, selector).ConfigureAwait(false);
var elementHandles = await QueryAll(element, selector).ConfigureAwait(false);
var exeCtx = element.ExecutionContext;
var jsHandle = await exeCtx.EvaluateFunctionHandleAsync("(...elements) => elements", elementHandles).ConfigureAwait(false);
return jsHandle;
};
}

return new()
{
QueryOne = queryOne,
WaitFor = waitFor,
QueryAll = queryAll,
QueryAllArray = queryAllArray,
QueryOne = QueryOne,
WaitFor = WaitFor,
QueryAll = QueryAll,
QueryAllArray = QueryAllArray,
};
}

Expand All @@ -104,7 +100,7 @@ private static async Task<IEnumerable<AXTreeNode>> QueryAXTreeAsync(CDPSession c

private static AriaQueryOption ParseAriaSelector(string selector)
{
string NormalizeValue(string value) => _normalizedRegex.Replace(value, " ").Trim();
static string NormalizeValue(string value) => _normalizedRegex.Replace(value, " ").Trim();

var knownAriaAttributes = new[] { "name", "role" };
AriaQueryOption queryOptions = new();
Expand Down
16 changes: 3 additions & 13 deletions lib/PuppeteerSharp/FirefoxTargetManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,7 @@ public void RemoveTargetInterceptor(CDPSession session, TargetInterceptor interc
{
_targetInterceptors.TryGetValue(session, out var interceptors);

if (interceptors != null)
{
interceptors.Remove(interceptor);
}
interceptors?.Remove(interceptor);
}

public async Task InitializeAsync()
Expand Down Expand Up @@ -158,15 +155,8 @@ private void OnTargetDestroyed(TargetDestroyedResponse e)
private void OnAttachedToTarget(object sender, TargetAttachedToTargetResponse e)
{
var parent = sender as ICDPConnection;
var parentSession = parent as CDPSession;
var targetInfo = e.TargetInfo;
var session = _connection.GetSession(e.SessionId);

if (session == null)
{
throw new PuppeteerException($"Session {e.SessionId} was not created.");
}

var session = _connection.GetSession(e.SessionId) ?? throw new PuppeteerException($"Session {e.SessionId} was not created.");
var existingTarget = _availableTargetsByTargetId.TryGetValue(targetInfo.TargetId, out var target);
session.MessageReceived += OnMessageReceived;

Expand All @@ -177,7 +167,7 @@ private void OnAttachedToTarget(object sender, TargetAttachedToTargetResponse e)
foreach (var interceptor in interceptors)
{
Target parentTarget = null;
if (parentSession != null && !_availableTargetsBySessionId.TryGetValue(parentSession.Id, out parentTarget))
if (parent is CDPSession parentSession && !_availableTargetsBySessionId.TryGetValue(parentSession.Id, out parentTarget))
{
throw new PuppeteerException("Parent session not found in attached targets");
}
Expand Down
61 changes: 55 additions & 6 deletions lib/PuppeteerSharp/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using PuppeteerSharp.Helpers;
using PuppeteerSharp.Input;

namespace PuppeteerSharp
Expand All @@ -21,10 +22,7 @@ internal Frame(FrameManager frameManager, Frame parentFrame, string frameId, CDP

LifecycleEvents = new List<string>();

if (parentFrame != null)
{
parentFrame.AddChildFrame(this);
}
parentFrame?.AddChildFrame(this);

UpdateClient(client);
}
Expand Down Expand Up @@ -192,14 +190,65 @@ public Task<IElementHandle> AddStyleTagAsync(AddTagOptions options)
}

/// <inheritdoc/>
public Task<IElementHandle> AddScriptTagAsync(AddTagOptions options)
public async Task<IElementHandle> AddScriptTagAsync(AddTagOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}

return MainWorld.AddScriptTagAsync(options);
if (string.IsNullOrEmpty(options.Url) && string.IsNullOrEmpty(options.Path) && string.IsNullOrEmpty(options.Content))
{
throw new ArgumentException("Provide options with a `Url`, `Path` or `Content` property");
}

var content = options.Content;

if (!string.IsNullOrEmpty(options.Path))
{
content = await AsyncFileHelper.ReadAllText(options.Path).ConfigureAwait(false);
content += "//# sourceURL=" + options.Path.Replace("\n", string.Empty);
}

var handle = await SecondaryWorld.EvaluateFunctionHandleAsync(
@"async (url, id, type, content) => {
const script = document.createElement('script');
script.type = type;
script.text = content;
if (url) {
script.src = url;
}
if (id) {
script.id = id;
}
let resolve;
const promise = new Promise((res, rej) => {
if (url) {
script.addEventListener('load', res, {once: true});
} else {
resolve = res;
}
script.addEventListener(
'error',
event => {
rej(event.message ?? 'Could not load script');
},
{once: true}
);
});
document.head.appendChild(script);
if (resolve) {
resolve();
}
await promise;
return script;
}",
options.Url,
options.Id,
options.Type,
content).ConfigureAwait(false);

return (await MainWorld.TransferHandleAsync(handle).ConfigureAwait(false)) as IElementHandle;
}

/// <inheritdoc/>
Expand Down
74 changes: 9 additions & 65 deletions lib/PuppeteerSharp/IsolatedWorld.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,16 @@ internal async Task<IElementHandle> AdoptBackendNodeAsync(object backendNodeId)
return executionContext.CreateJSHandle(obj.Object) as IElementHandle;
}

internal async Task<IElementHandle> AdoptHandleAsync(IElementHandle handle)
internal async Task<IJSHandle> TransferHandleAsync(IJSHandle handle)
{
var executionContext = await this.GetExecutionContextAsync().ConfigureAwait(false);
var result = await AdoptHandleAsync(handle).ConfigureAwait(false);
await handle.DisposeAsync().ConfigureAwait(false);
return result;
}

internal async Task<IJSHandle> AdoptHandleAsync(IJSHandle handle)
{
var executionContext = await GetExecutionContextAsync().ConfigureAwait(false);

if (executionContext == handle.ExecutionContext)
{
Expand Down Expand Up @@ -264,69 +271,6 @@ internal async Task SetContentAsync(string html, NavigationOptions options = nul
}
}

internal async Task<IElementHandle> AddScriptTagAsync(AddTagOptions options)
{
const string addScriptUrl = @"async function addScriptUrl(url, type) {
const script = document.createElement('script');
script.src = url;
if(type)
script.type = type;
const promise = new Promise((res, rej) => {
script.onload = res;
script.onerror = rej;
});
document.head.appendChild(script);
await promise;
return script;
}";
const string addScriptContent = @"function addScriptContent(content, type = 'text/javascript') {
const script = document.createElement('script');
script.type = type;
script.text = content;
let error = null;
script.onerror = e => error = e;
document.head.appendChild(script);
if (error)
throw error;
return script;
}";

async Task<IElementHandle> AddScriptTagPrivate(string script, string urlOrContent, string type)
{
var context = await GetExecutionContextAsync().ConfigureAwait(false);
return (string.IsNullOrEmpty(type)
? await context.EvaluateFunctionHandleAsync(script, urlOrContent).ConfigureAwait(false)
: await context.EvaluateFunctionHandleAsync(script, urlOrContent, type).ConfigureAwait(false)) as IElementHandle;
}

if (!string.IsNullOrEmpty(options.Url))
{
var url = options.Url;
try
{
return await AddScriptTagPrivate(addScriptUrl, url, options.Type).ConfigureAwait(false);
}
catch (PuppeteerException)
{
throw new PuppeteerException($"Loading script from {url} failed");
}
}

if (!string.IsNullOrEmpty(options.Path))
{
var contents = await AsyncFileHelper.ReadAllText(options.Path).ConfigureAwait(false);
contents += "//# sourceURL=" + options.Path.Replace("\n", string.Empty);
return await AddScriptTagPrivate(addScriptContent, contents, options.Type).ConfigureAwait(false);
}

if (!string.IsNullOrEmpty(options.Content))
{
return await AddScriptTagPrivate(addScriptContent, options.Content, options.Type).ConfigureAwait(false);
}

throw new ArgumentException("Provide options with a `Url`, `Path` or `Content` property");
}

internal async Task<IElementHandle> WaitForSelectorInPageAsync(string queryOne, string selector, WaitForSelectorOptions options, PageBinding[] bindings = null)
{
var waitForVisible = options?.Visible ?? false;
Expand Down

0 comments on commit 2cee48d

Please sign in to comment.