Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions src/NetInteractor.PuppeteerSharp/PuppeteerSharpWebAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Threading;
using System.Threading.Tasks;
using PuppeteerSharp;
using NetInteractor.Config;

namespace NetInteractor.WebAccessors
{
Expand Down Expand Up @@ -77,27 +78,61 @@ private async Task<IBrowser> GetBrowserAsync()
return _browser;
}

public async Task<ResponseInfo> GetAsync(string url)
public async Task<ResponseInfo> GetAsync(string url, InteractActionConfig config = null)
{
var browser = await GetBrowserAsync();
var page = await browser.NewPageAsync();

try
{
// Navigate to the URL and wait for network to be idle
// This will handle the initial page load and any immediate redirects
var response = await page.GoToAsync(url, new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
});

// Check if load delay is configured in options
var loadDelayStr = config?.Options?.FirstOrDefault(attr => attr.Name == "loadDelay")?.Value;
if (!string.IsNullOrEmpty(loadDelayStr) && int.TryParse(loadDelayStr, out var loadDelay))
{
// After the page loads, check if JavaScript might trigger a delayed redirect
// This handles cases like: setTimeout(() => window.location.href = '/other', 500)
// We race between a delay and a navigation wait
var delayTask = Task.Delay(loadDelay);
var navigationTask = page.WaitForNavigationAsync(new NavigationOptions
{
WaitUntil = new[] { WaitUntilNavigation.Networkidle0 }
});

var completedTask = await Task.WhenAny(delayTask, navigationTask);

if (completedTask == navigationTask)
{
// Navigation occurred - await it to get the response and handle any exceptions
try
{
response = await navigationTask;
}
catch (PuppeteerException)
{
// Navigation failed or was cancelled - use original response
}
}
// else: delay completed first, meaning no navigation occurred within timeout - use original response
// Note: The navigationTask will be cancelled when the page closes in the finally block
}

return await GetResultFromResponse(page, response);
}
finally
{
// Close the page. Any pending navigation will be cancelled.
await page.CloseAsync();
}
}

public async Task<ResponseInfo> PostAsync(string url, NameValueCollection formValues)
public async Task<ResponseInfo> PostAsync(string url, NameValueCollection formValues, InteractActionConfig config = null)
{
var browser = await GetBrowserAsync();
var page = await browser.NewPageAsync();
Expand Down
4 changes: 4 additions & 0 deletions src/NetInteractor/Config/InteractActionConfig.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;

namespace NetInteractor.Config
Expand All @@ -19,6 +20,9 @@ public abstract class InteractActionConfig : IInteractActionConfig
[XmlAttribute("expectedHttpStatusCodes")]
public string ExpectedHttpStatusCodes { get; set; }

[XmlAnyAttribute]
public XmlAttribute[] Options { get; set; }

public abstract IInteractAction GetAction();
}
}
5 changes: 3 additions & 2 deletions src/NetInteractor/IWebAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
using System.Collections.Specialized;
using System.Net;
using System.Threading.Tasks;
using NetInteractor.Config;

namespace NetInteractor
{
public interface IWebAccessor
{
Task<ResponseInfo> GetAsync(string url);
Task<ResponseInfo> GetAsync(string url, InteractActionConfig config = null);

Task<ResponseInfo> PostAsync(string url, NameValueCollection formValues);
Task<ResponseInfo> PostAsync(string url, NameValueCollection formValues, InteractActionConfig config = null);
}
}
2 changes: 1 addition & 1 deletion src/NetInteractor/Interacts/Get.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ protected override async Task<ResponseInfo> MakeRequest(InterationContext contex
{
var url = PrepareValue(context, Config.Url);
var webAccessor = context.WebAccessor;
return await webAccessor.GetAsync(url);
return await webAccessor.GetAsync(url, Config);
}
}
}
2 changes: 1 addition & 1 deletion src/NetInteractor/Interacts/Post.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ protected override async Task<ResponseInfo> MakeRequest(InterationContext contex
}
}

return await webAccessor.PostAsync(url, formValues);
return await webAccessor.PostAsync(url, formValues, Config);
}

