Skip to content

Commit 2cee48d

Browse files
authored
Migrate addScriptTag (#2183)
* Migrate addScriptTag * code factor
1 parent 5460ec3 commit 2cee48d

File tree

6 files changed

+110
-114
lines changed

6 files changed

+110
-114
lines changed

lib/PuppeteerSharp.Tests/PageTests/AddScriptTagTests.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ await Page.AddScriptTagAsync(new AddTagOptions
7676
public async Task ShouldThrowAnErrorIfLoadingFromUrlFail()
7777
{
7878
await Page.GoToAsync(TestConstants.EmptyPage);
79-
var exception = await Assert.ThrowsAsync<PuppeteerException>(()
79+
var exception = await Assert.ThrowsAnyAsync<PuppeteerException>(()
8080
=> Page.AddScriptTagAsync(new AddTagOptions { Url = "/nonexistfile.js" }));
81-
Assert.Equal("Loading script from /nonexistfile.js failed", exception.Message);
81+
Assert.Contains("Could not load script", exception.Message);
8282
}
8383

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

120+
[PuppeteerTest("page.spec.ts", "Page.addScriptTag", "should add id when provided")]
121+
[PuppeteerFact]
122+
public async Task ShouldAddIdWhenProvided()
123+
{
124+
await Page.GoToAsync(TestConstants.EmptyPage);
125+
await Page.AddScriptTagAsync(new AddTagOptions { Content = "window.__injected = 1;", Id= "one" });
126+
await Page.AddScriptTagAsync(new AddTagOptions { Url = "/injectedfile.js", Id = "two" });
127+
128+
Assert.NotNull(await Page.QuerySelectorAsync("#one"));
129+
Assert.NotNull(await Page.QuerySelectorAsync("#two"));
130+
}
131+
120132
[PuppeteerTest("page.spec.ts", "Page.addScriptTag", "should throw when added with content to the CSP page")]
121133
[PuppeteerFact(Skip = "@see https://github.com/GoogleChrome/puppeteer/issues/4840")]
122134
public async Task ShouldThrowWhenAddedWithContentToTheCSPPage()
@@ -135,7 +147,7 @@ public async Task ShouldThrowWhenAddedWithContentToTheCSPPage()
135147
public async Task ShouldThrowWhenAddedWithURLToTheCSPPage()
136148
{
137149
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
138-
var exception = await Assert.ThrowsAsync<PuppeteerException>(
150+
var exception = await Assert.ThrowsAnyAsync<PuppeteerException>(
139151
() => Page.AddScriptTagAsync(new AddTagOptions
140152
{
141153
Url = TestConstants.CrossProcessUrl + "/injectedfile.js"

lib/PuppeteerSharp/AddTagOptions.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,19 @@ public class AddTagOptions
1616
public string Path { get; set; }
1717

1818
/// <summary>
19-
/// Raw JavaScript content to be injected into frame.
19+
/// JavaScript to be injected into the frame.
2020
/// </summary>
2121
public string Content { get; set; }
2222

2323
/// <summary>
24-
/// Script type. Use <c>module</c> in order to load a Javascript ES6 module.
24+
/// Script type. Use <c>module</c> in order to load a Javascript ES2015 module.
2525
/// </summary>
2626
/// <seealso href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script"/>
27-
public string Type { get; set; }
27+
public string Type { get; set; } = "text/javascript";
28+
29+
/// <summary>
30+
/// Id of the script.
31+
/// </summary>
32+
public object Id { get; set; }
2833
}
2934
}

lib/PuppeteerSharp/AriaQueryHandlerFactory.cs

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Data;
43
using System.Linq;
5-
using System.Runtime.InteropServices;
64
using System.Text.RegularExpressions;
75
using System.Threading.Tasks;
8-
using System.Xml.Linq;
96
using PuppeteerSharp.Messaging;
10-
using PuppeteerSharp.PageAccessibility;
117
using static PuppeteerSharp.Messaging.AccessibilityGetFullAXTreeResponse;
128

139
namespace PuppeteerSharp
@@ -22,69 +18,69 @@ internal class AriaQueryHandlerFactory
2218

2319
internal static InternalQueryHandler Create()
2420
{
25-
Func<IElementHandle, string, Task<object>> queryOneId = async (IElementHandle element, string selector) =>
21+
async Task<object> QueryOneId(IElementHandle element, string selector)
2622
{
2723
var queryOptions = ParseAriaSelector(selector);
2824
var res = await QueryAXTreeAsync(((ElementHandle)element).Client, element, queryOptions.Name, queryOptions.Role).ConfigureAwait(false);
2925

3026
return res.FirstOrDefault()?.BackendDOMNodeId;
31-
};
27+
}
3228

33-
Func<IElementHandle, string, Task<IElementHandle>> queryOne = async (IElementHandle element, string selector) =>
29+
async Task<IElementHandle> QueryOne(IElementHandle element, string selector)
3430
{
35-
var id = await queryOneId(element, selector).ConfigureAwait(false);
31+
var id = await QueryOneId(element, selector).ConfigureAwait(false);
3632
if (id == null)
3733
{
3834
return null;
3935
}
4036

4137
return await ((ElementHandle)element).Frame.SecondaryWorld.AdoptBackendNodeAsync(id).ConfigureAwait(false);
42-
};
38+
}
4339

44-
Func<IElementHandle, string, WaitForSelectorOptions, Task<IElementHandle>> waitFor = async (IElementHandle root, string selector, WaitForSelectorOptions options) =>
40+
async Task<IElementHandle> WaitFor(IElementHandle root, string selector, WaitForSelectorOptions options)
4541
{
4642
var frame = ((ElementHandle)root).Frame;
47-
var element = await frame.SecondaryWorld.AdoptHandleAsync(root).ConfigureAwait(false);
43+
var element = (await frame.SecondaryWorld.AdoptHandleAsync(root).ConfigureAwait(false)) as IElementHandle;
4844

49-
Func<string, Task<IElementHandle>> func = (string selector) => queryOne(element, selector);
45+
Task<IElementHandle> Func(string selector) => QueryOne(element, selector);
5046

5147
var binding = new PageBinding()
5248
{
5349
Name = "ariaQuerySelector",
54-
Function = (Delegate)func,
50+
Function = (Func<string, Task<IElementHandle>>)Func,
5551
};
5652

5753
return await frame.SecondaryWorld.WaitForSelectorInPageAsync(
5854
@"(_, selector) => globalThis.ariaQuerySelector(selector)",
5955
selector,
6056
options,
6157
new[] { binding }).ConfigureAwait(false);
62-
};
58+
}
6359

64-
Func<IElementHandle, string, Task<IElementHandle[]>> queryAll = async (IElementHandle element, string selector) =>
60+
async Task<IElementHandle[]> QueryAll(IElementHandle element, string selector)
6561
{
6662
var exeCtx = (ExecutionContext)element.ExecutionContext;
6763
var world = exeCtx.World;
6864
var ariaSelector = ParseAriaSelector(selector);
6965
var res = await QueryAXTreeAsync(exeCtx.Client, element, ariaSelector.Name, ariaSelector.Role).ConfigureAwait(false);
7066
var elements = await Task.WhenAll(res.Select(axNode => world.AdoptBackendNodeAsync(axNode.BackendDOMNodeId))).ConfigureAwait(false);
7167
return elements.ToArray();
72-
};
68+
}
7369

74-
Func<IElementHandle, string, Task<IJSHandle>> queryAllArray = async (IElementHandle element, string selector) =>
70+
async Task<IJSHandle> QueryAllArray(IElementHandle element, string selector)
7571
{
76-
var elementHandles = await queryAll(element, selector).ConfigureAwait(false);
72+
var elementHandles = await QueryAll(element, selector).ConfigureAwait(false);
7773
var exeCtx = element.ExecutionContext;
7874
var jsHandle = await exeCtx.EvaluateFunctionHandleAsync("(...elements) => elements", elementHandles).ConfigureAwait(false);
7975
return jsHandle;
80-
};
76+
}
8177

8278
return new()
8379
{
84-
QueryOne = queryOne,
85-
WaitFor = waitFor,
86-
QueryAll = queryAll,
87-
QueryAllArray = queryAllArray,
80+
QueryOne = QueryOne,
81+
WaitFor = WaitFor,
82+
QueryAll = QueryAll,
83+
QueryAllArray = QueryAllArray,
8884
};
8985
}
9086

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

105101
private static AriaQueryOption ParseAriaSelector(string selector)
106102
{
107-
string NormalizeValue(string value) => _normalizedRegex.Replace(value, " ").Trim();
103+
static string NormalizeValue(string value) => _normalizedRegex.Replace(value, " ").Trim();
108104

109105
var knownAriaAttributes = new[] { "name", "role" };
110106
AriaQueryOption queryOptions = new();

lib/PuppeteerSharp/FirefoxTargetManager.cs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@ public void RemoveTargetInterceptor(CDPSession session, TargetInterceptor interc
6464
{
6565
_targetInterceptors.TryGetValue(session, out var interceptors);
6666

67-
if (interceptors != null)
68-
{
69-
interceptors.Remove(interceptor);
70-
}
67+
interceptors?.Remove(interceptor);
7168
}
7269

7370
public async Task InitializeAsync()
@@ -158,15 +155,8 @@ private void OnTargetDestroyed(TargetDestroyedResponse e)
158155
private void OnAttachedToTarget(object sender, TargetAttachedToTargetResponse e)
159156
{
160157
var parent = sender as ICDPConnection;
161-
var parentSession = parent as CDPSession;
162158
var targetInfo = e.TargetInfo;
163-
var session = _connection.GetSession(e.SessionId);
164-
165-
if (session == null)
166-
{
167-
throw new PuppeteerException($"Session {e.SessionId} was not created.");
168-
}
169-
159+
var session = _connection.GetSession(e.SessionId) ?? throw new PuppeteerException($"Session {e.SessionId} was not created.");
170160
var existingTarget = _availableTargetsByTargetId.TryGetValue(targetInfo.TargetId, out var target);
171161
session.MessageReceived += OnMessageReceived;
172162

@@ -177,7 +167,7 @@ private void OnAttachedToTarget(object sender, TargetAttachedToTargetResponse e)
177167
foreach (var interceptor in interceptors)
178168
{
179169
Target parentTarget = null;
180-
if (parentSession != null && !_availableTargetsBySessionId.TryGetValue(parentSession.Id, out parentTarget))
170+
if (parent is CDPSession parentSession && !_availableTargetsBySessionId.TryGetValue(parentSession.Id, out parentTarget))
181171
{
182172
throw new PuppeteerException("Parent session not found in attached targets");
183173
}

lib/PuppeteerSharp/Frame.cs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Threading.Tasks;
55
using Newtonsoft.Json.Linq;
6+
using PuppeteerSharp.Helpers;
67
using PuppeteerSharp.Input;
78

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

2223
LifecycleEvents = new List<string>();
2324

24-
if (parentFrame != null)
25-
{
26-
parentFrame.AddChildFrame(this);
27-
}
25+
parentFrame?.AddChildFrame(this);
2826

2927
UpdateClient(client);
3028
}
@@ -192,14 +190,65 @@ public Task<IElementHandle> AddStyleTagAsync(AddTagOptions options)
192190
}
193191

194192
/// <inheritdoc/>
195-
public Task<IElementHandle> AddScriptTagAsync(AddTagOptions options)
193+
public async Task<IElementHandle> AddScriptTagAsync(AddTagOptions options)
196194
{
197195
if (options == null)
198196
{
199197
throw new ArgumentNullException(nameof(options));
200198
}
201199

202-
return MainWorld.AddScriptTagAsync(options);
200+
if (string.IsNullOrEmpty(options.Url) && string.IsNullOrEmpty(options.Path) && string.IsNullOrEmpty(options.Content))
201+
{
202+
throw new ArgumentException("Provide options with a `Url`, `Path` or `Content` property");
203+
}
204+
205+
var content = options.Content;
206+
207+
if (!string.IsNullOrEmpty(options.Path))
208+
{
209+
content = await AsyncFileHelper.ReadAllText(options.Path).ConfigureAwait(false);
210+
content += "//# sourceURL=" + options.Path.Replace("\n", string.Empty);
211+
}
212+
213+
var handle = await SecondaryWorld.EvaluateFunctionHandleAsync(
214+
@"async (url, id, type, content) => {
215+
const script = document.createElement('script');
216+
script.type = type;
217+
script.text = content;
218+
if (url) {
219+
script.src = url;
220+
}
221+
if (id) {
222+
script.id = id;
223+
}
224+
let resolve;
225+
const promise = new Promise((res, rej) => {
226+
if (url) {
227+
script.addEventListener('load', res, {once: true});
228+
} else {
229+
resolve = res;
230+
}
231+
script.addEventListener(
232+
'error',
233+
event => {
234+
rej(event.message ?? 'Could not load script');
235+
},
236+
{once: true}
237+
);
238+
});
239+
document.head.appendChild(script);
240+
if (resolve) {
241+
resolve();
242+
}
243+
await promise;
244+
return script;
245+
}",
246+
options.Url,
247+
options.Id,
248+
options.Type,
249+
content).ConfigureAwait(false);
250+
251+
return (await MainWorld.TransferHandleAsync(handle).ConfigureAwait(false)) as IElementHandle;
203252
}
204253

205254
/// <inheritdoc/>

lib/PuppeteerSharp/IsolatedWorld.cs

Lines changed: 9 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,16 @@ internal async Task<IElementHandle> AdoptBackendNodeAsync(object backendNodeId)
134134
return executionContext.CreateJSHandle(obj.Object) as IElementHandle;
135135
}
136136

137-
internal async Task<IElementHandle> AdoptHandleAsync(IElementHandle handle)
137+
internal async Task<IJSHandle> TransferHandleAsync(IJSHandle handle)
138138
{
139-
var executionContext = await this.GetExecutionContextAsync().ConfigureAwait(false);
139+
var result = await AdoptHandleAsync(handle).ConfigureAwait(false);
140+
await handle.DisposeAsync().ConfigureAwait(false);
141+
return result;
142+
}
143+
144+
internal async Task<IJSHandle> AdoptHandleAsync(IJSHandle handle)
145+
{
146+
var executionContext = await GetExecutionContextAsync().ConfigureAwait(false);
140147

141148
if (executionContext == handle.ExecutionContext)
142149
{
@@ -264,69 +271,6 @@ await EvaluateFunctionAsync(
264271
}
265272
}
266273

267-
internal async Task<IElementHandle> AddScriptTagAsync(AddTagOptions options)
268-
{
269-
const string addScriptUrl = @"async function addScriptUrl(url, type) {
270-
const script = document.createElement('script');
271-
script.src = url;
272-
if(type)
273-
script.type = type;
274-
const promise = new Promise((res, rej) => {
275-
script.onload = res;
276-
script.onerror = rej;
277-
});
278-
document.head.appendChild(script);
279-
await promise;
280-
return script;
281-
}";
282-
const string addScriptContent = @"function addScriptContent(content, type = 'text/javascript') {
283-
const script = document.createElement('script');
284-
script.type = type;
285-
script.text = content;
286-
let error = null;
287-
script.onerror = e => error = e;
288-
document.head.appendChild(script);
289-
if (error)
290-
throw error;
291-
return script;
292-
}";
293-
294-
async Task<IElementHandle> AddScriptTagPrivate(string script, string urlOrContent, string type)
295-
{
296-
var context = await GetExecutionContextAsync().ConfigureAwait(false);
297-
return (string.IsNullOrEmpty(type)
298-
? await context.EvaluateFunctionHandleAsync(script, urlOrContent).ConfigureAwait(false)
299-
: await context.EvaluateFunctionHandleAsync(script, urlOrContent, type).ConfigureAwait(false)) as IElementHandle;
300-
}
301-
302-
if (!string.IsNullOrEmpty(options.Url))
303-
{
304-
var url = options.Url;
305-
try
306-
{
307-
return await AddScriptTagPrivate(addScriptUrl, url, options.Type).ConfigureAwait(false);
308-
}
309-
catch (PuppeteerException)
310-
{
311-
throw new PuppeteerException($"Loading script from {url} failed");
312-
}
313-
}
314-
315-
if (!string.IsNullOrEmpty(options.Path))
316-
{
317-
var contents = await AsyncFileHelper.ReadAllText(options.Path).ConfigureAwait(false);
318-
contents += "//# sourceURL=" + options.Path.Replace("\n", string.Empty);
319-
return await AddScriptTagPrivate(addScriptContent, contents, options.Type).ConfigureAwait(false);
320-
}
321-
322-
if (!string.IsNullOrEmpty(options.Content))
323-
{
324-
return await AddScriptTagPrivate(addScriptContent, options.Content, options.Type).ConfigureAwait(false);
325-
}
326-
327-
throw new ArgumentException("Provide options with a `Url`, `Path` or `Content` property");
328-
}
329-
330274
internal async Task<IElementHandle> WaitForSelectorInPageAsync(string queryOne, string selector, WaitForSelectorOptions options, PageBinding[] bindings = null)
331275
{
332276
var waitForVisible = options?.Visible ?? false;

0 commit comments

Comments
 (0)