feat(news): add scraper adapters, article deduplication, blocklist service, and remove tracked publish artifacts
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for Ariva.de.
|
||||
/// </summary>
|
||||
public class ArivaScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "ariva.de";
|
||||
|
||||
public override string ArticleBodySelector => "#news_teaser, div.readable, div.column.twothirds";
|
||||
|
||||
public override async Task<ScrapedArticleResult?> ExtractArticleContentAsync(IPage page)
|
||||
{
|
||||
// Remove forum widgets, ad containers, and comments before extraction
|
||||
await page.EvaluateAsync(@"() => {
|
||||
const el = document.querySelectorAll('#forum_box, #basics-indices-modal, #nativendo-article-desktop, .action-buttons, .summary');
|
||||
el.forEach(e => e.remove());
|
||||
}");
|
||||
|
||||
var titleLocator = page.Locator("#news_title").First;
|
||||
string title = string.Empty;
|
||||
if (await titleLocator.CountAsync() > 0)
|
||||
{
|
||||
title = CleanTitle(await titleLocator.InnerTextAsync());
|
||||
}
|
||||
|
||||
var teaserLocator = page.Locator("#news_teaser").First;
|
||||
if (await teaserLocator.CountAsync() > 0)
|
||||
{
|
||||
var text = await teaserLocator.InnerTextAsync();
|
||||
var cleaned = TruncateDisclaimers(text.Trim());
|
||||
|
||||
return new ScrapedArticleResult(
|
||||
Title: !string.IsNullOrWhiteSpace(title) ? title : CleanTitle(await page.TitleAsync()),
|
||||
TextContent: cleaned,
|
||||
HtmlContent: await teaserLocator.InnerHTMLAsync(),
|
||||
Author: "ARIVA.DE",
|
||||
Excerpt: cleaned.Length > 250 ? cleaned[..250] + "..." : cleaned,
|
||||
FinalUrl: page.Url
|
||||
);
|
||||
}
|
||||
|
||||
return await base.ExtractArticleContentAsync(page);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Record holding structured article content extracted via Mozilla Readability.
|
||||
/// Record holding structured article content extracted from a news webpage.
|
||||
/// </summary>
|
||||
public record ScrapedArticleResult(
|
||||
string Title,
|
||||
@@ -14,11 +15,13 @@ public record ScrapedArticleResult(
|
||||
string HtmlContent,
|
||||
string? Author,
|
||||
string? Excerpt,
|
||||
string FinalUrl
|
||||
string FinalUrl,
|
||||
DateTime? PublishedAt = null,
|
||||
string? Language = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base class representing a website-specific scraping adapter.
|
||||
/// Abstract base class representing a website-specific scraping adapter with heuristic extraction and DOM cleanup.
|
||||
/// </summary>
|
||||
public abstract class ArticleScraperAdapter
|
||||
{
|
||||
@@ -33,9 +36,17 @@ public abstract class ArticleScraperAdapter
|
||||
public virtual string? ReadMoreSelector => null;
|
||||
|
||||
/// <summary>
|
||||
/// Fallback CSS selector targeting the article body text elements if Readability fails.
|
||||
/// Fallback CSS selector targeting the article body text elements if Readability/JSON-LD fails.
|
||||
/// </summary>
|
||||
public virtual string ArticleBodySelector => "body";
|
||||
public virtual string ArticleBodySelector => "article, main, div.article-content, div.article-text, div.entry-content, body";
|
||||
|
||||
private static readonly Regex RegulatoryDisclaimerRegex = new(
|
||||
@"(Interessenkonflikt[e:]|Offenlegung nach §|Risikohinweis:|Disclaimer:|Hinweis auf Interessenkonflikte|Keine Anlageberatung).*$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
|
||||
private static readonly Regex TitlePortalSuffixRegex = new(
|
||||
@"\s*[-|–—]\s*(DER AKTIONÄR|ARIVA\.DE|IT-Times|onvista|wallstreet:online|Sharedeals\.de|boerse\.de|Handelsblatt|WirtschaftsWoche|finanzen\.net|Finanznachrichten|XTB|Lynx|T3n|ntg24|Moneycab).*$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to resolve 'Read More' links or external redirects.
|
||||
@@ -50,7 +61,6 @@ public abstract class ArticleScraperAdapter
|
||||
{
|
||||
try
|
||||
{
|
||||
// Playwright modern pattern for combined popup or navigation handling
|
||||
var popupTask = page.Context.WaitForPageAsync(new() { Timeout = 6000 });
|
||||
var navTask = page.WaitForURLAsync(url => url != page.Url, new() { Timeout = 6000 });
|
||||
|
||||
@@ -69,7 +79,7 @@ public abstract class ArticleScraperAdapter
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Timeout / Click failed -> Fallback to current URL
|
||||
// Fallback to current URL if click/navigation times out
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,21 +87,119 @@ public abstract class ArticleScraperAdapter
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Injects Mozilla's Readability.js into the Playwright page to parse article content cleanly.
|
||||
/// Extracts structured article content using JSON-LD metadata, DOM noise pruning, and Mozilla Readability.
|
||||
/// </summary>
|
||||
public virtual async Task<ScrapedArticleResult?> ExtractArticleContentAsync(IPage page)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. Inject Mozilla Readability Standalone JS Bundle via CDN
|
||||
// 1. Remove DOM noise and ads before extraction
|
||||
await page.EvaluateAsync(@"() => {
|
||||
const noiseSelectors = [
|
||||
'nav', 'header', 'footer', 'aside', '.cookie-banner', '#consent',
|
||||
'[id*=""ad-""], [class*=""banner""], [class*=""outbrain""], [class*=""taboola""]',
|
||||
'[class*=""share-buttons""], .social-media, .related-articles, .comments',
|
||||
'#forum_box', '.newsletter-box', '.audio-player', '.tags-container'
|
||||
];
|
||||
for (const sel of noiseSelectors) {
|
||||
document.querySelectorAll(sel).forEach(el => el.remove());
|
||||
}
|
||||
}");
|
||||
|
||||
// 2. Extract JSON-LD (Schema.org) if available
|
||||
var jsonLdResult = await page.EvaluateAsync<JsonElement?>(@"() => {
|
||||
const scripts = document.querySelectorAll('script[type=""application/ld+json""]');
|
||||
for (const script of scripts) {
|
||||
try {
|
||||
const data = JSON.parse(script.textContent);
|
||||
const items = Array.isArray(data) ? data : (data['@graph'] || [data]);
|
||||
for (const item of items) {
|
||||
if (item['@type'] === 'NewsArticle' || item['@type'] === 'Article' || item['@type'] === 'Report') {
|
||||
return {
|
||||
headline: item.headline || null,
|
||||
description: item.description || null,
|
||||
articleBody: item.articleBody || null,
|
||||
author: typeof item.author === 'object' ? item.author.name : item.author,
|
||||
datePublished: item.datePublished || item.dateCreated || null
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return null;
|
||||
}");
|
||||
|
||||
string? title = null;
|
||||
string? excerpt = null;
|
||||
string? author = null;
|
||||
DateTime? publishedAt = null;
|
||||
|
||||
if (jsonLdResult.HasValue && jsonLdResult.Value.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
var root = jsonLdResult.Value;
|
||||
if (root.TryGetProperty("headline", out var hl) && hl.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
title = CleanTitle(hl.GetString());
|
||||
}
|
||||
if (root.TryGetProperty("description", out var desc) && desc.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
excerpt = desc.GetString();
|
||||
}
|
||||
if (root.TryGetProperty("author", out var auth) && auth.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
author = auth.GetString();
|
||||
}
|
||||
if (root.TryGetProperty("datePublished", out var dp) && dp.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
if (DateTime.TryParse(dp.GetString(), out var dt)) publishedAt = dt;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fallback to OpenGraph and Meta tags for missing metadata
|
||||
if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(excerpt))
|
||||
{
|
||||
var metaResult = await page.EvaluateAsync<JsonElement?>(@"() => {
|
||||
const getMeta = (prop) => {
|
||||
const el = document.querySelector(`meta[property=""${prop}""], meta[name=""${prop}""]`);
|
||||
return el ? el.getAttribute('content') : null;
|
||||
};
|
||||
return {
|
||||
ogTitle: getMeta('og:title') || getMeta('twitter:title'),
|
||||
ogDesc: getMeta('og:description') || getMeta('description') || getMeta('twitter:description'),
|
||||
author: getMeta('author') || getMeta('article:author'),
|
||||
pubDate: getMeta('article:published_time') || getMeta('date')
|
||||
};
|
||||
}");
|
||||
|
||||
if (metaResult.HasValue && metaResult.Value.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
var m = metaResult.Value;
|
||||
if (string.IsNullOrWhiteSpace(title) && m.TryGetProperty("ogTitle", out var ogT) && ogT.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
title = CleanTitle(ogT.GetString());
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(excerpt) && m.TryGetProperty("ogDesc", out var ogD) && ogD.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
excerpt = ogD.GetString();
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(author) && m.TryGetProperty("author", out var ogA) && ogA.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
author = ogA.GetString();
|
||||
}
|
||||
if (!publishedAt.HasValue && m.TryGetProperty("pubDate", out var ogP) && ogP.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
if (DateTime.TryParse(ogP.GetString(), out var dt)) publishedAt = dt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Inject Mozilla Readability to extract main content
|
||||
await page.AddScriptTagAsync(new PageAddScriptTagOptions
|
||||
{
|
||||
Url = "https://cdn.jsdelivr.net/npm/@mozilla/readability@0.5.0/Readability.min.js"
|
||||
});
|
||||
|
||||
// 2. Execute Readability in the Browser Context
|
||||
var jsScript = @"
|
||||
() => {
|
||||
var readabilityResult = await page.EvaluateAsync<JsonElement?>(@"() => {
|
||||
if (typeof Readability === 'undefined') return null;
|
||||
const documentClone = document.cloneNode(true);
|
||||
const article = new Readability(documentClone).parse();
|
||||
@@ -104,37 +212,61 @@ public abstract class ArticleScraperAdapter
|
||||
author: article.byline || null,
|
||||
excerpt: article.excerpt || null
|
||||
};
|
||||
}";
|
||||
}");
|
||||
|
||||
var jsonResult = await page.EvaluateAsync<JsonElement?>(jsScript);
|
||||
|
||||
if (jsonResult.HasValue && jsonResult.Value.ValueKind != JsonValueKind.Null)
|
||||
if (readabilityResult.HasValue && readabilityResult.Value.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
var root = jsonResult.Value;
|
||||
var r = readabilityResult.Value;
|
||||
var rawText = r.GetProperty("textContent").GetString()?.Trim() ?? string.Empty;
|
||||
var cleanedText = TruncateDisclaimers(rawText);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
title = CleanTitle(r.GetProperty("title").GetString() ?? await page.TitleAsync());
|
||||
}
|
||||
|
||||
return new ScrapedArticleResult(
|
||||
Title: root.GetProperty("title").GetString() ?? page.TitleAsync().Result,
|
||||
TextContent: root.GetProperty("textContent").GetString()?.Trim() ?? string.Empty,
|
||||
HtmlContent: root.GetProperty("htmlContent").GetString() ?? string.Empty,
|
||||
Author: root.GetProperty("author").GetString(),
|
||||
Excerpt: root.GetProperty("excerpt").GetString(),
|
||||
FinalUrl: page.Url
|
||||
Title: title ?? string.Empty,
|
||||
TextContent: cleanedText,
|
||||
HtmlContent: r.GetProperty("htmlContent").GetString() ?? string.Empty,
|
||||
Author: author ?? r.GetProperty("author").GetString(),
|
||||
Excerpt: excerpt ?? r.GetProperty("excerpt").GetString(),
|
||||
FinalUrl: page.Url,
|
||||
PublishedAt: publishedAt
|
||||
);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Readability Injection / Parsing failed -> Fallback to standard selector parsing
|
||||
// Readability/Script injection failed -> Fallback to selector parsing
|
||||
}
|
||||
|
||||
// Fallback: Manuelles Auslesen via ArticleBodySelector
|
||||
var bodyText = await page.Locator(ArticleBodySelector).InnerTextAsync();
|
||||
// 5. Selector-based fallback extraction
|
||||
var bodyLocator = page.Locator(ArticleBodySelector).First;
|
||||
var bodyText = await bodyLocator.CountAsync() > 0 ? await bodyLocator.InnerTextAsync() : await page.InnerTextAsync("body");
|
||||
var fallbackCleaned = TruncateDisclaimers(bodyText.Trim());
|
||||
|
||||
return new ScrapedArticleResult(
|
||||
Title: await page.TitleAsync(),
|
||||
TextContent: bodyText.Trim(),
|
||||
HtmlContent: await page.Locator(ArticleBodySelector).InnerHTMLAsync(),
|
||||
Title: CleanTitle(await page.TitleAsync()),
|
||||
TextContent: fallbackCleaned,
|
||||
HtmlContent: string.Empty,
|
||||
Author: null,
|
||||
Excerpt: null,
|
||||
FinalUrl: page.Url
|
||||
);
|
||||
}
|
||||
|
||||
protected static string CleanTitle(string? title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title)) return string.Empty;
|
||||
var cleaned = TitlePortalSuffixRegex.Replace(title.Trim(), "");
|
||||
return cleaned.Trim();
|
||||
}
|
||||
|
||||
protected static string TruncateDisclaimers(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text)) return string.Empty;
|
||||
var truncated = RegulatoryDisclaimerRegex.Replace(text, "");
|
||||
return truncated.Trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for DerAktionaer.de (DER AKTIONÄR).
|
||||
/// </summary>
|
||||
public class DerAktionaerScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "deraktionaer.de";
|
||||
|
||||
public override string ArticleBodySelector => "article#article-detail, div.article-body";
|
||||
|
||||
public override async Task<ScrapedArticleResult?> ExtractArticleContentAsync(IPage page)
|
||||
{
|
||||
await page.EvaluateAsync(@"() => {
|
||||
document.querySelectorAll('.sp_message, .latest-release-item, .article-footer, .subscription-box').forEach(e => e.remove());
|
||||
}");
|
||||
|
||||
return await base.ExtractArticleContentAsync(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for IT-Times.de.
|
||||
/// </summary>
|
||||
public class ItTimesScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "it-times.de";
|
||||
|
||||
public override string ArticleBodySelector => "div.story_body, article.article, div.article_content";
|
||||
|
||||
public override async Task<ScrapedArticleResult?> ExtractArticleContentAsync(IPage page)
|
||||
{
|
||||
await page.EvaluateAsync(@"() => {
|
||||
document.querySelectorAll('.social_share, .related_stories, .banner, .ad').forEach(e => e.remove());
|
||||
}");
|
||||
|
||||
return await base.ExtractArticleContentAsync(page);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for LynxBroker.de.
|
||||
/// </summary>
|
||||
public class LynxBrokerScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "lynxbroker.de";
|
||||
|
||||
public override string ArticleBodySelector => "div.entry-content, article.post, div.post-content";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for Moneycab.com.
|
||||
/// </summary>
|
||||
public class MoneycabScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "moneycab.com";
|
||||
|
||||
public override string ArticleBodySelector => "div.entry-content, article, div.post-content";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for NTG24.de.
|
||||
/// </summary>
|
||||
public class Ntg24ScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "ntg24.de";
|
||||
|
||||
public override string ArticleBodySelector => "div.article-content, div.news-content, div.entry-content";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for Sharedeals.de.
|
||||
/// </summary>
|
||||
public class SharedealsScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "sharedeals.de";
|
||||
|
||||
public override string ArticleBodySelector => "div.entry-content, div.article-body, div.post-content";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for T3n.de.
|
||||
/// </summary>
|
||||
public class T3nScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "t3n.de";
|
||||
|
||||
public override string ArticleBodySelector => "article.c-article, div.c-article__body, div.c-content";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Playwright;
|
||||
|
||||
namespace FinlyticNews.Adapters.Scraping;
|
||||
|
||||
/// <summary>
|
||||
/// Specialized article scraper adapter for XTB.com.
|
||||
/// </summary>
|
||||
public class XtbScraperAdapter : ArticleScraperAdapter
|
||||
{
|
||||
public override string Hostname => "xtb.com";
|
||||
|
||||
public override string ArticleBodySelector => "div.news-detail, div.article__content, div.market-analysis-content";
|
||||
}
|
||||
@@ -41,9 +41,9 @@ public class NewsDbContext : DbContext, ISettingsDbContext
|
||||
public DbSet<MatchedAssetEntity> MatchedAssets => Set<MatchedAssetEntity>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the database set for microservice configuration settings.
|
||||
/// Gets or sets the database set for blocked news URLs (unindexable or duplicate articles).
|
||||
/// </summary>
|
||||
public DbSet<NewsSettingsEntity> Settings => Set<NewsSettingsEntity>();
|
||||
public DbSet<BlockedNewsUrlEntity> BlockedNewsUrls => Set<BlockedNewsUrlEntity>();
|
||||
|
||||
/// <summary>
|
||||
/// Configures the model mapping, database constraints, and unique indices.
|
||||
@@ -64,16 +64,27 @@ public class NewsDbContext : DbContext, ISettingsDbContext
|
||||
.HasIndex(a => a.SourceUrl)
|
||||
.IsUnique();
|
||||
|
||||
// Performance indexes for news queries
|
||||
// Performance & deduplication indexes for news queries
|
||||
modelBuilder.Entity<NewsArticleEntity>()
|
||||
.HasIndex(a => new { a.PublishedAt, a.ScrapedAt });
|
||||
|
||||
modelBuilder.Entity<NewsArticleEntity>()
|
||||
.HasIndex(a => a.Status);
|
||||
|
||||
modelBuilder.Entity<NewsArticleEntity>()
|
||||
.HasIndex(a => a.TitleHash);
|
||||
|
||||
modelBuilder.Entity<NewsArticleEntity>()
|
||||
.HasIndex(a => a.SimHash);
|
||||
|
||||
modelBuilder.Entity<MatchedAssetEntity>()
|
||||
.HasIndex(m => m.Isin);
|
||||
|
||||
// Blocked URLs index
|
||||
modelBuilder.Entity<BlockedNewsUrlEntity>()
|
||||
.HasIndex(b => b.Url)
|
||||
.IsUnique();
|
||||
|
||||
// Configure relationship between NewsArticle and MatchedAssets
|
||||
modelBuilder.Entity<MatchedAssetEntity>()
|
||||
.HasOne(m => m.NewsArticle)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Playwright Base Image for FinlyticNews
|
||||
# Shared Playwright Base Image
|
||||
#
|
||||
# Consumed by BOTH FinlyticNews/Dockerfile and FinlyticFundamentals/Dockerfile.
|
||||
#
|
||||
# BUILD ONCE (only rebuild when PLAYWRIGHT_VERSION changes):
|
||||
# docker build -f FinlyticNews/Dockerfile.playwright-base `
|
||||
# -t finlytic-playwright-base:1.49.0 `
|
||||
# FinlyticNews
|
||||
# .\rebuild-playwright-base.ps1
|
||||
# or, via Compose:
|
||||
# docker compose --profile build-base build finlytic-playwright-base
|
||||
#
|
||||
# Not built by a plain `docker compose build` — Compose does not resolve
|
||||
# FROM-references between services, so this must exist beforehand.
|
||||
#
|
||||
# This image is then used as the base in the main Dockerfile so that
|
||||
# Chromium + all its OS dependencies are already cached in the image layer
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace FinlyticNews.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Entity representing a blocked news URL (e.g. duplicates, missing content, or no matched assets).
|
||||
/// Prevents redundant re-discovery and re-scraping of known bad or duplicate URLs.
|
||||
/// </summary>
|
||||
[Table("BlockedNewsUrls")]
|
||||
public class BlockedNewsUrlEntity
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
[Required]
|
||||
[MaxLength(2048)]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(256)]
|
||||
public string Reason { get; set; } = string.Empty;
|
||||
|
||||
public DateTime BlockedAtUtc { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -57,11 +57,22 @@ public class NewsArticleEntity
|
||||
public DateTime PublishedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the processing lifecycle state (e.g., "Pending", "Processing", "Completed", "Analyzed", "Failed").
|
||||
/// Gets or sets the processing lifecycle state (e.g., "Pending", "Processing", "Completed", "Analyzed", "Failed", "Duplicate").
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Status { get; set; } = "Pending";
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the normalized SHA-256 hash of the cleaned title for duplicate detection.
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? TitleHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the 64-bit SimHash content fingerprint for fuzzy duplicate text detection.
|
||||
/// </summary>
|
||||
public long? SimHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of financial assets matched and associated with this news article.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace FinlyticNews.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Entity representing global runtime settings for the FinlyticNews microservice.
|
||||
/// Persisted in PostgreSQL and updated dynamically via Admin Panel MQTT events.
|
||||
/// </summary>
|
||||
public class NewsSettingsEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the primary key for the settings row.
|
||||
/// </summary>
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Frequency in minutes for polling RSS feed sources.
|
||||
/// </summary>
|
||||
public int PollingFrequencyMinutes { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Article retention period in days before archival or cleanup.
|
||||
/// </summary>
|
||||
public int ArticleRetentionDays { get; set; } = 90;
|
||||
|
||||
/// <summary>
|
||||
/// Default page size for historical news API pagination queries.
|
||||
/// </summary>
|
||||
public int DefaultPageSize { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Background scraper interval in minutes.
|
||||
/// </summary>
|
||||
public int ScrapingIntervalMinutes { get; set; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// Webhook URL for n8n AI article analysis.
|
||||
/// </summary>
|
||||
public string N8nWebhookUrl { get; set; } = "http://localhost:5678/webhook/finlytic-news";
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of last modification.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
@@ -43,4 +43,11 @@
|
||||
<Folder Include="Migrations\" />
|
||||
<Folder Include="Services\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="publish\appsettings.Development.json" />
|
||||
<_ContentIncludedByDefault Remove="publish\appsettings.json" />
|
||||
<_ContentIncludedByDefault Remove="publish\FinlyticNews.deps.json" />
|
||||
<_ContentIncludedByDefault Remove="publish\FinlyticNews.runtimeconfig.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
-174
@@ -1,174 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticNews.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticNews.Migrations
|
||||
{
|
||||
[DbContext(typeof(NewsDbContext))]
|
||||
[Migration("20260801073336_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.ArticleSourceEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ArticleSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("NewsArticleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("NewsArticleId");
|
||||
|
||||
b.ToTable("MatchedAssets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsArticleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ContentRaw")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("PublishedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ScrapedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SourceUrl")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("PublishedAt", "ScrapedAt");
|
||||
|
||||
b.ToTable("NewsArticles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("ArticleRetentionDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("DefaultPageSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("N8nWebhookUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("PollingFrequencyMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ScrapingIntervalMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.HasOne("FinlyticNews.Entities.NewsArticleEntity", "NewsArticle")
|
||||
.WithMany("MatchedAssets")
|
||||
.HasForeignKey("NewsArticleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("NewsArticle");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsArticleEntity", b =>
|
||||
{
|
||||
b.Navigation("MatchedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using FinlyticNews.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticNews.Migrations
|
||||
{
|
||||
[DbContext(typeof(NewsDbContext))]
|
||||
[Migration("20260813202646_CheckPendingNews")]
|
||||
partial class CheckPendingNews
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.ArticleSourceEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ArticleSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Isin")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("NewsArticleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Isin");
|
||||
|
||||
b.HasIndex("NewsArticleId");
|
||||
|
||||
b.ToTable("MatchedAssets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsArticleEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ContentRaw")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("PublishedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTime>("ScrapedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SourceUrl")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("PublishedAt", "ScrapedAt");
|
||||
|
||||
b.ToTable("NewsArticles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("ArticleRetentionDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("DefaultPageSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("N8nWebhookUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("PollingFrequencyMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ScrapingIntervalMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.HasOne("FinlyticNews.Entities.NewsArticleEntity", "NewsArticle")
|
||||
.WithMany("MatchedAssets")
|
||||
.HasForeignKey("NewsArticleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("NewsArticle");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsArticleEntity", b =>
|
||||
{
|
||||
b.Navigation("MatchedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticNews.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CheckPendingNews : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace FinlyticNews.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDynamicSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DynamicSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||
ValueJson = table.Column<string>(type: "text", nullable: false),
|
||||
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
LastUpdatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DynamicSettings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DynamicSettings_Key",
|
||||
table: "DynamicSettings",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "DynamicSettings");
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-32
@@ -12,8 +12,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
namespace FinlyticNews.Migrations
|
||||
{
|
||||
[DbContext(typeof(NewsDbContext))]
|
||||
[Migration("20260815183946_AddDynamicSettings")]
|
||||
partial class AddDynamicSettings
|
||||
[Migration("20260818194224_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
@@ -79,6 +79,33 @@ namespace FinlyticNews.Migrations
|
||||
b.ToTable("ArticleSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.BlockedNewsUrlEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("BlockedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Url")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BlockedNewsUrls");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -127,6 +154,9 @@ namespace FinlyticNews.Migrations
|
||||
b.Property<DateTime>("ScrapedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("SimHash")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -142,48 +172,26 @@ namespace FinlyticNews.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TitleHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SimHash");
|
||||
|
||||
b.HasIndex("SourceUrl")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TitleHash");
|
||||
|
||||
b.HasIndex("PublishedAt", "ScrapedAt");
|
||||
|
||||
b.ToTable("NewsArticles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("ArticleRetentionDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("DefaultPageSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("N8nWebhookUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("PollingFrequencyMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ScrapingIntervalMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.HasOne("FinlyticNews.Entities.NewsArticleEntity", "NewsArticle")
|
||||
+59
-20
@@ -25,6 +25,35 @@ namespace FinlyticNews.Migrations
|
||||
table.PrimaryKey("PK_ArticleSources", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BlockedNewsUrls",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Url = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
|
||||
Reason = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: false),
|
||||
BlockedAtUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BlockedNewsUrls", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "DynamicSettings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Key = table.Column<string>(type: "character varying(150)", maxLength: 150, nullable: false),
|
||||
ValueJson = table.Column<string>(type: "text", nullable: false),
|
||||
ServiceIdentifier = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
LastUpdatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_DynamicSettings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NewsArticles",
|
||||
columns: table => new
|
||||
@@ -38,30 +67,15 @@ namespace FinlyticNews.Migrations
|
||||
SourceUrl = table.Column<string>(type: "text", nullable: false),
|
||||
ScrapedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
PublishedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
Status = table.Column<string>(type: "text", nullable: false)
|
||||
Status = table.Column<string>(type: "text", nullable: false),
|
||||
TitleHash = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
|
||||
SimHash = table.Column<long>(type: "bigint", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NewsArticles", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Settings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
PollingFrequencyMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||
ArticleRetentionDays = table.Column<int>(type: "integer", nullable: false),
|
||||
DefaultPageSize = table.Column<int>(type: "integer", nullable: false),
|
||||
ScrapingIntervalMinutes = table.Column<int>(type: "integer", nullable: false),
|
||||
N8nWebhookUrl = table.Column<string>(type: "text", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Settings", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "MatchedAssets",
|
||||
columns: table => new
|
||||
@@ -82,6 +96,18 @@ namespace FinlyticNews.Migrations
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BlockedNewsUrls_Url",
|
||||
table: "BlockedNewsUrls",
|
||||
column: "Url",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_DynamicSettings_Key",
|
||||
table: "DynamicSettings",
|
||||
column: "Key",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_MatchedAssets_Isin",
|
||||
table: "MatchedAssets",
|
||||
@@ -97,6 +123,11 @@ namespace FinlyticNews.Migrations
|
||||
table: "NewsArticles",
|
||||
columns: new[] { "PublishedAt", "ScrapedAt" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NewsArticles_SimHash",
|
||||
table: "NewsArticles",
|
||||
column: "SimHash");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NewsArticles_SourceUrl",
|
||||
table: "NewsArticles",
|
||||
@@ -107,6 +138,11 @@ namespace FinlyticNews.Migrations
|
||||
name: "IX_NewsArticles_Status",
|
||||
table: "NewsArticles",
|
||||
column: "Status");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NewsArticles_TitleHash",
|
||||
table: "NewsArticles",
|
||||
column: "TitleHash");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -116,10 +152,13 @@ namespace FinlyticNews.Migrations
|
||||
name: "ArticleSources");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MatchedAssets");
|
||||
name: "BlockedNewsUrls");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Settings");
|
||||
name: "DynamicSettings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "MatchedAssets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NewsArticles");
|
||||
@@ -76,6 +76,33 @@ namespace FinlyticNews.Migrations
|
||||
b.ToTable("ArticleSources");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.BlockedNewsUrlEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("BlockedAtUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Url")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BlockedNewsUrls");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
@@ -124,6 +151,9 @@ namespace FinlyticNews.Migrations
|
||||
b.Property<DateTime>("ScrapedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("SimHash")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("SourceUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -139,48 +169,26 @@ namespace FinlyticNews.Migrations
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("TitleHash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("SimHash");
|
||||
|
||||
b.HasIndex("SourceUrl")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Status");
|
||||
|
||||
b.HasIndex("TitleHash");
|
||||
|
||||
b.HasIndex("PublishedAt", "ScrapedAt");
|
||||
|
||||
b.ToTable("NewsArticles");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.NewsSettingsEntity", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("ArticleRetentionDays")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("DefaultPageSize")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("N8nWebhookUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("PollingFrequencyMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ScrapingIntervalMinutes")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("FinlyticNews.Entities.MatchedAssetEntity", b =>
|
||||
{
|
||||
b.HasOne("FinlyticNews.Entities.NewsArticleEntity", "NewsArticle")
|
||||
|
||||
+22
-9
@@ -7,6 +7,8 @@ using FinlyticNews.Adapters.Discovery;
|
||||
using FinlyticNews.Adapters.Scraping;
|
||||
using FinlyticNews.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
@@ -27,13 +29,28 @@ builder.Services.AddSingleton<ArticleDiscoveryAdapter, FinanznachrichtenRssDisco
|
||||
|
||||
// Register Detail Scraper Adapters
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, FinanznachrichtenScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, ArivaScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, DerAktionaerScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, ItTimesScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, MoneycabScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, T3nScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, LynxBrokerScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, Ntg24ScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, SharedealsScraperAdapter>();
|
||||
builder.Services.AddSingleton<ArticleScraperAdapter, XtbScraperAdapter>();
|
||||
|
||||
// Register Service interfaces and implementations
|
||||
// Register Non-AI Matching, Validation, Deduplication, and Blocklist Services
|
||||
builder.Services.AddSingleton<INewsBlocklistService, NewsBlocklistService>();
|
||||
builder.Services.AddSingleton<IArticleDeduplicationService, ArticleDeduplicationService>();
|
||||
builder.Services.AddSingleton<AssetMatcherService>();
|
||||
builder.Services.AddSingleton<IAssetMatcherService>(sp => sp.GetRequiredService<AssetMatcherService>());
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<AssetMatcherService>());
|
||||
builder.Services.AddSingleton<IAssetValidationService, AssetValidationService>();
|
||||
|
||||
// Register Scraper and DB Services
|
||||
builder.Services.AddScoped<IArticleDiscoveryService, ArticleDiscoveryService>();
|
||||
builder.Services.AddScoped<IPlaywrightScraperService, PlaywrightScraperService>();
|
||||
builder.Services.AddScoped<IN8nService, N8nService>();
|
||||
builder.Services.AddScoped<INewsDbService, NewsDbService>();
|
||||
builder.Services.AddScoped<ISettingsDbService, SettingsDbService>();
|
||||
|
||||
// Register MQTT Client (as singleton hosted service)
|
||||
builder.Services.AddSingleton<NewsMqttClient>();
|
||||
@@ -50,12 +67,8 @@ using (var scope = host.Services.CreateScope())
|
||||
try
|
||||
{
|
||||
var context = scope.ServiceProvider.GetRequiredService<NewsDbContext>();
|
||||
await context.Database.MigrateAsync();
|
||||
|
||||
// Ensure default settings exist in DB
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsDbService>();
|
||||
await settingsService.GetSettingsAsync();
|
||||
|
||||
var connStr = builder.Configuration.GetConnectionString("DefaultConnection") ?? "";
|
||||
await context.MigrateWithBootstrapAsync(connStr);
|
||||
|
||||
|
||||
// Seed default article source if empty
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# Finlytic News Service
|
||||
|
||||
Finlytic News is a scalable C# microservice designed to periodically scrape, deduplicate, pre-filter, and process financial articles before distributing them to downstream consumers via MQTT. It leverages a dynamic adapter-based link discovery system, headless browser automation (Playwright/HtmlAgilityPack) to bypass dynamic site scrapers, integrates with n8n workflows for AI-driven classification, and maintains a resilient state machine database cache.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Data Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Cron[Cron Trigger: 15m + Jitter] -->|Start Discovery| ADS[ArticleDiscoveryService]
|
||||
ADS -->|Fetch Content| HTML[RSS Feed / HTML Page]
|
||||
|
||||
ADS -->|Parse XML| RSS[RssDiscoveryAdapter]
|
||||
ADS -->|Parse HTML| Agility[Custom Host Adapters e.g. HtmlAgilityPack]
|
||||
|
||||
RSS -->|Extract Links| Links[Discovered URLs]
|
||||
Agility -->|Extract Links| Links
|
||||
|
||||
Links -->|Redirect Resolve & Playwright| Body[Article Content]
|
||||
Body -->|Idempotency Check| DB{URL/Hash in PostgreSQL?}
|
||||
DB -->|Exists: Skip| Skip[End Process]
|
||||
DB -->|New: Load Index| IndexFilter[Asset Index Pre-Filter]
|
||||
|
||||
AssetIndex[assets/index/index.json] -->|ISIN & Name List| IndexFilter
|
||||
IndexFilter -->|Matched Assets| Webhook[n8n AI Webhook Pipeline]
|
||||
|
||||
Webhook -->|AI Extraction & Score| DBWrite[Save Article & Assets]
|
||||
DBWrite -->|State = Completed| Broadcast[MQTT Live Broadcast]
|
||||
|
||||
Downstream[Downstream Services] <-->|MQTT RPC / Recovery| MqttRpc[MQTT RPC Server]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Features & Capabilities
|
||||
|
||||
### 1. Dynamic Article Discovery & Scraping Architecture
|
||||
- **Adapter Separation**: Discovery Adapters (`RssDiscoveryAdapter`) & Detail Scrapers (`FinanznachrichtenScraperAdapter`).
|
||||
- **Stealth Timing**: 15-minute cron with randomized jitter offsets.
|
||||
- **Headless Scraping**: Playwright & HtmlAgilityPack node parsing.
|
||||
- **Redirect Resolution**: Automatic URL expansion for canonical links.
|
||||
|
||||
### 2. Data Deduplication & Asset Pre-Filtering
|
||||
- **Database Idempotency**: Source URL uniqueness index.
|
||||
- **Asset Index Pre-Filtering**: Matches raw text against `assets/index/index.json`.
|
||||
- **n8n AI Webhook Integration**: Enrichment & classification pipeline.
|
||||
|
||||
### 3. Database State Management & Performance Indexes
|
||||
- **Lifecycle States**: `Pending` -> `Processing` -> `Completed` / `Failed`.
|
||||
- **PostgreSQL Indizes**: Optimized compound index on `(PublishedAt, ScrapedAt)`, `Status`, `SourceUrl`, `Isin`.
|
||||
|
||||
### 4. Event-Driven Messaging (MQTT)
|
||||
- **Zero-Allocation Broadcast**: Publishes completed articles to `finlytic/news/new` and `finlytic/news/classification`.
|
||||
- **MQTT RPC Handler**: Serves historical article queries for gateway endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Feature Status
|
||||
|
||||
### Implemented Features
|
||||
- [x] RSS & HTML Adapter discovery architecture (`ArticleSourceEntity`).
|
||||
- [x] Playwright headless browser detail scraping.
|
||||
- [x] n8n AI webhook integration.
|
||||
- [x] Database compound indexes on `(PublishedAt, ScrapedAt)`, `Status`, `SourceUrl`.
|
||||
- [x] MQTT broadcast & RPC response handling.
|
||||
- [x] Zero-Allocation serialization via `FinlyticJsonSerializerContext`.
|
||||
|
||||
### Planned / Future Features
|
||||
- [ ] Multi-language translation pipeline (auto-translate foreign language news articles to DE/EN).
|
||||
- [ ] Direct RSS Feed WebSub (PubSubHubbub) real-time push ingestion for instant zero-latency discovery.
|
||||
@@ -0,0 +1,357 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticNews.Database;
|
||||
using FinlyticNews.Util;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a duplicate check on a candidate news article.
|
||||
/// </summary>
|
||||
public record DuplicateCheckResult(
|
||||
bool IsDuplicate,
|
||||
Guid? DuplicateOfArticleId,
|
||||
string? Reason,
|
||||
string TitleHash,
|
||||
long SimHash
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for detecting syndicated or near-identical duplicate news articles without AI.
|
||||
/// </summary>
|
||||
public interface IArticleDeduplicationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates whether an article is a duplicate based on normalized title similarity and 64-bit text SimHash fingerprinting.
|
||||
/// </summary>
|
||||
DuplicateCheckResult CheckDuplicate(string title, string content, DateTime publishedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a newly processed article into the in-memory deduplication cache.
|
||||
/// </summary>
|
||||
void RegisterArticle(Guid articleId, string titleHash, long simHash, string title, DateTime publishedAt);
|
||||
|
||||
/// <summary>
|
||||
/// Loads recent articles from the database into the deduplication cache on startup.
|
||||
/// </summary>
|
||||
Task InitializeAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the normalized SHA-256 hash of an article title.
|
||||
/// </summary>
|
||||
string ComputeTitleHash(string title);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the 64-bit SimHash content fingerprint of article text.
|
||||
/// </summary>
|
||||
long ComputeSimHash(string content);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance non-AI deduplication engine using 64-bit SimHash and N-Gram title similarity.
|
||||
/// </summary>
|
||||
public class ArticleDeduplicationService : IArticleDeduplicationService
|
||||
{
|
||||
private record CachedArticleEntry(
|
||||
Guid ArticleId,
|
||||
string TitleHash,
|
||||
long SimHash,
|
||||
string NormalizedTitle,
|
||||
DateTime PublishedAt
|
||||
);
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IFinlyticLogger<ArticleDeduplicationService> _finlyticLogger;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
// Rolling in-memory cache of recent articles
|
||||
private readonly ConcurrentDictionary<string, Guid> _titleHashIndex = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<CachedArticleEntry> _simHashIndex = [];
|
||||
private readonly ReaderWriterLockSlim _simHashLock = new();
|
||||
|
||||
private bool _initialized;
|
||||
private readonly SemaphoreSlim _initLock = new(1, 1);
|
||||
|
||||
private static readonly Regex TitlePortalSuffixRegex = new(
|
||||
@"\s*[-|–—]\s*(DER AKTIONÄR|ARIVA\.DE|IT-Times|onvista|wallstreet:online|Sharedeals\.de|boerse\.de|Handelsblatt|WirtschaftsWoche|finanzen\.net|Finanznachrichten|XTB|Lynx|T3n|ntg24|Moneycab).*$",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex NonAlphanumericRegex = new(@"[^\w\s]", RegexOptions.Compiled);
|
||||
private static readonly Regex MultipleSpacesRegex = new(@"\s+", RegexOptions.Compiled);
|
||||
|
||||
public ArticleDeduplicationService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IFinlyticLogger<ArticleDeduplicationService> finlyticLogger,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
await _initLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<NewsDbContext>();
|
||||
|
||||
var windowDays = await _settingsService.GetSettingAsync(SettingKeys.DeduplicationWindowDays, cancellationToken);
|
||||
var cutoff = DateTime.UtcNow.AddDays(-Math.Max(windowDays, 7));
|
||||
|
||||
var articles = await dbContext.NewsArticles
|
||||
.AsNoTracking()
|
||||
.Where(a => a.PublishedAt >= cutoff && a.Status == "Completed")
|
||||
.Select(a => new
|
||||
{
|
||||
a.Id,
|
||||
a.Title,
|
||||
a.TitleHash,
|
||||
a.SimHash,
|
||||
a.PublishedAt
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
_simHashLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var a in articles)
|
||||
{
|
||||
var tHash = a.TitleHash ?? ComputeTitleHash(a.Title);
|
||||
var sHash = a.SimHash ?? 0L;
|
||||
var normTitle = NormalizeTitle(a.Title);
|
||||
|
||||
_titleHashIndex[tHash] = a.Id;
|
||||
|
||||
if (sHash != 0L)
|
||||
{
|
||||
_simHashIndex.Add(new CachedArticleEntry(a.Id, tHash, sHash, normTitle, a.PublishedAt));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_simHashLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.DeduplicationChannel, "[ArticleDeduplicationService] Initialized with {Count} recent articles for duplicate detection.", articles.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_initLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public DuplicateCheckResult CheckDuplicate(string title, string content, DateTime publishedAt)
|
||||
{
|
||||
var titleHash = ComputeTitleHash(title);
|
||||
var simHash = ComputeSimHash(content);
|
||||
var normalizedTitle = NormalizeTitle(title);
|
||||
|
||||
// 1. Exact Title Hash Match
|
||||
if (_titleHashIndex.TryGetValue(titleHash, out var exactMatchId))
|
||||
{
|
||||
return new DuplicateCheckResult(
|
||||
IsDuplicate: true,
|
||||
DuplicateOfArticleId: exactMatchId,
|
||||
Reason: "ExactTitleHashMatch",
|
||||
TitleHash: titleHash,
|
||||
SimHash: simHash
|
||||
);
|
||||
}
|
||||
|
||||
_simHashLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var windowCutoff = publishedAt.AddDays(-7);
|
||||
|
||||
foreach (var entry in _simHashIndex)
|
||||
{
|
||||
if (entry.PublishedAt < windowCutoff) continue;
|
||||
|
||||
// 2. SimHash Content Similarity (Hamming Distance <= 3 bits -> > 90% identical text)
|
||||
if (simHash != 0L && entry.SimHash != 0L)
|
||||
{
|
||||
int distance = HammingDistance(simHash, entry.SimHash);
|
||||
if (distance <= 3)
|
||||
{
|
||||
return new DuplicateCheckResult(
|
||||
IsDuplicate: true,
|
||||
DuplicateOfArticleId: entry.ArticleId,
|
||||
Reason: $"SimHashSimilarity (HammingDistance: {distance})",
|
||||
TitleHash: titleHash,
|
||||
SimHash: simHash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Fuzzy Title Similarity (3-gram Jaccard Index >= 0.85)
|
||||
double titleSim = Calculate3GramJaccard(normalizedTitle, entry.NormalizedTitle);
|
||||
if (titleSim >= 0.85)
|
||||
{
|
||||
return new DuplicateCheckResult(
|
||||
IsDuplicate: true,
|
||||
DuplicateOfArticleId: entry.ArticleId,
|
||||
Reason: $"FuzzyTitleSimilarity ({titleSim:P0})",
|
||||
TitleHash: titleHash,
|
||||
SimHash: simHash
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_simHashLock.ExitReadLock();
|
||||
}
|
||||
|
||||
return new DuplicateCheckResult(
|
||||
IsDuplicate: false,
|
||||
DuplicateOfArticleId: null,
|
||||
Reason: null,
|
||||
TitleHash: titleHash,
|
||||
SimHash: simHash
|
||||
);
|
||||
}
|
||||
|
||||
public void RegisterArticle(Guid articleId, string titleHash, long simHash, string title, DateTime publishedAt)
|
||||
{
|
||||
_titleHashIndex[titleHash] = articleId;
|
||||
|
||||
_simHashLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_simHashIndex.Add(new CachedArticleEntry(articleId, titleHash, simHash, NormalizeTitle(title), publishedAt));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_simHashLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public string ComputeTitleHash(string title)
|
||||
{
|
||||
var normalized = NormalizeTitle(title);
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(normalized));
|
||||
return Convert.ToHexString(bytes).ToLowerInvariant();
|
||||
}
|
||||
|
||||
public long ComputeSimHash(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content)) return 0L;
|
||||
|
||||
// Clean and tokenize content into words
|
||||
var clean = NonAlphanumericRegex.Replace(content.ToLowerInvariant(), " ");
|
||||
var words = clean.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (words.Length < 10) return 0L;
|
||||
|
||||
// Generate 3-word shingles
|
||||
var v = new int[64];
|
||||
for (int i = 0; i < words.Length - 2; i++)
|
||||
{
|
||||
var shingle = $"{words[i]} {words[i + 1]} {words[i + 2]}";
|
||||
var hash = Hash64(shingle);
|
||||
|
||||
for (int bit = 0; bit < 64; bit++)
|
||||
{
|
||||
if (((hash >> bit) & 1L) == 1L)
|
||||
{
|
||||
v[bit]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
v[bit]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long simHash = 0L;
|
||||
for (int bit = 0; bit < 64; bit++)
|
||||
{
|
||||
if (v[bit] > 0)
|
||||
{
|
||||
simHash |= (1L << bit);
|
||||
}
|
||||
}
|
||||
|
||||
return simHash;
|
||||
}
|
||||
|
||||
private static string NormalizeTitle(string title)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(title)) return string.Empty;
|
||||
var stripped = TitlePortalSuffixRegex.Replace(title, "");
|
||||
var cleaned = NonAlphanumericRegex.Replace(stripped.ToLowerInvariant(), " ");
|
||||
return MultipleSpacesRegex.Replace(cleaned, " ").Trim();
|
||||
}
|
||||
|
||||
private static double Calculate3GramJaccard(string a, string b)
|
||||
{
|
||||
if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) return 0.0;
|
||||
if (a == b) return 1.0;
|
||||
|
||||
var gramsA = Get3Grams(a);
|
||||
var gramsB = Get3Grams(b);
|
||||
|
||||
if (gramsA.Count == 0 || gramsB.Count == 0) return 0.0;
|
||||
|
||||
int intersection = 0;
|
||||
foreach (var g in gramsA)
|
||||
{
|
||||
if (gramsB.Contains(g)) intersection++;
|
||||
}
|
||||
|
||||
int union = gramsA.Count + gramsB.Count - intersection;
|
||||
return union == 0 ? 0.0 : (double)intersection / union;
|
||||
}
|
||||
|
||||
private static HashSet<string> Get3Grams(string text)
|
||||
{
|
||||
var set = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (text.Length < 3)
|
||||
{
|
||||
set.Add(text);
|
||||
return set;
|
||||
}
|
||||
|
||||
for (int i = 0; i <= text.Length - 3; i++)
|
||||
{
|
||||
set.Add(text.Substring(i, 3));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
private static int HammingDistance(long a, long b)
|
||||
{
|
||||
return BitOperations.PopCount((ulong)(a ^ b));
|
||||
}
|
||||
|
||||
private static long Hash64(string text)
|
||||
{
|
||||
// 64-bit FNV-1a Hash
|
||||
ulong hash = 14695981039346656037UL;
|
||||
var bytes = Encoding.UTF8.GetBytes(text);
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
hash ^= b;
|
||||
hash *= 1099511628211UL;
|
||||
}
|
||||
return (long)hash;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Models.Assets;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Util;
|
||||
using FinlyticNews.Util;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Candidate asset match identified in an article.
|
||||
/// </summary>
|
||||
public record MatchedCandidateAsset(
|
||||
string Isin,
|
||||
string Name,
|
||||
int MentionCount,
|
||||
bool InTitle,
|
||||
bool InUrl,
|
||||
double InitialConfidence
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for fast in-memory asset detection from news text, titles, and URLs without AI.
|
||||
/// </summary>
|
||||
public interface IAssetMatcherService
|
||||
{
|
||||
/// <summary>
|
||||
/// Matches financial assets in the given text, title, and URL against the in-memory asset index.
|
||||
/// </summary>
|
||||
List<MatchedCandidateAsset> MatchAssets(string title, string content, string? url = null);
|
||||
|
||||
/// <summary>
|
||||
/// Forces a reload of the index.json from disk into memory.
|
||||
/// </summary>
|
||||
Task ReloadIndexAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory asset matching engine supporting ISIN regex, normalized name matching, legal suffix stripping, and stopword defenses.
|
||||
/// </summary>
|
||||
public class AssetMatcherService : IAssetMatcherService, IHostedService
|
||||
{
|
||||
private record CompiledNameMatcher(
|
||||
AssetIndex Asset,
|
||||
string CleanName,
|
||||
Regex WordRegex,
|
||||
bool IsShortOrCommon
|
||||
);
|
||||
|
||||
private readonly IFinlyticLogger<AssetMatcherService> _finlyticLogger;
|
||||
private readonly string _indexPath;
|
||||
|
||||
private readonly Dictionary<string, AssetIndex> _isinLookup = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<CompiledNameMatcher> _nameMatchers = [];
|
||||
private readonly ReaderWriterLockSlim _indexLock = new();
|
||||
|
||||
private static readonly Regex IsinRegex = new(@"\b[A-Z]{2}[A-Z0-9]{9}[0-9]\b", RegexOptions.Compiled);
|
||||
private static readonly Regex FinancialContextRegex = new(
|
||||
@"(aktie|aktien|kurs|kurse|börse|finanz|umsatz|gewinn|quartal|prognose|dividende|kgv|analyst|kursziel|konzern|shares|stock|stocks|revenue|earnings|ebitda|ceo|cfo|nasdaq|dow|dax|s&p|\$|€)",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex LegalSuffixRegex = new(
|
||||
@"\s+(AG|SE|GmbH|KGaA|Inc\.?|Incorporated|Corp\.?|Corporation|Ltd\.?|Limited|PLC|Plc|SA|NV|Holdings?|Group|Co\.?|Class\s+[A-Z]|ADR|SpA|Oyj|AB|A\/S|N\.V\.|S\.A\.|S\.E\.)\b",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly HashSet<string> CommonWordStopwords = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"CAN", "IT", "ALL", "BE", "NEXT", "MAN", "GAP", "ON", "FOR", "NOW", "ONE", "TOP", "REAL", "DO", "GO", "US",
|
||||
"DIE", "DER", "DAS", "UND", "VON", "MIT", "AUS", "IN", "AN", "ZU", "WAR", "AUF", "TAG", "PLUS", "FAST", "BEST",
|
||||
"NEW", "AIR", "SEE", "MAX", "PRO", "NET", "HOME", "WORK", "CAR", "AUTO", "CARE", "LIFE", "WELL", "PURE", "TRUE",
|
||||
"APP", "BOX", "HUB", "KEY", "PAY", "BUY", "WIN", "RUN", "SET", "GET", "RED", "BIG", "SUN", "SEA", "STAR", "BAY"
|
||||
};
|
||||
|
||||
public AssetMatcherService(IFinlyticLogger<AssetMatcherService> finlyticLogger)
|
||||
{
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_indexPath = Path.Combine(Volumes.IndexRelativePath, "index.json");
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await ReloadIndexAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public async Task ReloadIndexAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!File.Exists(_indexPath))
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.MatcherChannel, "[AssetMatcherService] Index file not found at: {Path}", _indexPath);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await using var stream = File.OpenRead(_indexPath);
|
||||
var assets = await JsonSerializer.DeserializeAsync<List<AssetIndex>>(stream, cancellationToken: cancellationToken);
|
||||
|
||||
if (assets == null || assets.Count == 0)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.MatcherChannel, "[AssetMatcherService] Index file is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
_indexLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_isinLookup.Clear();
|
||||
_nameMatchers.Clear();
|
||||
|
||||
foreach (var asset in assets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(asset.Isin)) continue;
|
||||
_isinLookup[asset.Isin.Trim()] = asset;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(asset.Name))
|
||||
{
|
||||
var cleanName = CleanCompanyName(asset.Name);
|
||||
if (cleanName.Length >= 2)
|
||||
{
|
||||
bool isShort = cleanName.Length < 4 || CommonWordStopwords.Contains(cleanName);
|
||||
var pattern = $@"\b{Regex.Escape(cleanName)}\b";
|
||||
var regex = new Regex(pattern, isShort ? RegexOptions.Compiled : RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
_nameMatchers.Add(new CompiledNameMatcher(asset, cleanName, regex, isShort));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_indexLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.MatcherChannel, "[AssetMatcherService] Successfully loaded {Count} assets and {MatcherCount} name matchers into memory.", _isinLookup.Count, _nameMatchers.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.MatcherChannel, ex, "[AssetMatcherService] Failed to load index from {Path}", _indexPath);
|
||||
}
|
||||
}
|
||||
|
||||
public List<MatchedCandidateAsset> MatchAssets(string title, string content, string? url = null)
|
||||
{
|
||||
var results = new Dictionary<string, MatchedCandidateAsset>(StringComparer.OrdinalIgnoreCase);
|
||||
var fullText = (title + " " + content);
|
||||
|
||||
_indexLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// 1. URL Mining for ISINs (100% confidence)
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
var isinMatchesInUrl = IsinRegex.Matches(url);
|
||||
foreach (Match m in isinMatchesInUrl)
|
||||
{
|
||||
if (_isinLookup.TryGetValue(m.Value, out var asset))
|
||||
{
|
||||
results[asset.Isin] = new MatchedCandidateAsset(
|
||||
Isin: asset.Isin,
|
||||
Name: asset.Name,
|
||||
MentionCount: 1,
|
||||
InTitle: false,
|
||||
InUrl: true,
|
||||
InitialConfidence: 1.0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. ISIN Regex in Title & Content
|
||||
var isinMatches = IsinRegex.Matches(fullText);
|
||||
foreach (Match m in isinMatches)
|
||||
{
|
||||
if (_isinLookup.TryGetValue(m.Value, out var asset))
|
||||
{
|
||||
bool inTitle = title.Contains(m.Value, StringComparison.OrdinalIgnoreCase);
|
||||
int count = isinMatches.Count(x => x.Value.Equals(m.Value, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
results[asset.Isin] = new MatchedCandidateAsset(
|
||||
Isin: asset.Isin,
|
||||
Name: asset.Name,
|
||||
MentionCount: count,
|
||||
InTitle: inTitle,
|
||||
InUrl: results.TryGetValue(asset.Isin, out var existing) && existing.InUrl,
|
||||
InitialConfidence: inTitle ? 1.0 : 0.95
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Name Matchers (Title & Content)
|
||||
bool hasFinancialContext = FinancialContextRegex.IsMatch(fullText);
|
||||
|
||||
foreach (var matcher in _nameMatchers)
|
||||
{
|
||||
if (results.ContainsKey(matcher.Asset.Isin)) continue;
|
||||
|
||||
// Check Title First
|
||||
bool titleMatch = matcher.WordRegex.IsMatch(title);
|
||||
if (titleMatch)
|
||||
{
|
||||
// If short/common, ensure financial context exists
|
||||
if (!matcher.IsShortOrCommon || hasFinancialContext)
|
||||
{
|
||||
results[matcher.Asset.Isin] = new MatchedCandidateAsset(
|
||||
Isin: matcher.Asset.Isin,
|
||||
Name: matcher.Asset.Name,
|
||||
MentionCount: 1,
|
||||
InTitle: true,
|
||||
InUrl: false,
|
||||
InitialConfidence: 0.9
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check Content
|
||||
var contentMatches = matcher.WordRegex.Matches(content);
|
||||
if (contentMatches.Count > 0)
|
||||
{
|
||||
if (matcher.IsShortOrCommon)
|
||||
{
|
||||
// Short words require >= 2 mentions AND financial context
|
||||
if (contentMatches.Count >= 2 && hasFinancialContext)
|
||||
{
|
||||
results[matcher.Asset.Isin] = new MatchedCandidateAsset(
|
||||
Isin: matcher.Asset.Isin,
|
||||
Name: matcher.Asset.Name,
|
||||
MentionCount: contentMatches.Count,
|
||||
InTitle: false,
|
||||
InUrl: false,
|
||||
InitialConfidence: 0.75
|
||||
);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
results[matcher.Asset.Isin] = new MatchedCandidateAsset(
|
||||
Isin: matcher.Asset.Isin,
|
||||
Name: matcher.Asset.Name,
|
||||
MentionCount: contentMatches.Count,
|
||||
InTitle: false,
|
||||
InUrl: false,
|
||||
InitialConfidence: contentMatches.Count >= 2 ? 0.85 : 0.7
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_indexLock.ExitReadLock();
|
||||
}
|
||||
|
||||
return results.Values.OrderByDescending(r => r.InitialConfidence).ThenByDescending(r => r.MentionCount).ToList();
|
||||
}
|
||||
|
||||
private static string CleanCompanyName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name)) return string.Empty;
|
||||
var stripped = LegalSuffixRegex.Replace(name, "").Trim();
|
||||
return stripped;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos.Assets;
|
||||
using FinlyticCore.Models.Assets;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticNews.Util;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for validating candidate asset matches against FinlyticAssets metadata and sector clustering.
|
||||
/// </summary>
|
||||
public interface IAssetValidationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates and filters candidate asset matches using FinlyticAssets metadata, sector clustering, and occurrence rules.
|
||||
/// </summary>
|
||||
Task<List<MatchedCandidateAsset>> ValidateCandidateAssetsAsync(
|
||||
List<MatchedCandidateAsset> candidates,
|
||||
string title,
|
||||
string content,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Heuristic asset validation engine verifying asset existence, title priority, mention density, and sector coherence.
|
||||
/// </summary>
|
||||
public class AssetValidationService : IAssetValidationService
|
||||
{
|
||||
private readonly IFinlyticLogger<AssetValidationService> _finlyticLogger;
|
||||
private readonly NewsMqttClient _mqttClient;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public AssetValidationService(
|
||||
IFinlyticLogger<AssetValidationService> finlyticLogger,
|
||||
NewsMqttClient mqttClient,
|
||||
ISettingsService settingsService)
|
||||
{
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_mqttClient = mqttClient;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
public async Task<List<MatchedCandidateAsset>> ValidateCandidateAssetsAsync(
|
||||
List<MatchedCandidateAsset> candidates,
|
||||
string title,
|
||||
string content,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (candidates.Count == 0) return [];
|
||||
|
||||
var verifiedAssets = new List<MatchedCandidateAsset>();
|
||||
var assetDetails = new Dictionary<string, AssetDto>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// 1. Fetch Asset Details from FinlyticAssets via MQTT RPC
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = new GetValidAssetRequest(candidate.Isin);
|
||||
var assets = await _mqttClient.RequestAsync<GetValidAssetRequest, List<AssetDto>>("services/request/assets_Get", req);
|
||||
|
||||
if (assets != null && assets.Count > 0)
|
||||
{
|
||||
var match = assets.FirstOrDefault(a => a.Isin.Equals(candidate.Isin, StringComparison.OrdinalIgnoreCase));
|
||||
if (match != null)
|
||||
{
|
||||
assetDetails[candidate.Isin] = match;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.MatcherChannel, ex, "[AssetValidationService] Failed to query FinlyticAssets for ISIN {Isin}. Falling back to index data.", candidate.Isin);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Tag & Sector Frequency Analysis (Clustering)
|
||||
var sectorCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (_, dto) in assetDetails)
|
||||
{
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
foreach (var tag in dto.Tags)
|
||||
{
|
||||
if (tag.Type.Equals("sector", StringComparison.OrdinalIgnoreCase) ||
|
||||
tag.Type.Equals("industry", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sectorCounts[tag.Name] = sectorCounts.GetValueOrDefault(tag.Name, 0) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool hasDominantSector = sectorCounts.Values.Any(count => count >= 2);
|
||||
string? dominantSector = sectorCounts.OrderByDescending(kv => kv.Value).FirstOrDefault().Key;
|
||||
|
||||
// 3. Evaluate Each Candidate
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
// Rule A: Direct Title Match or URL Match -> Guaranteed Valid
|
||||
if (candidate.InTitle || candidate.InUrl)
|
||||
{
|
||||
verifiedAssets.Add(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rule B: Multiple Mentions in Article Body
|
||||
if (candidate.MentionCount >= 2)
|
||||
{
|
||||
verifiedAssets.Add(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rule C: Single Mention Validation with Sector Clustering
|
||||
if (assetDetails.TryGetValue(candidate.Isin, out var dto))
|
||||
{
|
||||
bool matchesDominantSector = false;
|
||||
if (hasDominantSector && dto.Tags != null)
|
||||
{
|
||||
matchesDominantSector = dto.Tags.Any(t => t.Name.Equals(dominantSector, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (matchesDominantSector)
|
||||
{
|
||||
// Reinforced by cluster
|
||||
verifiedAssets.Add(candidate);
|
||||
}
|
||||
else if (candidate.InitialConfidence >= 0.85)
|
||||
{
|
||||
// High-confidence exact unique name match
|
||||
verifiedAssets.Add(candidate);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.MatcherChannel, "[AssetValidationService] Pruned low-confidence outlier candidate: {Name} ({Isin})", candidate.Name, candidate.Isin);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// If not found in FinlyticAssets, discard
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.MatcherChannel, "[AssetValidationService] Discarded candidate {Isin} as it does not exist in FinlyticAssets.", candidate.Isin);
|
||||
}
|
||||
}
|
||||
|
||||
return verifiedAssets;
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos.News;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Util;
|
||||
using FinlyticNews.Util;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines integration operations with the external n8n AI workflow webhook.
|
||||
/// </summary>
|
||||
public interface IN8nService
|
||||
{
|
||||
/// <summary>
|
||||
/// Submits raw article content and pre-filtered assets to n8n, returning the parsed response metadata.
|
||||
/// </summary>
|
||||
Task<N8nResponsePayload?> AnalyzeArticleAsync(string content, List<FilteredAssetPayload> filteredAssets, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public class N8nService : IN8nService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IFinlyticLogger<N8nService> _finlyticLogger;
|
||||
|
||||
public N8nService(
|
||||
HttpClient httpClient,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IConfiguration configuration,
|
||||
IFinlyticLogger<N8nService> finlyticLogger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_scopeFactory = scopeFactory;
|
||||
_configuration = configuration;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<N8nResponsePayload?> AnalyzeArticleAsync(string content, List<FilteredAssetPayload> filteredAssets, CancellationToken ct = default)
|
||||
{
|
||||
string? targetUrl = null;
|
||||
|
||||
using (var scope = _scopeFactory.CreateScope())
|
||||
{
|
||||
var settingsService = scope.ServiceProvider.GetService<ISettingsService>();
|
||||
if (settingsService != null)
|
||||
{
|
||||
var dynamicUrl = await settingsService.GetSettingAsync(SettingKeys.N8nArticleExtractionUrl, ct);
|
||||
if (!string.IsNullOrWhiteSpace(dynamicUrl))
|
||||
{
|
||||
targetUrl = dynamicUrl.Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(targetUrl))
|
||||
{
|
||||
var settingsDb = scope.ServiceProvider.GetService<ISettingsDbService>();
|
||||
if (settingsDb != null)
|
||||
{
|
||||
var settings = await settingsDb.GetSettingsAsync();
|
||||
targetUrl = settings?.N8nWebhookUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(targetUrl))
|
||||
{
|
||||
targetUrl = _configuration["N8N:ArticleExtractionUrl"]
|
||||
?? _configuration["N8N__ArticleExtractionUrl"];
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(targetUrl))
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, "[N8nService] N8nWebhookUrl is not configured in DB or application settings.");
|
||||
return null;
|
||||
}
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[N8nService] Posting article to n8n webhook pipeline at: {Url}", targetUrl);
|
||||
|
||||
var payload = new N8nRequestPayload(content, filteredAssets ?? []);
|
||||
|
||||
try
|
||||
{
|
||||
var jsonContent = JsonSerializer.Serialize(payload, FinlyticJsonSerializerContext.Default.N8nRequestPayload);
|
||||
using var requestContent = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await _httpClient.PostAsync(targetUrl, requestContent, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorMsg = await response.Content.ReadAsStringAsync(ct);
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, "[N8nService] n8n webhook returned status code {StatusCode}. Error payload: {Error}", response.StatusCode, errorMsg);
|
||||
return null;
|
||||
}
|
||||
|
||||
using var responseStream = await response.Content.ReadAsStreamAsync(ct);
|
||||
using var doc = await JsonDocument.ParseAsync(responseStream, cancellationToken: ct);
|
||||
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
if (root.GetArrayLength() == 0)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[N8nService] n8n webhook returned an empty array.");
|
||||
return null;
|
||||
}
|
||||
root = root[0];
|
||||
}
|
||||
|
||||
if (root.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (root.TryGetProperty("json", out var jsonChild) && jsonChild.ValueKind == JsonValueKind.Object)
|
||||
root = jsonChild;
|
||||
else if (root.TryGetProperty("output", out var outChild) && outChild.ValueKind == JsonValueKind.Object)
|
||||
root = outChild;
|
||||
else if (root.TryGetProperty("data", out var dataChild) && dataChild.ValueKind == JsonValueKind.Object)
|
||||
root = dataChild;
|
||||
else if (root.TryGetProperty("body", out var bodyChild) && bodyChild.ValueKind == JsonValueKind.Object)
|
||||
root = bodyChild;
|
||||
}
|
||||
|
||||
var result = root.Deserialize(FinlyticJsonSerializerContext.Default.N8nResponsePayload);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[N8nService] Failed to communicate with or parse response from n8n webhook workflow.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticNews.Database;
|
||||
using FinlyticNews.Entities;
|
||||
using FinlyticNews.Util;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for managing and querying blocked news article URLs (duplicates, missing content, or zero matched assets).
|
||||
/// </summary>
|
||||
public interface INewsBlocklistService
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether the given URL is already recorded on the blocklist.
|
||||
/// </summary>
|
||||
bool IsBlocked(string url);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a URL to the database blocklist and in-memory cache.
|
||||
/// </summary>
|
||||
Task BlockUrlAsync(string url, string reason, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Loads all existing blocked URLs from the database into memory on startup.
|
||||
/// </summary>
|
||||
Task InitializeAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// High-performance in-memory and database-backed blocklist service.
|
||||
/// </summary>
|
||||
public class NewsBlocklistService : INewsBlocklistService
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IFinlyticLogger<NewsBlocklistService> _finlyticLogger;
|
||||
private readonly ConcurrentDictionary<string, string> _blockedUrls = new(StringComparer.OrdinalIgnoreCase);
|
||||
private bool _initialized;
|
||||
private readonly SemaphoreSlim _initLock = new(1, 1);
|
||||
|
||||
public NewsBlocklistService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IFinlyticLogger<NewsBlocklistService> finlyticLogger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
await _initLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<NewsDbContext>();
|
||||
|
||||
var urls = await dbContext.BlockedNewsUrls
|
||||
.AsNoTracking()
|
||||
.Select(b => new { b.Url, b.Reason })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var item in urls)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(item.Url))
|
||||
{
|
||||
_blockedUrls[item.Url.Trim()] = item.Reason ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsBlocklistService] Initialized with {Count} blocked URLs in memory cache.", _blockedUrls.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_initLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBlocked(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return true;
|
||||
return _blockedUrls.ContainsKey(url.Trim());
|
||||
}
|
||||
|
||||
public async Task BlockUrlAsync(string url, string reason, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return;
|
||||
var cleanUrl = url.Trim();
|
||||
|
||||
_blockedUrls[cleanUrl] = reason;
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<NewsDbContext>();
|
||||
|
||||
bool exists = await dbContext.BlockedNewsUrls.AnyAsync(b => b.Url == cleanUrl, cancellationToken);
|
||||
if (!exists)
|
||||
{
|
||||
dbContext.BlockedNewsUrls.Add(new BlockedNewsUrlEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Url = cleanUrl,
|
||||
Reason = reason,
|
||||
BlockedAtUtc = DateTime.UtcNow
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsBlocklistService] Added URL to blocklist: {Url} (Reason: {Reason})", cleanUrl, reason);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, ex, "[NewsBlocklistService] Failed to persist blocked URL: {Url}", cleanUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ public interface INewsDbService
|
||||
/// <summary>
|
||||
/// Phase 1: Discovers and locks a new article URL by setting its status to "Pending".
|
||||
/// </summary>
|
||||
Task<NewsArticleEntity> CreatePendingArticleAsync(
|
||||
Task<NewsArticleEntity?> CreatePendingArticleAsync(
|
||||
string url,
|
||||
List<string>? discoveredIsins = null,
|
||||
string? title = null,
|
||||
@@ -35,15 +35,30 @@ public interface INewsDbService
|
||||
/// </summary>
|
||||
Task UpdateArticleStatusAsync(Guid id, string status);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes an article from the database entirely (e.g. when it fails or is a duplicate).
|
||||
/// </summary>
|
||||
Task DeleteArticleAsync(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the target URL of an article if a redirect is resolved during the Processing phase.
|
||||
/// </summary>
|
||||
Task UpdateArticleUrlAsync(Guid id, string resolvedUrl);
|
||||
|
||||
/// <summary>
|
||||
/// Phase 4: Saves AI classification from n8n and sets the lifecycle status to "Completed".
|
||||
/// Saves the processed article content, hashes, and matched assets, and transitions lifecycle status to "Completed".
|
||||
/// </summary>
|
||||
Task<NewsArticleEntity?> SaveArticleClassificationAsync(Guid id, N8nResponsePayload payload, List<MatchedAssetEntity> matchedAssets);
|
||||
Task<NewsArticleEntity?> SaveProcessedArticleAsync(
|
||||
Guid id,
|
||||
string title,
|
||||
string? author,
|
||||
string? summary,
|
||||
string contentRaw,
|
||||
string? language,
|
||||
DateTime? publishedAt,
|
||||
string titleHash,
|
||||
long simHash,
|
||||
List<MatchedAssetEntity> matchedAssets);
|
||||
|
||||
Task<List<NewsArticleEntity>> GetArticlesByStatusAsync(string status);
|
||||
|
||||
@@ -95,7 +110,7 @@ public class NewsDbService : INewsDbService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NewsArticleEntity> CreatePendingArticleAsync(
|
||||
public async Task<NewsArticleEntity?> CreatePendingArticleAsync(
|
||||
string url,
|
||||
List<string>? discoveredIsins = null,
|
||||
string? title = null,
|
||||
@@ -112,7 +127,7 @@ public class NewsDbService : INewsDbService
|
||||
if (existingArticle != null)
|
||||
{
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[Lifecycle] Article URL already exists (Duplicate hit): {Url}", trimmedUrl);
|
||||
return existingArticle;
|
||||
return null;
|
||||
}
|
||||
|
||||
var finalPublishedAt = publishedAt.HasValue
|
||||
@@ -166,9 +181,9 @@ public class NewsDbService : INewsDbService
|
||||
var existing = await _context.NewsArticles.AsNoTracking().FirstOrDefaultAsync(a => a.SourceUrl == trimmedUrl || a.SourceUrl == cleanUrl || a.SourceUrl.StartsWith(cleanUrl));
|
||||
if (existing != null)
|
||||
{
|
||||
return existing;
|
||||
return null;
|
||||
}
|
||||
return article;
|
||||
return null;
|
||||
}
|
||||
|
||||
return article;
|
||||
@@ -191,6 +206,19 @@ public class NewsDbService : INewsDbService
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteArticleAsync(Guid id)
|
||||
{
|
||||
var rowsAffected = await _context.NewsArticles
|
||||
.Where(a => a.Id == id)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (rowsAffected > 0)
|
||||
{
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[Lifecycle] Deleted unused/failed article {Id} from database", id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateArticleUrlAsync(Guid id, string resolvedUrl)
|
||||
{
|
||||
@@ -209,37 +237,41 @@ public class NewsDbService : INewsDbService
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NewsArticleEntity?> SaveArticleClassificationAsync(Guid id, N8nResponsePayload payload, List<MatchedAssetEntity> matchedAssets)
|
||||
public async Task<NewsArticleEntity?> SaveProcessedArticleAsync(
|
||||
Guid id,
|
||||
string title,
|
||||
string? author,
|
||||
string? summary,
|
||||
string contentRaw,
|
||||
string? language,
|
||||
DateTime? publishedAt,
|
||||
string titleHash,
|
||||
long simHash,
|
||||
List<MatchedAssetEntity> matchedAssets)
|
||||
{
|
||||
DateTime? finalPublishedAt = null;
|
||||
if (DateTime.TryParse(payload.PublishedAt, out var publishedDate))
|
||||
{
|
||||
finalPublishedAt = publishedDate.Kind == DateTimeKind.Unspecified
|
||||
? DateTime.SpecifyKind(publishedDate, DateTimeKind.Utc)
|
||||
: publishedDate.ToUniversalTime();
|
||||
}
|
||||
|
||||
DateTime? finalScrapedAt = null;
|
||||
if (DateTime.TryParse(payload.ScrapedAt, out var scrapedDate))
|
||||
{
|
||||
finalScrapedAt = scrapedDate.ToUniversalTime();
|
||||
}
|
||||
var finalPublishedAt = publishedAt.HasValue
|
||||
? (publishedAt.Value.Kind == DateTimeKind.Unspecified
|
||||
? DateTime.SpecifyKind(publishedAt.Value, DateTimeKind.Utc)
|
||||
: publishedAt.Value.ToUniversalTime())
|
||||
: DateTime.UtcNow;
|
||||
|
||||
var rowsAffected = await _context.NewsArticles
|
||||
.Where(a => a.Id == id)
|
||||
.ExecuteUpdateAsync(s => s
|
||||
.SetProperty(a => a.Title, payload.Title)
|
||||
.SetProperty(a => a.Author, payload.Author)
|
||||
.SetProperty(a => a.Summary, payload.Summary)
|
||||
.SetProperty(a => a.ContentRaw, payload.ContentRaw)
|
||||
.SetProperty(a => a.Language, payload.Language)
|
||||
.SetProperty(a => a.Title, title)
|
||||
.SetProperty(a => a.Author, author)
|
||||
.SetProperty(a => a.Summary, summary)
|
||||
.SetProperty(a => a.ContentRaw, contentRaw)
|
||||
.SetProperty(a => a.Language, language)
|
||||
.SetProperty(a => a.TitleHash, titleHash)
|
||||
.SetProperty(a => a.SimHash, simHash)
|
||||
.SetProperty(a => a.Status, "Completed")
|
||||
.SetProperty(a => a.PublishedAt, a => finalPublishedAt ?? a.PublishedAt)
|
||||
.SetProperty(a => a.ScrapedAt, a => finalScrapedAt ?? a.ScrapedAt));
|
||||
.SetProperty(a => a.PublishedAt, finalPublishedAt)
|
||||
.SetProperty(a => a.ScrapedAt, DateTime.UtcNow));
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[NewsChannel] Article with ID {Id} not found for classification update.", id);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[NewsChannel] Article with ID {Id} not found for completion update.", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -267,7 +299,7 @@ public class NewsDbService : INewsDbService
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.Id == id);
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[Lifecycle] Article {Id} successfully classified and marked 'Completed'. Title: '{Title}'", id, payload.Title);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[Lifecycle] Article {Id} successfully processed and marked 'Completed'. Title: '{Title}'", id, title);
|
||||
|
||||
return completedArticle;
|
||||
}
|
||||
@@ -320,7 +352,7 @@ public class NewsDbService : INewsDbService
|
||||
var cleanSearch = searchQuery.Trim().ToLower();
|
||||
query = query.Where(a =>
|
||||
a.Title.ToLower().Contains(cleanSearch) ||
|
||||
a.Summary.ToLower().Contains(cleanSearch) ||
|
||||
(a.Summary != null && a.Summary.ToLower().Contains(cleanSearch)) ||
|
||||
a.MatchedAssets.Any(m => m.Name.ToLower().Contains(cleanSearch)));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,62 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticAssets.Models;
|
||||
using FinlyticAssets.Util;
|
||||
using FinlyticCore.Dtos.News;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Util;
|
||||
using FinlyticNews.Entities;
|
||||
using FinlyticNews.Util;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A background worker service that orchestrates link discovery, Playwright scraping,
|
||||
/// pre-filtering, n8n AI enrichment, database persistence, and MQTT broadcasts.
|
||||
/// Background worker service orchestrating link discovery, Playwright scraping,
|
||||
/// non-AI deduplication, in-memory asset matching, plausibility validation, and MQTT broadcasts.
|
||||
/// </summary>
|
||||
public class NewsScraperBackgroundService : BackgroundService
|
||||
{
|
||||
private record CompiledAssetMatcher(
|
||||
AssetIndex Asset,
|
||||
string CoreName,
|
||||
Regex? WordRegex,
|
||||
Regex? CoreWordRegex
|
||||
);
|
||||
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly IFinlyticLogger<NewsScraperBackgroundService> _finlyticLogger;
|
||||
private readonly NewsMqttClient _mqttClient;
|
||||
private readonly string _indexPath;
|
||||
|
||||
private List<CompiledAssetMatcher>? _cachedAssetMatchers;
|
||||
private DateTime _lastIndexLoadTime = DateTime.MinValue;
|
||||
private readonly INewsBlocklistService _blocklistService;
|
||||
private readonly IArticleDeduplicationService _deduplicationService;
|
||||
private readonly IAssetMatcherService _assetMatcherService;
|
||||
private readonly IAssetValidationService _assetValidationService;
|
||||
|
||||
public NewsScraperBackgroundService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IFinlyticLogger<NewsScraperBackgroundService> finlyticLogger,
|
||||
NewsMqttClient mqttClient,
|
||||
IConfiguration configuration)
|
||||
INewsBlocklistService blocklistService,
|
||||
IArticleDeduplicationService deduplicationService,
|
||||
IAssetMatcherService assetMatcherService,
|
||||
IAssetValidationService assetValidationService)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_finlyticLogger = finlyticLogger;
|
||||
_mqttClient = mqttClient;
|
||||
_indexPath = Path.Combine(Volumes.IndexRelativePath, "index.json");
|
||||
_blocklistService = blocklistService;
|
||||
_deduplicationService = deduplicationService;
|
||||
_assetMatcherService = assetMatcherService;
|
||||
_assetValidationService = assetValidationService;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] NewsScraperBackgroundService started.");
|
||||
|
||||
// Initialize in-memory blocklist and deduplication cache on startup
|
||||
try
|
||||
{
|
||||
await _blocklistService.InitializeAsync(stoppingToken);
|
||||
await _deduplicationService.InitializeAsync(stoppingToken);
|
||||
await _assetMatcherService.ReloadIndexAsync(stoppingToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Error during startup service initialization.");
|
||||
}
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
@@ -76,7 +79,7 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] An unhandled exception occurred during news scraping cycle.");
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Unhandled exception in news scraping cycle.");
|
||||
}
|
||||
|
||||
int intervalMinutes = 15;
|
||||
@@ -89,7 +92,7 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
catch { }
|
||||
|
||||
var jitterSeconds = Random.Shared.Next(0, 60);
|
||||
var nextRunDelay = TimeSpan.FromMinutes(intervalMinutes) + TimeSpan.FromSeconds(jitterSeconds);
|
||||
var nextRunDelay = TimeSpan.FromMinutes(Math.Max(1, intervalMinutes)) + TimeSpan.FromSeconds(jitterSeconds);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Scraping cycle completed. Next cycle in {Delay} (interval: {Minutes}m).", nextRunDelay, intervalMinutes);
|
||||
|
||||
try
|
||||
@@ -111,29 +114,26 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
var discoveryService = scope.ServiceProvider.GetRequiredService<IArticleDiscoveryService>();
|
||||
var scraperService = scope.ServiceProvider.GetRequiredService<IPlaywrightScraperService>();
|
||||
var n8nService = scope.ServiceProvider.GetRequiredService<IN8nService>();
|
||||
var settings = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||
|
||||
var maxArticlesPerFeed = await settings.GetSettingAsync(SettingKeys.MaxArticlesPerFeed, stoppingToken);
|
||||
|
||||
var assetMatchers = await GetOrLoadAssetMatchersAsync();
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Loaded {Count} asset index items for text pre-filtering.", assetMatchers.Count);
|
||||
|
||||
var failedArticles = await dbService.GetArticlesByStatusAsync("Scraping");
|
||||
if (failedArticles.Count > 0)
|
||||
// Process any previously interrupted articles
|
||||
var pendingScraping = await dbService.GetArticlesByStatusAsync("Scraping");
|
||||
if (pendingScraping.Count > 0)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Found {Count} articles in status 'Scraping' that failed to scrape previously. Retrying...", failedArticles.Count);
|
||||
foreach (var article in failedArticles)
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Retrying {Count} pending scraping articles...", pendingScraping.Count);
|
||||
foreach (var article in pendingScraping)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) return;
|
||||
await ProcessSingleArticleAsync(article, scraperService, n8nService, dbService, assetMatchers, stoppingToken);
|
||||
await ProcessSingleArticleAsync(article, scraperService, dbService, stoppingToken);
|
||||
}
|
||||
}
|
||||
|
||||
var sources = await dbService.GetSourcesAsync();
|
||||
if (sources.Count == 0)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] No article sources configured in database. Skipping cycle.");
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] No article sources configured in database.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,30 +141,35 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Starting article link discovery for source: {SourceName} ({Url})", source.Name, source.Source);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Discovering articles from source: {Name} ({Url})", source.Name, source.Source);
|
||||
var discoveredArticles = await discoveryService.DiscoverLinksAsync(source.Source, source.Type, stoppingToken);
|
||||
|
||||
if (discoveredArticles == null || discoveredArticles.Count == 0)
|
||||
{
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] No links discovered from source: {SourceName}", source.Name);
|
||||
continue;
|
||||
}
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Discovered {Count} potential article links from {SourceName}.", discoveredArticles.Count, source.Name);
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Discovered {Count} potential article links from {Name}.", discoveredArticles.Count, source.Name);
|
||||
var toProcess = discoveredArticles.Take(maxArticlesPerFeed > 0 ? maxArticlesPerFeed : 20);
|
||||
|
||||
foreach (var discovered in toProcess)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
// 1. Fast Blocklist & Duplicate check before creating DB entry
|
||||
if (_blocklistService.IsBlocked(discovered.Url))
|
||||
{
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Skipping blocked article URL: {Url}", discovered.Url);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (await dbService.IsUrlDuplicateAsync(discovered.Url))
|
||||
{
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Skipping duplicate article URL: {Url}", discovered.Url);
|
||||
continue;
|
||||
}
|
||||
|
||||
NewsArticleEntity article;
|
||||
NewsArticleEntity? article;
|
||||
try
|
||||
{
|
||||
article = await dbService.CreatePendingArticleAsync(
|
||||
@@ -178,17 +183,13 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Failed to register initial pending state for URL: {Url}. Skipping.", discovered.Url);
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Failed to create pending article for {Url}", discovered.Url);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (article.Id == Guid.Empty)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Created pending article has invalid/empty ID for URL: {Url}. Skipping.", discovered.Url);
|
||||
continue;
|
||||
}
|
||||
if (article == null || article.Id == Guid.Empty) continue;
|
||||
|
||||
await ProcessSingleArticleAsync(article, scraperService, n8nService, dbService, assetMatchers, stoppingToken);
|
||||
await ProcessSingleArticleAsync(article, scraperService, dbService, stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,25 +197,24 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
private async Task ProcessSingleArticleAsync(
|
||||
NewsArticleEntity article,
|
||||
IPlaywrightScraperService scraperService,
|
||||
IN8nService n8nService,
|
||||
INewsDbService dbService,
|
||||
List<CompiledAssetMatcher> assetMatchers,
|
||||
CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await dbService.UpdateArticleStatusAsync(article.Id, "Processing");
|
||||
|
||||
var (resolvedUrl, rawContent) = await scraperService.ScrapeArticleAsync(article.SourceUrl);
|
||||
// 1. Playwright Headless Scrape & Redirect Resolution
|
||||
var (resolvedUrl, scrapeResult) = await scraperService.ScrapeArticleAsync(article.SourceUrl);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resolvedUrl) &&
|
||||
!resolvedUrl.Equals(article.SourceUrl, StringComparison.OrdinalIgnoreCase))
|
||||
// 2. Redirect Handling
|
||||
if (!string.IsNullOrWhiteSpace(resolvedUrl) && !resolvedUrl.Equals(article.SourceUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Redirect detected. Initial: {OldUrl} -> Resolved: {NewUrl}", article.SourceUrl, resolvedUrl);
|
||||
if (await dbService.IsUrlDuplicateAsync(resolvedUrl))
|
||||
if (_blocklistService.IsBlocked(resolvedUrl) || await dbService.IsUrlDuplicateAsync(resolvedUrl))
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Redirected URL {ResolvedUrl} is a duplicate. Terminating processing.", resolvedUrl);
|
||||
await dbService.UpdateArticleStatusAsync(article.Id, "Failed");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Redirected URL {Url} is already blocked/duplicate.", resolvedUrl);
|
||||
await _blocklistService.BlockUrlAsync(article.SourceUrl, "RedirectToDuplicate", stoppingToken);
|
||||
await dbService.DeleteArticleAsync(article.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,66 +222,84 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
article.SourceUrl = resolvedUrl;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rawContent) || rawContent.Length < 60)
|
||||
// 3. Content Validity Check
|
||||
var title = !string.IsNullOrWhiteSpace(scrapeResult.Title) ? scrapeResult.Title : article.Title;
|
||||
var content = scrapeResult.TextContent;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content) || content.Length < 80 || string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
await dbService.UpdateArticleStatusAsync(article.Id, "Failed");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Article {Id} has empty or insufficient content ({Length} chars). Adding to blocklist.", article.Id, content?.Length ?? 0);
|
||||
await _blocklistService.BlockUrlAsync(article.SourceUrl, "EmptyOrInvalidContent", stoppingToken);
|
||||
await dbService.DeleteArticleAsync(article.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var discoveredIsins = article.MatchedAssets.Select(m => m.Isin).Where(i => !string.IsNullOrEmpty(i)).ToList();
|
||||
var preFilteredAssets = PreFilterAssets(rawContent, article.Title, assetMatchers, discoveredIsins);
|
||||
var publishedAt = scrapeResult.PublishedAt ?? article.PublishedAt;
|
||||
|
||||
if (preFilteredAssets.Count == 0)
|
||||
// 4. Non-AI Content & Title Deduplication Check (SimHash + N-Gram)
|
||||
var dupCheck = _deduplicationService.CheckDuplicate(title, content, publishedAt);
|
||||
if (dupCheck.IsDuplicate)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Pre-filtering: Article {Id} does not reference any known assets. Terminating pipeline.", article.Id);
|
||||
await dbService.UpdateArticleStatusAsync(article.Id, "Failed");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.DeduplicationChannel, "[NewsScraperBackgroundService] Article {Id} identified as duplicate of {OriginalId} (Reason: {Reason}). Blocking URL.", article.Id, dupCheck.DuplicateOfArticleId?.ToString() ?? "Unknown", dupCheck.Reason ?? "Duplicate content");
|
||||
await _blocklistService.BlockUrlAsync(article.SourceUrl, $"Duplicate:{dupCheck.DuplicateOfArticleId}", stoppingToken);
|
||||
await dbService.DeleteArticleAsync(article.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Pre-filtering matched {Count} assets for article {Id}.", preFilteredAssets.Count, article.Id);
|
||||
|
||||
var n8nResponse = await n8nService.AnalyzeArticleAsync(rawContent, preFilteredAssets, stoppingToken);
|
||||
if (n8nResponse == null)
|
||||
// 5. In-Memory Asset Matching (ISIN Regex, Name Tokenizer, Suffix Trimming)
|
||||
var candidateMatches = _assetMatcherService.MatchAssets(title, content, article.SourceUrl);
|
||||
if (candidateMatches.Count == 0)
|
||||
{
|
||||
await dbService.UpdateArticleStatusAsync(article.Id, "Failed");
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.MatcherChannel, "[NewsScraperBackgroundService] No candidate assets matched for article {Id} ('{Title}'). Blocking URL.", article.Id, title);
|
||||
await _blocklistService.BlockUrlAsync(article.SourceUrl, "NoMatchedAssets", stoppingToken);
|
||||
await dbService.DeleteArticleAsync(article.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
var matchedEntities = new List<MatchedAssetEntity>();
|
||||
if (n8nResponse.MatchedAssets != null && n8nResponse.MatchedAssets.Count > 0)
|
||||
// 6. Multi-Criteria Asset Validation (Sector Clustering, Title Weighting, Existence)
|
||||
var validatedMatches = await _assetValidationService.ValidateCandidateAssetsAsync(candidateMatches, title, content, stoppingToken);
|
||||
if (validatedMatches.Count == 0)
|
||||
{
|
||||
foreach (var asset in n8nResponse.MatchedAssets)
|
||||
{
|
||||
var preMatch = preFilteredAssets.FirstOrDefault(p => p.Name.Equals(asset.Name, StringComparison.OrdinalIgnoreCase));
|
||||
var isin = preMatch?.Isin ?? asset.Ticker ?? "";
|
||||
if (string.IsNullOrWhiteSpace(isin)) continue;
|
||||
|
||||
matchedEntities.Add(new MatchedAssetEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
NewsArticleId = article.Id,
|
||||
Isin = isin.Trim().ToUpperInvariant(),
|
||||
Name = !string.IsNullOrWhiteSpace(asset.Name) ? asset.Name.Trim() : isin.Trim().ToUpperInvariant()
|
||||
});
|
||||
}
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.MatcherChannel, "[NewsScraperBackgroundService] All candidate matches pruned during validation for article {Id}. Blocking URL.", article.Id);
|
||||
await _blocklistService.BlockUrlAsync(article.SourceUrl, "NoValidatedAssets", stoppingToken);
|
||||
await dbService.DeleteArticleAsync(article.Id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchedEntities.Count == 0)
|
||||
// 7. Prepare Matched Asset Entities
|
||||
var matchedEntities = validatedMatches.Select(m => new MatchedAssetEntity
|
||||
{
|
||||
foreach (var preMatch in preFilteredAssets)
|
||||
{
|
||||
matchedEntities.Add(new MatchedAssetEntity
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
NewsArticleId = article.Id,
|
||||
Isin = preMatch.Isin,
|
||||
Name = preMatch.Name
|
||||
});
|
||||
}
|
||||
Id = Guid.NewGuid(),
|
||||
NewsArticleId = article.Id,
|
||||
Isin = m.Isin,
|
||||
Name = m.Name
|
||||
}).ToList();
|
||||
|
||||
// 8. Generate Clean Summary & Excerpt
|
||||
var summary = scrapeResult.Excerpt;
|
||||
if (string.IsNullOrWhiteSpace(summary))
|
||||
{
|
||||
summary = content.Length > 280 ? content[..280] + "..." : content;
|
||||
}
|
||||
|
||||
var updatedArticle = await dbService.SaveArticleClassificationAsync(article.Id, n8nResponse, matchedEntities);
|
||||
// 9. Persist Completed Article
|
||||
var updatedArticle = await dbService.SaveProcessedArticleAsync(
|
||||
id: article.Id,
|
||||
title: title,
|
||||
author: scrapeResult.Author,
|
||||
summary: summary,
|
||||
contentRaw: content,
|
||||
language: scrapeResult.Language ?? article.Language ?? "de",
|
||||
publishedAt: publishedAt,
|
||||
titleHash: dupCheck.TitleHash,
|
||||
simHash: dupCheck.SimHash,
|
||||
matchedAssets: matchedEntities
|
||||
);
|
||||
|
||||
// 10. Register in deduplication memory cache
|
||||
_deduplicationService.RegisterArticle(article.Id, dupCheck.TitleHash, dupCheck.SimHash, title, publishedAt);
|
||||
|
||||
// 11. Broadcast via MQTT
|
||||
if (updatedArticle != null)
|
||||
{
|
||||
var dto = new NewsArticleDto
|
||||
@@ -304,124 +322,13 @@ public class NewsScraperBackgroundService : BackgroundService
|
||||
};
|
||||
|
||||
await _mqttClient.BroadcastArticleAsync(dto);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Successfully processed and broadcasted article {Id} ('{Title}') with {Count} matched assets.", article.Id, title, matchedEntities.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Failed to complete processing pipeline for article: {Url}. Transitioning to 'Scraping' for next cycle retry.", article.SourceUrl);
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Exception processing article {Url}. Flagging for next cycle retry.", article.SourceUrl);
|
||||
await dbService.UpdateArticleStatusAsync(article.Id, "Scraping");
|
||||
}
|
||||
}
|
||||
|
||||
private List<FilteredAssetPayload> PreFilterAssets(
|
||||
string content,
|
||||
string? title,
|
||||
List<CompiledAssetMatcher> assetMatchers,
|
||||
List<string>? priorityIsins = null)
|
||||
{
|
||||
if (assetMatchers.Count == 0) return new List<FilteredAssetPayload>();
|
||||
|
||||
var fullText = (title != null ? title + " " + content : content);
|
||||
var matched = new Dictionary<string, FilteredAssetPayload>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (priorityIsins != null && priorityIsins.Count > 0)
|
||||
{
|
||||
foreach (var isin in priorityIsins)
|
||||
{
|
||||
var match = assetMatchers.FirstOrDefault(m => string.Equals(m.Asset.Isin, isin, StringComparison.OrdinalIgnoreCase));
|
||||
if (match != null && !matched.ContainsKey(match.Asset.Isin))
|
||||
{
|
||||
matched[match.Asset.Isin] = new FilteredAssetPayload(match.Asset.Name, match.Asset.Isin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var m in assetMatchers)
|
||||
{
|
||||
if (matched.ContainsKey(m.Asset.Isin)) continue;
|
||||
|
||||
if (fullText.Contains(m.Asset.Isin, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matched[m.Asset.Isin] = new FilteredAssetPayload(m.Asset.Name, m.Asset.Isin);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m.WordRegex != null && m.WordRegex.IsMatch(fullText))
|
||||
{
|
||||
matched[m.Asset.Isin] = new FilteredAssetPayload(m.Asset.Name, m.Asset.Isin);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m.CoreWordRegex != null && m.CoreWordRegex.IsMatch(fullText))
|
||||
{
|
||||
matched[m.Asset.Isin] = new FilteredAssetPayload(m.Asset.Name, m.Asset.Isin);
|
||||
}
|
||||
}
|
||||
|
||||
return matched.Values.ToList();
|
||||
}
|
||||
|
||||
private async Task<List<CompiledAssetMatcher>> GetOrLoadAssetMatchersAsync()
|
||||
{
|
||||
if (_cachedAssetMatchers != null && (DateTime.UtcNow - _lastIndexLoadTime).TotalMinutes < 60)
|
||||
{
|
||||
return _cachedAssetMatchers;
|
||||
}
|
||||
|
||||
if (!File.Exists(_indexPath))
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[NewsScraperBackgroundService] Asset index file not found at: {Path}. Pre-filtering will match 0 assets.", _indexPath);
|
||||
return new List<CompiledAssetMatcher>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(_indexPath);
|
||||
var indexList = await JsonSerializer.DeserializeAsync<List<AssetIndex>>(stream);
|
||||
|
||||
if (indexList == null || indexList.Count == 0)
|
||||
{
|
||||
return new List<CompiledAssetMatcher>();
|
||||
}
|
||||
|
||||
var compiled = new List<CompiledAssetMatcher>(indexList.Count);
|
||||
foreach (var asset in indexList)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(asset.Name) || string.IsNullOrWhiteSpace(asset.Isin))
|
||||
continue;
|
||||
|
||||
var rawName = asset.Name.Trim();
|
||||
var coreName = ExtractCoreName(rawName);
|
||||
|
||||
Regex? wordRegex = null;
|
||||
if (rawName.Length >= 4)
|
||||
{
|
||||
wordRegex = new Regex($@"\b{Regex.Escape(rawName)}\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
Regex? coreWordRegex = null;
|
||||
if (!string.IsNullOrWhiteSpace(coreName) && coreName.Length >= 4 && !coreName.Equals(rawName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
coreWordRegex = new Regex($@"\b{Regex.Escape(coreName)}\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
compiled.Add(new CompiledAssetMatcher(asset, coreName, wordRegex, coreWordRegex));
|
||||
}
|
||||
|
||||
_cachedAssetMatchers = compiled;
|
||||
_lastIndexLoadTime = DateTime.UtcNow;
|
||||
return _cachedAssetMatchers;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[NewsScraperBackgroundService] Failed to read or parse asset index file from {Path}.", _indexPath);
|
||||
return _cachedAssetMatchers ?? new List<CompiledAssetMatcher>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExtractCoreName(string rawName)
|
||||
{
|
||||
var cleaned = Regex.Replace(rawName, @"\b(AG|SE|SA|NV|PLC|INC|CORP|LLC|GMBH|CO|KG|HOLDING|GROUP|CLASS\s+[A-Z])\b", "", RegexOptions.IgnoreCase);
|
||||
return cleaned.Trim(' ', '.', ',', '-');
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,16 @@ using Microsoft.Playwright;
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a headless scraping service for extracting text and resolving redirects from news sites.
|
||||
/// Defines a headless scraping service for extracting text, metadata, and resolving redirects from news sites.
|
||||
/// </summary>
|
||||
public interface IPlaywrightScraperService
|
||||
{
|
||||
/// <summary>
|
||||
/// Scrapes the text body of an article, automatically following redirects and applying site-specific scraper adapters.
|
||||
/// Scrapes an article, automatically following redirects and applying site-specific scraper adapters.
|
||||
/// </summary>
|
||||
/// <param name="url">The initial article URL.</param>
|
||||
/// <returns>A tuple containing the final resolved URL and the extracted raw text content.</returns>
|
||||
Task<(string ResolvedUrl, string Content)> ScrapeArticleAsync(string url);
|
||||
/// <returns>A tuple containing the final resolved URL and the structured scrape result.</returns>
|
||||
Task<(string ResolvedUrl, ScrapedArticleResult Result)> ScrapeArticleAsync(string url);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -45,9 +45,9 @@ public class PlaywrightScraperService : IPlaywrightScraperService, IAsyncDisposa
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string ResolvedUrl, string Content)> ScrapeArticleAsync(string url)
|
||||
public async Task<(string ResolvedUrl, ScrapedArticleResult Result)> ScrapeArticleAsync(string url)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[PlaywrightScraperService] Launching browser context to scrape article: {Url}", url);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.ScraperChannel, "[PlaywrightScraperService] Launching browser context to scrape article: {Url}", url);
|
||||
|
||||
var browser = await GetOrInitBrowserAsync();
|
||||
|
||||
@@ -73,8 +73,6 @@ public class PlaywrightScraperService : IPlaywrightScraperService, IAsyncDisposa
|
||||
}
|
||||
|
||||
var finalUrl = page.Url;
|
||||
await _finlyticLogger.LogDebugAsync(SettingKeys.NewsChannel, "[PlaywrightScraperService] Navigation completed. Initial final URL: {Url}", finalUrl);
|
||||
|
||||
var host = new Uri(finalUrl).Host;
|
||||
var adapter = _scraperAdapters.FirstOrDefault(a =>
|
||||
host.EndsWith(a.Hostname, StringComparison.OrdinalIgnoreCase) ||
|
||||
@@ -82,14 +80,13 @@ public class PlaywrightScraperService : IPlaywrightScraperService, IAsyncDisposa
|
||||
|
||||
if (adapter != null)
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[PlaywrightScraperService] Executing adapter redirect check for host: {Host}", adapter.Hostname);
|
||||
try
|
||||
{
|
||||
var resolvedRedirectUrl = await adapter.TryResolveRedirectUrlAsync(page);
|
||||
if (!string.IsNullOrWhiteSpace(resolvedRedirectUrl) &&
|
||||
!resolvedRedirectUrl.Equals(finalUrl, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "[PlaywrightScraperService] Redirect resolved to target URL: {Url}", resolvedRedirectUrl);
|
||||
await _finlyticLogger.LogInfoAsync(SettingKeys.ScraperChannel, "[PlaywrightScraperService] Redirect resolved to target URL: {Url}", resolvedRedirectUrl);
|
||||
|
||||
var redirectResponse = await page.GotoAsync(resolvedRedirectUrl, new PageGotoOptions
|
||||
{
|
||||
@@ -97,11 +94,6 @@ public class PlaywrightScraperService : IPlaywrightScraperService, IAsyncDisposa
|
||||
Timeout = 30000
|
||||
});
|
||||
|
||||
if (redirectResponse == null)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, "[PlaywrightScraperService] Failed to load response for redirect URL: {Url}", resolvedRedirectUrl);
|
||||
}
|
||||
|
||||
finalUrl = page.Url;
|
||||
host = new Uri(finalUrl).Host;
|
||||
|
||||
@@ -112,26 +104,59 @@ public class PlaywrightScraperService : IPlaywrightScraperService, IAsyncDisposa
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, ex, "[PlaywrightScraperService] Failed to resolve redirect through adapter for host: {Host}. Continuing with current page.", adapter.Hostname);
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.ScraperChannel, ex, "[PlaywrightScraperService] Failed to resolve redirect through adapter for host: {Host}. Continuing with current page.", adapter?.Hostname ?? host);
|
||||
}
|
||||
}
|
||||
|
||||
string content;
|
||||
if (adapter != null)
|
||||
ScrapedArticleResult? result = null;
|
||||
|
||||
for (int attempt = 1; attempt <= 2; attempt++)
|
||||
{
|
||||
var result = await adapter.ExtractArticleContentAsync(page);
|
||||
content = result?.TextContent ?? await FallbackExtractContentAsync(page);
|
||||
}
|
||||
else
|
||||
{
|
||||
content = await FallbackExtractContentAsync(page);
|
||||
try
|
||||
{
|
||||
if (adapter != null)
|
||||
{
|
||||
result = await adapter.ExtractArticleContentAsync(page);
|
||||
}
|
||||
|
||||
if (result == null || string.IsNullOrWhiteSpace(result.TextContent))
|
||||
{
|
||||
var bodyText = await FallbackExtractContentAsync(page);
|
||||
result = new ScrapedArticleResult(
|
||||
Title: await page.TitleAsync(),
|
||||
TextContent: bodyText,
|
||||
HtmlContent: string.Empty,
|
||||
Author: null,
|
||||
Excerpt: null,
|
||||
FinalUrl: finalUrl
|
||||
);
|
||||
}
|
||||
|
||||
break; // Extraction succeeded without execution context getting destroyed
|
||||
}
|
||||
catch (PlaywrightException ex) when (ex.Message.Contains("Execution context was destroyed") && attempt == 1)
|
||||
{
|
||||
await _finlyticLogger.LogWarningAsync(SettingKeys.ScraperChannel, "[PlaywrightScraperService] Execution context destroyed (likely JS/Meta redirect). Waiting for new page load...");
|
||||
|
||||
try
|
||||
{
|
||||
await page.WaitForLoadStateAsync(LoadState.DOMContentLoaded, new PageWaitForLoadStateOptions { Timeout = 15000 });
|
||||
}
|
||||
catch (TimeoutException) { /* Ignored, try extracting anyway */ }
|
||||
|
||||
finalUrl = page.Url;
|
||||
host = new Uri(finalUrl).Host;
|
||||
adapter = _scraperAdapters.FirstOrDefault(a =>
|
||||
host.EndsWith(a.Hostname, StringComparison.OrdinalIgnoreCase) ||
|
||||
a.Hostname.EndsWith(host, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
return (finalUrl, content);
|
||||
return (finalUrl, result!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "[PlaywrightScraperService] Failed to scrape page content from URL: {Url}", url);
|
||||
await _finlyticLogger.LogErrorAsync(SettingKeys.ScraperChannel, ex, "[PlaywrightScraperService] Failed to scrape page content from URL: {Url}", url);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
using FinlyticNews.Database;
|
||||
using FinlyticNews.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace FinlyticNews.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service interface for retrieving and persisting runtime settings in PostgreSQL for FinlyticNews.
|
||||
/// </summary>
|
||||
public interface ISettingsDbService
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves current news settings from database, initializing default values if empty.
|
||||
/// </summary>
|
||||
Task<NewsSettingsEntity> GetSettingsAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Persists updated settings to PostgreSQL.
|
||||
/// </summary>
|
||||
Task<NewsSettingsEntity> SaveSettingsAsync(NewsSettingsEntity settings);
|
||||
|
||||
/// <summary>
|
||||
/// Updates settings from a key-value dictionary received via Admin Panel MQTT events.
|
||||
/// </summary>
|
||||
Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EF Core implementation of <see cref="ISettingsDbService"/>.
|
||||
/// </summary>
|
||||
public class SettingsDbService : ISettingsDbService
|
||||
{
|
||||
private readonly NewsDbContext _context;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SettingsDbService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="context">The database context mapping database tables.</param>
|
||||
public SettingsDbService(NewsDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NewsSettingsEntity> GetSettingsAsync()
|
||||
{
|
||||
var settings = await _context.Settings.AsNoTracking().FirstOrDefaultAsync();
|
||||
if (settings == null)
|
||||
{
|
||||
settings = new NewsSettingsEntity { Id = Guid.NewGuid() };
|
||||
_context.Settings.Add(settings);
|
||||
await _context.SaveChangesAsync();
|
||||
_context.ChangeTracker.Clear();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<NewsSettingsEntity> SaveSettingsAsync(NewsSettingsEntity settings)
|
||||
{
|
||||
var existing = await _context.Settings.FirstOrDefaultAsync();
|
||||
if (existing == null)
|
||||
{
|
||||
if (settings.Id == Guid.Empty) settings.Id = Guid.NewGuid();
|
||||
_context.Settings.Add(settings);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.PollingFrequencyMinutes = settings.PollingFrequencyMinutes;
|
||||
existing.ArticleRetentionDays = settings.ArticleRetentionDays;
|
||||
existing.DefaultPageSize = settings.DefaultPageSize;
|
||||
existing.ScrapingIntervalMinutes = settings.ScrapingIntervalMinutes;
|
||||
existing.N8nWebhookUrl = settings.N8nWebhookUrl;
|
||||
existing.UpdatedAt = settings.UpdatedAt;
|
||||
_context.Settings.Update(existing);
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
return settings;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task UpdateSettingsFromDictionaryAsync(Dictionary<string, string> dictionary)
|
||||
{
|
||||
var settings = await GetSettingsAsync();
|
||||
|
||||
foreach (var (key, value) in dictionary)
|
||||
{
|
||||
if (string.Equals(key, "PollingFrequencyMinutes", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var pfm))
|
||||
settings.PollingFrequencyMinutes = pfm;
|
||||
else if (string.Equals(key, "ArticleRetentionDays", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var ard))
|
||||
settings.ArticleRetentionDays = ard;
|
||||
else if (string.Equals(key, "DefaultPageSize", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var dps))
|
||||
settings.DefaultPageSize = dps;
|
||||
else if (string.Equals(key, "ScrapingIntervalMinutes", StringComparison.OrdinalIgnoreCase) && int.TryParse(value, out var sim))
|
||||
settings.ScrapingIntervalMinutes = sim;
|
||||
else if (string.Equals(key, "N8nWebhookUrl", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(value))
|
||||
settings.N8nWebhookUrl = value.Trim();
|
||||
}
|
||||
|
||||
settings.UpdatedAt = DateTime.UtcNow;
|
||||
await SaveSettingsAsync(settings);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FinlyticCore.Dtos;
|
||||
@@ -12,7 +10,6 @@ using FinlyticCore.Dtos.Settings;
|
||||
using FinlyticCore.Models;
|
||||
using FinlyticCore.Services;
|
||||
using FinlyticCore.Util;
|
||||
using FinlyticNews.Database;
|
||||
using FinlyticNews.Entities;
|
||||
using FinlyticNews.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
@@ -23,7 +20,7 @@ using Microsoft.Extensions.Logging;
|
||||
namespace FinlyticNews.Util;
|
||||
|
||||
/// <summary>
|
||||
/// A managed MQTT client for broadcasting completed news articles and responding to RPC requests.
|
||||
/// Managed MQTT client for broadcasting completed news articles and handling typed RPC requests for FinlyticNews.
|
||||
/// </summary>
|
||||
public class NewsMqttClient : ManagedMqttClient, IHostedService
|
||||
{
|
||||
@@ -44,12 +41,7 @@ public class NewsMqttClient : ManagedMqttClient, IHostedService
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var config = new MqttConfiguration
|
||||
{
|
||||
Host = _configuration["MQTT:Host"] ?? _configuration["MQTT__Host"] ?? "localhost",
|
||||
Port = Convert.ToInt32(_configuration["MQTT:Port"] ?? _configuration["MQTT__Port"] ?? "1883"),
|
||||
ClientId = $"{(_configuration["MQTT:ClientId"] ?? _configuration["MQTT__ClientId"] ?? "FinlyticNews")}_{Guid.NewGuid()}"
|
||||
};
|
||||
var config = MqttConfiguration.FromConfiguration(_configuration, "FinlyticNews");
|
||||
|
||||
_logger.LogInformation("Starting News MQTT client. Host: {Host}, ClientId: {ClientId}", config.Host, config.ClientId);
|
||||
await ConnectAsync(config);
|
||||
@@ -65,22 +57,22 @@ public class NewsMqttClient : ManagedMqttClient, IHostedService
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("News MQTT client connected. Subscribing to RPC topics...");
|
||||
_logger.LogInformation("News MQTT client connected. Registering RPC topic subscriptions...");
|
||||
|
||||
await SubscribeAsync("services/request/news_Get/#");
|
||||
await SubscribeAsync("services/request/news_GetById/#");
|
||||
await SubscribeAsync("services/request/news_GetPending/#");
|
||||
await SubscribeAsync("services/request/news_UpdateStatus/#");
|
||||
await SubscribeAsync("services/request/news_settings_GetAll/#");
|
||||
await SubscribeAsync("services/request/news_settings_Update/#");
|
||||
await SubscribeAsync("services/request/health_Ping/#");
|
||||
await SubscribeAsync("services/config/updated/#");
|
||||
await SubscribeAsync(MqttTopics.ResponseWildcard);
|
||||
await SubscribeRpcAsync<DailyNewsRequest, List<NewsArticleDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.NewsGet), HandleNewsGetRpcAsync);
|
||||
await SubscribeRpcAsync<ArticleRequest, NewsArticleDto?>(MqttTopics.RequestFilter(MqttTopics.Channels.NewsGetById), HandleNewsGetByIdRpcAsync);
|
||||
await SubscribeRpcAsync<object, List<NewsArticleDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.NewsGetPending), HandleNewsGetPendingRpcAsync);
|
||||
await SubscribeRpcAsync<UpdateNewsStatusRequest, UpdateNewsStatusResponse>(MqttTopics.RequestFilter(MqttTopics.Channels.NewsUpdateStatus), HandleNewsUpdateStatusRpcAsync);
|
||||
await SubscribeRpcAsync<object, List<DynamicSettingDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.NewsSettingsGetAll), HandleSettingsGetAllRpcAsync);
|
||||
await SubscribeRpcAsync<Dictionary<string, object?>, List<DynamicSettingDto>>(MqttTopics.RequestFilter(MqttTopics.Channels.NewsSettingsUpdate), HandleSettingsUpdateRpcAsync);
|
||||
await SubscribeAsync<object>(MqttTopics.RequestFilter(MqttTopics.Channels.HealthPing), HandleHealthPingRpcAsync);
|
||||
|
||||
FinlyticCore.Services.FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
||||
FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
||||
{
|
||||
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticNews", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await PublishAsync("finlytic/logs/FinlyticNews", logDto);
|
||||
await PublishAsync(MqttTopics.Logs("FinlyticNews"), logDto);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -90,317 +82,126 @@ public class NewsMqttClient : ManagedMqttClient, IHostedService
|
||||
/// </summary>
|
||||
public async Task BroadcastArticleAsync(NewsArticleDto article)
|
||||
{
|
||||
const string topic = "services/news/completed";
|
||||
const string topic = MqttTopics.NewsCompleted;
|
||||
_logger.LogInformation("Broadcasting completed article to MQTT topic: {Topic}. ID: {Id}", topic, article.Id);
|
||||
await PublishAsync(topic, article);
|
||||
|
||||
var firstIsin = article.MatchedAssets.FirstOrDefault()?.Isin;
|
||||
if (!string.IsNullOrWhiteSpace(firstIsin))
|
||||
{
|
||||
string isinTopic = $"finlytic/news/stream/{firstIsin.Trim().ToLowerInvariant()}";
|
||||
string isinTopic = MqttTopics.NewsStream(firstIsin);
|
||||
await PublishAsync(isinTopic, article);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnMessageReceivedAsync(string topic, string payload)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(topic)) return;
|
||||
|
||||
if (topic.StartsWith("services/config/updated", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (topic.EndsWith("FinlyticNews", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await OnConfigUpdatedAsync(payload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var segments = topic.Split('/');
|
||||
if (segments.Length < 4) return;
|
||||
|
||||
var channel = segments[2];
|
||||
var correlationId = segments[^1];
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case "news_Get":
|
||||
await OnGetNewsAsync(payload, correlationId);
|
||||
break;
|
||||
|
||||
case "news_GetById":
|
||||
await OnGetNewsByIdAsync(payload, correlationId);
|
||||
break;
|
||||
|
||||
case "news_GetPending":
|
||||
await OnGetPendingNewsAsync(payload, correlationId);
|
||||
break;
|
||||
|
||||
case "news_UpdateStatus":
|
||||
await OnUpdateNewsStatusAsync(payload, correlationId);
|
||||
break;
|
||||
|
||||
case "news_settings_GetAll":
|
||||
await OnSettingsGetAllAsync(correlationId);
|
||||
break;
|
||||
|
||||
case "news_settings_Update":
|
||||
await OnSettingsUpdateAsync(payload, correlationId);
|
||||
break;
|
||||
|
||||
case "health_Ping":
|
||||
await OnHealthPingAsync(segments, correlationId);
|
||||
break;
|
||||
|
||||
default:
|
||||
_logger.LogDebug("Received unhandled RPC channel: {Channel}", channel);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnGetNewsAsync(string payload, string correlationId)
|
||||
private async Task<List<NewsArticleDto>> HandleNewsGetRpcAsync(DailyNewsRequest? req, string correlationId)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<NewsMqttClient>>();
|
||||
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "Received RPC news_Get request. Correlation: {CorrelationId}", correlationId);
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "Received RPC news_Get request [CorrelationId: {CorrelationId}]", correlationId);
|
||||
|
||||
int limit = 20;
|
||||
int offset = 0;
|
||||
string? isin = null;
|
||||
DateTime? date = null;
|
||||
string? status = null;
|
||||
string? searchQuery = null;
|
||||
int limit = req?.Limit > 0 ? req.Limit : 20;
|
||||
int offset = req?.Offset >= 0 ? req.Offset : 0;
|
||||
string? isin = !string.IsNullOrWhiteSpace(req?.Isin) ? req.Isin : null;
|
||||
string? status = req?.Status;
|
||||
string? searchQuery = req?.Query;
|
||||
DateTime? date = req?.Date;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(payload))
|
||||
if (req?.HasSentiment == true && string.IsNullOrEmpty(status))
|
||||
{
|
||||
try
|
||||
{
|
||||
var req = JsonSerializer.Deserialize(payload, FinlyticJsonSerializerContext.Default.DailyNewsRequest);
|
||||
if (req != null)
|
||||
{
|
||||
limit = req.Limit > 0 ? req.Limit : 20;
|
||||
offset = req.Offset >= 0 ? req.Offset : 0;
|
||||
isin = !string.IsNullOrWhiteSpace(req.Isin) ? req.Isin : null;
|
||||
status = req.Status;
|
||||
searchQuery = req.Query;
|
||||
date = req.Date;
|
||||
|
||||
if (req.HasSentiment == true && string.IsNullOrEmpty(status))
|
||||
{
|
||||
status = "Analyzed";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await finlyticLogger.LogWarningAsync(SettingKeys.NewsChannel, ex, "Failed to parse DailyNewsRequest payload on news_Get");
|
||||
}
|
||||
status = "Analyzed";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
var articles = await dbService.GetFilteredNewsAsync(limit, offset, isin, date, status, searchQuery);
|
||||
var dtos = (await Task.WhenAll(articles.Select(a => MapToDtoAsync(a)))).ToList();
|
||||
|
||||
string responseTopic = $"services/response/news_Get/{correlationId}";
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.NewsChannel, "Publishing RPC response to {ResponseTopic} with {Count} articles.", responseTopic, dtos.Count);
|
||||
await PublishAsync(responseTopic, dtos);
|
||||
return (await Task.WhenAll(articles.Select(a => MapToDtoAsync(a)))).ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await finlyticLogger.LogErrorAsync(SettingKeys.NewsChannel, ex, "Failed to compile RPC response for news_Get");
|
||||
try
|
||||
{
|
||||
string responseTopic = $"services/response/news_Get/{correlationId}";
|
||||
await PublishAsync(responseTopic, new List<NewsArticleDto>());
|
||||
}
|
||||
catch { }
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnGetNewsByIdAsync(string payload, string correlationId)
|
||||
private async Task<NewsArticleDto?> HandleNewsGetByIdRpcAsync(ArticleRequest? req, string correlationId)
|
||||
{
|
||||
string responseTopic = $"services/response/news_GetById/{correlationId}";
|
||||
if (string.IsNullOrWhiteSpace(payload))
|
||||
var targetIdStr = req?.ArticleId ?? req?.Id;
|
||||
if (Guid.TryParse(targetIdStr, out var articleId))
|
||||
{
|
||||
await PublishAsync(responseTopic, (object?)null);
|
||||
return;
|
||||
}
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
var article = await dbService.GetArticleByIdAsync(articleId);
|
||||
|
||||
try
|
||||
{
|
||||
var request = JsonSerializer.Deserialize<ArticleRequest>(payload, FinlyticJsonSerializerContext.Default.ArticleRequest);
|
||||
var targetIdStr = request?.ArticleId ?? request?.Id;
|
||||
|
||||
if (Guid.TryParse(targetIdStr, out var articleId))
|
||||
if (article != null)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
var article = await dbService.GetArticleByIdAsync(articleId);
|
||||
|
||||
if (article != null)
|
||||
{
|
||||
var dto = await MapToDtoAsync(article);
|
||||
await PublishAsync(responseTopic, dto);
|
||||
return;
|
||||
}
|
||||
return await MapToDtoAsync(article);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
await PublishAsync(responseTopic, (object?)null);
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task OnGetPendingNewsAsync(string payload, string correlationId)
|
||||
private async Task<List<NewsArticleDto>> HandleNewsGetPendingRpcAsync(object? _, string correlationId)
|
||||
{
|
||||
int limit = 10;
|
||||
if (!string.IsNullOrWhiteSpace(payload))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(payload);
|
||||
if (doc.RootElement.TryGetProperty("limit", out var limitProp) && limitProp.TryGetInt32(out var parsedLimit))
|
||||
{
|
||||
limit = Math.Min(parsedLimit, 10);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
|
||||
var pendingArticles = await dbService.GetArticlesByStatusAsync("Pending");
|
||||
var dtos = (await Task.WhenAll(pendingArticles.Take(limit).Select(a => MapToDtoAsync(a)))).ToList();
|
||||
|
||||
string responseTopic = $"services/response/news_GetPending/{correlationId}";
|
||||
await PublishAsync(responseTopic, dtos);
|
||||
var pendingArticles = await dbService.GetArticlesByStatusAsync("Completed");
|
||||
return (await Task.WhenAll(pendingArticles.Take(10).Select(a => MapToDtoAsync(a)))).ToList();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task OnUpdateNewsStatusAsync(string payload, string correlationId)
|
||||
private async Task<UpdateNewsStatusResponse> HandleNewsUpdateStatusRpcAsync(UpdateNewsStatusRequest? req, string correlationId)
|
||||
{
|
||||
UpdateNewsStatusResponse response;
|
||||
try
|
||||
if (req != null && req.Id != Guid.Empty)
|
||||
{
|
||||
var request = JsonSerializer.Deserialize<UpdateNewsStatusRequest>(payload, FinlyticJsonSerializerContext.Default.UpdateNewsStatusRequest);
|
||||
if (request != null && request.Id != Guid.Empty)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
|
||||
await dbService.UpdateArticleStatusAsync(request.Id, request.Status);
|
||||
response = new UpdateNewsStatusResponse(true, "Status updated successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
response = new UpdateNewsStatusResponse(false, "Invalid payload.");
|
||||
}
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var dbService = scope.ServiceProvider.GetRequiredService<INewsDbService>();
|
||||
await dbService.UpdateArticleStatusAsync(req.Id, req.Status);
|
||||
return new UpdateNewsStatusResponse(true, "Status updated successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
response = new UpdateNewsStatusResponse(false, ex.Message);
|
||||
}
|
||||
|
||||
string responseTopic = $"services/response/news_UpdateStatus/{correlationId}";
|
||||
await PublishAsync(responseTopic, response);
|
||||
return new UpdateNewsStatusResponse(false, "Invalid payload.");
|
||||
}
|
||||
|
||||
private async Task OnSettingsGetAllAsync(string correlationId)
|
||||
private async Task<List<DynamicSettingDto>> HandleSettingsGetAllRpcAsync(object? _, string correlationId)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<NewsMqttClient>>();
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticNews] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId);
|
||||
try
|
||||
{
|
||||
var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||
var responseTopic = $"services/response/news_settings_GetAll/{correlationId}";
|
||||
|
||||
await PublishAsync(responseTopic, settings);
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticNews] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticNews] [Settings_GetAll] Failed to retrieve settings.");
|
||||
}
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticNews] [Settings_GetAll] Retrieving service dynamic settings [CorrelationId: {CorrelationId}]", correlationId);
|
||||
return await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||
}
|
||||
|
||||
private async Task OnSettingsUpdateAsync(string payload, string correlationId)
|
||||
private async Task<List<DynamicSettingDto>> HandleSettingsUpdateRpcAsync(Dictionary<string, object?>? updates, string correlationId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(payload)) return;
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<NewsMqttClient>>();
|
||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticNews] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
|
||||
try
|
||||
if (updates != null && updates.Count > 0)
|
||||
{
|
||||
Dictionary<string, object?>? updates = null;
|
||||
try
|
||||
{
|
||||
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
|
||||
if (list != null)
|
||||
{
|
||||
updates = new Dictionary<string, object?>();
|
||||
foreach (var item in list) updates[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (updates != null && updates.Count > 0)
|
||||
{
|
||||
await settingsService.UpdateSettingsAsync(updates);
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticNews] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
|
||||
}
|
||||
|
||||
var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||
var responseTopic = $"services/response/news_settings_Update/{correlationId}";
|
||||
await PublishAsync(responseTopic, currentSettings);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticNews] [Settings_Update] Failed to update settings.");
|
||||
await settingsService.UpdateSettingsAsync(updates);
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticNews] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
|
||||
}
|
||||
return await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||
}
|
||||
|
||||
private async Task OnConfigUpdatedAsync(string payload)
|
||||
private async Task HandleHealthPingRpcAsync(object? _, string topic, string correlationId)
|
||||
{
|
||||
try
|
||||
if (topic.Contains("FinlyticNews", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(payload);
|
||||
if (doc.RootElement.TryGetProperty("settings", out var settingsProp))
|
||||
{
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, object?>>(settingsProp.GetRawText());
|
||||
if (dict != null && dict.Count > 0)
|
||||
{
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var settings = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||
await settings.UpdateSettingsAsync(dict);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async Task OnHealthPingAsync(string[] segments, string correlationId)
|
||||
{
|
||||
bool isForMe = segments.Length >= 5 && segments[3].Equals("FinlyticNews", StringComparison.OrdinalIgnoreCase);
|
||||
if (isForMe)
|
||||
{
|
||||
string respTopic = $"services/response/health_Ping/{correlationId}";
|
||||
string respTopic = MqttTopics.ResponseTopic(MqttTopics.Channels.HealthPing, correlationId);
|
||||
await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticNews", "Online", DateTime.UtcNow, "Connected"));
|
||||
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<NewsMqttClient>>();
|
||||
await finlyticLogger.LogInfoAsync(SettingKeys.HealthPingChannel, "Responded to live health_Ping RPC request [CorrelationId: {CorrelationId}].", correlationId);
|
||||
@@ -408,7 +209,11 @@ public class NewsMqttClient : ManagedMqttClient, IHostedService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a NewsArticleEntity to a NewsArticleDto while performing zero-latency disk lookups for sentiment summaries.
|
||||
/// Maps a NewsArticleEntity to a NewsArticleDto, enriching it with the FinBERT sentiment result for the
|
||||
/// article by querying FinlyticSentiment over the <see cref="MqttTopics.Channels.SentimentGetArticle"/> RPC
|
||||
/// channel. This previously read a legacy on-disk cache at <c>data/summaries/articles/*.json</c> and
|
||||
/// <c>data/summaries/isin/*.json</c>, but nothing in this repository writes those files anymore (FinlyticSentiment
|
||||
/// persists results to its own database) - the disk lookup was silently degrading Sentiment to always-null.
|
||||
/// </summary>
|
||||
private async Task<NewsArticleDto> MapToDtoAsync(NewsArticleEntity a)
|
||||
{
|
||||
@@ -419,55 +224,27 @@ public class NewsMqttClient : ManagedMqttClient, IHostedService
|
||||
|
||||
try
|
||||
{
|
||||
var targetId = a.Id.ToString();
|
||||
IsinAnalysisEntry? sentimentEntry = null;
|
||||
|
||||
var articlePath = Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "articles", $"{targetId}.json");
|
||||
if (File.Exists(articlePath))
|
||||
if (IsConnected)
|
||||
{
|
||||
try
|
||||
var targetId = a.Id.ToString();
|
||||
var sentimentEntry = await SendRpcRequestAsync<IsinAnalysisEntry, ArticleRequest>(
|
||||
MqttTopics.Channels.SentimentGetArticle,
|
||||
new ArticleRequest(targetId, targetId),
|
||||
TimeSpan.FromSeconds(3));
|
||||
|
||||
if (sentimentEntry?.FinbertResult != null)
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(articlePath);
|
||||
sentimentEntry = JsonSerializer.Deserialize<IsinAnalysisEntry>(json, FinlyticJsonSerializerContext.Default.IsinAnalysisEntry);
|
||||
finbertResult = sentimentEntry.FinbertResult;
|
||||
sentimentLabel = finbertResult.Label;
|
||||
sentimentScore = finbertResult.CompoundScore;
|
||||
confidence = finbertResult.Confidence;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (sentimentEntry == null && a.MatchedAssets != null && a.MatchedAssets.Count > 0)
|
||||
{
|
||||
foreach (var asset in a.MatchedAssets)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(asset.Isin)) continue;
|
||||
var isinPath = Path.Combine(Directory.GetCurrentDirectory(), "data", "summaries", "isin", $"{asset.Isin.Trim()}.json");
|
||||
|
||||
if (File.Exists(isinPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(isinPath);
|
||||
var isinDoc = JsonSerializer.Deserialize<IsinSentimentSummaryDto>(json, FinlyticJsonSerializerContext.Default.IsinSentimentSummaryDto);
|
||||
var match = isinDoc?.Analyses?.FirstOrDefault(entry => string.Equals(entry.Article?.ArticleId?.Trim(), targetId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (match != null)
|
||||
{
|
||||
sentimentEntry = match;
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sentimentEntry?.FinbertResult != null)
|
||||
{
|
||||
finbertResult = sentimentEntry.FinbertResult;
|
||||
sentimentLabel = finbertResult.Label;
|
||||
sentimentScore = finbertResult.CompoundScore;
|
||||
confidence = finbertResult.Confidence;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "[NewsMqttClient] Failed to fetch sentiment for article {ArticleId} via '{Channel}' RPC.", a.Id, MqttTopics.Channels.SentimentGetArticle);
|
||||
}
|
||||
|
||||
return new NewsArticleDto
|
||||
{
|
||||
|
||||
@@ -2,26 +2,36 @@ using FinlyticCore.Models.Settings;
|
||||
|
||||
namespace FinlyticNews.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Service-scoped dynamic setting keys for FinlyticNews.
|
||||
/// </summary>
|
||||
public static class SettingKeys
|
||||
{
|
||||
// --- Logging-Kanäle ---
|
||||
public static readonly SettingKey<bool> NewsChannel = new("Logging.Channel.News", true);
|
||||
public static readonly SettingKey<bool> MqttChannel = new("Logging.Channel.MQTT", true);
|
||||
public static readonly SettingKey<bool> HealthPingChannel = new("Logging.Channel.Health", true);
|
||||
public static readonly SettingKey<bool> ScraperChannel = new("Logging.Channel.Scraper", true);
|
||||
public static readonly SettingKey<bool> MatcherChannel = new("Logging.Channel.Matcher", true);
|
||||
public static readonly SettingKey<bool> DeduplicationChannel = new("Logging.Channel.Deduplication", true);
|
||||
|
||||
// --- Scraping & Feed-Konfiguration ---
|
||||
public static readonly SettingKey<int> ScrapeIntervalMinutes = new("Scraping.IntervalMinutes", 15);
|
||||
public static readonly SettingKey<int> MaxArticlesPerFeed = new("Scraping.MaxArticlesPerFeed", 20);
|
||||
public static readonly SettingKey<bool> EnableAutoScraping = new("Feature.EnableAutoScraping", true);
|
||||
public static readonly SettingKey<int> HttpTimeoutSeconds = new("Scraping.HttpTimeoutSeconds", 20);
|
||||
public static readonly SettingKey<int> HttpTimeoutSeconds = new("Scraping.HttpTimeoutSeconds", 30);
|
||||
|
||||
// --- KI & Sentiment-Konfiguration ---
|
||||
public static readonly SettingKey<int> FinBertBatchSize = new("AI.FinBertBatchSize", 8);
|
||||
public static readonly SettingKey<double> MinSentimentConfidence = new("AI.MinSentimentConfidence", 0.65);
|
||||
// --- Deduplication & Fingerprinting ---
|
||||
public static readonly SettingKey<double> TitleSimilarityThreshold = new("Deduplication.TitleSimilarityThreshold", 0.85);
|
||||
public static readonly SettingKey<int> SimHashMaxHammingDistance = new("Deduplication.SimHashMaxHammingDistance", 3);
|
||||
public static readonly SettingKey<int> DeduplicationWindowDays = new("Deduplication.WindowDays", 7);
|
||||
|
||||
// --- Asset Matching & Validation ---
|
||||
public static readonly SettingKey<int> MinNameLength = new("Matching.MinNameLength", 3);
|
||||
public static readonly SettingKey<bool> EnableSectorClustering = new("Matching.EnableSectorClustering", true);
|
||||
public static readonly SettingKey<bool> RequireFinancialContextForShortNames = new("Matching.RequireFinancialContextForShortNames", true);
|
||||
|
||||
// --- Daten-Retention & Cleanup ---
|
||||
public static readonly SettingKey<int> ArticleRetentionDays = new("Data.ArticleRetentionDays", 90);
|
||||
|
||||
// --- N8N / Webhook-Konfiguration ---
|
||||
public static readonly SettingKey<string> N8nArticleExtractionUrl = new("N8N.ArticleExtractionUrl", "https://n8n.kleidukos.me/webhook/gemini/article/extraction");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace FinlyticAssets.Util;
|
||||
namespace FinlyticNews.Util;
|
||||
|
||||
public class Volumes
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,3 +0,0 @@
|
||||
# playwright-core
|
||||
|
||||
This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright).
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"comment": "Do not edit this file, use utils/roll_browser.js",
|
||||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1148",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "131.0.6778.33"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1148",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "131.0.6778.33"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1277",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "132.0.6834.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1466",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "132.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1465",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "132.0b8"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2104",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
"mac10.15": "1616",
|
||||
"mac11": "1816",
|
||||
"mac11-arm64": "1816",
|
||||
"mac12": "2009",
|
||||
"mac12-arm64": "2009",
|
||||
"ubuntu20.04-x64": "2092",
|
||||
"ubuntu20.04-arm64": "2092"
|
||||
},
|
||||
"browserVersion": "18.2"
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"revision": "1010",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac12": "1010",
|
||||
"mac12-arm64": "1010"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "android",
|
||||
"revision": "1001",
|
||||
"installByDefault": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const { program } = require('./lib/cli/programWithTestStub');
|
||||
program.parse(process.argv);
|
||||
@@ -1,17 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './types/types';
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const minimumMajorNodeVersion = 14;
|
||||
const currentNodeVersion = process.versions.node;
|
||||
const semver = currentNodeVersion.split('.');
|
||||
const [major] = [+semver[0]];
|
||||
|
||||
if (major < minimumMajorNodeVersion) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(
|
||||
'You are running Node.js ' +
|
||||
currentNodeVersion +
|
||||
'.\n' +
|
||||
`Playwright requires Node.js ${minimumMajorNodeVersion} or higher. \n` +
|
||||
'Please update your version of Node.js.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
module.exports = require('./lib/inprocess');
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import playwright from './index.js';
|
||||
|
||||
export const chromium = playwright.chromium;
|
||||
export const firefox = playwright.firefox;
|
||||
export const webkit = playwright.webkit;
|
||||
export const selectors = playwright.selectors;
|
||||
export const devices = playwright.devices;
|
||||
export const errors = playwright.errors;
|
||||
export const request = playwright.request;
|
||||
export const _electron = playwright._electron;
|
||||
export const _android = playwright._android;
|
||||
export default playwright;
|
||||
@@ -1,69 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.AndroidServerLauncherImpl = void 0;
|
||||
var _utilsBundle = require("./utilsBundle");
|
||||
var _utils = require("./utils");
|
||||
var _playwright = require("./server/playwright");
|
||||
var _playwrightServer = require("./remote/playwrightServer");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class AndroidServerLauncherImpl {
|
||||
async launchServer(options = {}) {
|
||||
const playwright = (0, _playwright.createPlaywright)({
|
||||
sdkLanguage: 'javascript',
|
||||
isServer: true
|
||||
});
|
||||
// 1. Pre-connect to the device
|
||||
let devices = await playwright.android.devices({
|
||||
host: options.adbHost,
|
||||
port: options.adbPort,
|
||||
omitDriverInstall: options.omitDriverInstall
|
||||
});
|
||||
if (devices.length === 0) throw new Error('No devices found');
|
||||
if (options.deviceSerialNumber) {
|
||||
devices = devices.filter(d => d.serial === options.deviceSerialNumber);
|
||||
if (devices.length === 0) throw new Error(`No device with serial number '${options.deviceSerialNumber}' not found`);
|
||||
}
|
||||
if (devices.length > 1) throw new Error(`More than one device found. Please specify deviceSerialNumber`);
|
||||
const device = devices[0];
|
||||
const path = options.wsPath ? options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}` : `/${(0, _utils.createGuid)()}`;
|
||||
|
||||
// 2. Start the server
|
||||
const server = new _playwrightServer.PlaywrightServer({
|
||||
mode: 'launchServer',
|
||||
path,
|
||||
maxConnections: 1,
|
||||
preLaunchedAndroidDevice: device
|
||||
});
|
||||
const wsEndpoint = await server.listen(options.port, options.host);
|
||||
|
||||
// 3. Return the BrowserServer interface
|
||||
const browserServer = new _utilsBundle.ws.EventEmitter();
|
||||
browserServer.wsEndpoint = () => wsEndpoint;
|
||||
browserServer.close = () => device.close();
|
||||
browserServer.kill = () => device.close();
|
||||
device.on('close', () => {
|
||||
server.close();
|
||||
browserServer.emit('close');
|
||||
});
|
||||
return browserServer;
|
||||
}
|
||||
}
|
||||
exports.AndroidServerLauncherImpl = AndroidServerLauncherImpl;
|
||||
@@ -1,92 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.BrowserServerLauncherImpl = void 0;
|
||||
var _utilsBundle = require("./utilsBundle");
|
||||
var _clientHelper = require("./client/clientHelper");
|
||||
var _utils = require("./utils");
|
||||
var _instrumentation = require("./server/instrumentation");
|
||||
var _playwright = require("./server/playwright");
|
||||
var _playwrightServer = require("./remote/playwrightServer");
|
||||
var _helper = require("./server/helper");
|
||||
var _stackTrace = require("./utils/stackTrace");
|
||||
var _socksProxy = require("./common/socksProxy");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class BrowserServerLauncherImpl {
|
||||
constructor(browserName) {
|
||||
this._browserName = void 0;
|
||||
this._browserName = browserName;
|
||||
}
|
||||
async launchServer(options = {}) {
|
||||
const playwright = (0, _playwright.createPlaywright)({
|
||||
sdkLanguage: 'javascript',
|
||||
isServer: true
|
||||
});
|
||||
// TODO: enable socks proxy once ipv6 is supported.
|
||||
const socksProxy = false ? new _socksProxy.SocksProxy() : undefined;
|
||||
playwright.options.socksProxyPort = await (socksProxy === null || socksProxy === void 0 ? void 0 : socksProxy.listen(0));
|
||||
|
||||
// 1. Pre-launch the browser
|
||||
const metadata = (0, _instrumentation.serverSideCallMetadata)();
|
||||
const browser = await playwright[this._browserName].launch(metadata, {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? (0, _clientHelper.envObjectToArray)(options.env) : undefined
|
||||
}, toProtocolLogger(options.logger)).catch(e => {
|
||||
const log = _helper.helper.formatBrowserLogs(metadata.log);
|
||||
(0, _stackTrace.rewriteErrorMessage)(e, `${e.message} Failed to launch browser.${log}`);
|
||||
throw e;
|
||||
});
|
||||
const path = options.wsPath ? options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}` : `/${(0, _utils.createGuid)()}`;
|
||||
|
||||
// 2. Start the server
|
||||
const server = new _playwrightServer.PlaywrightServer({
|
||||
mode: 'launchServer',
|
||||
path,
|
||||
maxConnections: Infinity,
|
||||
preLaunchedBrowser: browser,
|
||||
preLaunchedSocksProxy: socksProxy
|
||||
});
|
||||
const wsEndpoint = await server.listen(options.port, options.host);
|
||||
|
||||
// 3. Return the BrowserServer interface
|
||||
const browserServer = new _utilsBundle.ws.EventEmitter();
|
||||
browserServer.process = () => browser.options.browserProcess.process;
|
||||
browserServer.wsEndpoint = () => wsEndpoint;
|
||||
browserServer.close = () => browser.options.browserProcess.close();
|
||||
browserServer[Symbol.asyncDispose] = browserServer.close;
|
||||
browserServer.kill = () => browser.options.browserProcess.kill();
|
||||
browserServer._disconnectForTest = () => server.close();
|
||||
browserServer._userDataDirForTest = browser._userDataDirForTest;
|
||||
browser.options.browserProcess.onclose = (exitCode, signal) => {
|
||||
socksProxy === null || socksProxy === void 0 || socksProxy.close().catch(() => {});
|
||||
server.close();
|
||||
browserServer.emit('close', exitCode, signal);
|
||||
};
|
||||
return browserServer;
|
||||
}
|
||||
}
|
||||
exports.BrowserServerLauncherImpl = BrowserServerLauncherImpl;
|
||||
function toProtocolLogger(logger) {
|
||||
return logger ? (direction, message) => {
|
||||
if (logger.isEnabled('protocol', 'verbose')) logger.log('protocol', 'verbose', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message), [], {});
|
||||
} : undefined;
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.launchBrowserServer = launchBrowserServer;
|
||||
exports.printApiJson = printApiJson;
|
||||
exports.runDriver = runDriver;
|
||||
exports.runServer = runServer;
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var playwright = _interopRequireWildcard(require("../.."));
|
||||
var _server = require("../server");
|
||||
var _transport = require("../protocol/transport");
|
||||
var _playwrightServer = require("../remote/playwrightServer");
|
||||
var _processLauncher = require("../utils/processLauncher");
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
function printApiJson() {
|
||||
// Note: this file is generated by build-playwright-driver.sh
|
||||
console.log(JSON.stringify(require('../../api.json')));
|
||||
}
|
||||
function runDriver() {
|
||||
const dispatcherConnection = new _server.DispatcherConnection();
|
||||
new _server.RootDispatcher(dispatcherConnection, async (rootScope, {
|
||||
sdkLanguage
|
||||
}) => {
|
||||
const playwright = (0, _server.createPlaywright)({
|
||||
sdkLanguage
|
||||
});
|
||||
return new _server.PlaywrightDispatcher(rootScope, playwright);
|
||||
});
|
||||
const transport = new _transport.PipeTransport(process.stdout, process.stdin);
|
||||
transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message));
|
||||
// Certain Language Binding JSON parsers (e.g. .NET) do not like strings with lone surrogates.
|
||||
const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === 'javascript';
|
||||
const replacer = !isJavaScriptLanguageBinding && String.prototype.toWellFormed ? (key, value) => {
|
||||
if (typeof value === 'string') return value.toWellFormed();
|
||||
return value;
|
||||
} : undefined;
|
||||
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message, replacer));
|
||||
transport.onclose = () => {
|
||||
// Drop any messages during shutdown on the floor.
|
||||
dispatcherConnection.onmessage = () => {};
|
||||
(0, _processLauncher.gracefullyProcessExitDoNotHang)(0);
|
||||
};
|
||||
// Ignore the SIGINT signal in the driver process so the parent can gracefully close the connection.
|
||||
// We still will destruct everything (close browsers and exit) when the transport pipe closes.
|
||||
process.on('SIGINT', () => {
|
||||
// Keep the process running.
|
||||
});
|
||||
}
|
||||
async function runServer(options) {
|
||||
const {
|
||||
port,
|
||||
host,
|
||||
path = '/',
|
||||
maxConnections = Infinity,
|
||||
extension
|
||||
} = options;
|
||||
const server = new _playwrightServer.PlaywrightServer({
|
||||
mode: extension ? 'extension' : 'default',
|
||||
path,
|
||||
maxConnections
|
||||
});
|
||||
const wsEndpoint = await server.listen(port, host);
|
||||
process.on('exit', () => server.close().catch(console.error));
|
||||
console.log('Listening on ' + wsEndpoint);
|
||||
process.stdin.on('close', () => (0, _processLauncher.gracefullyProcessExitDoNotHang)(0));
|
||||
}
|
||||
async function launchBrowserServer(browserName, configFile) {
|
||||
let options = {};
|
||||
if (configFile) options = JSON.parse(_fs.default.readFileSync(configFile).toString());
|
||||
const browserType = playwright[browserName];
|
||||
const server = await browserType.launchServer(options);
|
||||
console.log(server.wsEndpoint());
|
||||
}
|
||||
@@ -1,607 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
Object.defineProperty(exports, "program", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _utilsBundle.program;
|
||||
}
|
||||
});
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _os = _interopRequireDefault(require("os"));
|
||||
var _path = _interopRequireDefault(require("path"));
|
||||
var _utilsBundle = require("../utilsBundle");
|
||||
var _driver = require("./driver");
|
||||
var _traceViewer = require("../server/trace/viewer/traceViewer");
|
||||
var playwright = _interopRequireWildcard(require("../.."));
|
||||
var _child_process = require("child_process");
|
||||
var _utils = require("../utils");
|
||||
var _server = require("../server");
|
||||
var _errors = require("../client/errors");
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
const packageJSON = require('../../package.json');
|
||||
_utilsBundle.program.version('Version ' + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME));
|
||||
_utilsBundle.program.command('mark-docker-image [dockerImageNameTemplate]', {
|
||||
hidden: true
|
||||
}).description('mark docker image').allowUnknownOption(true).action(function (dockerImageNameTemplate) {
|
||||
(0, _utils.assert)(dockerImageNameTemplate, 'dockerImageNameTemplate is required');
|
||||
(0, _server.writeDockerVersion)(dockerImageNameTemplate).catch(logErrorAndExit);
|
||||
});
|
||||
commandWithOpenOptions('open [url]', 'open page in browser specified via -b, --browser', []).action(function (url, options) {
|
||||
open(options, url, codegenId()).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ open
|
||||
$ open -b webkit https://example.com`);
|
||||
commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions', [['-o, --output <file name>', 'saves the generated script to a file'], ['--target <language>', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit`, codegenId()], ['--save-trace <filename>', 'record a trace for the session and save it to a file'], ['--test-id-attribute <attributeName>', 'use the specified attribute to generate data test ID selectors']]).action(function (url, options) {
|
||||
codegen(options, url).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ codegen
|
||||
$ codegen --target=python
|
||||
$ codegen -b webkit https://example.com`);
|
||||
_utilsBundle.program.command('debug <app> [args...]', {
|
||||
hidden: true
|
||||
}).description('run command in debug mode: disable timeout, open inspector').allowUnknownOption(true).action(function (app, options) {
|
||||
(0, _child_process.spawn)(app, options, {
|
||||
env: {
|
||||
...process.env,
|
||||
PWDEBUG: '1'
|
||||
},
|
||||
stdio: 'inherit'
|
||||
});
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ debug node test.js
|
||||
$ debug npm run test`);
|
||||
function suggestedBrowsersToInstall() {
|
||||
return _server.registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', ');
|
||||
}
|
||||
function defaultBrowsersToInstall(options) {
|
||||
let executables = _server.registry.defaultExecutables();
|
||||
if (options.noShell) executables = executables.filter(e => e.name !== 'chromium-headless-shell');
|
||||
if (options.onlyShell) executables = executables.filter(e => e.name !== 'chromium');
|
||||
return executables;
|
||||
}
|
||||
function checkBrowsersToInstall(args, options) {
|
||||
if (options.noShell && options.onlyShell) throw new Error(`Only one of --no-shell and --only-shell can be specified`);
|
||||
const faultyArguments = [];
|
||||
const executables = [];
|
||||
const handleArgument = arg => {
|
||||
const executable = _server.registry.findExecutable(arg);
|
||||
if (!executable || executable.installType === 'none') faultyArguments.push(arg);else executables.push(executable);
|
||||
if ((executable === null || executable === void 0 ? void 0 : executable.browserName) === 'chromium') executables.push(_server.registry.findExecutable('ffmpeg'));
|
||||
};
|
||||
for (const arg of args) {
|
||||
if (arg === 'chromium') {
|
||||
if (!options.onlyShell) handleArgument('chromium');
|
||||
if (!options.noShell) handleArgument('chromium-headless-shell');
|
||||
} else {
|
||||
handleArgument(arg);
|
||||
}
|
||||
}
|
||||
if (faultyArguments.length) throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
|
||||
return executables;
|
||||
}
|
||||
_utilsBundle.program.command('install [browser...]').description('ensure browsers necessary for this version of Playwright are installed').option('--with-deps', 'install system dependencies for browsers').option('--dry-run', 'do not execute installation, only print information').option('--force', 'force reinstall of stable browser channels').option('--only-shell', 'only install headless shell when installing chromium').option('--no-shell', 'do not install chromium headless shell').action(async function (args, options) {
|
||||
// For '--no-shell' option, commander sets `shell: false` instead.
|
||||
if (options.shell === false) options.noShell = true;
|
||||
if ((0, _utils.isLikelyNpxGlobal)()) {
|
||||
console.error((0, _utils.wrapInASCIIBox)([`WARNING: It looks like you are running 'npx playwright install' without first`, `installing your project's dependencies.`, ``, `To avoid unexpected behavior, please install your dependencies first, and`, `then run Playwright's install command:`, ``, ` npm install`, ` npx playwright install`, ``, `If your project does not yet depend on Playwright, first install the`, `applicable npm package (most commonly @playwright/test), and`, `then run Playwright's install command to download the browsers:`, ``, ` npm install @playwright/test`, ` npx playwright install`, ``].join('\n'), 1));
|
||||
}
|
||||
try {
|
||||
const hasNoArguments = !args.length;
|
||||
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
|
||||
if (options.withDeps) await _server.registry.installDeps(executables, !!options.dryRun);
|
||||
if (options.dryRun) {
|
||||
for (const executable of executables) {
|
||||
var _executable$directory, _executable$downloadU;
|
||||
const version = executable.browserVersion ? `version ` + executable.browserVersion : '';
|
||||
console.log(`browser: ${executable.name}${version ? ' ' + version : ''}`);
|
||||
console.log(` Install location: ${(_executable$directory = executable.directory) !== null && _executable$directory !== void 0 ? _executable$directory : '<system>'}`);
|
||||
if ((_executable$downloadU = executable.downloadURLs) !== null && _executable$downloadU !== void 0 && _executable$downloadU.length) {
|
||||
const [url, ...fallbacks] = executable.downloadURLs;
|
||||
console.log(` Download url: ${url}`);
|
||||
for (let i = 0; i < fallbacks.length; ++i) console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`);
|
||||
}
|
||||
console.log(``);
|
||||
}
|
||||
} else {
|
||||
const forceReinstall = hasNoArguments ? false : !!options.force;
|
||||
await _server.registry.install(executables, forceReinstall);
|
||||
await _server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || 'javascript').catch(e => {
|
||||
e.name = 'Playwright Host validation warning';
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browsers\n${e}`);
|
||||
(0, _utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
}).addHelpText('afterAll', `
|
||||
|
||||
Examples:
|
||||
- $ install
|
||||
Install default browsers.
|
||||
|
||||
- $ install chrome firefox
|
||||
Install custom browsers, supports ${suggestedBrowsersToInstall()}.`);
|
||||
_utilsBundle.program.command('uninstall').description('Removes browsers used by this installation of Playwright from the system (chromium, firefox, webkit, ffmpeg). This does not include branded channels.').option('--all', 'Removes all browsers used by any Playwright installation from the system.').action(async options => {
|
||||
delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC;
|
||||
await _server.registry.uninstall(!!options.all).then(({
|
||||
numberOfBrowsersLeft
|
||||
}) => {
|
||||
if (!options.all && numberOfBrowsersLeft > 0) {
|
||||
console.log('Successfully uninstalled Playwright browsers for the current Playwright installation.');
|
||||
console.log(`There are still ${numberOfBrowsersLeft} browsers left, used by other Playwright installations.\nTo uninstall Playwright browsers for all installations, re-run with --all flag.`);
|
||||
}
|
||||
}).catch(logErrorAndExit);
|
||||
});
|
||||
_utilsBundle.program.command('install-deps [browser...]').description('install dependencies necessary to run browsers (will ask for sudo permissions)').option('--dry-run', 'Do not execute installation commands, only print them').action(async function (args, options) {
|
||||
try {
|
||||
if (!args.length) await _server.registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);else await _server.registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browser dependencies\n${e}`);
|
||||
(0, _utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
- $ install-deps
|
||||
Install dependencies for default browsers.
|
||||
|
||||
- $ install-deps chrome firefox
|
||||
Install dependencies for specific browsers, supports ${suggestedBrowsersToInstall()}.`);
|
||||
const browsers = [{
|
||||
alias: 'cr',
|
||||
name: 'Chromium',
|
||||
type: 'chromium'
|
||||
}, {
|
||||
alias: 'ff',
|
||||
name: 'Firefox',
|
||||
type: 'firefox'
|
||||
}, {
|
||||
alias: 'wk',
|
||||
name: 'WebKit',
|
||||
type: 'webkit'
|
||||
}];
|
||||
for (const {
|
||||
alias,
|
||||
name,
|
||||
type
|
||||
} of browsers) {
|
||||
commandWithOpenOptions(`${alias} [url]`, `open page in ${name}`, []).action(function (url, options) {
|
||||
open({
|
||||
...options,
|
||||
browser: type
|
||||
}, url, options.target).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ ${alias} https://example.com`);
|
||||
}
|
||||
commandWithOpenOptions('screenshot <url> <filename>', 'capture a page screenshot', [['--wait-for-selector <selector>', 'wait for selector before taking a screenshot'], ['--wait-for-timeout <timeout>', 'wait for timeout in milliseconds before taking a screenshot'], ['--full-page', 'whether to take a full page screenshot (entire scrollable area)']]).action(function (url, filename, command) {
|
||||
screenshot(command, command, url, filename).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ screenshot -b webkit https://example.com example.png`);
|
||||
commandWithOpenOptions('pdf <url> <filename>', 'save page as pdf', [['--wait-for-selector <selector>', 'wait for given selector before saving as pdf'], ['--wait-for-timeout <timeout>', 'wait for given timeout in milliseconds before saving as pdf']]).action(function (url, filename, options) {
|
||||
pdf(options, options, url, filename).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ pdf https://example.com example.pdf`);
|
||||
_utilsBundle.program.command('run-driver', {
|
||||
hidden: true
|
||||
}).action(function (options) {
|
||||
(0, _driver.runDriver)();
|
||||
});
|
||||
_utilsBundle.program.command('run-server', {
|
||||
hidden: true
|
||||
}).option('--port <port>', 'Server port').option('--host <host>', 'Server host').option('--path <path>', 'Endpoint Path', '/').option('--max-clients <maxClients>', 'Maximum clients').option('--mode <mode>', 'Server mode, either "default" or "extension"').action(function (options) {
|
||||
(0, _driver.runServer)({
|
||||
port: options.port ? +options.port : undefined,
|
||||
host: options.host,
|
||||
path: options.path,
|
||||
maxConnections: options.maxClients ? +options.maxClients : Infinity,
|
||||
extension: options.mode === 'extension' || !!process.env.PW_EXTENSION_MODE
|
||||
}).catch(logErrorAndExit);
|
||||
});
|
||||
_utilsBundle.program.command('print-api-json', {
|
||||
hidden: true
|
||||
}).action(function (options) {
|
||||
(0, _driver.printApiJson)();
|
||||
});
|
||||
_utilsBundle.program.command('launch-server', {
|
||||
hidden: true
|
||||
}).requiredOption('--browser <browserName>', 'Browser name, one of "chromium", "firefox" or "webkit"').option('--config <path-to-config-file>', 'JSON file with launchServer options').action(function (options) {
|
||||
(0, _driver.launchBrowserServer)(options.browser, options.config);
|
||||
});
|
||||
_utilsBundle.program.command('show-trace [trace...]').option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium').option('-h, --host <host>', 'Host to serve trace on; specifying this option opens trace in a browser tab').option('-p, --port <port>', 'Port to serve trace on, 0 for any free port; specifying this option opens trace in a browser tab').option('--stdin', 'Accept trace URLs over stdin to update the viewer').description('show trace viewer').action(function (traces, options) {
|
||||
if (options.browser === 'cr') options.browser = 'chromium';
|
||||
if (options.browser === 'ff') options.browser = 'firefox';
|
||||
if (options.browser === 'wk') options.browser = 'webkit';
|
||||
const openOptions = {
|
||||
host: options.host,
|
||||
port: +options.port,
|
||||
isServer: !!options.stdin
|
||||
};
|
||||
if (options.port !== undefined || options.host !== undefined) (0, _traceViewer.runTraceInBrowser)(traces, openOptions).catch(logErrorAndExit);else (0, _traceViewer.runTraceViewerApp)(traces, options.browser, openOptions, true).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
$ show-trace https://example.com/trace.zip`);
|
||||
async function launchContext(options, extraOptions) {
|
||||
validateOptions(options);
|
||||
const browserType = lookupBrowserType(options);
|
||||
const launchOptions = extraOptions;
|
||||
if (options.channel) launchOptions.channel = options.channel;
|
||||
launchOptions.handleSIGINT = false;
|
||||
const contextOptions =
|
||||
// Copy the device descriptor since we have to compare and modify the options.
|
||||
options.device ? {
|
||||
...playwright.devices[options.device]
|
||||
} : {};
|
||||
|
||||
// In headful mode, use host device scale factor for things to look nice.
|
||||
// In headless, keep things the way it works in Playwright by default.
|
||||
// Assume high-dpi on MacOS. TODO: this is not perfect.
|
||||
if (!extraOptions.headless) contextOptions.deviceScaleFactor = _os.default.platform() === 'darwin' ? 2 : 1;
|
||||
|
||||
// Work around the WebKit GTK scrolling issue.
|
||||
if (browserType.name() === 'webkit' && process.platform === 'linux') {
|
||||
delete contextOptions.hasTouch;
|
||||
delete contextOptions.isMobile;
|
||||
}
|
||||
if (contextOptions.isMobile && browserType.name() === 'firefox') contextOptions.isMobile = undefined;
|
||||
if (options.blockServiceWorkers) contextOptions.serviceWorkers = 'block';
|
||||
|
||||
// Proxy
|
||||
|
||||
if (options.proxyServer) {
|
||||
launchOptions.proxy = {
|
||||
server: options.proxyServer
|
||||
};
|
||||
if (options.proxyBypass) launchOptions.proxy.bypass = options.proxyBypass;
|
||||
}
|
||||
const browser = await browserType.launch(launchOptions);
|
||||
if (process.env.PWTEST_CLI_IS_UNDER_TEST) {
|
||||
process._didSetSourcesForTest = text => {
|
||||
process.stdout.write('\n-------------8<-------------\n');
|
||||
process.stdout.write(text);
|
||||
process.stdout.write('\n-------------8<-------------\n');
|
||||
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
|
||||
if (autoExitCondition && text.includes(autoExitCondition)) closeBrowser();
|
||||
};
|
||||
// Make sure we exit abnormally when browser crashes.
|
||||
const logs = [];
|
||||
require('playwright-core/lib/utilsBundle').debug.log = (...args) => {
|
||||
const line = require('util').format(...args) + '\n';
|
||||
logs.push(line);
|
||||
process.stderr.write(line);
|
||||
};
|
||||
browser.on('disconnected', () => {
|
||||
const hasCrashLine = logs.some(line => line.includes('process did exit:') && !line.includes('process did exit: exitCode=0, signal=null'));
|
||||
if (hasCrashLine) {
|
||||
process.stderr.write('Detected browser crash.\n');
|
||||
(0, _utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Viewport size
|
||||
if (options.viewportSize) {
|
||||
try {
|
||||
const [width, height] = options.viewportSize.split(',').map(n => parseInt(n, 10));
|
||||
contextOptions.viewport = {
|
||||
width,
|
||||
height
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error('Invalid viewport size format: use "width, height", for example --viewport-size=800,600');
|
||||
}
|
||||
}
|
||||
|
||||
// Geolocation
|
||||
|
||||
if (options.geolocation) {
|
||||
try {
|
||||
const [latitude, longitude] = options.geolocation.split(',').map(n => parseFloat(n.trim()));
|
||||
contextOptions.geolocation = {
|
||||
latitude,
|
||||
longitude
|
||||
};
|
||||
} catch (e) {
|
||||
throw new Error('Invalid geolocation format, should be "lat,long". For example --geolocation="37.819722,-122.478611"');
|
||||
}
|
||||
contextOptions.permissions = ['geolocation'];
|
||||
}
|
||||
|
||||
// User agent
|
||||
|
||||
if (options.userAgent) contextOptions.userAgent = options.userAgent;
|
||||
|
||||
// Lang
|
||||
|
||||
if (options.lang) contextOptions.locale = options.lang;
|
||||
|
||||
// Color scheme
|
||||
|
||||
if (options.colorScheme) contextOptions.colorScheme = options.colorScheme;
|
||||
|
||||
// Timezone
|
||||
|
||||
if (options.timezone) contextOptions.timezoneId = options.timezone;
|
||||
|
||||
// Storage
|
||||
|
||||
if (options.loadStorage) contextOptions.storageState = options.loadStorage;
|
||||
if (options.ignoreHttpsErrors) contextOptions.ignoreHTTPSErrors = true;
|
||||
|
||||
// HAR
|
||||
|
||||
if (options.saveHar) {
|
||||
contextOptions.recordHar = {
|
||||
path: _path.default.resolve(process.cwd(), options.saveHar),
|
||||
mode: 'minimal'
|
||||
};
|
||||
if (options.saveHarGlob) contextOptions.recordHar.urlFilter = options.saveHarGlob;
|
||||
contextOptions.serviceWorkers = 'block';
|
||||
}
|
||||
|
||||
// Close app when the last window closes.
|
||||
|
||||
const context = await browser.newContext(contextOptions);
|
||||
let closingBrowser = false;
|
||||
async function closeBrowser() {
|
||||
// We can come here multiple times. For example, saving storage creates
|
||||
// a temporary page and we call closeBrowser again when that page closes.
|
||||
if (closingBrowser) return;
|
||||
closingBrowser = true;
|
||||
if (options.saveTrace) await context.tracing.stop({
|
||||
path: options.saveTrace
|
||||
});
|
||||
if (options.saveStorage) await context.storageState({
|
||||
path: options.saveStorage
|
||||
}).catch(e => null);
|
||||
if (options.saveHar) await context.close();
|
||||
await browser.close();
|
||||
}
|
||||
context.on('page', page => {
|
||||
page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed.
|
||||
page.on('close', () => {
|
||||
const hasPage = browser.contexts().some(context => context.pages().length > 0);
|
||||
if (hasPage) return;
|
||||
// Avoid the error when the last page is closed because the browser has been closed.
|
||||
closeBrowser().catch(() => {});
|
||||
});
|
||||
});
|
||||
process.on('SIGINT', async () => {
|
||||
await closeBrowser();
|
||||
(0, _utils.gracefullyProcessExitDoNotHang)(130);
|
||||
});
|
||||
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
|
||||
context.setDefaultTimeout(timeout);
|
||||
context.setDefaultNavigationTimeout(timeout);
|
||||
if (options.saveTrace) await context.tracing.start({
|
||||
screenshots: true,
|
||||
snapshots: true
|
||||
});
|
||||
|
||||
// Omit options that we add automatically for presentation purpose.
|
||||
delete launchOptions.headless;
|
||||
delete launchOptions.executablePath;
|
||||
delete launchOptions.handleSIGINT;
|
||||
delete contextOptions.deviceScaleFactor;
|
||||
return {
|
||||
browser,
|
||||
browserName: browserType.name(),
|
||||
context,
|
||||
contextOptions,
|
||||
launchOptions
|
||||
};
|
||||
}
|
||||
async function openPage(context, url) {
|
||||
const page = await context.newPage();
|
||||
if (url) {
|
||||
if (_fs.default.existsSync(url)) url = 'file://' + _path.default.resolve(url);else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:')) url = 'http://' + url;
|
||||
await page.goto(url).catch(error => {
|
||||
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && (0, _errors.isTargetClosedError)(error)) {
|
||||
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
|
||||
// in a stray navigation aborted error. We should ignore it.
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
return page;
|
||||
}
|
||||
async function open(options, url, language) {
|
||||
const {
|
||||
context,
|
||||
launchOptions,
|
||||
contextOptions
|
||||
} = await launchContext(options, {
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
||||
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH
|
||||
});
|
||||
await context._enableRecorder({
|
||||
language,
|
||||
launchOptions,
|
||||
contextOptions,
|
||||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
handleSIGINT: false
|
||||
});
|
||||
await openPage(context, url);
|
||||
}
|
||||
async function codegen(options, url) {
|
||||
const {
|
||||
target: language,
|
||||
output: outputFile,
|
||||
testIdAttribute: testIdAttributeName
|
||||
} = options;
|
||||
const tracesDir = _path.default.join(_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
|
||||
const {
|
||||
context,
|
||||
launchOptions,
|
||||
contextOptions
|
||||
} = await launchContext(options, {
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
||||
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
|
||||
tracesDir
|
||||
});
|
||||
_utilsBundle.dotenv.config({
|
||||
path: 'playwright.env'
|
||||
});
|
||||
await context._enableRecorder({
|
||||
language,
|
||||
launchOptions,
|
||||
contextOptions,
|
||||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
mode: 'recording',
|
||||
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? _path.default.resolve(outputFile) : undefined,
|
||||
handleSIGINT: false
|
||||
});
|
||||
await openPage(context, url);
|
||||
}
|
||||
async function waitForPage(page, captureOptions) {
|
||||
if (captureOptions.waitForSelector) {
|
||||
console.log(`Waiting for selector ${captureOptions.waitForSelector}...`);
|
||||
await page.waitForSelector(captureOptions.waitForSelector);
|
||||
}
|
||||
if (captureOptions.waitForTimeout) {
|
||||
console.log(`Waiting for timeout ${captureOptions.waitForTimeout}...`);
|
||||
await page.waitForTimeout(parseInt(captureOptions.waitForTimeout, 10));
|
||||
}
|
||||
}
|
||||
async function screenshot(options, captureOptions, url, path) {
|
||||
const {
|
||||
context
|
||||
} = await launchContext(options, {
|
||||
headless: true
|
||||
});
|
||||
console.log('Navigating to ' + url);
|
||||
const page = await openPage(context, url);
|
||||
await waitForPage(page, captureOptions);
|
||||
console.log('Capturing screenshot into ' + path);
|
||||
await page.screenshot({
|
||||
path,
|
||||
fullPage: !!captureOptions.fullPage
|
||||
});
|
||||
// launchContext takes care of closing the browser.
|
||||
await page.close();
|
||||
}
|
||||
async function pdf(options, captureOptions, url, path) {
|
||||
if (options.browser !== 'chromium') throw new Error('PDF creation is only working with Chromium');
|
||||
const {
|
||||
context
|
||||
} = await launchContext({
|
||||
...options,
|
||||
browser: 'chromium'
|
||||
}, {
|
||||
headless: true
|
||||
});
|
||||
console.log('Navigating to ' + url);
|
||||
const page = await openPage(context, url);
|
||||
await waitForPage(page, captureOptions);
|
||||
console.log('Saving as pdf into ' + path);
|
||||
await page.pdf({
|
||||
path
|
||||
});
|
||||
// launchContext takes care of closing the browser.
|
||||
await page.close();
|
||||
}
|
||||
function lookupBrowserType(options) {
|
||||
let name = options.browser;
|
||||
if (options.device) {
|
||||
const device = playwright.devices[options.device];
|
||||
name = device.defaultBrowserType;
|
||||
}
|
||||
let browserType;
|
||||
switch (name) {
|
||||
case 'chromium':
|
||||
browserType = playwright.chromium;
|
||||
break;
|
||||
case 'webkit':
|
||||
browserType = playwright.webkit;
|
||||
break;
|
||||
case 'firefox':
|
||||
browserType = playwright.firefox;
|
||||
break;
|
||||
case 'cr':
|
||||
browserType = playwright.chromium;
|
||||
break;
|
||||
case 'wk':
|
||||
browserType = playwright.webkit;
|
||||
break;
|
||||
case 'ff':
|
||||
browserType = playwright.firefox;
|
||||
break;
|
||||
}
|
||||
if (browserType) return browserType;
|
||||
_utilsBundle.program.help();
|
||||
}
|
||||
function validateOptions(options) {
|
||||
if (options.device && !(options.device in playwright.devices)) {
|
||||
const lines = [`Device descriptor not found: '${options.device}', available devices are:`];
|
||||
for (const name in playwright.devices) lines.push(` "${name}"`);
|
||||
throw new Error(lines.join('\n'));
|
||||
}
|
||||
if (options.colorScheme && !['light', 'dark'].includes(options.colorScheme)) throw new Error('Invalid color scheme, should be one of "light", "dark"');
|
||||
}
|
||||
function logErrorAndExit(e) {
|
||||
if (process.env.PWDEBUGIMPL) console.error(e);else console.error(e.name + ': ' + e.message);
|
||||
(0, _utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
function codegenId() {
|
||||
return process.env.PW_LANG_NAME || 'playwright-test';
|
||||
}
|
||||
function commandWithOpenOptions(command, description, options) {
|
||||
let result = _utilsBundle.program.command(command).description(description);
|
||||
for (const option of options) result = result.option(option[0], ...option.slice(1));
|
||||
return result.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium').option('--block-service-workers', 'block service workers').option('--channel <channel>', 'Chromium distribution channel, "chrome", "chrome-beta", "msedge-dev", etc').option('--color-scheme <scheme>', 'emulate preferred color scheme, "light" or "dark"').option('--device <deviceName>', 'emulate device, for example "iPhone 11"').option('--geolocation <coordinates>', 'specify geolocation coordinates, for example "37.819722,-122.478611"').option('--ignore-https-errors', 'ignore https errors').option('--load-storage <filename>', 'load context storage state from the file, previously saved with --save-storage').option('--lang <language>', 'specify language / locale, for example "en-GB"').option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"').option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"').option('--save-har <filename>', 'save HAR file with all network activity at the end').option('--save-har-glob <glob pattern>', 'filter entries in the HAR by matching url against this glob pattern').option('--save-storage <filename>', 'save context storage state at the end, for later use with --load-storage').option('--timezone <time zone>', 'time zone to emulate, for example "Europe/Rome"').option('--timeout <timeout>', 'timeout for Playwright actions in milliseconds, no timeout by default').option('--user-agent <ua string>', 'specify user agent string').option('--viewport-size <size>', 'specify browser viewport size in pixels, for example "1280, 720"');
|
||||
}
|
||||
function buildBasePlaywrightCLICommand(cliTargetLang) {
|
||||
switch (cliTargetLang) {
|
||||
case 'python':
|
||||
return `playwright`;
|
||||
case 'java':
|
||||
return `mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="...options.."`;
|
||||
case 'csharp':
|
||||
return `pwsh bin/Debug/netX/playwright.ps1`;
|
||||
default:
|
||||
{
|
||||
const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
|
||||
return `${packageManagerCommand} playwright`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
Object.defineProperty(exports, "program", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _program.program;
|
||||
}
|
||||
});
|
||||
var _utils = require("../utils");
|
||||
var _program = require("./program");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
|
||||
function printPlaywrightTestError(command) {
|
||||
const packages = [];
|
||||
for (const pkg of ['playwright', 'playwright-chromium', 'playwright-firefox', 'playwright-webkit']) {
|
||||
try {
|
||||
require.resolve(pkg);
|
||||
packages.push(pkg);
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!packages.length) packages.push('playwright');
|
||||
const packageManager = (0, _utils.getPackageManager)();
|
||||
if (packageManager === 'yarn') {
|
||||
console.error(`Please install @playwright/test package before running "yarn playwright ${command}"`);
|
||||
console.error(` yarn remove ${packages.join(' ')}`);
|
||||
console.error(' yarn add -D @playwright/test');
|
||||
} else if (packageManager === 'pnpm') {
|
||||
console.error(`Please install @playwright/test package before running "pnpm exec playwright ${command}"`);
|
||||
console.error(` pnpm remove ${packages.join(' ')}`);
|
||||
console.error(' pnpm add -D @playwright/test');
|
||||
} else {
|
||||
console.error(`Please install @playwright/test package before running "npx playwright ${command}"`);
|
||||
console.error(` npm uninstall ${packages.join(' ')}`);
|
||||
console.error(' npm install -D @playwright/test');
|
||||
}
|
||||
}
|
||||
const kExternalPlaywrightTestCommands = [['test', 'Run tests with Playwright Test.'], ['show-report', 'Show Playwright Test HTML report.'], ['merge-reports', 'Merge Playwright Test Blob reports']];
|
||||
function addExternalPlaywrightTestCommands() {
|
||||
for (const [command, description] of kExternalPlaywrightTestCommands) {
|
||||
const playwrightTest = _program.program.command(command).allowUnknownOption(true);
|
||||
playwrightTest.description(`${description} Available in @playwright/test package.`);
|
||||
playwrightTest.action(async () => {
|
||||
printPlaywrightTestError(command);
|
||||
(0, _utils.gracefullyProcessExitDoNotHang)(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!process.env.PW_LANG_NAME) addExternalPlaywrightTestCommands();
|
||||
@@ -1,50 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Accessibility = void 0;
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function axNodeFromProtocol(axNode) {
|
||||
const result = {
|
||||
...axNode,
|
||||
value: axNode.valueNumber !== undefined ? axNode.valueNumber : axNode.valueString,
|
||||
checked: axNode.checked === 'checked' ? true : axNode.checked === 'unchecked' ? false : axNode.checked,
|
||||
pressed: axNode.pressed === 'pressed' ? true : axNode.pressed === 'released' ? false : axNode.pressed,
|
||||
children: axNode.children ? axNode.children.map(axNodeFromProtocol) : undefined
|
||||
};
|
||||
delete result.valueNumber;
|
||||
delete result.valueString;
|
||||
return result;
|
||||
}
|
||||
class Accessibility {
|
||||
constructor(channel) {
|
||||
this._channel = void 0;
|
||||
this._channel = channel;
|
||||
}
|
||||
async snapshot(options = {}) {
|
||||
const root = options.root ? options.root._elementChannel : undefined;
|
||||
const result = await this._channel.accessibilitySnapshot({
|
||||
interestingOnly: options.interestingOnly,
|
||||
root
|
||||
});
|
||||
return result.rootAXNode ? axNodeFromProtocol(result.rootAXNode) : null;
|
||||
}
|
||||
}
|
||||
exports.Accessibility = Accessibility;
|
||||
@@ -1,473 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.AndroidWebView = exports.AndroidSocket = exports.AndroidInput = exports.AndroidDevice = exports.Android = void 0;
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _utils = require("../utils");
|
||||
var _events = require("./events");
|
||||
var _browserContext = require("./browserContext");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _timeoutSettings = require("../common/timeoutSettings");
|
||||
var _waiter = require("./waiter");
|
||||
var _events2 = require("events");
|
||||
var _connection = require("./connection");
|
||||
var _errors = require("./errors");
|
||||
var _timeoutRunner = require("../utils/timeoutRunner");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
class Android extends _channelOwner.ChannelOwner {
|
||||
static from(android) {
|
||||
return android._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._timeoutSettings = void 0;
|
||||
this._serverLauncher = void 0;
|
||||
this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
this._channel.setDefaultTimeoutNoReply({
|
||||
timeout
|
||||
});
|
||||
}
|
||||
async devices(options = {}) {
|
||||
const {
|
||||
devices
|
||||
} = await this._channel.devices(options);
|
||||
return devices.map(d => AndroidDevice.from(d));
|
||||
}
|
||||
async launchServer(options = {}) {
|
||||
if (!this._serverLauncher) throw new Error('Launching server is not supported');
|
||||
return await this._serverLauncher.launchServer(options);
|
||||
}
|
||||
async connect(wsEndpoint, options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const deadline = options.timeout ? (0, _utils.monotonicTime)() + options.timeout : 0;
|
||||
const headers = {
|
||||
'x-playwright-browser': 'android',
|
||||
...options.headers
|
||||
};
|
||||
const localUtils = this._connection.localUtils();
|
||||
const connectParams = {
|
||||
wsEndpoint,
|
||||
headers,
|
||||
slowMo: options.slowMo,
|
||||
timeout: options.timeout
|
||||
};
|
||||
const {
|
||||
pipe
|
||||
} = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new _connection.Connection(localUtils, this._instrumentation);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
let device;
|
||||
let closeError;
|
||||
const onPipeClosed = () => {
|
||||
var _device;
|
||||
(_device = device) === null || _device === void 0 || _device._didClose();
|
||||
connection.close(closeError);
|
||||
};
|
||||
pipe.on('closed', onPipeClosed);
|
||||
connection.onmessage = message => pipe.send({
|
||||
message
|
||||
}).catch(onPipeClosed);
|
||||
pipe.on('message', ({
|
||||
message
|
||||
}) => {
|
||||
try {
|
||||
connection.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
const result = await (0, _timeoutRunner.raceAgainstDeadline)(async () => {
|
||||
const playwright = await connection.initializePlaywright();
|
||||
if (!playwright._initializer.preConnectedAndroidDevice) {
|
||||
closePipe();
|
||||
throw new Error('Malformed endpoint. Did you use Android.launchServer method?');
|
||||
}
|
||||
device = AndroidDevice.from(playwright._initializer.preConnectedAndroidDevice);
|
||||
device._shouldCloseConnectionOnClose = true;
|
||||
device.on(_events.Events.AndroidDevice.Close, closePipe);
|
||||
return device;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
closePipe();
|
||||
throw new Error(`Timeout ${options.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.Android = Android;
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class AndroidDevice extends _channelOwner.ChannelOwner {
|
||||
static from(androidDevice) {
|
||||
return androidDevice._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._timeoutSettings = void 0;
|
||||
this._webViews = new Map();
|
||||
this._shouldCloseConnectionOnClose = false;
|
||||
this.input = void 0;
|
||||
this.input = new AndroidInput(this);
|
||||
this._timeoutSettings = new _timeoutSettings.TimeoutSettings(parent._timeoutSettings);
|
||||
this._channel.on('webViewAdded', ({
|
||||
webView
|
||||
}) => this._onWebViewAdded(webView));
|
||||
this._channel.on('webViewRemoved', ({
|
||||
socketName
|
||||
}) => this._onWebViewRemoved(socketName));
|
||||
this._channel.on('close', () => this._didClose());
|
||||
}
|
||||
_onWebViewAdded(webView) {
|
||||
const view = new AndroidWebView(this, webView);
|
||||
this._webViews.set(webView.socketName, view);
|
||||
this.emit(_events.Events.AndroidDevice.WebView, view);
|
||||
}
|
||||
_onWebViewRemoved(socketName) {
|
||||
const view = this._webViews.get(socketName);
|
||||
this._webViews.delete(socketName);
|
||||
if (view) view.emit(_events.Events.AndroidWebView.Close);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
this._channel.setDefaultTimeoutNoReply({
|
||||
timeout
|
||||
});
|
||||
}
|
||||
serial() {
|
||||
return this._initializer.serial;
|
||||
}
|
||||
model() {
|
||||
return this._initializer.model;
|
||||
}
|
||||
webViews() {
|
||||
return [...this._webViews.values()];
|
||||
}
|
||||
async webView(selector, options) {
|
||||
const predicate = v => {
|
||||
if (selector.pkg) return v.pkg() === selector.pkg;
|
||||
if (selector.socketName) return v._socketName() === selector.socketName;
|
||||
return false;
|
||||
};
|
||||
const webView = [...this._webViews.values()].find(predicate);
|
||||
if (webView) return webView;
|
||||
return await this.waitForEvent('webview', {
|
||||
...options,
|
||||
predicate
|
||||
});
|
||||
}
|
||||
async wait(selector, options) {
|
||||
await this._channel.wait({
|
||||
selector: toSelectorChannel(selector),
|
||||
...options
|
||||
});
|
||||
}
|
||||
async fill(selector, text, options) {
|
||||
await this._channel.fill({
|
||||
selector: toSelectorChannel(selector),
|
||||
text,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async press(selector, key, options) {
|
||||
await this.tap(selector, options);
|
||||
await this.input.press(key);
|
||||
}
|
||||
async tap(selector, options) {
|
||||
await this._channel.tap({
|
||||
selector: toSelectorChannel(selector),
|
||||
...options
|
||||
});
|
||||
}
|
||||
async drag(selector, dest, options) {
|
||||
await this._channel.drag({
|
||||
selector: toSelectorChannel(selector),
|
||||
dest,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async fling(selector, direction, options) {
|
||||
await this._channel.fling({
|
||||
selector: toSelectorChannel(selector),
|
||||
direction,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async longTap(selector, options) {
|
||||
await this._channel.longTap({
|
||||
selector: toSelectorChannel(selector),
|
||||
...options
|
||||
});
|
||||
}
|
||||
async pinchClose(selector, percent, options) {
|
||||
await this._channel.pinchClose({
|
||||
selector: toSelectorChannel(selector),
|
||||
percent,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async pinchOpen(selector, percent, options) {
|
||||
await this._channel.pinchOpen({
|
||||
selector: toSelectorChannel(selector),
|
||||
percent,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async scroll(selector, direction, percent, options) {
|
||||
await this._channel.scroll({
|
||||
selector: toSelectorChannel(selector),
|
||||
direction,
|
||||
percent,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async swipe(selector, direction, percent, options) {
|
||||
await this._channel.swipe({
|
||||
selector: toSelectorChannel(selector),
|
||||
direction,
|
||||
percent,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async info(selector) {
|
||||
return (await this._channel.info({
|
||||
selector: toSelectorChannel(selector)
|
||||
})).info;
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
const {
|
||||
binary
|
||||
} = await this._channel.screenshot();
|
||||
if (options.path) await _fs.default.promises.writeFile(options.path, binary);
|
||||
return binary;
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async close() {
|
||||
try {
|
||||
if (this._shouldCloseConnectionOnClose) this._connection.close();else await this._channel.close();
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e)) return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
_didClose() {
|
||||
this.emit(_events.Events.AndroidDevice.Close, this);
|
||||
}
|
||||
async shell(command) {
|
||||
const {
|
||||
result
|
||||
} = await this._channel.shell({
|
||||
command
|
||||
});
|
||||
return result;
|
||||
}
|
||||
async open(command) {
|
||||
return AndroidSocket.from((await this._channel.open({
|
||||
command
|
||||
})).socket);
|
||||
}
|
||||
async installApk(file, options) {
|
||||
await this._channel.installApk({
|
||||
file: await loadFile(file),
|
||||
args: options && options.args
|
||||
});
|
||||
}
|
||||
async push(file, path, options) {
|
||||
await this._channel.push({
|
||||
file: await loadFile(file),
|
||||
path,
|
||||
mode: options ? options.mode : undefined
|
||||
});
|
||||
}
|
||||
async launchBrowser(options = {}) {
|
||||
const contextOptions = await (0, _browserContext.prepareBrowserContextParams)(options);
|
||||
const result = await this._channel.launchBrowser(contextOptions);
|
||||
const context = _browserContext.BrowserContext.from(result.context);
|
||||
context._setOptions(contextOptions, {});
|
||||
return context;
|
||||
}
|
||||
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = _waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== _events.Events.AndroidDevice.Close) waiter.rejectOnEvent(this, _events.Events.AndroidDevice.Close, () => new _errors.TargetClosedError());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.AndroidDevice = AndroidDevice;
|
||||
class AndroidSocket extends _channelOwner.ChannelOwner {
|
||||
static from(androidDevice) {
|
||||
return androidDevice._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._channel.on('data', ({
|
||||
data
|
||||
}) => this.emit(_events.Events.AndroidSocket.Data, data));
|
||||
this._channel.on('close', () => this.emit(_events.Events.AndroidSocket.Close));
|
||||
}
|
||||
async write(data) {
|
||||
await this._channel.write({
|
||||
data
|
||||
});
|
||||
}
|
||||
async close() {
|
||||
await this._channel.close();
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
exports.AndroidSocket = AndroidSocket;
|
||||
async function loadFile(file) {
|
||||
if ((0, _utils.isString)(file)) return await _fs.default.promises.readFile(file);
|
||||
return file;
|
||||
}
|
||||
class AndroidInput {
|
||||
constructor(device) {
|
||||
this._device = void 0;
|
||||
this._device = device;
|
||||
}
|
||||
async type(text) {
|
||||
await this._device._channel.inputType({
|
||||
text
|
||||
});
|
||||
}
|
||||
async press(key) {
|
||||
await this._device._channel.inputPress({
|
||||
key
|
||||
});
|
||||
}
|
||||
async tap(point) {
|
||||
await this._device._channel.inputTap({
|
||||
point
|
||||
});
|
||||
}
|
||||
async swipe(from, segments, steps) {
|
||||
await this._device._channel.inputSwipe({
|
||||
segments,
|
||||
steps
|
||||
});
|
||||
}
|
||||
async drag(from, to, steps) {
|
||||
await this._device._channel.inputDrag({
|
||||
from,
|
||||
to,
|
||||
steps
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.AndroidInput = AndroidInput;
|
||||
function toSelectorChannel(selector) {
|
||||
const {
|
||||
checkable,
|
||||
checked,
|
||||
clazz,
|
||||
clickable,
|
||||
depth,
|
||||
desc,
|
||||
enabled,
|
||||
focusable,
|
||||
focused,
|
||||
hasChild,
|
||||
hasDescendant,
|
||||
longClickable,
|
||||
pkg,
|
||||
res,
|
||||
scrollable,
|
||||
selected,
|
||||
text
|
||||
} = selector;
|
||||
const toRegex = value => {
|
||||
if (value === undefined) return undefined;
|
||||
if ((0, _utils.isRegExp)(value)) return value.source;
|
||||
return '^' + value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') + '$';
|
||||
};
|
||||
return {
|
||||
checkable,
|
||||
checked,
|
||||
clazz: toRegex(clazz),
|
||||
pkg: toRegex(pkg),
|
||||
desc: toRegex(desc),
|
||||
res: toRegex(res),
|
||||
text: toRegex(text),
|
||||
clickable,
|
||||
depth,
|
||||
enabled,
|
||||
focusable,
|
||||
focused,
|
||||
hasChild: hasChild ? {
|
||||
selector: toSelectorChannel(hasChild.selector)
|
||||
} : undefined,
|
||||
hasDescendant: hasDescendant ? {
|
||||
selector: toSelectorChannel(hasDescendant.selector),
|
||||
maxDepth: hasDescendant.maxDepth
|
||||
} : undefined,
|
||||
longClickable,
|
||||
scrollable,
|
||||
selected
|
||||
};
|
||||
}
|
||||
class AndroidWebView extends _events2.EventEmitter {
|
||||
constructor(device, data) {
|
||||
super();
|
||||
this._device = void 0;
|
||||
this._data = void 0;
|
||||
this._pagePromise = void 0;
|
||||
this._device = device;
|
||||
this._data = data;
|
||||
}
|
||||
pid() {
|
||||
return this._data.pid;
|
||||
}
|
||||
pkg() {
|
||||
return this._data.pkg;
|
||||
}
|
||||
_socketName() {
|
||||
return this._data.socketName;
|
||||
}
|
||||
async page() {
|
||||
if (!this._pagePromise) this._pagePromise = this._fetchPage();
|
||||
return await this._pagePromise;
|
||||
}
|
||||
async _fetchPage() {
|
||||
const {
|
||||
context
|
||||
} = await this._device._channel.connectToWebView({
|
||||
socketName: this._data.socketName
|
||||
});
|
||||
return _browserContext.BrowserContext.from(context).pages()[0];
|
||||
}
|
||||
}
|
||||
exports.AndroidWebView = AndroidWebView;
|
||||
@@ -1,285 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
Object.defineProperty(exports, "APIRequest", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _fetch.APIRequest;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "APIRequestContext", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _fetch.APIRequestContext;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "APIResponse", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _fetch.APIResponse;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Accessibility", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _accessibility.Accessibility;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Android", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _android.Android;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "AndroidDevice", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _android.AndroidDevice;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "AndroidInput", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _android.AndroidInput;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "AndroidSocket", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _android.AndroidSocket;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "AndroidWebView", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _android.AndroidWebView;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Browser", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _browser.Browser;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "BrowserContext", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _browserContext.BrowserContext;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "BrowserType", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _browserType.BrowserType;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "CDPSession", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _cdpSession.CDPSession;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Clock", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _clock.Clock;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "ConsoleMessage", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _consoleMessage.ConsoleMessage;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Coverage", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _coverage.Coverage;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Dialog", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _dialog.Dialog;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Download", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _download.Download;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Electron", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _electron.Electron;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "ElectronApplication", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _electron.ElectronApplication;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "ElementHandle", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _elementHandle.ElementHandle;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "FileChooser", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _fileChooser.FileChooser;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Frame", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _frame.Frame;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "FrameLocator", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _locator.FrameLocator;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "JSHandle", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _jsHandle.JSHandle;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Keyboard", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _input.Keyboard;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Locator", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _locator.Locator;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Mouse", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _input.Mouse;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Page", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _page.Page;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Playwright", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _playwright.Playwright;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Request", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _network.Request;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Response", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _network.Response;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Route", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _network.Route;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Selectors", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _selectors.Selectors;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "TimeoutError", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _errors.TimeoutError;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Touchscreen", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _input.Touchscreen;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Tracing", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _tracing.Tracing;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Video", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _video.Video;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "WebError", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _webError.WebError;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "WebSocket", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _network.WebSocket;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "WebSocketRoute", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _network.WebSocketRoute;
|
||||
}
|
||||
});
|
||||
Object.defineProperty(exports, "Worker", {
|
||||
enumerable: true,
|
||||
get: function () {
|
||||
return _worker.Worker;
|
||||
}
|
||||
});
|
||||
var _accessibility = require("./accessibility");
|
||||
var _android = require("./android");
|
||||
var _browser = require("./browser");
|
||||
var _browserContext = require("./browserContext");
|
||||
var _browserType = require("./browserType");
|
||||
var _clock = require("./clock");
|
||||
var _consoleMessage = require("./consoleMessage");
|
||||
var _coverage = require("./coverage");
|
||||
var _dialog = require("./dialog");
|
||||
var _download = require("./download");
|
||||
var _electron = require("./electron");
|
||||
var _locator = require("./locator");
|
||||
var _elementHandle = require("./elementHandle");
|
||||
var _fileChooser = require("./fileChooser");
|
||||
var _errors = require("./errors");
|
||||
var _frame = require("./frame");
|
||||
var _input = require("./input");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _network = require("./network");
|
||||
var _fetch = require("./fetch");
|
||||
var _page = require("./page");
|
||||
var _selectors = require("./selectors");
|
||||
var _tracing = require("./tracing");
|
||||
var _video = require("./video");
|
||||
var _worker = require("./worker");
|
||||
var _cdpSession = require("./cdpSession");
|
||||
var _playwright = require("./playwright");
|
||||
var _webError = require("./webError");
|
||||
@@ -1,79 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Artifact = void 0;
|
||||
var fs = _interopRequireWildcard(require("fs"));
|
||||
var _stream = require("./stream");
|
||||
var _fileUtils = require("../utils/fileUtils");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Artifact extends _channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
async pathAfterFinished() {
|
||||
if (this._connection.isRemote()) throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
|
||||
return (await this._channel.pathAfterFinished()).value;
|
||||
}
|
||||
async saveAs(path) {
|
||||
if (!this._connection.isRemote()) {
|
||||
await this._channel.saveAs({
|
||||
path
|
||||
});
|
||||
return;
|
||||
}
|
||||
const result = await this._channel.saveAsStream();
|
||||
const stream = _stream.Stream.from(result.stream);
|
||||
await (0, _fileUtils.mkdirIfNeeded)(path);
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.stream().pipe(fs.createWriteStream(path)).on('finish', resolve).on('error', reject);
|
||||
});
|
||||
}
|
||||
async failure() {
|
||||
return (await this._channel.failure()).error || null;
|
||||
}
|
||||
async createReadStream() {
|
||||
const result = await this._channel.stream();
|
||||
const stream = _stream.Stream.from(result.stream);
|
||||
return stream.stream();
|
||||
}
|
||||
async readIntoBuffer() {
|
||||
const stream = await this.createReadStream();
|
||||
return await new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream.on('data', chunk => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
stream.on('error', reject);
|
||||
});
|
||||
}
|
||||
async cancel() {
|
||||
return await this._channel.cancel();
|
||||
}
|
||||
async delete() {
|
||||
return await this._channel.delete();
|
||||
}
|
||||
}
|
||||
exports.Artifact = Artifact;
|
||||
@@ -1,145 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Browser = void 0;
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _browserContext = require("./browserContext");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _events = require("./events");
|
||||
var _errors = require("./errors");
|
||||
var _cdpSession = require("./cdpSession");
|
||||
var _artifact = require("./artifact");
|
||||
var _utils = require("../utils");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class Browser extends _channelOwner.ChannelOwner {
|
||||
static from(browser) {
|
||||
return browser._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._contexts = new Set();
|
||||
this._isConnected = true;
|
||||
this._closedPromise = void 0;
|
||||
this._shouldCloseConnectionOnClose = false;
|
||||
this._browserType = void 0;
|
||||
this._options = {};
|
||||
this._name = void 0;
|
||||
this._path = void 0;
|
||||
// Used from @playwright/test fixtures.
|
||||
this._connectHeaders = void 0;
|
||||
this._closeReason = void 0;
|
||||
this._name = initializer.name;
|
||||
this._channel.on('close', () => this._didClose());
|
||||
this._closedPromise = new Promise(f => this.once(_events.Events.Browser.Disconnected, f));
|
||||
}
|
||||
browserType() {
|
||||
return this._browserType;
|
||||
}
|
||||
async newContext(options = {}) {
|
||||
return await this._innerNewContext(options, false);
|
||||
}
|
||||
async _newContextForReuse(options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
for (const context of this._contexts) {
|
||||
await this._browserType._willCloseContext(context);
|
||||
for (const page of context.pages()) page._onClose();
|
||||
context._onClose();
|
||||
}
|
||||
return await this._innerNewContext(options, true);
|
||||
}, true);
|
||||
}
|
||||
async _stopPendingOperations(reason) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
await this._channel.stopPendingOperations({
|
||||
reason
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
async _innerNewContext(options = {}, forReuse) {
|
||||
options = {
|
||||
...this._browserType._defaultContextOptions,
|
||||
...options
|
||||
};
|
||||
const contextOptions = await (0, _browserContext.prepareBrowserContextParams)(options);
|
||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||
const context = _browserContext.BrowserContext.from(response.context);
|
||||
await this._browserType._didCreateContext(context, contextOptions, this._options, options.logger || this._logger);
|
||||
return context;
|
||||
}
|
||||
contexts() {
|
||||
return [...this._contexts];
|
||||
}
|
||||
version() {
|
||||
return this._initializer.version;
|
||||
}
|
||||
async newPage(options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const context = await this.newContext(options);
|
||||
const page = await context.newPage();
|
||||
page._ownedContext = context;
|
||||
context._ownerPage = page;
|
||||
return page;
|
||||
});
|
||||
}
|
||||
isConnected() {
|
||||
return this._isConnected;
|
||||
}
|
||||
async newBrowserCDPSession() {
|
||||
return _cdpSession.CDPSession.from((await this._channel.newBrowserCDPSession()).session);
|
||||
}
|
||||
async startTracing(page, options = {}) {
|
||||
this._path = options.path;
|
||||
await this._channel.startTracing({
|
||||
...options,
|
||||
page: page ? page._channel : undefined
|
||||
});
|
||||
}
|
||||
async stopTracing() {
|
||||
const artifact = _artifact.Artifact.from((await this._channel.stopTracing()).artifact);
|
||||
const buffer = await artifact.readIntoBuffer();
|
||||
await artifact.delete();
|
||||
if (this._path) {
|
||||
await (0, _utils.mkdirIfNeeded)(this._path);
|
||||
await _fs.default.promises.writeFile(this._path, buffer);
|
||||
this._path = undefined;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async close(options = {}) {
|
||||
this._closeReason = options.reason;
|
||||
try {
|
||||
if (this._shouldCloseConnectionOnClose) this._connection.close();else await this._channel.close(options);
|
||||
await this._closedPromise;
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e)) return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
_didClose() {
|
||||
this._isConnected = false;
|
||||
this.emit(_events.Events.Browser.Disconnected, this);
|
||||
}
|
||||
}
|
||||
exports.Browser = Browser;
|
||||
@@ -1,559 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.BrowserContext = void 0;
|
||||
exports.prepareBrowserContextParams = prepareBrowserContextParams;
|
||||
exports.toClientCertificatesProtocol = toClientCertificatesProtocol;
|
||||
var _page = require("./page");
|
||||
var _frame = require("./frame");
|
||||
var network = _interopRequireWildcard(require("./network"));
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _path = _interopRequireDefault(require("path"));
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _clientHelper = require("./clientHelper");
|
||||
var _browser = require("./browser");
|
||||
var _worker = require("./worker");
|
||||
var _events = require("./events");
|
||||
var _timeoutSettings = require("../common/timeoutSettings");
|
||||
var _waiter = require("./waiter");
|
||||
var _utils = require("../utils");
|
||||
var _cdpSession = require("./cdpSession");
|
||||
var _tracing = require("./tracing");
|
||||
var _artifact = require("./artifact");
|
||||
var _fetch = require("./fetch");
|
||||
var _stackTrace = require("../utils/stackTrace");
|
||||
var _harRouter = require("./harRouter");
|
||||
var _consoleMessage = require("./consoleMessage");
|
||||
var _dialog = require("./dialog");
|
||||
var _webError = require("./webError");
|
||||
var _errors = require("./errors");
|
||||
var _clock = require("./clock");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class BrowserContext extends _channelOwner.ChannelOwner {
|
||||
static from(context) {
|
||||
return context._object;
|
||||
}
|
||||
static fromNullable(context) {
|
||||
return context ? BrowserContext.from(context) : null;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
var _this$_browser, _this$_browser2;
|
||||
super(parent, type, guid, initializer);
|
||||
this._pages = new Set();
|
||||
this._routes = [];
|
||||
this._webSocketRoutes = [];
|
||||
this._browser = null;
|
||||
this._browserType = void 0;
|
||||
this._bindings = new Map();
|
||||
this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
|
||||
this._ownerPage = void 0;
|
||||
this._closedPromise = void 0;
|
||||
this._options = {};
|
||||
this.request = void 0;
|
||||
this.tracing = void 0;
|
||||
this.clock = void 0;
|
||||
this._backgroundPages = new Set();
|
||||
this._serviceWorkers = new Set();
|
||||
this._isChromium = void 0;
|
||||
this._harRecorders = new Map();
|
||||
this._closeWasCalled = false;
|
||||
this._closeReason = void 0;
|
||||
this._harRouters = [];
|
||||
if (parent instanceof _browser.Browser) this._browser = parent;
|
||||
(_this$_browser = this._browser) === null || _this$_browser === void 0 || _this$_browser._contexts.add(this);
|
||||
this._isChromium = ((_this$_browser2 = this._browser) === null || _this$_browser2 === void 0 ? void 0 : _this$_browser2._name) === 'chromium';
|
||||
this.tracing = _tracing.Tracing.from(initializer.tracing);
|
||||
this.request = _fetch.APIRequestContext.from(initializer.requestContext);
|
||||
this.clock = new _clock.Clock(this);
|
||||
this._channel.on('bindingCall', ({
|
||||
binding
|
||||
}) => this._onBinding(_page.BindingCall.from(binding)));
|
||||
this._channel.on('close', () => this._onClose());
|
||||
this._channel.on('page', ({
|
||||
page
|
||||
}) => this._onPage(_page.Page.from(page)));
|
||||
this._channel.on('route', ({
|
||||
route
|
||||
}) => this._onRoute(network.Route.from(route)));
|
||||
this._channel.on('webSocketRoute', ({
|
||||
webSocketRoute
|
||||
}) => this._onWebSocketRoute(network.WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on('backgroundPage', ({
|
||||
page
|
||||
}) => {
|
||||
const backgroundPage = _page.Page.from(page);
|
||||
this._backgroundPages.add(backgroundPage);
|
||||
this.emit(_events.Events.BrowserContext.BackgroundPage, backgroundPage);
|
||||
});
|
||||
this._channel.on('serviceWorker', ({
|
||||
worker
|
||||
}) => {
|
||||
const serviceWorker = _worker.Worker.from(worker);
|
||||
serviceWorker._context = this;
|
||||
this._serviceWorkers.add(serviceWorker);
|
||||
this.emit(_events.Events.BrowserContext.ServiceWorker, serviceWorker);
|
||||
});
|
||||
this._channel.on('console', event => {
|
||||
const consoleMessage = new _consoleMessage.ConsoleMessage(event);
|
||||
this.emit(_events.Events.BrowserContext.Console, consoleMessage);
|
||||
const page = consoleMessage.page();
|
||||
if (page) page.emit(_events.Events.Page.Console, consoleMessage);
|
||||
});
|
||||
this._channel.on('pageError', ({
|
||||
error,
|
||||
page
|
||||
}) => {
|
||||
const pageObject = _page.Page.from(page);
|
||||
const parsedError = (0, _errors.parseError)(error);
|
||||
this.emit(_events.Events.BrowserContext.WebError, new _webError.WebError(pageObject, parsedError));
|
||||
if (pageObject) pageObject.emit(_events.Events.Page.PageError, parsedError);
|
||||
});
|
||||
this._channel.on('dialog', ({
|
||||
dialog
|
||||
}) => {
|
||||
const dialogObject = _dialog.Dialog.from(dialog);
|
||||
let hasListeners = this.emit(_events.Events.BrowserContext.Dialog, dialogObject);
|
||||
const page = dialogObject.page();
|
||||
if (page) hasListeners = page.emit(_events.Events.Page.Dialog, dialogObject) || hasListeners;
|
||||
if (!hasListeners) {
|
||||
// Although we do similar handling on the server side, we still need this logic
|
||||
// on the client side due to a possible race condition between two async calls:
|
||||
// a) removing "dialog" listener subscription (client->server)
|
||||
// b) actual "dialog" event (server->client)
|
||||
if (dialogObject.type() === 'beforeunload') dialog.accept({}).catch(() => {});else dialog.dismiss().catch(() => {});
|
||||
}
|
||||
});
|
||||
this._channel.on('request', ({
|
||||
request,
|
||||
page
|
||||
}) => this._onRequest(network.Request.from(request), _page.Page.fromNullable(page)));
|
||||
this._channel.on('requestFailed', ({
|
||||
request,
|
||||
failureText,
|
||||
responseEndTiming,
|
||||
page
|
||||
}) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, _page.Page.fromNullable(page)));
|
||||
this._channel.on('requestFinished', params => this._onRequestFinished(params));
|
||||
this._channel.on('response', ({
|
||||
response,
|
||||
page
|
||||
}) => this._onResponse(network.Response.from(response), _page.Page.fromNullable(page)));
|
||||
this._closedPromise = new Promise(f => this.once(_events.Events.BrowserContext.Close, f));
|
||||
this._setEventToSubscriptionMapping(new Map([[_events.Events.BrowserContext.Console, 'console'], [_events.Events.BrowserContext.Dialog, 'dialog'], [_events.Events.BrowserContext.Request, 'request'], [_events.Events.BrowserContext.Response, 'response'], [_events.Events.BrowserContext.RequestFinished, 'requestFinished'], [_events.Events.BrowserContext.RequestFailed, 'requestFailed']]));
|
||||
}
|
||||
_setOptions(contextOptions, browserOptions) {
|
||||
this._options = contextOptions;
|
||||
if (this._options.recordHar) this._harRecorders.set('', {
|
||||
path: this._options.recordHar.path,
|
||||
content: this._options.recordHar.content
|
||||
});
|
||||
this.tracing._tracesDir = browserOptions.tracesDir;
|
||||
}
|
||||
_onPage(page) {
|
||||
this._pages.add(page);
|
||||
this.emit(_events.Events.BrowserContext.Page, page);
|
||||
if (page._opener && !page._opener.isClosed()) page._opener.emit(_events.Events.Page.Popup, page);
|
||||
}
|
||||
_onRequest(request, page) {
|
||||
this.emit(_events.Events.BrowserContext.Request, request);
|
||||
if (page) page.emit(_events.Events.Page.Request, request);
|
||||
}
|
||||
_onResponse(response, page) {
|
||||
this.emit(_events.Events.BrowserContext.Response, response);
|
||||
if (page) page.emit(_events.Events.Page.Response, response);
|
||||
}
|
||||
_onRequestFailed(request, responseEndTiming, failureText, page) {
|
||||
request._failureText = failureText || null;
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(_events.Events.BrowserContext.RequestFailed, request);
|
||||
if (page) page.emit(_events.Events.Page.RequestFailed, request);
|
||||
}
|
||||
_onRequestFinished(params) {
|
||||
const {
|
||||
responseEndTiming
|
||||
} = params;
|
||||
const request = network.Request.from(params.request);
|
||||
const response = network.Response.fromNullable(params.response);
|
||||
const page = _page.Page.fromNullable(params.page);
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(_events.Events.BrowserContext.RequestFinished, request);
|
||||
if (page) page.emit(_events.Events.Page.RequestFinished, request);
|
||||
if (response) response._finishedPromise.resolve(null);
|
||||
}
|
||||
async _onRoute(route) {
|
||||
route._context = this;
|
||||
const page = route.request()._safePage();
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
// If the page or the context was closed we stall all requests right away.
|
||||
if (page !== null && page !== void 0 && page._closeWasCalled || this._closeWasCalled) return;
|
||||
if (!routeHandler.matches(route.request().url())) continue;
|
||||
const index = this._routes.indexOf(routeHandler);
|
||||
if (index === -1) continue;
|
||||
if (routeHandler.willExpire()) this._routes.splice(index, 1);
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length) this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
|
||||
if (handled) return;
|
||||
}
|
||||
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
|
||||
// the method will throw an error - silence it.
|
||||
await route._innerContinue(true /* isFallback */).catch(() => {});
|
||||
}
|
||||
async _onWebSocketRoute(webSocketRoute) {
|
||||
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
|
||||
if (routeHandler) await routeHandler.handle(webSocketRoute);else webSocketRoute.connectToServer();
|
||||
}
|
||||
async _onBinding(bindingCall) {
|
||||
const func = this._bindings.get(bindingCall._initializer.name);
|
||||
if (!func) return;
|
||||
await bindingCall.call(func);
|
||||
}
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
this._wrapApiCall(async () => {
|
||||
this._channel.setDefaultNavigationTimeoutNoReply({
|
||||
timeout
|
||||
}).catch(() => {});
|
||||
}, true);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
this._wrapApiCall(async () => {
|
||||
this._channel.setDefaultTimeoutNoReply({
|
||||
timeout
|
||||
}).catch(() => {});
|
||||
}, true);
|
||||
}
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
pages() {
|
||||
return [...this._pages];
|
||||
}
|
||||
async newPage() {
|
||||
if (this._ownerPage) throw new Error('Please use browser.newContext()');
|
||||
return _page.Page.from((await this._channel.newPage()).page);
|
||||
}
|
||||
async cookies(urls) {
|
||||
if (!urls) urls = [];
|
||||
if (urls && typeof urls === 'string') urls = [urls];
|
||||
return (await this._channel.cookies({
|
||||
urls: urls
|
||||
})).cookies;
|
||||
}
|
||||
async addCookies(cookies) {
|
||||
await this._channel.addCookies({
|
||||
cookies
|
||||
});
|
||||
}
|
||||
async clearCookies(options = {}) {
|
||||
await this._channel.clearCookies({
|
||||
name: (0, _utils.isString)(options.name) ? options.name : undefined,
|
||||
nameRegexSource: (0, _utils.isRegExp)(options.name) ? options.name.source : undefined,
|
||||
nameRegexFlags: (0, _utils.isRegExp)(options.name) ? options.name.flags : undefined,
|
||||
domain: (0, _utils.isString)(options.domain) ? options.domain : undefined,
|
||||
domainRegexSource: (0, _utils.isRegExp)(options.domain) ? options.domain.source : undefined,
|
||||
domainRegexFlags: (0, _utils.isRegExp)(options.domain) ? options.domain.flags : undefined,
|
||||
path: (0, _utils.isString)(options.path) ? options.path : undefined,
|
||||
pathRegexSource: (0, _utils.isRegExp)(options.path) ? options.path.source : undefined,
|
||||
pathRegexFlags: (0, _utils.isRegExp)(options.path) ? options.path.flags : undefined
|
||||
});
|
||||
}
|
||||
async grantPermissions(permissions, options) {
|
||||
await this._channel.grantPermissions({
|
||||
permissions,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async clearPermissions() {
|
||||
await this._channel.clearPermissions();
|
||||
}
|
||||
async setGeolocation(geolocation) {
|
||||
await this._channel.setGeolocation({
|
||||
geolocation: geolocation || undefined
|
||||
});
|
||||
}
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
network.validateHeaders(headers);
|
||||
await this._channel.setExtraHTTPHeaders({
|
||||
headers: (0, _utils.headersObjectToArray)(headers)
|
||||
});
|
||||
}
|
||||
async setOffline(offline) {
|
||||
await this._channel.setOffline({
|
||||
offline
|
||||
});
|
||||
}
|
||||
async setHTTPCredentials(httpCredentials) {
|
||||
await this._channel.setHTTPCredentials({
|
||||
httpCredentials: httpCredentials || undefined
|
||||
});
|
||||
}
|
||||
async addInitScript(script, arg) {
|
||||
const source = await (0, _clientHelper.evaluationScript)(script, arg);
|
||||
await this._channel.addInitScript({
|
||||
source
|
||||
});
|
||||
}
|
||||
async exposeBinding(name, callback, options = {}) {
|
||||
await this._channel.exposeBinding({
|
||||
name,
|
||||
needsHandle: options.handle
|
||||
});
|
||||
this._bindings.set(name, callback);
|
||||
}
|
||||
async exposeFunction(name, callback) {
|
||||
await this._channel.exposeBinding({
|
||||
name
|
||||
});
|
||||
const binding = (source, ...args) => callback(...args);
|
||||
this._bindings.set(name, binding);
|
||||
}
|
||||
async route(url, handler, options = {}) {
|
||||
this._routes.unshift(new network.RouteHandler(this._options.baseURL, url, handler, options.times));
|
||||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
async routeWebSocket(url, handler) {
|
||||
this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler));
|
||||
await this._updateWebSocketInterceptionPatterns();
|
||||
}
|
||||
async _recordIntoHAR(har, page, options = {}) {
|
||||
var _options$updateConten, _options$updateMode, _options$updateConten2;
|
||||
const {
|
||||
harId
|
||||
} = await this._channel.harStart({
|
||||
page: page === null || page === void 0 ? void 0 : page._channel,
|
||||
options: prepareRecordHarOptions({
|
||||
path: har,
|
||||
content: (_options$updateConten = options.updateContent) !== null && _options$updateConten !== void 0 ? _options$updateConten : 'attach',
|
||||
mode: (_options$updateMode = options.updateMode) !== null && _options$updateMode !== void 0 ? _options$updateMode : 'minimal',
|
||||
urlFilter: options.url
|
||||
})
|
||||
});
|
||||
this._harRecorders.set(harId, {
|
||||
path: har,
|
||||
content: (_options$updateConten2 = options.updateContent) !== null && _options$updateConten2 !== void 0 ? _options$updateConten2 : 'attach'
|
||||
});
|
||||
}
|
||||
async routeFromHAR(har, options = {}) {
|
||||
if (options.update) {
|
||||
await this._recordIntoHAR(har, null, options);
|
||||
return;
|
||||
}
|
||||
const harRouter = await _harRouter.HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', {
|
||||
urlMatch: options.url
|
||||
});
|
||||
this._harRouters.push(harRouter);
|
||||
await harRouter.addContextRoute(this);
|
||||
}
|
||||
_disposeHarRouters() {
|
||||
this._harRouters.forEach(router => router.dispose());
|
||||
this._harRouters = [];
|
||||
}
|
||||
async unrouteAll(options) {
|
||||
await this._unrouteInternal(this._routes, [], options === null || options === void 0 ? void 0 : options.behavior);
|
||||
this._disposeHarRouters();
|
||||
}
|
||||
async unroute(url, handler) {
|
||||
const removed = [];
|
||||
const remaining = [];
|
||||
for (const route of this._routes) {
|
||||
if ((0, _utils.urlMatchesEqual)(route.url, url) && (!handler || route.handler === handler)) removed.push(route);else remaining.push(route);
|
||||
}
|
||||
await this._unrouteInternal(removed, remaining, 'default');
|
||||
}
|
||||
async _unrouteInternal(removed, remaining, behavior) {
|
||||
this._routes = remaining;
|
||||
await this._updateInterceptionPatterns();
|
||||
if (!behavior || behavior === 'default') return;
|
||||
const promises = removed.map(routeHandler => routeHandler.stop(behavior));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
async _updateInterceptionPatterns() {
|
||||
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||
await this._channel.setNetworkInterceptionPatterns({
|
||||
patterns
|
||||
});
|
||||
}
|
||||
async _updateWebSocketInterceptionPatterns() {
|
||||
const patterns = network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||
await this._channel.setWebSocketInterceptionPatterns({
|
||||
patterns
|
||||
});
|
||||
}
|
||||
_effectiveCloseReason() {
|
||||
var _this$_browser3;
|
||||
return this._closeReason || ((_this$_browser3 = this._browser) === null || _this$_browser3 === void 0 ? void 0 : _this$_browser3._closeReason);
|
||||
}
|
||||
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = _waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== _events.Events.BrowserContext.Close) waiter.rejectOnEvent(this, _events.Events.BrowserContext.Close, () => new _errors.TargetClosedError(this._effectiveCloseReason()));
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
async storageState(options = {}) {
|
||||
const state = await this._channel.storageState();
|
||||
if (options.path) {
|
||||
await (0, _utils.mkdirIfNeeded)(options.path);
|
||||
await _fs.default.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
backgroundPages() {
|
||||
return [...this._backgroundPages];
|
||||
}
|
||||
serviceWorkers() {
|
||||
return [...this._serviceWorkers];
|
||||
}
|
||||
async newCDPSession(page) {
|
||||
// channelOwner.ts's validation messages don't handle the pseudo-union type, so we're explicit here
|
||||
if (!(page instanceof _page.Page) && !(page instanceof _frame.Frame)) throw new Error('page: expected Page or Frame');
|
||||
const result = await this._channel.newCDPSession(page instanceof _page.Page ? {
|
||||
page: page._channel
|
||||
} : {
|
||||
frame: page._channel
|
||||
});
|
||||
return _cdpSession.CDPSession.from(result.session);
|
||||
}
|
||||
_onClose() {
|
||||
var _this$_browserType;
|
||||
if (this._browser) this._browser._contexts.delete(this);
|
||||
(_this$_browserType = this._browserType) === null || _this$_browserType === void 0 || (_this$_browserType = _this$_browserType._contexts) === null || _this$_browserType === void 0 || _this$_browserType.delete(this);
|
||||
this._disposeHarRouters();
|
||||
this.tracing._resetStackCounter();
|
||||
this.emit(_events.Events.BrowserContext.Close, this);
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async close(options = {}) {
|
||||
if (this._closeWasCalled) return;
|
||||
this._closeReason = options.reason;
|
||||
this._closeWasCalled = true;
|
||||
await this._wrapApiCall(async () => {
|
||||
await this.request.dispose(options);
|
||||
}, true);
|
||||
await this._wrapApiCall(async () => {
|
||||
var _this$_browserType2;
|
||||
await ((_this$_browserType2 = this._browserType) === null || _this$_browserType2 === void 0 ? void 0 : _this$_browserType2._willCloseContext(this));
|
||||
for (const [harId, harParams] of this._harRecorders) {
|
||||
const har = await this._channel.harExport({
|
||||
harId
|
||||
});
|
||||
const artifact = _artifact.Artifact.from(har.artifact);
|
||||
// Server side will compress artifact if content is attach or if file is .zip.
|
||||
const isCompressed = harParams.content === 'attach' || harParams.path.endsWith('.zip');
|
||||
const needCompressed = harParams.path.endsWith('.zip');
|
||||
if (isCompressed && !needCompressed) {
|
||||
await artifact.saveAs(harParams.path + '.tmp');
|
||||
await this._connection.localUtils()._channel.harUnzip({
|
||||
zipFile: harParams.path + '.tmp',
|
||||
harFile: harParams.path
|
||||
});
|
||||
} else {
|
||||
await artifact.saveAs(harParams.path);
|
||||
}
|
||||
await artifact.delete();
|
||||
}
|
||||
}, true);
|
||||
await this._channel.close(options);
|
||||
await this._closedPromise;
|
||||
}
|
||||
async _enableRecorder(params) {
|
||||
await this._channel.enableRecorder(params);
|
||||
}
|
||||
}
|
||||
exports.BrowserContext = BrowserContext;
|
||||
async function prepareStorageState(options) {
|
||||
if (typeof options.storageState !== 'string') return options.storageState;
|
||||
try {
|
||||
return JSON.parse(await _fs.default.promises.readFile(options.storageState, 'utf8'));
|
||||
} catch (e) {
|
||||
(0, _stackTrace.rewriteErrorMessage)(e, `Error reading storage state from ${options.storageState}:\n` + e.message);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
function prepareRecordHarOptions(options) {
|
||||
if (!options) return;
|
||||
return {
|
||||
path: options.path,
|
||||
content: options.content || (options.omitContent ? 'omit' : undefined),
|
||||
urlGlob: (0, _utils.isString)(options.urlFilter) ? options.urlFilter : undefined,
|
||||
urlRegexSource: (0, _utils.isRegExp)(options.urlFilter) ? options.urlFilter.source : undefined,
|
||||
urlRegexFlags: (0, _utils.isRegExp)(options.urlFilter) ? options.urlFilter.flags : undefined,
|
||||
mode: options.mode
|
||||
};
|
||||
}
|
||||
async function prepareBrowserContextParams(options) {
|
||||
if (options.videoSize && !options.videosPath) throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||
if (options.extraHTTPHeaders) network.validateHeaders(options.extraHTTPHeaders);
|
||||
const contextParams = {
|
||||
...options,
|
||||
viewport: options.viewport === null ? undefined : options.viewport,
|
||||
noDefaultViewport: options.viewport === null,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? (0, _utils.headersObjectToArray)(options.extraHTTPHeaders) : undefined,
|
||||
storageState: await prepareStorageState(options),
|
||||
serviceWorkers: options.serviceWorkers,
|
||||
recordHar: prepareRecordHarOptions(options.recordHar),
|
||||
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
|
||||
reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion,
|
||||
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors,
|
||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||
clientCertificates: await toClientCertificatesProtocol(options.clientCertificates)
|
||||
};
|
||||
if (!contextParams.recordVideo && options.videosPath) {
|
||||
contextParams.recordVideo = {
|
||||
dir: options.videosPath,
|
||||
size: options.videoSize
|
||||
};
|
||||
}
|
||||
if (contextParams.recordVideo && contextParams.recordVideo.dir) contextParams.recordVideo.dir = _path.default.resolve(process.cwd(), contextParams.recordVideo.dir);
|
||||
return contextParams;
|
||||
}
|
||||
function toAcceptDownloadsProtocol(acceptDownloads) {
|
||||
if (acceptDownloads === undefined) return undefined;
|
||||
if (acceptDownloads) return 'accept';
|
||||
return 'deny';
|
||||
}
|
||||
async function toClientCertificatesProtocol(certs) {
|
||||
if (!certs) return undefined;
|
||||
const bufferizeContent = async (value, path) => {
|
||||
if (value) return value;
|
||||
if (path) return await _fs.default.promises.readFile(path);
|
||||
};
|
||||
return await Promise.all(certs.map(async cert => ({
|
||||
origin: cert.origin,
|
||||
cert: await bufferizeContent(cert.cert, cert.certPath),
|
||||
key: await bufferizeContent(cert.key, cert.keyPath),
|
||||
pfx: await bufferizeContent(cert.pfx, cert.pfxPath),
|
||||
passphrase: cert.passphrase
|
||||
})));
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.BrowserType = void 0;
|
||||
var _browser3 = require("./browser");
|
||||
var _browserContext = require("./browserContext");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _connection = require("./connection");
|
||||
var _events = require("./events");
|
||||
var _clientHelper = require("./clientHelper");
|
||||
var _utils = require("../utils");
|
||||
var _timeoutRunner = require("../utils/timeoutRunner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This is here just for api generation and checking.
|
||||
|
||||
class BrowserType extends _channelOwner.ChannelOwner {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this._serverLauncher = void 0;
|
||||
this._contexts = new Set();
|
||||
this._playwright = void 0;
|
||||
// Instrumentation.
|
||||
this._defaultContextOptions = void 0;
|
||||
this._defaultContextTimeout = void 0;
|
||||
this._defaultContextNavigationTimeout = void 0;
|
||||
this._defaultLaunchOptions = void 0;
|
||||
}
|
||||
static from(browserType) {
|
||||
return browserType._object;
|
||||
}
|
||||
executablePath() {
|
||||
if (!this._initializer.executablePath) throw new Error('Browser is not supported on current platform');
|
||||
return this._initializer.executablePath;
|
||||
}
|
||||
name() {
|
||||
return this._initializer.name;
|
||||
}
|
||||
async launch(options = {}) {
|
||||
var _this$_defaultLaunchO;
|
||||
(0, _utils.assert)(!options.userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
|
||||
(0, _utils.assert)(!options.port, 'Cannot specify a port without launching as a server.');
|
||||
const logger = options.logger || ((_this$_defaultLaunchO = this._defaultLaunchOptions) === null || _this$_defaultLaunchO === void 0 ? void 0 : _this$_defaultLaunchO.logger);
|
||||
options = {
|
||||
...this._defaultLaunchOptions,
|
||||
...options
|
||||
};
|
||||
const launchOptions = {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? (0, _clientHelper.envObjectToArray)(options.env) : undefined
|
||||
};
|
||||
return await this._wrapApiCall(async () => {
|
||||
const browser = _browser3.Browser.from((await this._channel.launch(launchOptions)).browser);
|
||||
this._didLaunchBrowser(browser, options, logger);
|
||||
return browser;
|
||||
});
|
||||
}
|
||||
async launchServer(options = {}) {
|
||||
if (!this._serverLauncher) throw new Error('Launching server is not supported');
|
||||
options = {
|
||||
...this._defaultLaunchOptions,
|
||||
...options
|
||||
};
|
||||
return await this._serverLauncher.launchServer(options);
|
||||
}
|
||||
async launchPersistentContext(userDataDir, options = {}) {
|
||||
var _this$_defaultLaunchO2;
|
||||
const logger = options.logger || ((_this$_defaultLaunchO2 = this._defaultLaunchOptions) === null || _this$_defaultLaunchO2 === void 0 ? void 0 : _this$_defaultLaunchO2.logger);
|
||||
(0, _utils.assert)(!options.port, 'Cannot specify a port without launching as a server.');
|
||||
options = {
|
||||
...this._defaultLaunchOptions,
|
||||
...this._defaultContextOptions,
|
||||
...options
|
||||
};
|
||||
const contextParams = await (0, _browserContext.prepareBrowserContextParams)(options);
|
||||
const persistentParams = {
|
||||
...contextParams,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? (0, _clientHelper.envObjectToArray)(options.env) : undefined,
|
||||
channel: options.channel,
|
||||
userDataDir
|
||||
};
|
||||
return await this._wrapApiCall(async () => {
|
||||
const result = await this._channel.launchPersistentContext(persistentParams);
|
||||
const context = _browserContext.BrowserContext.from(result.context);
|
||||
await this._didCreateContext(context, contextParams, options, logger);
|
||||
return context;
|
||||
});
|
||||
}
|
||||
async connect(optionsOrWsEndpoint, options) {
|
||||
if (typeof optionsOrWsEndpoint === 'string') return await this._connect({
|
||||
...options,
|
||||
wsEndpoint: optionsOrWsEndpoint
|
||||
});
|
||||
(0, _utils.assert)(optionsOrWsEndpoint.wsEndpoint, 'options.wsEndpoint is required');
|
||||
return await this._connect(optionsOrWsEndpoint);
|
||||
}
|
||||
async _connect(params) {
|
||||
const logger = params.logger;
|
||||
return await this._wrapApiCall(async () => {
|
||||
var _params$exposeNetwork;
|
||||
const deadline = params.timeout ? (0, _utils.monotonicTime)() + params.timeout : 0;
|
||||
const headers = {
|
||||
'x-playwright-browser': this.name(),
|
||||
...params.headers
|
||||
};
|
||||
const localUtils = this._connection.localUtils();
|
||||
const connectParams = {
|
||||
wsEndpoint: params.wsEndpoint,
|
||||
headers,
|
||||
exposeNetwork: (_params$exposeNetwork = params.exposeNetwork) !== null && _params$exposeNetwork !== void 0 ? _params$exposeNetwork : params._exposeNetwork,
|
||||
slowMo: params.slowMo,
|
||||
timeout: params.timeout
|
||||
};
|
||||
if (params.__testHookRedirectPortForwarding) connectParams.socksProxyRedirectPortForTest = params.__testHookRedirectPortForwarding;
|
||||
const {
|
||||
pipe,
|
||||
headers: connectHeaders
|
||||
} = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new _connection.Connection(localUtils, this._instrumentation);
|
||||
connection.markAsRemote();
|
||||
connection.on('close', closePipe);
|
||||
let browser;
|
||||
let closeError;
|
||||
const onPipeClosed = reason => {
|
||||
// Emulate all pages, contexts and the browser closing upon disconnect.
|
||||
for (const context of ((_browser = browser) === null || _browser === void 0 ? void 0 : _browser.contexts()) || []) {
|
||||
var _browser;
|
||||
for (const page of context.pages()) page._onClose();
|
||||
context._onClose();
|
||||
}
|
||||
connection.close(reason || closeError);
|
||||
// Give a chance to any API call promises to reject upon page/context closure.
|
||||
// This happens naturally when we receive page.onClose and browser.onClose from the server
|
||||
// in separate tasks. However, upon pipe closure we used to dispatch them all synchronously
|
||||
// here and promises did not have a chance to reject.
|
||||
// The order of rejects vs closure is a part of the API contract and our test runner
|
||||
// relies on it to attribute rejections to the right test.
|
||||
setTimeout(() => {
|
||||
var _browser2;
|
||||
return (_browser2 = browser) === null || _browser2 === void 0 ? void 0 : _browser2._didClose();
|
||||
}, 0);
|
||||
};
|
||||
pipe.on('closed', params => onPipeClosed(params.reason));
|
||||
connection.onmessage = message => this._wrapApiCall(() => pipe.send({
|
||||
message
|
||||
}).catch(() => onPipeClosed()), /* isInternal */true);
|
||||
pipe.on('message', ({
|
||||
message
|
||||
}) => {
|
||||
try {
|
||||
connection.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
closePipe();
|
||||
}
|
||||
});
|
||||
const result = await (0, _timeoutRunner.raceAgainstDeadline)(async () => {
|
||||
// For tests.
|
||||
if (params.__testHookBeforeCreateBrowser) await params.__testHookBeforeCreateBrowser();
|
||||
const playwright = await connection.initializePlaywright();
|
||||
if (!playwright._initializer.preLaunchedBrowser) {
|
||||
closePipe();
|
||||
throw new Error('Malformed endpoint. Did you use BrowserType.launchServer method?');
|
||||
}
|
||||
playwright._setSelectors(this._playwright.selectors);
|
||||
browser = _browser3.Browser.from(playwright._initializer.preLaunchedBrowser);
|
||||
this._didLaunchBrowser(browser, {}, logger);
|
||||
browser._shouldCloseConnectionOnClose = true;
|
||||
browser._connectHeaders = connectHeaders;
|
||||
browser.on(_events.Events.Browser.Disconnected, () => this._wrapApiCall(() => closePipe(), /* isInternal */true));
|
||||
return browser;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
closePipe();
|
||||
throw new Error(`Timeout ${params.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
}
|
||||
async connectOverCDP(endpointURLOrOptions, options) {
|
||||
if (typeof endpointURLOrOptions === 'string') return await this._connectOverCDP(endpointURLOrOptions, options);
|
||||
const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
|
||||
(0, _utils.assert)(endpointURL, 'Cannot connect over CDP without wsEndpoint.');
|
||||
return await this.connectOverCDP(endpointURL, endpointURLOrOptions);
|
||||
}
|
||||
async _connectOverCDP(endpointURL, params = {}) {
|
||||
if (this.name() !== 'chromium') throw new Error('Connecting over CDP is only supported in Chromium.');
|
||||
const headers = params.headers ? (0, _utils.headersObjectToArray)(params.headers) : undefined;
|
||||
const result = await this._channel.connectOverCDP({
|
||||
endpointURL,
|
||||
headers,
|
||||
slowMo: params.slowMo,
|
||||
timeout: params.timeout
|
||||
});
|
||||
const browser = _browser3.Browser.from(result.browser);
|
||||
this._didLaunchBrowser(browser, {}, params.logger);
|
||||
if (result.defaultContext) await this._didCreateContext(_browserContext.BrowserContext.from(result.defaultContext), {}, {}, params.logger);
|
||||
return browser;
|
||||
}
|
||||
_didLaunchBrowser(browser, browserOptions, logger) {
|
||||
browser._browserType = this;
|
||||
browser._options = browserOptions;
|
||||
browser._logger = logger;
|
||||
}
|
||||
async _didCreateContext(context, contextOptions, browserOptions, logger) {
|
||||
context._logger = logger;
|
||||
context._browserType = this;
|
||||
this._contexts.add(context);
|
||||
context._setOptions(contextOptions, browserOptions);
|
||||
if (this._defaultContextTimeout !== undefined) context.setDefaultTimeout(this._defaultContextTimeout);
|
||||
if (this._defaultContextNavigationTimeout !== undefined) context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
|
||||
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||
}
|
||||
async _willCloseContext(context) {
|
||||
this._contexts.delete(context);
|
||||
await this._instrumentation.runBeforeCloseBrowserContext(context);
|
||||
}
|
||||
}
|
||||
exports.BrowserType = BrowserType;
|
||||
@@ -1,53 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.CDPSession = void 0;
|
||||
var _channelOwner = require("./channelOwner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class CDPSession extends _channelOwner.ChannelOwner {
|
||||
static from(cdpSession) {
|
||||
return cdpSession._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._channel.on('event', ({
|
||||
method,
|
||||
params
|
||||
}) => {
|
||||
this.emit(method, params);
|
||||
});
|
||||
this.on = super.on;
|
||||
this.addListener = super.addListener;
|
||||
this.off = super.removeListener;
|
||||
this.removeListener = super.removeListener;
|
||||
this.once = super.once;
|
||||
}
|
||||
async send(method, params) {
|
||||
const result = await this._channel.send({
|
||||
method,
|
||||
params
|
||||
});
|
||||
return result.result;
|
||||
}
|
||||
async detach() {
|
||||
return await this._channel.detach();
|
||||
}
|
||||
}
|
||||
exports.CDPSession = CDPSession;
|
||||
@@ -1,235 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.ChannelOwner = void 0;
|
||||
var _eventEmitter = require("./eventEmitter");
|
||||
var _validator = require("../protocol/validator");
|
||||
var _debugLogger = require("../utils/debugLogger");
|
||||
var _stackTrace = require("../utils/stackTrace");
|
||||
var _utils = require("../utils");
|
||||
var _zones = require("../utils/zones");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class ChannelOwner extends _eventEmitter.EventEmitter {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super();
|
||||
this._connection = void 0;
|
||||
this._parent = void 0;
|
||||
this._objects = new Map();
|
||||
this._type = void 0;
|
||||
this._guid = void 0;
|
||||
this._channel = void 0;
|
||||
this._initializer = void 0;
|
||||
this._logger = void 0;
|
||||
this._instrumentation = void 0;
|
||||
this._eventToSubscriptionMapping = new Map();
|
||||
this._isInternalType = false;
|
||||
this._wasCollected = false;
|
||||
this.setMaxListeners(0);
|
||||
this._connection = parent instanceof ChannelOwner ? parent._connection : parent;
|
||||
this._type = type;
|
||||
this._guid = guid;
|
||||
this._parent = parent instanceof ChannelOwner ? parent : undefined;
|
||||
this._instrumentation = this._connection._instrumentation;
|
||||
this._connection._objects.set(guid, this);
|
||||
if (this._parent) {
|
||||
this._parent._objects.set(guid, this);
|
||||
this._logger = this._parent._logger;
|
||||
}
|
||||
this._channel = this._createChannel(new _eventEmitter.EventEmitter());
|
||||
this._initializer = initializer;
|
||||
}
|
||||
markAsInternalType() {
|
||||
this._isInternalType = true;
|
||||
}
|
||||
_setEventToSubscriptionMapping(mapping) {
|
||||
this._eventToSubscriptionMapping = mapping;
|
||||
}
|
||||
_updateSubscription(event, enabled) {
|
||||
const protocolEvent = this._eventToSubscriptionMapping.get(String(event));
|
||||
if (protocolEvent) {
|
||||
this._wrapApiCall(async () => {
|
||||
await this._channel.updateSubscription({
|
||||
event: protocolEvent,
|
||||
enabled
|
||||
});
|
||||
}, true).catch(() => {});
|
||||
}
|
||||
}
|
||||
on(event, listener) {
|
||||
if (!this.listenerCount(event)) this._updateSubscription(event, true);
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
addListener(event, listener) {
|
||||
if (!this.listenerCount(event)) this._updateSubscription(event, true);
|
||||
super.addListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
prependListener(event, listener) {
|
||||
if (!this.listenerCount(event)) this._updateSubscription(event, true);
|
||||
super.prependListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
off(event, listener) {
|
||||
super.off(event, listener);
|
||||
if (!this.listenerCount(event)) this._updateSubscription(event, false);
|
||||
return this;
|
||||
}
|
||||
removeListener(event, listener) {
|
||||
super.removeListener(event, listener);
|
||||
if (!this.listenerCount(event)) this._updateSubscription(event, false);
|
||||
return this;
|
||||
}
|
||||
_adopt(child) {
|
||||
child._parent._objects.delete(child._guid);
|
||||
this._objects.set(child._guid, child);
|
||||
child._parent = this;
|
||||
}
|
||||
_dispose(reason) {
|
||||
// Clean up from parent and connection.
|
||||
if (this._parent) this._parent._objects.delete(this._guid);
|
||||
this._connection._objects.delete(this._guid);
|
||||
this._wasCollected = reason === 'gc';
|
||||
|
||||
// Dispose all children.
|
||||
for (const object of [...this._objects.values()]) object._dispose(reason);
|
||||
this._objects.clear();
|
||||
}
|
||||
_debugScopeState() {
|
||||
return {
|
||||
_guid: this._guid,
|
||||
objects: Array.from(this._objects.values()).map(o => o._debugScopeState())
|
||||
};
|
||||
}
|
||||
_createChannel(base) {
|
||||
const channel = new Proxy(base, {
|
||||
get: (obj, prop) => {
|
||||
if (typeof prop === 'string') {
|
||||
const validator = (0, _validator.maybeFindValidator)(this._type, prop, 'Params');
|
||||
if (validator) {
|
||||
return async params => {
|
||||
return await this._wrapApiCall(async apiZone => {
|
||||
const {
|
||||
apiName,
|
||||
frames,
|
||||
csi,
|
||||
callCookie,
|
||||
stepId
|
||||
} = apiZone.reported ? {
|
||||
apiName: undefined,
|
||||
csi: undefined,
|
||||
callCookie: undefined,
|
||||
frames: [],
|
||||
stepId: undefined
|
||||
} : apiZone;
|
||||
apiZone.reported = true;
|
||||
let currentStepId = stepId;
|
||||
if (csi && apiName) {
|
||||
const out = {};
|
||||
csi.onApiCallBegin(apiName, params, frames, callCookie, out);
|
||||
currentStepId = out.stepId;
|
||||
}
|
||||
return await this._connection.sendMessageToServer(this, prop, validator(params, '', {
|
||||
tChannelImpl: tChannelImplToWire,
|
||||
binary: this._connection.rawBuffers() ? 'buffer' : 'toBase64'
|
||||
}), apiName, frames, currentStepId);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
return obj[prop];
|
||||
}
|
||||
});
|
||||
channel._object = this;
|
||||
return channel;
|
||||
}
|
||||
async _wrapApiCall(func, isInternal) {
|
||||
const logger = this._logger;
|
||||
const apiZone = _zones.zones.zoneData('apiZone');
|
||||
if (apiZone) return await func(apiZone);
|
||||
const stackTrace = (0, _stackTrace.captureLibraryStackTrace)();
|
||||
let apiName = stackTrace.apiName;
|
||||
const frames = stackTrace.frames;
|
||||
if (isInternal === undefined) isInternal = this._isInternalType;
|
||||
if (isInternal) apiName = undefined;
|
||||
|
||||
// Enclosing zone could have provided the apiName and wallTime.
|
||||
const expectZone = _zones.zones.zoneData('expectZone');
|
||||
const stepId = expectZone === null || expectZone === void 0 ? void 0 : expectZone.stepId;
|
||||
if (!isInternal && expectZone) apiName = expectZone.title;
|
||||
|
||||
// If we are coming from the expectZone, there is no need to generate a new
|
||||
// step for the API call, since it will be generated by the expect itself.
|
||||
const csi = isInternal || expectZone ? undefined : this._instrumentation;
|
||||
const callCookie = {};
|
||||
try {
|
||||
logApiCall(logger, `=> ${apiName} started`, isInternal);
|
||||
const apiZone = {
|
||||
apiName,
|
||||
frames,
|
||||
isInternal,
|
||||
reported: false,
|
||||
csi,
|
||||
callCookie,
|
||||
stepId
|
||||
};
|
||||
const result = await _zones.zones.run('apiZone', apiZone, async () => await func(apiZone));
|
||||
csi === null || csi === void 0 || csi.onApiCallEnd(callCookie);
|
||||
logApiCall(logger, `<= ${apiName} succeeded`, isInternal);
|
||||
return result;
|
||||
} catch (e) {
|
||||
const innerError = (process.env.PWDEBUGIMPL || (0, _utils.isUnderTest)()) && e.stack ? '\n<inner error>\n' + e.stack : '';
|
||||
if (apiName && !apiName.includes('<anonymous>')) e.message = apiName + ': ' + e.message;
|
||||
const stackFrames = '\n' + (0, _stackTrace.stringifyStackFrames)(stackTrace.frames).join('\n') + innerError;
|
||||
if (stackFrames.trim()) e.stack = e.message + stackFrames;else e.stack = '';
|
||||
csi === null || csi === void 0 || csi.onApiCallEnd(callCookie, e);
|
||||
logApiCall(logger, `<= ${apiName} failed`, isInternal);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
_toImpl() {
|
||||
var _this$_connection$toI, _this$_connection;
|
||||
return (_this$_connection$toI = (_this$_connection = this._connection).toImpl) === null || _this$_connection$toI === void 0 ? void 0 : _this$_connection$toI.call(_this$_connection, this);
|
||||
}
|
||||
toJSON() {
|
||||
// Jest's expect library tries to print objects sometimes.
|
||||
// RPC objects can contain links to lots of other objects,
|
||||
// which can cause jest to crash. Let's help it out
|
||||
// by just returning the important values.
|
||||
return {
|
||||
_type: this._type,
|
||||
_guid: this._guid
|
||||
};
|
||||
}
|
||||
}
|
||||
exports.ChannelOwner = ChannelOwner;
|
||||
function logApiCall(logger, message, isNested) {
|
||||
if (isNested) return;
|
||||
if (logger && logger.isEnabled('api', 'info')) logger.log('api', 'info', message, [], {
|
||||
color: 'cyan'
|
||||
});
|
||||
_debugLogger.debugLogger.log('api', message);
|
||||
}
|
||||
function tChannelImplToWire(names, arg, path, context) {
|
||||
if (arg._object instanceof ChannelOwner && (names === '*' || names.includes(arg._object._type))) return {
|
||||
guid: arg._object._guid
|
||||
};
|
||||
throw new _validator.ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.addSourceUrlToScript = addSourceUrlToScript;
|
||||
exports.envObjectToArray = envObjectToArray;
|
||||
exports.evaluationScript = evaluationScript;
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _utils = require("../utils");
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function envObjectToArray(env) {
|
||||
const result = [];
|
||||
for (const name in env) {
|
||||
if (!Object.is(env[name], undefined)) result.push({
|
||||
name,
|
||||
value: String(env[name])
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function evaluationScript(fun, arg, addSourceUrl = true) {
|
||||
if (typeof fun === 'function') {
|
||||
const source = fun.toString();
|
||||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||
return `(${source})(${argString})`;
|
||||
}
|
||||
if (arg !== undefined) throw new Error('Cannot evaluate a string with arguments');
|
||||
if ((0, _utils.isString)(fun)) return fun;
|
||||
if (fun.content !== undefined) return fun.content;
|
||||
if (fun.path !== undefined) {
|
||||
let source = await _fs.default.promises.readFile(fun.path, 'utf8');
|
||||
if (addSourceUrl) source = addSourceUrlToScript(source, fun.path);
|
||||
return source;
|
||||
}
|
||||
throw new Error('Either path or content property must be present');
|
||||
}
|
||||
function addSourceUrlToScript(source, path) {
|
||||
return `${source}\n//# sourceURL=${path.replace(/\n/g, '')}`;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.createInstrumentation = createInstrumentation;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function createInstrumentation() {
|
||||
const listeners = [];
|
||||
return new Proxy({}, {
|
||||
get: (obj, prop) => {
|
||||
if (typeof prop !== 'string') return obj[prop];
|
||||
if (prop === 'addListener') return listener => listeners.push(listener);
|
||||
if (prop === 'removeListener') return listener => listeners.splice(listeners.indexOf(listener), 1);
|
||||
if (prop === 'removeAllListeners') return () => listeners.splice(0, listeners.length);
|
||||
if (prop.startsWith('run')) {
|
||||
return async (...params) => {
|
||||
for (const listener of listeners) {
|
||||
var _prop, _ref;
|
||||
await ((_prop = (_ref = listener)[prop]) === null || _prop === void 0 ? void 0 : _prop.call(_ref, ...params));
|
||||
}
|
||||
};
|
||||
}
|
||||
if (prop.startsWith('on')) {
|
||||
return (...params) => {
|
||||
for (const listener of listeners) {
|
||||
var _prop2, _ref2;
|
||||
(_prop2 = (_ref2 = listener)[prop]) === null || _prop2 === void 0 || _prop2.call(_ref2, ...params);
|
||||
}
|
||||
};
|
||||
}
|
||||
return obj[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Clock = void 0;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Clock {
|
||||
constructor(browserContext) {
|
||||
this._browserContext = void 0;
|
||||
this._browserContext = browserContext;
|
||||
}
|
||||
async install(options = {}) {
|
||||
await this._browserContext._channel.clockInstall(options.time !== undefined ? parseTime(options.time) : {});
|
||||
}
|
||||
async fastForward(ticks) {
|
||||
await this._browserContext._channel.clockFastForward(parseTicks(ticks));
|
||||
}
|
||||
async pauseAt(time) {
|
||||
await this._browserContext._channel.clockPauseAt(parseTime(time));
|
||||
}
|
||||
async resume() {
|
||||
await this._browserContext._channel.clockResume({});
|
||||
}
|
||||
async runFor(ticks) {
|
||||
await this._browserContext._channel.clockRunFor(parseTicks(ticks));
|
||||
}
|
||||
async setFixedTime(time) {
|
||||
await this._browserContext._channel.clockSetFixedTime(parseTime(time));
|
||||
}
|
||||
async setSystemTime(time) {
|
||||
await this._browserContext._channel.clockSetSystemTime(parseTime(time));
|
||||
}
|
||||
}
|
||||
exports.Clock = Clock;
|
||||
function parseTime(time) {
|
||||
if (typeof time === 'number') return {
|
||||
timeNumber: time
|
||||
};
|
||||
if (typeof time === 'string') return {
|
||||
timeString: time
|
||||
};
|
||||
if (!isFinite(time.getTime())) throw new Error(`Invalid date: ${time}`);
|
||||
return {
|
||||
timeNumber: time.getTime()
|
||||
};
|
||||
}
|
||||
function parseTicks(ticks) {
|
||||
return {
|
||||
ticksNumber: typeof ticks === 'number' ? ticks : undefined,
|
||||
ticksString: typeof ticks === 'string' ? ticks : undefined
|
||||
};
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Connection = void 0;
|
||||
var _browser = require("./browser");
|
||||
var _browserContext = require("./browserContext");
|
||||
var _browserType = require("./browserType");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _elementHandle = require("./elementHandle");
|
||||
var _frame = require("./frame");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _network = require("./network");
|
||||
var _page = require("./page");
|
||||
var _worker = require("./worker");
|
||||
var _dialog = require("./dialog");
|
||||
var _errors = require("./errors");
|
||||
var _cdpSession = require("./cdpSession");
|
||||
var _playwright = require("./playwright");
|
||||
var _electron = require("./electron");
|
||||
var _stream = require("./stream");
|
||||
var _writableStream = require("./writableStream");
|
||||
var _debugLogger = require("../utils/debugLogger");
|
||||
var _selectors = require("./selectors");
|
||||
var _android = require("./android");
|
||||
var _artifact = require("./artifact");
|
||||
var _events = require("events");
|
||||
var _jsonPipe = require("./jsonPipe");
|
||||
var _fetch = require("./fetch");
|
||||
var _localUtils = require("./localUtils");
|
||||
var _tracing = require("./tracing");
|
||||
var _validator = require("../protocol/validator");
|
||||
var _clientInstrumentation = require("./clientInstrumentation");
|
||||
var _utils = require("../utils");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Root extends _channelOwner.ChannelOwner {
|
||||
constructor(connection) {
|
||||
super(connection, 'Root', '', {});
|
||||
}
|
||||
async initialize() {
|
||||
return _playwright.Playwright.from((await this._channel.initialize({
|
||||
sdkLanguage: 'javascript'
|
||||
})).playwright);
|
||||
}
|
||||
}
|
||||
class DummyChannelOwner extends _channelOwner.ChannelOwner {}
|
||||
class Connection extends _events.EventEmitter {
|
||||
constructor(localUtils, instrumentation) {
|
||||
super();
|
||||
this._objects = new Map();
|
||||
this.onmessage = message => {};
|
||||
this._lastId = 0;
|
||||
this._callbacks = new Map();
|
||||
this._rootObject = void 0;
|
||||
this._closedError = void 0;
|
||||
this._isRemote = false;
|
||||
this._localUtils = void 0;
|
||||
this._rawBuffers = false;
|
||||
// Some connections allow resolving in-process dispatchers.
|
||||
this.toImpl = void 0;
|
||||
this._tracingCount = 0;
|
||||
this._instrumentation = void 0;
|
||||
this._rootObject = new Root(this);
|
||||
this._localUtils = localUtils;
|
||||
this._instrumentation = instrumentation || (0, _clientInstrumentation.createInstrumentation)();
|
||||
}
|
||||
markAsRemote() {
|
||||
this._isRemote = true;
|
||||
}
|
||||
isRemote() {
|
||||
return this._isRemote;
|
||||
}
|
||||
useRawBuffers() {
|
||||
this._rawBuffers = true;
|
||||
}
|
||||
rawBuffers() {
|
||||
return this._rawBuffers;
|
||||
}
|
||||
localUtils() {
|
||||
return this._localUtils;
|
||||
}
|
||||
async initializePlaywright() {
|
||||
return await this._rootObject.initialize();
|
||||
}
|
||||
getObjectWithKnownName(guid) {
|
||||
return this._objects.get(guid);
|
||||
}
|
||||
setIsTracing(isTracing) {
|
||||
if (isTracing) this._tracingCount++;else this._tracingCount--;
|
||||
}
|
||||
async sendMessageToServer(object, method, params, apiName, frames, stepId) {
|
||||
var _this$_localUtils;
|
||||
if (this._closedError) throw this._closedError;
|
||||
if (object._wasCollected) throw new Error('The object has been collected to prevent unbounded heap growth.');
|
||||
const guid = object._guid;
|
||||
const type = object._type;
|
||||
const id = ++this._lastId;
|
||||
const message = {
|
||||
id,
|
||||
guid,
|
||||
method,
|
||||
params
|
||||
};
|
||||
if (_debugLogger.debugLogger.isEnabled('channel')) {
|
||||
// Do not include metadata in debug logs to avoid noise.
|
||||
_debugLogger.debugLogger.log('channel', 'SEND> ' + JSON.stringify(message));
|
||||
}
|
||||
const location = frames[0] ? {
|
||||
file: frames[0].file,
|
||||
line: frames[0].line,
|
||||
column: frames[0].column
|
||||
} : undefined;
|
||||
const metadata = {
|
||||
apiName,
|
||||
location,
|
||||
internal: !apiName,
|
||||
stepId
|
||||
};
|
||||
if (this._tracingCount && frames && type !== 'LocalUtils') (_this$_localUtils = this._localUtils) === null || _this$_localUtils === void 0 || _this$_localUtils._channel.addStackToTracingNoReply({
|
||||
callData: {
|
||||
stack: frames,
|
||||
id
|
||||
}
|
||||
}).catch(() => {});
|
||||
// We need to exit zones before calling into the server, otherwise
|
||||
// when we receive events from the server, we would be in an API zone.
|
||||
_utils.zones.exitZones(() => this.onmessage({
|
||||
...message,
|
||||
metadata
|
||||
}));
|
||||
return await new Promise((resolve, reject) => this._callbacks.set(id, {
|
||||
resolve,
|
||||
reject,
|
||||
apiName,
|
||||
type,
|
||||
method
|
||||
}));
|
||||
}
|
||||
dispatch(message) {
|
||||
if (this._closedError) return;
|
||||
const {
|
||||
id,
|
||||
guid,
|
||||
method,
|
||||
params,
|
||||
result,
|
||||
error,
|
||||
log
|
||||
} = message;
|
||||
if (id) {
|
||||
if (_debugLogger.debugLogger.isEnabled('channel')) _debugLogger.debugLogger.log('channel', '<RECV ' + JSON.stringify(message));
|
||||
const callback = this._callbacks.get(id);
|
||||
if (!callback) throw new Error(`Cannot find command to respond: ${id}`);
|
||||
this._callbacks.delete(id);
|
||||
if (error && !result) {
|
||||
const parsedError = (0, _errors.parseError)(error);
|
||||
(0, _utils.rewriteErrorMessage)(parsedError, parsedError.message + (0, _utils.formatCallLog)(log));
|
||||
callback.reject(parsedError);
|
||||
} else {
|
||||
const validator = (0, _validator.findValidator)(callback.type, callback.method, 'Result');
|
||||
callback.resolve(validator(result, '', {
|
||||
tChannelImpl: this._tChannelImplFromWire.bind(this),
|
||||
binary: this._rawBuffers ? 'buffer' : 'fromBase64'
|
||||
}));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_debugLogger.debugLogger.isEnabled('channel')) _debugLogger.debugLogger.log('channel', '<EVENT ' + JSON.stringify(message));
|
||||
if (method === '__create__') {
|
||||
this._createRemoteObject(guid, params.type, params.guid, params.initializer);
|
||||
return;
|
||||
}
|
||||
const object = this._objects.get(guid);
|
||||
if (!object) throw new Error(`Cannot find object to "${method}": ${guid}`);
|
||||
if (method === '__adopt__') {
|
||||
const child = this._objects.get(params.guid);
|
||||
if (!child) throw new Error(`Unknown new child: ${params.guid}`);
|
||||
object._adopt(child);
|
||||
return;
|
||||
}
|
||||
if (method === '__dispose__') {
|
||||
object._dispose(params.reason);
|
||||
return;
|
||||
}
|
||||
const validator = (0, _validator.findValidator)(object._type, method, 'Event');
|
||||
object._channel.emit(method, validator(params, '', {
|
||||
tChannelImpl: this._tChannelImplFromWire.bind(this),
|
||||
binary: this._rawBuffers ? 'buffer' : 'fromBase64'
|
||||
}));
|
||||
}
|
||||
close(cause) {
|
||||
if (this._closedError) return;
|
||||
this._closedError = new _errors.TargetClosedError(cause);
|
||||
for (const callback of this._callbacks.values()) callback.reject(this._closedError);
|
||||
this._callbacks.clear();
|
||||
this.emit('close');
|
||||
}
|
||||
_tChannelImplFromWire(names, arg, path, context) {
|
||||
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
|
||||
const object = this._objects.get(arg.guid);
|
||||
if (!object) throw new Error(`Object with guid ${arg.guid} was not bound in the connection`);
|
||||
if (names !== '*' && !names.includes(object._type)) throw new _validator.ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
return object._channel;
|
||||
}
|
||||
throw new _validator.ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
}
|
||||
_createRemoteObject(parentGuid, type, guid, initializer) {
|
||||
const parent = this._objects.get(parentGuid);
|
||||
if (!parent) throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
|
||||
let result;
|
||||
const validator = (0, _validator.findValidator)(type, '', 'Initializer');
|
||||
initializer = validator(initializer, '', {
|
||||
tChannelImpl: this._tChannelImplFromWire.bind(this),
|
||||
binary: this._rawBuffers ? 'buffer' : 'fromBase64'
|
||||
});
|
||||
switch (type) {
|
||||
case 'Android':
|
||||
result = new _android.Android(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'AndroidSocket':
|
||||
result = new _android.AndroidSocket(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'AndroidDevice':
|
||||
result = new _android.AndroidDevice(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'APIRequestContext':
|
||||
result = new _fetch.APIRequestContext(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Artifact':
|
||||
result = new _artifact.Artifact(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'BindingCall':
|
||||
result = new _page.BindingCall(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Browser':
|
||||
result = new _browser.Browser(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'BrowserContext':
|
||||
result = new _browserContext.BrowserContext(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'BrowserType':
|
||||
result = new _browserType.BrowserType(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'CDPSession':
|
||||
result = new _cdpSession.CDPSession(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Dialog':
|
||||
result = new _dialog.Dialog(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Electron':
|
||||
result = new _electron.Electron(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'ElectronApplication':
|
||||
result = new _electron.ElectronApplication(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'ElementHandle':
|
||||
result = new _elementHandle.ElementHandle(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Frame':
|
||||
result = new _frame.Frame(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'JSHandle':
|
||||
result = new _jsHandle.JSHandle(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'JsonPipe':
|
||||
result = new _jsonPipe.JsonPipe(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'LocalUtils':
|
||||
result = new _localUtils.LocalUtils(parent, type, guid, initializer);
|
||||
if (!this._localUtils) this._localUtils = result;
|
||||
break;
|
||||
case 'Page':
|
||||
result = new _page.Page(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Playwright':
|
||||
result = new _playwright.Playwright(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Request':
|
||||
result = new _network.Request(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Response':
|
||||
result = new _network.Response(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Route':
|
||||
result = new _network.Route(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Stream':
|
||||
result = new _stream.Stream(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Selectors':
|
||||
result = new _selectors.SelectorsOwner(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'SocksSupport':
|
||||
result = new DummyChannelOwner(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Tracing':
|
||||
result = new _tracing.Tracing(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'WebSocket':
|
||||
result = new _network.WebSocket(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'WebSocketRoute':
|
||||
result = new _network.WebSocketRoute(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'Worker':
|
||||
result = new _worker.Worker(parent, type, guid, initializer);
|
||||
break;
|
||||
case 'WritableStream':
|
||||
result = new _writableStream.WritableStream(parent, type, guid, initializer);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Missing type ' + type);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
exports.Connection = Connection;
|
||||
@@ -1,55 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.ConsoleMessage = void 0;
|
||||
var util = _interopRequireWildcard(require("util"));
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _page = require("./page");
|
||||
let _util$inspect$custom;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
_util$inspect$custom = util.inspect.custom;
|
||||
class ConsoleMessage {
|
||||
constructor(event) {
|
||||
this._page = void 0;
|
||||
this._event = void 0;
|
||||
this._page = 'page' in event && event.page ? _page.Page.from(event.page) : null;
|
||||
this._event = event;
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
type() {
|
||||
return this._event.type;
|
||||
}
|
||||
text() {
|
||||
return this._event.text;
|
||||
}
|
||||
args() {
|
||||
return this._event.args.map(_jsHandle.JSHandle.from);
|
||||
}
|
||||
location() {
|
||||
return this._event.location;
|
||||
}
|
||||
[_util$inspect$custom]() {
|
||||
return this.text();
|
||||
}
|
||||
}
|
||||
exports.ConsoleMessage = ConsoleMessage;
|
||||
@@ -1,41 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Coverage = void 0;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Coverage {
|
||||
constructor(channel) {
|
||||
this._channel = void 0;
|
||||
this._channel = channel;
|
||||
}
|
||||
async startJSCoverage(options = {}) {
|
||||
await this._channel.startJSCoverage(options);
|
||||
}
|
||||
async stopJSCoverage() {
|
||||
return (await this._channel.stopJSCoverage()).entries;
|
||||
}
|
||||
async startCSSCoverage(options = {}) {
|
||||
await this._channel.startCSSCoverage(options);
|
||||
}
|
||||
async stopCSSCoverage() {
|
||||
return (await this._channel.stopCSSCoverage()).entries;
|
||||
}
|
||||
}
|
||||
exports.Coverage = Coverage;
|
||||
@@ -1,57 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Dialog = void 0;
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _page = require("./page");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Dialog extends _channelOwner.ChannelOwner {
|
||||
static from(dialog) {
|
||||
return dialog._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
// Note: dialogs that open early during page initialization block it.
|
||||
// Therefore, we must report the dialog without a page to be able to handle it.
|
||||
this._page = void 0;
|
||||
this._page = _page.Page.fromNullable(initializer.page);
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
type() {
|
||||
return this._initializer.type;
|
||||
}
|
||||
message() {
|
||||
return this._initializer.message;
|
||||
}
|
||||
defaultValue() {
|
||||
return this._initializer.defaultValue;
|
||||
}
|
||||
async accept(promptText) {
|
||||
await this._channel.accept({
|
||||
promptText
|
||||
});
|
||||
}
|
||||
async dismiss() {
|
||||
await this._channel.dismiss();
|
||||
}
|
||||
}
|
||||
exports.Dialog = Dialog;
|
||||
@@ -1,62 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Download = void 0;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Download {
|
||||
constructor(page, url, suggestedFilename, artifact) {
|
||||
this._page = void 0;
|
||||
this._url = void 0;
|
||||
this._suggestedFilename = void 0;
|
||||
this._artifact = void 0;
|
||||
this._page = page;
|
||||
this._url = url;
|
||||
this._suggestedFilename = suggestedFilename;
|
||||
this._artifact = artifact;
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
suggestedFilename() {
|
||||
return this._suggestedFilename;
|
||||
}
|
||||
async path() {
|
||||
return await this._artifact.pathAfterFinished();
|
||||
}
|
||||
async saveAs(path) {
|
||||
return await this._artifact.saveAs(path);
|
||||
}
|
||||
async failure() {
|
||||
return await this._artifact.failure();
|
||||
}
|
||||
async createReadStream() {
|
||||
return await this._artifact.createReadStream();
|
||||
}
|
||||
async cancel() {
|
||||
return await this._artifact.cancel();
|
||||
}
|
||||
async delete() {
|
||||
return await this._artifact.delete();
|
||||
}
|
||||
}
|
||||
exports.Download = Download;
|
||||
@@ -1,135 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.ElectronApplication = exports.Electron = void 0;
|
||||
var _timeoutSettings = require("../common/timeoutSettings");
|
||||
var _browserContext = require("./browserContext");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _clientHelper = require("./clientHelper");
|
||||
var _events = require("./events");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _consoleMessage = require("./consoleMessage");
|
||||
var _waiter = require("./waiter");
|
||||
var _errors = require("./errors");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
class Electron extends _channelOwner.ChannelOwner {
|
||||
static from(electron) {
|
||||
return electron._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
async launch(options = {}) {
|
||||
const params = {
|
||||
...(await (0, _browserContext.prepareBrowserContextParams)(options)),
|
||||
env: (0, _clientHelper.envObjectToArray)(options.env ? options.env : process.env),
|
||||
tracesDir: options.tracesDir
|
||||
};
|
||||
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);
|
||||
app._context._setOptions(params, options);
|
||||
return app;
|
||||
}
|
||||
}
|
||||
exports.Electron = Electron;
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class ElectronApplication extends _channelOwner.ChannelOwner {
|
||||
static from(electronApplication) {
|
||||
return electronApplication._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._context = void 0;
|
||||
this._windows = new Set();
|
||||
this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
|
||||
this._context = _browserContext.BrowserContext.from(initializer.context);
|
||||
for (const page of this._context._pages) this._onPage(page);
|
||||
this._context.on(_events.Events.BrowserContext.Page, page => this._onPage(page));
|
||||
this._channel.on('close', () => {
|
||||
this.emit(_events.Events.ElectronApplication.Close);
|
||||
});
|
||||
this._channel.on('console', event => this.emit(_events.Events.ElectronApplication.Console, new _consoleMessage.ConsoleMessage(event)));
|
||||
this._setEventToSubscriptionMapping(new Map([[_events.Events.ElectronApplication.Console, 'console']]));
|
||||
}
|
||||
process() {
|
||||
return this._toImpl().process();
|
||||
}
|
||||
_onPage(page) {
|
||||
this._windows.add(page);
|
||||
this.emit(_events.Events.ElectronApplication.Window, page);
|
||||
page.once(_events.Events.Page.Close, () => this._windows.delete(page));
|
||||
}
|
||||
windows() {
|
||||
// TODO: add ElectronPage class inheriting from Page.
|
||||
return [...this._windows];
|
||||
}
|
||||
async firstWindow(options) {
|
||||
if (this._windows.size) return this._windows.values().next().value;
|
||||
return await this.waitForEvent('window', options);
|
||||
}
|
||||
context() {
|
||||
return this._context;
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async close() {
|
||||
try {
|
||||
await this._context.close();
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e)) return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = _waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== _events.Events.ElectronApplication.Close) waiter.rejectOnEvent(this, _events.Events.ElectronApplication.Close, () => new _errors.TargetClosedError());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
async browserWindow(page) {
|
||||
const result = await this._channel.browserWindow({
|
||||
page: page._channel
|
||||
});
|
||||
return _jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
const result = await this._channel.evaluateExpression({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
const result = await this._channel.evaluateExpressionHandle({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return _jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
}
|
||||
exports.ElectronApplication = ElectronApplication;
|
||||
@@ -1,321 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.ElementHandle = void 0;
|
||||
exports.convertInputFiles = convertInputFiles;
|
||||
exports.convertSelectOptionValues = convertSelectOptionValues;
|
||||
exports.determineScreenshotType = determineScreenshotType;
|
||||
var _frame = require("./frame");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _utilsBundle = require("../utilsBundle");
|
||||
var _path = _interopRequireDefault(require("path"));
|
||||
var _utils = require("../utils");
|
||||
var _fileUtils = require("../utils/fileUtils");
|
||||
var _writableStream = require("./writableStream");
|
||||
var _stream = require("stream");
|
||||
var _util = require("util");
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const pipelineAsync = (0, _util.promisify)(_stream.pipeline);
|
||||
class ElementHandle extends _jsHandle.JSHandle {
|
||||
static from(handle) {
|
||||
return handle._object;
|
||||
}
|
||||
static fromNullable(handle) {
|
||||
return handle ? ElementHandle.from(handle) : null;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._elementChannel = void 0;
|
||||
this._elementChannel = this._channel;
|
||||
}
|
||||
asElement() {
|
||||
return this;
|
||||
}
|
||||
async ownerFrame() {
|
||||
return _frame.Frame.fromNullable((await this._elementChannel.ownerFrame()).frame);
|
||||
}
|
||||
async contentFrame() {
|
||||
return _frame.Frame.fromNullable((await this._elementChannel.contentFrame()).frame);
|
||||
}
|
||||
async getAttribute(name) {
|
||||
const value = (await this._elementChannel.getAttribute({
|
||||
name
|
||||
})).value;
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
async inputValue() {
|
||||
return (await this._elementChannel.inputValue()).value;
|
||||
}
|
||||
async textContent() {
|
||||
const value = (await this._elementChannel.textContent()).value;
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
async innerText() {
|
||||
return (await this._elementChannel.innerText()).value;
|
||||
}
|
||||
async innerHTML() {
|
||||
return (await this._elementChannel.innerHTML()).value;
|
||||
}
|
||||
async isChecked() {
|
||||
return (await this._elementChannel.isChecked()).value;
|
||||
}
|
||||
async isDisabled() {
|
||||
return (await this._elementChannel.isDisabled()).value;
|
||||
}
|
||||
async isEditable() {
|
||||
return (await this._elementChannel.isEditable()).value;
|
||||
}
|
||||
async isEnabled() {
|
||||
return (await this._elementChannel.isEnabled()).value;
|
||||
}
|
||||
async isHidden() {
|
||||
return (await this._elementChannel.isHidden()).value;
|
||||
}
|
||||
async isVisible() {
|
||||
return (await this._elementChannel.isVisible()).value;
|
||||
}
|
||||
async dispatchEvent(type, eventInit = {}) {
|
||||
await this._elementChannel.dispatchEvent({
|
||||
type,
|
||||
eventInit: (0, _jsHandle.serializeArgument)(eventInit)
|
||||
});
|
||||
}
|
||||
async scrollIntoViewIfNeeded(options = {}) {
|
||||
await this._elementChannel.scrollIntoViewIfNeeded(options);
|
||||
}
|
||||
async hover(options = {}) {
|
||||
await this._elementChannel.hover(options);
|
||||
}
|
||||
async click(options = {}) {
|
||||
return await this._elementChannel.click(options);
|
||||
}
|
||||
async dblclick(options = {}) {
|
||||
return await this._elementChannel.dblclick(options);
|
||||
}
|
||||
async tap(options = {}) {
|
||||
return await this._elementChannel.tap(options);
|
||||
}
|
||||
async selectOption(values, options = {}) {
|
||||
const result = await this._elementChannel.selectOption({
|
||||
...convertSelectOptionValues(values),
|
||||
...options
|
||||
});
|
||||
return result.values;
|
||||
}
|
||||
async fill(value, options = {}) {
|
||||
return await this._elementChannel.fill({
|
||||
value,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async selectText(options = {}) {
|
||||
await this._elementChannel.selectText(options);
|
||||
}
|
||||
async setInputFiles(files, options = {}) {
|
||||
const frame = await this.ownerFrame();
|
||||
if (!frame) throw new Error('Cannot set input files to detached element');
|
||||
const converted = await convertInputFiles(files, frame.page().context());
|
||||
await this._elementChannel.setInputFiles({
|
||||
...converted,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async focus() {
|
||||
await this._elementChannel.focus();
|
||||
}
|
||||
async type(text, options = {}) {
|
||||
await this._elementChannel.type({
|
||||
text,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async press(key, options = {}) {
|
||||
await this._elementChannel.press({
|
||||
key,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async check(options = {}) {
|
||||
return await this._elementChannel.check(options);
|
||||
}
|
||||
async uncheck(options = {}) {
|
||||
return await this._elementChannel.uncheck(options);
|
||||
}
|
||||
async setChecked(checked, options) {
|
||||
if (checked) await this.check(options);else await this.uncheck(options);
|
||||
}
|
||||
async boundingBox() {
|
||||
const value = (await this._elementChannel.boundingBox()).value;
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
const copy = {
|
||||
...options,
|
||||
mask: undefined
|
||||
};
|
||||
if (!copy.type) copy.type = determineScreenshotType(options);
|
||||
if (options.mask) {
|
||||
copy.mask = options.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector
|
||||
}));
|
||||
}
|
||||
const result = await this._elementChannel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await (0, _fileUtils.mkdirIfNeeded)(options.path);
|
||||
await _fs.default.promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
async $(selector) {
|
||||
return ElementHandle.fromNullable((await this._elementChannel.querySelector({
|
||||
selector
|
||||
})).element);
|
||||
}
|
||||
async $$(selector) {
|
||||
const result = await this._elementChannel.querySelectorAll({
|
||||
selector
|
||||
});
|
||||
return result.elements.map(h => ElementHandle.from(h));
|
||||
}
|
||||
async $eval(selector, pageFunction, arg) {
|
||||
const result = await this._elementChannel.evalOnSelector({
|
||||
selector,
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $$eval(selector, pageFunction, arg) {
|
||||
const result = await this._elementChannel.evalOnSelectorAll({
|
||||
selector,
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async waitForElementState(state, options = {}) {
|
||||
return await this._elementChannel.waitForElementState({
|
||||
state,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async waitForSelector(selector, options = {}) {
|
||||
const result = await this._elementChannel.waitForSelector({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
return ElementHandle.fromNullable(result.element);
|
||||
}
|
||||
}
|
||||
exports.ElementHandle = ElementHandle;
|
||||
function convertSelectOptionValues(values) {
|
||||
if (values === null) return {};
|
||||
if (!Array.isArray(values)) values = [values];
|
||||
if (!values.length) return {};
|
||||
for (let i = 0; i < values.length; i++) (0, _utils.assert)(values[i] !== null, `options[${i}]: expected object, got null`);
|
||||
if (values[0] instanceof ElementHandle) return {
|
||||
elements: values.map(v => v._elementChannel)
|
||||
};
|
||||
if ((0, _utils.isString)(values[0])) return {
|
||||
options: values.map(valueOrLabel => ({
|
||||
valueOrLabel
|
||||
}))
|
||||
};
|
||||
return {
|
||||
options: values
|
||||
};
|
||||
}
|
||||
function filePayloadExceedsSizeLimit(payloads) {
|
||||
return payloads.reduce((size, item) => size + (item.buffer ? item.buffer.byteLength : 0), 0) >= _fileUtils.fileUploadSizeLimit;
|
||||
}
|
||||
async function resolvePathsAndDirectoryForInputFiles(items) {
|
||||
var _localPaths2;
|
||||
let localPaths;
|
||||
let localDirectory;
|
||||
for (const item of items) {
|
||||
const stat = await _fs.default.promises.stat(item);
|
||||
if (stat.isDirectory()) {
|
||||
if (localDirectory) throw new Error('Multiple directories are not supported');
|
||||
localDirectory = _path.default.resolve(item);
|
||||
} else {
|
||||
var _localPaths;
|
||||
(_localPaths = localPaths) !== null && _localPaths !== void 0 ? _localPaths : localPaths = [];
|
||||
localPaths.push(_path.default.resolve(item));
|
||||
}
|
||||
}
|
||||
if ((_localPaths2 = localPaths) !== null && _localPaths2 !== void 0 && _localPaths2.length && localDirectory) throw new Error('File paths must be all files or a single directory');
|
||||
return [localPaths, localDirectory];
|
||||
}
|
||||
async function convertInputFiles(files, context) {
|
||||
const items = Array.isArray(files) ? files.slice() : [files];
|
||||
if (items.some(item => typeof item === 'string')) {
|
||||
if (!items.every(item => typeof item === 'string')) throw new Error('File paths cannot be mixed with buffers');
|
||||
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(items);
|
||||
if (context._connection.isRemote()) {
|
||||
const files = localDirectory ? (await _fs.default.promises.readdir(localDirectory, {
|
||||
withFileTypes: true,
|
||||
recursive: true
|
||||
})).filter(f => f.isFile()).map(f => _path.default.join(f.path, f.name)) : localPaths;
|
||||
const {
|
||||
writableStreams,
|
||||
rootDir
|
||||
} = await context._wrapApiCall(async () => context._channel.createTempFiles({
|
||||
rootDirName: localDirectory ? _path.default.basename(localDirectory) : undefined,
|
||||
items: await Promise.all(files.map(async file => {
|
||||
const lastModifiedMs = (await _fs.default.promises.stat(file)).mtimeMs;
|
||||
return {
|
||||
name: localDirectory ? _path.default.relative(localDirectory, file) : _path.default.basename(file),
|
||||
lastModifiedMs
|
||||
};
|
||||
}))
|
||||
}), true);
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const writable = _writableStream.WritableStream.from(writableStreams[i]);
|
||||
await pipelineAsync(_fs.default.createReadStream(files[i]), writable.stream());
|
||||
}
|
||||
return {
|
||||
directoryStream: rootDir,
|
||||
streams: localDirectory ? undefined : writableStreams
|
||||
};
|
||||
}
|
||||
return {
|
||||
localPaths,
|
||||
localDirectory
|
||||
};
|
||||
}
|
||||
const payloads = items;
|
||||
if (filePayloadExceedsSizeLimit(payloads)) throw new Error('Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.');
|
||||
return {
|
||||
payloads
|
||||
};
|
||||
}
|
||||
function determineScreenshotType(options) {
|
||||
if (options.path) {
|
||||
const mimeType = _utilsBundle.mime.getType(options.path);
|
||||
if (mimeType === 'image/png') return 'png';else if (mimeType === 'image/jpeg') return 'jpeg';
|
||||
throw new Error(`path: unsupported mime type "${mimeType}"`);
|
||||
}
|
||||
return options.type;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.TimeoutError = exports.TargetClosedError = void 0;
|
||||
exports.isTargetClosedError = isTargetClosedError;
|
||||
exports.parseError = parseError;
|
||||
exports.serializeError = serializeError;
|
||||
var _utils = require("../utils");
|
||||
var _serializers = require("../protocol/serializers");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class TimeoutError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'TimeoutError';
|
||||
}
|
||||
}
|
||||
exports.TimeoutError = TimeoutError;
|
||||
class TargetClosedError extends Error {
|
||||
constructor(cause) {
|
||||
super(cause || 'Target page, context or browser has been closed');
|
||||
}
|
||||
}
|
||||
exports.TargetClosedError = TargetClosedError;
|
||||
function isTargetClosedError(error) {
|
||||
return error instanceof TargetClosedError;
|
||||
}
|
||||
function serializeError(e) {
|
||||
if ((0, _utils.isError)(e)) return {
|
||||
error: {
|
||||
message: e.message,
|
||||
stack: e.stack,
|
||||
name: e.name
|
||||
}
|
||||
};
|
||||
return {
|
||||
value: (0, _serializers.serializeValue)(e, value => ({
|
||||
fallThrough: value
|
||||
}))
|
||||
};
|
||||
}
|
||||
function parseError(error) {
|
||||
if (!error.error) {
|
||||
if (error.value === undefined) throw new Error('Serialized error must have either an error or a value');
|
||||
return (0, _serializers.parseSerializedValue)(error.value, undefined);
|
||||
}
|
||||
if (error.error.name === 'TimeoutError') {
|
||||
const e = new TimeoutError(error.error.message);
|
||||
e.stack = error.error.stack || '';
|
||||
return e;
|
||||
}
|
||||
if (error.error.name === 'TargetClosedError') {
|
||||
const e = new TargetClosedError(error.error.message);
|
||||
e.stack = error.error.stack || '';
|
||||
return e;
|
||||
}
|
||||
const e = new Error(error.error.message);
|
||||
e.stack = error.error.stack || '';
|
||||
e.name = error.error.name;
|
||||
return e;
|
||||
}
|
||||
@@ -1,314 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.EventEmitter = void 0;
|
||||
var _events = require("events");
|
||||
var _utils = require("../utils");
|
||||
/**
|
||||
* Copyright Joyent, Inc. and other Node contributors.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a
|
||||
* copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
* persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
* USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this._events = undefined;
|
||||
this._eventsCount = 0;
|
||||
this._maxListeners = undefined;
|
||||
this._pendingHandlers = new Map();
|
||||
this._rejectionHandler = void 0;
|
||||
if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
|
||||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
}
|
||||
this._maxListeners = this._maxListeners || undefined;
|
||||
this.on = this.addListener;
|
||||
this.off = this.removeListener;
|
||||
}
|
||||
setMaxListeners(n) {
|
||||
if (typeof n !== 'number' || n < 0 || Number.isNaN(n)) throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
||||
this._maxListeners = n;
|
||||
return this;
|
||||
}
|
||||
getMaxListeners() {
|
||||
return this._maxListeners === undefined ? _events.EventEmitter.defaultMaxListeners : this._maxListeners;
|
||||
}
|
||||
emit(type, ...args) {
|
||||
const events = this._events;
|
||||
if (events === undefined) return false;
|
||||
const handler = events === null || events === void 0 ? void 0 : events[type];
|
||||
if (handler === undefined) return false;
|
||||
if (typeof handler === 'function') {
|
||||
this._callHandler(type, handler, args);
|
||||
} else {
|
||||
const len = handler.length;
|
||||
const listeners = handler.slice();
|
||||
for (let i = 0; i < len; ++i) this._callHandler(type, listeners[i], args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
_callHandler(type, handler, args) {
|
||||
const promise = Reflect.apply(handler, this, args);
|
||||
if (!(promise instanceof Promise)) return;
|
||||
let set = this._pendingHandlers.get(type);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this._pendingHandlers.set(type, set);
|
||||
}
|
||||
set.add(promise);
|
||||
promise.catch(e => {
|
||||
if (this._rejectionHandler) this._rejectionHandler(e);else throw e;
|
||||
}).finally(() => set.delete(promise));
|
||||
}
|
||||
addListener(type, listener) {
|
||||
return this._addListener(type, listener, false);
|
||||
}
|
||||
on(type, listener) {
|
||||
return this._addListener(type, listener, false);
|
||||
}
|
||||
_addListener(type, listener, prepend) {
|
||||
checkListener(listener);
|
||||
let events = this._events;
|
||||
let existing;
|
||||
if (events === undefined) {
|
||||
events = this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
} else {
|
||||
// To avoid recursion in the case that type === "newListener"! Before
|
||||
// adding it to the listeners, first emit "newListener".
|
||||
if (events.newListener !== undefined) {
|
||||
this.emit('newListener', type, unwrapListener(listener));
|
||||
|
||||
// Re-assign `events` because a newListener handler could have caused the
|
||||
// this._events to be assigned to a new object
|
||||
events = this._events;
|
||||
}
|
||||
existing = events[type];
|
||||
}
|
||||
if (existing === undefined) {
|
||||
// Optimize the case of one listener. Don't need the extra array object.
|
||||
existing = events[type] = listener;
|
||||
++this._eventsCount;
|
||||
} else {
|
||||
if (typeof existing === 'function') {
|
||||
// Adding the second element, need to change to array.
|
||||
existing = events[type] = prepend ? [listener, existing] : [existing, listener];
|
||||
// If we've already got an array, just append.
|
||||
} else if (prepend) {
|
||||
existing.unshift(listener);
|
||||
} else {
|
||||
existing.push(listener);
|
||||
}
|
||||
|
||||
// Check for listener leak
|
||||
const m = this.getMaxListeners();
|
||||
if (m > 0 && existing.length > m && !existing.warned) {
|
||||
existing.warned = true;
|
||||
// No error code for this since it is a Warning
|
||||
const w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + String(type) + ' listeners ' + 'added. Use emitter.setMaxListeners() to ' + 'increase limit');
|
||||
w.name = 'MaxListenersExceededWarning';
|
||||
w.emitter = this;
|
||||
w.type = type;
|
||||
w.count = existing.length;
|
||||
if (!(0, _utils.isUnderTest)()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
prependListener(type, listener) {
|
||||
return this._addListener(type, listener, true);
|
||||
}
|
||||
once(type, listener) {
|
||||
checkListener(listener);
|
||||
this.on(type, new OnceWrapper(this, type, listener).wrapperFunction);
|
||||
return this;
|
||||
}
|
||||
prependOnceListener(type, listener) {
|
||||
checkListener(listener);
|
||||
this.prependListener(type, new OnceWrapper(this, type, listener).wrapperFunction);
|
||||
return this;
|
||||
}
|
||||
removeListener(type, listener) {
|
||||
checkListener(listener);
|
||||
const events = this._events;
|
||||
if (events === undefined) return this;
|
||||
const list = events[type];
|
||||
if (list === undefined) return this;
|
||||
if (list === listener || list.listener === listener) {
|
||||
if (--this._eventsCount === 0) {
|
||||
this._events = Object.create(null);
|
||||
} else {
|
||||
var _listener;
|
||||
delete events[type];
|
||||
if (events.removeListener) this.emit('removeListener', type, (_listener = list.listener) !== null && _listener !== void 0 ? _listener : listener);
|
||||
}
|
||||
} else if (typeof list !== 'function') {
|
||||
let position = -1;
|
||||
let originalListener;
|
||||
for (let i = list.length - 1; i >= 0; i--) {
|
||||
if (list[i] === listener || wrappedListener(list[i]) === listener) {
|
||||
originalListener = wrappedListener(list[i]);
|
||||
position = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (position < 0) return this;
|
||||
if (position === 0) list.shift();else list.splice(position, 1);
|
||||
if (list.length === 1) events[type] = list[0];
|
||||
if (events.removeListener !== undefined) this.emit('removeListener', type, originalListener || listener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
off(type, listener) {
|
||||
return this.removeListener(type, listener);
|
||||
}
|
||||
removeAllListeners(type, options) {
|
||||
this._removeAllListeners(type);
|
||||
if (!options) return this;
|
||||
if (options.behavior === 'wait') {
|
||||
const errors = [];
|
||||
this._rejectionHandler = error => errors.push(error);
|
||||
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
|
||||
return this._waitFor(type).then(() => {
|
||||
if (errors.length) throw errors[0];
|
||||
});
|
||||
}
|
||||
if (options.behavior === 'ignoreErrors') this._rejectionHandler = () => {};
|
||||
|
||||
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
|
||||
return Promise.resolve();
|
||||
}
|
||||
_removeAllListeners(type) {
|
||||
const events = this._events;
|
||||
if (!events) return;
|
||||
|
||||
// not listening for removeListener, no need to emit
|
||||
if (!events.removeListener) {
|
||||
if (type === undefined) {
|
||||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
} else if (events[type] !== undefined) {
|
||||
if (--this._eventsCount === 0) this._events = Object.create(null);else delete events[type];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// emit removeListener for all listeners on all events
|
||||
if (type === undefined) {
|
||||
const keys = Object.keys(events);
|
||||
let key;
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
key = keys[i];
|
||||
if (key === 'removeListener') continue;
|
||||
this._removeAllListeners(key);
|
||||
}
|
||||
this._removeAllListeners('removeListener');
|
||||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
return;
|
||||
}
|
||||
const listeners = events[type];
|
||||
if (typeof listeners === 'function') {
|
||||
this.removeListener(type, listeners);
|
||||
} else if (listeners !== undefined) {
|
||||
// LIFO order
|
||||
for (let i = listeners.length - 1; i >= 0; i--) this.removeListener(type, listeners[i]);
|
||||
}
|
||||
}
|
||||
listeners(type) {
|
||||
return this._listeners(this, type, true);
|
||||
}
|
||||
rawListeners(type) {
|
||||
return this._listeners(this, type, false);
|
||||
}
|
||||
listenerCount(type) {
|
||||
const events = this._events;
|
||||
if (events !== undefined) {
|
||||
const listener = events[type];
|
||||
if (typeof listener === 'function') return 1;
|
||||
if (listener !== undefined) return listener.length;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
eventNames() {
|
||||
return this._eventsCount > 0 && this._events ? Reflect.ownKeys(this._events) : [];
|
||||
}
|
||||
async _waitFor(type) {
|
||||
let promises = [];
|
||||
if (type) {
|
||||
promises = [...(this._pendingHandlers.get(type) || [])];
|
||||
} else {
|
||||
promises = [];
|
||||
for (const [, pending] of this._pendingHandlers) promises.push(...pending);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
_listeners(target, type, unwrap) {
|
||||
const events = target._events;
|
||||
if (events === undefined) return [];
|
||||
const listener = events[type];
|
||||
if (listener === undefined) return [];
|
||||
if (typeof listener === 'function') return unwrap ? [unwrapListener(listener)] : [listener];
|
||||
return unwrap ? unwrapListeners(listener) : listener.slice();
|
||||
}
|
||||
}
|
||||
exports.EventEmitter = EventEmitter;
|
||||
function checkListener(listener) {
|
||||
if (typeof listener !== 'function') throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
||||
}
|
||||
class OnceWrapper {
|
||||
constructor(eventEmitter, eventType, listener) {
|
||||
this._fired = false;
|
||||
this.wrapperFunction = void 0;
|
||||
this._listener = void 0;
|
||||
this._eventEmitter = void 0;
|
||||
this._eventType = void 0;
|
||||
this._eventEmitter = eventEmitter;
|
||||
this._eventType = eventType;
|
||||
this._listener = listener;
|
||||
this.wrapperFunction = this._handle.bind(this);
|
||||
this.wrapperFunction.listener = listener;
|
||||
}
|
||||
_handle(...args) {
|
||||
if (this._fired) return;
|
||||
this._fired = true;
|
||||
this._eventEmitter.removeListener(this._eventType, this.wrapperFunction);
|
||||
return this._listener.apply(this._eventEmitter, args);
|
||||
}
|
||||
}
|
||||
function unwrapListener(l) {
|
||||
var _wrappedListener;
|
||||
return (_wrappedListener = wrappedListener(l)) !== null && _wrappedListener !== void 0 ? _wrappedListener : l;
|
||||
}
|
||||
function unwrapListeners(arr) {
|
||||
return arr.map(l => {
|
||||
var _wrappedListener2;
|
||||
return (_wrappedListener2 = wrappedListener(l)) !== null && _wrappedListener2 !== void 0 ? _wrappedListener2 : l;
|
||||
});
|
||||
}
|
||||
function wrappedListener(l) {
|
||||
return l.listener;
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Events = void 0;
|
||||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const Events = exports.Events = {
|
||||
AndroidDevice: {
|
||||
WebView: 'webview',
|
||||
Close: 'close'
|
||||
},
|
||||
AndroidSocket: {
|
||||
Data: 'data',
|
||||
Close: 'close'
|
||||
},
|
||||
AndroidWebView: {
|
||||
Close: 'close'
|
||||
},
|
||||
Browser: {
|
||||
Disconnected: 'disconnected'
|
||||
},
|
||||
BrowserContext: {
|
||||
Console: 'console',
|
||||
Close: 'close',
|
||||
Dialog: 'dialog',
|
||||
Page: 'page',
|
||||
// Can't use just 'error' due to node.js special treatment of error events.
|
||||
// @see https://nodejs.org/api/events.html#events_error_events
|
||||
WebError: 'weberror',
|
||||
BackgroundPage: 'backgroundpage',
|
||||
ServiceWorker: 'serviceworker',
|
||||
Request: 'request',
|
||||
Response: 'response',
|
||||
RequestFailed: 'requestfailed',
|
||||
RequestFinished: 'requestfinished'
|
||||
},
|
||||
BrowserServer: {
|
||||
Close: 'close'
|
||||
},
|
||||
Page: {
|
||||
Close: 'close',
|
||||
Crash: 'crash',
|
||||
Console: 'console',
|
||||
Dialog: 'dialog',
|
||||
Download: 'download',
|
||||
FileChooser: 'filechooser',
|
||||
DOMContentLoaded: 'domcontentloaded',
|
||||
// Can't use just 'error' due to node.js special treatment of error events.
|
||||
// @see https://nodejs.org/api/events.html#events_error_events
|
||||
PageError: 'pageerror',
|
||||
Request: 'request',
|
||||
Response: 'response',
|
||||
RequestFailed: 'requestfailed',
|
||||
RequestFinished: 'requestfinished',
|
||||
FrameAttached: 'frameattached',
|
||||
FrameDetached: 'framedetached',
|
||||
FrameNavigated: 'framenavigated',
|
||||
Load: 'load',
|
||||
Popup: 'popup',
|
||||
WebSocket: 'websocket',
|
||||
Worker: 'worker'
|
||||
},
|
||||
WebSocket: {
|
||||
Close: 'close',
|
||||
Error: 'socketerror',
|
||||
FrameReceived: 'framereceived',
|
||||
FrameSent: 'framesent'
|
||||
},
|
||||
Worker: {
|
||||
Close: 'close'
|
||||
},
|
||||
ElectronApplication: {
|
||||
Close: 'close',
|
||||
Console: 'console',
|
||||
Window: 'window'
|
||||
}
|
||||
};
|
||||
@@ -1,391 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.APIResponse = exports.APIRequestContext = exports.APIRequest = void 0;
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _path = _interopRequireDefault(require("path"));
|
||||
var util = _interopRequireWildcard(require("util"));
|
||||
var _utils = require("../utils");
|
||||
var _fileUtils = require("../utils/fileUtils");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _network = require("./network");
|
||||
var _tracing = require("./tracing");
|
||||
var _errors = require("./errors");
|
||||
var _browserContext = require("./browserContext");
|
||||
let _Symbol$asyncDispose, _Symbol$asyncDispose2, _util$inspect$custom;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
class APIRequest {
|
||||
constructor(playwright) {
|
||||
this._playwright = void 0;
|
||||
this._contexts = new Set();
|
||||
// Instrumentation.
|
||||
this._defaultContextOptions = void 0;
|
||||
this._playwright = playwright;
|
||||
}
|
||||
async newContext(options = {}) {
|
||||
var _this$_defaultContext;
|
||||
options = {
|
||||
...this._defaultContextOptions,
|
||||
...options
|
||||
};
|
||||
const storageState = typeof options.storageState === 'string' ? JSON.parse(await _fs.default.promises.readFile(options.storageState, 'utf8')) : options.storageState;
|
||||
// We do not expose tracesDir in the API, so do not allow options to accidentally override it.
|
||||
const tracesDir = (_this$_defaultContext = this._defaultContextOptions) === null || _this$_defaultContext === void 0 ? void 0 : _this$_defaultContext.tracesDir;
|
||||
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? (0, _utils.headersObjectToArray)(options.extraHTTPHeaders) : undefined,
|
||||
storageState,
|
||||
tracesDir,
|
||||
clientCertificates: await (0, _browserContext.toClientCertificatesProtocol)(options.clientCertificates)
|
||||
})).request);
|
||||
this._contexts.add(context);
|
||||
context._request = this;
|
||||
context._tracing._tracesDir = tracesDir;
|
||||
await context._instrumentation.runAfterCreateRequestContext(context);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
exports.APIRequest = APIRequest;
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class APIRequestContext extends _channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._request = void 0;
|
||||
this._tracing = void 0;
|
||||
this._closeReason = void 0;
|
||||
this._tracing = _tracing.Tracing.from(initializer.tracing);
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose(options = {}) {
|
||||
var _this$_request;
|
||||
this._closeReason = options.reason;
|
||||
await this._instrumentation.runBeforeCloseRequestContext(this);
|
||||
try {
|
||||
await this._channel.dispose(options);
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e)) return;
|
||||
throw e;
|
||||
}
|
||||
this._tracing._resetStackCounter();
|
||||
(_this$_request = this._request) === null || _this$_request === void 0 || _this$_request._contexts.delete(this);
|
||||
}
|
||||
async delete(url, options) {
|
||||
return await this.fetch(url, {
|
||||
...options,
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
async head(url, options) {
|
||||
return await this.fetch(url, {
|
||||
...options,
|
||||
method: 'HEAD'
|
||||
});
|
||||
}
|
||||
async get(url, options) {
|
||||
return await this.fetch(url, {
|
||||
...options,
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
async patch(url, options) {
|
||||
return await this.fetch(url, {
|
||||
...options,
|
||||
method: 'PATCH'
|
||||
});
|
||||
}
|
||||
async post(url, options) {
|
||||
return await this.fetch(url, {
|
||||
...options,
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
async put(url, options) {
|
||||
return await this.fetch(url, {
|
||||
...options,
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
async fetch(urlOrRequest, options = {}) {
|
||||
const url = (0, _utils.isString)(urlOrRequest) ? urlOrRequest : undefined;
|
||||
const request = (0, _utils.isString)(urlOrRequest) ? undefined : urlOrRequest;
|
||||
return await this._innerFetch({
|
||||
url,
|
||||
request,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async _innerFetch(options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
var _options$request, _options$request2, _options$request3;
|
||||
if (this._closeReason) throw new _errors.TargetClosedError(this._closeReason);
|
||||
(0, _utils.assert)(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
|
||||
(0, _utils.assert)((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||
(0, _utils.assert)(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
|
||||
(0, _utils.assert)(options.maxRetries === undefined || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`);
|
||||
const url = options.url !== undefined ? options.url : options.request.url();
|
||||
const method = options.method || ((_options$request = options.request) === null || _options$request === void 0 ? void 0 : _options$request.method());
|
||||
let encodedParams = undefined;
|
||||
if (typeof options.params === 'string') encodedParams = options.params;else if (options.params instanceof URLSearchParams) encodedParams = options.params.toString();
|
||||
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
||||
const headersObj = options.headers || ((_options$request2 = options.request) === null || _options$request2 === void 0 ? void 0 : _options$request2.headers());
|
||||
const headers = headersObj ? (0, _utils.headersObjectToArray)(headersObj) : undefined;
|
||||
let jsonData;
|
||||
let formData;
|
||||
let multipartData;
|
||||
let postDataBuffer;
|
||||
if (options.data !== undefined) {
|
||||
if ((0, _utils.isString)(options.data)) {
|
||||
if (isJsonContentType(headers)) jsonData = isJsonParsable(options.data) ? options.data : JSON.stringify(options.data);else postDataBuffer = Buffer.from(options.data, 'utf8');
|
||||
} else if (Buffer.isBuffer(options.data)) {
|
||||
postDataBuffer = options.data;
|
||||
} else if (typeof options.data === 'object' || typeof options.data === 'number' || typeof options.data === 'boolean') {
|
||||
jsonData = JSON.stringify(options.data);
|
||||
} else {
|
||||
throw new Error(`Unexpected 'data' type`);
|
||||
}
|
||||
} else if (options.form) {
|
||||
if (globalThis.FormData && options.form instanceof FormData) {
|
||||
formData = [];
|
||||
for (const [name, value] of options.form.entries()) {
|
||||
if (typeof value !== 'string') throw new Error(`Expected string for options.form["${name}"], found File. Please use options.multipart instead.`);
|
||||
formData.push({
|
||||
name,
|
||||
value
|
||||
});
|
||||
}
|
||||
} else {
|
||||
formData = objectToArray(options.form);
|
||||
}
|
||||
} else if (options.multipart) {
|
||||
multipartData = [];
|
||||
if (globalThis.FormData && options.multipart instanceof FormData) {
|
||||
const form = options.multipart;
|
||||
for (const [name, value] of form.entries()) {
|
||||
if ((0, _utils.isString)(value)) {
|
||||
multipartData.push({
|
||||
name,
|
||||
value
|
||||
});
|
||||
} else {
|
||||
const file = {
|
||||
name: value.name,
|
||||
mimeType: value.type,
|
||||
buffer: Buffer.from(await value.arrayBuffer())
|
||||
};
|
||||
multipartData.push({
|
||||
name,
|
||||
file
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart)) multipartData.push(await toFormField(name, value));
|
||||
}
|
||||
}
|
||||
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined) postDataBuffer = ((_options$request3 = options.request) === null || _options$request3 === void 0 ? void 0 : _options$request3.postDataBuffer()) || undefined;
|
||||
const fixtures = {
|
||||
__testHookLookup: options.__testHookLookup
|
||||
};
|
||||
const result = await this._channel.fetch({
|
||||
url,
|
||||
params: typeof options.params === 'object' ? objectToArray(options.params) : undefined,
|
||||
encodedParams,
|
||||
method,
|
||||
headers,
|
||||
postData: postDataBuffer,
|
||||
jsonData,
|
||||
formData,
|
||||
multipartData,
|
||||
timeout: options.timeout,
|
||||
failOnStatusCode: options.failOnStatusCode,
|
||||
ignoreHTTPSErrors: options.ignoreHTTPSErrors,
|
||||
maxRedirects: options.maxRedirects,
|
||||
maxRetries: options.maxRetries,
|
||||
...fixtures
|
||||
});
|
||||
return new APIResponse(this, result.response);
|
||||
});
|
||||
}
|
||||
async storageState(options = {}) {
|
||||
const state = await this._channel.storageState();
|
||||
if (options.path) {
|
||||
await (0, _fileUtils.mkdirIfNeeded)(options.path);
|
||||
await _fs.default.promises.writeFile(options.path, JSON.stringify(state, undefined, 2), 'utf8');
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
exports.APIRequestContext = APIRequestContext;
|
||||
async function toFormField(name, value) {
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value;
|
||||
if (!Buffer.isBuffer(payload.buffer)) throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
return {
|
||||
name,
|
||||
file: filePayloadToJson(payload)
|
||||
};
|
||||
} else if (value instanceof _fs.default.ReadStream) {
|
||||
return {
|
||||
name,
|
||||
file: await readStreamToJson(value)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name,
|
||||
value: String(value)
|
||||
};
|
||||
}
|
||||
}
|
||||
function isJsonParsable(value) {
|
||||
if (typeof value !== 'string') return false;
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) return false;else throw e;
|
||||
}
|
||||
}
|
||||
_Symbol$asyncDispose2 = Symbol.asyncDispose;
|
||||
_util$inspect$custom = util.inspect.custom;
|
||||
class APIResponse {
|
||||
constructor(context, initializer) {
|
||||
this._initializer = void 0;
|
||||
this._headers = void 0;
|
||||
this._request = void 0;
|
||||
this._request = context;
|
||||
this._initializer = initializer;
|
||||
this._headers = new _network.RawHeaders(this._initializer.headers);
|
||||
}
|
||||
ok() {
|
||||
return this._initializer.status >= 200 && this._initializer.status <= 299;
|
||||
}
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
status() {
|
||||
return this._initializer.status;
|
||||
}
|
||||
statusText() {
|
||||
return this._initializer.statusText;
|
||||
}
|
||||
headers() {
|
||||
return this._headers.headers();
|
||||
}
|
||||
headersArray() {
|
||||
return this._headers.headersArray();
|
||||
}
|
||||
async body() {
|
||||
try {
|
||||
const result = await this._request._channel.fetchResponseBody({
|
||||
fetchUid: this._fetchUid()
|
||||
});
|
||||
if (result.binary === undefined) throw new Error('Response has been disposed');
|
||||
return result.binary;
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e)) throw new Error('Response has been disposed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async text() {
|
||||
const content = await this.body();
|
||||
return content.toString('utf8');
|
||||
}
|
||||
async json() {
|
||||
const content = await this.text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
async [_Symbol$asyncDispose2]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose() {
|
||||
await this._request._channel.disposeAPIResponse({
|
||||
fetchUid: this._fetchUid()
|
||||
});
|
||||
}
|
||||
[_util$inspect$custom]() {
|
||||
const headers = this.headersArray().map(({
|
||||
name,
|
||||
value
|
||||
}) => ` ${name}: ${value}`);
|
||||
return `APIResponse: ${this.status()} ${this.statusText()}\n${headers.join('\n')}`;
|
||||
}
|
||||
_fetchUid() {
|
||||
return this._initializer.fetchUid;
|
||||
}
|
||||
async _fetchLog() {
|
||||
const {
|
||||
log
|
||||
} = await this._request._channel.fetchLog({
|
||||
fetchUid: this._fetchUid()
|
||||
});
|
||||
return log;
|
||||
}
|
||||
}
|
||||
exports.APIResponse = APIResponse;
|
||||
function filePayloadToJson(payload) {
|
||||
return {
|
||||
name: payload.name,
|
||||
mimeType: payload.mimeType,
|
||||
buffer: payload.buffer
|
||||
};
|
||||
}
|
||||
async function readStreamToJson(stream) {
|
||||
const buffer = await new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
stream.on('data', chunk => chunks.push(chunk));
|
||||
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
stream.on('error', err => reject(err));
|
||||
});
|
||||
const streamPath = Buffer.isBuffer(stream.path) ? stream.path.toString('utf8') : stream.path;
|
||||
return {
|
||||
name: _path.default.basename(streamPath),
|
||||
buffer
|
||||
};
|
||||
}
|
||||
function isJsonContentType(headers) {
|
||||
if (!headers) return false;
|
||||
for (const {
|
||||
name,
|
||||
value
|
||||
} of headers) {
|
||||
if (name.toLocaleLowerCase() === 'content-type') return value === 'application/json';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function objectToArray(map) {
|
||||
if (!map) return undefined;
|
||||
const result = [];
|
||||
for (const [name, value] of Object.entries(map)) result.push({
|
||||
name,
|
||||
value: String(value)
|
||||
});
|
||||
return result;
|
||||
}
|
||||
function isFilePayload(value) {
|
||||
return typeof value === 'object' && value['name'] && value['mimeType'] && value['buffer'];
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.FileChooser = void 0;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class FileChooser {
|
||||
constructor(page, elementHandle, isMultiple) {
|
||||
this._page = void 0;
|
||||
this._elementHandle = void 0;
|
||||
this._isMultiple = void 0;
|
||||
this._page = page;
|
||||
this._elementHandle = elementHandle;
|
||||
this._isMultiple = isMultiple;
|
||||
}
|
||||
element() {
|
||||
return this._elementHandle;
|
||||
}
|
||||
isMultiple() {
|
||||
return this._isMultiple;
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
async setFiles(files, options) {
|
||||
return await this._elementHandle.setInputFiles(files, options);
|
||||
}
|
||||
}
|
||||
exports.FileChooser = FileChooser;
|
||||
@@ -1,504 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Frame = void 0;
|
||||
exports.verifyLoadState = verifyLoadState;
|
||||
var _utils = require("../utils");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _locator = require("./locator");
|
||||
var _locatorUtils = require("../utils/isomorphic/locatorUtils");
|
||||
var _elementHandle = require("./elementHandle");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var network = _interopRequireWildcard(require("./network"));
|
||||
var _events = require("events");
|
||||
var _waiter = require("./waiter");
|
||||
var _events2 = require("./events");
|
||||
var _types = require("./types");
|
||||
var _clientHelper = require("./clientHelper");
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Frame extends _channelOwner.ChannelOwner {
|
||||
static from(frame) {
|
||||
return frame._object;
|
||||
}
|
||||
static fromNullable(frame) {
|
||||
return frame ? Frame.from(frame) : null;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._eventEmitter = void 0;
|
||||
this._loadStates = void 0;
|
||||
this._parentFrame = null;
|
||||
this._url = '';
|
||||
this._name = '';
|
||||
this._detached = false;
|
||||
this._childFrames = new Set();
|
||||
this._page = void 0;
|
||||
this._eventEmitter = new _events.EventEmitter();
|
||||
this._eventEmitter.setMaxListeners(0);
|
||||
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
|
||||
if (this._parentFrame) this._parentFrame._childFrames.add(this);
|
||||
this._name = initializer.name;
|
||||
this._url = initializer.url;
|
||||
this._loadStates = new Set(initializer.loadStates);
|
||||
this._channel.on('loadstate', event => {
|
||||
if (event.add) {
|
||||
this._loadStates.add(event.add);
|
||||
this._eventEmitter.emit('loadstate', event.add);
|
||||
}
|
||||
if (event.remove) this._loadStates.delete(event.remove);
|
||||
if (!this._parentFrame && event.add === 'load' && this._page) this._page.emit(_events2.Events.Page.Load, this._page);
|
||||
if (!this._parentFrame && event.add === 'domcontentloaded' && this._page) this._page.emit(_events2.Events.Page.DOMContentLoaded, this._page);
|
||||
});
|
||||
this._channel.on('navigated', event => {
|
||||
this._url = event.url;
|
||||
this._name = event.name;
|
||||
this._eventEmitter.emit('navigated', event);
|
||||
if (!event.error && this._page) this._page.emit(_events2.Events.Page.FrameNavigated, this);
|
||||
});
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
async goto(url, options = {}) {
|
||||
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
return network.Response.fromNullable((await this._channel.goto({
|
||||
url,
|
||||
...options,
|
||||
waitUntil
|
||||
})).response);
|
||||
}
|
||||
_setupNavigationWaiter(options) {
|
||||
const waiter = new _waiter.Waiter(this._page, '');
|
||||
if (this._page.isClosed()) waiter.rejectImmediately(this._page._closeErrorWithReason());
|
||||
waiter.rejectOnEvent(this._page, _events2.Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||
waiter.rejectOnEvent(this._page, _events2.Events.Page.Crash, new Error('Navigation failed because page crashed!'));
|
||||
waiter.rejectOnEvent(this._page, _events2.Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
|
||||
const timeout = this._page._timeoutSettings.navigationTimeout(options);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded.`);
|
||||
return waiter;
|
||||
}
|
||||
async waitForNavigation(options = {}) {
|
||||
return await this._page._wrapApiCall(async () => {
|
||||
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
const waiter = this._setupNavigationWaiter(options);
|
||||
const toUrl = typeof options.url === 'string' ? ` to "${options.url}"` : '';
|
||||
waiter.log(`waiting for navigation${toUrl} until "${waitUntil}"`);
|
||||
const navigatedEvent = await waiter.waitForEvent(this._eventEmitter, 'navigated', event => {
|
||||
var _this$_page;
|
||||
// Any failed navigation results in a rejection.
|
||||
if (event.error) return true;
|
||||
waiter.log(` navigated to "${event.url}"`);
|
||||
return (0, _utils.urlMatches)((_this$_page = this._page) === null || _this$_page === void 0 ? void 0 : _this$_page.context()._options.baseURL, event.url, options.url);
|
||||
});
|
||||
if (navigatedEvent.error) {
|
||||
const e = new Error(navigatedEvent.error);
|
||||
e.stack = '';
|
||||
await waiter.waitForPromise(Promise.reject(e));
|
||||
}
|
||||
if (!this._loadStates.has(waitUntil)) {
|
||||
await waiter.waitForEvent(this._eventEmitter, 'loadstate', s => {
|
||||
waiter.log(` "${s}" event fired`);
|
||||
return s === waitUntil;
|
||||
});
|
||||
}
|
||||
const request = navigatedEvent.newDocument ? network.Request.fromNullable(navigatedEvent.newDocument.request) : null;
|
||||
const response = request ? await waiter.waitForPromise(request._finalRequest()._internalResponse()) : null;
|
||||
waiter.dispose();
|
||||
return response;
|
||||
});
|
||||
}
|
||||
async waitForLoadState(state = 'load', options = {}) {
|
||||
state = verifyLoadState('state', state);
|
||||
return await this._page._wrapApiCall(async () => {
|
||||
const waiter = this._setupNavigationWaiter(options);
|
||||
if (this._loadStates.has(state)) {
|
||||
waiter.log(` not waiting, "${state}" event already fired`);
|
||||
} else {
|
||||
await waiter.waitForEvent(this._eventEmitter, 'loadstate', s => {
|
||||
waiter.log(` "${s}" event fired`);
|
||||
return s === state;
|
||||
});
|
||||
}
|
||||
waiter.dispose();
|
||||
});
|
||||
}
|
||||
async waitForURL(url, options = {}) {
|
||||
var _this$_page2;
|
||||
if ((0, _utils.urlMatches)((_this$_page2 = this._page) === null || _this$_page2 === void 0 ? void 0 : _this$_page2.context()._options.baseURL, this.url(), url)) return await this.waitForLoadState(options.waitUntil, options);
|
||||
await this.waitForNavigation({
|
||||
url,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async frameElement() {
|
||||
return _elementHandle.ElementHandle.from((await this._channel.frameElement()).element);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpressionHandle({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return _jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpression({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async _evaluateExposeUtilityScript(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpression({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $(selector, options) {
|
||||
const result = await this._channel.querySelector({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
return _elementHandle.ElementHandle.fromNullable(result.element);
|
||||
}
|
||||
async waitForSelector(selector, options = {}) {
|
||||
if (options.visibility) throw new Error('options.visibility is not supported, did you mean options.state?');
|
||||
if (options.waitFor && options.waitFor !== 'visible') throw new Error('options.waitFor is not supported, did you mean options.state?');
|
||||
const result = await this._channel.waitForSelector({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
return _elementHandle.ElementHandle.fromNullable(result.element);
|
||||
}
|
||||
async dispatchEvent(selector, type, eventInit, options = {}) {
|
||||
await this._channel.dispatchEvent({
|
||||
selector,
|
||||
type,
|
||||
eventInit: (0, _jsHandle.serializeArgument)(eventInit),
|
||||
...options
|
||||
});
|
||||
}
|
||||
async $eval(selector, pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
const result = await this._channel.evalOnSelector({
|
||||
selector,
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $$eval(selector, pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
const result = await this._channel.evalOnSelectorAll({
|
||||
selector,
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $$(selector) {
|
||||
const result = await this._channel.querySelectorAll({
|
||||
selector
|
||||
});
|
||||
return result.elements.map(e => _elementHandle.ElementHandle.from(e));
|
||||
}
|
||||
async _queryCount(selector) {
|
||||
return (await this._channel.queryCount({
|
||||
selector
|
||||
})).value;
|
||||
}
|
||||
async content() {
|
||||
return (await this._channel.content()).value;
|
||||
}
|
||||
async setContent(html, options = {}) {
|
||||
const waitUntil = verifyLoadState('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
await this._channel.setContent({
|
||||
html,
|
||||
...options,
|
||||
waitUntil
|
||||
});
|
||||
}
|
||||
name() {
|
||||
return this._name || '';
|
||||
}
|
||||
url() {
|
||||
return this._url;
|
||||
}
|
||||
parentFrame() {
|
||||
return this._parentFrame;
|
||||
}
|
||||
childFrames() {
|
||||
return Array.from(this._childFrames);
|
||||
}
|
||||
isDetached() {
|
||||
return this._detached;
|
||||
}
|
||||
async addScriptTag(options = {}) {
|
||||
const copy = {
|
||||
...options
|
||||
};
|
||||
if (copy.path) {
|
||||
copy.content = (await _fs.default.promises.readFile(copy.path)).toString();
|
||||
copy.content = (0, _clientHelper.addSourceUrlToScript)(copy.content, copy.path);
|
||||
}
|
||||
return _elementHandle.ElementHandle.from((await this._channel.addScriptTag({
|
||||
...copy
|
||||
})).element);
|
||||
}
|
||||
async addStyleTag(options = {}) {
|
||||
const copy = {
|
||||
...options
|
||||
};
|
||||
if (copy.path) {
|
||||
copy.content = (await _fs.default.promises.readFile(copy.path)).toString();
|
||||
copy.content += '/*# sourceURL=' + copy.path.replace(/\n/g, '') + '*/';
|
||||
}
|
||||
return _elementHandle.ElementHandle.from((await this._channel.addStyleTag({
|
||||
...copy
|
||||
})).element);
|
||||
}
|
||||
async click(selector, options = {}) {
|
||||
return await this._channel.click({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async dblclick(selector, options = {}) {
|
||||
return await this._channel.dblclick({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async dragAndDrop(source, target, options = {}) {
|
||||
return await this._channel.dragAndDrop({
|
||||
source,
|
||||
target,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async tap(selector, options = {}) {
|
||||
return await this._channel.tap({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async fill(selector, value, options = {}) {
|
||||
return await this._channel.fill({
|
||||
selector,
|
||||
value,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async _highlight(selector) {
|
||||
return await this._channel.highlight({
|
||||
selector
|
||||
});
|
||||
}
|
||||
locator(selector, options) {
|
||||
return new _locator.Locator(this, selector, options);
|
||||
}
|
||||
getByTestId(testId) {
|
||||
return this.locator((0, _locatorUtils.getByTestIdSelector)((0, _locator.testIdAttributeName)(), testId));
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByAltTextSelector)(text, options));
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByLabelSelector)(text, options));
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByPlaceholderSelector)(text, options));
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByTextSelector)(text, options));
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByTitleSelector)(text, options));
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.locator((0, _locatorUtils.getByRoleSelector)(role, options));
|
||||
}
|
||||
frameLocator(selector) {
|
||||
return new _locator.FrameLocator(this, selector);
|
||||
}
|
||||
async focus(selector, options = {}) {
|
||||
await this._channel.focus({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async textContent(selector, options = {}) {
|
||||
const value = (await this._channel.textContent({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
async innerText(selector, options = {}) {
|
||||
return (await this._channel.innerText({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async innerHTML(selector, options = {}) {
|
||||
return (await this._channel.innerHTML({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async getAttribute(selector, name, options = {}) {
|
||||
const value = (await this._channel.getAttribute({
|
||||
selector,
|
||||
name,
|
||||
...options
|
||||
})).value;
|
||||
return value === undefined ? null : value;
|
||||
}
|
||||
async inputValue(selector, options = {}) {
|
||||
return (await this._channel.inputValue({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async isChecked(selector, options = {}) {
|
||||
return (await this._channel.isChecked({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async isDisabled(selector, options = {}) {
|
||||
return (await this._channel.isDisabled({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async isEditable(selector, options = {}) {
|
||||
return (await this._channel.isEditable({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async isEnabled(selector, options = {}) {
|
||||
return (await this._channel.isEnabled({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async isHidden(selector, options = {}) {
|
||||
return (await this._channel.isHidden({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async isVisible(selector, options = {}) {
|
||||
return (await this._channel.isVisible({
|
||||
selector,
|
||||
...options
|
||||
})).value;
|
||||
}
|
||||
async hover(selector, options = {}) {
|
||||
await this._channel.hover({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async selectOption(selector, values, options = {}) {
|
||||
return (await this._channel.selectOption({
|
||||
selector,
|
||||
...(0, _elementHandle.convertSelectOptionValues)(values),
|
||||
...options
|
||||
})).values;
|
||||
}
|
||||
async setInputFiles(selector, files, options = {}) {
|
||||
const converted = await (0, _elementHandle.convertInputFiles)(files, this.page().context());
|
||||
await this._channel.setInputFiles({
|
||||
selector,
|
||||
...converted,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async type(selector, text, options = {}) {
|
||||
await this._channel.type({
|
||||
selector,
|
||||
text,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async press(selector, key, options = {}) {
|
||||
await this._channel.press({
|
||||
selector,
|
||||
key,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async check(selector, options = {}) {
|
||||
await this._channel.check({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async uncheck(selector, options = {}) {
|
||||
await this._channel.uncheck({
|
||||
selector,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async setChecked(selector, checked, options) {
|
||||
if (checked) await this.check(selector, options);else await this.uncheck(selector, options);
|
||||
}
|
||||
async waitForTimeout(timeout) {
|
||||
await this._channel.waitForTimeout({
|
||||
timeout
|
||||
});
|
||||
}
|
||||
async waitForFunction(pageFunction, arg, options = {}) {
|
||||
if (typeof options.polling === 'string') (0, _utils.assert)(options.polling === 'raf', 'Unknown polling option: ' + options.polling);
|
||||
const result = await this._channel.waitForFunction({
|
||||
...options,
|
||||
pollingInterval: options.polling === 'raf' ? undefined : options.polling,
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return _jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
async title() {
|
||||
return (await this._channel.title()).value;
|
||||
}
|
||||
}
|
||||
exports.Frame = Frame;
|
||||
function verifyLoadState(name, waitUntil) {
|
||||
if (waitUntil === 'networkidle0') waitUntil = 'networkidle';
|
||||
if (!_types.kLifecycleEvents.has(waitUntil)) throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
|
||||
return waitUntil;
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.HarRouter = void 0;
|
||||
var _debugLogger = require("../utils/debugLogger");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class HarRouter {
|
||||
static async create(localUtils, file, notFoundAction, options) {
|
||||
const {
|
||||
harId,
|
||||
error
|
||||
} = await localUtils._channel.harOpen({
|
||||
file
|
||||
});
|
||||
if (error) throw new Error(error);
|
||||
return new HarRouter(localUtils, harId, notFoundAction, options);
|
||||
}
|
||||
constructor(localUtils, harId, notFoundAction, options) {
|
||||
this._localUtils = void 0;
|
||||
this._harId = void 0;
|
||||
this._notFoundAction = void 0;
|
||||
this._options = void 0;
|
||||
this._localUtils = localUtils;
|
||||
this._harId = harId;
|
||||
this._options = options;
|
||||
this._notFoundAction = notFoundAction;
|
||||
}
|
||||
async _handle(route) {
|
||||
const request = route.request();
|
||||
const response = await this._localUtils._channel.harLookup({
|
||||
harId: this._harId,
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
headers: await request.headersArray(),
|
||||
postData: request.postDataBuffer() || undefined,
|
||||
isNavigationRequest: request.isNavigationRequest()
|
||||
});
|
||||
if (response.action === 'redirect') {
|
||||
_debugLogger.debugLogger.log('api', `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
|
||||
await route._redirectNavigationRequest(response.redirectURL);
|
||||
return;
|
||||
}
|
||||
if (response.action === 'fulfill') {
|
||||
// If the response status is -1, the request was canceled or stalled, so we just stall it here.
|
||||
// See https://github.com/microsoft/playwright/issues/29311.
|
||||
// TODO: it'd be better to abort such requests, but then we likely need to respect the timing,
|
||||
// because the request might have been stalled for a long time until the very end of the
|
||||
// test when HAR was recorded but we'd abort it immediately.
|
||||
if (response.status === -1) return;
|
||||
await route.fulfill({
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers.map(h => [h.name, h.value])),
|
||||
body: response.body
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (response.action === 'error') _debugLogger.debugLogger.log('api', 'HAR: ' + response.message);
|
||||
// Report the error, but fall through to the default handler.
|
||||
|
||||
if (this._notFoundAction === 'abort') {
|
||||
await route.abort();
|
||||
return;
|
||||
}
|
||||
await route.fallback();
|
||||
}
|
||||
async addContextRoute(context) {
|
||||
await context.route(this._options.urlMatch || '**/*', route => this._handle(route));
|
||||
}
|
||||
async addPageRoute(page) {
|
||||
await page.route(this._options.urlMatch || '**/*', route => this._handle(route));
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
dispose() {
|
||||
this._localUtils._channel.harClose({
|
||||
harId: this._harId
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
exports.HarRouter = HarRouter;
|
||||
@@ -1,111 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Touchscreen = exports.Mouse = exports.Keyboard = void 0;
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Keyboard {
|
||||
constructor(page) {
|
||||
this._page = void 0;
|
||||
this._page = page;
|
||||
}
|
||||
async down(key) {
|
||||
await this._page._channel.keyboardDown({
|
||||
key
|
||||
});
|
||||
}
|
||||
async up(key) {
|
||||
await this._page._channel.keyboardUp({
|
||||
key
|
||||
});
|
||||
}
|
||||
async insertText(text) {
|
||||
await this._page._channel.keyboardInsertText({
|
||||
text
|
||||
});
|
||||
}
|
||||
async type(text, options = {}) {
|
||||
await this._page._channel.keyboardType({
|
||||
text,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async press(key, options = {}) {
|
||||
await this._page._channel.keyboardPress({
|
||||
key,
|
||||
...options
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.Keyboard = Keyboard;
|
||||
class Mouse {
|
||||
constructor(page) {
|
||||
this._page = void 0;
|
||||
this._page = page;
|
||||
}
|
||||
async move(x, y, options = {}) {
|
||||
await this._page._channel.mouseMove({
|
||||
x,
|
||||
y,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async down(options = {}) {
|
||||
await this._page._channel.mouseDown({
|
||||
...options
|
||||
});
|
||||
}
|
||||
async up(options = {}) {
|
||||
await this._page._channel.mouseUp(options);
|
||||
}
|
||||
async click(x, y, options = {}) {
|
||||
await this._page._channel.mouseClick({
|
||||
x,
|
||||
y,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async dblclick(x, y, options = {}) {
|
||||
await this.click(x, y, {
|
||||
...options,
|
||||
clickCount: 2
|
||||
});
|
||||
}
|
||||
async wheel(deltaX, deltaY) {
|
||||
await this._page._channel.mouseWheel({
|
||||
deltaX,
|
||||
deltaY
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.Mouse = Mouse;
|
||||
class Touchscreen {
|
||||
constructor(page) {
|
||||
this._page = void 0;
|
||||
this._page = page;
|
||||
}
|
||||
async tap(x, y) {
|
||||
await this._page._channel.touchscreenTap({
|
||||
x,
|
||||
y
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.Touchscreen = Touchscreen;
|
||||
@@ -1,121 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.JSHandle = void 0;
|
||||
exports.assertMaxArguments = assertMaxArguments;
|
||||
exports.parseResult = parseResult;
|
||||
exports.serializeArgument = serializeArgument;
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _serializers = require("../protocol/serializers");
|
||||
var _errors = require("./errors");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class JSHandle extends _channelOwner.ChannelOwner {
|
||||
static from(handle) {
|
||||
return handle._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._preview = void 0;
|
||||
this._preview = this._initializer.preview;
|
||||
this._channel.on('previewUpdated', ({
|
||||
preview
|
||||
}) => this._preview = preview);
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
const result = await this._channel.evaluateExpression({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: serializeArgument(arg)
|
||||
});
|
||||
return parseResult(result.value);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
const result = await this._channel.evaluateExpressionHandle({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: serializeArgument(arg)
|
||||
});
|
||||
return JSHandle.from(result.handle);
|
||||
}
|
||||
async getProperty(propertyName) {
|
||||
const result = await this._channel.getProperty({
|
||||
name: propertyName
|
||||
});
|
||||
return JSHandle.from(result.handle);
|
||||
}
|
||||
async getProperties() {
|
||||
const map = new Map();
|
||||
for (const {
|
||||
name,
|
||||
value
|
||||
} of (await this._channel.getPropertyList()).properties) map.set(name, JSHandle.from(value));
|
||||
return map;
|
||||
}
|
||||
async jsonValue() {
|
||||
return parseResult((await this._channel.jsonValue()).value);
|
||||
}
|
||||
asElement() {
|
||||
return null;
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose() {
|
||||
try {
|
||||
await this._channel.dispose();
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e)) return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
toString() {
|
||||
return this._preview;
|
||||
}
|
||||
}
|
||||
|
||||
// This function takes care of converting all JSHandles to their channels,
|
||||
// so that generic channel serializer converts them to guids.
|
||||
exports.JSHandle = JSHandle;
|
||||
function serializeArgument(arg) {
|
||||
const handles = [];
|
||||
const pushHandle = channel => {
|
||||
handles.push(channel);
|
||||
return handles.length - 1;
|
||||
};
|
||||
const value = (0, _serializers.serializeValue)(arg, value => {
|
||||
if (value instanceof JSHandle) return {
|
||||
h: pushHandle(value._channel)
|
||||
};
|
||||
return {
|
||||
fallThrough: value
|
||||
};
|
||||
});
|
||||
return {
|
||||
value,
|
||||
handles
|
||||
};
|
||||
}
|
||||
function parseResult(value) {
|
||||
return (0, _serializers.parseSerializedValue)(value, undefined);
|
||||
}
|
||||
function assertMaxArguments(count, max) {
|
||||
if (count > max) throw new Error('Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.');
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.JsonPipe = void 0;
|
||||
var _channelOwner = require("./channelOwner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class JsonPipe extends _channelOwner.ChannelOwner {
|
||||
static from(jsonPipe) {
|
||||
return jsonPipe._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
channel() {
|
||||
return this._channel;
|
||||
}
|
||||
}
|
||||
exports.JsonPipe = JsonPipe;
|
||||
@@ -1,36 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.LocalUtils = void 0;
|
||||
var _channelOwner = require("./channelOwner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class LocalUtils extends _channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.devices = void 0;
|
||||
this.markAsInternalType();
|
||||
this.devices = {};
|
||||
for (const {
|
||||
name,
|
||||
descriptor
|
||||
} of initializer.deviceDescriptors) this.devices[name] = descriptor;
|
||||
}
|
||||
}
|
||||
exports.LocalUtils = LocalUtils;
|
||||
@@ -1,448 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Locator = exports.FrameLocator = void 0;
|
||||
exports.setTestIdAttribute = setTestIdAttribute;
|
||||
exports.testIdAttributeName = testIdAttributeName;
|
||||
var util = _interopRequireWildcard(require("util"));
|
||||
var _utils = require("../utils");
|
||||
var _elementHandle = require("./elementHandle");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _stringUtils = require("../utils/isomorphic/stringUtils");
|
||||
var _locatorUtils = require("../utils/isomorphic/locatorUtils");
|
||||
let _util$inspect$custom;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
|
||||
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
|
||||
_util$inspect$custom = util.inspect.custom;
|
||||
class Locator {
|
||||
constructor(frame, selector, options) {
|
||||
this._frame = void 0;
|
||||
this._selector = void 0;
|
||||
this._frame = frame;
|
||||
this._selector = selector;
|
||||
if (options !== null && options !== void 0 && options.hasText) this._selector += ` >> internal:has-text=${(0, _stringUtils.escapeForTextSelector)(options.hasText, false)}`;
|
||||
if (options !== null && options !== void 0 && options.hasNotText) this._selector += ` >> internal:has-not-text=${(0, _stringUtils.escapeForTextSelector)(options.hasNotText, false)}`;
|
||||
if (options !== null && options !== void 0 && options.has) {
|
||||
const locator = options.has;
|
||||
if (locator._frame !== frame) throw new Error(`Inner "has" locator must belong to the same frame.`);
|
||||
this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
if (options !== null && options !== void 0 && options.hasNot) {
|
||||
const locator = options.hasNot;
|
||||
if (locator._frame !== frame) throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
|
||||
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
}
|
||||
async _withElement(task, timeout) {
|
||||
timeout = this._frame.page()._timeoutSettings.timeout({
|
||||
timeout
|
||||
});
|
||||
const deadline = timeout ? (0, _utils.monotonicTime)() + timeout : 0;
|
||||
return await this._frame._wrapApiCall(async () => {
|
||||
const result = await this._frame._channel.waitForSelector({
|
||||
selector: this._selector,
|
||||
strict: true,
|
||||
state: 'attached',
|
||||
timeout
|
||||
});
|
||||
const handle = _elementHandle.ElementHandle.fromNullable(result.element);
|
||||
if (!handle) throw new Error(`Could not resolve ${this._selector} to DOM Element`);
|
||||
try {
|
||||
return await task(handle, deadline ? deadline - (0, _utils.monotonicTime)() : 0);
|
||||
} finally {
|
||||
await handle.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
_equals(locator) {
|
||||
return this._frame === locator._frame && this._selector === locator._selector;
|
||||
}
|
||||
page() {
|
||||
return this._frame.page();
|
||||
}
|
||||
async boundingBox(options) {
|
||||
return await this._withElement(h => h.boundingBox(), options === null || options === void 0 ? void 0 : options.timeout);
|
||||
}
|
||||
async check(options = {}) {
|
||||
return await this._frame.check(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async click(options = {}) {
|
||||
return await this._frame.click(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async dblclick(options = {}) {
|
||||
return await this._frame.dblclick(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async dispatchEvent(type, eventInit = {}, options) {
|
||||
return await this._frame.dispatchEvent(this._selector, type, eventInit, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async dragTo(target, options = {}) {
|
||||
return await this._frame.dragAndDrop(this._selector, target._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async evaluate(pageFunction, arg, options) {
|
||||
return await this._withElement(h => h.evaluate(pageFunction, arg), options === null || options === void 0 ? void 0 : options.timeout);
|
||||
}
|
||||
async evaluateAll(pageFunction, arg) {
|
||||
return await this._frame.$$eval(this._selector, pageFunction, arg);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg, options) {
|
||||
return await this._withElement(h => h.evaluateHandle(pageFunction, arg), options === null || options === void 0 ? void 0 : options.timeout);
|
||||
}
|
||||
async fill(value, options = {}) {
|
||||
return await this._frame.fill(this._selector, value, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async clear(options = {}) {
|
||||
return await this.fill('', options);
|
||||
}
|
||||
async _highlight() {
|
||||
// VS Code extension uses this one, keep it for now.
|
||||
return await this._frame._highlight(this._selector);
|
||||
}
|
||||
async highlight() {
|
||||
return await this._frame._highlight(this._selector);
|
||||
}
|
||||
locator(selectorOrLocator, options) {
|
||||
if ((0, _utils.isString)(selectorOrLocator)) return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
|
||||
if (selectorOrLocator._frame !== this._frame) throw new Error(`Locators must belong to the same frame.`);
|
||||
return new Locator(this._frame, this._selector + ' >> internal:chain=' + JSON.stringify(selectorOrLocator._selector), options);
|
||||
}
|
||||
getByTestId(testId) {
|
||||
return this.locator((0, _locatorUtils.getByTestIdSelector)(testIdAttributeName(), testId));
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByAltTextSelector)(text, options));
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByLabelSelector)(text, options));
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByPlaceholderSelector)(text, options));
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByTextSelector)(text, options));
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByTitleSelector)(text, options));
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.locator((0, _locatorUtils.getByRoleSelector)(role, options));
|
||||
}
|
||||
frameLocator(selector) {
|
||||
return new FrameLocator(this._frame, this._selector + ' >> ' + selector);
|
||||
}
|
||||
filter(options) {
|
||||
return new Locator(this._frame, this._selector, options);
|
||||
}
|
||||
async elementHandle(options) {
|
||||
return await this._frame.waitForSelector(this._selector, {
|
||||
strict: true,
|
||||
state: 'attached',
|
||||
...options
|
||||
});
|
||||
}
|
||||
async elementHandles() {
|
||||
return await this._frame.$$(this._selector);
|
||||
}
|
||||
contentFrame() {
|
||||
return new FrameLocator(this._frame, this._selector);
|
||||
}
|
||||
first() {
|
||||
return new Locator(this._frame, this._selector + ' >> nth=0');
|
||||
}
|
||||
last() {
|
||||
return new Locator(this._frame, this._selector + ` >> nth=-1`);
|
||||
}
|
||||
nth(index) {
|
||||
return new Locator(this._frame, this._selector + ` >> nth=${index}`);
|
||||
}
|
||||
and(locator) {
|
||||
if (locator._frame !== this._frame) throw new Error(`Locators must belong to the same frame.`);
|
||||
return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(locator._selector));
|
||||
}
|
||||
or(locator) {
|
||||
if (locator._frame !== this._frame) throw new Error(`Locators must belong to the same frame.`);
|
||||
return new Locator(this._frame, this._selector + ` >> internal:or=` + JSON.stringify(locator._selector));
|
||||
}
|
||||
async focus(options) {
|
||||
return await this._frame.focus(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async blur(options) {
|
||||
await this._frame._channel.blur({
|
||||
selector: this._selector,
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async count() {
|
||||
return await this._frame._queryCount(this._selector);
|
||||
}
|
||||
async getAttribute(name, options) {
|
||||
return await this._frame.getAttribute(this._selector, name, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async hover(options = {}) {
|
||||
return await this._frame.hover(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async innerHTML(options) {
|
||||
return await this._frame.innerHTML(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async innerText(options) {
|
||||
return await this._frame.innerText(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async inputValue(options) {
|
||||
return await this._frame.inputValue(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async isChecked(options) {
|
||||
return await this._frame.isChecked(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async isDisabled(options) {
|
||||
return await this._frame.isDisabled(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async isEditable(options) {
|
||||
return await this._frame.isEditable(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async isEnabled(options) {
|
||||
return await this._frame.isEnabled(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async isHidden(options) {
|
||||
return await this._frame.isHidden(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async isVisible(options) {
|
||||
return await this._frame.isVisible(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async press(key, options = {}) {
|
||||
return await this._frame.press(this._selector, key, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
return await this._withElement((h, timeout) => h.screenshot({
|
||||
...options,
|
||||
timeout
|
||||
}), options.timeout);
|
||||
}
|
||||
async ariaSnapshot(options) {
|
||||
const result = await this._frame._channel.ariaSnapshot({
|
||||
...options,
|
||||
selector: this._selector
|
||||
});
|
||||
return result.snapshot;
|
||||
}
|
||||
async scrollIntoViewIfNeeded(options = {}) {
|
||||
return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({
|
||||
...options,
|
||||
timeout
|
||||
}), options.timeout);
|
||||
}
|
||||
async selectOption(values, options = {}) {
|
||||
return await this._frame.selectOption(this._selector, values, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async selectText(options = {}) {
|
||||
return await this._withElement((h, timeout) => h.selectText({
|
||||
...options,
|
||||
timeout
|
||||
}), options.timeout);
|
||||
}
|
||||
async setChecked(checked, options) {
|
||||
if (checked) await this.check(options);else await this.uncheck(options);
|
||||
}
|
||||
async setInputFiles(files, options = {}) {
|
||||
return await this._frame.setInputFiles(this._selector, files, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async tap(options = {}) {
|
||||
return await this._frame.tap(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async textContent(options) {
|
||||
return await this._frame.textContent(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async type(text, options = {}) {
|
||||
return await this._frame.type(this._selector, text, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async pressSequentially(text, options = {}) {
|
||||
return await this.type(text, options);
|
||||
}
|
||||
async uncheck(options = {}) {
|
||||
return await this._frame.uncheck(this._selector, {
|
||||
strict: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async all() {
|
||||
return new Array(await this.count()).fill(0).map((e, i) => this.nth(i));
|
||||
}
|
||||
async allInnerTexts() {
|
||||
return await this._frame.$$eval(this._selector, ee => ee.map(e => e.innerText));
|
||||
}
|
||||
async allTextContents() {
|
||||
return await this._frame.$$eval(this._selector, ee => ee.map(e => e.textContent || ''));
|
||||
}
|
||||
async waitFor(options) {
|
||||
await this._frame._channel.waitForSelector({
|
||||
selector: this._selector,
|
||||
strict: true,
|
||||
omitReturnValue: true,
|
||||
...options
|
||||
});
|
||||
}
|
||||
async _expect(expression, options) {
|
||||
const params = {
|
||||
selector: this._selector,
|
||||
expression,
|
||||
...options,
|
||||
isNot: !!options.isNot
|
||||
};
|
||||
params.expectedValue = (0, _jsHandle.serializeArgument)(options.expectedValue);
|
||||
const result = await this._frame._channel.expect(params);
|
||||
if (result.received !== undefined) result.received = (0, _jsHandle.parseResult)(result.received);
|
||||
return result;
|
||||
}
|
||||
[_util$inspect$custom]() {
|
||||
return this.toString();
|
||||
}
|
||||
toString() {
|
||||
return (0, _utils.asLocator)('javascript', this._selector);
|
||||
}
|
||||
}
|
||||
exports.Locator = Locator;
|
||||
class FrameLocator {
|
||||
constructor(frame, selector) {
|
||||
this._frame = void 0;
|
||||
this._frameSelector = void 0;
|
||||
this._frame = frame;
|
||||
this._frameSelector = selector;
|
||||
}
|
||||
locator(selectorOrLocator, options) {
|
||||
if ((0, _utils.isString)(selectorOrLocator)) return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator, options);
|
||||
if (selectorOrLocator._frame !== this._frame) throw new Error(`Locators must belong to the same frame.`);
|
||||
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator._selector, options);
|
||||
}
|
||||
getByTestId(testId) {
|
||||
return this.locator((0, _locatorUtils.getByTestIdSelector)(testIdAttributeName(), testId));
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByAltTextSelector)(text, options));
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByLabelSelector)(text, options));
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByPlaceholderSelector)(text, options));
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByTextSelector)(text, options));
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.locator((0, _locatorUtils.getByTitleSelector)(text, options));
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.locator((0, _locatorUtils.getByRoleSelector)(role, options));
|
||||
}
|
||||
owner() {
|
||||
return new Locator(this._frame, this._frameSelector);
|
||||
}
|
||||
frameLocator(selector) {
|
||||
return new FrameLocator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selector);
|
||||
}
|
||||
first() {
|
||||
return new FrameLocator(this._frame, this._frameSelector + ' >> nth=0');
|
||||
}
|
||||
last() {
|
||||
return new FrameLocator(this._frame, this._frameSelector + ` >> nth=-1`);
|
||||
}
|
||||
nth(index) {
|
||||
return new FrameLocator(this._frame, this._frameSelector + ` >> nth=${index}`);
|
||||
}
|
||||
}
|
||||
exports.FrameLocator = FrameLocator;
|
||||
let _testIdAttributeName = 'data-testid';
|
||||
function testIdAttributeName() {
|
||||
return _testIdAttributeName;
|
||||
}
|
||||
function setTestIdAttribute(attributeName) {
|
||||
_testIdAttributeName = attributeName;
|
||||
}
|
||||
@@ -1,767 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.WebSocketRouteHandler = exports.WebSocketRoute = exports.WebSocket = exports.RouteHandler = exports.Route = exports.Response = exports.Request = exports.RawHeaders = void 0;
|
||||
exports.validateHeaders = validateHeaders;
|
||||
var _url = require("url");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _frame = require("./frame");
|
||||
var _worker = require("./worker");
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _utilsBundle = require("../utilsBundle");
|
||||
var _utils = require("../utils");
|
||||
var _manualPromise = require("../utils/manualPromise");
|
||||
var _events = require("./events");
|
||||
var _waiter = require("./waiter");
|
||||
var _fetch = require("./fetch");
|
||||
var _errors = require("./errors");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
class Request extends _channelOwner.ChannelOwner {
|
||||
static from(request) {
|
||||
return request._object;
|
||||
}
|
||||
static fromNullable(request) {
|
||||
return request ? Request.from(request) : null;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._redirectedFrom = null;
|
||||
this._redirectedTo = null;
|
||||
this._failureText = null;
|
||||
this._provisionalHeaders = void 0;
|
||||
this._actualHeadersPromise = void 0;
|
||||
this._timing = void 0;
|
||||
this._fallbackOverrides = {};
|
||||
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
|
||||
if (this._redirectedFrom) this._redirectedFrom._redirectedTo = this;
|
||||
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||
this._timing = {
|
||||
startTime: 0,
|
||||
domainLookupStart: -1,
|
||||
domainLookupEnd: -1,
|
||||
connectStart: -1,
|
||||
secureConnectionStart: -1,
|
||||
connectEnd: -1,
|
||||
requestStart: -1,
|
||||
responseStart: -1,
|
||||
responseEnd: -1
|
||||
};
|
||||
}
|
||||
url() {
|
||||
return this._fallbackOverrides.url || this._initializer.url;
|
||||
}
|
||||
resourceType() {
|
||||
return this._initializer.resourceType;
|
||||
}
|
||||
method() {
|
||||
return this._fallbackOverrides.method || this._initializer.method;
|
||||
}
|
||||
postData() {
|
||||
var _ref;
|
||||
return ((_ref = this._fallbackOverrides.postDataBuffer || this._initializer.postData) === null || _ref === void 0 ? void 0 : _ref.toString('utf-8')) || null;
|
||||
}
|
||||
postDataBuffer() {
|
||||
return this._fallbackOverrides.postDataBuffer || this._initializer.postData || null;
|
||||
}
|
||||
postDataJSON() {
|
||||
const postData = this.postData();
|
||||
if (!postData) return null;
|
||||
const contentType = this.headers()['content-type'];
|
||||
if (contentType !== null && contentType !== void 0 && contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const entries = {};
|
||||
const parsed = new _url.URLSearchParams(postData);
|
||||
for (const [k, v] of parsed.entries()) entries[k] = v;
|
||||
return entries;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(postData);
|
||||
} catch (e) {
|
||||
throw new Error('POST data is not a valid JSON object: ' + postData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
headers() {
|
||||
if (this._fallbackOverrides.headers) return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers).headers();
|
||||
return this._provisionalHeaders.headers();
|
||||
}
|
||||
async _actualHeaders() {
|
||||
if (this._fallbackOverrides.headers) return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers);
|
||||
if (!this._actualHeadersPromise) {
|
||||
this._actualHeadersPromise = this._wrapApiCall(async () => {
|
||||
return new RawHeaders((await this._channel.rawRequestHeaders()).headers);
|
||||
});
|
||||
}
|
||||
return await this._actualHeadersPromise;
|
||||
}
|
||||
async allHeaders() {
|
||||
return (await this._actualHeaders()).headers();
|
||||
}
|
||||
async headersArray() {
|
||||
return (await this._actualHeaders()).headersArray();
|
||||
}
|
||||
async headerValue(name) {
|
||||
return (await this._actualHeaders()).get(name);
|
||||
}
|
||||
async response() {
|
||||
return Response.fromNullable((await this._channel.response()).response);
|
||||
}
|
||||
async _internalResponse() {
|
||||
return await this._wrapApiCall(async () => {
|
||||
return Response.fromNullable((await this._channel.response()).response);
|
||||
}, true);
|
||||
}
|
||||
frame() {
|
||||
if (!this._initializer.frame) {
|
||||
(0, _utils.assert)(this.serviceWorker());
|
||||
throw new Error('Service Worker requests do not have an associated frame.');
|
||||
}
|
||||
const frame = _frame.Frame.from(this._initializer.frame);
|
||||
if (!frame._page) {
|
||||
throw new Error(['Frame for this navigation request is not available, because the request', 'was issued before the frame is created. You can check whether the request', 'is a navigation request by calling isNavigationRequest() method.'].join('\n'));
|
||||
}
|
||||
return frame;
|
||||
}
|
||||
_safePage() {
|
||||
var _Frame$fromNullable;
|
||||
return ((_Frame$fromNullable = _frame.Frame.fromNullable(this._initializer.frame)) === null || _Frame$fromNullable === void 0 ? void 0 : _Frame$fromNullable._page) || null;
|
||||
}
|
||||
serviceWorker() {
|
||||
return this._initializer.serviceWorker ? _worker.Worker.from(this._initializer.serviceWorker) : null;
|
||||
}
|
||||
isNavigationRequest() {
|
||||
return this._initializer.isNavigationRequest;
|
||||
}
|
||||
redirectedFrom() {
|
||||
return this._redirectedFrom;
|
||||
}
|
||||
redirectedTo() {
|
||||
return this._redirectedTo;
|
||||
}
|
||||
failure() {
|
||||
if (this._failureText === null) return null;
|
||||
return {
|
||||
errorText: this._failureText
|
||||
};
|
||||
}
|
||||
timing() {
|
||||
return this._timing;
|
||||
}
|
||||
async sizes() {
|
||||
const response = await this.response();
|
||||
if (!response) throw new Error('Unable to fetch sizes for failed request');
|
||||
return (await response._channel.sizes()).sizes;
|
||||
}
|
||||
_setResponseEndTiming(responseEndTiming) {
|
||||
this._timing.responseEnd = responseEndTiming;
|
||||
if (this._timing.responseStart === -1) this._timing.responseStart = responseEndTiming;
|
||||
}
|
||||
_finalRequest() {
|
||||
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
|
||||
}
|
||||
_applyFallbackOverrides(overrides) {
|
||||
if (overrides.url) this._fallbackOverrides.url = overrides.url;
|
||||
if (overrides.method) this._fallbackOverrides.method = overrides.method;
|
||||
if (overrides.headers) this._fallbackOverrides.headers = overrides.headers;
|
||||
if ((0, _utils.isString)(overrides.postData)) this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, 'utf-8');else if (overrides.postData instanceof Buffer) this._fallbackOverrides.postDataBuffer = overrides.postData;else if (overrides.postData) this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), 'utf-8');
|
||||
}
|
||||
_fallbackOverridesForContinue() {
|
||||
return this._fallbackOverrides;
|
||||
}
|
||||
_targetClosedScope() {
|
||||
var _this$serviceWorker, _this$_safePage;
|
||||
return ((_this$serviceWorker = this.serviceWorker()) === null || _this$serviceWorker === void 0 ? void 0 : _this$serviceWorker._closedScope) || ((_this$_safePage = this._safePage()) === null || _this$_safePage === void 0 ? void 0 : _this$_safePage._closedOrCrashedScope) || new _manualPromise.LongStandingScope();
|
||||
}
|
||||
}
|
||||
exports.Request = Request;
|
||||
class Route extends _channelOwner.ChannelOwner {
|
||||
static from(route) {
|
||||
return route._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._handlingPromise = null;
|
||||
this._context = void 0;
|
||||
this._didThrow = false;
|
||||
this.markAsInternalType();
|
||||
}
|
||||
request() {
|
||||
return Request.from(this._initializer.request);
|
||||
}
|
||||
async _raceWithTargetClose(promise) {
|
||||
// When page closes or crashes, we catch any potential rejects from this Route.
|
||||
// Note that page could be missing when routing popup's initial request that
|
||||
// does not have a Page initialized just yet.
|
||||
return await this.request()._targetClosedScope().safeRace(promise);
|
||||
}
|
||||
async _startHandling() {
|
||||
this._handlingPromise = new _manualPromise.ManualPromise();
|
||||
return await this._handlingPromise;
|
||||
}
|
||||
async fallback(options = {}) {
|
||||
this._checkNotHandled();
|
||||
this.request()._applyFallbackOverrides(options);
|
||||
this._reportHandled(false);
|
||||
}
|
||||
async abort(errorCode) {
|
||||
await this._handleRoute(async () => {
|
||||
await this._raceWithTargetClose(this._channel.abort({
|
||||
errorCode
|
||||
}));
|
||||
});
|
||||
}
|
||||
async _redirectNavigationRequest(url) {
|
||||
await this._handleRoute(async () => {
|
||||
await this._raceWithTargetClose(this._channel.redirectNavigationRequest({
|
||||
url
|
||||
}));
|
||||
});
|
||||
}
|
||||
async fetch(options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
return await this._context.request._innerFetch({
|
||||
request: this.request(),
|
||||
data: options.postData,
|
||||
...options
|
||||
});
|
||||
});
|
||||
}
|
||||
async fulfill(options = {}) {
|
||||
await this._handleRoute(async () => {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._innerFulfill(options);
|
||||
});
|
||||
});
|
||||
}
|
||||
async _handleRoute(callback) {
|
||||
this._checkNotHandled();
|
||||
try {
|
||||
await callback();
|
||||
this._reportHandled(true);
|
||||
} catch (e) {
|
||||
this._didThrow = true;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async _innerFulfill(options = {}) {
|
||||
let fetchResponseUid;
|
||||
let {
|
||||
status: statusOption,
|
||||
headers: headersOption,
|
||||
body
|
||||
} = options;
|
||||
if (options.json !== undefined) {
|
||||
(0, _utils.assert)(options.body === undefined, 'Can specify either body or json parameters');
|
||||
body = JSON.stringify(options.json);
|
||||
}
|
||||
if (options.response instanceof _fetch.APIResponse) {
|
||||
var _statusOption, _headersOption;
|
||||
(_statusOption = statusOption) !== null && _statusOption !== void 0 ? _statusOption : statusOption = options.response.status();
|
||||
(_headersOption = headersOption) !== null && _headersOption !== void 0 ? _headersOption : headersOption = options.response.headers();
|
||||
if (body === undefined && options.path === undefined) {
|
||||
if (options.response._request._connection === this._connection) fetchResponseUid = options.response._fetchUid();else body = await options.response.body();
|
||||
}
|
||||
}
|
||||
let isBase64 = false;
|
||||
let length = 0;
|
||||
if (options.path) {
|
||||
const buffer = await _fs.default.promises.readFile(options.path);
|
||||
body = buffer.toString('base64');
|
||||
isBase64 = true;
|
||||
length = buffer.length;
|
||||
} else if ((0, _utils.isString)(body)) {
|
||||
isBase64 = false;
|
||||
length = Buffer.byteLength(body);
|
||||
} else if (body) {
|
||||
length = body.length;
|
||||
body = body.toString('base64');
|
||||
isBase64 = true;
|
||||
}
|
||||
const headers = {};
|
||||
for (const header of Object.keys(headersOption || {})) headers[header.toLowerCase()] = String(headersOption[header]);
|
||||
if (options.contentType) headers['content-type'] = String(options.contentType);else if (options.json) headers['content-type'] = 'application/json';else if (options.path) headers['content-type'] = _utilsBundle.mime.getType(options.path) || 'application/octet-stream';
|
||||
if (length && !('content-length' in headers)) headers['content-length'] = String(length);
|
||||
await this._raceWithTargetClose(this._channel.fulfill({
|
||||
status: statusOption || 200,
|
||||
headers: (0, _utils.headersObjectToArray)(headers),
|
||||
body,
|
||||
isBase64,
|
||||
fetchResponseUid
|
||||
}));
|
||||
}
|
||||
async continue(options = {}) {
|
||||
await this._handleRoute(async () => {
|
||||
this.request()._applyFallbackOverrides(options);
|
||||
await this._innerContinue(false /* isFallback */);
|
||||
});
|
||||
}
|
||||
_checkNotHandled() {
|
||||
if (!this._handlingPromise) throw new Error('Route is already handled!');
|
||||
}
|
||||
_reportHandled(done) {
|
||||
const chain = this._handlingPromise;
|
||||
this._handlingPromise = null;
|
||||
chain.resolve(done);
|
||||
}
|
||||
async _innerContinue(isFallback) {
|
||||
const options = this.request()._fallbackOverridesForContinue();
|
||||
return await this._raceWithTargetClose(this._channel.continue({
|
||||
url: options.url,
|
||||
method: options.method,
|
||||
headers: options.headers ? (0, _utils.headersObjectToArray)(options.headers) : undefined,
|
||||
postData: options.postDataBuffer,
|
||||
isFallback
|
||||
}));
|
||||
}
|
||||
}
|
||||
exports.Route = Route;
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class WebSocketRoute extends _channelOwner.ChannelOwner {
|
||||
static from(route) {
|
||||
return route._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._onPageMessage = void 0;
|
||||
this._onPageClose = void 0;
|
||||
this._onServerMessage = void 0;
|
||||
this._onServerClose = void 0;
|
||||
this._server = void 0;
|
||||
this._connected = false;
|
||||
this.markAsInternalType();
|
||||
this._server = {
|
||||
onMessage: handler => {
|
||||
this._onServerMessage = handler;
|
||||
},
|
||||
onClose: handler => {
|
||||
this._onServerClose = handler;
|
||||
},
|
||||
connectToServer: () => {
|
||||
throw new Error(`connectToServer must be called on the page-side WebSocketRoute`);
|
||||
},
|
||||
url: () => {
|
||||
return this._initializer.url;
|
||||
},
|
||||
close: async (options = {}) => {
|
||||
await this._channel.closeServer({
|
||||
...options,
|
||||
wasClean: true
|
||||
}).catch(() => {});
|
||||
},
|
||||
send: message => {
|
||||
if ((0, _utils.isString)(message)) this._channel.sendToServer({
|
||||
message,
|
||||
isBase64: false
|
||||
}).catch(() => {});else this._channel.sendToServer({
|
||||
message: message.toString('base64'),
|
||||
isBase64: true
|
||||
}).catch(() => {});
|
||||
},
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
};
|
||||
this._channel.on('messageFromPage', ({
|
||||
message,
|
||||
isBase64
|
||||
}) => {
|
||||
if (this._onPageMessage) this._onPageMessage(isBase64 ? Buffer.from(message, 'base64') : message);else if (this._connected) this._channel.sendToServer({
|
||||
message,
|
||||
isBase64
|
||||
}).catch(() => {});
|
||||
});
|
||||
this._channel.on('messageFromServer', ({
|
||||
message,
|
||||
isBase64
|
||||
}) => {
|
||||
if (this._onServerMessage) this._onServerMessage(isBase64 ? Buffer.from(message, 'base64') : message);else this._channel.sendToPage({
|
||||
message,
|
||||
isBase64
|
||||
}).catch(() => {});
|
||||
});
|
||||
this._channel.on('closePage', ({
|
||||
code,
|
||||
reason,
|
||||
wasClean
|
||||
}) => {
|
||||
if (this._onPageClose) this._onPageClose(code, reason);else this._channel.closeServer({
|
||||
code,
|
||||
reason,
|
||||
wasClean
|
||||
}).catch(() => {});
|
||||
});
|
||||
this._channel.on('closeServer', ({
|
||||
code,
|
||||
reason,
|
||||
wasClean
|
||||
}) => {
|
||||
if (this._onServerClose) this._onServerClose(code, reason);else this._channel.closePage({
|
||||
code,
|
||||
reason,
|
||||
wasClean
|
||||
}).catch(() => {});
|
||||
});
|
||||
}
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
async close(options = {}) {
|
||||
await this._channel.closePage({
|
||||
...options,
|
||||
wasClean: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
connectToServer() {
|
||||
if (this._connected) throw new Error('Already connected to the server');
|
||||
this._connected = true;
|
||||
this._channel.connect().catch(() => {});
|
||||
return this._server;
|
||||
}
|
||||
send(message) {
|
||||
if ((0, _utils.isString)(message)) this._channel.sendToPage({
|
||||
message,
|
||||
isBase64: false
|
||||
}).catch(() => {});else this._channel.sendToPage({
|
||||
message: message.toString('base64'),
|
||||
isBase64: true
|
||||
}).catch(() => {});
|
||||
}
|
||||
onMessage(handler) {
|
||||
this._onPageMessage = handler;
|
||||
}
|
||||
onClose(handler) {
|
||||
this._onPageClose = handler;
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async _afterHandle() {
|
||||
if (this._connected) return;
|
||||
// Ensure that websocket is "open" and can send messages without an actual server connection.
|
||||
await this._channel.ensureOpened();
|
||||
}
|
||||
}
|
||||
exports.WebSocketRoute = WebSocketRoute;
|
||||
class WebSocketRouteHandler {
|
||||
constructor(baseURL, url, handler) {
|
||||
this._baseURL = void 0;
|
||||
this.url = void 0;
|
||||
this.handler = void 0;
|
||||
this._baseURL = baseURL;
|
||||
this.url = url;
|
||||
this.handler = handler;
|
||||
}
|
||||
static prepareInterceptionPatterns(handlers) {
|
||||
const patterns = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
if ((0, _utils.isString)(handler.url)) patterns.push({
|
||||
glob: handler.url
|
||||
});else if ((0, _utils.isRegExp)(handler.url)) patterns.push({
|
||||
regexSource: handler.url.source,
|
||||
regexFlags: handler.url.flags
|
||||
});else all = true;
|
||||
}
|
||||
if (all) return [{
|
||||
glob: '**/*'
|
||||
}];
|
||||
return patterns;
|
||||
}
|
||||
matches(wsURL) {
|
||||
return (0, _utils.urlMatches)(this._baseURL, wsURL, this.url);
|
||||
}
|
||||
async handle(webSocketRoute) {
|
||||
const handler = this.handler;
|
||||
await handler(webSocketRoute);
|
||||
await webSocketRoute._afterHandle();
|
||||
}
|
||||
}
|
||||
exports.WebSocketRouteHandler = WebSocketRouteHandler;
|
||||
class Response extends _channelOwner.ChannelOwner {
|
||||
static from(response) {
|
||||
return response._object;
|
||||
}
|
||||
static fromNullable(response) {
|
||||
return response ? Response.from(response) : null;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._provisionalHeaders = void 0;
|
||||
this._actualHeadersPromise = void 0;
|
||||
this._request = void 0;
|
||||
this._finishedPromise = new _manualPromise.ManualPromise();
|
||||
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||
this._request = Request.from(this._initializer.request);
|
||||
Object.assign(this._request._timing, this._initializer.timing);
|
||||
}
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
ok() {
|
||||
// Status 0 is for file:// URLs
|
||||
return this._initializer.status === 0 || this._initializer.status >= 200 && this._initializer.status <= 299;
|
||||
}
|
||||
status() {
|
||||
return this._initializer.status;
|
||||
}
|
||||
statusText() {
|
||||
return this._initializer.statusText;
|
||||
}
|
||||
fromServiceWorker() {
|
||||
return this._initializer.fromServiceWorker;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
headers() {
|
||||
return this._provisionalHeaders.headers();
|
||||
}
|
||||
async _actualHeaders() {
|
||||
if (!this._actualHeadersPromise) {
|
||||
this._actualHeadersPromise = (async () => {
|
||||
return new RawHeaders((await this._channel.rawResponseHeaders()).headers);
|
||||
})();
|
||||
}
|
||||
return await this._actualHeadersPromise;
|
||||
}
|
||||
async allHeaders() {
|
||||
return (await this._actualHeaders()).headers();
|
||||
}
|
||||
async headersArray() {
|
||||
return (await this._actualHeaders()).headersArray().slice();
|
||||
}
|
||||
async headerValue(name) {
|
||||
return (await this._actualHeaders()).get(name);
|
||||
}
|
||||
async headerValues(name) {
|
||||
return (await this._actualHeaders()).getAll(name);
|
||||
}
|
||||
async finished() {
|
||||
return await this.request()._targetClosedScope().race(this._finishedPromise);
|
||||
}
|
||||
async body() {
|
||||
return (await this._channel.body()).binary;
|
||||
}
|
||||
async text() {
|
||||
const content = await this.body();
|
||||
return content.toString('utf8');
|
||||
}
|
||||
async json() {
|
||||
const content = await this.text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
request() {
|
||||
return this._request;
|
||||
}
|
||||
frame() {
|
||||
return this._request.frame();
|
||||
}
|
||||
async serverAddr() {
|
||||
return (await this._channel.serverAddr()).value || null;
|
||||
}
|
||||
async securityDetails() {
|
||||
return (await this._channel.securityDetails()).value || null;
|
||||
}
|
||||
}
|
||||
exports.Response = Response;
|
||||
class WebSocket extends _channelOwner.ChannelOwner {
|
||||
static from(webSocket) {
|
||||
return webSocket._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._page = void 0;
|
||||
this._isClosed = void 0;
|
||||
this._isClosed = false;
|
||||
this._page = parent;
|
||||
this._channel.on('frameSent', event => {
|
||||
if (event.opcode === 1) this.emit(_events.Events.WebSocket.FrameSent, {
|
||||
payload: event.data
|
||||
});else if (event.opcode === 2) this.emit(_events.Events.WebSocket.FrameSent, {
|
||||
payload: Buffer.from(event.data, 'base64')
|
||||
});
|
||||
});
|
||||
this._channel.on('frameReceived', event => {
|
||||
if (event.opcode === 1) this.emit(_events.Events.WebSocket.FrameReceived, {
|
||||
payload: event.data
|
||||
});else if (event.opcode === 2) this.emit(_events.Events.WebSocket.FrameReceived, {
|
||||
payload: Buffer.from(event.data, 'base64')
|
||||
});
|
||||
});
|
||||
this._channel.on('socketError', ({
|
||||
error
|
||||
}) => this.emit(_events.Events.WebSocket.Error, error));
|
||||
this._channel.on('close', () => {
|
||||
this._isClosed = true;
|
||||
this.emit(_events.Events.WebSocket.Close, this);
|
||||
});
|
||||
}
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
isClosed() {
|
||||
return this._isClosed;
|
||||
}
|
||||
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = _waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== _events.Events.WebSocket.Error) waiter.rejectOnEvent(this, _events.Events.WebSocket.Error, new Error('Socket error'));
|
||||
if (event !== _events.Events.WebSocket.Close) waiter.rejectOnEvent(this, _events.Events.WebSocket.Close, new Error('Socket closed'));
|
||||
waiter.rejectOnEvent(this._page, _events.Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
exports.WebSocket = WebSocket;
|
||||
function validateHeaders(headers) {
|
||||
for (const key of Object.keys(headers)) {
|
||||
const value = headers[key];
|
||||
if (!Object.is(value, undefined) && !(0, _utils.isString)(value)) throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
|
||||
}
|
||||
}
|
||||
class RouteHandler {
|
||||
constructor(baseURL, url, handler, times = Number.MAX_SAFE_INTEGER) {
|
||||
this.handledCount = 0;
|
||||
this._baseURL = void 0;
|
||||
this._times = void 0;
|
||||
this.url = void 0;
|
||||
this.handler = void 0;
|
||||
this._ignoreException = false;
|
||||
this._activeInvocations = new Set();
|
||||
this._svedZone = void 0;
|
||||
this._baseURL = baseURL;
|
||||
this._times = times;
|
||||
this.url = url;
|
||||
this.handler = handler;
|
||||
this._svedZone = _utils.zones.currentZone();
|
||||
}
|
||||
static prepareInterceptionPatterns(handlers) {
|
||||
const patterns = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
if ((0, _utils.isString)(handler.url)) patterns.push({
|
||||
glob: handler.url
|
||||
});else if ((0, _utils.isRegExp)(handler.url)) patterns.push({
|
||||
regexSource: handler.url.source,
|
||||
regexFlags: handler.url.flags
|
||||
});else all = true;
|
||||
}
|
||||
if (all) return [{
|
||||
glob: '**/*'
|
||||
}];
|
||||
return patterns;
|
||||
}
|
||||
matches(requestURL) {
|
||||
return (0, _utils.urlMatches)(this._baseURL, requestURL, this.url);
|
||||
}
|
||||
async handle(route) {
|
||||
return await this._svedZone.run(async () => this._handleImpl(route));
|
||||
}
|
||||
async _handleImpl(route) {
|
||||
const handlerInvocation = {
|
||||
complete: new _manualPromise.ManualPromise(),
|
||||
route
|
||||
};
|
||||
this._activeInvocations.add(handlerInvocation);
|
||||
try {
|
||||
return await this._handleInternal(route);
|
||||
} catch (e) {
|
||||
// If the handler was stopped (without waiting for completion), we ignore all exceptions.
|
||||
if (this._ignoreException) return false;
|
||||
if ((0, _errors.isTargetClosedError)(e)) {
|
||||
// We are failing in the handler because the target close closed.
|
||||
// Give user a hint!
|
||||
(0, _utils.rewriteErrorMessage)(e, `"${e.message}" while running route callback.\nConsider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\`\nbefore the end of the test to ignore remaining routes in flight.`);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
handlerInvocation.complete.resolve();
|
||||
this._activeInvocations.delete(handlerInvocation);
|
||||
}
|
||||
}
|
||||
async stop(behavior) {
|
||||
// When a handler is manually unrouted or its page/context is closed we either
|
||||
// - wait for the current handler invocations to finish
|
||||
// - or do not wait, if the user opted out of it, but swallow all exceptions
|
||||
// that happen after the unroute/close.
|
||||
if (behavior === 'ignoreErrors') {
|
||||
this._ignoreException = true;
|
||||
} else {
|
||||
const promises = [];
|
||||
for (const activation of this._activeInvocations) {
|
||||
if (!activation.route._didThrow) promises.push(activation.complete);
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
async _handleInternal(route) {
|
||||
++this.handledCount;
|
||||
const handledPromise = route._startHandling();
|
||||
// Extract handler into a variable to avoid [RouteHandler.handler] in the stack.
|
||||
const handler = this.handler;
|
||||
const [handled] = await Promise.all([handledPromise, handler(route, route.request())]);
|
||||
return handled;
|
||||
}
|
||||
willExpire() {
|
||||
return this.handledCount + 1 >= this._times;
|
||||
}
|
||||
}
|
||||
exports.RouteHandler = RouteHandler;
|
||||
class RawHeaders {
|
||||
static _fromHeadersObjectLossy(headers) {
|
||||
const headersArray = Object.entries(headers).map(([name, value]) => ({
|
||||
name,
|
||||
value
|
||||
})).filter(header => header.value !== undefined);
|
||||
return new RawHeaders(headersArray);
|
||||
}
|
||||
constructor(headers) {
|
||||
this._headersArray = void 0;
|
||||
this._headersMap = new _utils.MultiMap();
|
||||
this._headersArray = headers;
|
||||
for (const header of headers) this._headersMap.set(header.name.toLowerCase(), header.value);
|
||||
}
|
||||
get(name) {
|
||||
const values = this.getAll(name);
|
||||
if (!values || !values.length) return null;
|
||||
return values.join(name.toLowerCase() === 'set-cookie' ? '\n' : ', ');
|
||||
}
|
||||
getAll(name) {
|
||||
return [...this._headersMap.get(name.toLowerCase())];
|
||||
}
|
||||
headers() {
|
||||
const result = {};
|
||||
for (const name of this._headersMap.keys()) result[name] = this.get(name);
|
||||
return result;
|
||||
}
|
||||
headersArray() {
|
||||
return this._headersArray;
|
||||
}
|
||||
}
|
||||
exports.RawHeaders = RawHeaders;
|
||||
@@ -1,748 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Page = exports.BindingCall = void 0;
|
||||
var _fs = _interopRequireDefault(require("fs"));
|
||||
var _path = _interopRequireDefault(require("path"));
|
||||
var _errors = require("./errors");
|
||||
var _timeoutSettings = require("../common/timeoutSettings");
|
||||
var _utils = require("../utils");
|
||||
var _accessibility = require("./accessibility");
|
||||
var _artifact = require("./artifact");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _clientHelper = require("./clientHelper");
|
||||
var _coverage = require("./coverage");
|
||||
var _download = require("./download");
|
||||
var _elementHandle = require("./elementHandle");
|
||||
var _events = require("./events");
|
||||
var _fileChooser = require("./fileChooser");
|
||||
var _frame = require("./frame");
|
||||
var _input = require("./input");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _network = require("./network");
|
||||
var _video = require("./video");
|
||||
var _waiter = require("./waiter");
|
||||
var _worker = require("./worker");
|
||||
var _harRouter = require("./harRouter");
|
||||
let _Symbol$asyncDispose;
|
||||
/**
|
||||
* Copyright 2017 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
_Symbol$asyncDispose = Symbol.asyncDispose;
|
||||
class Page extends _channelOwner.ChannelOwner {
|
||||
static from(page) {
|
||||
return page._object;
|
||||
}
|
||||
static fromNullable(page) {
|
||||
return page ? Page.from(page) : null;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._browserContext = void 0;
|
||||
this._ownedContext = void 0;
|
||||
this._mainFrame = void 0;
|
||||
this._frames = new Set();
|
||||
this._workers = new Set();
|
||||
this._closed = false;
|
||||
this._closedOrCrashedScope = new _utils.LongStandingScope();
|
||||
this._viewportSize = void 0;
|
||||
this._routes = [];
|
||||
this._webSocketRoutes = [];
|
||||
this.accessibility = void 0;
|
||||
this.coverage = void 0;
|
||||
this.keyboard = void 0;
|
||||
this.mouse = void 0;
|
||||
this.request = void 0;
|
||||
this.touchscreen = void 0;
|
||||
this.clock = void 0;
|
||||
this._bindings = new Map();
|
||||
this._timeoutSettings = void 0;
|
||||
this._video = null;
|
||||
this._opener = void 0;
|
||||
this._closeReason = void 0;
|
||||
this._closeWasCalled = false;
|
||||
this._harRouters = [];
|
||||
this._locatorHandlers = new Map();
|
||||
this._browserContext = parent;
|
||||
this._timeoutSettings = new _timeoutSettings.TimeoutSettings(this._browserContext._timeoutSettings);
|
||||
this.accessibility = new _accessibility.Accessibility(this._channel);
|
||||
this.keyboard = new _input.Keyboard(this);
|
||||
this.mouse = new _input.Mouse(this);
|
||||
this.request = this._browserContext.request;
|
||||
this.touchscreen = new _input.Touchscreen(this);
|
||||
this.clock = this._browserContext.clock;
|
||||
this._mainFrame = _frame.Frame.from(initializer.mainFrame);
|
||||
this._mainFrame._page = this;
|
||||
this._frames.add(this._mainFrame);
|
||||
this._viewportSize = initializer.viewportSize || null;
|
||||
this._closed = initializer.isClosed;
|
||||
this._opener = Page.fromNullable(initializer.opener);
|
||||
this._channel.on('bindingCall', ({
|
||||
binding
|
||||
}) => this._onBinding(BindingCall.from(binding)));
|
||||
this._channel.on('close', () => this._onClose());
|
||||
this._channel.on('crash', () => this._onCrash());
|
||||
this._channel.on('download', ({
|
||||
url,
|
||||
suggestedFilename,
|
||||
artifact
|
||||
}) => {
|
||||
const artifactObject = _artifact.Artifact.from(artifact);
|
||||
this.emit(_events.Events.Page.Download, new _download.Download(this, url, suggestedFilename, artifactObject));
|
||||
});
|
||||
this._channel.on('fileChooser', ({
|
||||
element,
|
||||
isMultiple
|
||||
}) => this.emit(_events.Events.Page.FileChooser, new _fileChooser.FileChooser(this, _elementHandle.ElementHandle.from(element), isMultiple)));
|
||||
this._channel.on('frameAttached', ({
|
||||
frame
|
||||
}) => this._onFrameAttached(_frame.Frame.from(frame)));
|
||||
this._channel.on('frameDetached', ({
|
||||
frame
|
||||
}) => this._onFrameDetached(_frame.Frame.from(frame)));
|
||||
this._channel.on('locatorHandlerTriggered', ({
|
||||
uid
|
||||
}) => this._onLocatorHandlerTriggered(uid));
|
||||
this._channel.on('route', ({
|
||||
route
|
||||
}) => this._onRoute(_network.Route.from(route)));
|
||||
this._channel.on('webSocketRoute', ({
|
||||
webSocketRoute
|
||||
}) => this._onWebSocketRoute(_network.WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on('video', ({
|
||||
artifact
|
||||
}) => {
|
||||
const artifactObject = _artifact.Artifact.from(artifact);
|
||||
this._forceVideo()._artifactReady(artifactObject);
|
||||
});
|
||||
this._channel.on('webSocket', ({
|
||||
webSocket
|
||||
}) => this.emit(_events.Events.Page.WebSocket, _network.WebSocket.from(webSocket)));
|
||||
this._channel.on('worker', ({
|
||||
worker
|
||||
}) => this._onWorker(_worker.Worker.from(worker)));
|
||||
this.coverage = new _coverage.Coverage(this._channel);
|
||||
this.once(_events.Events.Page.Close, () => this._closedOrCrashedScope.close(this._closeErrorWithReason()));
|
||||
this.once(_events.Events.Page.Crash, () => this._closedOrCrashedScope.close(new _errors.TargetClosedError()));
|
||||
this._setEventToSubscriptionMapping(new Map([[_events.Events.Page.Console, 'console'], [_events.Events.Page.Dialog, 'dialog'], [_events.Events.Page.Request, 'request'], [_events.Events.Page.Response, 'response'], [_events.Events.Page.RequestFinished, 'requestFinished'], [_events.Events.Page.RequestFailed, 'requestFailed'], [_events.Events.Page.FileChooser, 'fileChooser']]));
|
||||
}
|
||||
_onFrameAttached(frame) {
|
||||
frame._page = this;
|
||||
this._frames.add(frame);
|
||||
if (frame._parentFrame) frame._parentFrame._childFrames.add(frame);
|
||||
this.emit(_events.Events.Page.FrameAttached, frame);
|
||||
}
|
||||
_onFrameDetached(frame) {
|
||||
this._frames.delete(frame);
|
||||
frame._detached = true;
|
||||
if (frame._parentFrame) frame._parentFrame._childFrames.delete(frame);
|
||||
this.emit(_events.Events.Page.FrameDetached, frame);
|
||||
}
|
||||
async _onRoute(route) {
|
||||
route._context = this.context();
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
// If the page was closed we stall all requests right away.
|
||||
if (this._closeWasCalled || this._browserContext._closeWasCalled) return;
|
||||
if (!routeHandler.matches(route.request().url())) continue;
|
||||
const index = this._routes.indexOf(routeHandler);
|
||||
if (index === -1) continue;
|
||||
if (routeHandler.willExpire()) this._routes.splice(index, 1);
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length) this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
|
||||
if (handled) return;
|
||||
}
|
||||
await this._browserContext._onRoute(route);
|
||||
}
|
||||
async _onWebSocketRoute(webSocketRoute) {
|
||||
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
|
||||
if (routeHandler) await routeHandler.handle(webSocketRoute);else await this._browserContext._onWebSocketRoute(webSocketRoute);
|
||||
}
|
||||
async _onBinding(bindingCall) {
|
||||
const func = this._bindings.get(bindingCall._initializer.name);
|
||||
if (func) {
|
||||
await bindingCall.call(func);
|
||||
return;
|
||||
}
|
||||
await this._browserContext._onBinding(bindingCall);
|
||||
}
|
||||
_onWorker(worker) {
|
||||
this._workers.add(worker);
|
||||
worker._page = this;
|
||||
this.emit(_events.Events.Page.Worker, worker);
|
||||
}
|
||||
_onClose() {
|
||||
this._closed = true;
|
||||
this._browserContext._pages.delete(this);
|
||||
this._browserContext._backgroundPages.delete(this);
|
||||
this._disposeHarRouters();
|
||||
this.emit(_events.Events.Page.Close, this);
|
||||
}
|
||||
_onCrash() {
|
||||
this.emit(_events.Events.Page.Crash, this);
|
||||
}
|
||||
context() {
|
||||
return this._browserContext;
|
||||
}
|
||||
async opener() {
|
||||
if (!this._opener || this._opener.isClosed()) return null;
|
||||
return this._opener;
|
||||
}
|
||||
mainFrame() {
|
||||
return this._mainFrame;
|
||||
}
|
||||
frame(frameSelector) {
|
||||
const name = (0, _utils.isString)(frameSelector) ? frameSelector : frameSelector.name;
|
||||
const url = (0, _utils.isObject)(frameSelector) ? frameSelector.url : undefined;
|
||||
(0, _utils.assert)(name || url, 'Either name or url matcher should be specified');
|
||||
return this.frames().find(f => {
|
||||
if (name) return f.name() === name;
|
||||
return (0, _utils.urlMatches)(this._browserContext._options.baseURL, f.url(), url);
|
||||
}) || null;
|
||||
}
|
||||
frames() {
|
||||
return [...this._frames];
|
||||
}
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
this._wrapApiCall(async () => {
|
||||
this._channel.setDefaultNavigationTimeoutNoReply({
|
||||
timeout
|
||||
}).catch(() => {});
|
||||
}, true);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
this._wrapApiCall(async () => {
|
||||
this._channel.setDefaultTimeoutNoReply({
|
||||
timeout
|
||||
}).catch(() => {});
|
||||
}, true);
|
||||
}
|
||||
_forceVideo() {
|
||||
if (!this._video) this._video = new _video.Video(this, this._connection);
|
||||
return this._video;
|
||||
}
|
||||
video() {
|
||||
// Note: we are creating Video object lazily, because we do not know
|
||||
// BrowserContextOptions when constructing the page - it is assigned
|
||||
// too late during launchPersistentContext.
|
||||
if (!this._browserContext._options.recordVideo) return null;
|
||||
return this._forceVideo();
|
||||
}
|
||||
async $(selector, options) {
|
||||
return await this._mainFrame.$(selector, options);
|
||||
}
|
||||
async waitForSelector(selector, options) {
|
||||
return await this._mainFrame.waitForSelector(selector, options);
|
||||
}
|
||||
async dispatchEvent(selector, type, eventInit, options) {
|
||||
return await this._mainFrame.dispatchEvent(selector, type, eventInit, options);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
return await this._mainFrame.evaluateHandle(pageFunction, arg);
|
||||
}
|
||||
async $eval(selector, pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
return await this._mainFrame.$eval(selector, pageFunction, arg);
|
||||
}
|
||||
async $$eval(selector, pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
return await this._mainFrame.$$eval(selector, pageFunction, arg);
|
||||
}
|
||||
async $$(selector) {
|
||||
return await this._mainFrame.$$(selector);
|
||||
}
|
||||
async addScriptTag(options = {}) {
|
||||
return await this._mainFrame.addScriptTag(options);
|
||||
}
|
||||
async addStyleTag(options = {}) {
|
||||
return await this._mainFrame.addStyleTag(options);
|
||||
}
|
||||
async exposeFunction(name, callback) {
|
||||
await this._channel.exposeBinding({
|
||||
name
|
||||
});
|
||||
const binding = (source, ...args) => callback(...args);
|
||||
this._bindings.set(name, binding);
|
||||
}
|
||||
async exposeBinding(name, callback, options = {}) {
|
||||
await this._channel.exposeBinding({
|
||||
name,
|
||||
needsHandle: options.handle
|
||||
});
|
||||
this._bindings.set(name, callback);
|
||||
}
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
(0, _network.validateHeaders)(headers);
|
||||
await this._channel.setExtraHTTPHeaders({
|
||||
headers: (0, _utils.headersObjectToArray)(headers)
|
||||
});
|
||||
}
|
||||
url() {
|
||||
return this._mainFrame.url();
|
||||
}
|
||||
async content() {
|
||||
return await this._mainFrame.content();
|
||||
}
|
||||
async setContent(html, options) {
|
||||
return await this._mainFrame.setContent(html, options);
|
||||
}
|
||||
async goto(url, options) {
|
||||
return await this._mainFrame.goto(url, options);
|
||||
}
|
||||
async reload(options = {}) {
|
||||
const waitUntil = (0, _frame.verifyLoadState)('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
return _network.Response.fromNullable((await this._channel.reload({
|
||||
...options,
|
||||
waitUntil
|
||||
})).response);
|
||||
}
|
||||
async addLocatorHandler(locator, handler, options = {}) {
|
||||
if (locator._frame !== this._mainFrame) throw new Error(`Locator must belong to the main frame of this page`);
|
||||
if (options.times === 0) return;
|
||||
const {
|
||||
uid
|
||||
} = await this._channel.registerLocatorHandler({
|
||||
selector: locator._selector,
|
||||
noWaitAfter: options.noWaitAfter
|
||||
});
|
||||
this._locatorHandlers.set(uid, {
|
||||
locator,
|
||||
handler,
|
||||
times: options.times
|
||||
});
|
||||
}
|
||||
async _onLocatorHandlerTriggered(uid) {
|
||||
let remove = false;
|
||||
try {
|
||||
const handler = this._locatorHandlers.get(uid);
|
||||
if (handler && handler.times !== 0) {
|
||||
if (handler.times !== undefined) handler.times--;
|
||||
await handler.handler(handler.locator);
|
||||
}
|
||||
remove = (handler === null || handler === void 0 ? void 0 : handler.times) === 0;
|
||||
} finally {
|
||||
if (remove) this._locatorHandlers.delete(uid);
|
||||
this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({
|
||||
uid,
|
||||
remove
|
||||
}), true).catch(() => {});
|
||||
}
|
||||
}
|
||||
async removeLocatorHandler(locator) {
|
||||
for (const [uid, data] of this._locatorHandlers) {
|
||||
if (data.locator._equals(locator)) {
|
||||
this._locatorHandlers.delete(uid);
|
||||
await this._channel.unregisterLocatorHandler({
|
||||
uid
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
async waitForLoadState(state, options) {
|
||||
return await this._mainFrame.waitForLoadState(state, options);
|
||||
}
|
||||
async waitForNavigation(options) {
|
||||
return await this._mainFrame.waitForNavigation(options);
|
||||
}
|
||||
async waitForURL(url, options) {
|
||||
return await this._mainFrame.waitForURL(url, options);
|
||||
}
|
||||
async waitForRequest(urlOrPredicate, options = {}) {
|
||||
const predicate = async request => {
|
||||
if ((0, _utils.isString)(urlOrPredicate) || (0, _utils.isRegExp)(urlOrPredicate)) return (0, _utils.urlMatches)(this._browserContext._options.baseURL, request.url(), urlOrPredicate);
|
||||
return await urlOrPredicate(request);
|
||||
};
|
||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||
const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : undefined;
|
||||
return await this._waitForEvent(_events.Events.Page.Request, {
|
||||
predicate,
|
||||
timeout: options.timeout
|
||||
}, logLine);
|
||||
}
|
||||
async waitForResponse(urlOrPredicate, options = {}) {
|
||||
const predicate = async response => {
|
||||
if ((0, _utils.isString)(urlOrPredicate) || (0, _utils.isRegExp)(urlOrPredicate)) return (0, _utils.urlMatches)(this._browserContext._options.baseURL, response.url(), urlOrPredicate);
|
||||
return await urlOrPredicate(response);
|
||||
};
|
||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||
const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : undefined;
|
||||
return await this._waitForEvent(_events.Events.Page.Response, {
|
||||
predicate,
|
||||
timeout: options.timeout
|
||||
}, logLine);
|
||||
}
|
||||
async waitForEvent(event, optionsOrPredicate = {}) {
|
||||
return await this._waitForEvent(event, optionsOrPredicate, `waiting for event "${event}"`);
|
||||
}
|
||||
_closeErrorWithReason() {
|
||||
return new _errors.TargetClosedError(this._closeReason || this._browserContext._effectiveCloseReason());
|
||||
}
|
||||
async _waitForEvent(event, optionsOrPredicate, logLine) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = _waiter.Waiter.createForEvent(this, event);
|
||||
if (logLine) waiter.log(logLine);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== _events.Events.Page.Crash) waiter.rejectOnEvent(this, _events.Events.Page.Crash, new Error('Page crashed'));
|
||||
if (event !== _events.Events.Page.Close) waiter.rejectOnEvent(this, _events.Events.Page.Close, () => this._closeErrorWithReason());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
async goBack(options = {}) {
|
||||
const waitUntil = (0, _frame.verifyLoadState)('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
return _network.Response.fromNullable((await this._channel.goBack({
|
||||
...options,
|
||||
waitUntil
|
||||
})).response);
|
||||
}
|
||||
async goForward(options = {}) {
|
||||
const waitUntil = (0, _frame.verifyLoadState)('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
|
||||
return _network.Response.fromNullable((await this._channel.goForward({
|
||||
...options,
|
||||
waitUntil
|
||||
})).response);
|
||||
}
|
||||
async requestGC() {
|
||||
await this._channel.requestGC();
|
||||
}
|
||||
async emulateMedia(options = {}) {
|
||||
await this._channel.emulateMedia({
|
||||
media: options.media === null ? 'no-override' : options.media,
|
||||
colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme,
|
||||
reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion,
|
||||
forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors
|
||||
});
|
||||
}
|
||||
async setViewportSize(viewportSize) {
|
||||
this._viewportSize = viewportSize;
|
||||
await this._channel.setViewportSize({
|
||||
viewportSize
|
||||
});
|
||||
}
|
||||
viewportSize() {
|
||||
return this._viewportSize;
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
return await this._mainFrame.evaluate(pageFunction, arg);
|
||||
}
|
||||
async addInitScript(script, arg) {
|
||||
const source = await (0, _clientHelper.evaluationScript)(script, arg);
|
||||
await this._channel.addInitScript({
|
||||
source
|
||||
});
|
||||
}
|
||||
async route(url, handler, options = {}) {
|
||||
this._routes.unshift(new _network.RouteHandler(this._browserContext._options.baseURL, url, handler, options.times));
|
||||
await this._updateInterceptionPatterns();
|
||||
}
|
||||
async routeFromHAR(har, options = {}) {
|
||||
if (options.update) {
|
||||
await this._browserContext._recordIntoHAR(har, this, options);
|
||||
return;
|
||||
}
|
||||
const harRouter = await _harRouter.HarRouter.create(this._connection.localUtils(), har, options.notFound || 'abort', {
|
||||
urlMatch: options.url
|
||||
});
|
||||
this._harRouters.push(harRouter);
|
||||
await harRouter.addPageRoute(this);
|
||||
}
|
||||
async routeWebSocket(url, handler) {
|
||||
this._webSocketRoutes.unshift(new _network.WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler));
|
||||
await this._updateWebSocketInterceptionPatterns();
|
||||
}
|
||||
_disposeHarRouters() {
|
||||
this._harRouters.forEach(router => router.dispose());
|
||||
this._harRouters = [];
|
||||
}
|
||||
async unrouteAll(options) {
|
||||
await this._unrouteInternal(this._routes, [], options === null || options === void 0 ? void 0 : options.behavior);
|
||||
this._disposeHarRouters();
|
||||
}
|
||||
async unroute(url, handler) {
|
||||
const removed = [];
|
||||
const remaining = [];
|
||||
for (const route of this._routes) {
|
||||
if ((0, _utils.urlMatchesEqual)(route.url, url) && (!handler || route.handler === handler)) removed.push(route);else remaining.push(route);
|
||||
}
|
||||
await this._unrouteInternal(removed, remaining, 'default');
|
||||
}
|
||||
async _unrouteInternal(removed, remaining, behavior) {
|
||||
this._routes = remaining;
|
||||
await this._updateInterceptionPatterns();
|
||||
if (!behavior || behavior === 'default') return;
|
||||
const promises = removed.map(routeHandler => routeHandler.stop(behavior));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
async _updateInterceptionPatterns() {
|
||||
const patterns = _network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||
await this._channel.setNetworkInterceptionPatterns({
|
||||
patterns
|
||||
});
|
||||
}
|
||||
async _updateWebSocketInterceptionPatterns() {
|
||||
const patterns = _network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||
await this._channel.setWebSocketInterceptionPatterns({
|
||||
patterns
|
||||
});
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
const copy = {
|
||||
...options,
|
||||
mask: undefined
|
||||
};
|
||||
if (!copy.type) copy.type = (0, _elementHandle.determineScreenshotType)(options);
|
||||
if (options.mask) {
|
||||
copy.mask = options.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector
|
||||
}));
|
||||
}
|
||||
const result = await this._channel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await (0, _utils.mkdirIfNeeded)(options.path);
|
||||
await _fs.default.promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
async _expectScreenshot(options) {
|
||||
const mask = options !== null && options !== void 0 && options.mask ? options === null || options === void 0 ? void 0 : options.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector
|
||||
})) : undefined;
|
||||
const locator = options.locator ? {
|
||||
frame: options.locator._frame._channel,
|
||||
selector: options.locator._selector
|
||||
} : undefined;
|
||||
return await this._channel.expectScreenshot({
|
||||
...options,
|
||||
isNot: !!options.isNot,
|
||||
locator,
|
||||
mask
|
||||
});
|
||||
}
|
||||
async title() {
|
||||
return await this._mainFrame.title();
|
||||
}
|
||||
async bringToFront() {
|
||||
await this._channel.bringToFront();
|
||||
}
|
||||
async [_Symbol$asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async close(options = {}) {
|
||||
this._closeReason = options.reason;
|
||||
this._closeWasCalled = true;
|
||||
try {
|
||||
if (this._ownedContext) await this._ownedContext.close();else await this._channel.close(options);
|
||||
} catch (e) {
|
||||
if ((0, _errors.isTargetClosedError)(e) && !options.runBeforeUnload) return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
isClosed() {
|
||||
return this._closed;
|
||||
}
|
||||
async click(selector, options) {
|
||||
return await this._mainFrame.click(selector, options);
|
||||
}
|
||||
async dragAndDrop(source, target, options) {
|
||||
return await this._mainFrame.dragAndDrop(source, target, options);
|
||||
}
|
||||
async dblclick(selector, options) {
|
||||
return await this._mainFrame.dblclick(selector, options);
|
||||
}
|
||||
async tap(selector, options) {
|
||||
return await this._mainFrame.tap(selector, options);
|
||||
}
|
||||
async fill(selector, value, options) {
|
||||
return await this._mainFrame.fill(selector, value, options);
|
||||
}
|
||||
locator(selector, options) {
|
||||
return this.mainFrame().locator(selector, options);
|
||||
}
|
||||
getByTestId(testId) {
|
||||
return this.mainFrame().getByTestId(testId);
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.mainFrame().getByAltText(text, options);
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.mainFrame().getByLabel(text, options);
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.mainFrame().getByPlaceholder(text, options);
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.mainFrame().getByText(text, options);
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.mainFrame().getByTitle(text, options);
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.mainFrame().getByRole(role, options);
|
||||
}
|
||||
frameLocator(selector) {
|
||||
return this.mainFrame().frameLocator(selector);
|
||||
}
|
||||
async focus(selector, options) {
|
||||
return await this._mainFrame.focus(selector, options);
|
||||
}
|
||||
async textContent(selector, options) {
|
||||
return await this._mainFrame.textContent(selector, options);
|
||||
}
|
||||
async innerText(selector, options) {
|
||||
return await this._mainFrame.innerText(selector, options);
|
||||
}
|
||||
async innerHTML(selector, options) {
|
||||
return await this._mainFrame.innerHTML(selector, options);
|
||||
}
|
||||
async getAttribute(selector, name, options) {
|
||||
return await this._mainFrame.getAttribute(selector, name, options);
|
||||
}
|
||||
async inputValue(selector, options) {
|
||||
return await this._mainFrame.inputValue(selector, options);
|
||||
}
|
||||
async isChecked(selector, options) {
|
||||
return await this._mainFrame.isChecked(selector, options);
|
||||
}
|
||||
async isDisabled(selector, options) {
|
||||
return await this._mainFrame.isDisabled(selector, options);
|
||||
}
|
||||
async isEditable(selector, options) {
|
||||
return await this._mainFrame.isEditable(selector, options);
|
||||
}
|
||||
async isEnabled(selector, options) {
|
||||
return await this._mainFrame.isEnabled(selector, options);
|
||||
}
|
||||
async isHidden(selector, options) {
|
||||
return await this._mainFrame.isHidden(selector, options);
|
||||
}
|
||||
async isVisible(selector, options) {
|
||||
return await this._mainFrame.isVisible(selector, options);
|
||||
}
|
||||
async hover(selector, options) {
|
||||
return await this._mainFrame.hover(selector, options);
|
||||
}
|
||||
async selectOption(selector, values, options) {
|
||||
return await this._mainFrame.selectOption(selector, values, options);
|
||||
}
|
||||
async setInputFiles(selector, files, options) {
|
||||
return await this._mainFrame.setInputFiles(selector, files, options);
|
||||
}
|
||||
async type(selector, text, options) {
|
||||
return await this._mainFrame.type(selector, text, options);
|
||||
}
|
||||
async press(selector, key, options) {
|
||||
return await this._mainFrame.press(selector, key, options);
|
||||
}
|
||||
async check(selector, options) {
|
||||
return await this._mainFrame.check(selector, options);
|
||||
}
|
||||
async uncheck(selector, options) {
|
||||
return await this._mainFrame.uncheck(selector, options);
|
||||
}
|
||||
async setChecked(selector, checked, options) {
|
||||
return await this._mainFrame.setChecked(selector, checked, options);
|
||||
}
|
||||
async waitForTimeout(timeout) {
|
||||
return await this._mainFrame.waitForTimeout(timeout);
|
||||
}
|
||||
async waitForFunction(pageFunction, arg, options) {
|
||||
return await this._mainFrame.waitForFunction(pageFunction, arg, options);
|
||||
}
|
||||
workers() {
|
||||
return [...this._workers];
|
||||
}
|
||||
async pause(_options) {
|
||||
var _this$_instrumentatio;
|
||||
if (require('inspector').url()) return;
|
||||
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
|
||||
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();
|
||||
this._browserContext.setDefaultNavigationTimeout(0);
|
||||
this._browserContext.setDefaultTimeout(0);
|
||||
(_this$_instrumentatio = this._instrumentation) === null || _this$_instrumentatio === void 0 || _this$_instrumentatio.onWillPause({
|
||||
keepTestTimeout: !!(_options !== null && _options !== void 0 && _options.__testHookKeepTestTimeout)
|
||||
});
|
||||
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause());
|
||||
this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
|
||||
this._browserContext.setDefaultTimeout(defaultTimeout);
|
||||
}
|
||||
async pdf(options = {}) {
|
||||
const transportOptions = {
|
||||
...options
|
||||
};
|
||||
if (transportOptions.margin) transportOptions.margin = {
|
||||
...transportOptions.margin
|
||||
};
|
||||
if (typeof options.width === 'number') transportOptions.width = options.width + 'px';
|
||||
if (typeof options.height === 'number') transportOptions.height = options.height + 'px';
|
||||
for (const margin of ['top', 'right', 'bottom', 'left']) {
|
||||
const index = margin;
|
||||
if (options.margin && typeof options.margin[index] === 'number') transportOptions.margin[index] = transportOptions.margin[index] + 'px';
|
||||
}
|
||||
const result = await this._channel.pdf(transportOptions);
|
||||
if (options.path) {
|
||||
await _fs.default.promises.mkdir(_path.default.dirname(options.path), {
|
||||
recursive: true
|
||||
});
|
||||
await _fs.default.promises.writeFile(options.path, result.pdf);
|
||||
}
|
||||
return result.pdf;
|
||||
}
|
||||
}
|
||||
exports.Page = Page;
|
||||
class BindingCall extends _channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
async call(func) {
|
||||
try {
|
||||
const frame = _frame.Frame.from(this._initializer.frame);
|
||||
const source = {
|
||||
context: frame._page.context(),
|
||||
page: frame._page,
|
||||
frame
|
||||
};
|
||||
let result;
|
||||
if (this._initializer.handle) result = await func(source, _jsHandle.JSHandle.from(this._initializer.handle));else result = await func(source, ...this._initializer.args.map(_jsHandle.parseResult));
|
||||
this._channel.resolve({
|
||||
result: (0, _jsHandle.serializeArgument)(result)
|
||||
}).catch(() => {});
|
||||
} catch (e) {
|
||||
this._channel.reject({
|
||||
error: (0, _errors.serializeError)(e)
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.BindingCall = BindingCall;
|
||||
function trimUrl(param) {
|
||||
if ((0, _utils.isRegExp)(param)) return `/${(0, _utils.trimStringWithEllipsis)(param.source, 50)}/${param.flags}`;
|
||||
if ((0, _utils.isString)(param)) return `"${(0, _utils.trimStringWithEllipsis)(param, 50)}"`;
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Playwright = void 0;
|
||||
var _errors = require("./errors");
|
||||
var _android = require("./android");
|
||||
var _browserType = require("./browserType");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _electron = require("./electron");
|
||||
var _fetch = require("./fetch");
|
||||
var _selectors = require("./selectors");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Playwright extends _channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
var _this$_connection$loc, _this$_connection$loc2;
|
||||
super(parent, type, guid, initializer);
|
||||
this._android = void 0;
|
||||
this._electron = void 0;
|
||||
this._bidiChromium = void 0;
|
||||
this._bidiFirefox = void 0;
|
||||
this.chromium = void 0;
|
||||
this.firefox = void 0;
|
||||
this.webkit = void 0;
|
||||
this.devices = void 0;
|
||||
this.selectors = void 0;
|
||||
this.request = void 0;
|
||||
this.errors = void 0;
|
||||
this.request = new _fetch.APIRequest(this);
|
||||
this.chromium = _browserType.BrowserType.from(initializer.chromium);
|
||||
this.chromium._playwright = this;
|
||||
this.firefox = _browserType.BrowserType.from(initializer.firefox);
|
||||
this.firefox._playwright = this;
|
||||
this.webkit = _browserType.BrowserType.from(initializer.webkit);
|
||||
this.webkit._playwright = this;
|
||||
this._android = _android.Android.from(initializer.android);
|
||||
this._electron = _electron.Electron.from(initializer.electron);
|
||||
this._bidiChromium = _browserType.BrowserType.from(initializer.bidiChromium);
|
||||
this._bidiChromium._playwright = this;
|
||||
this._bidiFirefox = _browserType.BrowserType.from(initializer.bidiFirefox);
|
||||
this._bidiFirefox._playwright = this;
|
||||
this.devices = (_this$_connection$loc = (_this$_connection$loc2 = this._connection.localUtils()) === null || _this$_connection$loc2 === void 0 ? void 0 : _this$_connection$loc2.devices) !== null && _this$_connection$loc !== void 0 ? _this$_connection$loc : {};
|
||||
this.selectors = new _selectors.Selectors();
|
||||
this.errors = {
|
||||
TimeoutError: _errors.TimeoutError
|
||||
};
|
||||
const selectorsOwner = _selectors.SelectorsOwner.from(initializer.selectors);
|
||||
this.selectors._addChannel(selectorsOwner);
|
||||
this._connection.on('close', () => {
|
||||
this.selectors._removeChannel(selectorsOwner);
|
||||
});
|
||||
global._playwrightInstance = this;
|
||||
}
|
||||
_setSelectors(selectors) {
|
||||
const selectorsOwner = _selectors.SelectorsOwner.from(this._initializer.selectors);
|
||||
this.selectors._removeChannel(selectorsOwner);
|
||||
this.selectors = selectors;
|
||||
this.selectors._addChannel(selectorsOwner);
|
||||
}
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
}
|
||||
exports.Playwright = Playwright;
|
||||
@@ -1,67 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.SelectorsOwner = exports.Selectors = void 0;
|
||||
var _clientHelper = require("./clientHelper");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _locator = require("./locator");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Selectors {
|
||||
constructor() {
|
||||
this._channels = new Set();
|
||||
this._registrations = [];
|
||||
}
|
||||
async register(name, script, options = {}) {
|
||||
const source = await (0, _clientHelper.evaluationScript)(script, undefined, false);
|
||||
const params = {
|
||||
...options,
|
||||
name,
|
||||
source
|
||||
};
|
||||
for (const channel of this._channels) await channel._channel.register(params);
|
||||
this._registrations.push(params);
|
||||
}
|
||||
setTestIdAttribute(attributeName) {
|
||||
(0, _locator.setTestIdAttribute)(attributeName);
|
||||
for (const channel of this._channels) channel._channel.setTestIdAttributeName({
|
||||
testIdAttributeName: attributeName
|
||||
}).catch(() => {});
|
||||
}
|
||||
_addChannel(channel) {
|
||||
this._channels.add(channel);
|
||||
for (const params of this._registrations) {
|
||||
// This should not fail except for connection closure, but just in case we catch.
|
||||
channel._channel.register(params).catch(() => {});
|
||||
channel._channel.setTestIdAttributeName({
|
||||
testIdAttributeName: (0, _locator.testIdAttributeName)()
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
_removeChannel(channel) {
|
||||
this._channels.delete(channel);
|
||||
}
|
||||
}
|
||||
exports.Selectors = Selectors;
|
||||
class SelectorsOwner extends _channelOwner.ChannelOwner {
|
||||
static from(browser) {
|
||||
return browser._object;
|
||||
}
|
||||
}
|
||||
exports.SelectorsOwner = SelectorsOwner;
|
||||
@@ -1,54 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Stream = void 0;
|
||||
var _stream = require("stream");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Stream extends _channelOwner.ChannelOwner {
|
||||
static from(Stream) {
|
||||
return Stream._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
stream() {
|
||||
return new StreamImpl(this._channel);
|
||||
}
|
||||
}
|
||||
exports.Stream = Stream;
|
||||
class StreamImpl extends _stream.Readable {
|
||||
constructor(channel) {
|
||||
super();
|
||||
this._channel = void 0;
|
||||
this._channel = channel;
|
||||
}
|
||||
async _read() {
|
||||
const result = await this._channel.read({
|
||||
size: 1024 * 1024
|
||||
});
|
||||
if (result.binary.byteLength) this.push(result.binary);else this.push(null);
|
||||
}
|
||||
_destroy(error, callback) {
|
||||
// Stream might be destroyed after the connection was closed.
|
||||
this._channel.close().catch(e => null);
|
||||
super._destroy(error, callback);
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Tracing = void 0;
|
||||
var _artifact = require("./artifact");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Tracing extends _channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._includeSources = false;
|
||||
this._tracesDir = void 0;
|
||||
this._stacksId = void 0;
|
||||
this._isTracing = false;
|
||||
this.markAsInternalType();
|
||||
}
|
||||
async start(options = {}) {
|
||||
this._includeSources = !!options.sources;
|
||||
await this._channel.tracingStart({
|
||||
name: options.name,
|
||||
snapshots: options.snapshots,
|
||||
screenshots: options.screenshots,
|
||||
live: options._live
|
||||
});
|
||||
const {
|
||||
traceName
|
||||
} = await this._channel.tracingStartChunk({
|
||||
name: options.name,
|
||||
title: options.title
|
||||
});
|
||||
await this._startCollectingStacks(traceName);
|
||||
}
|
||||
async startChunk(options = {}) {
|
||||
const {
|
||||
traceName
|
||||
} = await this._channel.tracingStartChunk(options);
|
||||
await this._startCollectingStacks(traceName);
|
||||
}
|
||||
async group(name, options = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._channel.tracingGroup({
|
||||
name,
|
||||
location: options.location
|
||||
});
|
||||
}, false);
|
||||
}
|
||||
async groupEnd() {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._channel.tracingGroupEnd();
|
||||
}, false);
|
||||
}
|
||||
async _startCollectingStacks(traceName) {
|
||||
if (!this._isTracing) {
|
||||
this._isTracing = true;
|
||||
this._connection.setIsTracing(true);
|
||||
}
|
||||
const result = await this._connection.localUtils()._channel.tracingStarted({
|
||||
tracesDir: this._tracesDir,
|
||||
traceName
|
||||
});
|
||||
this._stacksId = result.stacksId;
|
||||
}
|
||||
async stopChunk(options = {}) {
|
||||
await this._doStopChunk(options.path);
|
||||
}
|
||||
async stop(options = {}) {
|
||||
await this._doStopChunk(options.path);
|
||||
await this._channel.tracingStop();
|
||||
}
|
||||
async _doStopChunk(filePath) {
|
||||
this._resetStackCounter();
|
||||
if (!filePath) {
|
||||
// Not interested in artifacts.
|
||||
await this._channel.tracingStopChunk({
|
||||
mode: 'discard'
|
||||
});
|
||||
if (this._stacksId) await this._connection.localUtils()._channel.traceDiscarded({
|
||||
stacksId: this._stacksId
|
||||
});
|
||||
return;
|
||||
}
|
||||
const isLocal = !this._connection.isRemote();
|
||||
if (isLocal) {
|
||||
const result = await this._channel.tracingStopChunk({
|
||||
mode: 'entries'
|
||||
});
|
||||
await this._connection.localUtils()._channel.zip({
|
||||
zipFile: filePath,
|
||||
entries: result.entries,
|
||||
mode: 'write',
|
||||
stacksId: this._stacksId,
|
||||
includeSources: this._includeSources
|
||||
});
|
||||
return;
|
||||
}
|
||||
const result = await this._channel.tracingStopChunk({
|
||||
mode: 'archive'
|
||||
});
|
||||
|
||||
// The artifact may be missing if the browser closed while stopping tracing.
|
||||
if (!result.artifact) {
|
||||
if (this._stacksId) await this._connection.localUtils()._channel.traceDiscarded({
|
||||
stacksId: this._stacksId
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Save trace to the final local file.
|
||||
const artifact = _artifact.Artifact.from(result.artifact);
|
||||
await artifact.saveAs(filePath);
|
||||
await artifact.delete();
|
||||
await this._connection.localUtils()._channel.zip({
|
||||
zipFile: filePath,
|
||||
entries: [],
|
||||
mode: 'append',
|
||||
stacksId: this._stacksId,
|
||||
includeSources: this._includeSources
|
||||
});
|
||||
}
|
||||
_resetStackCounter() {
|
||||
if (this._isTracing) {
|
||||
this._isTracing = false;
|
||||
this._connection.setIsTracing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.Tracing = Tracing;
|
||||
@@ -1,24 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.kLifecycleEvents = void 0;
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const kLifecycleEvents = exports.kLifecycleEvents = new Set(['load', 'domcontentloaded', 'networkidle', 'commit']);
|
||||
@@ -1,51 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Video = void 0;
|
||||
var _utils = require("../utils");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Video {
|
||||
constructor(page, connection) {
|
||||
this._artifact = null;
|
||||
this._artifactReadyPromise = new _utils.ManualPromise();
|
||||
this._isRemote = false;
|
||||
this._isRemote = connection.isRemote();
|
||||
this._artifact = page._closedOrCrashedScope.safeRace(this._artifactReadyPromise);
|
||||
}
|
||||
_artifactReady(artifact) {
|
||||
this._artifactReadyPromise.resolve(artifact);
|
||||
}
|
||||
async path() {
|
||||
if (this._isRemote) throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
|
||||
const artifact = await this._artifact;
|
||||
if (!artifact) throw new Error('Page did not produce any video frames');
|
||||
return artifact._initializer.absolutePath;
|
||||
}
|
||||
async saveAs(path) {
|
||||
const artifact = await this._artifact;
|
||||
if (!artifact) throw new Error('Page did not produce any video frames');
|
||||
return await artifact.saveAs(path);
|
||||
}
|
||||
async delete() {
|
||||
const artifact = await this._artifact;
|
||||
if (artifact) await artifact.delete();
|
||||
}
|
||||
}
|
||||
exports.Video = Video;
|
||||
@@ -1,162 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Waiter = void 0;
|
||||
var _stackTrace = require("../utils/stackTrace");
|
||||
var _errors = require("./errors");
|
||||
var _utils = require("../utils");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Waiter {
|
||||
constructor(channelOwner, event) {
|
||||
this._dispose = void 0;
|
||||
this._failures = [];
|
||||
this._immediateError = void 0;
|
||||
this._logs = [];
|
||||
this._channelOwner = void 0;
|
||||
this._waitId = void 0;
|
||||
this._error = void 0;
|
||||
this._savedZone = void 0;
|
||||
this._waitId = (0, _utils.createGuid)();
|
||||
this._channelOwner = channelOwner;
|
||||
this._savedZone = _utils.zones.currentZone();
|
||||
this._channelOwner._channel.waitForEventInfo({
|
||||
info: {
|
||||
waitId: this._waitId,
|
||||
phase: 'before',
|
||||
event
|
||||
}
|
||||
}).catch(() => {});
|
||||
this._dispose = [() => this._channelOwner._wrapApiCall(async () => {
|
||||
await this._channelOwner._channel.waitForEventInfo({
|
||||
info: {
|
||||
waitId: this._waitId,
|
||||
phase: 'after',
|
||||
error: this._error
|
||||
}
|
||||
});
|
||||
}, true).catch(() => {})];
|
||||
}
|
||||
static createForEvent(channelOwner, event) {
|
||||
return new Waiter(channelOwner, event);
|
||||
}
|
||||
async waitForEvent(emitter, event, predicate) {
|
||||
const {
|
||||
promise,
|
||||
dispose
|
||||
} = waitForEvent(emitter, event, this._savedZone, predicate);
|
||||
return await this.waitForPromise(promise, dispose);
|
||||
}
|
||||
rejectOnEvent(emitter, event, error, predicate) {
|
||||
const {
|
||||
promise,
|
||||
dispose
|
||||
} = waitForEvent(emitter, event, this._savedZone, predicate);
|
||||
this._rejectOn(promise.then(() => {
|
||||
throw typeof error === 'function' ? error() : error;
|
||||
}), dispose);
|
||||
}
|
||||
rejectOnTimeout(timeout, message) {
|
||||
if (!timeout) return;
|
||||
const {
|
||||
promise,
|
||||
dispose
|
||||
} = waitForTimeout(timeout);
|
||||
this._rejectOn(promise.then(() => {
|
||||
throw new _errors.TimeoutError(message);
|
||||
}), dispose);
|
||||
}
|
||||
rejectImmediately(error) {
|
||||
this._immediateError = error;
|
||||
}
|
||||
dispose() {
|
||||
for (const dispose of this._dispose) dispose();
|
||||
}
|
||||
async waitForPromise(promise, dispose) {
|
||||
try {
|
||||
if (this._immediateError) throw this._immediateError;
|
||||
const result = await Promise.race([promise, ...this._failures]);
|
||||
if (dispose) dispose();
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (dispose) dispose();
|
||||
this._error = e.message;
|
||||
this.dispose();
|
||||
(0, _stackTrace.rewriteErrorMessage)(e, e.message + formatLogRecording(this._logs));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
log(s) {
|
||||
this._logs.push(s);
|
||||
this._channelOwner._wrapApiCall(async () => {
|
||||
await this._channelOwner._channel.waitForEventInfo({
|
||||
info: {
|
||||
waitId: this._waitId,
|
||||
phase: 'log',
|
||||
message: s
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, true);
|
||||
}
|
||||
_rejectOn(promise, dispose) {
|
||||
this._failures.push(promise);
|
||||
if (dispose) this._dispose.push(dispose);
|
||||
}
|
||||
}
|
||||
exports.Waiter = Waiter;
|
||||
function waitForEvent(emitter, event, savedZone, predicate) {
|
||||
let listener;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
listener = async eventArg => {
|
||||
await savedZone.run(async () => {
|
||||
try {
|
||||
if (predicate && !(await predicate(eventArg))) return;
|
||||
emitter.removeListener(event, listener);
|
||||
resolve(eventArg);
|
||||
} catch (e) {
|
||||
emitter.removeListener(event, listener);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
emitter.addListener(event, listener);
|
||||
});
|
||||
const dispose = () => emitter.removeListener(event, listener);
|
||||
return {
|
||||
promise,
|
||||
dispose
|
||||
};
|
||||
}
|
||||
function waitForTimeout(timeout) {
|
||||
let timeoutId;
|
||||
const promise = new Promise(resolve => timeoutId = setTimeout(resolve, timeout));
|
||||
const dispose = () => clearTimeout(timeoutId);
|
||||
return {
|
||||
promise,
|
||||
dispose
|
||||
};
|
||||
}
|
||||
function formatLogRecording(log) {
|
||||
if (!log.length) return '';
|
||||
const header = ` logs `;
|
||||
const headerLength = 60;
|
||||
const leftLength = (headerLength - header.length) / 2;
|
||||
const rightLength = headerLength - header.length - leftLength;
|
||||
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.WebError = void 0;
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class WebError {
|
||||
constructor(page, error) {
|
||||
this._page = void 0;
|
||||
this._error = void 0;
|
||||
this._page = page;
|
||||
this._error = error;
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
error() {
|
||||
return this._error;
|
||||
}
|
||||
}
|
||||
exports.WebError = WebError;
|
||||
@@ -1,71 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Worker = void 0;
|
||||
var _events = require("./events");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
var _jsHandle = require("./jsHandle");
|
||||
var _utils = require("../utils");
|
||||
var _errors = require("./errors");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class Worker extends _channelOwner.ChannelOwner {
|
||||
static from(worker) {
|
||||
return worker._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._page = void 0;
|
||||
// Set for web workers.
|
||||
this._context = void 0;
|
||||
// Set for service workers.
|
||||
this._closedScope = new _utils.LongStandingScope();
|
||||
this._channel.on('close', () => {
|
||||
if (this._page) this._page._workers.delete(this);
|
||||
if (this._context) this._context._serviceWorkers.delete(this);
|
||||
this.emit(_events.Events.Worker.Close, this);
|
||||
});
|
||||
this.once(_events.Events.Worker.Close, () => {
|
||||
var _this$_page;
|
||||
return this._closedScope.close(((_this$_page = this._page) === null || _this$_page === void 0 ? void 0 : _this$_page._closeErrorWithReason()) || new _errors.TargetClosedError());
|
||||
});
|
||||
}
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpression({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return (0, _jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
(0, _jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpressionHandle({
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === 'function',
|
||||
arg: (0, _jsHandle.serializeArgument)(arg)
|
||||
});
|
||||
return _jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
}
|
||||
exports.Worker = Worker;
|
||||
@@ -1,54 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.WritableStream = void 0;
|
||||
var _stream = require("stream");
|
||||
var _channelOwner = require("./channelOwner");
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
class WritableStream extends _channelOwner.ChannelOwner {
|
||||
static from(Stream) {
|
||||
return Stream._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
stream() {
|
||||
return new WritableStreamImpl(this._channel);
|
||||
}
|
||||
}
|
||||
exports.WritableStream = WritableStream;
|
||||
class WritableStreamImpl extends _stream.Writable {
|
||||
constructor(channel) {
|
||||
super();
|
||||
this._channel = void 0;
|
||||
this._channel = channel;
|
||||
}
|
||||
async _write(chunk, encoding, callback) {
|
||||
const error = await this._channel.write({
|
||||
binary: typeof chunk === 'string' ? Buffer.from(chunk) : chunk
|
||||
}).catch(e => e);
|
||||
callback(error || null);
|
||||
}
|
||||
async _final(callback) {
|
||||
// Stream might be destroyed after the connection was closed.
|
||||
const error = await this._channel.close().catch(e => e);
|
||||
callback(error || null);
|
||||
}
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.SocksProxyHandler = exports.SocksProxy = void 0;
|
||||
exports.parsePattern = parsePattern;
|
||||
var _events = _interopRequireDefault(require("events"));
|
||||
var _net = _interopRequireDefault(require("net"));
|
||||
var _debugLogger = require("../utils/debugLogger");
|
||||
var _happyEyeballs = require("../utils/happy-eyeballs");
|
||||
var _utils = require("../utils");
|
||||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
// https://tools.ietf.org/html/rfc1928
|
||||
var SocksAuth = /*#__PURE__*/function (SocksAuth) {
|
||||
SocksAuth[SocksAuth["NO_AUTHENTICATION_REQUIRED"] = 0] = "NO_AUTHENTICATION_REQUIRED";
|
||||
SocksAuth[SocksAuth["GSSAPI"] = 1] = "GSSAPI";
|
||||
SocksAuth[SocksAuth["USERNAME_PASSWORD"] = 2] = "USERNAME_PASSWORD";
|
||||
SocksAuth[SocksAuth["NO_ACCEPTABLE_METHODS"] = 255] = "NO_ACCEPTABLE_METHODS";
|
||||
return SocksAuth;
|
||||
}(SocksAuth || {});
|
||||
var SocksAddressType = /*#__PURE__*/function (SocksAddressType) {
|
||||
SocksAddressType[SocksAddressType["IPv4"] = 1] = "IPv4";
|
||||
SocksAddressType[SocksAddressType["FqName"] = 3] = "FqName";
|
||||
SocksAddressType[SocksAddressType["IPv6"] = 4] = "IPv6";
|
||||
return SocksAddressType;
|
||||
}(SocksAddressType || {});
|
||||
var SocksCommand = /*#__PURE__*/function (SocksCommand) {
|
||||
SocksCommand[SocksCommand["CONNECT"] = 1] = "CONNECT";
|
||||
SocksCommand[SocksCommand["BIND"] = 2] = "BIND";
|
||||
SocksCommand[SocksCommand["UDP_ASSOCIATE"] = 3] = "UDP_ASSOCIATE";
|
||||
return SocksCommand;
|
||||
}(SocksCommand || {});
|
||||
var SocksReply = /*#__PURE__*/function (SocksReply) {
|
||||
SocksReply[SocksReply["Succeeded"] = 0] = "Succeeded";
|
||||
SocksReply[SocksReply["GeneralServerFailure"] = 1] = "GeneralServerFailure";
|
||||
SocksReply[SocksReply["NotAllowedByRuleSet"] = 2] = "NotAllowedByRuleSet";
|
||||
SocksReply[SocksReply["NetworkUnreachable"] = 3] = "NetworkUnreachable";
|
||||
SocksReply[SocksReply["HostUnreachable"] = 4] = "HostUnreachable";
|
||||
SocksReply[SocksReply["ConnectionRefused"] = 5] = "ConnectionRefused";
|
||||
SocksReply[SocksReply["TtlExpired"] = 6] = "TtlExpired";
|
||||
SocksReply[SocksReply["CommandNotSupported"] = 7] = "CommandNotSupported";
|
||||
SocksReply[SocksReply["AddressTypeNotSupported"] = 8] = "AddressTypeNotSupported";
|
||||
return SocksReply;
|
||||
}(SocksReply || {});
|
||||
class SocksConnection {
|
||||
constructor(uid, socket, client) {
|
||||
this._buffer = Buffer.from([]);
|
||||
this._offset = 0;
|
||||
this._fence = 0;
|
||||
this._fenceCallback = void 0;
|
||||
this._socket = void 0;
|
||||
this._boundOnData = void 0;
|
||||
this._uid = void 0;
|
||||
this._client = void 0;
|
||||
this._uid = uid;
|
||||
this._socket = socket;
|
||||
this._client = client;
|
||||
this._boundOnData = this._onData.bind(this);
|
||||
socket.on('data', this._boundOnData);
|
||||
socket.on('close', () => this._onClose());
|
||||
socket.on('end', () => this._onClose());
|
||||
socket.on('error', () => this._onClose());
|
||||
this._run().catch(() => this._socket.end());
|
||||
}
|
||||
async _run() {
|
||||
(0, _utils.assert)(await this._authenticate());
|
||||
const {
|
||||
command,
|
||||
host,
|
||||
port
|
||||
} = await this._parseRequest();
|
||||
if (command !== SocksCommand.CONNECT) {
|
||||
this._writeBytes(Buffer.from([0x05, SocksReply.CommandNotSupported, 0x00,
|
||||
// RSV
|
||||
0x01,
|
||||
// IPv4
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
// Address
|
||||
0x00, 0x00 // Port
|
||||
]));
|
||||
return;
|
||||
}
|
||||
this._socket.off('data', this._boundOnData);
|
||||
this._client.onSocketRequested({
|
||||
uid: this._uid,
|
||||
host,
|
||||
port
|
||||
});
|
||||
}
|
||||
async _authenticate() {
|
||||
// Request:
|
||||
// +----+----------+----------+
|
||||
// |VER | NMETHODS | METHODS |
|
||||
// +----+----------+----------+
|
||||
// | 1 | 1 | 1 to 255 |
|
||||
// +----+----------+----------+
|
||||
|
||||
// Response:
|
||||
// +----+--------+
|
||||
// |VER | METHOD |
|
||||
// +----+--------+
|
||||
// | 1 | 1 |
|
||||
// +----+--------+
|
||||
|
||||
const version = await this._readByte();
|
||||
(0, _utils.assert)(version === 0x05, 'The VER field must be set to x05 for this version of the protocol, was ' + version);
|
||||
const nMethods = await this._readByte();
|
||||
(0, _utils.assert)(nMethods, 'No authentication methods specified');
|
||||
const methods = await this._readBytes(nMethods);
|
||||
for (const method of methods) {
|
||||
if (method === 0) {
|
||||
this._writeBytes(Buffer.from([version, method]));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
this._writeBytes(Buffer.from([version, SocksAuth.NO_ACCEPTABLE_METHODS]));
|
||||
return false;
|
||||
}
|
||||
async _parseRequest() {
|
||||
// Request.
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
|
||||
// Response.
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// |VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
// | 1 | 1 | X'00' | 1 | Variable | 2 |
|
||||
// +----+-----+-------+------+----------+----------+
|
||||
|
||||
const version = await this._readByte();
|
||||
(0, _utils.assert)(version === 0x05, 'The VER field must be set to x05 for this version of the protocol, was ' + version);
|
||||
const command = await this._readByte();
|
||||
await this._readByte(); // skip reserved.
|
||||
const addressType = await this._readByte();
|
||||
let host = '';
|
||||
switch (addressType) {
|
||||
case SocksAddressType.IPv4:
|
||||
host = (await this._readBytes(4)).join('.');
|
||||
break;
|
||||
case SocksAddressType.FqName:
|
||||
const length = await this._readByte();
|
||||
host = (await this._readBytes(length)).toString();
|
||||
break;
|
||||
case SocksAddressType.IPv6:
|
||||
const bytes = await this._readBytes(16);
|
||||
const tokens = [];
|
||||
for (let i = 0; i < 8; ++i) tokens.push(bytes.readUInt16BE(i * 2).toString(16));
|
||||
host = tokens.join(':');
|
||||
break;
|
||||
}
|
||||
const port = (await this._readBytes(2)).readUInt16BE(0);
|
||||
this._buffer = Buffer.from([]);
|
||||
this._offset = 0;
|
||||
this._fence = 0;
|
||||
return {
|
||||
command,
|
||||
host,
|
||||
port
|
||||
};
|
||||
}
|
||||
async _readByte() {
|
||||
const buffer = await this._readBytes(1);
|
||||
return buffer[0];
|
||||
}
|
||||
async _readBytes(length) {
|
||||
this._fence = this._offset + length;
|
||||
if (!this._buffer || this._buffer.length < this._fence) await new Promise(f => this._fenceCallback = f);
|
||||
this._offset += length;
|
||||
return this._buffer.slice(this._offset - length, this._offset);
|
||||
}
|
||||
_writeBytes(buffer) {
|
||||
if (this._socket.writable) this._socket.write(buffer);
|
||||
}
|
||||
_onClose() {
|
||||
this._client.onSocketClosed({
|
||||
uid: this._uid
|
||||
});
|
||||
}
|
||||
_onData(buffer) {
|
||||
this._buffer = Buffer.concat([this._buffer, buffer]);
|
||||
if (this._fenceCallback && this._buffer.length >= this._fence) {
|
||||
const callback = this._fenceCallback;
|
||||
this._fenceCallback = undefined;
|
||||
callback();
|
||||
}
|
||||
}
|
||||
socketConnected(host, port) {
|
||||
this._writeBytes(Buffer.from([0x05, SocksReply.Succeeded, 0x00,
|
||||
// RSV
|
||||
...ipToSocksAddress(host),
|
||||
// ATYP, Address
|
||||
port >> 8, port & 0xFF // Port
|
||||
]));
|
||||
this._socket.on('data', data => this._client.onSocketData({
|
||||
uid: this._uid,
|
||||
data
|
||||
}));
|
||||
}
|
||||
socketFailed(errorCode) {
|
||||
const buffer = Buffer.from([0x05, 0, 0x00,
|
||||
// RSV
|
||||
...ipToSocksAddress('0.0.0.0'),
|
||||
// ATYP, Address
|
||||
0, 0 // Port
|
||||
]);
|
||||
switch (errorCode) {
|
||||
case 'ENOENT':
|
||||
case 'ENOTFOUND':
|
||||
case 'ETIMEDOUT':
|
||||
case 'EHOSTUNREACH':
|
||||
buffer[1] = SocksReply.HostUnreachable;
|
||||
break;
|
||||
case 'ENETUNREACH':
|
||||
buffer[1] = SocksReply.NetworkUnreachable;
|
||||
break;
|
||||
case 'ECONNREFUSED':
|
||||
buffer[1] = SocksReply.ConnectionRefused;
|
||||
break;
|
||||
case 'ERULESET':
|
||||
buffer[1] = SocksReply.NotAllowedByRuleSet;
|
||||
break;
|
||||
}
|
||||
this._writeBytes(buffer);
|
||||
this._socket.end();
|
||||
}
|
||||
sendData(data) {
|
||||
this._socket.write(data);
|
||||
}
|
||||
end() {
|
||||
this._socket.end();
|
||||
}
|
||||
error(error) {
|
||||
this._socket.destroy(new Error(error));
|
||||
}
|
||||
}
|
||||
function hexToNumber(hex) {
|
||||
// Note: parseInt has a few issues including ignoring trailing characters and allowing leading 0x.
|
||||
return [...hex].reduce((value, digit) => {
|
||||
const code = digit.charCodeAt(0);
|
||||
if (code >= 48 && code <= 57)
|
||||
// 0..9
|
||||
return value + code;
|
||||
if (code >= 97 && code <= 102)
|
||||
// a..f
|
||||
return value + (code - 97) + 10;
|
||||
if (code >= 65 && code <= 70)
|
||||
// A..F
|
||||
return value + (code - 65) + 10;
|
||||
throw new Error('Invalid IPv6 token ' + hex);
|
||||
}, 0);
|
||||
}
|
||||
function ipToSocksAddress(address) {
|
||||
if (_net.default.isIPv4(address)) {
|
||||
return [0x01,
|
||||
// IPv4
|
||||
...address.split('.', 4).map(t => +t & 0xFF) // Address
|
||||
];
|
||||
}
|
||||
if (_net.default.isIPv6(address)) {
|
||||
const result = [0x04]; // IPv6
|
||||
const tokens = address.split(':', 8);
|
||||
while (tokens.length < 8) tokens.unshift('');
|
||||
for (const token of tokens) {
|
||||
const value = hexToNumber(token);
|
||||
result.push(value >> 8 & 0xFF, value & 0xFF); // Big-endian
|
||||
}
|
||||
return result;
|
||||
}
|
||||
throw new Error('Only IPv4 and IPv6 addresses are supported');
|
||||
}
|
||||
function starMatchToRegex(pattern) {
|
||||
const source = pattern.split('*').map(s => {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
|
||||
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}).join('.*');
|
||||
return new RegExp('^' + source + '$');
|
||||
}
|
||||
|
||||
// This follows "Proxy bypass rules" syntax without implicit and negative rules.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:net/docs/proxy.md;l=331
|
||||
function parsePattern(pattern) {
|
||||
if (!pattern) return () => false;
|
||||
const matchers = pattern.split(',').map(token => {
|
||||
const match = token.match(/^(.*?)(?::(\d+))?$/);
|
||||
if (!match) throw new Error(`Unsupported token "${token}" in pattern "${pattern}"`);
|
||||
const tokenPort = match[2] ? +match[2] : undefined;
|
||||
const portMatches = port => tokenPort === undefined || tokenPort === port;
|
||||
let tokenHost = match[1];
|
||||
if (tokenHost === '<loopback>') {
|
||||
return (host, port) => {
|
||||
if (!portMatches(port)) return false;
|
||||
return host === 'localhost' || host.endsWith('.localhost') || host === '127.0.0.1' || host === '[::1]';
|
||||
};
|
||||
}
|
||||
if (tokenHost === '*') return (host, port) => portMatches(port);
|
||||
if (_net.default.isIPv4(tokenHost) || _net.default.isIPv6(tokenHost)) return (host, port) => host === tokenHost && portMatches(port);
|
||||
if (tokenHost[0] === '.') tokenHost = '*' + tokenHost;
|
||||
const tokenRegex = starMatchToRegex(tokenHost);
|
||||
return (host, port) => {
|
||||
if (!portMatches(port)) return false;
|
||||
if (_net.default.isIPv4(host) || _net.default.isIPv6(host)) return false;
|
||||
return !!host.match(tokenRegex);
|
||||
};
|
||||
});
|
||||
return (host, port) => matchers.some(matcher => matcher(host, port));
|
||||
}
|
||||
class SocksProxy extends _events.default {
|
||||
constructor() {
|
||||
super();
|
||||
this._server = void 0;
|
||||
this._connections = new Map();
|
||||
this._sockets = new Set();
|
||||
this._closed = false;
|
||||
this._port = void 0;
|
||||
this._patternMatcher = () => false;
|
||||
this._directSockets = new Map();
|
||||
this._server = new _net.default.Server(socket => {
|
||||
const uid = (0, _utils.createGuid)();
|
||||
const connection = new SocksConnection(uid, socket, this);
|
||||
this._connections.set(uid, connection);
|
||||
});
|
||||
this._server.on('connection', socket => {
|
||||
if (this._closed) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
this._sockets.add(socket);
|
||||
socket.once('close', () => this._sockets.delete(socket));
|
||||
});
|
||||
}
|
||||
setPattern(pattern) {
|
||||
try {
|
||||
this._patternMatcher = parsePattern(pattern);
|
||||
} catch (e) {
|
||||
this._patternMatcher = () => false;
|
||||
}
|
||||
}
|
||||
async _handleDirect(request) {
|
||||
try {
|
||||
var _this$_connections$ge4;
|
||||
const socket = await (0, _happyEyeballs.createSocket)(request.host, request.port);
|
||||
socket.on('data', data => {
|
||||
var _this$_connections$ge;
|
||||
return (_this$_connections$ge = this._connections.get(request.uid)) === null || _this$_connections$ge === void 0 ? void 0 : _this$_connections$ge.sendData(data);
|
||||
});
|
||||
socket.on('error', error => {
|
||||
var _this$_connections$ge2;
|
||||
(_this$_connections$ge2 = this._connections.get(request.uid)) === null || _this$_connections$ge2 === void 0 || _this$_connections$ge2.error(error.message);
|
||||
this._directSockets.delete(request.uid);
|
||||
});
|
||||
socket.on('end', () => {
|
||||
var _this$_connections$ge3;
|
||||
(_this$_connections$ge3 = this._connections.get(request.uid)) === null || _this$_connections$ge3 === void 0 || _this$_connections$ge3.end();
|
||||
this._directSockets.delete(request.uid);
|
||||
});
|
||||
const localAddress = socket.localAddress;
|
||||
const localPort = socket.localPort;
|
||||
this._directSockets.set(request.uid, socket);
|
||||
(_this$_connections$ge4 = this._connections.get(request.uid)) === null || _this$_connections$ge4 === void 0 || _this$_connections$ge4.socketConnected(localAddress, localPort);
|
||||
} catch (error) {
|
||||
var _this$_connections$ge5;
|
||||
(_this$_connections$ge5 = this._connections.get(request.uid)) === null || _this$_connections$ge5 === void 0 || _this$_connections$ge5.socketFailed(error.code);
|
||||
}
|
||||
}
|
||||
port() {
|
||||
return this._port;
|
||||
}
|
||||
async listen(port, hostname) {
|
||||
return new Promise(f => {
|
||||
this._server.listen(port, hostname, () => {
|
||||
const port = this._server.address().port;
|
||||
this._port = port;
|
||||
f(port);
|
||||
});
|
||||
});
|
||||
}
|
||||
async close() {
|
||||
if (this._closed) return;
|
||||
this._closed = true;
|
||||
for (const socket of this._sockets) socket.destroy();
|
||||
this._sockets.clear();
|
||||
await new Promise(f => this._server.close(f));
|
||||
}
|
||||
onSocketRequested(payload) {
|
||||
if (!this._patternMatcher(payload.host, payload.port)) {
|
||||
this._handleDirect(payload);
|
||||
return;
|
||||
}
|
||||
this.emit(SocksProxy.Events.SocksRequested, payload);
|
||||
}
|
||||
onSocketData(payload) {
|
||||
const direct = this._directSockets.get(payload.uid);
|
||||
if (direct) {
|
||||
direct.write(payload.data);
|
||||
return;
|
||||
}
|
||||
this.emit(SocksProxy.Events.SocksData, payload);
|
||||
}
|
||||
onSocketClosed(payload) {
|
||||
const direct = this._directSockets.get(payload.uid);
|
||||
if (direct) {
|
||||
direct.destroy();
|
||||
this._directSockets.delete(payload.uid);
|
||||
return;
|
||||
}
|
||||
this.emit(SocksProxy.Events.SocksClosed, payload);
|
||||
}
|
||||
socketConnected({
|
||||
uid,
|
||||
host,
|
||||
port
|
||||
}) {
|
||||
var _this$_connections$ge6;
|
||||
(_this$_connections$ge6 = this._connections.get(uid)) === null || _this$_connections$ge6 === void 0 || _this$_connections$ge6.socketConnected(host, port);
|
||||
}
|
||||
socketFailed({
|
||||
uid,
|
||||
errorCode
|
||||
}) {
|
||||
var _this$_connections$ge7;
|
||||
(_this$_connections$ge7 = this._connections.get(uid)) === null || _this$_connections$ge7 === void 0 || _this$_connections$ge7.socketFailed(errorCode);
|
||||
}
|
||||
sendSocketData({
|
||||
uid,
|
||||
data
|
||||
}) {
|
||||
var _this$_connections$ge8;
|
||||
(_this$_connections$ge8 = this._connections.get(uid)) === null || _this$_connections$ge8 === void 0 || _this$_connections$ge8.sendData(data);
|
||||
}
|
||||
sendSocketEnd({
|
||||
uid
|
||||
}) {
|
||||
var _this$_connections$ge9;
|
||||
(_this$_connections$ge9 = this._connections.get(uid)) === null || _this$_connections$ge9 === void 0 || _this$_connections$ge9.end();
|
||||
}
|
||||
sendSocketError({
|
||||
uid,
|
||||
error
|
||||
}) {
|
||||
var _this$_connections$ge10;
|
||||
(_this$_connections$ge10 = this._connections.get(uid)) === null || _this$_connections$ge10 === void 0 || _this$_connections$ge10.error(error);
|
||||
}
|
||||
}
|
||||
exports.SocksProxy = SocksProxy;
|
||||
SocksProxy.Events = {
|
||||
SocksRequested: 'socksRequested',
|
||||
SocksData: 'socksData',
|
||||
SocksClosed: 'socksClosed'
|
||||
};
|
||||
class SocksProxyHandler extends _events.default {
|
||||
constructor(pattern, redirectPortForTest) {
|
||||
super();
|
||||
this._sockets = new Map();
|
||||
this._patternMatcher = () => false;
|
||||
this._redirectPortForTest = void 0;
|
||||
this._patternMatcher = parsePattern(pattern);
|
||||
this._redirectPortForTest = redirectPortForTest;
|
||||
}
|
||||
cleanup() {
|
||||
for (const uid of this._sockets.keys()) this.socketClosed({
|
||||
uid
|
||||
});
|
||||
}
|
||||
async socketRequested({
|
||||
uid,
|
||||
host,
|
||||
port
|
||||
}) {
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] => request ${host}:${port}`);
|
||||
if (!this._patternMatcher(host, port)) {
|
||||
const payload = {
|
||||
uid,
|
||||
errorCode: 'ERULESET'
|
||||
};
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] <= pattern error ${payload.errorCode}`);
|
||||
this.emit(SocksProxyHandler.Events.SocksFailed, payload);
|
||||
return;
|
||||
}
|
||||
if (host === 'local.playwright') host = 'localhost';
|
||||
try {
|
||||
if (this._redirectPortForTest) port = this._redirectPortForTest;
|
||||
const socket = await (0, _happyEyeballs.createSocket)(host, port);
|
||||
socket.on('data', data => {
|
||||
const payload = {
|
||||
uid,
|
||||
data
|
||||
};
|
||||
this.emit(SocksProxyHandler.Events.SocksData, payload);
|
||||
});
|
||||
socket.on('error', error => {
|
||||
const payload = {
|
||||
uid,
|
||||
error: error.message
|
||||
};
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] <= network socket error ${payload.error}`);
|
||||
this.emit(SocksProxyHandler.Events.SocksError, payload);
|
||||
this._sockets.delete(uid);
|
||||
});
|
||||
socket.on('end', () => {
|
||||
const payload = {
|
||||
uid
|
||||
};
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] <= network socket closed`);
|
||||
this.emit(SocksProxyHandler.Events.SocksEnd, payload);
|
||||
this._sockets.delete(uid);
|
||||
});
|
||||
const localAddress = socket.localAddress;
|
||||
const localPort = socket.localPort;
|
||||
this._sockets.set(uid, socket);
|
||||
const payload = {
|
||||
uid,
|
||||
host: localAddress,
|
||||
port: localPort
|
||||
};
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] <= connected to network ${payload.host}:${payload.port}`);
|
||||
this.emit(SocksProxyHandler.Events.SocksConnected, payload);
|
||||
} catch (error) {
|
||||
const payload = {
|
||||
uid,
|
||||
errorCode: error.code
|
||||
};
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] <= connect error ${payload.errorCode}`);
|
||||
this.emit(SocksProxyHandler.Events.SocksFailed, payload);
|
||||
}
|
||||
}
|
||||
sendSocketData({
|
||||
uid,
|
||||
data
|
||||
}) {
|
||||
var _this$_sockets$get;
|
||||
(_this$_sockets$get = this._sockets.get(uid)) === null || _this$_sockets$get === void 0 || _this$_sockets$get.write(data);
|
||||
}
|
||||
socketClosed({
|
||||
uid
|
||||
}) {
|
||||
var _this$_sockets$get2;
|
||||
_debugLogger.debugLogger.log('socks', `[${uid}] <= browser socket closed`);
|
||||
(_this$_sockets$get2 = this._sockets.get(uid)) === null || _this$_sockets$get2 === void 0 || _this$_sockets$get2.destroy();
|
||||
this._sockets.delete(uid);
|
||||
}
|
||||
}
|
||||
exports.SocksProxyHandler = SocksProxyHandler;
|
||||
SocksProxyHandler.Events = {
|
||||
SocksConnected: 'socksConnected',
|
||||
SocksData: 'socksData',
|
||||
SocksError: 'socksError',
|
||||
SocksFailed: 'socksFailed',
|
||||
SocksEnd: 'socksEnd'
|
||||
};
|
||||
@@ -1,73 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.TimeoutSettings = exports.DEFAULT_TIMEOUT = exports.DEFAULT_LAUNCH_TIMEOUT = void 0;
|
||||
var _utils = require("../utils");
|
||||
/**
|
||||
* Copyright 2019 Google Inc. All rights reserved.
|
||||
* Modifications copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const DEFAULT_TIMEOUT = exports.DEFAULT_TIMEOUT = 30000;
|
||||
const DEFAULT_LAUNCH_TIMEOUT = exports.DEFAULT_LAUNCH_TIMEOUT = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
class TimeoutSettings {
|
||||
constructor(parent) {
|
||||
this._parent = void 0;
|
||||
this._defaultTimeout = void 0;
|
||||
this._defaultNavigationTimeout = void 0;
|
||||
this._parent = parent;
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._defaultTimeout = timeout;
|
||||
}
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._defaultNavigationTimeout = timeout;
|
||||
}
|
||||
defaultNavigationTimeout() {
|
||||
return this._defaultNavigationTimeout;
|
||||
}
|
||||
defaultTimeout() {
|
||||
return this._defaultTimeout;
|
||||
}
|
||||
navigationTimeout(options) {
|
||||
if (typeof options.timeout === 'number') return options.timeout;
|
||||
if (this._defaultNavigationTimeout !== undefined) return this._defaultNavigationTimeout;
|
||||
if ((0, _utils.debugMode)()) return 0;
|
||||
if (this._defaultTimeout !== undefined) return this._defaultTimeout;
|
||||
if (this._parent) return this._parent.navigationTimeout(options);
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
timeout(options) {
|
||||
if (typeof options.timeout === 'number') return options.timeout;
|
||||
if ((0, _utils.debugMode)()) return 0;
|
||||
if (this._defaultTimeout !== undefined) return this._defaultTimeout;
|
||||
if (this._parent) return this._parent.timeout(options);
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
static timeout(options) {
|
||||
if (typeof options.timeout === 'number') return options.timeout;
|
||||
if ((0, _utils.debugMode)()) return 0;
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
static launchTimeout(options) {
|
||||
if (typeof options.timeout === 'number') return options.timeout;
|
||||
if ((0, _utils.debugMode)()) return 0;
|
||||
return DEFAULT_LAUNCH_TIMEOUT;
|
||||
}
|
||||
}
|
||||
exports.TimeoutSettings = TimeoutSettings;
|
||||
@@ -1,5 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user