private NameValueCollection MergeFormValues(InterationContext context, FormInfo form, FormValue[] formValues)
Expand Down
2 changes: 1 addition & 1 deletion src/NetInteractor/Interacts/WebInteractionBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ protected WebInteractionBase(TConfig config)
private Task<ResponseInfo> MakeRedirectRequest(InterationContext context, string redirectUrl)
{
var webAccessor = context.WebAccessor;
return webAccessor.GetAsync(redirectUrl);
return webAccessor.GetAsync(redirectUrl, Config);
}

private async Task<ResponseInfo> MakeRequestInternal(InterationContext context, string redirectUrl = null)
Expand Down
5 changes: 3 additions & 2 deletions src/NetInteractor/WebAccessors/HttpClientWebAccessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
using NetInteractor.Config;

namespace NetInteractor.WebAccessors
{
Expand Down Expand Up @@ -35,7 +36,7 @@ public HttpClientWebAccessor()
{
}

public virtual async Task<ResponseInfo> GetAsync(string url)
public virtual async Task<ResponseInfo> GetAsync(string url, InteractActionConfig config = null)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);

Expand All @@ -44,7 +45,7 @@ public virtual async Task<ResponseInfo> GetAsync(string url)
return await GetResultFromResponse(response);
}

public virtual async Task<ResponseInfo> PostAsync(string url, NameValueCollection formValues)
public virtual async Task<ResponseInfo> PostAsync(string url, NameValueCollection formValues, InteractActionConfig config = null)
{
var request = new HttpRequestMessage(HttpMethod.Post, url);

Expand Down
31 changes: 31 additions & 0 deletions test/NetInteractor.Test/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -393,5 +393,36 @@ public async Task TestRedirect_AfterPost_FollowsRedirect(IWebAccessor webAccesso
if (webAccessor is IDisposable disposable)
disposable.Dispose();
}

[Theory]
[ClassData(typeof(WebAccessorTestData))]
public async Task TestJavaScriptRedirect_FollowsRedirect(IWebAccessor webAccessor, string baseUrl)
{
// This test verifies that JavaScript redirects are handled properly when
// loadDelay option is configured in the config
// JavaScript redirects only work with PuppeteerSharp (browser automation)
// HttpClient cannot execute JavaScript, so we skip this test for HttpClient
if (webAccessor is HttpClientWebAccessor)
{
// Skip test for HttpClient - it cannot handle JavaScript redirects
return;
}

// Arrange
var executor = new InterationExecutor(webAccessor);
var config = LoadConfig("JavaScriptRedirectTest.config");
var inputs = new NameValueCollection { ["BaseUrl"] = baseUrl };

// Act - Should follow JavaScript redirect from /js-redirect-test to /products
var result = await executor.ExecuteAsync(config, inputs);

// Assert
Assert.True(result.Ok, result.Message);
Assert.Equal("Products", result.Outputs["title"]); // Should get products page after redirect

// Cleanup
if (webAccessor is IDisposable disposable)
disposable.Dispose();
}
}
}
158 changes: 0 additions & 158 deletions test/NetInteractor.Test/PuppeteerSharpWebAccessorTests.cs

This file was deleted.

7 changes: 7 additions & 0 deletions test/NetInteractor.Test/Scripts/JavaScriptRedirectTest.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<InteractConfig defaultTarget='Main'>
<target name='Main'>
<get url='$(BaseUrl)/js-redirect-test' loadDelay='1500'>
<output name='title' xpath='//h1' attr='text()' />
</get>
</target>
</InteractConfig>
16 changes: 16 additions & 0 deletions test/NetInteractor.Test/TestWebApp/Pages/js-redirect.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<title>JavaScript Redirect Test</title>
<script type="text/javascript">
// Redirect after a short delay to simulate real-world JavaScript redirect
setTimeout(function() {
window.location.href = '/products';
}, 500);
</script>
</head>
<body>
<h1>Redirecting...</h1>
<p>You should be redirected to the products page shortly.</p>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,12 @@ public TestWebApplicationFactory(ServerMode mode = ServerMode.TestServer)
await context.Response.WriteAsync(html);
});

// JavaScript redirect test endpoint
_app.MapGet("/js-redirect-test", async context =>
{
await context.Response.WriteAsync(LoadPage("js-redirect.html"));
});

_app.Start();

if (mode == ServerMode.Kestrel)
Expand Down