Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cbae31916 | |||
| 9367e358d9 | |||
| 9f1460c9c0 | |||
| 94e536178c | |||
| 456d054229 | |||
| f38a1e3273 | |||
| 03b425a12f | |||
| 8453449833 | |||
| ad986561c9 | |||
| 0d5553f3b2 | |||
| 87bbf158a4 | |||
| f453a95f28 | |||
| d9e98a55d2 | |||
| 99deca3b62 | |||
| 23e64829bb | |||
| cd4c233b60 | |||
| 6c07216c64 | |||
| b23bea6db0 | |||
| cf07356147 | |||
| 39b103a482 | |||
| 4aae2df5b5 | |||
| 3416c242f1 | |||
| 7e31846777 | |||
| ed35362c7a | |||
| 81119035c7 | |||
| 0deef574c3 | |||
| efc4e9dadf | |||
| ca7e905832 | |||
| 32d09b38f1 | |||
| 3cee1307fc | |||
| be89327c01 | |||
| 8f955851e5 | |||
| 972123c7a2 | |||
| 9dd36238bb | |||
| 8ae61b6c78 | |||
| 97745f9a65 | |||
| e124fd5c8b | |||
| 6c2fe6e1c4 | |||
| f625be01a3 | |||
| e6c4cfb38b | |||
| 5b5247624c | |||
| 91a176ce5c | |||
| a7ad0e1de8 | |||
| e4a256a6c4 | |||
| e476b4744d | |||
| 04d16109ae | |||
| f0f3717681 | |||
| e23b006139 | |||
| 0f35946973 | |||
| 19e1ce858f | |||
| 026e646295 | |||
| b7fcefa765 | |||
| 1722cd4124 | |||
| c3742e1585 | |||
| 1a6f855c05 | |||
| d28e639d1b | |||
| 10f668fd73 | |||
| 19b7a9a473 | |||
| 4650ba3d4d | |||
| 1eba50cf0f | |||
| e443457139 | |||
| edf56c1164 | |||
| b9cd693421 | |||
| d77b3778ac | |||
| a7bf97a2df | |||
| 05935b110a | |||
| 64a9c1531b | |||
| f018653c18 | |||
| b7ab85ff92 | |||
| 15b070398b | |||
| 14f220347b | |||
| baec0b33f7 | |||
| ce7b00b68c | |||
| dfb1d34af3 | |||
| c5c1244177 | |||
| 8c86eba4f2 | |||
| d4dddfa727 | |||
| 1bb07162cd | |||
| ec925f9e08 | |||
| 600196f679 | |||
| eb13283e76 | |||
| 30c644a8ec | |||
| 0e480adbf6 | |||
| eaab0af51f | |||
| 51a5268bc2 | |||
| a0bdd2b5b4 | |||
| 21b39161a3 | |||
| b241daf15e | |||
| 25140554ad | |||
| 46cadea367 | |||
| cfe937c0c3 | |||
| 3ad6b0d08f | |||
| fdac0240d1 | |||
| 81dc34bab4 | |||
| b9e9449c8b | |||
| fd38785942 | |||
| 33277de727 | |||
| 4ac62551f4 | |||
| 7fa385aeb8 | |||
| 8452ea3fcd | |||
| 9b34ff564e | |||
| 24f3df1bbc | |||
| 551116d7e5 | |||
| 8768e9813b | |||
| 4a7087cc0c | |||
| 59b152c89f | |||
| 441898b52f | |||
| 3e30397302 | |||
| 31c5746e5b | |||
| 3f9ac27afa | |||
| df504674e9 | |||
| 07796b05c8 | |||
| 2bf8871892 | |||
| 8a0a564885 | |||
| dd4785b048 | |||
| e185e3b7e3 | |||
| 8acbc8605d | |||
| 485f0b69c8 | |||
| f380c152ca | |||
| 79c8c7e6a4 | |||
| 6cf355071b | |||
| ebd474ae81 | |||
| 3c390a2e05 | |||
| 0df2353d4f | |||
| be0a5b26e2 | |||
| 36680eced9 | |||
| 27aa4e0ea6 | |||
| b2d6fae400 | |||
| 3a1928f9bf | |||
| df9863a0bb | |||
| 6cefdff18c | |||
| 91a5dbe30c | |||
| b2a1b9a0be | |||
| 1a44133a63 | |||
| 7020797a25 | |||
| 3b5511a703 | |||
| 8df37ca760 | |||
| 7239f55308 | |||
| 09e077897b | |||
| 051c86810e | |||
| 6721de91e4 | |||
| 226a6237a6 | |||
| cf6acc125f | |||
| f467862877 | |||
| 7ad7d84016 | |||
| 75b0a8afe2 | |||
| 38748c2152 | |||
| 4ec55e7290 | |||
| 3eda91f170 | |||
| cefdf3e35c | |||
| f34ee749be | |||
| 357ef84001 | |||
| 7a1a697dc2 | |||
| 539c6c2559 | |||
| a947494cbd | |||
| 7e79a13cb1 | |||
| 2ad6df1195 | |||
| dc3cd75ea4 | |||
| a73f14fa7f | |||
| 0af31c39b3 | |||
| e1256503be | |||
| b69ff6db3a | |||
| 66231822af | |||
| d5ad9fa073 | |||
| d134dd51e5 | |||
| 1df7c13abd | |||
| 4a8778504f | |||
| f1d7054b3e | |||
| 46b950baf2 | |||
| 4e9c9d321a | |||
| 0c8723ef84 | |||
| 377bb1ce38 | |||
| 2acf54e1a9 | |||
| 0b24c320cd | |||
| 350f2d7658 | |||
| 856d202b78 | |||
| 8caaa84eac | |||
| e70f7ee9f1 | |||
| 6a918c2afc | |||
| 27bfd4db4d | |||
| 787d1504ef | |||
| 726bebdce9 | |||
| 786b78e502 | |||
| cb1b6dceb6 | |||
| fb31fa7eb3 | |||
| 637be701ea | |||
| e9cd67f5d9 | |||
| 433090effd | |||
| 4ca90f561e | |||
| f95397204c | |||
| 31d305b66a | |||
| 42a8c089d5 | |||
| 2c353f2e7f | |||
| c02a5584b4 | |||
| 17da692dce | |||
| 656f830898 | |||
| dde66c807f | |||
| feff0fa73d | |||
| 59beba2e15 | |||
| 959e323f3a | |||
| e2f9e9ae4f | |||
| 328b195127 | |||
| f6d457fe0e | |||
| c65445b94e | |||
| ccb094e57a | |||
| 0204430fa5 | |||
| 4fd9c52aaf | |||
| fde24b09c9 | |||
| a255893ada | |||
| d94612cc9c | |||
| 14026818e2 | |||
| 42eff3357e | |||
| d3a5d827f9 | |||
| 1229081436 | |||
| cf9dcfb4c1 | |||
| a33687f7bd | |||
| 0afb474c3e | |||
| 7e1676cfd7 | |||
| 379b0de885 | |||
| edd7389d7d | |||
| 61866e1d1e | |||
| bc9de38da3 | |||
| 2694863d07 | |||
| 8646fa83c8 | |||
| 796d084ea6 | |||
| 6d23c63912 | |||
| 3803d16731 | |||
| 29fd7163dc | |||
| 9a52e7fae5 | |||
| 0d980e651a | |||
| 3278152d83 | |||
| fc35fd123c | |||
| f40d58ac2e | |||
| fb979bc88d | |||
| 12f784f34c | |||
| 90f93b6e2f | |||
| 135fd6f8d7 | |||
| ff231d9dd2 | |||
| e3c76ce7ce | |||
| 2cfe093780 | |||
| bbedaedeaa | |||
| acbd9f60be | |||
| d3863c713b | |||
| 4085ff7c73 | |||
| 96ae3639ae | |||
| 8c30d5bf5a | |||
| 9292f3169c | |||
| 810d5a5dc1 | |||
| 8a09a91234 | |||
| d8622b3187 | |||
| 2d8827ad5c | |||
| 4d10175ce3 | |||
| 9c4c20e8bd | |||
| ecb285657a | |||
| 0054b7d108 | |||
| 63a85b6ce9 | |||
| 71caa93461 | |||
| 2e73cfab54 | |||
| 74414c6c71 | |||
| a8fb56e8ec | |||
| ca4fb959aa | |||
| 7e0699d5bd | |||
| f383339465 | |||
| 11a1b91be1 | |||
| b0d8d5c612 | |||
| 713efbc2b6 | |||
| c7a60a1fad | |||
| c45a6826bd | |||
| 010d0437c2 | |||
| 20ae11be03 | |||
| 4c070e7487 | |||
| 03d3f57f7b | |||
| 7fe8bc81c6 | |||
| a689b1752d | |||
| 4b845d7a1a | |||
| 56c8b71706 | |||
| 1e22acf1dc | |||
| 676f63e7dc | |||
| d75e5a02ed | |||
| b92a9f78df | |||
| 444748adb7 | |||
| 4c58d57928 | |||
| ee3158b7d5 | |||
| 87a235ed1d | |||
| be5717ebe2 | |||
| 1392ddccda | |||
| 06c914e65c | |||
| 92570bb1f6 | |||
| e447cdc803 | |||
| 2885bc1228 | |||
| aa0efe7c6f | |||
| 7a5fef4741 | |||
| 1088af7697 | |||
| 50b1794799 | |||
| e9e37b0bf7 | |||
| 7de65910e3 | |||
| 145da7b5c4 | |||
| 4182286a31 | |||
| 5e3b0b9ddf | |||
| 3aeec4ffb2 | |||
| 28b7b9f86b | |||
| cf36e41139 | |||
| 97cf6dcbf0 | |||
| 4b65572f6f | |||
| f881b7dd53 | |||
| 1fc79b77fe | |||
| 3ee08b5e43 | |||
| 924748c631 | |||
| 3ae636d771 | |||
| 90f333c8f3 | |||
| 0b6a7a14c4 | |||
| a9048dea2e | |||
| 3ff6a96bc8 | |||
| 8148908a66 | |||
| c18b580ec9 | |||
| a2d48c8b58 | |||
| d9bf80cc9a | |||
| 6569d9c4ea | |||
| 2b89fcf483 | |||
| a9a8ea41c6 | |||
| 0b798cadb4 | |||
| 49e3d73c67 | |||
| 90a06c6acd | |||
| 9221fcc783 | |||
| 167dc0c146 | |||
| ac3e4452b2 | |||
| 3669fda852 | |||
| 8de9cd04b8 | |||
| 296f85e33b | |||
| 900a52f89d | |||
| 73df72ab97 | |||
| 45441c1d07 | |||
| 64e9abceac | |||
| b1337d3b61 | |||
| 8aae30765f | |||
| 4d27a378ac | |||
| 6993c2c462 | |||
| 1cb7a8ca4a | |||
| 90bc0d965f | |||
| 80b0e547cc | |||
| 92dc3ebd08 | |||
| 5631d1d57a | |||
| cad728ba66 | |||
| 0ea192d55b | |||
| 8491b308eb | |||
| 404ab3c45d | |||
| 90a01571e3 | |||
| a4b8ae611a | |||
| 3899860c1f | |||
| f03a198e79 | |||
| cb7bbc37bd | |||
| 931d6d40da |
@@ -1,148 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(dir:*)",
|
||||
"Bash(dotnet restore:*)",
|
||||
"Bash(dotnet clean:*)",
|
||||
"Bash(findstr:*)",
|
||||
"Bash(dotnet ef migrations add:*)",
|
||||
"Bash(dotnet ef migrations remove:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(dotnet ef database update:*)",
|
||||
"Bash(sqlcmd:*)",
|
||||
"Bash(dotnet ef migrations script:*)",
|
||||
"Bash(dotnet run:*)",
|
||||
"Bash(timeout /t 15 dotnet run:*)",
|
||||
"Bash(timeout /t 10 /nobreak)",
|
||||
"Bash(ping:*)",
|
||||
"Bash(start /B dotnet run:*)",
|
||||
"Bash(test:*)",
|
||||
"Bash(dotnet ef migrations:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(xargs -I {} bash -c 'echo \"\"=== {} ===\"\" && head -20 {} | grep -E \"\"class|Authorize\"\"')",
|
||||
"Bash(powershell:*)",
|
||||
"Bash(dotnet tool install:*)",
|
||||
"Bash(dotnet tool update:*)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(powershell -Command \"cd src\\\\PowderCoating.Web; dotnet ef migrations add UpdateQuoteForProspects --project ..\\\\PowderCoating.Infrastructure\")",
|
||||
"Bash(powershell -Command:*)",
|
||||
"Bash(taskkill:*)",
|
||||
"Bash(netstat:*)",
|
||||
"Bash(libman restore:*)",
|
||||
"Bash(./start-app.bat)",
|
||||
"Bash(dotnet-ef migrations add:*)",
|
||||
"Bash(dotnet-ef database update:*)",
|
||||
"Bash(./stop-app.bat)",
|
||||
"Bash(timeout /t 3 /nobreak)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(if [ -f \"stop-app.bat\" ])",
|
||||
"Bash(then cmd.exe /c stop-app.bat)",
|
||||
"Bash(else echo \"stop-app.bat not found\")",
|
||||
"Bash(fi)",
|
||||
"Bash(powershell.exe -Command \"Unblock-File -Path 'src/PowderCoating.Web/dotnet-tools.json'\":*)",
|
||||
"Bash(powershell.exe -Command \"Get-Process | Where-Object {$_ProcessName -like ''*PowderCoating*''} | Stop-Process -Force\")",
|
||||
"Bash(powershell.exe:*)",
|
||||
"Bash(Select-String -Pattern \"error|Error\")",
|
||||
"Bash(Select-String -NotMatch \"warning\")",
|
||||
"Bash(tasklist:*)",
|
||||
"Bash(dotnet add package:*)",
|
||||
"Bash(start-process dotnet run:*)",
|
||||
"Bash(Select-Object -ExpandProperty Id)",
|
||||
"Bash(find:*)",
|
||||
"Bash(cmd.exe:*)",
|
||||
"Bash(dotnet ef dbcontext:*)",
|
||||
"Bash(handle \"PowderCoating.Web.pdb\")",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(del /F \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Web\\\\obj\\\\Debug\\\\net8.0\\\\PowderCoating.Web.pdb\")",
|
||||
"Bash(Select-String -Pattern \"Build succeeded|Build FAILED|error\")",
|
||||
"Bash(Select-Object -Last 10)",
|
||||
"Bash(del \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Infrastructure\\\\Migrations\\\\20260211031319_RemovePreexistingCatalogData.cs\" \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Infrastructure\\\\Migrations\\\\20260211031319_RemovePreexistingCatalogData.Designer.cs\")",
|
||||
"Bash(Select-String:*)",
|
||||
"Bash(Select-Object -Last 5)",
|
||||
"Bash(start-app.bat)",
|
||||
"Bash(dotnet script:*)",
|
||||
"Bash(dotnet list:*)",
|
||||
"Bash(dotnet new:*)",
|
||||
"Bash(stop-app.bat)",
|
||||
"Bash(dotnet watch run:*)",
|
||||
"Bash(cmd /c \"taskkill /F /PID 42108\")",
|
||||
"Bash(cmd /c start-app.bat)",
|
||||
"Bash(\"Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Services/PdfService.cs\":*)",
|
||||
"Bash(/y/PCC/PowderCoatingApp/src/PowderCoating.Application/Services/PdfService.cs:*)",
|
||||
"Bash(/tmp/remove_tempdata.pl:*)",
|
||||
"Bash(chmod:*)",
|
||||
"Bash(perl:*)",
|
||||
"Bash(done)",
|
||||
"Bash(cmd:*)",
|
||||
"Bash(tail:*)",
|
||||
"Bash(del:*)",
|
||||
"Bash(dotnet add:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(Stop-Process:*)",
|
||||
"Bash(mv:*)",
|
||||
"Bash(dotnet tool:*)",
|
||||
"Bash(where libman:*)",
|
||||
"Bash(find \"Y:/PCC/PowderCoatingApp\" -type f \\\\\\( -name \"*template*\" -o -name \"*import*\" -o -name \"*export*\" \\\\\\) -iname \"*.csv\" -o -iname \"*.xlsx\" -o -iname \"*.xls\" 2>/dev/null | head -50)",
|
||||
"Bash(grep -n \"powderCostOverride\\\\|PowderCostOverride\\\\|pageMeta\\\\|quoteItems\\\\|existingItems\" \"Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Quotes/Create.cshtml\" | head -20\ngrep -n \"powderCostOverride\\\\|PowderCostOverride\\\\|pageMeta\\\\|quoteItems\\\\|existingItems\" \"Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Quotes/Edit.cshtml\" 2>/dev/null | head -20)",
|
||||
"Bash(cat /tmp/sdktest/Program.cs | xxd | head -20)",
|
||||
"Bash(cd /tmp/sdktest && rm -rf bin obj && cat Program.cs)",
|
||||
"Bash(cat /tmp/sdktest/Program.cs | xxd | head -5)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:www.nuget.org)",
|
||||
"Bash(wmic process:*)",
|
||||
"Bash(grep -rn \"AI Photo\\\\|ai.*photo\\\\|photo.*quote\\\\|item-type\\\\|AiPhotoQuotes\\\\|ai_photo\" \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Web\\\\Views\\\\Quotes\\\\\" | grep -i \"photo\\\\|ai\" | head -20)",
|
||||
"Bash(sed -i 's|\"aiAnalyzeUrl\": \"@Url.Action\\(\\\\\"AiAnalyzeItem\\\\\", \\\\\"Quotes\\\\\"\\)\",|\"aiAnalyzeUrl\": \"@Url.Action\\(\\\\\"AiAnalyzeItem\\\\\", \\\\\"Quotes\\\\\"\\)\",\\\\n \"aiPhotoQuotesEnabled\": @Json.Serialize\\(\\(bool\\)\\(ViewBag.AiPhotoQuotesEnabled ?? true\\)\\),|g' \\\\\n \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Web\\\\Views\\\\Quotes\\\\Edit.cshtml\" \\\\\n \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Web\\\\Views\\\\Jobs\\\\Create.cshtml\" \\\\\n \"Y:\\\\PCC\\\\PowderCoatingApp\\\\src\\\\PowderCoating.Web\\\\Views\\\\Jobs\\\\Edit.cshtml\")",
|
||||
"Bash(cp:*)",
|
||||
"Bash(dotnet fsi -e \":*)",
|
||||
"Read(//y/tmp/**)",
|
||||
"Bash(cp /c/Users/spoul/.nuget/packages/stripe.net/50.4.1/stripe.net.50.4.1.nupkg stripe.zip)",
|
||||
"Bash(unzip -o stripe.zip *.cs -d stripe_src)",
|
||||
"Bash(dotnet ef:*)",
|
||||
"Bash(Payment)",
|
||||
"Bash(Deposit \")",
|
||||
"Bash(node:*)",
|
||||
"WebFetch(domain:quickbooks.intuit.com)",
|
||||
"WebFetch(domain:www.saasant.com)",
|
||||
"WebFetch(domain:www.liveflow.com)",
|
||||
"WebFetch(domain:www.gentlefrog.com)",
|
||||
"WebFetch(domain:blog.coupler.io)",
|
||||
"WebFetch(domain:litextension.com)",
|
||||
"WebFetch(domain:www.dancingnumbers.com)",
|
||||
"WebFetch(domain:www.bizbooks.pro)",
|
||||
"WebFetch(domain:support.saasant.com)",
|
||||
"WebFetch(domain:support.getcount.com)",
|
||||
"WebFetch(domain:planergy.com)",
|
||||
"WebFetch(domain:www.wizxpert.com)",
|
||||
"WebFetch(domain:www.trykeep.com)",
|
||||
"WebFetch(domain:gentlefrog.com)",
|
||||
"WebFetch(domain:www.syscloud.com)",
|
||||
"WebFetch(domain:interopay.zendesk.com)",
|
||||
"WebFetch(domain:docs.d-tools.cloud)",
|
||||
"WebFetch(domain:paygration.com)",
|
||||
"Bash([ ! -d \"Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/$controller\" ])",
|
||||
"Bash(bash /tmp/check_actions.sh)",
|
||||
"Bash(bash /tmp/verify_endpoints.sh)",
|
||||
"Bash(bash /tmp/verify_services.sh)",
|
||||
"Read(//y/PCC/Deployments/**)",
|
||||
"Bash(mkdir -p \"Y:/PCC/Deployments\")",
|
||||
"Bash(dotnet-script -e \"using System.Reflection; var a = Assembly.LoadFrom\\(\\\\\"Anthropic.SDK.dll\\\\\"\\); var types = a.GetTypes\\(\\).Where\\(t => t.Name.Contains\\(\\\\\"Document\\\\\"\\) || t.Name.Contains\\(\\\\\"Content\\\\\"\\)\\).Select\\(t => t.Name\\).OrderBy\\(n => n\\); foreach\\(var t in types\\) Console.WriteLine\\(t\\);\")",
|
||||
"Bash(sort -t'-' -k3 -r)",
|
||||
"Bash(wsl grep:*)",
|
||||
"Bash(find src:*)",
|
||||
"Bash(dotnet csharp *)",
|
||||
"Read(//c/Users/spoul/.nuget/packages/stripe.net/50.4.1/lib/netstandard2.0/**)",
|
||||
"Bash(dotnet publish *)",
|
||||
"Bash(Compress-Archive -Path * -DestinationPath \"..\\\\deploy.zip\" -Force)",
|
||||
"Bash(az webapp *)",
|
||||
"Read(//y/PCC/**)",
|
||||
"Bash(Get-Date -Format 'yyyyMMdd_HHmmss')",
|
||||
"PowerShell(Get-Content *)",
|
||||
"PowerShell(dotnet build *)",
|
||||
"PowerShell(New-Item *)",
|
||||
"PowerShell(& \"Y:\\\\PCC\\\\PowderCoatingApp\\\\scripts\\\\generate-migration-script.ps1\")",
|
||||
"PowerShell(if \\(Test-Path \"Y:\\\\pcc\\\\deployment\\\\migrations.sql\"\\) { $f = Get-Item \"Y:\\\\pcc\\\\deployment\\\\migrations.sql\"; Write-Host \"File exists: $\\($f.Length\\) bytes\" } else { Write-Host \"File not created\" })"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
# Pre-commit hook: block commits containing corrupted Unicode in .cshtml files.
|
||||
#
|
||||
# All corruption variants start with the UTF-8 byte sequence for a-circumflex
|
||||
# followed by euro-sign (bytes C3 A2 E2 82 AC), which is the first two chars
|
||||
# of every known corruption pattern. Grep for that byte sequence in staged files.
|
||||
|
||||
STAGED=$(git diff --cached --name-only | grep '\.cshtml$')
|
||||
if [ -z "$STAGED" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# $'\xc3\xa2\xe2\x82\xac' = UTF-8 bytes for a-circumflex + euro-sign
|
||||
CORRUPT=$(echo "$STAGED" | xargs grep -l $'\xc3\xa2\xe2\x82\xac' 2>/dev/null)
|
||||
|
||||
if [ -n "$CORRUPT" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Corrupted Unicode characters detected in staged .cshtml files:"
|
||||
echo "$CORRUPT" | sed 's/^/ /'
|
||||
echo ""
|
||||
echo "Fix by running: .\\tools\\Fix-Encoding.ps1"
|
||||
echo "Then re-stage the files and commit again."
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -1,6 +1,11 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# Claude Code tool settings and build logs
|
||||
.claude/settings.local.json
|
||||
.claude/settings.json
|
||||
BuildLog*.txt
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
@@ -129,3 +134,7 @@ DataProtection-Keys/
|
||||
# Secrets
|
||||
appsettings.secrets.json
|
||||
*.pfx
|
||||
|
||||
# Local task tracking
|
||||
TODO.txt
|
||||
TODO.txt.bak
|
||||
|
||||
@@ -0,0 +1,571 @@
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
A production-ready ASP.NET Core 8.0 MVC application for managing powder coating business operations. The application implements Clean Architecture with six projects across three layers (Domain, Application, Infrastructure) plus two presentation layers (Web MVC, RESTful API).
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Building and Running
|
||||
|
||||
```bash
|
||||
# Build entire solution
|
||||
dotnet build
|
||||
|
||||
# Run web application (MVC)
|
||||
cd src/PowderCoating.Web
|
||||
dotnet run
|
||||
# Access at: https://localhost:58461
|
||||
|
||||
# Run web with auto-reload
|
||||
dotnet watch run
|
||||
|
||||
# Run API
|
||||
cd src/PowderCoating.Api
|
||||
dotnet run
|
||||
# Swagger UI at root URL
|
||||
|
||||
# Run tests
|
||||
dotnet test # All tests
|
||||
dotnet test tests/PowderCoating.UnitTests # Unit tests only
|
||||
dotnet test tests/PowderCoating.IntegrationTests # Integration tests only
|
||||
```
|
||||
|
||||
### Database Operations
|
||||
|
||||
```bash
|
||||
# All EF commands run from Web project directory
|
||||
cd src/PowderCoating.Web
|
||||
|
||||
# Create migration (must specify Infrastructure project)
|
||||
dotnet ef migrations add MigrationName --project ../PowderCoating.Infrastructure
|
||||
|
||||
# Apply migrations
|
||||
dotnet ef database update --project ../PowderCoating.Infrastructure
|
||||
|
||||
# Reset database (WARNING: deletes all data)
|
||||
dotnet ef database drop --project ../PowderCoating.Infrastructure
|
||||
dotnet ef database update --project ../PowderCoating.Infrastructure
|
||||
|
||||
# List migrations
|
||||
dotnet ef migrations list --project ../PowderCoating.Infrastructure
|
||||
|
||||
# Remove last migration (if not applied)
|
||||
dotnet ef migrations remove --project ../PowderCoating.Infrastructure
|
||||
```
|
||||
|
||||
### Default Credentials
|
||||
|
||||
```
|
||||
SuperAdmin (break glass): artemis@powdercoatinglogix.com / SuperAdmin123!
|
||||
SuperAdmin (seed): superadmin@powdercoatinglogix.com / SuperAdmin123!
|
||||
SuperAdmin (seed): spouliot@powdercoatinglogix.com / SuperAdmin123!
|
||||
Company Admin (seed): demo@powdercoatinglogix.com / CompanyAdmin123!
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Clean Architecture Layers
|
||||
|
||||
**Domain Layer (PowderCoating.Core)**
|
||||
- Contains business entities, enums, and repository interfaces
|
||||
- `BaseEntity` provides common properties for all entities (Id, CompanyId, CreatedAt, UpdatedAt, IsDeleted, audit fields)
|
||||
- All entities inherit from BaseEntity and support soft delete
|
||||
- No dependencies on other projects
|
||||
|
||||
**Application Layer (PowderCoating.Application)**
|
||||
- DTOs organized by domain (Customer, Job, Equipment, Inventory, Maintenance)
|
||||
- AutoMapper profiles with reverse mappings
|
||||
- Service interfaces (IFileService, etc.)
|
||||
- No UI or infrastructure dependencies
|
||||
|
||||
**Infrastructure Layer (PowderCoating.Infrastructure)**
|
||||
- `ApplicationDbContext` with global query filters for soft deletes and multi-tenancy
|
||||
- Generic `Repository<T>` implementing `IRepository<T>`
|
||||
- `UnitOfWork` implementing `IUnitOfWork` with lazy-loaded repositories
|
||||
- Seed data is triggered **manually** via Platform Management → Seed Data (not automatic on startup)
|
||||
|
||||
**Presentation Layers**
|
||||
- `PowderCoating.Web`: MVC application with Razor views, Bootstrap 5 UI
|
||||
- `PowderCoating.Api`: RESTful API with JWT authentication, Swagger documentation
|
||||
|
||||
### Key Design Patterns
|
||||
|
||||
**Repository Pattern**
|
||||
- Generic `Repository<T>` in Infrastructure
|
||||
- All CRUD operations, search, pagination, eager loading support
|
||||
- Soft delete with `SoftDeleteAsync()` method
|
||||
|
||||
**Unit of Work Pattern**
|
||||
- Coordinates multiple repositories
|
||||
- Transaction support: `BeginTransactionAsync()`, `CommitTransactionAsync()`, `RollbackTransactionAsync()`
|
||||
- Lazy instantiation of repositories
|
||||
- `SaveChangesAsync()` or `CompleteAsync()` to persist changes
|
||||
|
||||
**Dependency Injection**
|
||||
- All dependencies registered in `Program.cs`
|
||||
- Controllers inject `IUnitOfWork` and `IMapper`
|
||||
- Services are scoped to request lifetime
|
||||
|
||||
**Global Query Filters**
|
||||
- Soft deletes: All queries automatically filter `IsDeleted == false`
|
||||
- Multi-tenancy: Non-SuperAdmin users see only their company data
|
||||
- Bypass with `ignoreQueryFilters: true` parameter in repository methods
|
||||
|
||||
### Multi-Tenancy Implementation
|
||||
|
||||
- `CompanyId` foreign key on all business entities
|
||||
- `ITenantContext` injected into DbContext resolves current company
|
||||
- SuperAdmin role can view all companies
|
||||
- Global query filters enforce company isolation at database level
|
||||
- Users have both system role (SuperAdmin) and company role (CompanyAdmin, Manager, Worker, Viewer)
|
||||
|
||||
## Data Access Rules (ENFORCE THESE)
|
||||
|
||||
> **`ApplicationDbContext` is NEVER injected into a controller.**
|
||||
> All data access in controllers goes through `IUnitOfWork`. No exceptions outside the list below.
|
||||
> **This rule is enforced at startup:** `EnforceDataAccessArchitecture()` in `Program.cs` scans all
|
||||
> controllers at boot and throws if any non-exempt controller injects `ApplicationDbContext`.
|
||||
> Full rationale and permanent exceptions list: `docs/DATA_ACCESS_ARCHITECTURE.md`
|
||||
|
||||
### Three tiers — use the right one:
|
||||
|
||||
**Tier 1 — Simple CRUD** → `IUnitOfWork.EntityName` (generic `IRepository<T>`)
|
||||
```csharp
|
||||
var items = await _unitOfWork.CatalogItems.GetAllAsync();
|
||||
await _unitOfWork.Announcements.AddAsync(entity);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
```
|
||||
|
||||
**Tier 2 — Complex domain queries** → typed repositories on `IUnitOfWork`
|
||||
```csharp
|
||||
// Include chains and domain-specific queries belong in the repository, not the controller
|
||||
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id);
|
||||
var invoice = await _unitOfWork.Invoices.LoadForViewAsync(id);
|
||||
var quote = await _unitOfWork.Quotes.GetByApprovalTokenAsync(token);
|
||||
```
|
||||
Typed repositories: `IJobRepository`, `IInvoiceRepository`, `IQuoteRepository`,
|
||||
`ICustomerRepository`, `IBillRepository`, `IPurchaseOrderRepository`
|
||||
— defined in `Core/Interfaces/Repositories/`, implemented in `Infrastructure/Repositories/`
|
||||
|
||||
**Tier 3 — Aggregate/reporting queries** → injected read services
|
||||
```csharp
|
||||
// P&L, AR aging, cycle time, powder usage — shaped DTOs, never tracked entities
|
||||
var aging = await _financialReports.GetArAgingAsync(companyId);
|
||||
```
|
||||
Services: `IFinancialReportService`, `IOperationalReportService`
|
||||
— defined in `Core/Interfaces/Services/`, implemented in `Infrastructure/Services/`
|
||||
|
||||
### Permanent exceptions (ApplicationDbContext allowed — intentional, documented):
|
||||
`StripeWebhookController`, `WebhooksController`, `PaymentController`, `RegistrationController`,
|
||||
`DataExportController`, `AccountDataExportController`, `DataPurgeController`,
|
||||
`SystemInfoController`, `SystemLogsController`, `CompanyHealthController`
|
||||
|
||||
If you think you need a new exception, you almost certainly don't. Check the spec first.
|
||||
|
||||
---
|
||||
|
||||
## Data Access Patterns
|
||||
|
||||
### Common Controller Pattern
|
||||
|
||||
```csharp
|
||||
public class ExampleController : Controller
|
||||
{
|
||||
private readonly IUnitOfWork _unitOfWork;
|
||||
private readonly IMapper _mapper;
|
||||
|
||||
public ExampleController(IUnitOfWork unitOfWork, IMapper mapper)
|
||||
{
|
||||
_unitOfWork = unitOfWork;
|
||||
_mapper = mapper;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var entities = await _unitOfWork.Examples.GetAllAsync();
|
||||
var dtos = _mapper.Map<List<ExampleDto>>(entities);
|
||||
return View(dtos);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Create(CreateExampleDto dto)
|
||||
{
|
||||
var entity = _mapper.Map<Example>(dto);
|
||||
await _unitOfWork.Examples.AddAsync(entity);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
public async Task<IActionResult> Delete(int id)
|
||||
{
|
||||
await _unitOfWork.Examples.SoftDeleteAsync(id);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using Unit of Work Repositories
|
||||
|
||||
All entity repositories are available via `IUnitOfWork` properties:
|
||||
- `_unitOfWork.Customers`
|
||||
- `_unitOfWork.Jobs`
|
||||
- `_unitOfWork.JobItems`
|
||||
- `_unitOfWork.Quotes`
|
||||
- `_unitOfWork.InventoryItems`
|
||||
- `_unitOfWork.Equipment`
|
||||
- `_unitOfWork.MaintenanceRecords`
|
||||
- Plus additional entities (Suppliers, JobPhotos, JobNotes, etc.)
|
||||
|
||||
### Eager Loading Related Data
|
||||
|
||||
```csharp
|
||||
// Load customer with related data
|
||||
var customer = await _unitOfWork.Customers.GetByIdAsync(
|
||||
id,
|
||||
c => c.Jobs,
|
||||
c => c.Quotes,
|
||||
c => c.PricingTier
|
||||
);
|
||||
|
||||
// Find with predicate and includes
|
||||
var activeJobs = await _unitOfWork.Jobs.FindAsync(
|
||||
j => j.Status != JobStatus.Completed,
|
||||
j => j.Customer,
|
||||
j => j.JobItems
|
||||
);
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
```csharp
|
||||
var pagedJobs = await _unitOfWork.Jobs.GetPagedAsync(
|
||||
pageNumber: 1,
|
||||
pageSize: 25,
|
||||
j => j.Status == JobStatus.InPreparation,
|
||||
j => j.Customer
|
||||
);
|
||||
```
|
||||
|
||||
## Important Domain Concepts
|
||||
|
||||
### Job Lifecycle
|
||||
|
||||
Jobs progress through 16 statuses:
|
||||
1. **Pending** → Initial state
|
||||
2. **Quoted** → Quote generated
|
||||
3. **Approved** → Customer approved
|
||||
4. **InPreparation** → Job prep started
|
||||
5. **Sandblasting** → Surface prep
|
||||
6. **MaskingTaping** → Masking areas
|
||||
7. **Cleaning** → Pre-coat cleaning
|
||||
8. **InOven** → Pre-heating
|
||||
9. **Coating** → Applying powder
|
||||
10. **Curing** → Heat curing
|
||||
11. **QualityCheck** → Inspection
|
||||
12. **Completed** → Work finished
|
||||
13. **ReadyForPickup** → Awaiting customer
|
||||
14. **Delivered** → Job delivered
|
||||
15. **OnHold** → Paused
|
||||
16. **Cancelled** → Cancelled
|
||||
|
||||
**Job Priorities**: Low, Normal, High, Urgent, Rush (color-coded in UI)
|
||||
|
||||
### Customer Types
|
||||
|
||||
- **Commercial**: B2B customers with pricing tiers, credit limits
|
||||
- **Non-Commercial**: Individual customers, typically simpler pricing
|
||||
|
||||
### Inventory Management
|
||||
|
||||
**Transaction Types**: Purchase, Sale, Adjustment, Transfer, Return, Waste, Initial
|
||||
- All transactions tracked in `InventoryTransaction` entity
|
||||
- Reorder points trigger low-stock alerts
|
||||
|
||||
### Equipment & Maintenance
|
||||
|
||||
**Equipment Status**: Operational, NeedsMaintenance, UnderMaintenance, OutOfService, Retired
|
||||
**Maintenance Priority**: Low, Normal, High, Critical
|
||||
**Maintenance Status**: Scheduled, InProgress, Completed, Cancelled, Overdue
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### Web Application (src/PowderCoating.Web/appsettings.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=.\\SQLEXPRESS;Database=PowderCoatingDb;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true"
|
||||
},
|
||||
"AppSettings": {
|
||||
"CompanyName": "Powder Coating Logix",
|
||||
"DefaultQuoteValidityDays": 30,
|
||||
"DefaultPaymentTerms": "Net 30",
|
||||
"TaxRate": 0.0,
|
||||
"Currency": "USD",
|
||||
"TrialPeriodDays": 7,
|
||||
"QuoteApprovalTokenDays": 30
|
||||
},
|
||||
"AI": {
|
||||
"Anthropic": {
|
||||
"ApiKey": "your-anthropic-api-key-here"
|
||||
}
|
||||
},
|
||||
"SendGrid": { ... },
|
||||
"Stripe": { ... },
|
||||
"Storage": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**AI uses Anthropic Codex Sonnet 4.6** (`Codex-sonnet-4-6`) — NOT OpenAI. The `AI:Anthropic:ApiKey` config key is what the AI photo quoting and AI scheduling services read.
|
||||
|
||||
### API (src/PowderCoating.Api/appsettings.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"JwtSettings": {
|
||||
"SecretKey": "CHANGE-THIS-TO-YOUR-OWN-SECRET-KEY-AT-LEAST-32-CHARACTERS",
|
||||
"Issuer": "PowderCoatingAPI",
|
||||
"Audience": "PowderCoatingMobileApp",
|
||||
"ExpirationMinutes": 1440
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Launch Settings (src/PowderCoating.Web/Properties/launchSettings.json)
|
||||
|
||||
Default ports:
|
||||
- HTTPS: 58461
|
||||
- HTTP: 58462
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### System Roles
|
||||
- **SuperAdmin**: Platform-wide access, sees all companies and deleted records
|
||||
- **Administrator**: Company admin
|
||||
- **Manager**: Operations management
|
||||
- **Employee**: Create/edit jobs and quotes
|
||||
- **ShopFloor**: Update job status
|
||||
- **ReadOnly**: View-only access
|
||||
|
||||
### Custom Authorization Policies
|
||||
|
||||
Defined in `PowderCoating.Shared/Constants/AppConstants.cs`:
|
||||
- `RequireAdministratorRole`
|
||||
- `CanManageJobs`
|
||||
- `CanManageInventory`
|
||||
- `CanManageUsers`
|
||||
- `CanViewData`
|
||||
|
||||
Apply with `[Authorize(Policy = "PolicyName")]` on controllers/actions.
|
||||
|
||||
### JWT Authentication (API Only)
|
||||
|
||||
API uses JWT Bearer tokens. Web uses cookie-based Identity authentication.
|
||||
|
||||
## AutoMapper Configuration
|
||||
|
||||
AutoMapper is registered as singleton in `Program.cs`:
|
||||
```csharp
|
||||
builder.Services.AddSingleton(provider => new MapperConfiguration(cfg =>
|
||||
{
|
||||
cfg.AddMaps(typeof(ApplicationAssemblyMarker).Assembly);
|
||||
}).CreateMapper());
|
||||
```
|
||||
|
||||
All profiles in `Application/Mappings/` are auto-discovered. Profiles include reverse mappings for entity ↔ DTO conversion.
|
||||
|
||||
## Logging
|
||||
|
||||
Serilog configured to write:
|
||||
- Console (structured logs)
|
||||
- File: `logs/powdercoating-{Date}.txt` (rolling daily)
|
||||
|
||||
Access via constructor injection:
|
||||
```csharp
|
||||
private readonly ILogger<ExampleController> _logger;
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Adding a New Entity
|
||||
|
||||
1. Create entity class in `Core/Entities/` inheriting from `BaseEntity`
|
||||
2. Add DbSet to `ApplicationDbContext`
|
||||
3. Register repository property in `IUnitOfWork` interface
|
||||
4. Add lazy-loaded property in `UnitOfWork` implementation
|
||||
5. Create migration: `dotnet ef migrations add AddEntityName --project ../PowderCoating.Infrastructure`
|
||||
6. Apply migration: `dotnet ef database update --project ../PowderCoating.Infrastructure`
|
||||
|
||||
### Adding a New Controller
|
||||
|
||||
1. Create DTOs in `Application/DTOs/`
|
||||
2. Create AutoMapper profile in `Application/Mappings/`
|
||||
3. Create controller in `Web/Controllers/`
|
||||
4. Create views in `Web/Views/[ControllerName]/`
|
||||
5. Add navigation link in `Views/Shared/_Layout.cshtml`
|
||||
|
||||
### Working with Soft Deletes
|
||||
|
||||
```csharp
|
||||
// Soft delete (sets IsDeleted = true)
|
||||
await _unitOfWork.Customers.SoftDeleteAsync(id);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Physical delete (use sparingly)
|
||||
await _unitOfWork.Customers.DeleteAsync(entity);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
|
||||
// Include deleted records in query
|
||||
var allCustomers = await _unitOfWork.Customers.GetAllAsync(ignoreQueryFilters: true);
|
||||
```
|
||||
|
||||
### Bypassing Multi-Tenancy Filters
|
||||
|
||||
Only for SuperAdmin users:
|
||||
```csharp
|
||||
// See all companies' data
|
||||
var allJobs = await _unitOfWork.Jobs.GetAllAsync(ignoreQueryFilters: true);
|
||||
```
|
||||
|
||||
## Implemented Modules
|
||||
|
||||
All modules below are fully implemented with controllers, views, and migrations applied.
|
||||
|
||||
### Operations
|
||||
- **Jobs** — full lifecycle (16 statuses), worker assignment, time entries, rework tracking, shop access codes, job templates
|
||||
- **Quotes** — multi-item pricing engine, AI Photo Quoting (Anthropic Codex Sonnet 4.6), quote-to-job conversion, customer approval portal, online payment
|
||||
- **Invoices** — create from job, partial payments, voids, PDF download, email send; 1:1 Job→Invoice enforced by unique index
|
||||
- **Deposits** — record against customer/job/quote; auto-applied to invoices on creation; receipt PDF via QuestPDF
|
||||
- **Customers** — commercial and non-commercial types, pricing tiers, tax exempt flag + certificate upload, credit limits
|
||||
- **Oven Scheduler** — batch jobs into named ovens, capacity planning, suggested batches
|
||||
|
||||
### Inventory & Purchasing
|
||||
- **Inventory** — stock tracking, transactions, reorder alerts, powder coverage/efficiency fields
|
||||
- **Vendors** — supplier management, payment terms, linked to inventory items
|
||||
- **Purchase Orders** — create/submit/receive POs, convert to vendor bills
|
||||
- **Accounts Payable** — vendor bills, AP ledger, payment tracking
|
||||
|
||||
### Shop Management
|
||||
- **Shop Workers** — roles (Coater, Sandblaster, etc.), assignment to jobs and maintenance tasks
|
||||
- **Equipment & Maintenance** — equipment status lifecycle, scheduled/completed maintenance records
|
||||
- **Catalog Items** — pre-priced service catalog with default prices
|
||||
- **Pricing Tiers** — customer discount tiers; use `CompanyAdminOnly` policy (not `RequireAdministratorRole`)
|
||||
|
||||
### Billing & Payments
|
||||
- **Stripe** — subscription plans, checkout sessions, customer portal, webhooks (`/stripe/webhook`)
|
||||
- **Stripe Connect** — embedded payments, OAuth flow for tenant onboarding
|
||||
- **Twilio SMS** — `ISmsService` fully implemented; webhook at `POST /Webhooks/TwilioSms`
|
||||
|
||||
### Platform (SuperAdmin only)
|
||||
- **Platform Users** — create/manage SuperAdmin accounts
|
||||
- **Companies** — view/manage all tenant companies
|
||||
- **Seed Data** — manual seeding via Platform Management UI (not automatic)
|
||||
- **Subscription Plans** — `SubscriptionPlanConfig` controls per-plan limits and pricing
|
||||
|
||||
### Other
|
||||
- **Help Center** — 14 fully-written articles at `Views/Help/`
|
||||
- **Setup Wizard** — 10-step onboarding wizard at `SetupWizardController`
|
||||
- **Reports** — 24 report actions including P&L, AR Aging, Powder Usage, Job Cycle Time, PDF exports
|
||||
- **Gift Certificates** — issue, redeem, track balance
|
||||
- **Announcements** — platform-wide announcements to tenants
|
||||
|
||||
### Key Pricing Rules
|
||||
- Custom powder (no inventory item + `PowderToOrder` > 0): charge for the **full ordered quantity**, not just calculated usage
|
||||
- In-stock inventory powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
|
||||
- Tax exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
|
||||
|
||||
### Pricing Routing Flags — Must Stay In Sync Across All Three Layers
|
||||
|
||||
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes each item to the correct pricing path using boolean flags. **These flags MUST exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, AND be mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
|
||||
|
||||
| Flag | Effect if missing on JobItem |
|
||||
|------|------------------------------|
|
||||
| `IsAiItem` | Job repriced as calculated item; oven cost double-charged on every save |
|
||||
| `IsGenericItem` | ManualUnitPrice ignored; price recalculated from surface area |
|
||||
| `IsLaborItem` | Item repriced at surface-area rate instead of hours × labor rate |
|
||||
| `IsSalesItem` | ManualUnitPrice ignored; item repriced using coat/surface math |
|
||||
|
||||
**Checklist when adding a new pricing routing flag:**
|
||||
1. Add the property to `QuoteItem` (Core/Entities)
|
||||
2. Add the property to `JobItem` (Core/Entities)
|
||||
3. Add it to `CreateQuoteItemDto` (Application/DTOs)
|
||||
4. Add it to `JobItemSeed` (private class in JobItemAssemblyService)
|
||||
5. Map it in all three `JobItemAssemblyService.CreateJobItem` overloads
|
||||
6. Include it in every `existingItemsData` JSON block in job views (`Edit.cshtml`, `EditItems.cshtml`) and in all job controller actions that build `CreateQuoteItemDto` from a `JobItem`
|
||||
7. Add a migration if the field is new on a persisted entity
|
||||
8. The structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` in `JobItemAssemblyServiceTests` will fail until steps 1–3 are done — this is intentional
|
||||
|
||||
### Branding
|
||||
- Application name: **Powder Coating Logix**
|
||||
- PCL logo: `wwwroot/images/pcl-logo.png` — used in sidebar header (when no tenant logo), login/register pages, sidebar footer
|
||||
- Sidebar footer always shows PCL logo linking to `http://www.powdercoatinglogix.com`
|
||||
- Tenant companies can upload their own logo (stored in Azure Blob `companylogos` container); it replaces the PCL logo in the sidebar header
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Entity Framework warnings about global query filters on related entities (non-critical, informational only)
|
||||
|
||||
## File Upload Configuration
|
||||
|
||||
Limits defined in `AppConstants.cs`:
|
||||
- Max file size: 10 MB
|
||||
- Allowed extensions: jpg, jpeg, png, gif, pdf, doc, docx, xls, xlsx
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- **Unit Tests**: Test business logic in isolation
|
||||
- **Integration Tests**: Test full request pipeline with test database
|
||||
- Use xUnit framework
|
||||
- Mock `IUnitOfWork` in unit tests
|
||||
|
||||
## Extending the System
|
||||
|
||||
### Adding AI Features
|
||||
|
||||
AI uses Anthropic Codex Sonnet 4.6 via `IAiQuoteService`. Configure the key under `AI:Anthropic:ApiKey` in `appsettings.json`.
|
||||
1. Create service interface in `Application/Interfaces/`
|
||||
2. Implement in `Infrastructure/Services/` calling the Anthropic client
|
||||
3. Inject into controllers via DI
|
||||
|
||||
### SignalR Hubs
|
||||
|
||||
Two hubs are already implemented and mapped in `Program.cs`:
|
||||
- `NotificationHub` → `/hubs/notifications` (company-scoped push notifications)
|
||||
- `ShopHub` → `/hubs/shop` (real-time shop floor updates)
|
||||
|
||||
To add a new hub:
|
||||
1. Create hub class in `Web/Hubs/`
|
||||
2. Map hub in `Program.cs`: `app.MapHub<YourHub>("/hubpath")`
|
||||
3. Use JavaScript client in views to connect
|
||||
|
||||
### Adding API Endpoints
|
||||
|
||||
1. Create controller in `Api/Controllers/` with `[ApiController]` attribute
|
||||
2. Return `ActionResult<T>` types
|
||||
3. Use `[Authorize]` for protected endpoints
|
||||
4. Document with XML comments for Swagger
|
||||
|
||||
## Project Dependencies
|
||||
|
||||
Key NuGet packages:
|
||||
- **AutoMapper 16.0.0**: Entity-to-DTO mapping
|
||||
- **Entity Framework Core 8.0.11**: ORM and database access
|
||||
- **Serilog.AspNetCore 8.0.3**: Structured logging
|
||||
- **Microsoft.AspNetCore.Identity.UI 8.0.11**: Authentication
|
||||
- **Swashbuckle.AspNetCore 7.2.0**: API documentation (API project)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Password requirements: 8+ chars, uppercase, lowercase, digit
|
||||
- HTTPS enforced in production
|
||||
- SQL injection prevented by EF Core parameterization
|
||||
- XSS protection via Razor encoding
|
||||
- CSRF tokens on all forms (automatic with ASP.NET Core)
|
||||
- Sensitive settings (connection strings, API keys) should use User Secrets in development and Azure Key Vault in production
|
||||
|
||||
## Active design work
|
||||
A visual redesign is in progress. If the user asks about UI changes, dashboard/jobs/board styling, or the new design tokens, read `design_handoff_pcl_redesign/README.md` and follow `design_handoff_pcl_redesign/AGENTS.md` for that work.
|
||||
@@ -122,6 +122,51 @@ Company Admin (seed): demo@powdercoatinglogix.com / CompanyAdmin123!
|
||||
- Global query filters enforce company isolation at database level
|
||||
- Users have both system role (SuperAdmin) and company role (CompanyAdmin, Manager, Worker, Viewer)
|
||||
|
||||
## Data Access Rules (ENFORCE THESE)
|
||||
|
||||
> **`ApplicationDbContext` is NEVER injected into a controller.**
|
||||
> All data access in controllers goes through `IUnitOfWork`. No exceptions outside the list below.
|
||||
> **This rule is enforced at startup:** `EnforceDataAccessArchitecture()` in `Program.cs` scans all
|
||||
> controllers at boot and throws if any non-exempt controller injects `ApplicationDbContext`.
|
||||
> Full rationale and permanent exceptions list: `docs/DATA_ACCESS_ARCHITECTURE.md`
|
||||
|
||||
### Three tiers — use the right one:
|
||||
|
||||
**Tier 1 — Simple CRUD** → `IUnitOfWork.EntityName` (generic `IRepository<T>`)
|
||||
```csharp
|
||||
var items = await _unitOfWork.CatalogItems.GetAllAsync();
|
||||
await _unitOfWork.Announcements.AddAsync(entity);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
```
|
||||
|
||||
**Tier 2 — Complex domain queries** → typed repositories on `IUnitOfWork`
|
||||
```csharp
|
||||
// Include chains and domain-specific queries belong in the repository, not the controller
|
||||
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id);
|
||||
var invoice = await _unitOfWork.Invoices.LoadForViewAsync(id);
|
||||
var quote = await _unitOfWork.Quotes.GetByApprovalTokenAsync(token);
|
||||
```
|
||||
Typed repositories: `IJobRepository`, `IInvoiceRepository`, `IQuoteRepository`,
|
||||
`ICustomerRepository`, `IBillRepository`, `IPurchaseOrderRepository`
|
||||
— defined in `Core/Interfaces/Repositories/`, implemented in `Infrastructure/Repositories/`
|
||||
|
||||
**Tier 3 — Aggregate/reporting queries** → injected read services
|
||||
```csharp
|
||||
// P&L, AR aging, cycle time, powder usage — shaped DTOs, never tracked entities
|
||||
var aging = await _financialReports.GetArAgingAsync(companyId);
|
||||
```
|
||||
Services: `IFinancialReportService`, `IOperationalReportService`
|
||||
— defined in `Core/Interfaces/Services/`, implemented in `Infrastructure/Services/`
|
||||
|
||||
### Permanent exceptions (ApplicationDbContext allowed — intentional, documented):
|
||||
`StripeWebhookController`, `WebhooksController`, `PaymentController`, `RegistrationController`,
|
||||
`DataExportController`, `AccountDataExportController`, `DataPurgeController`,
|
||||
`SystemInfoController`, `SystemLogsController`, `CompanyHealthController`
|
||||
|
||||
If you think you need a new exception, you almost certainly don't. Check the spec first.
|
||||
|
||||
---
|
||||
|
||||
## Data Access Patterns
|
||||
|
||||
### Common Controller Pattern
|
||||
@@ -433,6 +478,27 @@ All modules below are fully implemented with controllers, views, and migrations
|
||||
- In-stock inventory powder: charge for calculated usage only (surface area × lbs/sqft × unit cost)
|
||||
- Tax exempt customers (`Customer.IsTaxExempt`): `TaxPercent` defaults to 0 on quote and invoice create; customer dropdown marks exempt customers with ★
|
||||
|
||||
### Pricing Routing Flags — Must Stay In Sync Across All Three Layers
|
||||
|
||||
`PricingCalculationService.CalculateQuoteItemPriceAsync` routes each item to the correct pricing path using boolean flags. **These flags MUST exist identically on `QuoteItem`, `JobItem`, and `CreateQuoteItemDto`, AND be mapped in all three `JobItemAssemblyService.CreateJobItem` overloads.**
|
||||
|
||||
| Flag | Effect if missing on JobItem |
|
||||
|------|------------------------------|
|
||||
| `IsAiItem` | Job repriced as calculated item; oven cost double-charged on every save |
|
||||
| `IsGenericItem` | ManualUnitPrice ignored; price recalculated from surface area |
|
||||
| `IsLaborItem` | Item repriced at surface-area rate instead of hours × labor rate |
|
||||
| `IsSalesItem` | ManualUnitPrice ignored; item repriced using coat/surface math |
|
||||
|
||||
**Checklist when adding a new pricing routing flag:**
|
||||
1. Add the property to `QuoteItem` (Core/Entities)
|
||||
2. Add the property to `JobItem` (Core/Entities)
|
||||
3. Add it to `CreateQuoteItemDto` (Application/DTOs)
|
||||
4. Add it to `JobItemSeed` (private class in JobItemAssemblyService)
|
||||
5. Map it in all three `JobItemAssemblyService.CreateJobItem` overloads
|
||||
6. Include it in every `existingItemsData` JSON block in job views (`Edit.cshtml`, `EditItems.cshtml`) and in all job controller actions that build `CreateQuoteItemDto` from a `JobItem`
|
||||
7. Add a migration if the field is new on a persisted entity
|
||||
8. The structural test `PricingRoutingFlags_ExistOnBothQuoteItemAndJobItem` in `JobItemAssemblyServiceTests` will fail until steps 1–3 are done — this is intentional
|
||||
|
||||
### Branding
|
||||
- Application name: **Powder Coating Logix**
|
||||
- PCL logo: `wwwroot/images/pcl-logo.png` — used in sidebar header (when no tenant logo), login/register pages, sidebar footer
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<!--
|
||||
NCalc2 2.1.0 -> Antlr4 4.6.4 -> Antlr4.Runtime -> NETStandard.Library 1.6.0 pulls in
|
||||
old package versions that trigger NU1605 downgrade warnings when publishing for linux-x64.
|
||||
These are harmless false positives — .NET 8 supplies all of these natively at runtime.
|
||||
Suppressing NU1605 here is cleaner than pinning every affected transitive package individually.
|
||||
-->
|
||||
<NoWarn>$(NoWarn);NU1605</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Vendored
+68
-115
@@ -1,39 +1,32 @@
|
||||
pipeline {
|
||||
agent any
|
||||
agent { label 'appdev' }
|
||||
|
||||
// No triggers — start this pipeline manually from the Jenkins UI only.
|
||||
options {
|
||||
disableConcurrentBuilds()
|
||||
timestamps()
|
||||
}
|
||||
|
||||
environment {
|
||||
DOTNET_CLI_HOME = '/tmp/dotnet_cli_home'
|
||||
WEB_PROJECT = 'src/PowderCoating.Web/PowderCoating.Web.csproj'
|
||||
INFRA_PROJECT = 'src/PowderCoating.Infrastructure/PowderCoating.Infrastructure.csproj'
|
||||
PUBLISH_DIR = "${WORKSPACE}/publish"
|
||||
DEPLOY_ZIP = "${WORKSPACE}/deploy_${BUILD_NUMBER}.zip"
|
||||
MIGRATION_SQL = "${WORKSPACE}/migration_${BUILD_NUMBER}.sql"
|
||||
PATH = "C:\\Program Files\\Microsoft SDKs\\Azure\\CLI2\\wbin;${env.PATH}"
|
||||
}
|
||||
|
||||
stages {
|
||||
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
checkout([
|
||||
$class: 'GitSCM',
|
||||
branches: [[name: 'refs/heads/master']],
|
||||
userRemoteConfigs: scm.userRemoteConfigs
|
||||
])
|
||||
echo "Building commit: ${GIT_COMMIT}"
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
|
||||
stage('Build & Test') {
|
||||
stage('Restore & Build') {
|
||||
steps {
|
||||
sh 'dotnet restore'
|
||||
sh 'dotnet build --no-restore -c Release'
|
||||
sh '''
|
||||
dotnet test --no-build -c Release \
|
||||
--logger "trx;LogFileName=results.trx" \
|
||||
--results-directory TestResults
|
||||
'''
|
||||
bat 'dotnet restore PowderCoatingApp.sln'
|
||||
bat 'dotnet build PowderCoatingApp.sln -c Release --no-restore'
|
||||
}
|
||||
}
|
||||
|
||||
stage('Test') {
|
||||
steps {
|
||||
bat 'dotnet test tests\\PowderCoating.UnitTests --no-build -c Release --logger "trx;LogFileName=results.trx" --results-directory TestResults'
|
||||
}
|
||||
post {
|
||||
always {
|
||||
@@ -42,121 +35,81 @@ pipeline {
|
||||
}
|
||||
}
|
||||
|
||||
stage('Run Migrations') {
|
||||
steps {
|
||||
bat 'dotnet tool install --global dotnet-ef 2>nul || dotnet tool update --global dotnet-ef 2>nul'
|
||||
withCredentials([string(credentialsId: 'pcl-prod-sql', variable: 'SQL_CONN')]) {
|
||||
bat '"%USERPROFILE%\\.dotnet\\tools\\dotnet-ef.exe" database update --project src\\PowderCoating.Infrastructure --startup-project src\\PowderCoating.Web --configuration Release --no-build --context ApplicationDbContext --connection "%SQL_CONN%"'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Publish') {
|
||||
steps {
|
||||
sh """
|
||||
dotnet publish '${WEB_PROJECT}' \
|
||||
-c Release --no-build \
|
||||
-o '${PUBLISH_DIR}'
|
||||
"""
|
||||
bat 'dotnet publish src\\PowderCoating.Web\\PowderCoating.Web.csproj -c Release -r linux-x64 --self-contained false -o publish'
|
||||
bat 'xcopy /E /Y /I src\\PowderCoating.Web\\wwwroot publish\\wwwroot\\'
|
||||
}
|
||||
}
|
||||
|
||||
// Generates an idempotent SQL migration script (no live DB connection required).
|
||||
// The script checks which migrations have already been applied before running each one.
|
||||
stage('Generate Migration Script') {
|
||||
stage('Deploy to Azure') {
|
||||
steps {
|
||||
sh """
|
||||
dotnet ef migrations script \
|
||||
--idempotent \
|
||||
--output '${MIGRATION_SQL}' \
|
||||
--project '${INFRA_PROJECT}' \
|
||||
--startup-project '${WEB_PROJECT}' \
|
||||
--context ApplicationDbContext \
|
||||
--no-build
|
||||
"""
|
||||
archiveArtifacts artifacts: "migration_${BUILD_NUMBER}.sql", fingerprint: true
|
||||
echo "Migration script archived — review it in the Jenkins build artifacts before this pipeline runs next time."
|
||||
powershell '''
|
||||
Add-Type -Assembly System.IO.Compression.FileSystem
|
||||
if (Test-Path deploy.zip) { Remove-Item deploy.zip }
|
||||
$publishDir = (Resolve-Path "publish").Path
|
||||
$zip = [System.IO.Compression.ZipFile]::Open("deploy.zip", "Create")
|
||||
Get-ChildItem -Path $publishDir -Recurse -File | ForEach-Object {
|
||||
$entryName = $_.FullName.Substring($publishDir.Length + 1).Replace("\\", "/")
|
||||
[System.IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $_.FullName, $entryName, "Optimal") | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
stage('Apply Migration to Azure SQL') {
|
||||
steps {
|
||||
withCredentials([
|
||||
string(credentialsId: 'PCL_SQL_SERVER', variable: 'SQL_SERVER'),
|
||||
string(credentialsId: 'PCL_SQL_DATABASE', variable: 'SQL_DATABASE'),
|
||||
string(credentialsId: 'PCL_SQL_USER', variable: 'SQL_USER'),
|
||||
string(credentialsId: 'PCL_SQL_PASSWORD', variable: 'SQL_PASSWORD')
|
||||
]) {
|
||||
sh '''
|
||||
echo "Applying migration to ${SQL_SERVER}/${SQL_DATABASE} ..."
|
||||
/opt/mssql-tools18/bin/sqlcmd \
|
||||
-S "${SQL_SERVER}" \
|
||||
-d "${SQL_DATABASE}" \
|
||||
-U "${SQL_USER}" \
|
||||
-P "${SQL_PASSWORD}" \
|
||||
-C \
|
||||
-b \
|
||||
-i "${MIGRATION_SQL}"
|
||||
echo "Migration applied successfully."
|
||||
$zip.Dispose()
|
||||
Write-Host "deploy.zip created with forward-slash entry paths"
|
||||
'''
|
||||
withCredentials([azureServicePrincipal(
|
||||
credentialsId: 'azure-pcl',
|
||||
subscriptionIdVariable: 'AZ_SUB_ID',
|
||||
clientIdVariable: 'AZ_CLIENT_ID',
|
||||
clientSecretVariable: 'AZ_CLIENT_SECRET',
|
||||
tenantIdVariable: 'AZ_TENANT_ID'
|
||||
)]) {
|
||||
bat 'az login --service-principal -u "%AZ_CLIENT_ID%" -p "%AZ_CLIENT_SECRET%" --tenant "%AZ_TENANT_ID%" --output none'
|
||||
bat 'az account set --subscription "%AZ_SUB_ID%"'
|
||||
bat 'az webapp deploy --resource-group rg-powdercoatinglogix-prod --name linuxpcl --src-path deploy.zip --type zip --async true'
|
||||
bat 'az logout'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Deploy to Azure App Service') {
|
||||
stage('Health Check') {
|
||||
steps {
|
||||
withCredentials([
|
||||
string(credentialsId: 'PCL_AZURE_CLIENT_ID', variable: 'AZ_CLIENT_ID'),
|
||||
string(credentialsId: 'PCL_AZURE_CLIENT_SECRET', variable: 'AZ_CLIENT_SECRET'),
|
||||
string(credentialsId: 'PCL_AZURE_TENANT_ID', variable: 'AZ_TENANT_ID'),
|
||||
string(credentialsId: 'PCL_AZURE_SUBSCRIPTION_ID', variable: 'AZ_SUBSCRIPTION_ID'),
|
||||
string(credentialsId: 'PCL_AZURE_RESOURCE_GROUP', variable: 'AZ_RG'),
|
||||
string(credentialsId: 'PCL_AZURE_APP_NAME', variable: 'AZ_APP')
|
||||
]) {
|
||||
sh '''
|
||||
az login --service-principal \
|
||||
--username "$AZ_CLIENT_ID" \
|
||||
--password "$AZ_CLIENT_SECRET" \
|
||||
--tenant "$AZ_TENANT_ID" \
|
||||
--output none
|
||||
|
||||
az account set --subscription "$AZ_SUBSCRIPTION_ID"
|
||||
|
||||
echo "Packaging deployment artifact ..."
|
||||
cd "$PUBLISH_DIR"
|
||||
zip -r "$DEPLOY_ZIP" .
|
||||
|
||||
echo "Pushing ZIP to ${AZ_APP} ..."
|
||||
az webapp deployment source config-zip \
|
||||
--resource-group "$AZ_RG" \
|
||||
--name "$AZ_APP" \
|
||||
--src "$DEPLOY_ZIP"
|
||||
|
||||
az logout
|
||||
echo "Deploy complete."
|
||||
'''
|
||||
powershell '''
|
||||
$url = "https://app.powdercoatinglogix.com/"
|
||||
$timeout = 180
|
||||
$elapsed = 0
|
||||
Write-Host "Polling $url for up to $timeout seconds..."
|
||||
do {
|
||||
Start-Sleep -Seconds 10
|
||||
$elapsed += 10
|
||||
try {
|
||||
$r = Invoke-WebRequest $url -UseBasicParsing -TimeoutSec 10
|
||||
if ($r.StatusCode -lt 400) {
|
||||
Write-Host "App responded HTTP $($r.StatusCode) after ${elapsed}s"
|
||||
exit 0
|
||||
}
|
||||
} catch {
|
||||
Write-Host "[${elapsed}s] Not yet responding: $_"
|
||||
}
|
||||
}
|
||||
|
||||
stage('Smoke Test') {
|
||||
steps {
|
||||
withCredentials([
|
||||
string(credentialsId: 'PCL_AZURE_APP_NAME', variable: 'AZ_APP')
|
||||
]) {
|
||||
sh '''
|
||||
APP_URL="https://${AZ_APP}.azurewebsites.net"
|
||||
echo "Smoke-testing ${APP_URL} ..."
|
||||
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
||||
--max-time 45 --retry 3 --retry-delay 10 \
|
||||
"${APP_URL}")
|
||||
echo "HTTP status: ${HTTP_STATUS}"
|
||||
# 200 = OK, 302 = redirect to login (both are healthy)
|
||||
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then
|
||||
echo "SMOKE TEST FAILED — got HTTP ${HTTP_STATUS}"
|
||||
} while ($elapsed -lt $timeout)
|
||||
Write-Error "App did not come healthy within $timeout seconds"
|
||||
exit 1
|
||||
fi
|
||||
echo "Smoke test passed."
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post {
|
||||
success {
|
||||
echo "Production deployment #${BUILD_NUMBER} (${GIT_COMMIT}) completed successfully."
|
||||
echo "Production deployment #${BUILD_NUMBER} completed successfully."
|
||||
}
|
||||
failure {
|
||||
echo "Pipeline #${BUILD_NUMBER} FAILED — review the stage logs above."
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
Shop Management App TO DO List
|
||||
==============================
|
||||
-Look into possibly having AI scan a product catalog and suggest prices for items.
|
||||
-Add images to product catalog items for easily identification of parts
|
||||
-AI Company Lookup (similar to inventory lookup)
|
||||
|
||||
|
||||
-Add ability to save a quoted item to the product catalog either from an AI Photo Quote or from the calculated item
|
||||
|
||||
-Check my ChatGPT chat about surface area for a few solid ideas for the system
|
||||
|
||||
-Add SMS capabilities
|
||||
-Fix up approve/decline messages between customer and user on quote approval feature
|
||||
|
||||
Done and need testing
|
||||
=====================
|
||||
-Add sorting to all grids
|
||||
-Add searching to all grids
|
||||
-Add Workers to the system
|
||||
-Allow jobs to be assigned to workers
|
||||
-Add Shop Job Board display to show in the shop
|
||||
-Added quick edits on a few pages
|
||||
-Fix job page customer drop down. It's only showing business names and not individuals
|
||||
-Add country drop down on customer edit and add pages
|
||||
-Conver customer once quote accepted not complete
|
||||
-Add Dashboard page
|
||||
-Low Inventory Warnings display
|
||||
-Overdue jobs
|
||||
-Todays Jobs
|
||||
-new quote button on customer page doesnt pre-select customer
|
||||
-Add customer job history page
|
||||
-Profiles can now change from a light theme to a dark theme as well as other appearance changes
|
||||
-Date format can be customized per profile
|
||||
-Timezone can now be changed per profile
|
||||
-Have company logos stored in the database with the other company information
|
||||
-Add Company Name under Logo in navbar
|
||||
-Make logo bigger
|
||||
-Update create quote page to show names of individual customers or company name depending on which type it is
|
||||
-Validate that the company has entered operating costs before allowing the quote page to be loaded
|
||||
-Make phone number and contact required on quotes for new prospects
|
||||
-Move the create quote button to the right side of the screen to be consistent with other pages
|
||||
-Add setting for tax exempt on customer
|
||||
-Added tax certificate upload as well
|
||||
-Add shop minimum to quoting system and company settings
|
||||
-Add Rush Job Fee (customizable in company settings)
|
||||
-Add ability to quick change the status on the job listing and record who changed the status.
|
||||
-Deactivating company should NOT allow any users to login at all.
|
||||
-Allow superadmins to create company users/managers
|
||||
-Add a print quote button
|
||||
-Add a download PDF button for quotes
|
||||
-When adding users, also create worker records
|
||||
-Add quick update to all view pages
|
||||
-Add Mobile layouts
|
||||
-Fix a few text pieces on the dashboard page that did not invert properly when dark mode was selected
|
||||
-Add ability to upload job photos
|
||||
-Allow photo uploads for jobs before and after photos
|
||||
-Added Log Viewer
|
||||
-Added Seed Data option for super admins that will assist during testing
|
||||
-Add an item list with prices for repeat parts and such
|
||||
-Add manual data seeding that super admins can use to seed a company one at a time if needed
|
||||
-Add Log Viewer for Super Admins
|
||||
-Quotes cleaned up quite a bit and calculations and style changed
|
||||
-Approving a Quote will now auto-create a Job and link back to the quote it came from.
|
||||
-Job Items now appear on the Job Screen with the line items from the quote
|
||||
-Job items can be edited
|
||||
-Add a way to convert a quote into a job
|
||||
-Add multiple item types to add to a quote
|
||||
1. Pre-Defined item that we can choose from our product list
|
||||
2. Batch items where we enter the square footage manually as well as the quantity
|
||||
-Add Quickbooks import for customers and price lists (Desktop and Online)
|
||||
-Custom Order Powder not saving or displaying properly on quuote page
|
||||
-Added ability for Companies to define their own Job Status, Job Priority, and Quote Status' via Company Settings > Data Lookups
|
||||
-Add Randomizer Wheel
|
||||
-Add Quickbooks format export for
|
||||
-Customers
|
||||
-Product Catalog
|
||||
-Invoices
|
||||
-Quote for Product Catalog Item is only selecting items from Powder Coating, need all items
|
||||
-Add a Shop Supplies operating cost that will be used on quote calculations
|
||||
-Fix Quote screen, only Powder showing in item dropdown. Need to get all items in an IsCoating category showing up.
|
||||
-Update everywhere that uses tax rate to read and use this setting
|
||||
-Add ability to export a full price list for known items
|
||||
-Add tracking for all changes and show change history on view page. Possibly in a hidden grid or modal
|
||||
-Update the inventory screen to not duplicate color name fields and the like
|
||||
-Add option for metric system
|
||||
-Add Bulk Upload for
|
||||
-Powder
|
||||
-Product Catalog
|
||||
-Customer Data
|
||||
-Add an Appointment engine and Calendar. Also show Maintenence tasks that are scheduled on it
|
||||
-Allow shops to put employee days off on the calendar as well
|
||||
-Fix and Verify user permissions are honored
|
||||
-Run a full security check on the application
|
||||
-Add support for multi stage coatings on an item
|
||||
-Fix Seed Data routines to track errors better and continue past error imports
|
||||
-Add ability to complete a job and enter actual time and materials used
|
||||
-Add export for all data to CSV format
|
||||
-Check calendar resizing with the browser. It's off a bit
|
||||
-Add ability to apply discounts
|
||||
-Remove powder from inventory when completeing a job
|
||||
-Add color change ability for appointment types
|
||||
-Add code to honor the rush charge on a quote
|
||||
-Add options to quote for Sandblasting, Masking, Chemical Strip, Outgas, Phosphate Wash, Degrease
|
||||
-Add ability to add sq ft to product catalog item for powder estimation
|
||||
-Add better UX design for validation errors and such
|
||||
Option 1: Change "ModelOnly" to "All" (1 line change) - Shows all validation errors at top of form in red alert box
|
||||
- User would have seen: "The field Estimated Minutes must be between 0 and 10,000"
|
||||
Option 2: Add inline validation (more complex)
|
||||
- Show error messages right next to the problematic field
|
||||
- Better UX but requires adding validation spans to dynamic fields
|
||||
|
||||
Option 3: Toast notifications (requires new library/code)
|
||||
- Modern popup notifications for success/error messages
|
||||
- Would need to add a toast library (like Toastr) and wire it up
|
||||
-Add Import/Export for Company Settings
|
||||
-Allow Super Admin to modify permissions for company admins in case we add any in the future, or if anything gets messed up we can fix it!
|
||||
-Allow recurring scheduled maintenance
|
||||
-Let's show scheduled maintenence on the job schedule as well. At the top of the screen
|
||||
-Make sure maintenence shows on the calendar list view.
|
||||
-Add viewing quotes on the customer details page so we can see all quotes/jobs for a given customer to make things easier to find.
|
||||
-Add support for multiple ovens in operating costs
|
||||
-Display oven selected on quote and job detail pages
|
||||
-Allow user to choose an oven on a quote, and have it follow through to a job
|
||||
-Check for any old and outdated code and DB fields!
|
||||
-Add ability to email a quote
|
||||
-Add email capabilities
|
||||
-Add search on super admin companies screen
|
||||
-Set limits on job photos per app tier
|
||||
-Check subscription signup page to make sure the selected subscription is actually saved.
|
||||
-Don't seed the product catalog on a new user
|
||||
-Check to make sure subscription page has quotes and all fields on it
|
||||
-Allow customizing of the quote sheets and invoices (If we do them)
|
||||
-Add feature to allow username changes
|
||||
-Fix quickbooks imports based on files Colton sent
|
||||
-Add thicker border around input fields to signify they are text boxes
|
||||
-Check to make sure emails get sent when a quote is created
|
||||
-Add buttons to send emails manually if needed
|
||||
-Modify price calculations to prompt for service times (ie... sandblasting, oven cure times, outgas times etc)
|
||||
-Add ability to modify items on jobs
|
||||
-Swap quoting page to use modals to add items to segregate it a bit better.
|
||||
-Build account ledger/transaction summary view
|
||||
-Add security for financial pages
|
||||
-Allow opening balances for accounts
|
||||
-Create P&L and other reports
|
||||
-Allow receipet upload on expenses and bills
|
||||
-Download PDF for invoices throws and error
|
||||
-Emailing invoice doesn't seem to trigger
|
||||
-When a customer record has email notifications turned off, disable any email buttons that may send one and alert the user that this customer is set to have notifications turned off.
|
||||
-When doing anything that sends mail, prompt the user to alert them a message will be sent
|
||||
-Create a setup wizard for new users that will walk through system setup. Allow re-running later.
|
||||
-Check Workflow steps in wizard, might need adjusting
|
||||
-Account Summary, use permanent alert for info message at bottom
|
||||
-Add steps so that the new user can customize the data lookups and re-order them
|
||||
-Reorder menu to work better
|
||||
-Add ability to print a job invoice once completed
|
||||
-Add ability to email a job invoice
|
||||
-Integrate invoicing/billing/reports
|
||||
-Add customer portal to approve quotes from a link for now. We can do a full login later.
|
||||
-Need a complexity score for quoting parts (Simple, moderate, complex, extreme)
|
||||
-Add tagging options for quotes and jobs (user driven)
|
||||
-Can we also add this tag system to quotes and jobs to allow users to tag themselves and we can use that data later as well? We'd have to add a good
|
||||
description of WHY the user should add some tags though.
|
||||
-Inventory forecasting might be worth looking into
|
||||
-Build some AI powder usage predictions into the system
|
||||
-AI Production Scheduling - Batching enough parts together to fill the oven automagically
|
||||
-Update dashboard to show some $$$ fields
|
||||
-Update Setup Wizard
|
||||
-Update the Setup Checklist
|
||||
-Modify system to keep running balances of all accounts
|
||||
- Make sure ALL job updates refresh the Shop Display
|
||||
-Add multiple item types to add to a quote
|
||||
AI Agent item where we upload a picture and it will calculate the approximate sq ft and quote from that
|
||||
-Integration with stripe or square to accept online paymens from our users customers.
|
||||
-AI Assistant for help
|
||||
-Allow customer filtering on quotes and jobs
|
||||
-New job page blanks when validation fails
|
||||
-Can we keep track of which users have completed the setup wizard?
|
||||
-Make sure we're tracking logins. I see a user logged on, but the company health page states they have never logged in.
|
||||
-Allow printing blank work orders (model after the SCP Powder Coating blank work order)
|
||||
-IDEA: Print powders to use on work order with their QR code so they can be scanned right from there and usage recorded.
|
||||
|
||||
|
||||
Ideas Removed
|
||||
=======================
|
||||
-Add Deactivate Customer button on Customer Detail page
|
||||
|
||||
|
||||
Logins:
|
||||
rich@r2r.com/Ragz2Richs123!
|
||||
|
||||
rich@cannon.com/Cannon123!
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
Shop Management App TO DO List
|
||||
==============================
|
||||
-Add ability to save a quoted item to the product catalog either from an AI Photo Quote or from the calculated item
|
||||
-Check my ChatGPT chat about surface area for a few solid ideas for the system
|
||||
|
||||
-Add SMS capabilities
|
||||
-Fix up approve/decline messages between customer and user on quote approval feature
|
||||
|
||||
Done and need testing
|
||||
=====================
|
||||
-Add sorting to all grids
|
||||
-Add searching to all grids
|
||||
-Add Workers to the system
|
||||
-Allow jobs to be assigned to workers
|
||||
-Add Shop Job Board display to show in the shop
|
||||
-Added quick edits on a few pages
|
||||
-Fix job page customer drop down. It's only showing business names and not individuals
|
||||
-Add country drop down on customer edit and add pages
|
||||
-Conver customer once quote accepted not complete
|
||||
-Add Dashboard page
|
||||
-Low Inventory Warnings display
|
||||
-Overdue jobs
|
||||
-Todays Jobs
|
||||
-new quote button on customer page doesnt pre-select customer
|
||||
-Add customer job history page
|
||||
-Profiles can now change from a light theme to a dark theme as well as other appearance changes
|
||||
-Date format can be customized per profile
|
||||
-Timezone can now be changed per profile
|
||||
-Have company logos stored in the database with the other company information
|
||||
-Add Company Name under Logo in navbar
|
||||
-Make logo bigger
|
||||
-Update create quote page to show names of individual customers or company name depending on which type it is
|
||||
-Validate that the company has entered operating costs before allowing the quote page to be loaded
|
||||
-Make phone number and contact required on quotes for new prospects
|
||||
-Move the create quote button to the right side of the screen to be consistent with other pages
|
||||
-Add setting for tax exempt on customer
|
||||
-Added tax certificate upload as well
|
||||
-Add shop minimum to quoting system and company settings
|
||||
-Add Rush Job Fee (customizable in company settings)
|
||||
-Add ability to quick change the status on the job listing and record who changed the status.
|
||||
-Deactivating company should NOT allow any users to login at all.
|
||||
-Allow superadmins to create company users/managers
|
||||
-Add a print quote button
|
||||
-Add a download PDF button for quotes
|
||||
-When adding users, also create worker records
|
||||
-Add quick update to all view pages
|
||||
-Add Mobile layouts
|
||||
-Fix a few text pieces on the dashboard page that did not invert properly when dark mode was selected
|
||||
-Add ability to upload job photos
|
||||
-Allow photo uploads for jobs before and after photos
|
||||
-Added Log Viewer
|
||||
-Added Seed Data option for super admins that will assist during testing
|
||||
-Add an item list with prices for repeat parts and such
|
||||
-Add manual data seeding that super admins can use to seed a company one at a time if needed
|
||||
-Add Log Viewer for Super Admins
|
||||
-Quotes cleaned up quite a bit and calculations and style changed
|
||||
-Approving a Quote will now auto-create a Job and link back to the quote it came from.
|
||||
-Job Items now appear on the Job Screen with the line items from the quote
|
||||
-Job items can be edited
|
||||
-Add a way to convert a quote into a job
|
||||
-Add multiple item types to add to a quote
|
||||
1. Pre-Defined item that we can choose from our product list
|
||||
2. Batch items where we enter the square footage manually as well as the quantity
|
||||
-Add Quickbooks import for customers and price lists (Desktop and Online)
|
||||
-Custom Order Powder not saving or displaying properly on quuote page
|
||||
-Added ability for Companies to define their own Job Status, Job Priority, and Quote Status' via Company Settings > Data Lookups
|
||||
-Add Randomizer Wheel
|
||||
-Add Quickbooks format export for
|
||||
-Customers
|
||||
-Product Catalog
|
||||
-Invoices
|
||||
-Quote for Product Catalog Item is only selecting items from Powder Coating, need all items
|
||||
-Add a Shop Supplies operating cost that will be used on quote calculations
|
||||
-Fix Quote screen, only Powder showing in item dropdown. Need to get all items in an IsCoating category showing up.
|
||||
-Update everywhere that uses tax rate to read and use this setting
|
||||
-Add ability to export a full price list for known items
|
||||
-Add tracking for all changes and show change history on view page. Possibly in a hidden grid or modal
|
||||
-Update the inventory screen to not duplicate color name fields and the like
|
||||
-Add option for metric system
|
||||
-Add Bulk Upload for
|
||||
-Powder
|
||||
-Product Catalog
|
||||
-Customer Data
|
||||
-Add an Appointment engine and Calendar. Also show Maintenence tasks that are scheduled on it
|
||||
-Allow shops to put employee days off on the calendar as well
|
||||
-Fix and Verify user permissions are honored
|
||||
-Run a full security check on the application
|
||||
-Add support for multi stage coatings on an item
|
||||
-Fix Seed Data routines to track errors better and continue past error imports
|
||||
-Add ability to complete a job and enter actual time and materials used
|
||||
-Add export for all data to CSV format
|
||||
-Check calendar resizing with the browser. It's off a bit
|
||||
-Add ability to apply discounts
|
||||
-Remove powder from inventory when completeing a job
|
||||
-Add color change ability for appointment types
|
||||
-Add code to honor the rush charge on a quote
|
||||
-Add options to quote for Sandblasting, Masking, Chemical Strip, Outgas, Phosphate Wash, Degrease
|
||||
-Add ability to add sq ft to product catalog item for powder estimation
|
||||
-Add better UX design for validation errors and such
|
||||
Option 1: Change "ModelOnly" to "All" (1 line change) - Shows all validation errors at top of form in red alert box
|
||||
- User would have seen: "The field Estimated Minutes must be between 0 and 10,000"
|
||||
Option 2: Add inline validation (more complex)
|
||||
- Show error messages right next to the problematic field
|
||||
- Better UX but requires adding validation spans to dynamic fields
|
||||
|
||||
Option 3: Toast notifications (requires new library/code)
|
||||
- Modern popup notifications for success/error messages
|
||||
- Would need to add a toast library (like Toastr) and wire it up
|
||||
-Add Import/Export for Company Settings
|
||||
-Allow Super Admin to modify permissions for company admins in case we add any in the future, or if anything gets messed up we can fix it!
|
||||
-Allow recurring scheduled maintenance
|
||||
-Let's show scheduled maintenence on the job schedule as well. At the top of the screen
|
||||
-Make sure maintenence shows on the calendar list view.
|
||||
-Add viewing quotes on the customer details page so we can see all quotes/jobs for a given customer to make things easier to find.
|
||||
-Add support for multiple ovens in operating costs
|
||||
-Display oven selected on quote and job detail pages
|
||||
-Allow user to choose an oven on a quote, and have it follow through to a job
|
||||
-Check for any old and outdated code and DB fields!
|
||||
-Add ability to email a quote
|
||||
-Add email capabilities
|
||||
-Add search on super admin companies screen
|
||||
-Set limits on job photos per app tier
|
||||
-Check subscription signup page to make sure the selected subscription is actually saved.
|
||||
-Don't seed the product catalog on a new user
|
||||
-Check to make sure subscription page has quotes and all fields on it
|
||||
-Allow customizing of the quote sheets and invoices (If we do them)
|
||||
-Add feature to allow username changes
|
||||
-Fix quickbooks imports based on files Colton sent
|
||||
-Add thicker border around input fields to signify they are text boxes
|
||||
-Check to make sure emails get sent when a quote is created
|
||||
-Add buttons to send emails manually if needed
|
||||
-Modify price calculations to prompt for service times (ie... sandblasting, oven cure times, outgas times etc)
|
||||
-Add ability to modify items on jobs
|
||||
-Swap quoting page to use modals to add items to segregate it a bit better.
|
||||
-Build account ledger/transaction summary view
|
||||
-Add security for financial pages
|
||||
-Allow opening balances for accounts
|
||||
-Create P&L and other reports
|
||||
-Allow receipet upload on expenses and bills
|
||||
-Download PDF for invoices throws and error
|
||||
-Emailing invoice doesn't seem to trigger
|
||||
-When a customer record has email notifications turned off, disable any email buttons that may send one and alert the user that this customer is set to have notifications turned off.
|
||||
-When doing anything that sends mail, prompt the user to alert them a message will be sent
|
||||
-Create a setup wizard for new users that will walk through system setup. Allow re-running later.
|
||||
-Check Workflow steps in wizard, might need adjusting
|
||||
-Account Summary, use permanent alert for info message at bottom
|
||||
-Add steps so that the new user can customize the data lookups and re-order them
|
||||
-Reorder menu to work better
|
||||
-Add ability to print a job invoice once completed
|
||||
-Add ability to email a job invoice
|
||||
-Integrate invoicing/billing/reports
|
||||
-Add customer portal to approve quotes from a link for now. We can do a full login later.
|
||||
-Need a complexity score for quoting parts (Simple, moderate, complex, extreme)
|
||||
-Add tagging options for quotes and jobs (user driven)
|
||||
-Can we also add this tag system to quotes and jobs to allow users to tag themselves and we can use that data later as well? We'd have to add a good
|
||||
description of WHY the user should add some tags though.
|
||||
-Inventory forecasting might be worth looking into
|
||||
-Build some AI powder usage predictions into the system
|
||||
-AI Production Scheduling - Batching enough parts together to fill the oven automagically
|
||||
-Update dashboard to show some $$$ fields
|
||||
-Update Setup Wizard
|
||||
-Update the Setup Checklist
|
||||
-Modify system to keep running balances of all accounts
|
||||
- Make sure ALL job updates refresh the Shop Display
|
||||
-Add multiple item types to add to a quote
|
||||
AI Agent item where we upload a picture and it will calculate the approximate sq ft and quote from that
|
||||
-Integration with stripe or square to accept online paymens from our users customers.
|
||||
-AI Assistant for help
|
||||
-Allow customer filtering on quotes and jobs
|
||||
-New job page blanks when validation fails
|
||||
-Can we keep track of which users have completed the setup wizard?
|
||||
-Make sure we're tracking logins. I see a user logged on, but the company health page states they have never logged in.
|
||||
-Allow printing blank work orders (model after the SCP Powder Coating blank work order)
|
||||
-IDEA: Print powders to use on work order with their QR code so they can be scanned right from there and usage recorded.
|
||||
|
||||
|
||||
Ideas Removed
|
||||
=======================
|
||||
-Add Deactivate Customer button on Customer Detail page
|
||||
|
||||
|
||||
Logins:
|
||||
rich@r2r.com/Ragz2Richs123!
|
||||
|
||||
rich@cannon.com/Cannon123!
|
||||
@@ -0,0 +1,346 @@
|
||||
# Data Access Architecture
|
||||
|
||||
## Status: Complete ✓ (2026-04-28)
|
||||
|
||||
This document defines the target data access architecture for Powder Coating Logix and tracks
|
||||
the migration from the current mixed pattern to the clean layered pattern.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
The codebase currently has ~50 controllers injecting `ApplicationDbContext` directly alongside
|
||||
`IUnitOfWork`. This happened organically: the generic `Repository<T>` could not express complex
|
||||
multi-level include queries, so `_context` became the escape hatch. Once injected for one complex
|
||||
query, it was used for everything else in that controller too. The inconsistency compounds with
|
||||
every new controller a developer writes.
|
||||
|
||||
For a solo developer this is manageable. For a team it creates a daily decision tax — "which
|
||||
pattern do I follow?" — with no clear answer. New developers copy the nearest example, which is
|
||||
usually `_context`, so the problem grows.
|
||||
|
||||
---
|
||||
|
||||
## The Rule (Short Version)
|
||||
|
||||
> **`ApplicationDbContext` is never injected into a controller. Ever.**
|
||||
>
|
||||
> All data access in controllers goes through `IUnitOfWork`.
|
||||
> Complex queries that the generic `Repository<T>` cannot express live in typed repositories or
|
||||
> read services — both accessible through `IUnitOfWork`.
|
||||
|
||||
---
|
||||
|
||||
## Target Architecture
|
||||
|
||||
```
|
||||
Controllers (Presentation Layer)
|
||||
│
|
||||
├── IUnitOfWork.EntityName → IRepository<T> Simple CRUD
|
||||
├── IUnitOfWork.Jobs → IJobRepository Complex domain queries
|
||||
├── IUnitOfWork.Invoices → IInvoiceRepository Complex domain queries
|
||||
├── IUnitOfWork.Quotes → IQuoteRepository Complex domain queries
|
||||
├── IUnitOfWork.Customers → ICustomerRepository Complex domain queries
|
||||
├── IUnitOfWork.Bills → IBillRepository Complex domain queries
|
||||
│
|
||||
├── IFinancialReportService Aggregate/reporting reads
|
||||
└── IOperationalReportService Aggregate/reporting reads
|
||||
|
||||
Infrastructure Layer (the only layer that knows about ApplicationDbContext)
|
||||
│
|
||||
├── Repository<T> Generic implementation
|
||||
├── JobRepository : IJobRepository Typed implementations
|
||||
├── InvoiceRepository ...
|
||||
├── QuoteRepository ...
|
||||
├── CustomerRepository ...
|
||||
├── BillRepository ...
|
||||
│
|
||||
├── FinancialReportService DbContext used directly (read-only, no tracking)
|
||||
└── OperationalReportService DbContext used directly (read-only, no tracking)
|
||||
```
|
||||
|
||||
`ApplicationDbContext` never crosses into the Presentation layer. It lives in Infrastructure and
|
||||
only Infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## Three Tiers of Data Access
|
||||
|
||||
### Tier 1 — Simple CRUD → Generic `IRepository<T>` via `IUnitOfWork`
|
||||
|
||||
Use for: single-entity lookups, lists, adds, soft deletes, simple filtered queries.
|
||||
|
||||
```csharp
|
||||
// Good
|
||||
var items = await _unitOfWork.CatalogItems.GetAllAsync();
|
||||
var item = await _unitOfWork.CatalogItems.GetByIdAsync(id);
|
||||
await _unitOfWork.Announcements.AddAsync(entity);
|
||||
await _unitOfWork.CompleteAsync();
|
||||
```
|
||||
|
||||
Entities in this tier (generic repo is sufficient):
|
||||
- Announcements, BugReports, CatalogItems, CatalogCategories, CatalogPriceCheckReports
|
||||
- CompanyBlastSetups, CompanyOperatingCosts, CompanyPreferences
|
||||
- ContactSubmissions, CreditMemos, CreditMemoApplications
|
||||
- DashboardTips, Deposits, Equipment
|
||||
- GiftCertificates, GiftCertificateRedemptions
|
||||
- InventoryItems, InventoryTransactions
|
||||
- JobChangeHistories, JobDailyPriorities, JobItemCoats, JobItems, JobNotes, JobPhotos
|
||||
- JobStatusHistory, JobTemplates, JobTemplateItems, JobTemplateItemCoats, JobTemplateItemPrepServices
|
||||
- JobTimeEntries, MaintenanceRecords, ManufacturerLookupPatterns
|
||||
- NotificationLogs, NotificationTemplates
|
||||
- OvenBatches, OvenBatchItems, OvenCosts
|
||||
- Payments, PrepServices, PricingTiers
|
||||
- PowderUsageLogs, PurchaseOrderItems
|
||||
- QuoteChangeHistories, QuoteItemCoats, QuoteItems, QuoteItemPrepServices, QuotePhotos
|
||||
- Refunds, ReworkRecords
|
||||
- ShopWorkers, ShopWorkerRoleCosts, SubscriptionPlanConfigs
|
||||
- Vendors
|
||||
|
||||
### Tier 2 — Complex Domain Queries → Typed Repositories
|
||||
|
||||
Use for: multi-level include chains, domain-specific filtered loads, queries that require
|
||||
`IgnoreQueryFilters`, queries that span multiple related entities in non-trivial ways.
|
||||
|
||||
The typed repository interface lives in `Core/Interfaces/Repositories/`.
|
||||
The implementation lives in `Infrastructure/Repositories/`.
|
||||
The property is on `IUnitOfWork` — same access point as Tier 1.
|
||||
|
||||
```csharp
|
||||
// Good — the complex include chain lives in the repository, not the controller
|
||||
var job = await _unitOfWork.Jobs.LoadForDetailsAsync(id);
|
||||
var invoice = await _unitOfWork.Invoices.LoadForViewAsync(id);
|
||||
var quote = await _unitOfWork.Quotes.GetByApprovalTokenAsync(token);
|
||||
```
|
||||
|
||||
#### `IJobRepository`
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `LoadForDetailsAsync(int id)` | Full include chain for Job Details view |
|
||||
| `LoadForEditAsync(int id)` | Includes needed for Job Edit form |
|
||||
| `LoadForBoardAsync(int companyId, ...)` | Jobs for the kanban board with status/priority filters |
|
||||
| `GetByStatusAsync(int companyId, int statusId)` | Filtered by status with customer include |
|
||||
| `GetAssignedToWorkerAsync(int workerId)` | All active jobs for a worker |
|
||||
| `GetOverdueAsync(int companyId)` | Jobs past due date |
|
||||
|
||||
#### `IInvoiceRepository`
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `LoadForViewAsync(int id)` | Full 8-table include chain (current `LoadInvoiceForViewAsync`) |
|
||||
| `GetOverdueAsync(int companyId)` | Invoices past due date with customer info |
|
||||
| `GetByPaymentTokenAsync(string token)` | Online payment portal lookup |
|
||||
| `GetForJobAsync(int jobId, bool includeDeleted)` | Invoice for a given job (1:1 check) |
|
||||
|
||||
#### `IQuoteRepository`
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `LoadForViewAsync(int id)` | Full include chain for Quote Details |
|
||||
| `LoadForEditAsync(int id)` | Includes needed for wizard edit |
|
||||
| `GetByApprovalTokenAsync(string token)` | Customer approval portal lookup |
|
||||
| `GetPendingApprovalsAsync(int companyId)` | Quotes awaiting customer approval |
|
||||
|
||||
#### `ICustomerRepository`
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `LoadForDetailsAsync(int id)` | Customer with jobs, quotes, invoices, notes summary |
|
||||
| `GetWithOutstandingBalancesAsync(int companyId)` | AR summary data |
|
||||
| `FindByEmailAsync(string email, int companyId)` | Duplicate check on create/edit |
|
||||
|
||||
#### `IBillRepository`
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `LoadForViewAsync(int id)` | Bill with line items, payments, vendor |
|
||||
| `GetApPayablesAsync(int companyId)` | Open AP ledger with aging |
|
||||
|
||||
#### `IPurchaseOrderRepository`
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `LoadForViewAsync(int id)` | PO with line items and vendor |
|
||||
| `GetByStatusAsync(int companyId, string status)` | Filtered PO list |
|
||||
|
||||
### Tier 3 — Aggregate/Reporting Queries → Read Services
|
||||
|
||||
Use for: P&L calculations, AR aging, powder usage aggregates, job cycle time, any query that
|
||||
uses `GROUP BY`, window functions, or multi-table joins that return shaped result DTOs rather
|
||||
than tracked entities.
|
||||
|
||||
These services are injected directly into controllers alongside `IUnitOfWork`. They use
|
||||
`ApplicationDbContext` internally (with `.AsNoTracking()`) — that is correct and intentional,
|
||||
because they live in Infrastructure.
|
||||
|
||||
```csharp
|
||||
// Controller constructor
|
||||
public ReportsController(IUnitOfWork unitOfWork, IFinancialReportService financialReports, ...)
|
||||
|
||||
// Usage
|
||||
var aging = await _financialReports.GetArAgingAsync(companyId);
|
||||
var pl = await _financialReports.GetProfitLossAsync(companyId, startDate, endDate);
|
||||
```
|
||||
|
||||
#### `IFinancialReportService`
|
||||
- `GetArAgingAsync(int companyId)` → AR aging buckets (current, 30, 60, 90+ days)
|
||||
- `GetProfitLossAsync(int companyId, DateTime start, DateTime end)` → P&L summary
|
||||
- `GetMonthlyRevenueAsync(int companyId, int months)` → monthly invoiced vs collected
|
||||
- `GetTopOutstandingCustomersAsync(int companyId, int count)` → largest open balances
|
||||
- `GetCashFlowProjectionAsync(int companyId, int days)` → forward-looking cash position
|
||||
- `GetAnomaliesAsync(int companyId, int lookbackDays)` → bill/expense anomaly detection
|
||||
- `GetRecentPaymentsAsync(int companyId, int count)` → recent payment activity
|
||||
|
||||
#### `IOperationalReportService`
|
||||
- `GetJobCycleTimeAsync(int companyId, DateTime start, DateTime end)` → avg days per stage
|
||||
- `GetPowderUsageAsync(int companyId, DateTime start, DateTime end)` → usage by color/vendor
|
||||
- `GetWorkerProductivityAsync(int companyId, DateTime start, DateTime end)` → jobs per worker
|
||||
- `GetOvenUtilizationAsync(int companyId, DateTime start, DateTime end)` → oven throughput
|
||||
- `GetReworkRateAsync(int companyId, DateTime start, DateTime end)` → defect/rework trends
|
||||
- `GetStatusFlowAsync(int companyId, DateTime start, DateTime end)` → job status transitions
|
||||
|
||||
---
|
||||
|
||||
## Permanent Exceptions
|
||||
|
||||
The following controllers are **intentionally allowed** to inject `ApplicationDbContext` directly.
|
||||
This is not a smell — it is correct for their use cases. Each file has a comment explaining why.
|
||||
|
||||
| Controller | Reason |
|
||||
|------------|--------|
|
||||
| `StripeWebhookController` | Idempotency key lookup must bypass soft-delete and tenant filters |
|
||||
| `WebhooksController` | Twilio raw event handling; same reasoning as Stripe |
|
||||
| `PaymentController` | Stripe Connect embedded payment flow; raw session state needed |
|
||||
| `RegistrationController` | PendingRegistrationSession queries bypass normal tenant scoping |
|
||||
| `DataExportController` | Bulk streaming export; repository pattern adds unnecessary overhead |
|
||||
| `AccountDataExportController` | Same as above |
|
||||
| `DataPurgeController` | Destructive bulk operations; needs direct transaction control |
|
||||
| `SystemInfoController` | Infrastructure diagnostics; queries metadata, not business data |
|
||||
| `SystemLogsController` | Log table queries; not a business entity |
|
||||
| `CompanyHealthController` | Cross-tenant health checks for SuperAdmin; ignores all filters |
|
||||
| `PasskeyController` | WebAuthn/FIDO2 identity infrastructure; UserPasskeys is an ASP.NET Identity concern outside IUnitOfWork; anonymous login path has no tenant context |
|
||||
| `AuditLogController` | Append-only audit log with `long` PK; platform infrastructure table outside the business entity graph; same reasoning as `SystemLogsController` |
|
||||
| `UserActivityController` | Queries ASP.NET Identity `ApplicationUser` across all tenants with `Include(u => u.Company)`; Identity entities live outside IUnitOfWork |
|
||||
| `EmailBroadcastController` | Cross-tenant fan-out querying ASP.NET Identity Users table with company joins; Identity entities live outside IUnitOfWork |
|
||||
| `RevenueController` | Cross-tenant MRR/ARR metrics joining Company + SubscriptionPlanConfig; same pattern as `CompanyHealthController` |
|
||||
| `StripeEventsController` | `StripeWebhookEvents` is a platform infrastructure table, not a business entity; same reasoning as `StripeWebhookController` |
|
||||
| `SubscriptionManagementController` | Cross-tenant Company management with raw SQL audit log writes that bypass the tenant pipeline; platform-level concern |
|
||||
| `UsageQuotaController` | Cross-tenant bulk GROUP BY quota queries; routing through IUnitOfWork would require O(n) repository round-trips |
|
||||
|
||||
If you think you need to add a controller to this list, you almost certainly don't. Ask first.
|
||||
|
||||
---
|
||||
|
||||
## Migration Roadmap
|
||||
|
||||
### Phase 1 — Foundation (no behavior change)
|
||||
- [ ] Create `Core/Interfaces/Repositories/` directory
|
||||
- [ ] Define `IJobRepository`, `IInvoiceRepository`, `IQuoteRepository`, `ICustomerRepository`, `IBillRepository`, `IPurchaseOrderRepository`
|
||||
- [ ] Define `IFinancialReportService`, `IOperationalReportService` in `Core/Interfaces/Services/`
|
||||
- [ ] Create `Infrastructure/Repositories/` directory
|
||||
- [ ] Implement all typed repositories (move include chains from controllers)
|
||||
- [ ] Implement `FinancialReportService` (move aggregate queries from `ReportsController`)
|
||||
- [ ] Implement `OperationalReportService`
|
||||
- [ ] Extend `IUnitOfWork` with typed repository properties
|
||||
- [ ] Register all new types in `Program.cs`
|
||||
- [ ] Build passes, all tests green — no controller has changed yet
|
||||
|
||||
### Phase 2 — Complex controller migration ✓ COMPLETE (2026-04-27)
|
||||
- [x] `InvoicesController` → `IInvoiceRepository`
|
||||
- [x] `JobsController` → `IJobRepository`
|
||||
- [x] `QuotesController` → `IQuoteRepository`
|
||||
- [x] `CustomersController` → `ICustomerRepository`
|
||||
- [x] `BillsController` → `IBillRepository`
|
||||
- [x] `PurchaseOrdersController` → `IPurchaseOrderRepository`
|
||||
- [x] `ReportsController` → `IFinancialReportService` + `IOperationalReportService`
|
||||
|
||||
### Phase 3 — Simple controller sweep ✓ COMPLETE (2026-04-28)
|
||||
Remove `ApplicationDbContext` injection from all controllers not in the permanent exceptions list,
|
||||
replacing with existing `IUnitOfWork` generic repository calls.
|
||||
|
||||
- [x] `AnnouncementsController`
|
||||
- [x] `AiQuickQuoteController`
|
||||
- [x] `AiUsageReportController`
|
||||
- [x] `AuditLogController` → permanent exception (Identity/platform infra)
|
||||
- [x] `BannedIpsController`
|
||||
- [x] `BugReportController`
|
||||
- [x] `CompaniesController`
|
||||
- [x] `CompanySettingsController`
|
||||
- [x] `CompanyUsersController`
|
||||
- [x] `DashboardController`
|
||||
- [x] `DashboardTipsController`
|
||||
- [x] `DepositsController`
|
||||
- [x] `EmailBroadcastController` → permanent exception (Identity fan-out)
|
||||
- [x] `ExpensesController`
|
||||
- [x] `InAppNotificationsController`
|
||||
- [x] `InventoryController`
|
||||
- [x] `JobsPriorityController`
|
||||
- [x] `JobTemplatesController`
|
||||
- [x] `NotificationLogsController`
|
||||
- [x] `PasskeyController` → permanent exception (WebAuthn/FIDO2 identity infra)
|
||||
- [x] `PlatformNotificationsController`
|
||||
- [x] `QuoteApprovalController`
|
||||
- [x] `ReleaseNotesController`
|
||||
- [x] `RevenueController` → permanent exception (cross-tenant MRR/ARR)
|
||||
- [x] `SetupWizardController`
|
||||
- [x] `SmsConsentAuditController`
|
||||
- [x] `StripeEventsController` → permanent exception (platform infra table)
|
||||
- [x] `SubscriptionManagementController` → permanent exception (platform-level cross-tenant)
|
||||
- [x] `UnsubscribeController`
|
||||
- [x] `UsageQuotaController` → permanent exception (bulk GROUP BY)
|
||||
- [x] `UserActivityController` → permanent exception (Identity entities)
|
||||
- [x] `VendorsController`
|
||||
|
||||
### Phase 4 — Enforcement ✓ COMPLETE (2026-04-28)
|
||||
- [x] `EnforceDataAccessArchitecture()` added to `Program.cs` — scans all Controller subclasses at
|
||||
startup via reflection and throws `InvalidOperationException` if any non-exempt controller
|
||||
has `ApplicationDbContext` in its constructor. The app cannot start with a violation.
|
||||
- [x] Permanent exceptions list hardcoded in the enforcement function (18 controllers).
|
||||
- [x] This document status updated to Complete.
|
||||
- [ ] Update `CLAUDE.md` to mark migration complete (optional — CLAUDE.md already reflects the rule)
|
||||
|
||||
---
|
||||
|
||||
## File Locations Reference
|
||||
|
||||
```
|
||||
src/
|
||||
PowderCoating.Core/
|
||||
Interfaces/
|
||||
IRepository.cs existing
|
||||
IUnitOfWork.cs existing — extended in Phase 1
|
||||
Repositories/ NEW in Phase 1
|
||||
IJobRepository.cs
|
||||
IInvoiceRepository.cs
|
||||
IQuoteRepository.cs
|
||||
ICustomerRepository.cs
|
||||
IBillRepository.cs
|
||||
IPurchaseOrderRepository.cs
|
||||
Services/ NEW in Phase 1
|
||||
IFinancialReportService.cs
|
||||
IOperationalReportService.cs
|
||||
|
||||
PowderCoating.Infrastructure/
|
||||
Repositories/ NEW in Phase 1
|
||||
UnitOfWork.cs existing — extended
|
||||
Repository.cs existing
|
||||
JobRepository.cs
|
||||
InvoiceRepository.cs
|
||||
QuoteRepository.cs
|
||||
CustomerRepository.cs
|
||||
BillRepository.cs
|
||||
PurchaseOrderRepository.cs
|
||||
Services/
|
||||
FinancialReportService.cs NEW in Phase 1
|
||||
OperationalReportService.cs NEW in Phase 1
|
||||
NotificationService.cs existing — correct as-is
|
||||
PdfService.cs existing — correct as-is
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
When reviewing a PR that touches data access:
|
||||
|
||||
1. Does the controller inject `ApplicationDbContext`? If yes and it's not in the permanent
|
||||
exceptions list → request changes.
|
||||
2. Is a complex include chain written inline in a controller action? → move to typed repository.
|
||||
3. Is a GROUP BY / aggregate query inline in a controller action? → move to report service.
|
||||
4. Does a new typed repository method duplicate logic already in another repository? → consolidate.
|
||||
5. Are all DbContext calls in report services using `.AsNoTracking()`? → required for read services.
|
||||
@@ -0,0 +1,47 @@
|
||||
I looked through the reporting code, and I do not see a dedicated sales tax report.
|
||||
|
||||
What exists today:
|
||||
|
||||
- The report menu has Financial Summary, AR Aging, Balance Sheet, and Sales & Income, but no sales-tax-specific report card in /Y:/PCC/
|
||||
PowderCoatingApp/src/PowderCoating.Web/Views/Reports/Landing.cshtml:147 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Reports/
|
||||
Landing.cshtml:211.
|
||||
- The reporting service only exposes four finance reports in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Interfaces/
|
||||
IFinancialReportService.cs:13 through /Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Interfaces/IFinancialReportService.cs:22.
|
||||
- Sales & Income does surface tax, but only as a total and per-invoice column in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/
|
||||
Services/FinancialReportService.cs:406, /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/Services/FinancialReportService.cs:419,
|
||||
and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Reports/SalesAndIncome.cshtml:268.
|
||||
- The good news is the underlying data is already there: invoices store TaxAmount and SalesTaxAccountId in /Y:/PCC/PowderCoatingApp/src/
|
||||
PowderCoating.Core/Entities/Invoice.cs:23 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Core/Entities/Invoice.cs:55, and the chart of
|
||||
accounts seeds 2200 Sales Tax Payable in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/Services/SeedDataService.Accounts.cs:59.
|
||||
|
||||
Plan I’d recommend:
|
||||
|
||||
1. Add a new Sales Tax report under Reports > Finance with date range filters, matching the existing report pattern in /Y:/PCC/PowderCoatingApp/
|
||||
src/PowderCoating.Web/Controllers/ReportsController.cs:993.
|
||||
2. Build a SalesTaxReportDto plus GetSalesTaxReportAsync(...) in the reporting interface/service.
|
||||
3. Phase 1 report contents:
|
||||
|
||||
- Total taxable sales
|
||||
- Total non-taxed sales
|
||||
- Total sales tax billed
|
||||
- Breakdown by sales-tax liability account
|
||||
- Breakdown by month
|
||||
- Invoice detail grid: invoice date, invoice #, customer, subtotal, tax %, tax amount, total, amount paid, balance due, status, tax account
|
||||
|
||||
4. Add PDF export, and probably CSV too, since this is the kind of report people hand to accountants.
|
||||
5. Put a report card on the Reports landing page and gate it the same way as the other accounting reports.
|
||||
|
||||
Two important caveats before we build it:
|
||||
|
||||
- The biggest decision is whether this should be invoice-basis or cash-basis. The current model naturally supports invoice-basis first.
|
||||
- Refunds and credit memos do not appear to store a separate tax adjustment value in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Core/Entities/
|
||||
Refund.cs:1 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Core/Entities/CreditMemo.cs:1, so phase 1 would be best framed as “sales tax billed
|
||||
on invoices,” with net tax adjustments as a phase 2 enhancement.
|
||||
|
||||
My recommendation is:
|
||||
|
||||
- Build phase 1 as an invoice-basis Sales Tax Liability report
|
||||
- Label zero-tax invoices as non-taxed sales
|
||||
- Add cash-basis and refund/credit tax adjustments later if you need filing-grade accuracy for more complex cases
|
||||
|
||||
If you want, I can turn this plan into the actual report next.
|
||||
@@ -0,0 +1,32 @@
|
||||
I looked through the reporting code, and I do not see a dedicated sales tax report.
|
||||
|
||||
What exists today:
|
||||
|
||||
- The report menu has Financial Summary, AR Aging, Balance Sheet, and Sales & Income, but no sales-tax-specific report card in /Y:/PCC/
|
||||
PowderCoatingApp/src/PowderCoating.Web/Views/Reports/Landing.cshtml:147 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Reports/
|
||||
Landing.cshtml:211.
|
||||
- The reporting service only exposes four finance reports in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Interfaces/
|
||||
IFinancialReportService.cs:13 through /Y:/PCC/PowderCoatingApp/src/PowderCoating.Application/Interfaces/IFinancialReportService.cs:22.
|
||||
- Sales & Income does surface tax, but only as a total and per-invoice column in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/
|
||||
Services/FinancialReportService.cs:406, /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/Services/FinancialReportService.cs:419,
|
||||
and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Web/Views/Reports/SalesAndIncome.cshtml:268.
|
||||
- The good news is the underlying data is already there: invoices store TaxAmount and SalesTaxAccountId in /Y:/PCC/PowderCoatingApp/src/
|
||||
PowderCoating.Core/Entities/Invoice.cs:23 and /Y:/PCC/PowderCoatingApp/src/PowderCoating.Core/Entities/Invoice.cs:55, and the chart of
|
||||
accounts seeds 2200 Sales Tax Payable in /Y:/PCC/PowderCoatingApp/src/PowderCoating.Infrastructure/Services/SeedDataService.Accounts.cs:59.
|
||||
|
||||
Plan I’d recommend:
|
||||
|
||||
1. Add a new Sales Tax report under Reports > Finance with date range filters, matching the existing report pattern in /Y:/PCC/PowderCoatingApp/
|
||||
src/PowderCoating.Web/Controllers/ReportsController.cs:993.
|
||||
2. Build a SalesTaxReportDto plus GetSalesTaxReportAsync(...) in the reporting interface/service.
|
||||
3. Phase 1 report contents:
|
||||
|
||||
- Total taxable sales
|
||||
- Total non-taxed sales
|
||||
- Total sales tax billed
|
||||
- Breakdown by sales-tax liability account
|
||||
- Breakdown by month
|
||||
- Invoice detail grid: invoice date, invoice #, customer, subtotal, tax %, tax amount, total, amount paid, balance due, status, tax account
|
||||
|
||||
4. Add PDF export, and probably CSV too, since this is the kind of report people hand to accountants.
|
||||
5. Put a report card on the Reports landing page and gate it the same way as the other accounting reports.
|
||||
@@ -0,0 +1,258 @@
|
||||
# Guided Activation Flow Feature Spec
|
||||
|
||||
## Overview
|
||||
|
||||
This feature introduces a **post-setup guided activation flow** for new companies.
|
||||
|
||||
After completing the setup wizard, users should be guided through their **first real workflow** so they understand how to use the system immediately.
|
||||
|
||||
This is NOT a tooltip tour.
|
||||
|
||||
This is a **guided outcome flow using real system actions** (quotes, jobs, invoices).
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Current behavior:
|
||||
- Users complete setup wizard
|
||||
- Land on dashboard
|
||||
- Do not create quotes, jobs, or invoices
|
||||
- Drop off
|
||||
|
||||
Goal:
|
||||
- Ensure users complete at least ONE real workflow
|
||||
- Create an "aha moment" within first session
|
||||
|
||||
---
|
||||
|
||||
## Business Workflows
|
||||
|
||||
### 1. Quote-First Workflow
|
||||
- Create Quote
|
||||
- Send to customer
|
||||
- Convert Quote → Job
|
||||
- Process Job
|
||||
- Create Invoice
|
||||
- Customer Pays
|
||||
|
||||
### 2. Job-First Workflow (Walk-in)
|
||||
- Create Job directly
|
||||
- Process Job
|
||||
- Create Invoice
|
||||
- Customer Pays
|
||||
|
||||
---
|
||||
|
||||
## Feature Behavior
|
||||
|
||||
### Trigger Condition
|
||||
|
||||
IF:
|
||||
- setup wizard is completed
|
||||
- AND firstWorkflowCompleted == false
|
||||
|
||||
THEN:
|
||||
→ redirect user to guided activation flow
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Workflow Selection
|
||||
|
||||
Display full-screen page:
|
||||
|
||||
### Title:
|
||||
"Your shop is set up. Let’s run your first workflow."
|
||||
|
||||
### Subtitle:
|
||||
"Choose how jobs usually start for your shop and we’ll guide you through it."
|
||||
|
||||
### Question:
|
||||
"How do jobs usually start for your shop?"
|
||||
|
||||
### Options:
|
||||
|
||||
#### Option A:
|
||||
Title: "I send a quote first"
|
||||
Description: "Create a quote, convert it to a job, then invoice when work is complete."
|
||||
|
||||
#### Option B:
|
||||
Title: "I start with a job"
|
||||
Description: "For walk-ins or approved work where you start immediately."
|
||||
|
||||
---
|
||||
|
||||
### On Selection:
|
||||
Save:
|
||||
- onboardingPath = "quote_first" | "job_first"
|
||||
|
||||
Then continue into guided flow
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Guided Flow
|
||||
|
||||
### Path A — Quote First
|
||||
|
||||
#### Step A1: Create Quote
|
||||
- Use existing quote creation logic
|
||||
- Pre-fill fields:
|
||||
- Customer: "Sample Customer"
|
||||
- Item: "Wheel Set"
|
||||
- Quantity: 4
|
||||
- Notes: "Sample onboarding quote"
|
||||
- Allow editing before submit
|
||||
|
||||
#### Step A2: Show Quote Created
|
||||
Message:
|
||||
"This is the quote you would send to your customer."
|
||||
|
||||
CTA:
|
||||
"Convert to Job"
|
||||
|
||||
#### Step A3: Convert Quote → Job
|
||||
- Use existing conversion logic
|
||||
|
||||
#### Step A4: Show Job
|
||||
Message:
|
||||
"This job is now tracked in your workflow."
|
||||
|
||||
CTA:
|
||||
"Create Invoice" (if supported)
|
||||
|
||||
#### Step A5: Create Invoice (optional)
|
||||
- Use existing invoice logic
|
||||
|
||||
#### Completion:
|
||||
Set:
|
||||
- firstWorkflowCompleted = true
|
||||
|
||||
---
|
||||
|
||||
### Path B — Job First
|
||||
|
||||
#### Step B1: Create Job
|
||||
- Use existing job creation logic
|
||||
- Pre-fill:
|
||||
- Customer: "Walk-in Customer"
|
||||
- Item: "Wheel Set"
|
||||
- Quantity: 4
|
||||
- Notes: "Sample onboarding job"
|
||||
|
||||
#### Step B2: Show Job
|
||||
Message:
|
||||
"This job is now in your workflow."
|
||||
|
||||
CTA:
|
||||
"Create Invoice" (optional)
|
||||
|
||||
#### Step B3: Create Invoice (optional)
|
||||
|
||||
#### Completion:
|
||||
Set:
|
||||
- firstWorkflowCompleted = true
|
||||
|
||||
---
|
||||
|
||||
## Skipping
|
||||
|
||||
Provide "Skip for now" option.
|
||||
|
||||
If skipped:
|
||||
- DO NOT set firstWorkflowCompleted
|
||||
- Redirect to dashboard
|
||||
- Continue showing activation banner
|
||||
|
||||
---
|
||||
|
||||
## Dashboard Behavior
|
||||
|
||||
If:
|
||||
- setup complete
|
||||
- AND firstWorkflowCompleted == false
|
||||
|
||||
Show persistent banner:
|
||||
|
||||
Title:
|
||||
"Create your first job or quote"
|
||||
|
||||
Text:
|
||||
"Run a quick 2-minute workflow to see how the system works."
|
||||
|
||||
CTA:
|
||||
"Start first workflow"
|
||||
|
||||
---
|
||||
|
||||
## Data Model Changes
|
||||
|
||||
Add to Company or User:
|
||||
|
||||
- onboardingPath: string | null
|
||||
- firstWorkflowCompleted: boolean
|
||||
|
||||
Optional:
|
||||
- firstQuoteCreatedAt: datetime
|
||||
- firstJobCreatedAt: datetime
|
||||
- firstInvoiceCreatedAt: datetime
|
||||
|
||||
---
|
||||
|
||||
## Events / Tracking (if system exists)
|
||||
|
||||
Track:
|
||||
- onboarding_path_selected
|
||||
- first_quote_created
|
||||
- first_job_created
|
||||
- first_invoice_created
|
||||
- first_workflow_completed
|
||||
- first_workflow_skipped
|
||||
|
||||
---
|
||||
|
||||
## Implementation Constraints
|
||||
|
||||
- MUST reuse existing quote/job/invoice logic
|
||||
- DO NOT duplicate business logic
|
||||
- DO NOT create separate fake systems
|
||||
- Use existing forms and APIs where possible
|
||||
- Keep UI minimal and fast
|
||||
- Pre-fill as much as possible
|
||||
|
||||
---
|
||||
|
||||
## UX Requirements
|
||||
|
||||
- No tooltip tours
|
||||
- Linear guided flow only
|
||||
- One action at a time
|
||||
- Minimize user effort
|
||||
- Show immediate visual feedback
|
||||
|
||||
---
|
||||
|
||||
## Developer Instructions
|
||||
|
||||
Before coding:
|
||||
1. Inspect setup wizard completion logic
|
||||
2. Identify routing after setup
|
||||
3. Identify quote/job/invoice creation flows
|
||||
4. Identify data model structure
|
||||
|
||||
Then:
|
||||
5. Propose implementation plan
|
||||
6. Wait for approval
|
||||
7. Implement incrementally
|
||||
8. Summarize changes
|
||||
9. Provide manual QA steps
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- % of users creating first job increases significantly
|
||||
- Users complete at least one workflow during onboarding
|
||||
- Reduced drop-off after setup wizard
|
||||
|
||||
Target:
|
||||
≥ 30% of new users create at least one job or quote
|
||||
@@ -0,0 +1,173 @@
|
||||
Add a dashboard progress widget for post-onboarding activation.
|
||||
|
||||
Context:
|
||||
This is a powder coating shop management app. We recently shortened the setup wizard and added a guided activation flow. Some setup items are intentionally deferred so users can evaluate the system quickly before fully configuring everything.
|
||||
|
||||
Goal:
|
||||
Create a dashboard widget that helps users “get the most out of their shop” without making it feel like unfinished homework.
|
||||
|
||||
Do NOT call it “Complete setup.”
|
||||
|
||||
Recommended title:
|
||||
“Get the most out of your shop”
|
||||
|
||||
Purpose:
|
||||
Show progress based on real usage/configuration milestones and give users clear next actions.
|
||||
|
||||
Requirements:
|
||||
|
||||
1. Inspect existing dashboard structure
|
||||
|
||||
* Locate the dashboard controller/view/components.
|
||||
* Reuse existing card, alert, progress bar, and button styles.
|
||||
* Follow existing UI conventions.
|
||||
|
||||
2. Widget visibility
|
||||
Show the widget for companies that:
|
||||
|
||||
* Have completed the setup wizard
|
||||
* Are not yet meaningfully activated OR still have recommended setup tasks incomplete
|
||||
|
||||
It is okay to keep showing it until all tasks are complete.
|
||||
|
||||
3. Progress calculation
|
||||
Create a checklist of 5–6 items max.
|
||||
|
||||
Suggested items:
|
||||
|
||||
A. Create your first quote or job
|
||||
Complete when:
|
||||
|
||||
* company has at least one quote OR at least one job
|
||||
|
||||
CTA:
|
||||
|
||||
* “Create quote/job” or “Start workflow”
|
||||
|
||||
B. Move a job through your workflow
|
||||
Complete when:
|
||||
|
||||
* at least one job has had a status/stage change
|
||||
* If there is no existing way to detect this, use the closest available activity/history/status timestamp
|
||||
|
||||
CTA:
|
||||
|
||||
* “Open daily board”
|
||||
|
||||
C. Create your first invoice
|
||||
Complete when:
|
||||
|
||||
* company has at least one invoice
|
||||
|
||||
CTA:
|
||||
|
||||
* “Create invoice”
|
||||
|
||||
D. Invite your team
|
||||
Complete when:
|
||||
|
||||
* company has more than one active user/team member
|
||||
|
||||
CTA:
|
||||
|
||||
* “Invite team”
|
||||
|
||||
E. Customize pricing
|
||||
Complete when:
|
||||
|
||||
* company has configured pricing tiers/custom pricing settings beyond defaults
|
||||
* If this is hard to detect reliably, make this optional or use a simple existing flag/count
|
||||
|
||||
CTA:
|
||||
|
||||
* “Customize pricing”
|
||||
|
||||
F. Review payment terms
|
||||
Complete when:
|
||||
|
||||
* company has customized payment terms from default
|
||||
* If this is hard to detect reliably, make this optional or use a simple existing flag/value comparison
|
||||
|
||||
CTA:
|
||||
|
||||
* “Review terms”
|
||||
|
||||
4. UX copy
|
||||
Use friendly, value-focused language.
|
||||
|
||||
Widget title:
|
||||
“Get the most out of your shop”
|
||||
|
||||
Subtitle:
|
||||
“Complete a few quick steps to unlock the full workflow.”
|
||||
|
||||
Progress text:
|
||||
“X of Y complete”
|
||||
|
||||
Avoid wording like:
|
||||
|
||||
* “Incomplete setup”
|
||||
* “Missing configuration”
|
||||
* “Required steps”
|
||||
|
||||
5. Visual design
|
||||
|
||||
* Use a card-style widget near the top of the dashboard.
|
||||
* Include a progress bar.
|
||||
* Show checklist rows with completed and incomplete states.
|
||||
* Completed items should feel rewarding.
|
||||
* Incomplete items should have one clear CTA.
|
||||
* Keep it compact and non-annoying.
|
||||
|
||||
6. Behavior
|
||||
|
||||
* Each checklist item should link to the most relevant existing page or action.
|
||||
* Do not build new duplicate workflows.
|
||||
* Reuse existing guided activation route for “Create your first quote or job” if available.
|
||||
* If a task cannot be detected reliably yet, implement it conservatively or leave a TODO comment explaining why.
|
||||
|
||||
7. Data/query logic
|
||||
|
||||
* Prefer calculating progress server-side in the dashboard view model.
|
||||
* Avoid expensive queries.
|
||||
* Reuse existing repositories/services if available.
|
||||
* Keep the logic readable and testable.
|
||||
|
||||
8. Dismissal behavior
|
||||
Add optional dismissal if easy:
|
||||
|
||||
* Let user collapse or dismiss the widget.
|
||||
* If dismissed, do not permanently hide it forever unless all tasks are complete.
|
||||
* Prefer “collapse” over full dismissal.
|
||||
* Store dismissal/collapse state only if there is already a simple place to store dashboard preferences.
|
||||
|
||||
9. Important product guidance
|
||||
This widget should guide users from evaluation into real adoption.
|
||||
|
||||
The emotional framing should be:
|
||||
“You’re already making progress — here are the next valuable things to try.”
|
||||
|
||||
Not:
|
||||
“You failed to finish setup.”
|
||||
|
||||
10. Implementation style
|
||||
Before coding:
|
||||
|
||||
* Inspect relevant dashboard, setup wizard, guided activation, company preference, quote, job, invoice, user/team, pricing, and payment term structures.
|
||||
* Propose a concise implementation plan.
|
||||
* Then implement incrementally.
|
||||
|
||||
After coding:
|
||||
|
||||
* Summarize changed files.
|
||||
* Explain how progress is calculated.
|
||||
* Provide manual QA steps.
|
||||
|
||||
Manual QA scenarios:
|
||||
|
||||
* Brand new company after setup wizard
|
||||
* Company with first quote/job created
|
||||
* Company with moved job/status change
|
||||
* Company with invoice created
|
||||
* Company with invited team member
|
||||
* Company with all tasks complete
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
BEGIN TRANSACTION;
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [FirstInvoiceCreatedAt] datetime2 NULL;
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [FirstJobCreatedAt] datetime2 NULL;
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [FirstQuoteCreatedAt] datetime2 NULL;
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [FirstWorkflowCompleted] bit NOT NULL DEFAULT CAST(0 AS bit);
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [FirstWorkflowCompletedAt] datetime2 NULL;
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [GuidedActivationDismissedAt] datetime2 NULL;
|
||||
GO
|
||||
|
||||
ALTER TABLE [CompanyPreferences] ADD [OnboardingPath] nvarchar(max) NULL;
|
||||
GO
|
||||
|
||||
UPDATE [PricingTiers] SET [CreatedAt] = '2026-04-28T16:40:22.3595055Z'
|
||||
WHERE [Id] = 1;
|
||||
SELECT @@ROWCOUNT;
|
||||
|
||||
GO
|
||||
|
||||
UPDATE [PricingTiers] SET [CreatedAt] = '2026-04-28T16:40:22.3595063Z'
|
||||
WHERE [Id] = 2;
|
||||
SELECT @@ROWCOUNT;
|
||||
|
||||
GO
|
||||
|
||||
UPDATE [PricingTiers] SET [CreatedAt] = '2026-04-28T16:40:22.3595065Z'
|
||||
WHERE [Id] = 3;
|
||||
SELECT @@ROWCOUNT;
|
||||
|
||||
GO
|
||||
|
||||
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
|
||||
VALUES (N'20260428164026_AddGuidedActivationFields', N'8.0.11');
|
||||
GO
|
||||
|
||||
COMMIT;
|
||||
GO
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,319 @@
|
||||
# Discover-Prismatic-Product-Urls-By-ColorParam.ps1
|
||||
#
|
||||
# Discovers Prismatic Powders product URLs by visiting color filter URLs like:
|
||||
# https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_red
|
||||
#
|
||||
# Outputs:
|
||||
# .\product-urls.txt
|
||||
# .\color-discovery-log.json
|
||||
#
|
||||
# First-time setup:
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
# .\Discover-Prismatic-Product-Urls-By-ColorParam.ps1 -InstallPlaywright -Headed
|
||||
#
|
||||
# Normal run:
|
||||
# .\Discover-Prismatic-Product-Urls-By-ColorParam.ps1
|
||||
#
|
||||
# Watch browser:
|
||||
# .\Discover-Prismatic-Product-Urls-By-ColorParam.ps1 -Headed
|
||||
|
||||
param(
|
||||
[switch]$InstallPlaywright,
|
||||
[switch]$Headed,
|
||||
[int]$MaxScrollsPerColor = 180,
|
||||
[int]$StopAfterNoNewScrolls = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Ensure-NodeAvailable {
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
throw "Node.js is required. Install Node.js LTS from https://nodejs.org/"
|
||||
}
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required. It usually comes with Node.js."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PlaywrightIfNeeded {
|
||||
param([bool]$Requested)
|
||||
|
||||
Ensure-NodeAvailable
|
||||
|
||||
if ($Requested -or -not (Test-Path ".\node_modules\playwright")) {
|
||||
Write-Host "Installing Playwright package locally..."
|
||||
npm init -y | Out-Null
|
||||
npm install playwright | Out-Null
|
||||
|
||||
Write-Host "Installing Playwright Chromium browser..."
|
||||
npx playwright install chromium
|
||||
}
|
||||
}
|
||||
|
||||
function Write-NodeDiscoveryScript {
|
||||
$js = @'
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
|
||||
function getArgValue(name, defaultValue) {
|
||||
const prefix = `--${name}=`;
|
||||
const found = process.argv.find(x => x.startsWith(prefix));
|
||||
return found ? found.slice(prefix.length) : defaultValue;
|
||||
}
|
||||
|
||||
const maxScrollsPerColor = parseInt(getArgValue("max-scrolls-per-color", "180"), 10);
|
||||
const stopAfterNoNewScrolls = parseInt(getArgValue("stop-after-no-new-scrolls", "10"), 10);
|
||||
|
||||
const baseUrl = "https://www.prismaticpowders.com/shop/powder-coating-colors";
|
||||
const outputFile = "product-urls.txt";
|
||||
const logFile = "color-discovery-log.json";
|
||||
|
||||
// Update this list if you find more color params in the site HTML.
|
||||
const colorParams = [
|
||||
"pris_black",
|
||||
"pris_blue",
|
||||
"pris_bronze",
|
||||
"pris_brown",
|
||||
"pris_clear",
|
||||
"pris_copper",
|
||||
"pris_gold",
|
||||
"pris_gray",
|
||||
"pris_green",
|
||||
"pris_orange",
|
||||
"pris_pink",
|
||||
"pris_purple",
|
||||
"pris_red",
|
||||
"pris_silver",
|
||||
"pris_tan",
|
||||
"pris_white",
|
||||
"pris_yellow"
|
||||
];
|
||||
|
||||
function cleanUrl(url) {
|
||||
return (url || "").split("?")[0].split("#")[0].trim();
|
||||
}
|
||||
|
||||
function isProductUrl(url) {
|
||||
return /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(url || "");
|
||||
}
|
||||
|
||||
function readExistingUrls() {
|
||||
if (!fs.existsSync(outputFile)) return [];
|
||||
|
||||
return fs.readFileSync(outputFile, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map(cleanUrl)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function writeUrls(urls) {
|
||||
const sorted = [...urls].sort();
|
||||
fs.writeFileSync(outputFile, sorted.join("\r\n") + "\r\n", "utf8");
|
||||
}
|
||||
|
||||
function readLog() {
|
||||
if (!fs.existsSync(logFile)) {
|
||||
return {
|
||||
completed_colors: {},
|
||||
runs: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(logFile, "utf8"));
|
||||
} catch {
|
||||
return {
|
||||
completed_colors: {},
|
||||
runs: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(log) {
|
||||
fs.writeFileSync(logFile, JSON.stringify(log, null, 2), "utf8");
|
||||
}
|
||||
|
||||
async function collectProductLinks(page) {
|
||||
const links = await page.locator("a").evaluateAll(anchors =>
|
||||
anchors
|
||||
.map(a => a.href)
|
||||
.filter(Boolean)
|
||||
.filter(h => /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(h))
|
||||
);
|
||||
|
||||
return links.map(cleanUrl).filter(Boolean);
|
||||
}
|
||||
|
||||
async function scrollAndCollect(page, urls, label) {
|
||||
let noNewScrolls = 0;
|
||||
let totalAddedForThisColor = 0;
|
||||
|
||||
for (let i = 0; i < maxScrollsPerColor; i++) {
|
||||
const before = urls.size;
|
||||
|
||||
for (const link of await collectProductLinks(page)) {
|
||||
urls.add(link);
|
||||
}
|
||||
|
||||
const after = urls.size;
|
||||
const added = after - before;
|
||||
totalAddedForThisColor += added;
|
||||
|
||||
if (added === 0) {
|
||||
noNewScrolls++;
|
||||
} else {
|
||||
noNewScrolls = 0;
|
||||
}
|
||||
|
||||
writeUrls(urls);
|
||||
|
||||
console.log(`[${label}] Scroll ${i + 1}/${maxScrollsPerColor}: +${added}, total ${after}, no-new ${noNewScrolls}`);
|
||||
|
||||
if (noNewScrolls >= stopAfterNoNewScrolls) {
|
||||
break;
|
||||
}
|
||||
|
||||
await page.mouse.wheel(0, 2500);
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
return totalAddedForThisColor;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const existingUrls = readExistingUrls();
|
||||
const urls = new Set(existingUrls);
|
||||
const log = readLog();
|
||||
|
||||
console.log(`Existing URLs in ${outputFile}: ${existingUrls.length}`);
|
||||
|
||||
const browser = await chromium.launch({ headless: !headed });
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
const runRecord = {
|
||||
started_at: new Date().toISOString(),
|
||||
existing_at_start: existingUrls.length,
|
||||
colors_attempted: []
|
||||
};
|
||||
|
||||
for (const color of colorParams) {
|
||||
if (log.completed_colors[color]) {
|
||||
console.log(`Skipping completed color: ${color}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = `${baseUrl}?color=${encodeURIComponent(color)}`;
|
||||
console.log("");
|
||||
console.log(`Opening color filter: ${color}`);
|
||||
console.log(url);
|
||||
|
||||
try {
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const status = response ? response.status() : "unknown";
|
||||
console.log(`HTTP status: ${status}`);
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const before = urls.size;
|
||||
const addedDuringScroll = await scrollAndCollect(page, urls, color);
|
||||
const after = urls.size;
|
||||
const netAdded = after - before;
|
||||
|
||||
log.completed_colors[color] = {
|
||||
url,
|
||||
http_status: status,
|
||||
added: netAdded,
|
||||
added_during_scroll: addedDuringScroll,
|
||||
total_after: after,
|
||||
completed_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
runRecord.colors_attempted.push({
|
||||
color,
|
||||
url,
|
||||
http_status: status,
|
||||
added: netAdded,
|
||||
total_after: after
|
||||
});
|
||||
|
||||
writeLog(log);
|
||||
writeUrls(urls);
|
||||
|
||||
console.log(`Color complete: ${color}; added ${netAdded}; total ${after}`);
|
||||
|
||||
// Polite pause between filters.
|
||||
await page.waitForTimeout(3000);
|
||||
} catch (err) {
|
||||
console.log(`Color failed: ${color}; ${err.message}`);
|
||||
|
||||
runRecord.colors_attempted.push({
|
||||
color,
|
||||
url,
|
||||
added: 0,
|
||||
error: err.message
|
||||
});
|
||||
|
||||
writeLog(log);
|
||||
}
|
||||
}
|
||||
|
||||
runRecord.finished_at = new Date().toISOString();
|
||||
runRecord.final_total = urls.size;
|
||||
runRecord.new_this_run = urls.size - existingUrls.length;
|
||||
|
||||
log.runs.push(runRecord);
|
||||
writeLog(log);
|
||||
writeUrls(urls);
|
||||
|
||||
console.log("");
|
||||
console.log("Color-param discovery complete.");
|
||||
console.log(`Existing at start: ${existingUrls.length}`);
|
||||
console.log(`Final total: ${urls.size}`);
|
||||
console.log(`New this run: ${urls.size - existingUrls.length}`);
|
||||
console.log(`Output: ${outputFile}`);
|
||||
console.log(`Log: ${logFile}`);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
'@
|
||||
|
||||
Set-Content -Path ".\discover-prismatic-by-color-param.js" -Value $js -Encoding UTF8
|
||||
}
|
||||
|
||||
try {
|
||||
Install-PlaywrightIfNeeded -Requested:$InstallPlaywright
|
||||
Write-NodeDiscoveryScript
|
||||
|
||||
Write-Host "Running color-param URL discovery..."
|
||||
|
||||
$nodeArgs = @(
|
||||
".\discover-prismatic-by-color-param.js",
|
||||
"--max-scrolls-per-color=$MaxScrollsPerColor",
|
||||
"--stop-after-no-new-scrolls=$StopAfterNoNewScrolls"
|
||||
)
|
||||
|
||||
if ($Headed) {
|
||||
$nodeArgs += "--headed"
|
||||
}
|
||||
|
||||
node @nodeArgs
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
# Get-Product-Info-Resumable.ps1
|
||||
#
|
||||
# Resumable, slow/polite Prismatic Powders product scraper.
|
||||
#
|
||||
# Inputs:
|
||||
# .\product-urls.txt
|
||||
#
|
||||
# Outputs:
|
||||
# .\prismatic_powders.json
|
||||
# .\prismatic-scrape-progress.log
|
||||
#
|
||||
# First-time setup:
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
# .\Get-Product-Info-Resumable.ps1 -InstallPlaywright -Headed -MaxProducts 5
|
||||
#
|
||||
# Normal full run:
|
||||
# .\Get-Product-Info-Resumable.ps1
|
||||
#
|
||||
# Test first 25 remaining:
|
||||
# .\Get-Product-Info-Resumable.ps1 -MaxProducts 25 -Headed
|
||||
#
|
||||
# Retry failed URLs too:
|
||||
# .\Get-Product-Info-Resumable.ps1 -RetryErrors
|
||||
#
|
||||
# Slow it down more:
|
||||
# .\Get-Product-Info-Resumable.ps1 -MinDelaySeconds 12 -MaxDelaySeconds 25
|
||||
|
||||
param(
|
||||
[switch]$InstallPlaywright,
|
||||
[switch]$Headed,
|
||||
|
||||
[string]$InputFile = ".\product-urls.txt",
|
||||
[string]$OutputJson = ".\prismatic_powders.json",
|
||||
[string]$ProgressLog = ".\prismatic-scrape-progress.log",
|
||||
|
||||
[int]$MinDelaySeconds = 4,
|
||||
[int]$MaxDelaySeconds = 10,
|
||||
[int]$PageSettleSeconds = 4,
|
||||
|
||||
# 0 means no limit.
|
||||
[int]$MaxProducts = 0,
|
||||
|
||||
# By default, URLs in errors are skipped on resume.
|
||||
# Use -RetryErrors to try failed URLs again.
|
||||
[switch]$RetryErrors
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Ensure-NodeAvailable {
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
throw "Node.js is required. Install Node.js LTS from https://nodejs.org/"
|
||||
}
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required. It usually comes with Node.js."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PlaywrightIfNeeded {
|
||||
param([bool]$Requested)
|
||||
|
||||
Ensure-NodeAvailable
|
||||
|
||||
if ($Requested -or -not (Test-Path ".\node_modules\playwright")) {
|
||||
Write-Host "Installing Playwright package locally..."
|
||||
npm init -y | Out-Null
|
||||
npm install playwright | Out-Null
|
||||
|
||||
Write-Host "Installing Playwright Chromium browser..."
|
||||
npx playwright install chromium
|
||||
}
|
||||
}
|
||||
|
||||
function Write-NodeScraper {
|
||||
$js = @'
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
const retryErrors = process.argv.includes("--retry-errors");
|
||||
|
||||
function getArgValue(name, defaultValue) {
|
||||
const prefix = `--${name}=`;
|
||||
const found = process.argv.find(x => x.startsWith(prefix));
|
||||
return found ? found.slice(prefix.length) : defaultValue;
|
||||
}
|
||||
|
||||
const inputFile = getArgValue("input-file", "product-urls.txt");
|
||||
const outputJson = getArgValue("output-json", "prismatic_powders.json");
|
||||
const progressLog = getArgValue("progress-log", "prismatic-scrape-progress.log");
|
||||
|
||||
const minDelaySeconds = parseInt(getArgValue("min-delay-seconds", "8"), 10);
|
||||
const maxDelaySeconds = parseInt(getArgValue("max-delay-seconds", "18"), 10);
|
||||
const pageSettleSeconds = parseInt(getArgValue("page-settle-seconds", "4"), 10);
|
||||
const maxProducts = parseInt(getArgValue("max-products", "0"), 10);
|
||||
|
||||
function clean(text) {
|
||||
return (text || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function cleanUrl(url) {
|
||||
return (url || "").split("?")[0].split("#")[0].trim();
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function randomDelayMs() {
|
||||
const minMs = Math.max(0, minDelaySeconds * 1000);
|
||||
const maxMs = Math.max(minMs, maxDelaySeconds * 1000);
|
||||
return Math.floor(minMs + Math.random() * (maxMs - minMs + 1));
|
||||
}
|
||||
|
||||
function logLine(message) {
|
||||
const line = `[${new Date().toISOString()}] ${message}`;
|
||||
console.log(line);
|
||||
fs.appendFileSync(progressLog, line + "\r\n", "utf8");
|
||||
}
|
||||
|
||||
function absoluteUrl(baseUrl, maybeUrl) {
|
||||
if (!maybeUrl) return "";
|
||||
try {
|
||||
return new URL(maybeUrl, baseUrl).href;
|
||||
} catch {
|
||||
return maybeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function loadInputUrls() {
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
throw new Error(`Input file not found: ${inputFile}`);
|
||||
}
|
||||
|
||||
const urls = fs.readFileSync(inputFile, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map(cleanUrl)
|
||||
.filter(Boolean)
|
||||
.filter(x => !x.startsWith("#"))
|
||||
.filter(x => /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(x));
|
||||
|
||||
return [...new Set(urls)];
|
||||
}
|
||||
|
||||
function loadOutput() {
|
||||
if (!fs.existsSync(outputJson)) {
|
||||
return { results: [], errors: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(outputJson, "utf8"));
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return { results: parsed, errors: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
results: Array.isArray(parsed.results) ? parsed.results : [],
|
||||
errors: Array.isArray(parsed.errors) ? parsed.errors : []
|
||||
};
|
||||
} catch (err) {
|
||||
const backup = `${outputJson}.invalid-${Date.now()}.bak`;
|
||||
fs.copyFileSync(outputJson, backup);
|
||||
throw new Error(`Could not parse existing ${outputJson}. Backed it up to ${backup}. Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function saveOutput(data) {
|
||||
const tempFile = `${outputJson}.tmp`;
|
||||
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2), "utf8");
|
||||
fs.renameSync(tempFile, outputJson);
|
||||
}
|
||||
|
||||
function parsePriceTiers(plainText) {
|
||||
const priceMatches = [...plainText.matchAll(/(\d+\s*-\s*\d+\s*lbs|\d+\s*\+\s*lbs)\s*\$([\d.]+)/gi)];
|
||||
|
||||
return priceMatches.map(m => {
|
||||
const rangeText = clean(m[1]);
|
||||
const price = parseFloat(m[2]);
|
||||
|
||||
let min = null;
|
||||
let max = null;
|
||||
|
||||
const rangeMatch = rangeText.match(/(\d+)\s*-\s*(\d+)/);
|
||||
if (rangeMatch) {
|
||||
min = parseInt(rangeMatch[1], 10);
|
||||
max = parseInt(rangeMatch[2], 10);
|
||||
}
|
||||
|
||||
const plusMatch = rangeText.match(/(\d+)\s*\+/);
|
||||
if (plusMatch) {
|
||||
min = parseInt(plusMatch[1], 10);
|
||||
max = null;
|
||||
}
|
||||
|
||||
return { min, max, price };
|
||||
});
|
||||
}
|
||||
|
||||
async function getLinkByText(page, patterns) {
|
||||
const links = await page.locator("a").evaluateAll((anchors) =>
|
||||
anchors.map(a => ({
|
||||
text: (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim(),
|
||||
href: a.getAttribute("href") || ""
|
||||
}))
|
||||
);
|
||||
|
||||
for (const link of links) {
|
||||
if (patterns.some(p => new RegExp(p, "i").test(link.text))) {
|
||||
return absoluteUrl(page.url(), link.href);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function getSampleImageUrl(page) {
|
||||
const imageUrls = await page.locator("img").evaluateAll((imgs) =>
|
||||
imgs.map(img =>
|
||||
img.currentSrc ||
|
||||
img.src ||
|
||||
img.getAttribute("src") ||
|
||||
img.getAttribute("data-src") ||
|
||||
""
|
||||
).filter(Boolean)
|
||||
);
|
||||
|
||||
return (
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src) && !/thumbnail/i.test(src)) ||
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src)) ||
|
||||
imageUrls.find(src => /prismatic|powder|color/i.test(src)) ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
async function parseProduct(page, url) {
|
||||
logLine(`Scraping ${url}`);
|
||||
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
await page.waitForTimeout(pageSettleSeconds * 1000);
|
||||
|
||||
const status = response ? response.status() : 0;
|
||||
const pageTitle = clean(await page.title().catch(() => ""));
|
||||
const plainText = clean(await page.locator("body").innerText().catch(() => ""));
|
||||
|
||||
logLine(`HTTP status ${status}; title "${pageTitle}"`);
|
||||
|
||||
if (status === 403 || /^403 Forbidden$/i.test(pageTitle) || /^403 Forbidden$/i.test(plainText)) {
|
||||
throw new Error("403 Forbidden returned by site.");
|
||||
}
|
||||
|
||||
if (status === 404 || /404|Page Not Found/i.test(pageTitle)) {
|
||||
throw new Error("404 Not Found returned by site.");
|
||||
}
|
||||
|
||||
const title = clean(await page.locator("h1").first().innerText().catch(() => ""));
|
||||
|
||||
const skuMatch = plainText.match(/Item:\s*([A-Z0-9-]+)/i);
|
||||
const sku = skuMatch ? skuMatch[1] : "";
|
||||
|
||||
if (!sku && !title) {
|
||||
throw new Error("Could not find SKU or title on product page.");
|
||||
}
|
||||
|
||||
const descMatch = plainText.match(/Description:\s*(.*?)(WARNING:|What does this match\?|$)/is);
|
||||
const description = descMatch ? clean(descMatch[1]) : "";
|
||||
|
||||
const priceTiers = parsePriceTiers(plainText);
|
||||
|
||||
const safetyDataSheetUrl = await getLinkByText(page, ["Safety Data Sheet", "\\bSDS\\b"]);
|
||||
const applicationGuideUrl = await getLinkByText(page, ["Application Guide"]);
|
||||
const technicalDataSheetUrl = await getLinkByText(page, ["Tech Data Sheet", "Technical Data Sheet", "\\bTDS\\b"]);
|
||||
const sampleImageUrl = await getSampleImageUrl(page);
|
||||
|
||||
return {
|
||||
sku,
|
||||
color_name: title,
|
||||
description,
|
||||
price_tiers: priceTiers,
|
||||
safety_data_sheet_url: safetyDataSheetUrl,
|
||||
technical_data_sheet_url: technicalDataSheetUrl,
|
||||
application_guide_url: applicationGuideUrl,
|
||||
sample_image_url: sampleImageUrl,
|
||||
product_url: url,
|
||||
scraped_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const allUrls = loadInputUrls();
|
||||
const data = loadOutput();
|
||||
|
||||
const completedUrls = new Set(data.results.map(r => cleanUrl(r.product_url)).filter(Boolean));
|
||||
const errorUrls = new Set(data.errors.map(e => cleanUrl(e.product_url)).filter(Boolean));
|
||||
|
||||
let remainingUrls = allUrls.filter(url => {
|
||||
if (completedUrls.has(url)) return false;
|
||||
if (!retryErrors && errorUrls.has(url)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (maxProducts > 0) {
|
||||
remainingUrls = remainingUrls.slice(0, maxProducts);
|
||||
}
|
||||
|
||||
logLine(`Input URLs: ${allUrls.length}`);
|
||||
logLine(`Already scraped: ${completedUrls.size}`);
|
||||
logLine(`Existing errors: ${errorUrls.size}`);
|
||||
logLine(`Retry errors: ${retryErrors ? "yes" : "no"}`);
|
||||
logLine(`This run target count: ${remainingUrls.length}`);
|
||||
logLine(`Delay range: ${minDelaySeconds}-${maxDelaySeconds} seconds; page settle: ${pageSettleSeconds} seconds`);
|
||||
|
||||
if (remainingUrls.length === 0) {
|
||||
logLine("Nothing to scrape. Done.");
|
||||
saveOutput(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: !headed
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
let processedThisRun = 0;
|
||||
|
||||
for (const url of remainingUrls) {
|
||||
try {
|
||||
const row = await parseProduct(page, url);
|
||||
|
||||
// If retrying an old error, keep the old error history but avoid duplicate successful result.
|
||||
if (!completedUrls.has(url)) {
|
||||
data.results.push(row);
|
||||
completedUrls.add(url);
|
||||
}
|
||||
|
||||
processedThisRun++;
|
||||
saveOutput(data);
|
||||
|
||||
logLine(`Saved result ${processedThisRun}/${remainingUrls.length}: ${row.sku || "(no sku)"} ${row.color_name || ""}`);
|
||||
} catch (err) {
|
||||
const errorRecord = {
|
||||
product_url: url,
|
||||
error: err.message,
|
||||
scraped_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
data.errors.push(errorRecord);
|
||||
saveOutput(data);
|
||||
|
||||
logLine(`ERROR ${url}: ${err.message}`);
|
||||
}
|
||||
|
||||
const delay = randomDelayMs();
|
||||
logLine(`Waiting ${(delay / 1000).toFixed(1)} seconds before next product...`);
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
logLine(`Done. Results: ${data.results.length}; Errors: ${data.errors.length}; Output: ${outputJson}`);
|
||||
})();
|
||||
'@
|
||||
|
||||
Set-Content -Path ".\prismatic-browser-scraper.js" -Value $js -Encoding UTF8
|
||||
}
|
||||
|
||||
try {
|
||||
Install-PlaywrightIfNeeded -Requested:$InstallPlaywright
|
||||
Write-NodeScraper
|
||||
|
||||
Write-Host "Running resumable browser scraper..."
|
||||
|
||||
$nodeArgs = @(
|
||||
".\prismatic-browser-scraper.js",
|
||||
"--input-file=$InputFile",
|
||||
"--output-json=$OutputJson",
|
||||
"--progress-log=$ProgressLog",
|
||||
"--min-delay-seconds=$MinDelaySeconds",
|
||||
"--max-delay-seconds=$MaxDelaySeconds",
|
||||
"--page-settle-seconds=$PageSettleSeconds",
|
||||
"--max-products=$MaxProducts"
|
||||
)
|
||||
|
||||
if ($Headed) {
|
||||
$nodeArgs += "--headed"
|
||||
}
|
||||
|
||||
if ($RetryErrors) {
|
||||
$nodeArgs += "--retry-errors"
|
||||
}
|
||||
|
||||
node @nodeArgs
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
# Get-Product-Info-Resumable.ps1
|
||||
#
|
||||
# Resumable, slow/polite Prismatic Powders product scraper.
|
||||
#
|
||||
# Inputs:
|
||||
# .\product-urls.txt
|
||||
#
|
||||
# Outputs:
|
||||
# .\prismatic_powders.json
|
||||
# .\prismatic-scrape-progress.log
|
||||
#
|
||||
# First-time setup:
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
# .\Get-Product-Info-Resumable.ps1 -InstallPlaywright -Headed -MaxProducts 5
|
||||
#
|
||||
# Normal full run:
|
||||
# .\Get-Product-Info-Resumable.ps1
|
||||
#
|
||||
# Test first 25 remaining:
|
||||
# .\Get-Product-Info-Resumable.ps1 -MaxProducts 25 -Headed
|
||||
#
|
||||
# Retry failed URLs too:
|
||||
# .\Get-Product-Info-Resumable.ps1 -RetryErrors
|
||||
#
|
||||
# Slow it down more:
|
||||
# .\Get-Product-Info-Resumable.ps1 -MinDelaySeconds 12 -MaxDelaySeconds 25
|
||||
|
||||
param(
|
||||
[switch]$InstallPlaywright,
|
||||
[switch]$Headed,
|
||||
|
||||
[string]$InputFile = ".\product-urls.txt",
|
||||
[string]$OutputJson = ".\prismatic_powders.json",
|
||||
[string]$ProgressLog = ".\prismatic-scrape-progress.log",
|
||||
|
||||
[int]$MinDelaySeconds = 8,
|
||||
[int]$MaxDelaySeconds = 18,
|
||||
[int]$PageSettleSeconds = 4,
|
||||
|
||||
# 0 means no limit.
|
||||
[int]$MaxProducts = 0,
|
||||
|
||||
# By default, URLs in errors are skipped on resume.
|
||||
# Use -RetryErrors to try failed URLs again.
|
||||
[switch]$RetryErrors
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Ensure-NodeAvailable {
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
throw "Node.js is required. Install Node.js LTS from https://nodejs.org/"
|
||||
}
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required. It usually comes with Node.js."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PlaywrightIfNeeded {
|
||||
param([bool]$Requested)
|
||||
|
||||
Ensure-NodeAvailable
|
||||
|
||||
if ($Requested -or -not (Test-Path ".\node_modules\playwright")) {
|
||||
Write-Host "Installing Playwright package locally..."
|
||||
npm init -y | Out-Null
|
||||
npm install playwright | Out-Null
|
||||
|
||||
Write-Host "Installing Playwright Chromium browser..."
|
||||
npx playwright install chromium
|
||||
}
|
||||
}
|
||||
|
||||
function Write-NodeScraper {
|
||||
$js = @'
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
const retryErrors = process.argv.includes("--retry-errors");
|
||||
|
||||
function getArgValue(name, defaultValue) {
|
||||
const prefix = `--${name}=`;
|
||||
const found = process.argv.find(x => x.startsWith(prefix));
|
||||
return found ? found.slice(prefix.length) : defaultValue;
|
||||
}
|
||||
|
||||
const inputFile = getArgValue("input-file", "product-urls.txt");
|
||||
const outputJson = getArgValue("output-json", "prismatic_powders.json");
|
||||
const progressLog = getArgValue("progress-log", "prismatic-scrape-progress.log");
|
||||
|
||||
const minDelaySeconds = parseInt(getArgValue("min-delay-seconds", "8"), 10);
|
||||
const maxDelaySeconds = parseInt(getArgValue("max-delay-seconds", "18"), 10);
|
||||
const pageSettleSeconds = parseInt(getArgValue("page-settle-seconds", "4"), 10);
|
||||
const maxProducts = parseInt(getArgValue("max-products", "0"), 10);
|
||||
|
||||
function clean(text) {
|
||||
return (text || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function cleanUrl(url) {
|
||||
return (url || "").split("?")[0].split("#")[0].trim();
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function randomDelayMs() {
|
||||
const minMs = Math.max(0, minDelaySeconds * 1000);
|
||||
const maxMs = Math.max(minMs, maxDelaySeconds * 1000);
|
||||
return Math.floor(minMs + Math.random() * (maxMs - minMs + 1));
|
||||
}
|
||||
|
||||
function logLine(message) {
|
||||
const line = `[${new Date().toISOString()}] ${message}`;
|
||||
console.log(line);
|
||||
fs.appendFileSync(progressLog, line + "\r\n", "utf8");
|
||||
}
|
||||
|
||||
function absoluteUrl(baseUrl, maybeUrl) {
|
||||
if (!maybeUrl) return "";
|
||||
try {
|
||||
return new URL(maybeUrl, baseUrl).href;
|
||||
} catch {
|
||||
return maybeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function loadInputUrls() {
|
||||
if (!fs.existsSync(inputFile)) {
|
||||
throw new Error(`Input file not found: ${inputFile}`);
|
||||
}
|
||||
|
||||
const urls = fs.readFileSync(inputFile, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map(cleanUrl)
|
||||
.filter(Boolean)
|
||||
.filter(x => !x.startsWith("#"))
|
||||
.filter(x => /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(x));
|
||||
|
||||
return [...new Set(urls)];
|
||||
}
|
||||
|
||||
function loadOutput() {
|
||||
if (!fs.existsSync(outputJson)) {
|
||||
return { results: [], errors: [] };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(outputJson, "utf8"));
|
||||
|
||||
if (Array.isArray(parsed)) {
|
||||
return { results: parsed, errors: [] };
|
||||
}
|
||||
|
||||
return {
|
||||
results: Array.isArray(parsed.results) ? parsed.results : [],
|
||||
errors: Array.isArray(parsed.errors) ? parsed.errors : []
|
||||
};
|
||||
} catch (err) {
|
||||
const backup = `${outputJson}.invalid-${Date.now()}.bak`;
|
||||
fs.copyFileSync(outputJson, backup);
|
||||
throw new Error(`Could not parse existing ${outputJson}. Backed it up to ${backup}. Error: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function saveOutput(data) {
|
||||
const tempFile = `${outputJson}.tmp`;
|
||||
fs.writeFileSync(tempFile, JSON.stringify(data, null, 2), "utf8");
|
||||
fs.renameSync(tempFile, outputJson);
|
||||
}
|
||||
|
||||
function parsePriceTiers(plainText) {
|
||||
const priceMatches = [...plainText.matchAll(/(\d+\s*-\s*\d+\s*lbs|\d+\s*\+\s*lbs)\s*\$([\d.]+)/gi)];
|
||||
|
||||
return priceMatches.map(m => {
|
||||
const rangeText = clean(m[1]);
|
||||
const price = parseFloat(m[2]);
|
||||
|
||||
let min = null;
|
||||
let max = null;
|
||||
|
||||
const rangeMatch = rangeText.match(/(\d+)\s*-\s*(\d+)/);
|
||||
if (rangeMatch) {
|
||||
min = parseInt(rangeMatch[1], 10);
|
||||
max = parseInt(rangeMatch[2], 10);
|
||||
}
|
||||
|
||||
const plusMatch = rangeText.match(/(\d+)\s*\+/);
|
||||
if (plusMatch) {
|
||||
min = parseInt(plusMatch[1], 10);
|
||||
max = null;
|
||||
}
|
||||
|
||||
return { min, max, price };
|
||||
});
|
||||
}
|
||||
|
||||
async function getLinkByText(page, patterns) {
|
||||
const links = await page.locator("a").evaluateAll((anchors) =>
|
||||
anchors.map(a => ({
|
||||
text: (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim(),
|
||||
href: a.getAttribute("href") || ""
|
||||
}))
|
||||
);
|
||||
|
||||
for (const link of links) {
|
||||
if (patterns.some(p => new RegExp(p, "i").test(link.text))) {
|
||||
return absoluteUrl(page.url(), link.href);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async function getSampleImageUrl(page) {
|
||||
const imageUrls = await page.locator("img").evaluateAll((imgs) =>
|
||||
imgs.map(img =>
|
||||
img.currentSrc ||
|
||||
img.src ||
|
||||
img.getAttribute("src") ||
|
||||
img.getAttribute("data-src") ||
|
||||
""
|
||||
).filter(Boolean)
|
||||
);
|
||||
|
||||
return (
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src) && !/thumbnail/i.test(src)) ||
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src)) ||
|
||||
imageUrls.find(src => /prismatic|powder|color/i.test(src)) ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
async function parseProduct(page, url) {
|
||||
logLine(`Scraping ${url}`);
|
||||
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
await page.waitForTimeout(pageSettleSeconds * 1000);
|
||||
|
||||
const status = response ? response.status() : 0;
|
||||
const pageTitle = clean(await page.title().catch(() => ""));
|
||||
const plainText = clean(await page.locator("body").innerText().catch(() => ""));
|
||||
|
||||
logLine(`HTTP status ${status}; title "${pageTitle}"`);
|
||||
|
||||
if (status === 403 || /^403 Forbidden$/i.test(pageTitle) || /^403 Forbidden$/i.test(plainText)) {
|
||||
throw new Error("403 Forbidden returned by site.");
|
||||
}
|
||||
|
||||
if (status === 404 || /404|Page Not Found/i.test(pageTitle)) {
|
||||
throw new Error("404 Not Found returned by site.");
|
||||
}
|
||||
|
||||
const title = clean(await page.locator("h1").first().innerText().catch(() => ""));
|
||||
|
||||
const skuMatch = plainText.match(/Item:\s*([A-Z0-9-]+)/i);
|
||||
const sku = skuMatch ? skuMatch[1] : "";
|
||||
|
||||
if (!sku && !title) {
|
||||
throw new Error("Could not find SKU or title on product page.");
|
||||
}
|
||||
|
||||
const descMatch = plainText.match(/Description:\s*(.*?)(WARNING:|What does this match\?|$)/is);
|
||||
const description = descMatch ? clean(descMatch[1]) : "";
|
||||
|
||||
const priceTiers = parsePriceTiers(plainText);
|
||||
|
||||
const safetyDataSheetUrl = await getLinkByText(page, ["Safety Data Sheet", "\\bSDS\\b"]);
|
||||
const applicationGuideUrl = await getLinkByText(page, ["Application Guide"]);
|
||||
const technicalDataSheetUrl = await getLinkByText(page, ["Tech Data Sheet", "Technical Data Sheet", "\\bTDS\\b"]);
|
||||
const sampleImageUrl = await getSampleImageUrl(page);
|
||||
|
||||
return {
|
||||
sku,
|
||||
color_name: title,
|
||||
description,
|
||||
price_tiers: priceTiers,
|
||||
safety_data_sheet_url: safetyDataSheetUrl,
|
||||
technical_data_sheet_url: technicalDataSheetUrl,
|
||||
application_guide_url: applicationGuideUrl,
|
||||
sample_image_url: sampleImageUrl,
|
||||
product_url: url,
|
||||
scraped_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const allUrls = loadInputUrls();
|
||||
const data = loadOutput();
|
||||
|
||||
const completedUrls = new Set(data.results.map(r => cleanUrl(r.product_url)).filter(Boolean));
|
||||
const errorUrls = new Set(data.errors.map(e => cleanUrl(e.product_url)).filter(Boolean));
|
||||
|
||||
let remainingUrls = allUrls.filter(url => {
|
||||
if (completedUrls.has(url)) return false;
|
||||
if (!retryErrors && errorUrls.has(url)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (maxProducts > 0) {
|
||||
remainingUrls = remainingUrls.slice(0, maxProducts);
|
||||
}
|
||||
|
||||
logLine(`Input URLs: ${allUrls.length}`);
|
||||
logLine(`Already scraped: ${completedUrls.size}`);
|
||||
logLine(`Existing errors: ${errorUrls.size}`);
|
||||
logLine(`Retry errors: ${retryErrors ? "yes" : "no"}`);
|
||||
logLine(`This run target count: ${remainingUrls.length}`);
|
||||
logLine(`Delay range: ${minDelaySeconds}-${maxDelaySeconds} seconds; page settle: ${pageSettleSeconds} seconds`);
|
||||
|
||||
if (remainingUrls.length === 0) {
|
||||
logLine("Nothing to scrape. Done.");
|
||||
saveOutput(data);
|
||||
return;
|
||||
}
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: !headed
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
let processedThisRun = 0;
|
||||
|
||||
for (const url of remainingUrls) {
|
||||
try {
|
||||
const row = await parseProduct(page, url);
|
||||
|
||||
// If retrying an old error, keep the old error history but avoid duplicate successful result.
|
||||
if (!completedUrls.has(url)) {
|
||||
data.results.push(row);
|
||||
completedUrls.add(url);
|
||||
}
|
||||
|
||||
processedThisRun++;
|
||||
saveOutput(data);
|
||||
|
||||
logLine(`Saved result ${processedThisRun}/${remainingUrls.length}: ${row.sku || "(no sku)"} ${row.color_name || ""}`);
|
||||
} catch (err) {
|
||||
const errorRecord = {
|
||||
product_url: url,
|
||||
error: err.message,
|
||||
scraped_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
data.errors.push(errorRecord);
|
||||
saveOutput(data);
|
||||
|
||||
logLine(`ERROR ${url}: ${err.message}`);
|
||||
}
|
||||
|
||||
const delay = randomDelayMs();
|
||||
logLine(`Waiting ${(delay / 1000).toFixed(1)} seconds before next product...`);
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
logLine(`Done. Results: ${data.results.length}; Errors: ${data.errors.length}; Output: ${outputJson}`);
|
||||
})();
|
||||
'@
|
||||
|
||||
Set-Content -Path ".\prismatic-browser-scraper.js" -Value $js -Encoding UTF8
|
||||
}
|
||||
|
||||
try {
|
||||
Install-PlaywrightIfNeeded -Requested:$InstallPlaywright
|
||||
Write-NodeScraper
|
||||
|
||||
Write-Host "Running resumable browser scraper..."
|
||||
|
||||
$nodeArgs = @(
|
||||
".\prismatic-browser-scraper.js",
|
||||
"--input-file=$InputFile",
|
||||
"--output-json=$OutputJson",
|
||||
"--progress-log=$ProgressLog",
|
||||
"--min-delay-seconds=$MinDelaySeconds",
|
||||
"--max-delay-seconds=$MaxDelaySeconds",
|
||||
"--page-settle-seconds=$PageSettleSeconds",
|
||||
"--max-products=$MaxProducts"
|
||||
)
|
||||
|
||||
if ($Headed) {
|
||||
$nodeArgs += "--headed"
|
||||
}
|
||||
|
||||
if ($RetryErrors) {
|
||||
$nodeArgs += "--retry-errors"
|
||||
}
|
||||
|
||||
node @nodeArgs
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
# Crawl and Index Prismatic Colors - Known-Good Style JSON.ps1
|
||||
#
|
||||
# Rollback to the earlier working browser pattern:
|
||||
# - Playwright Chromium
|
||||
# - Full Chrome-style User-Agent
|
||||
# - JSON output
|
||||
# - Structured price tiers
|
||||
# - Color matches from #collection-list
|
||||
#
|
||||
# First-time setup:
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
# .\Crawl-and-Index-Prismatic-colors-known-good-json.ps1 -InstallPlaywright
|
||||
#
|
||||
# Normal run:
|
||||
# .\Crawl-and-Index-Prismatic-colors-known-good-json.ps1
|
||||
#
|
||||
# Watch browser:
|
||||
# .\Crawl-and-Index-Prismatic-colors-known-good-json.ps1 -Headed
|
||||
|
||||
param(
|
||||
[switch]$InstallPlaywright,
|
||||
[switch]$Headed
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Ensure-NodeAvailable {
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
throw "Node.js is required. Install Node.js LTS from https://nodejs.org/"
|
||||
}
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required. It usually comes with Node.js."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PlaywrightIfNeeded {
|
||||
param([bool]$Requested)
|
||||
|
||||
Ensure-NodeAvailable
|
||||
|
||||
if ($Requested -or -not (Test-Path ".\node_modules\playwright")) {
|
||||
Write-Host "Installing Playwright package locally..."
|
||||
npm init -y | Out-Null
|
||||
npm install playwright | Out-Null
|
||||
|
||||
Write-Host "Installing Playwright Chromium browser..."
|
||||
npx playwright install chromium
|
||||
}
|
||||
}
|
||||
|
||||
function Write-NodeScraper {
|
||||
# Single-quoted here-string prevents PowerShell from interpreting JavaScript regex/template strings.
|
||||
$js = @'
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
|
||||
const productUrls = [
|
||||
"https://www.prismaticpowders.com/shop/powder-coating-colors/PSS-11248/high-gloss-black"
|
||||
];
|
||||
|
||||
const outputJson = "prismatic_powders.json";
|
||||
|
||||
function clean(text) {
|
||||
return (text || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function absoluteUrl(baseUrl, maybeUrl) {
|
||||
if (!maybeUrl) return "";
|
||||
try {
|
||||
return new URL(maybeUrl, baseUrl).href;
|
||||
} catch {
|
||||
return maybeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function unique(items) {
|
||||
return [...new Set(items.filter(Boolean).map(clean).filter(Boolean))];
|
||||
}
|
||||
|
||||
async function getLinkByText(page, patterns) {
|
||||
const links = await page.locator("a").evaluateAll((anchors) =>
|
||||
anchors.map(a => ({
|
||||
text: (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim(),
|
||||
href: a.getAttribute("href") || ""
|
||||
}))
|
||||
);
|
||||
|
||||
for (const link of links) {
|
||||
if (patterns.some(p => new RegExp(p, "i").test(link.text))) {
|
||||
return absoluteUrl(page.url(), link.href);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function parsePriceTiers(plainText) {
|
||||
const priceMatches = [...plainText.matchAll(/(\d+\s*-\s*\d+\s*lbs|\d+\s*\+\s*lbs)\s*\$([\d.]+)/gi)];
|
||||
|
||||
return priceMatches.map(m => {
|
||||
const rangeText = clean(m[1]);
|
||||
const price = parseFloat(m[2]);
|
||||
|
||||
let min = null;
|
||||
let max = null;
|
||||
|
||||
const rangeMatch = rangeText.match(/(\d+)\s*-\s*(\d+)/);
|
||||
if (rangeMatch) {
|
||||
min = parseInt(rangeMatch[1], 10);
|
||||
max = parseInt(rangeMatch[2], 10);
|
||||
}
|
||||
|
||||
const plusMatch = rangeText.match(/(\d+)\s*\+/);
|
||||
if (plusMatch) {
|
||||
min = parseInt(plusMatch[1], 10);
|
||||
max = null;
|
||||
}
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
price
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getSampleImageUrl(page) {
|
||||
const imageUrls = await page.locator("img").evaluateAll((imgs) =>
|
||||
imgs.map(img =>
|
||||
img.currentSrc ||
|
||||
img.src ||
|
||||
img.getAttribute("src") ||
|
||||
img.getAttribute("data-src") ||
|
||||
""
|
||||
).filter(Boolean)
|
||||
);
|
||||
|
||||
return (
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src) && !/thumbnail/i.test(src)) ||
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src)) ||
|
||||
imageUrls.find(src => /prismatic|powder|color/i.test(src)) ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
async function parseProduct(page, url) {
|
||||
console.log(`Scraping ${url}`);
|
||||
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const status = response ? response.status() : 0;
|
||||
const pageTitle = clean(await page.title().catch(() => ""));
|
||||
const plainText = clean(await page.locator("body").innerText().catch(() => ""));
|
||||
|
||||
console.log(`HTTP status: ${status}`);
|
||||
console.log(`Page title: ${pageTitle}`);
|
||||
|
||||
// Do not silently output a fake product if blocked.
|
||||
if (status === 403 || /^403 Forbidden$/i.test(pageTitle) || /^403 Forbidden$/i.test(plainText)) {
|
||||
throw new Error("403 Forbidden returned by site.");
|
||||
}
|
||||
|
||||
const title = clean(await page.locator("h1").first().innerText().catch(() => ""));
|
||||
|
||||
const skuMatch = plainText.match(/Item:\s*([A-Z0-9-]+)/i);
|
||||
const sku = skuMatch ? skuMatch[1] : "";
|
||||
|
||||
const descMatch = plainText.match(/Description:\s*(.*?)(WARNING:|What does this match\?|$)/is);
|
||||
const description = descMatch ? clean(descMatch[1]) : "";
|
||||
|
||||
const priceTiers = parsePriceTiers(plainText);
|
||||
|
||||
const safetyDataSheetUrl = await getLinkByText(page, ["Safety Data Sheet", "\\bSDS\\b"]);
|
||||
const applicationGuideUrl = await getLinkByText(page, ["Application Guide"]);
|
||||
const technicalDataSheetUrl = await getLinkByText(page, ["Tech Data Sheet", "Technical Data Sheet", "\\bTDS\\b"]);
|
||||
const sampleImageUrl = await getSampleImageUrl(page);
|
||||
|
||||
return {
|
||||
sku,
|
||||
color_name: title,
|
||||
description,
|
||||
price_tiers: priceTiers,
|
||||
safety_data_sheet_url: safetyDataSheetUrl,
|
||||
technical_data_sheet_url: technicalDataSheetUrl,
|
||||
application_guide_url: applicationGuideUrl,
|
||||
sample_image_url: sampleImageUrl,
|
||||
product_url: url,
|
||||
scraped_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
headless: !headed
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (const url of productUrls) {
|
||||
try {
|
||||
const row = await parseProduct(page, url);
|
||||
results.push(row);
|
||||
await page.waitForTimeout(3000);
|
||||
} catch (err) {
|
||||
console.warn(`Failed ${url}: ${err.message}`);
|
||||
errors.push({
|
||||
product_url: url,
|
||||
error: err.message,
|
||||
scraped_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// If you prefer only the array, change this to JSON.stringify(results, null, 2)
|
||||
const output = {
|
||||
results,
|
||||
errors
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputJson, JSON.stringify(output, null, 2), "utf8");
|
||||
|
||||
console.log(`Done. Output: ${outputJson}`);
|
||||
})();
|
||||
'@
|
||||
|
||||
Set-Content -Path ".\prismatic-browser-scraper.js" -Value $js -Encoding UTF8
|
||||
}
|
||||
|
||||
try {
|
||||
Install-PlaywrightIfNeeded -Requested:$InstallPlaywright
|
||||
Write-NodeScraper
|
||||
|
||||
Write-Host "Running browser scraper..."
|
||||
|
||||
if ($Headed) {
|
||||
node .\prismatic-browser-scraper.js --headed
|
||||
}
|
||||
else {
|
||||
node .\prismatic-browser-scraper.js
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
# Discover-Prismatic-Product-Urls-By-ColorParam.ps1
|
||||
#
|
||||
# Discovers Prismatic Powders product URLs by visiting color filter URLs like:
|
||||
# https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_red
|
||||
#
|
||||
# Outputs:
|
||||
# .\product-urls.txt
|
||||
# .\color-discovery-log.json
|
||||
#
|
||||
# First-time setup:
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
# .\Discover-Prismatic-Product-Urls-By-ColorParam.ps1 -InstallPlaywright -Headed
|
||||
#
|
||||
# Normal run:
|
||||
# .\Discover-Prismatic-Product-Urls-By-ColorParam.ps1
|
||||
#
|
||||
# Watch browser:
|
||||
# .\Discover-Prismatic-Product-Urls-By-ColorParam.ps1 -Headed
|
||||
|
||||
param(
|
||||
[switch]$InstallPlaywright,
|
||||
[switch]$Headed,
|
||||
[int]$MaxScrollsPerColor = 180,
|
||||
[int]$StopAfterNoNewScrolls = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Ensure-NodeAvailable {
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
throw "Node.js is required. Install Node.js LTS from https://nodejs.org/"
|
||||
}
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required. It usually comes with Node.js."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PlaywrightIfNeeded {
|
||||
param([bool]$Requested)
|
||||
|
||||
Ensure-NodeAvailable
|
||||
|
||||
if ($Requested -or -not (Test-Path ".\node_modules\playwright")) {
|
||||
Write-Host "Installing Playwright package locally..."
|
||||
npm init -y | Out-Null
|
||||
npm install playwright | Out-Null
|
||||
|
||||
Write-Host "Installing Playwright Chromium browser..."
|
||||
npx playwright install chromium
|
||||
}
|
||||
}
|
||||
|
||||
function Write-NodeDiscoveryScript {
|
||||
$js = @'
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
|
||||
function getArgValue(name, defaultValue) {
|
||||
const prefix = `--${name}=`;
|
||||
const found = process.argv.find(x => x.startsWith(prefix));
|
||||
return found ? found.slice(prefix.length) : defaultValue;
|
||||
}
|
||||
|
||||
const maxScrollsPerColor = parseInt(getArgValue("max-scrolls-per-color", "180"), 10);
|
||||
const stopAfterNoNewScrolls = parseInt(getArgValue("stop-after-no-new-scrolls", "10"), 10);
|
||||
|
||||
const baseUrl = "https://www.prismaticpowders.com/shop/powder-coating-colors";
|
||||
const outputFile = "product-urls.txt";
|
||||
const logFile = "color-discovery-log.json";
|
||||
|
||||
// Update this list if you find more color params in the site HTML.
|
||||
const colorParams = [
|
||||
"pris_black",
|
||||
"pris_blue",
|
||||
"pris_bronze",
|
||||
"pris_brown",
|
||||
"pris_clear",
|
||||
"pris_copper",
|
||||
"pris_gold",
|
||||
"pris_gray",
|
||||
"pris_green",
|
||||
"pris_orange",
|
||||
"pris_pink",
|
||||
"pris_purple",
|
||||
"pris_red",
|
||||
"pris_silver",
|
||||
"pris_tan",
|
||||
"pris_white",
|
||||
"pris_yellow"
|
||||
];
|
||||
|
||||
function cleanUrl(url) {
|
||||
return (url || "").split("?")[0].split("#")[0].trim();
|
||||
}
|
||||
|
||||
function isProductUrl(url) {
|
||||
return /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(url || "");
|
||||
}
|
||||
|
||||
function readExistingUrls() {
|
||||
if (!fs.existsSync(outputFile)) return [];
|
||||
|
||||
return fs.readFileSync(outputFile, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map(cleanUrl)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function writeUrls(urls) {
|
||||
const sorted = [...urls].sort();
|
||||
fs.writeFileSync(outputFile, sorted.join("\r\n") + "\r\n", "utf8");
|
||||
}
|
||||
|
||||
function readLog() {
|
||||
if (!fs.existsSync(logFile)) {
|
||||
return {
|
||||
completed_colors: {},
|
||||
runs: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(logFile, "utf8"));
|
||||
} catch {
|
||||
return {
|
||||
completed_colors: {},
|
||||
runs: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(log) {
|
||||
fs.writeFileSync(logFile, JSON.stringify(log, null, 2), "utf8");
|
||||
}
|
||||
|
||||
async function collectProductLinks(page) {
|
||||
const links = await page.locator("a").evaluateAll(anchors =>
|
||||
anchors
|
||||
.map(a => a.href)
|
||||
.filter(Boolean)
|
||||
.filter(h => /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(h))
|
||||
);
|
||||
|
||||
return links.map(cleanUrl).filter(Boolean);
|
||||
}
|
||||
|
||||
async function scrollAndCollect(page, urls, label) {
|
||||
let noNewScrolls = 0;
|
||||
let totalAddedForThisColor = 0;
|
||||
|
||||
for (let i = 0; i < maxScrollsPerColor; i++) {
|
||||
const before = urls.size;
|
||||
|
||||
for (const link of await collectProductLinks(page)) {
|
||||
urls.add(link);
|
||||
}
|
||||
|
||||
const after = urls.size;
|
||||
const added = after - before;
|
||||
totalAddedForThisColor += added;
|
||||
|
||||
if (added === 0) {
|
||||
noNewScrolls++;
|
||||
} else {
|
||||
noNewScrolls = 0;
|
||||
}
|
||||
|
||||
writeUrls(urls);
|
||||
|
||||
console.log(`[${label}] Scroll ${i + 1}/${maxScrollsPerColor}: +${added}, total ${after}, no-new ${noNewScrolls}`);
|
||||
|
||||
if (noNewScrolls >= stopAfterNoNewScrolls) {
|
||||
break;
|
||||
}
|
||||
|
||||
await page.mouse.wheel(0, 2500);
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
return totalAddedForThisColor;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const existingUrls = readExistingUrls();
|
||||
const urls = new Set(existingUrls);
|
||||
const log = readLog();
|
||||
|
||||
console.log(`Existing URLs in ${outputFile}: ${existingUrls.length}`);
|
||||
|
||||
const browser = await chromium.launch({ headless: !headed });
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
const runRecord = {
|
||||
started_at: new Date().toISOString(),
|
||||
existing_at_start: existingUrls.length,
|
||||
colors_attempted: []
|
||||
};
|
||||
|
||||
for (const color of colorParams) {
|
||||
if (log.completed_colors[color]) {
|
||||
console.log(`Skipping completed color: ${color}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = `${baseUrl}?color=${encodeURIComponent(color)}`;
|
||||
console.log("");
|
||||
console.log(`Opening color filter: ${color}`);
|
||||
console.log(url);
|
||||
|
||||
try {
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const status = response ? response.status() : "unknown";
|
||||
console.log(`HTTP status: ${status}`);
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const before = urls.size;
|
||||
const addedDuringScroll = await scrollAndCollect(page, urls, color);
|
||||
const after = urls.size;
|
||||
const netAdded = after - before;
|
||||
|
||||
log.completed_colors[color] = {
|
||||
url,
|
||||
http_status: status,
|
||||
added: netAdded,
|
||||
added_during_scroll: addedDuringScroll,
|
||||
total_after: after,
|
||||
completed_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
runRecord.colors_attempted.push({
|
||||
color,
|
||||
url,
|
||||
http_status: status,
|
||||
added: netAdded,
|
||||
total_after: after
|
||||
});
|
||||
|
||||
writeLog(log);
|
||||
writeUrls(urls);
|
||||
|
||||
console.log(`Color complete: ${color}; added ${netAdded}; total ${after}`);
|
||||
|
||||
// Polite pause between filters.
|
||||
await page.waitForTimeout(3000);
|
||||
} catch (err) {
|
||||
console.log(`Color failed: ${color}; ${err.message}`);
|
||||
|
||||
runRecord.colors_attempted.push({
|
||||
color,
|
||||
url,
|
||||
added: 0,
|
||||
error: err.message
|
||||
});
|
||||
|
||||
writeLog(log);
|
||||
}
|
||||
}
|
||||
|
||||
runRecord.finished_at = new Date().toISOString();
|
||||
runRecord.final_total = urls.size;
|
||||
runRecord.new_this_run = urls.size - existingUrls.length;
|
||||
|
||||
log.runs.push(runRecord);
|
||||
writeLog(log);
|
||||
writeUrls(urls);
|
||||
|
||||
console.log("");
|
||||
console.log("Color-param discovery complete.");
|
||||
console.log(`Existing at start: ${existingUrls.length}`);
|
||||
console.log(`Final total: ${urls.size}`);
|
||||
console.log(`New this run: ${urls.size - existingUrls.length}`);
|
||||
console.log(`Output: ${outputFile}`);
|
||||
console.log(`Log: ${logFile}`);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
'@
|
||||
|
||||
Set-Content -Path ".\discover-prismatic-by-color-param.js" -Value $js -Encoding UTF8
|
||||
}
|
||||
|
||||
try {
|
||||
Install-PlaywrightIfNeeded -Requested:$InstallPlaywright
|
||||
Write-NodeDiscoveryScript
|
||||
|
||||
Write-Host "Running color-param URL discovery..."
|
||||
|
||||
$nodeArgs = @(
|
||||
".\discover-prismatic-by-color-param.js",
|
||||
"--max-scrolls-per-color=$MaxScrollsPerColor",
|
||||
"--stop-after-no-new-scrolls=$StopAfterNoNewScrolls"
|
||||
)
|
||||
|
||||
if ($Headed) {
|
||||
$nodeArgs += "--headed"
|
||||
}
|
||||
|
||||
node @nodeArgs
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
# Crawl and Index Prismatic Colors - Known-Good Style JSON.ps1
|
||||
#
|
||||
# Rollback to the earlier working browser pattern:
|
||||
# - Playwright Chromium
|
||||
# - Full Chrome-style User-Agent
|
||||
# - JSON output
|
||||
# - Structured price tiers
|
||||
# - Color matches from #collection-list
|
||||
#
|
||||
# First-time setup:
|
||||
# Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
|
||||
# .\Crawl-and-Index-Prismatic-colors-known-good-json.ps1 -InstallPlaywright
|
||||
#
|
||||
# Normal run:
|
||||
# .\Crawl-and-Index-Prismatic-colors-known-good-json.ps1
|
||||
#
|
||||
# Watch browser:
|
||||
# .\Crawl-and-Index-Prismatic-colors-known-good-json.ps1 -Headed
|
||||
|
||||
param(
|
||||
[switch]$InstallPlaywright,
|
||||
[switch]$Headed
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Ensure-NodeAvailable {
|
||||
if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
|
||||
throw "Node.js is required. Install Node.js LTS from https://nodejs.org/"
|
||||
}
|
||||
|
||||
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
throw "npm is required. It usually comes with Node.js."
|
||||
}
|
||||
}
|
||||
|
||||
function Install-PlaywrightIfNeeded {
|
||||
param([bool]$Requested)
|
||||
|
||||
Ensure-NodeAvailable
|
||||
|
||||
if ($Requested -or -not (Test-Path ".\node_modules\playwright")) {
|
||||
Write-Host "Installing Playwright package locally..."
|
||||
npm init -y | Out-Null
|
||||
npm install playwright | Out-Null
|
||||
|
||||
Write-Host "Installing Playwright Chromium browser..."
|
||||
npx playwright install chromium
|
||||
}
|
||||
}
|
||||
|
||||
function Write-NodeScraper {
|
||||
# Single-quoted here-string prevents PowerShell from interpreting JavaScript regex/template strings.
|
||||
$js = @'
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
|
||||
const productUrls = [
|
||||
"https://www.prismaticpowders.com/shop/powder-coating-colors/PSS-11248/high-gloss-black"
|
||||
];
|
||||
|
||||
const outputJson = "prismatic_powders.json";
|
||||
|
||||
function clean(text) {
|
||||
return (text || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function absoluteUrl(baseUrl, maybeUrl) {
|
||||
if (!maybeUrl) return "";
|
||||
try {
|
||||
return new URL(maybeUrl, baseUrl).href;
|
||||
} catch {
|
||||
return maybeUrl;
|
||||
}
|
||||
}
|
||||
|
||||
function unique(items) {
|
||||
return [...new Set(items.filter(Boolean).map(clean).filter(Boolean))];
|
||||
}
|
||||
|
||||
async function getLinkByText(page, patterns) {
|
||||
const links = await page.locator("a").evaluateAll((anchors) =>
|
||||
anchors.map(a => ({
|
||||
text: (a.innerText || a.textContent || "").replace(/\s+/g, " ").trim(),
|
||||
href: a.getAttribute("href") || ""
|
||||
}))
|
||||
);
|
||||
|
||||
for (const link of links) {
|
||||
if (patterns.some(p => new RegExp(p, "i").test(link.text))) {
|
||||
return absoluteUrl(page.url(), link.href);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function parsePriceTiers(plainText) {
|
||||
const priceMatches = [...plainText.matchAll(/(\d+\s*-\s*\d+\s*lbs|\d+\s*\+\s*lbs)\s*\$([\d.]+)/gi)];
|
||||
|
||||
return priceMatches.map(m => {
|
||||
const rangeText = clean(m[1]);
|
||||
const price = parseFloat(m[2]);
|
||||
|
||||
let min = null;
|
||||
let max = null;
|
||||
|
||||
const rangeMatch = rangeText.match(/(\d+)\s*-\s*(\d+)/);
|
||||
if (rangeMatch) {
|
||||
min = parseInt(rangeMatch[1], 10);
|
||||
max = parseInt(rangeMatch[2], 10);
|
||||
}
|
||||
|
||||
const plusMatch = rangeText.match(/(\d+)\s*\+/);
|
||||
if (plusMatch) {
|
||||
min = parseInt(plusMatch[1], 10);
|
||||
max = null;
|
||||
}
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
price
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getSampleImageUrl(page) {
|
||||
const imageUrls = await page.locator("img").evaluateAll((imgs) =>
|
||||
imgs.map(img =>
|
||||
img.currentSrc ||
|
||||
img.src ||
|
||||
img.getAttribute("src") ||
|
||||
img.getAttribute("data-src") ||
|
||||
""
|
||||
).filter(Boolean)
|
||||
);
|
||||
|
||||
return (
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src) && !/thumbnail/i.test(src)) ||
|
||||
imageUrls.find(src => /images\.nicindustries\.com/i.test(src)) ||
|
||||
imageUrls.find(src => /prismatic|powder|color/i.test(src)) ||
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
async function parseProduct(page, url) {
|
||||
console.log(`Scraping ${url}`);
|
||||
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
const status = response ? response.status() : 0;
|
||||
const pageTitle = clean(await page.title().catch(() => ""));
|
||||
const plainText = clean(await page.locator("body").innerText().catch(() => ""));
|
||||
|
||||
console.log(`HTTP status: ${status}`);
|
||||
console.log(`Page title: ${pageTitle}`);
|
||||
|
||||
// Do not silently output a fake product if blocked.
|
||||
if (status === 403 || /^403 Forbidden$/i.test(pageTitle) || /^403 Forbidden$/i.test(plainText)) {
|
||||
throw new Error("403 Forbidden returned by site.");
|
||||
}
|
||||
|
||||
const title = clean(await page.locator("h1").first().innerText().catch(() => ""));
|
||||
|
||||
const skuMatch = plainText.match(/Item:\s*([A-Z0-9-]+)/i);
|
||||
const sku = skuMatch ? skuMatch[1] : "";
|
||||
|
||||
const descMatch = plainText.match(/Description:\s*(.*?)(WARNING:|What does this match\?|$)/is);
|
||||
const description = descMatch ? clean(descMatch[1]) : "";
|
||||
|
||||
const priceTiers = parsePriceTiers(plainText);
|
||||
|
||||
const safetyDataSheetUrl = await getLinkByText(page, ["Safety Data Sheet", "\\bSDS\\b"]);
|
||||
const applicationGuideUrl = await getLinkByText(page, ["Application Guide"]);
|
||||
const technicalDataSheetUrl = await getLinkByText(page, ["Tech Data Sheet", "Technical Data Sheet", "\\bTDS\\b"]);
|
||||
const sampleImageUrl = await getSampleImageUrl(page);
|
||||
|
||||
return {
|
||||
sku,
|
||||
color_name: title,
|
||||
description,
|
||||
price_tiers: priceTiers,
|
||||
safety_data_sheet_url: safetyDataSheetUrl,
|
||||
technical_data_sheet_url: technicalDataSheetUrl,
|
||||
application_guide_url: applicationGuideUrl,
|
||||
sample_image_url: sampleImageUrl,
|
||||
product_url: url,
|
||||
scraped_at: new Date().toISOString()
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({
|
||||
headless: !headed
|
||||
});
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (const url of productUrls) {
|
||||
try {
|
||||
const row = await parseProduct(page, url);
|
||||
results.push(row);
|
||||
await page.waitForTimeout(3000);
|
||||
} catch (err) {
|
||||
console.warn(`Failed ${url}: ${err.message}`);
|
||||
errors.push({
|
||||
product_url: url,
|
||||
error: err.message,
|
||||
scraped_at: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
|
||||
// If you prefer only the array, change this to JSON.stringify(results, null, 2)
|
||||
const output = {
|
||||
results,
|
||||
errors
|
||||
};
|
||||
|
||||
fs.writeFileSync(outputJson, JSON.stringify(output, null, 2), "utf8");
|
||||
|
||||
console.log(`Done. Output: ${outputJson}`);
|
||||
})();
|
||||
'@
|
||||
|
||||
Set-Content -Path ".\prismatic-browser-scraper.js" -Value $js -Encoding UTF8
|
||||
}
|
||||
|
||||
try {
|
||||
Install-PlaywrightIfNeeded -Requested:$InstallPlaywright
|
||||
Write-NodeScraper
|
||||
|
||||
Write-Host "Running browser scraper..."
|
||||
|
||||
if ($Headed) {
|
||||
node .\prismatic-browser-scraper.js --headed
|
||||
}
|
||||
else {
|
||||
node .\prismatic-browser-scraper.js
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error $_.Exception.Message
|
||||
exit 1
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,270 @@
|
||||
{
|
||||
"completed_colors": {
|
||||
"pris_black": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_black",
|
||||
"http_status": 200,
|
||||
"added": 472,
|
||||
"added_during_scroll": 472,
|
||||
"total_after": 472,
|
||||
"completed_at": "2026-04-30T00:47:46.289Z"
|
||||
},
|
||||
"pris_blue": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_blue",
|
||||
"http_status": 200,
|
||||
"added": 948,
|
||||
"added_during_scroll": 948,
|
||||
"total_after": 1420,
|
||||
"completed_at": "2026-04-30T00:49:25.145Z"
|
||||
},
|
||||
"pris_bronze": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_bronze",
|
||||
"http_status": 200,
|
||||
"added": 358,
|
||||
"added_during_scroll": 358,
|
||||
"total_after": 1778,
|
||||
"completed_at": "2026-04-30T00:50:18.466Z"
|
||||
},
|
||||
"pris_brown": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_brown",
|
||||
"http_status": 200,
|
||||
"added": 373,
|
||||
"added_during_scroll": 373,
|
||||
"total_after": 2151,
|
||||
"completed_at": "2026-04-30T00:51:18.033Z"
|
||||
},
|
||||
"pris_clear": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_clear",
|
||||
"http_status": 200,
|
||||
"added": 19,
|
||||
"added_during_scroll": 19,
|
||||
"total_after": 2170,
|
||||
"completed_at": "2026-04-30T00:51:42.889Z"
|
||||
},
|
||||
"pris_copper": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_copper",
|
||||
"http_status": 200,
|
||||
"added": 1094,
|
||||
"added_during_scroll": 1094,
|
||||
"total_after": 3264,
|
||||
"completed_at": "2026-04-30T00:56:34.934Z"
|
||||
},
|
||||
"pris_gold": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_gold",
|
||||
"http_status": 200,
|
||||
"added": 152,
|
||||
"added_during_scroll": 152,
|
||||
"total_after": 3416,
|
||||
"completed_at": "2026-04-30T00:57:26.775Z"
|
||||
},
|
||||
"pris_gray": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_gray",
|
||||
"http_status": 200,
|
||||
"added": 0,
|
||||
"added_during_scroll": 0,
|
||||
"total_after": 3416,
|
||||
"completed_at": "2026-04-30T00:57:49.624Z"
|
||||
},
|
||||
"pris_green": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_green",
|
||||
"http_status": 200,
|
||||
"added": 0,
|
||||
"added_during_scroll": 0,
|
||||
"total_after": 3416,
|
||||
"completed_at": "2026-04-30T00:58:12.277Z"
|
||||
},
|
||||
"pris_orange": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_orange",
|
||||
"http_status": 200,
|
||||
"added": 233,
|
||||
"added_during_scroll": 233,
|
||||
"total_after": 3649,
|
||||
"completed_at": "2026-04-30T00:59:06.776Z"
|
||||
},
|
||||
"pris_pink": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_pink",
|
||||
"http_status": 200,
|
||||
"added": 169,
|
||||
"added_during_scroll": 169,
|
||||
"total_after": 3818,
|
||||
"completed_at": "2026-04-30T00:59:49.323Z"
|
||||
},
|
||||
"pris_purple": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_purple",
|
||||
"http_status": 200,
|
||||
"added": 182,
|
||||
"added_during_scroll": 182,
|
||||
"total_after": 4000,
|
||||
"completed_at": "2026-04-30T01:00:38.111Z"
|
||||
},
|
||||
"pris_red": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_red",
|
||||
"http_status": 200,
|
||||
"added": 346,
|
||||
"added_during_scroll": 346,
|
||||
"total_after": 4346,
|
||||
"completed_at": "2026-04-30T01:01:51.910Z"
|
||||
},
|
||||
"pris_silver": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_silver",
|
||||
"http_status": 200,
|
||||
"added": 210,
|
||||
"added_during_scroll": 210,
|
||||
"total_after": 4556,
|
||||
"completed_at": "2026-04-30T01:02:51.835Z"
|
||||
},
|
||||
"pris_tan": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_tan",
|
||||
"http_status": 200,
|
||||
"added": 219,
|
||||
"added_during_scroll": 219,
|
||||
"total_after": 4775,
|
||||
"completed_at": "2026-04-30T01:03:43.244Z"
|
||||
},
|
||||
"pris_white": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_white",
|
||||
"http_status": 200,
|
||||
"added": 218,
|
||||
"added_during_scroll": 218,
|
||||
"total_after": 4993,
|
||||
"completed_at": "2026-04-30T01:04:39.931Z"
|
||||
},
|
||||
"pris_yellow": {
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_yellow",
|
||||
"http_status": 200,
|
||||
"added": 199,
|
||||
"added_during_scroll": 199,
|
||||
"total_after": 5192,
|
||||
"completed_at": "2026-04-30T01:05:31.945Z"
|
||||
}
|
||||
},
|
||||
"runs": [
|
||||
{
|
||||
"started_at": "2026-04-30T00:46:47.692Z",
|
||||
"existing_at_start": 0,
|
||||
"colors_attempted": [
|
||||
{
|
||||
"color": "pris_black",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_black",
|
||||
"http_status": 200,
|
||||
"added": 472,
|
||||
"total_after": 472
|
||||
},
|
||||
{
|
||||
"color": "pris_blue",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_blue",
|
||||
"http_status": 200,
|
||||
"added": 948,
|
||||
"total_after": 1420
|
||||
},
|
||||
{
|
||||
"color": "pris_bronze",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_bronze",
|
||||
"http_status": 200,
|
||||
"added": 358,
|
||||
"total_after": 1778
|
||||
},
|
||||
{
|
||||
"color": "pris_brown",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_brown",
|
||||
"http_status": 200,
|
||||
"added": 373,
|
||||
"total_after": 2151
|
||||
},
|
||||
{
|
||||
"color": "pris_clear",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_clear",
|
||||
"http_status": 200,
|
||||
"added": 19,
|
||||
"total_after": 2170
|
||||
},
|
||||
{
|
||||
"color": "pris_copper",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_copper",
|
||||
"http_status": 200,
|
||||
"added": 1094,
|
||||
"total_after": 3264
|
||||
},
|
||||
{
|
||||
"color": "pris_gold",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_gold",
|
||||
"http_status": 200,
|
||||
"added": 152,
|
||||
"total_after": 3416
|
||||
},
|
||||
{
|
||||
"color": "pris_gray",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_gray",
|
||||
"http_status": 200,
|
||||
"added": 0,
|
||||
"total_after": 3416
|
||||
},
|
||||
{
|
||||
"color": "pris_green",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_green",
|
||||
"http_status": 200,
|
||||
"added": 0,
|
||||
"total_after": 3416
|
||||
},
|
||||
{
|
||||
"color": "pris_orange",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_orange",
|
||||
"http_status": 200,
|
||||
"added": 233,
|
||||
"total_after": 3649
|
||||
},
|
||||
{
|
||||
"color": "pris_pink",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_pink",
|
||||
"http_status": 200,
|
||||
"added": 169,
|
||||
"total_after": 3818
|
||||
},
|
||||
{
|
||||
"color": "pris_purple",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_purple",
|
||||
"http_status": 200,
|
||||
"added": 182,
|
||||
"total_after": 4000
|
||||
},
|
||||
{
|
||||
"color": "pris_red",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_red",
|
||||
"http_status": 200,
|
||||
"added": 346,
|
||||
"total_after": 4346
|
||||
},
|
||||
{
|
||||
"color": "pris_silver",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_silver",
|
||||
"http_status": 200,
|
||||
"added": 210,
|
||||
"total_after": 4556
|
||||
},
|
||||
{
|
||||
"color": "pris_tan",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_tan",
|
||||
"http_status": 200,
|
||||
"added": 219,
|
||||
"total_after": 4775
|
||||
},
|
||||
{
|
||||
"color": "pris_white",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_white",
|
||||
"http_status": 200,
|
||||
"added": 218,
|
||||
"total_after": 4993
|
||||
},
|
||||
{
|
||||
"color": "pris_yellow",
|
||||
"url": "https://www.prismaticpowders.com/shop/powder-coating-colors?color=pris_yellow",
|
||||
"http_status": 200,
|
||||
"added": 199,
|
||||
"total_after": 5192
|
||||
}
|
||||
],
|
||||
"finished_at": "2026-04-30T01:05:34.987Z",
|
||||
"final_total": 5192,
|
||||
"new_this_run": 5192
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
const fs = require("fs");
|
||||
const { chromium } = require("playwright");
|
||||
|
||||
const headed = process.argv.includes("--headed");
|
||||
|
||||
function getArgValue(name, defaultValue) {
|
||||
const prefix = `--${name}=`;
|
||||
const found = process.argv.find(x => x.startsWith(prefix));
|
||||
return found ? found.slice(prefix.length) : defaultValue;
|
||||
}
|
||||
|
||||
const maxScrollsPerColor = parseInt(getArgValue("max-scrolls-per-color", "180"), 10);
|
||||
const stopAfterNoNewScrolls = parseInt(getArgValue("stop-after-no-new-scrolls", "10"), 10);
|
||||
|
||||
const baseUrl = "https://www.prismaticpowders.com/shop/powder-coating-colors";
|
||||
const outputFile = "product-urls.txt";
|
||||
const logFile = "color-discovery-log.json";
|
||||
|
||||
// Update this list if you find more color params in the site HTML.
|
||||
const colorParams = [
|
||||
"pris_black",
|
||||
"pris_blue",
|
||||
"pris_bronze",
|
||||
"pris_brown",
|
||||
"pris_clear",
|
||||
"pris_copper",
|
||||
"pris_gold",
|
||||
"pris_gray",
|
||||
"pris_green",
|
||||
"pris_orange",
|
||||
"pris_pink",
|
||||
"pris_purple",
|
||||
"pris_red",
|
||||
"pris_silver",
|
||||
"pris_tan",
|
||||
"pris_white",
|
||||
"pris_yellow"
|
||||
];
|
||||
|
||||
function cleanUrl(url) {
|
||||
return (url || "").split("?")[0].split("#")[0].trim();
|
||||
}
|
||||
|
||||
function isProductUrl(url) {
|
||||
return /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(url || "");
|
||||
}
|
||||
|
||||
function readExistingUrls() {
|
||||
if (!fs.existsSync(outputFile)) return [];
|
||||
|
||||
return fs.readFileSync(outputFile, "utf8")
|
||||
.split(/\r?\n/)
|
||||
.map(cleanUrl)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function writeUrls(urls) {
|
||||
const sorted = [...urls].sort();
|
||||
fs.writeFileSync(outputFile, sorted.join("\r\n") + "\r\n", "utf8");
|
||||
}
|
||||
|
||||
function readLog() {
|
||||
if (!fs.existsSync(logFile)) {
|
||||
return {
|
||||
completed_colors: {},
|
||||
runs: []
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(logFile, "utf8"));
|
||||
} catch {
|
||||
return {
|
||||
completed_colors: {},
|
||||
runs: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(log) {
|
||||
fs.writeFileSync(logFile, JSON.stringify(log, null, 2), "utf8");
|
||||
}
|
||||
|
||||
async function collectProductLinks(page) {
|
||||
const links = await page.locator("a").evaluateAll(anchors =>
|
||||
anchors
|
||||
.map(a => a.href)
|
||||
.filter(Boolean)
|
||||
.filter(h => /\/shop\/powder-coating-colors\/[A-Z0-9-]+\//i.test(h))
|
||||
);
|
||||
|
||||
return links.map(cleanUrl).filter(Boolean);
|
||||
}
|
||||
|
||||
async function scrollAndCollect(page, urls, label) {
|
||||
let noNewScrolls = 0;
|
||||
let totalAddedForThisColor = 0;
|
||||
|
||||
for (let i = 0; i < maxScrollsPerColor; i++) {
|
||||
const before = urls.size;
|
||||
|
||||
for (const link of await collectProductLinks(page)) {
|
||||
urls.add(link);
|
||||
}
|
||||
|
||||
const after = urls.size;
|
||||
const added = after - before;
|
||||
totalAddedForThisColor += added;
|
||||
|
||||
if (added === 0) {
|
||||
noNewScrolls++;
|
||||
} else {
|
||||
noNewScrolls = 0;
|
||||
}
|
||||
|
||||
writeUrls(urls);
|
||||
|
||||
console.log(`[${label}] Scroll ${i + 1}/${maxScrollsPerColor}: +${added}, total ${after}, no-new ${noNewScrolls}`);
|
||||
|
||||
if (noNewScrolls >= stopAfterNoNewScrolls) {
|
||||
break;
|
||||
}
|
||||
|
||||
await page.mouse.wheel(0, 2500);
|
||||
await page.waitForTimeout(1500);
|
||||
}
|
||||
|
||||
return totalAddedForThisColor;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const existingUrls = readExistingUrls();
|
||||
const urls = new Set(existingUrls);
|
||||
const log = readLog();
|
||||
|
||||
console.log(`Existing URLs in ${outputFile}: ${existingUrls.length}`);
|
||||
|
||||
const browser = await chromium.launch({ headless: !headed });
|
||||
|
||||
const context = await browser.newContext({
|
||||
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
viewport: { width: 1365, height: 900 },
|
||||
locale: "en-US",
|
||||
timezoneId: "America/New_York"
|
||||
});
|
||||
|
||||
const page = await context.newPage();
|
||||
|
||||
const runRecord = {
|
||||
started_at: new Date().toISOString(),
|
||||
existing_at_start: existingUrls.length,
|
||||
colors_attempted: []
|
||||
};
|
||||
|
||||
for (const color of colorParams) {
|
||||
if (log.completed_colors[color]) {
|
||||
console.log(`Skipping completed color: ${color}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const url = `${baseUrl}?color=${encodeURIComponent(color)}`;
|
||||
console.log("");
|
||||
console.log(`Opening color filter: ${color}`);
|
||||
console.log(url);
|
||||
|
||||
try {
|
||||
const response = await page.goto(url, {
|
||||
waitUntil: "domcontentloaded",
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
const status = response ? response.status() : "unknown";
|
||||
console.log(`HTTP status: ${status}`);
|
||||
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const before = urls.size;
|
||||
const addedDuringScroll = await scrollAndCollect(page, urls, color);
|
||||
const after = urls.size;
|
||||
const netAdded = after - before;
|
||||
|
||||
log.completed_colors[color] = {
|
||||
url,
|
||||
http_status: status,
|
||||
added: netAdded,
|
||||
added_during_scroll: addedDuringScroll,
|
||||
total_after: after,
|
||||
completed_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
runRecord.colors_attempted.push({
|
||||
color,
|
||||
url,
|
||||
http_status: status,
|
||||
added: netAdded,
|
||||
total_after: after
|
||||
});
|
||||
|
||||
writeLog(log);
|
||||
writeUrls(urls);
|
||||
|
||||
console.log(`Color complete: ${color}; added ${netAdded}; total ${after}`);
|
||||
|
||||
// Polite pause between filters.
|
||||
await page.waitForTimeout(3000);
|
||||
} catch (err) {
|
||||
console.log(`Color failed: ${color}; ${err.message}`);
|
||||
|
||||
runRecord.colors_attempted.push({
|
||||
color,
|
||||
url,
|
||||
added: 0,
|
||||
error: err.message
|
||||
});
|
||||
|
||||
writeLog(log);
|
||||
}
|
||||
}
|
||||
|
||||
runRecord.finished_at = new Date().toISOString();
|
||||
runRecord.final_total = urls.size;
|
||||
runRecord.new_this_run = urls.size - existingUrls.length;
|
||||
|
||||
log.runs.push(runRecord);
|
||||
writeLog(log);
|
||||
writeUrls(urls);
|
||||
|
||||
console.log("");
|
||||
console.log("Color-param discovery complete.");
|
||||
console.log(`Existing at start: ${existingUrls.length}`);
|
||||
console.log(`Final total: ${urls.size}`);
|
||||
console.log(`New this run: ${urls.size - existingUrls.length}`);
|
||||
console.log(`Output: ${outputFile}`);
|
||||
console.log(`Log: ${logFile}`);
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../playwright/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../playwright/cli.js" "$@"
|
||||
fi
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*|*MINGW*|*MSYS*)
|
||||
if command -v cygpath > /dev/null 2>&1; then
|
||||
basedir=`cygpath -w "$basedir"`
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../playwright-core/cli.js" "$@"
|
||||
else
|
||||
exec node "$basedir/../playwright-core/cli.js" "$@"
|
||||
fi
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\playwright-core\cli.js" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../playwright-core/cli.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../playwright-core/cli.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../playwright-core/cli.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../playwright-core/cli.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
@ECHO off
|
||||
GOTO start
|
||||
:find_dp0
|
||||
SET dp0=%~dp0
|
||||
EXIT /b
|
||||
:start
|
||||
SETLOCAL
|
||||
CALL :find_dp0
|
||||
|
||||
IF EXIST "%dp0%\node.exe" (
|
||||
SET "_prog=%dp0%\node.exe"
|
||||
) ELSE (
|
||||
SET "_prog=node"
|
||||
SET PATHEXT=%PATHEXT:;.JS;=;%
|
||||
)
|
||||
|
||||
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\playwright\cli.js" %*
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env pwsh
|
||||
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
|
||||
|
||||
$exe=""
|
||||
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
|
||||
# Fix case when both the Windows and Linux builds of Node
|
||||
# are installed in the same directory
|
||||
$exe=".exe"
|
||||
}
|
||||
$ret=0
|
||||
if (Test-Path "$basedir/node$exe") {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "$basedir/node$exe" "$basedir/../playwright/cli.js" $args
|
||||
} else {
|
||||
& "$basedir/node$exe" "$basedir/../playwright/cli.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
} else {
|
||||
# Support pipeline input
|
||||
if ($MyInvocation.ExpectingInput) {
|
||||
$input | & "node$exe" "$basedir/../playwright/cli.js" $args
|
||||
} else {
|
||||
& "node$exe" "$basedir/../playwright/cli.js" $args
|
||||
}
|
||||
$ret=$LASTEXITCODE
|
||||
}
|
||||
exit $ret
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "web-scraping",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/playwright": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
|
||||
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.59.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.59.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
|
||||
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Portions Copyright (c) Microsoft Corporation.
|
||||
Portions Copyright 2017 Google Inc.
|
||||
|
||||
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.
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
Playwright
|
||||
Copyright (c) Microsoft Corporation
|
||||
|
||||
This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer),
|
||||
available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE).
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# playwright-core
|
||||
|
||||
This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright).
|
||||
+3552
File diff suppressed because it is too large
Load Diff
+81
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"comment": "Do not edit this file, use utils/roll_browser.js",
|
||||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1217",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "147.0.7727.15",
|
||||
"title": "Chrome for Testing"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1217",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "147.0.7727.15",
|
||||
"title": "Chrome Headless Shell"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1417",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "148.0.7755.0",
|
||||
"title": "Chrome Canary for Testing"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree-headless-shell",
|
||||
"revision": "1417",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "148.0.7755.0",
|
||||
"title": "Chrome Canary Headless Shell"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1511",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "148.0.2",
|
||||
"title": "Firefox"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1505",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "148.0b9",
|
||||
"title": "Firefox Beta"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2272",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac14": "2251",
|
||||
"mac14-arm64": "2251",
|
||||
"debian11-x64": "2105",
|
||||
"debian11-arm64": "2105",
|
||||
"ubuntu20.04-x64": "2092",
|
||||
"ubuntu20.04-arm64": "2092"
|
||||
},
|
||||
"browserVersion": "26.4",
|
||||
"title": "WebKit"
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"revision": "1011",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac12": "1010",
|
||||
"mac12-arm64": "1010"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "winldd",
|
||||
"revision": "1007",
|
||||
"installByDefault": false
|
||||
},
|
||||
{
|
||||
"name": "android",
|
||||
"revision": "1001",
|
||||
"installByDefault": false
|
||||
}
|
||||
]
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
#!/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);
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* 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';
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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 = 18;
|
||||
const currentNodeVersion = process.versions.node;
|
||||
const semver = currentNodeVersion.split('.');
|
||||
const [major] = [+semver[0]];
|
||||
|
||||
if (major < minimumMajorNodeVersion) {
|
||||
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');
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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;
|
||||
Generated
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var androidServerImpl_exports = {};
|
||||
__export(androidServerImpl_exports, {
|
||||
AndroidServerLauncherImpl: () => AndroidServerLauncherImpl
|
||||
});
|
||||
module.exports = __toCommonJS(androidServerImpl_exports);
|
||||
var import_playwrightServer = require("./remote/playwrightServer");
|
||||
var import_playwright = require("./server/playwright");
|
||||
var import_crypto = require("./server/utils/crypto");
|
||||
var import_utilsBundle = require("./utilsBundle");
|
||||
var import_progress = require("./server/progress");
|
||||
class AndroidServerLauncherImpl {
|
||||
async launchServer(options = {}) {
|
||||
const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true });
|
||||
const controller = new import_progress.ProgressController();
|
||||
let devices = await controller.run((progress) => playwright.android.devices(progress, {
|
||||
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}' was 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, import_crypto.createGuid)()}`;
|
||||
const server = new import_playwrightServer.PlaywrightServer({ mode: "launchServer", path, maxConnections: 1, preLaunchedAndroidDevice: device });
|
||||
const wsEndpoint = await server.listen(options.port, options.host);
|
||||
const browserServer = new import_utilsBundle.ws.EventEmitter();
|
||||
browserServer.wsEndpoint = () => wsEndpoint;
|
||||
browserServer.close = () => device.close();
|
||||
browserServer.kill = () => device.close();
|
||||
device.on("close", () => {
|
||||
server.close();
|
||||
browserServer.emit("close");
|
||||
});
|
||||
return browserServer;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
AndroidServerLauncherImpl
|
||||
});
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
if (process.env.PW_INSTRUMENT_MODULES) {
|
||||
const Module = require("module");
|
||||
const originalLoad = Module._load;
|
||||
const root = { name: "<root>", selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
|
||||
let current = root;
|
||||
const stack = [];
|
||||
Module._load = function(request, _parent, _isMain) {
|
||||
const node = { name: request, selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
|
||||
current.children.push(node);
|
||||
stack.push(current);
|
||||
current = node;
|
||||
const start = performance.now();
|
||||
let result;
|
||||
try {
|
||||
result = originalLoad.apply(this, arguments);
|
||||
} catch (e) {
|
||||
current = stack.pop();
|
||||
current.children.pop();
|
||||
throw e;
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
node.totalMs = duration;
|
||||
node.selfMs = Math.max(0, duration - node.childrenMs);
|
||||
current = stack.pop();
|
||||
current.childrenMs += duration;
|
||||
return result;
|
||||
};
|
||||
process.on("exit", () => {
|
||||
function printTree(node, prefix, isLast, lines2, depth) {
|
||||
if (node.totalMs < 1 && depth > 0)
|
||||
return;
|
||||
const connector = depth === 0 ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
||||
const time = `${node.totalMs.toFixed(1).padStart(8)}ms`;
|
||||
const self = node.children.length ? ` (self: ${node.selfMs.toFixed(1)}ms)` : "";
|
||||
lines2.push(`${time} ${prefix}${connector}${node.name}${self}`);
|
||||
const childPrefix = prefix + (depth === 0 ? "" : isLast ? " " : "\u2502 ");
|
||||
const sorted2 = node.children.slice().sort((a, b) => b.totalMs - a.totalMs);
|
||||
for (let i = 0; i < sorted2.length; i++)
|
||||
printTree(sorted2[i], childPrefix, i === sorted2.length - 1, lines2, depth + 1);
|
||||
}
|
||||
let totalModules = 0;
|
||||
function count(n) {
|
||||
totalModules++;
|
||||
n.children.forEach(count);
|
||||
}
|
||||
root.children.forEach(count);
|
||||
const lines = [];
|
||||
const sorted = root.children.slice().sort((a, b) => b.totalMs - a.totalMs);
|
||||
for (let i = 0; i < sorted.length; i++)
|
||||
printTree(sorted[i], "", i === sorted.length - 1, lines, 0);
|
||||
const totalMs = root.children.reduce((s, c) => s + c.totalMs, 0);
|
||||
process.stderr.write(`
|
||||
--- Module load tree: ${totalModules} modules, ${totalMs.toFixed(0)}ms total ---
|
||||
` + lines.join("\n") + "\n");
|
||||
const flat = /* @__PURE__ */ new Map();
|
||||
function gather(n) {
|
||||
const existing = flat.get(n.name);
|
||||
if (existing) {
|
||||
existing.selfMs += n.selfMs;
|
||||
existing.totalMs += n.totalMs;
|
||||
existing.count++;
|
||||
} else {
|
||||
flat.set(n.name, { selfMs: n.selfMs, totalMs: n.totalMs, count: 1 });
|
||||
}
|
||||
n.children.forEach(gather);
|
||||
}
|
||||
root.children.forEach(gather);
|
||||
const top50 = [...flat.entries()].sort((a, b) => b[1].selfMs - a[1].selfMs).slice(0, 50);
|
||||
const flatLines = top50.map(
|
||||
([mod, { selfMs, totalMs: totalMs2, count: count2 }]) => `${selfMs.toFixed(1).padStart(8)}ms self ${totalMs2.toFixed(1).padStart(8)}ms total (x${String(count2).padStart(3)}) ${mod}`
|
||||
);
|
||||
process.stderr.write(`
|
||||
--- Top 50 modules by self time ---
|
||||
` + flatLines.join("\n") + "\n");
|
||||
});
|
||||
}
|
||||
Generated
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var browserServerImpl_exports = {};
|
||||
__export(browserServerImpl_exports, {
|
||||
BrowserServerLauncherImpl: () => BrowserServerLauncherImpl
|
||||
});
|
||||
module.exports = __toCommonJS(browserServerImpl_exports);
|
||||
var import_playwrightServer = require("./remote/playwrightServer");
|
||||
var import_helper = require("./server/helper");
|
||||
var import_playwright = require("./server/playwright");
|
||||
var import_crypto = require("./server/utils/crypto");
|
||||
var import_debug = require("./server/utils/debug");
|
||||
var import_stackTrace = require("./utils/isomorphic/stackTrace");
|
||||
var import_time = require("./utils/isomorphic/time");
|
||||
var import_utilsBundle = require("./utilsBundle");
|
||||
var validatorPrimitives = __toESM(require("./protocol/validatorPrimitives"));
|
||||
var import_progress = require("./server/progress");
|
||||
class BrowserServerLauncherImpl {
|
||||
constructor(browserName) {
|
||||
this._browserName = browserName;
|
||||
}
|
||||
async launchServer(options = {}) {
|
||||
const playwright = (0, import_playwright.createPlaywright)({ sdkLanguage: "javascript", isServer: true });
|
||||
const metadata = { id: "", startTime: 0, endTime: 0, type: "Internal", method: "", params: {}, log: [], internal: true };
|
||||
const validatorContext = {
|
||||
tChannelImpl: (names, arg, path2) => {
|
||||
throw new validatorPrimitives.ValidationError(`${path2}: channels are not expected in launchServer`);
|
||||
},
|
||||
binary: "buffer",
|
||||
isUnderTest: import_debug.isUnderTest
|
||||
};
|
||||
let launchOptions = {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? envObjectToArray(options.env) : void 0,
|
||||
timeout: options.timeout ?? import_time.DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT
|
||||
};
|
||||
let browser;
|
||||
try {
|
||||
const controller = new import_progress.ProgressController(metadata);
|
||||
browser = await controller.run(async (progress) => {
|
||||
if (options._userDataDir !== void 0) {
|
||||
const validator = validatorPrimitives.scheme["BrowserTypeLaunchPersistentContextParams"];
|
||||
launchOptions = validator({ ...launchOptions, userDataDir: options._userDataDir }, "", validatorContext);
|
||||
const context = await playwright[this._browserName].launchPersistentContext(progress, options._userDataDir, launchOptions);
|
||||
return context._browser;
|
||||
} else {
|
||||
const validator = validatorPrimitives.scheme["BrowserTypeLaunchParams"];
|
||||
launchOptions = validator(launchOptions, "", validatorContext);
|
||||
return await playwright[this._browserName].launch(progress, launchOptions, toProtocolLogger(options.logger));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
const log = import_helper.helper.formatBrowserLogs(metadata.log);
|
||||
(0, import_stackTrace.rewriteErrorMessage)(e, `${e.message} Failed to launch browser.${log}`);
|
||||
throw e;
|
||||
}
|
||||
const path = options.wsPath ? options.wsPath.startsWith("/") ? options.wsPath : `/${options.wsPath}` : `/${(0, import_crypto.createGuid)()}`;
|
||||
const server = new import_playwrightServer.PlaywrightServer({ mode: options._sharedBrowser ? "launchServerShared" : "launchServer", path, maxConnections: Infinity, preLaunchedBrowser: browser });
|
||||
const wsEndpoint = await server.listen(options.port, options.host);
|
||||
const browserServer = new import_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) => {
|
||||
server.close();
|
||||
browserServer.emit("close", exitCode, signal);
|
||||
};
|
||||
return browserServer;
|
||||
}
|
||||
}
|
||||
function toProtocolLogger(logger) {
|
||||
return logger ? (direction, message) => {
|
||||
if (logger.isEnabled("protocol", "verbose"))
|
||||
logger.log("protocol", "verbose", (direction === "send" ? "SEND \u25BA " : "\u25C0 RECV ") + JSON.stringify(message), [], {});
|
||||
} : void 0;
|
||||
}
|
||||
function envObjectToArray(env) {
|
||||
const result = [];
|
||||
for (const name in env) {
|
||||
if (!Object.is(env[name], void 0))
|
||||
result.push({ name, value: String(env[name]) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
BrowserServerLauncherImpl
|
||||
});
|
||||
Generated
Vendored
+308
@@ -0,0 +1,308 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var browserActions_exports = {};
|
||||
__export(browserActions_exports, {
|
||||
codegen: () => codegen,
|
||||
open: () => open,
|
||||
pdf: () => pdf,
|
||||
screenshot: () => screenshot
|
||||
});
|
||||
module.exports = __toCommonJS(browserActions_exports);
|
||||
var import_fs = __toESM(require("fs"));
|
||||
var import_os = __toESM(require("os"));
|
||||
var import_path = __toESM(require("path"));
|
||||
var playwright = __toESM(require("../.."));
|
||||
var import_utils = require("../utils");
|
||||
var import_utilsBundle = require("../utilsBundle");
|
||||
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] } : {}
|
||||
);
|
||||
if (!extraOptions.headless)
|
||||
contextOptions.deviceScaleFactor = import_os.default.platform() === "darwin" ? 2 : 1;
|
||||
if (browserType.name() === "webkit" && process.platform === "linux") {
|
||||
delete contextOptions.hasTouch;
|
||||
delete contextOptions.isMobile;
|
||||
}
|
||||
if (contextOptions.isMobile && browserType.name() === "firefox")
|
||||
contextOptions.isMobile = void 0;
|
||||
if (options.blockServiceWorkers)
|
||||
contextOptions.serviceWorkers = "block";
|
||||
if (options.proxyServer) {
|
||||
launchOptions.proxy = {
|
||||
server: options.proxyServer
|
||||
};
|
||||
if (options.proxyBypass)
|
||||
launchOptions.proxy.bypass = options.proxyBypass;
|
||||
}
|
||||
if (options.viewportSize) {
|
||||
try {
|
||||
const [width, height] = options.viewportSize.split(",").map((n) => +n);
|
||||
if (isNaN(width) || isNaN(height))
|
||||
throw new Error("bad values");
|
||||
contextOptions.viewport = { width, height };
|
||||
} catch (e) {
|
||||
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
||||
}
|
||||
}
|
||||
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"];
|
||||
}
|
||||
if (options.userAgent)
|
||||
contextOptions.userAgent = options.userAgent;
|
||||
if (options.lang)
|
||||
contextOptions.locale = options.lang;
|
||||
if (options.colorScheme)
|
||||
contextOptions.colorScheme = options.colorScheme;
|
||||
if (options.timezone)
|
||||
contextOptions.timezoneId = options.timezone;
|
||||
if (options.loadStorage)
|
||||
contextOptions.storageState = options.loadStorage;
|
||||
if (options.ignoreHttpsErrors)
|
||||
contextOptions.ignoreHTTPSErrors = true;
|
||||
if (options.saveHar) {
|
||||
contextOptions.recordHar = { path: import_path.default.resolve(process.cwd(), options.saveHar), mode: "minimal" };
|
||||
if (options.saveHarGlob)
|
||||
contextOptions.recordHar.urlFilter = options.saveHarGlob;
|
||||
contextOptions.serviceWorkers = "block";
|
||||
}
|
||||
let browser;
|
||||
let context;
|
||||
if (options.userDataDir) {
|
||||
context = await browserType.launchPersistentContext(options.userDataDir, { ...launchOptions, ...contextOptions });
|
||||
browser = context.browser();
|
||||
} else {
|
||||
browser = await browserType.launch(launchOptions);
|
||||
context = await browser.newContext(contextOptions);
|
||||
}
|
||||
let closingBrowser = false;
|
||||
async function closeBrowser() {
|
||||
if (closingBrowser)
|
||||
return;
|
||||
closingBrowser = true;
|
||||
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", () => {
|
||||
});
|
||||
page.on("close", () => {
|
||||
const hasPage = browser.contexts().some((context2) => context2.pages().length > 0);
|
||||
if (hasPage)
|
||||
return;
|
||||
closeBrowser().catch(() => {
|
||||
});
|
||||
});
|
||||
});
|
||||
process.on("SIGINT", async () => {
|
||||
await closeBrowser();
|
||||
(0, import_utils.gracefullyProcessExitDoNotHang)(130);
|
||||
});
|
||||
const timeout = options.timeout ? parseInt(options.timeout, 10) : 0;
|
||||
context.setDefaultTimeout(timeout);
|
||||
context.setDefaultNavigationTimeout(timeout);
|
||||
delete launchOptions.headless;
|
||||
delete launchOptions.executablePath;
|
||||
delete launchOptions.handleSIGINT;
|
||||
delete contextOptions.deviceScaleFactor;
|
||||
return { browser, browserName: browserType.name(), context, contextOptions, launchOptions, closeBrowser };
|
||||
}
|
||||
async function openPage(context, url) {
|
||||
let page = context.pages()[0];
|
||||
if (!page)
|
||||
page = await context.newPage();
|
||||
if (url) {
|
||||
if (import_fs.default.existsSync(url))
|
||||
url = "file://" + import_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);
|
||||
}
|
||||
return page;
|
||||
}
|
||||
async function open(options, url) {
|
||||
const { context } = await launchContext(options, { headless: !!process.env.PWTEST_CLI_HEADLESS, executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH });
|
||||
await context._exposeConsoleApi();
|
||||
await openPage(context, url);
|
||||
}
|
||||
async function codegen(options, url) {
|
||||
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
||||
const tracesDir = import_path.default.join(import_os.default.tmpdir(), `playwright-recorder-trace-${Date.now()}`);
|
||||
const { context, browser, launchOptions, contextOptions, closeBrowser } = await launchContext(options, {
|
||||
headless: !!process.env.PWTEST_CLI_HEADLESS,
|
||||
executablePath: process.env.PWTEST_CLI_EXECUTABLE_PATH,
|
||||
tracesDir
|
||||
});
|
||||
const donePromise = new import_utils.ManualPromise();
|
||||
maybeSetupTestHooks(browser, closeBrowser, donePromise);
|
||||
import_utilsBundle.dotenv.config({ path: "playwright.env" });
|
||||
await context._enableRecorder({
|
||||
language,
|
||||
launchOptions,
|
||||
contextOptions,
|
||||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
mode: "recording",
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? import_path.default.resolve(outputFile) : void 0,
|
||||
handleSIGINT: false
|
||||
});
|
||||
await openPage(context, url);
|
||||
donePromise.resolve();
|
||||
}
|
||||
async function maybeSetupTestHooks(browser, closeBrowser, donePromise) {
|
||||
if (!process.env.PWTEST_CLI_IS_UNDER_TEST)
|
||||
return;
|
||||
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, import_utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
});
|
||||
const close = async () => {
|
||||
await donePromise;
|
||||
await closeBrowser();
|
||||
};
|
||||
if (process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT) {
|
||||
setTimeout(close, +process.env.PWTEST_CLI_EXIT_AFTER_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
let stdin = "";
|
||||
process.stdin.on("data", (data) => {
|
||||
stdin += data.toString();
|
||||
if (stdin.startsWith("exit")) {
|
||||
process.stdin.destroy();
|
||||
close();
|
||||
}
|
||||
});
|
||||
}
|
||||
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, path2) {
|
||||
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 " + path2);
|
||||
await page.screenshot({ path: path2, fullPage: !!captureOptions.fullPage });
|
||||
await page.close();
|
||||
}
|
||||
async function pdf(options, captureOptions, url, path2) {
|
||||
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 " + path2);
|
||||
await page.pdf({ path: path2, format: captureOptions.paperFormat });
|
||||
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;
|
||||
import_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"');
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
codegen,
|
||||
open,
|
||||
pdf,
|
||||
screenshot
|
||||
});
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var driver_exports = {};
|
||||
__export(driver_exports, {
|
||||
launchBrowserServer: () => launchBrowserServer,
|
||||
printApiJson: () => printApiJson,
|
||||
runDriver: () => runDriver,
|
||||
runServer: () => runServer
|
||||
});
|
||||
module.exports = __toCommonJS(driver_exports);
|
||||
var import_fs = __toESM(require("fs"));
|
||||
var playwright = __toESM(require("../.."));
|
||||
var import_pipeTransport = require("../server/utils/pipeTransport");
|
||||
var import_playwrightServer = require("../remote/playwrightServer");
|
||||
var import_server = require("../server");
|
||||
var import_processLauncher = require("../server/utils/processLauncher");
|
||||
function printApiJson() {
|
||||
console.log(JSON.stringify(require("../../api.json")));
|
||||
}
|
||||
function runDriver() {
|
||||
const dispatcherConnection = new import_server.DispatcherConnection();
|
||||
new import_server.RootDispatcher(dispatcherConnection, async (rootScope, { sdkLanguage }) => {
|
||||
const playwright2 = (0, import_server.createPlaywright)({ sdkLanguage });
|
||||
return new import_server.PlaywrightDispatcher(rootScope, playwright2);
|
||||
});
|
||||
const transport = new import_pipeTransport.PipeTransport(process.stdout, process.stdin);
|
||||
transport.onmessage = (message) => dispatcherConnection.dispatch(JSON.parse(message));
|
||||
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;
|
||||
} : void 0;
|
||||
dispatcherConnection.onmessage = (message) => transport.send(JSON.stringify(message, replacer));
|
||||
transport.onclose = () => {
|
||||
dispatcherConnection.onmessage = () => {
|
||||
};
|
||||
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(0);
|
||||
};
|
||||
process.on("SIGINT", () => {
|
||||
});
|
||||
}
|
||||
async function runServer(options) {
|
||||
const {
|
||||
port,
|
||||
host,
|
||||
path = "/",
|
||||
maxConnections = Infinity,
|
||||
extension,
|
||||
artifactsDir
|
||||
} = options;
|
||||
const server = new import_playwrightServer.PlaywrightServer({ mode: extension ? "extension" : "default", path, maxConnections, artifactsDir });
|
||||
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, import_processLauncher.gracefullyProcessExitDoNotHang)(0));
|
||||
}
|
||||
async function launchBrowserServer(browserName, configFile) {
|
||||
let options = {};
|
||||
if (configFile)
|
||||
options = JSON.parse(import_fs.default.readFileSync(configFile).toString());
|
||||
const browserType = playwright[browserName];
|
||||
const server = await browserType.launchServer(options);
|
||||
console.log(server.wsEndpoint());
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
launchBrowserServer,
|
||||
printApiJson,
|
||||
runDriver,
|
||||
runServer
|
||||
});
|
||||
Generated
Vendored
+171
@@ -0,0 +1,171 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var installActions_exports = {};
|
||||
__export(installActions_exports, {
|
||||
installBrowsers: () => installBrowsers,
|
||||
installDeps: () => installDeps,
|
||||
markDockerImage: () => markDockerImage,
|
||||
registry: () => import_server.registry,
|
||||
uninstallBrowsers: () => uninstallBrowsers
|
||||
});
|
||||
module.exports = __toCommonJS(installActions_exports);
|
||||
var import_path = __toESM(require("path"));
|
||||
var import_server = require("../server");
|
||||
var import_utils = require("../utils");
|
||||
var import_utils2 = require("../utils");
|
||||
var import_ascii = require("../server/utils/ascii");
|
||||
function printInstalledBrowsers(browsers) {
|
||||
const browserPaths = /* @__PURE__ */ new Set();
|
||||
for (const browser of browsers)
|
||||
browserPaths.add(browser.browserPath);
|
||||
console.log(` Browsers:`);
|
||||
for (const browserPath of [...browserPaths].sort())
|
||||
console.log(` ${browserPath}`);
|
||||
console.log(` References:`);
|
||||
const references = /* @__PURE__ */ new Set();
|
||||
for (const browser of browsers)
|
||||
references.add(browser.referenceDir);
|
||||
for (const reference of [...references].sort())
|
||||
console.log(` ${reference}`);
|
||||
}
|
||||
function printGroupedByPlaywrightVersion(browsers) {
|
||||
const dirToVersion = /* @__PURE__ */ new Map();
|
||||
for (const browser of browsers) {
|
||||
if (dirToVersion.has(browser.referenceDir))
|
||||
continue;
|
||||
const packageJSON = require(import_path.default.join(browser.referenceDir, "package.json"));
|
||||
const version = packageJSON.version;
|
||||
dirToVersion.set(browser.referenceDir, version);
|
||||
}
|
||||
const groupedByPlaywrightMinorVersion = /* @__PURE__ */ new Map();
|
||||
for (const browser of browsers) {
|
||||
const version = dirToVersion.get(browser.referenceDir);
|
||||
let entries = groupedByPlaywrightMinorVersion.get(version);
|
||||
if (!entries) {
|
||||
entries = [];
|
||||
groupedByPlaywrightMinorVersion.set(version, entries);
|
||||
}
|
||||
entries.push(browser);
|
||||
}
|
||||
const sortedVersions = [...groupedByPlaywrightMinorVersion.keys()].sort((a, b) => {
|
||||
const aComponents = a.split(".");
|
||||
const bComponents = b.split(".");
|
||||
const aMajor = parseInt(aComponents[0], 10);
|
||||
const bMajor = parseInt(bComponents[0], 10);
|
||||
if (aMajor !== bMajor)
|
||||
return aMajor - bMajor;
|
||||
const aMinor = parseInt(aComponents[1], 10);
|
||||
const bMinor = parseInt(bComponents[1], 10);
|
||||
if (aMinor !== bMinor)
|
||||
return aMinor - bMinor;
|
||||
return aComponents.slice(2).join(".").localeCompare(bComponents.slice(2).join("."));
|
||||
});
|
||||
for (const version of sortedVersions) {
|
||||
console.log(`
|
||||
Playwright version: ${version}`);
|
||||
printInstalledBrowsers(groupedByPlaywrightMinorVersion.get(version));
|
||||
}
|
||||
}
|
||||
async function markDockerImage(dockerImageNameTemplate) {
|
||||
(0, import_utils2.assert)(dockerImageNameTemplate, "dockerImageNameTemplate is required");
|
||||
await (0, import_server.writeDockerVersion)(dockerImageNameTemplate);
|
||||
}
|
||||
async function installBrowsers(args, options) {
|
||||
if ((0, import_utils.isLikelyNpxGlobal)()) {
|
||||
console.error((0, import_ascii.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));
|
||||
}
|
||||
if (options.shell === false && options.onlyShell)
|
||||
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
|
||||
const shell = options.shell === false ? "no" : options.onlyShell ? "only" : void 0;
|
||||
const executables = import_server.registry.resolveBrowsers(args, { shell });
|
||||
if (options.withDeps)
|
||||
await import_server.registry.installDeps(executables, !!options.dryRun);
|
||||
if (options.dryRun && options.list)
|
||||
throw new Error(`Only one of --dry-run and --list can be specified`);
|
||||
if (options.dryRun) {
|
||||
for (const executable of executables) {
|
||||
console.log(import_server.registry.calculateDownloadTitle(executable));
|
||||
console.log(` Install location: ${executable.directory ?? "<system>"}`);
|
||||
if (executable.downloadURLs?.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 if (options.list) {
|
||||
const browsers = await import_server.registry.listInstalledBrowsers();
|
||||
printGroupedByPlaywrightVersion(browsers);
|
||||
} else {
|
||||
await import_server.registry.install(executables, { force: options.force });
|
||||
await import_server.registry.validateHostRequirementsForExecutablesIfNeeded(executables, process.env.PW_LANG_NAME || "javascript").catch((e) => {
|
||||
e.name = "Playwright Host validation warning";
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
async function uninstallBrowsers(options) {
|
||||
delete process.env.PLAYWRIGHT_SKIP_BROWSER_GC;
|
||||
await import_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.
|
||||
To uninstall Playwright browsers for all installations, re-run with --all flag.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
async function installDeps(args, options) {
|
||||
await import_server.registry.installDeps(import_server.registry.resolveBrowsers(args, {}), !!options.dryRun);
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
installBrowsers,
|
||||
installDeps,
|
||||
markDockerImage,
|
||||
registry,
|
||||
uninstallBrowsers
|
||||
});
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var program_exports = {};
|
||||
__export(program_exports, {
|
||||
program: () => import_utilsBundle2.program
|
||||
});
|
||||
module.exports = __toCommonJS(program_exports);
|
||||
var import_bootstrap = require("../bootstrap");
|
||||
var import_utils = require("../utils");
|
||||
var import_traceCli = require("../tools/trace/traceCli");
|
||||
var import_utilsBundle = require("../utilsBundle");
|
||||
var import_utilsBundle2 = require("../utilsBundle");
|
||||
const packageJSON = require("../../package.json");
|
||||
import_utilsBundle.program.version("Version " + (process.env.PW_CLI_DISPLAY_VERSION || packageJSON.version)).name(buildBasePlaywrightCLICommand(process.env.PW_LANG_NAME));
|
||||
import_utilsBundle.program.command("mark-docker-image [dockerImageNameTemplate]", { hidden: true }).description("mark docker image").allowUnknownOption(true).action(async function(dockerImageNameTemplate) {
|
||||
const { markDockerImage } = require("./installActions");
|
||||
markDockerImage(dockerImageNameTemplate).catch(logErrorAndExit);
|
||||
});
|
||||
commandWithOpenOptions("open [url]", "open page in browser specified via -b, --browser", []).action(async function(url, options) {
|
||||
const { open } = require("./browserActions");
|
||||
open(options, url).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()],
|
||||
["--test-id-attribute <attributeName>", "use the specified attribute to generate data test ID selectors"]
|
||||
]
|
||||
).action(async function(url, options) {
|
||||
const { codegen } = require("./browserActions");
|
||||
await codegen(options, url);
|
||||
}).addHelpText("afterAll", `
|
||||
Examples:
|
||||
|
||||
$ codegen
|
||||
$ codegen --target=python
|
||||
$ codegen -b webkit https://example.com`);
|
||||
import_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("--list", "prints list of browsers from all playwright installations").option("--force", "force reinstall of already installed browsers").option("--only-shell", "only install headless shell when installing chromium").option("--no-shell", "do not install chromium headless shell").action(async function(args, options) {
|
||||
try {
|
||||
const { installBrowsers } = require("./installActions");
|
||||
await installBrowsers(args, options);
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browsers
|
||||
${e}`);
|
||||
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
}).addHelpText("afterAll", `
|
||||
|
||||
Examples:
|
||||
- $ install
|
||||
Install default browsers.
|
||||
|
||||
- $ install chrome firefox
|
||||
Install custom browsers, supports chromium, firefox, webkit, chromium-headless-shell.`);
|
||||
import_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) => {
|
||||
const { uninstallBrowsers } = require("./installActions");
|
||||
uninstallBrowsers(options).catch(logErrorAndExit);
|
||||
});
|
||||
import_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 {
|
||||
const { installDeps } = require("./installActions");
|
||||
await installDeps(args, options);
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browser dependencies
|
||||
${e}`);
|
||||
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
}).addHelpText("afterAll", `
|
||||
Examples:
|
||||
- $ install-deps
|
||||
Install dependencies for default browsers.
|
||||
|
||||
- $ install-deps chrome firefox
|
||||
Install dependencies for specific browsers, supports chromium, firefox, webkit, chromium-headless-shell.`);
|
||||
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(async function(url, options) {
|
||||
const { open } = require("./browserActions");
|
||||
open({ ...options, browser: type }, url).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(async function(url, filename, command) {
|
||||
const { screenshot } = require("./browserActions");
|
||||
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",
|
||||
[
|
||||
["--paper-format <format>", "paper format: Letter, Legal, Tabloid, Ledger, A0, A1, A2, A3, A4, A5, A6"],
|
||||
["--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(async function(url, filename, options) {
|
||||
const { pdf } = require("./browserActions");
|
||||
pdf(options, options, url, filename).catch(logErrorAndExit);
|
||||
}).addHelpText("afterAll", `
|
||||
Examples:
|
||||
|
||||
$ pdf https://example.com example.pdf`);
|
||||
import_utilsBundle.program.command("run-driver", { hidden: true }).action(async function(options) {
|
||||
const { runDriver } = require("./driver");
|
||||
runDriver();
|
||||
});
|
||||
import_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"').option("--artifacts-dir <artifactsDir>", "Artifacts directory").action(async function(options) {
|
||||
const { runServer } = require("./driver");
|
||||
runServer({
|
||||
port: options.port ? +options.port : void 0,
|
||||
host: options.host,
|
||||
path: options.path,
|
||||
maxConnections: options.maxClients ? +options.maxClients : Infinity,
|
||||
extension: options.mode === "extension" || !!process.env.PW_EXTENSION_MODE,
|
||||
artifactsDir: options.artifactsDir
|
||||
}).catch(logErrorAndExit);
|
||||
});
|
||||
import_utilsBundle.program.command("print-api-json", { hidden: true }).action(async function(options) {
|
||||
const { printApiJson } = require("./driver");
|
||||
printApiJson();
|
||||
});
|
||||
import_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(async function(options) {
|
||||
const { launchBrowserServer } = require("./driver");
|
||||
launchBrowserServer(options.browser, options.config);
|
||||
});
|
||||
import_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(async function(trace, 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
|
||||
};
|
||||
const { runTraceInBrowser, runTraceViewerApp } = require("../server/trace/viewer/traceViewer");
|
||||
if (options.port !== void 0 || options.host !== void 0)
|
||||
runTraceInBrowser(trace, openOptions).catch(logErrorAndExit);
|
||||
else
|
||||
runTraceViewerApp(trace, options.browser, openOptions).catch(logErrorAndExit);
|
||||
}).addHelpText("afterAll", `
|
||||
Examples:
|
||||
|
||||
$ show-trace
|
||||
$ show-trace https://example.com/trace.zip`);
|
||||
(0, import_traceCli.addTraceCommands)(import_utilsBundle.program, logErrorAndExit);
|
||||
import_utilsBundle.program.command("cli", { hidden: true }).allowExcessArguments(true).allowUnknownOption(true).action(async (options) => {
|
||||
const { program: cliProgram } = require("../tools/cli-client/program");
|
||||
process.argv.splice(process.argv.indexOf("cli"), 1);
|
||||
cliProgram().catch(logErrorAndExit);
|
||||
});
|
||||
function logErrorAndExit(e) {
|
||||
if (process.env.PWDEBUGIMPL)
|
||||
console.error(e);
|
||||
else
|
||||
console.error(e.name + ": " + e.message);
|
||||
(0, import_utils.gracefullyProcessExitDoNotHang)(1);
|
||||
}
|
||||
function codegenId() {
|
||||
return process.env.PW_LANG_NAME || "playwright-test";
|
||||
}
|
||||
function commandWithOpenOptions(command, description, options) {
|
||||
let result = import_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("--user-data-dir <directory>", "use the specified user data directory instead of a new context").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, import_utils.getPackageManagerExecCommand)();
|
||||
return `${packageManagerCommand} playwright`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
program
|
||||
});
|
||||
Generated
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var programWithTestStub_exports = {};
|
||||
__export(programWithTestStub_exports, {
|
||||
program: () => import_program2.program
|
||||
});
|
||||
module.exports = __toCommonJS(programWithTestStub_exports);
|
||||
var import_processLauncher = require("../server/utils/processLauncher");
|
||||
var import_utils = require("../utils");
|
||||
var import_program = require("./program");
|
||||
var import_program2 = require("./program");
|
||||
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, import_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 = import_program.program.command(command).allowUnknownOption(true).allowExcessArguments(true);
|
||||
playwrightTest.description(`${description} Available in @playwright/test package.`);
|
||||
playwrightTest.action(async () => {
|
||||
printPlaywrightTestError(command);
|
||||
(0, import_processLauncher.gracefullyProcessExitDoNotHang)(1);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!process.env.PW_LANG_NAME)
|
||||
addExternalPlaywrightTestCommands();
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
program
|
||||
});
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var android_exports = {};
|
||||
__export(android_exports, {
|
||||
Android: () => Android,
|
||||
AndroidDevice: () => AndroidDevice,
|
||||
AndroidInput: () => AndroidInput,
|
||||
AndroidSocket: () => AndroidSocket,
|
||||
AndroidWebView: () => AndroidWebView
|
||||
});
|
||||
module.exports = __toCommonJS(android_exports);
|
||||
var import_eventEmitter = require("./eventEmitter");
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_errors = require("./errors");
|
||||
var import_events = require("./events");
|
||||
var import_waiter = require("./waiter");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_time = require("../utils/isomorphic/time");
|
||||
var import_timeoutRunner = require("../utils/isomorphic/timeoutRunner");
|
||||
var import_connect = require("./connect");
|
||||
class Android extends import_channelOwner.ChannelOwner {
|
||||
static from(android) {
|
||||
return android._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(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(endpoint, options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
const deadline = options.timeout ? (0, import_time.monotonicTime)() + options.timeout : 0;
|
||||
const headers = { "x-playwright-browser": "android", ...options.headers };
|
||||
const connectParams = { endpoint, headers, slowMo: options.slowMo, timeout: options.timeout || 0 };
|
||||
const connection = await (0, import_connect.connectToEndpoint)(this._connection, connectParams);
|
||||
let device;
|
||||
connection.on("close", () => {
|
||||
device?._didClose();
|
||||
});
|
||||
const result = await (0, import_timeoutRunner.raceAgainstDeadline)(async () => {
|
||||
const playwright = await connection.initializePlaywright();
|
||||
if (!playwright._initializer.preConnectedAndroidDevice) {
|
||||
connection.close();
|
||||
throw new Error("Malformed endpoint. Did you use Android.launchServer method?");
|
||||
}
|
||||
device = AndroidDevice.from(playwright._initializer.preConnectedAndroidDevice);
|
||||
device._shouldCloseConnectionOnClose = true;
|
||||
device.on(import_events.Events.AndroidDevice.Close, () => connection.close());
|
||||
return device;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
connection.close();
|
||||
throw new Error(`Timeout ${options.timeout}ms exceeded`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
class AndroidDevice extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._webViews = /* @__PURE__ */ new Map();
|
||||
this._shouldCloseConnectionOnClose = false;
|
||||
this._android = parent;
|
||||
this.input = new AndroidInput(this);
|
||||
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform, parent._timeoutSettings);
|
||||
this._channel.on("webViewAdded", ({ webView }) => this._onWebViewAdded(webView));
|
||||
this._channel.on("webViewRemoved", ({ socketName }) => this._onWebViewRemoved(socketName));
|
||||
this._channel.on("close", () => this._didClose());
|
||||
}
|
||||
static from(androidDevice) {
|
||||
return androidDevice._object;
|
||||
}
|
||||
_onWebViewAdded(webView) {
|
||||
const view = new AndroidWebView(this, webView);
|
||||
this._webViews.set(webView.socketName, view);
|
||||
this.emit(import_events.Events.AndroidDevice.WebView, view);
|
||||
}
|
||||
_onWebViewRemoved(socketName) {
|
||||
const view = this._webViews.get(socketName);
|
||||
this._webViews.delete(socketName);
|
||||
if (view)
|
||||
view.emit(import_events.Events.AndroidWebView.Close);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(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({ androidSelector: toSelectorChannel(selector), ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async fill(selector, text, options = {}) {
|
||||
await this._channel.fill({ androidSelector: toSelectorChannel(selector), text, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async press(selector, key, options = {}) {
|
||||
await this.tap(selector, options);
|
||||
await this.input.press(key);
|
||||
}
|
||||
async tap(selector, options = {}) {
|
||||
await this._channel.tap({ androidSelector: toSelectorChannel(selector), ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async drag(selector, dest, options = {}) {
|
||||
await this._channel.drag({ androidSelector: toSelectorChannel(selector), dest, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async fling(selector, direction, options = {}) {
|
||||
await this._channel.fling({ androidSelector: toSelectorChannel(selector), direction, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async longTap(selector, options = {}) {
|
||||
await this._channel.longTap({ androidSelector: toSelectorChannel(selector), ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async pinchClose(selector, percent, options = {}) {
|
||||
await this._channel.pinchClose({ androidSelector: toSelectorChannel(selector), percent, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async pinchOpen(selector, percent, options = {}) {
|
||||
await this._channel.pinchOpen({ androidSelector: toSelectorChannel(selector), percent, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async scroll(selector, direction, percent, options = {}) {
|
||||
await this._channel.scroll({ androidSelector: toSelectorChannel(selector), direction, percent, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async swipe(selector, direction, percent, options = {}) {
|
||||
await this._channel.swipe({ androidSelector: toSelectorChannel(selector), direction, percent, ...options, timeout: this._timeoutSettings.timeout(options) });
|
||||
}
|
||||
async info(selector) {
|
||||
return (await this._channel.info({ androidSelector: toSelectorChannel(selector) })).info;
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
const { binary } = await this._channel.screenshot();
|
||||
if (options.path)
|
||||
await this._platform.fs().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, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
_didClose() {
|
||||
this.emit(import_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(this._platform, file), args: options && options.args });
|
||||
}
|
||||
async push(file, path, options) {
|
||||
await this._channel.push({ file: await loadFile(this._platform, file), path, mode: options ? options.mode : void 0 });
|
||||
}
|
||||
async launchBrowser(options = {}) {
|
||||
const contextOptions = await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options);
|
||||
const result = await this._channel.launchBrowser(contextOptions);
|
||||
const context = import_browserContext.BrowserContext.from(result.context);
|
||||
const selectors = this._android._playwright.selectors;
|
||||
selectors._contextsForSelectors.add(context);
|
||||
context.once(import_events.Events.BrowserContext.Close, () => selectors._contextsForSelectors.delete(context));
|
||||
await context._initializeHarFromOptions(options.recordHar);
|
||||
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 = import_waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== import_events.Events.AndroidDevice.Close)
|
||||
waiter.rejectOnEvent(this, import_events.Events.AndroidDevice.Close, () => new import_errors.TargetClosedError());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
class AndroidSocket extends import_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(import_events.Events.AndroidSocket.Data, data));
|
||||
this._channel.on("close", () => this.emit(import_events.Events.AndroidSocket.Close));
|
||||
}
|
||||
async write(data) {
|
||||
await this._channel.write({ data });
|
||||
}
|
||||
async close() {
|
||||
await this._channel.close();
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
}
|
||||
async function loadFile(platform, file) {
|
||||
if ((0, import_rtti.isString)(file))
|
||||
return await platform.fs().promises.readFile(file);
|
||||
return file;
|
||||
}
|
||||
class AndroidInput {
|
||||
constructor(device) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
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 === void 0)
|
||||
return void 0;
|
||||
if ((0, import_rtti.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 ? { androidSelector: toSelectorChannel(hasChild.selector) } : void 0,
|
||||
hasDescendant: hasDescendant ? { androidSelector: toSelectorChannel(hasDescendant.selector), maxDepth: hasDescendant.maxDepth } : void 0,
|
||||
longClickable,
|
||||
scrollable,
|
||||
selected
|
||||
};
|
||||
}
|
||||
class AndroidWebView extends import_eventEmitter.EventEmitter {
|
||||
constructor(device, data) {
|
||||
super(device._platform);
|
||||
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 import_browserContext.BrowserContext.from(context).pages()[0];
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Android,
|
||||
AndroidDevice,
|
||||
AndroidInput,
|
||||
AndroidSocket,
|
||||
AndroidWebView
|
||||
});
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var api_exports = {};
|
||||
__export(api_exports, {
|
||||
APIRequest: () => import_fetch.APIRequest,
|
||||
APIRequestContext: () => import_fetch.APIRequestContext,
|
||||
APIResponse: () => import_fetch.APIResponse,
|
||||
Android: () => import_android.Android,
|
||||
AndroidDevice: () => import_android.AndroidDevice,
|
||||
AndroidInput: () => import_android.AndroidInput,
|
||||
AndroidSocket: () => import_android.AndroidSocket,
|
||||
AndroidWebView: () => import_android.AndroidWebView,
|
||||
Browser: () => import_browser.Browser,
|
||||
BrowserContext: () => import_browserContext.BrowserContext,
|
||||
BrowserType: () => import_browserType.BrowserType,
|
||||
CDPSession: () => import_cdpSession.CDPSession,
|
||||
Clock: () => import_clock.Clock,
|
||||
ConsoleMessage: () => import_consoleMessage.ConsoleMessage,
|
||||
Coverage: () => import_coverage.Coverage,
|
||||
Debugger: () => import_debugger.Debugger,
|
||||
Dialog: () => import_dialog.Dialog,
|
||||
Download: () => import_download.Download,
|
||||
Electron: () => import_electron.Electron,
|
||||
ElectronApplication: () => import_electron.ElectronApplication,
|
||||
ElementHandle: () => import_elementHandle.ElementHandle,
|
||||
FileChooser: () => import_fileChooser.FileChooser,
|
||||
Frame: () => import_frame.Frame,
|
||||
FrameLocator: () => import_locator.FrameLocator,
|
||||
JSHandle: () => import_jsHandle.JSHandle,
|
||||
Keyboard: () => import_input.Keyboard,
|
||||
Locator: () => import_locator.Locator,
|
||||
Mouse: () => import_input.Mouse,
|
||||
Page: () => import_page.Page,
|
||||
Playwright: () => import_playwright.Playwright,
|
||||
Request: () => import_network.Request,
|
||||
Response: () => import_network.Response,
|
||||
Route: () => import_network.Route,
|
||||
Selectors: () => import_selectors.Selectors,
|
||||
TimeoutError: () => import_errors.TimeoutError,
|
||||
Touchscreen: () => import_input.Touchscreen,
|
||||
Tracing: () => import_tracing.Tracing,
|
||||
Video: () => import_video.Video,
|
||||
WebError: () => import_webError.WebError,
|
||||
WebSocket: () => import_network.WebSocket,
|
||||
WebSocketRoute: () => import_network.WebSocketRoute,
|
||||
Worker: () => import_worker.Worker
|
||||
});
|
||||
module.exports = __toCommonJS(api_exports);
|
||||
var import_android = require("./android");
|
||||
var import_browser = require("./browser");
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_browserType = require("./browserType");
|
||||
var import_clock = require("./clock");
|
||||
var import_consoleMessage = require("./consoleMessage");
|
||||
var import_coverage = require("./coverage");
|
||||
var import_debugger = require("./debugger");
|
||||
var import_dialog = require("./dialog");
|
||||
var import_download = require("./download");
|
||||
var import_electron = require("./electron");
|
||||
var import_locator = require("./locator");
|
||||
var import_elementHandle = require("./elementHandle");
|
||||
var import_fileChooser = require("./fileChooser");
|
||||
var import_errors = require("./errors");
|
||||
var import_frame = require("./frame");
|
||||
var import_input = require("./input");
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
var import_network = require("./network");
|
||||
var import_fetch = require("./fetch");
|
||||
var import_page = require("./page");
|
||||
var import_selectors = require("./selectors");
|
||||
var import_tracing = require("./tracing");
|
||||
var import_video = require("./video");
|
||||
var import_worker = require("./worker");
|
||||
var import_cdpSession = require("./cdpSession");
|
||||
var import_playwright = require("./playwright");
|
||||
var import_webError = require("./webError");
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
APIRequest,
|
||||
APIRequestContext,
|
||||
APIResponse,
|
||||
Android,
|
||||
AndroidDevice,
|
||||
AndroidInput,
|
||||
AndroidSocket,
|
||||
AndroidWebView,
|
||||
Browser,
|
||||
BrowserContext,
|
||||
BrowserType,
|
||||
CDPSession,
|
||||
Clock,
|
||||
ConsoleMessage,
|
||||
Coverage,
|
||||
Debugger,
|
||||
Dialog,
|
||||
Download,
|
||||
Electron,
|
||||
ElectronApplication,
|
||||
ElementHandle,
|
||||
FileChooser,
|
||||
Frame,
|
||||
FrameLocator,
|
||||
JSHandle,
|
||||
Keyboard,
|
||||
Locator,
|
||||
Mouse,
|
||||
Page,
|
||||
Playwright,
|
||||
Request,
|
||||
Response,
|
||||
Route,
|
||||
Selectors,
|
||||
TimeoutError,
|
||||
Touchscreen,
|
||||
Tracing,
|
||||
Video,
|
||||
WebError,
|
||||
WebSocket,
|
||||
WebSocketRoute,
|
||||
Worker
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var artifact_exports = {};
|
||||
__export(artifact_exports, {
|
||||
Artifact: () => Artifact
|
||||
});
|
||||
module.exports = __toCommonJS(artifact_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_stream = require("./stream");
|
||||
var import_fileUtils = require("./fileUtils");
|
||||
class Artifact extends import_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 = import_stream.Stream.from(result.stream);
|
||||
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, path);
|
||||
await new Promise((resolve, reject) => {
|
||||
stream.stream().pipe(this._platform.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 = import_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();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Artifact
|
||||
});
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var browser_exports = {};
|
||||
__export(browser_exports, {
|
||||
Browser: () => Browser
|
||||
});
|
||||
module.exports = __toCommonJS(browser_exports);
|
||||
var import_artifact = require("./artifact");
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_cdpSession = require("./cdpSession");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_errors = require("./errors");
|
||||
var import_events = require("./events");
|
||||
var import_fileUtils = require("./fileUtils");
|
||||
class Browser extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._contexts = /* @__PURE__ */ new Set();
|
||||
this._isConnected = true;
|
||||
this._shouldCloseConnectionOnClose = false;
|
||||
this._options = {};
|
||||
this._name = initializer.name;
|
||||
this._browserName = initializer.browserName;
|
||||
this._channel.on("context", ({ context }) => this._didCreateContext(import_browserContext.BrowserContext.from(context)));
|
||||
this._channel.on("close", () => this._didClose());
|
||||
this._closedPromise = new Promise((f) => this.once(import_events.Events.Browser.Disconnected, f));
|
||||
}
|
||||
static from(browser) {
|
||||
return browser._object;
|
||||
}
|
||||
browserType() {
|
||||
return this._browserType;
|
||||
}
|
||||
async newContext(options = {}) {
|
||||
return await this._innerNewContext(options, false);
|
||||
}
|
||||
async _newContextForReuse(options = {}) {
|
||||
return await this._innerNewContext(options, true);
|
||||
}
|
||||
async _disconnectFromReusedContext(reason) {
|
||||
const context = [...this._contexts].find((context2) => context2._forReuse);
|
||||
if (!context)
|
||||
return;
|
||||
await this._instrumentation.runBeforeCloseBrowserContext(context);
|
||||
for (const page of context.pages())
|
||||
page._onClose();
|
||||
context._onClose();
|
||||
await this._channel.disconnectFromReusedContext({ reason });
|
||||
}
|
||||
async _innerNewContext(userOptions = {}, forReuse) {
|
||||
const options = this._browserType._playwright.selectors._withSelectorOptions(userOptions);
|
||||
await this._instrumentation.runBeforeCreateBrowserContext(options);
|
||||
const contextOptions = await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options);
|
||||
const response = forReuse ? await this._channel.newContextForReuse(contextOptions) : await this._channel.newContext(contextOptions);
|
||||
const context = import_browserContext.BrowserContext.from(response.context);
|
||||
if (forReuse)
|
||||
context._forReuse = true;
|
||||
if (options.logger)
|
||||
context._logger = options.logger;
|
||||
await context._initializeHarFromOptions(options.recordHar);
|
||||
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||
return context;
|
||||
}
|
||||
_connectToBrowserType(browserType, browserOptions, logger) {
|
||||
this._browserType = browserType;
|
||||
this._options = browserOptions;
|
||||
this._logger = logger;
|
||||
for (const context of this._contexts)
|
||||
this._setupBrowserContext(context);
|
||||
}
|
||||
_didCreateContext(context) {
|
||||
context._browser = this;
|
||||
this._contexts.add(context);
|
||||
if (this._browserType)
|
||||
this._setupBrowserContext(context);
|
||||
}
|
||||
_setupBrowserContext(context) {
|
||||
context._logger = this._logger;
|
||||
context.tracing._tracesDir = this._options.tracesDir;
|
||||
this._browserType._contexts.add(context);
|
||||
this._browserType._playwright.selectors._contextsForSelectors.add(context);
|
||||
context.setDefaultTimeout(this._browserType._playwright._defaultContextTimeout);
|
||||
context.setDefaultNavigationTimeout(this._browserType._playwright._defaultContextNavigationTimeout);
|
||||
}
|
||||
contexts() {
|
||||
return [...this._contexts];
|
||||
}
|
||||
version() {
|
||||
return this._initializer.version;
|
||||
}
|
||||
async bind(title, options = {}) {
|
||||
const { endpoint } = await this._channel.startServer({ title, ...options });
|
||||
return { endpoint };
|
||||
}
|
||||
async unbind() {
|
||||
await this._channel.stopServer();
|
||||
}
|
||||
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;
|
||||
}, { title: "Create page" });
|
||||
}
|
||||
isConnected() {
|
||||
return this._isConnected;
|
||||
}
|
||||
async newBrowserCDPSession() {
|
||||
return import_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 : void 0 });
|
||||
}
|
||||
async stopTracing() {
|
||||
const artifact = import_artifact.Artifact.from((await this._channel.stopTracing()).artifact);
|
||||
const buffer = await artifact.readIntoBuffer();
|
||||
await artifact.delete();
|
||||
if (this._path) {
|
||||
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, this._path);
|
||||
await this._platform.fs().promises.writeFile(this._path, buffer);
|
||||
this._path = void 0;
|
||||
}
|
||||
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, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
_didClose() {
|
||||
this._isConnected = false;
|
||||
this.emit(import_events.Events.Browser.Disconnected, this);
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Browser
|
||||
});
|
||||
Generated
Vendored
+563
@@ -0,0 +1,563 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var browserContext_exports = {};
|
||||
__export(browserContext_exports, {
|
||||
BrowserContext: () => BrowserContext,
|
||||
prepareBrowserContextParams: () => prepareBrowserContextParams,
|
||||
toClientCertificatesProtocol: () => toClientCertificatesProtocol
|
||||
});
|
||||
module.exports = __toCommonJS(browserContext_exports);
|
||||
var import_artifact = require("./artifact");
|
||||
var import_cdpSession = require("./cdpSession");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_clientHelper = require("./clientHelper");
|
||||
var import_clock = require("./clock");
|
||||
var import_consoleMessage = require("./consoleMessage");
|
||||
var import_debugger = require("./debugger");
|
||||
var import_dialog = require("./dialog");
|
||||
var import_disposable = require("./disposable");
|
||||
var import_errors = require("./errors");
|
||||
var import_events = require("./events");
|
||||
var import_fetch = require("./fetch");
|
||||
var import_frame = require("./frame");
|
||||
var import_harRouter = require("./harRouter");
|
||||
var network = __toESM(require("./network"));
|
||||
var import_page = require("./page");
|
||||
var import_tracing = require("./tracing");
|
||||
var import_waiter = require("./waiter");
|
||||
var import_webError = require("./webError");
|
||||
var import_worker = require("./worker");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
var import_fileUtils = require("./fileUtils");
|
||||
var import_headers = require("../utils/isomorphic/headers");
|
||||
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||
class BrowserContext extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._pages = /* @__PURE__ */ new Set();
|
||||
this._routes = [];
|
||||
this._webSocketRoutes = [];
|
||||
// Browser is null for browser contexts created outside of normal browser, e.g. android or electron.
|
||||
this._browser = null;
|
||||
this._bindings = /* @__PURE__ */ new Map();
|
||||
this._forReuse = false;
|
||||
this._serviceWorkers = /* @__PURE__ */ new Set();
|
||||
this._harRecorders = /* @__PURE__ */ new Map();
|
||||
this._closingStatus = "none";
|
||||
this._harRouters = [];
|
||||
this._options = initializer.options;
|
||||
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||
this.debugger = import_debugger.Debugger.from(initializer.debugger);
|
||||
this.tracing = import_tracing.Tracing.from(initializer.tracing);
|
||||
this.request = import_fetch.APIRequestContext.from(initializer.requestContext);
|
||||
this.request._timeoutSettings = this._timeoutSettings;
|
||||
this.clock = new import_clock.Clock(this);
|
||||
this._channel.on("bindingCall", ({ binding }) => this._onBinding(import_page.BindingCall.from(binding)));
|
||||
this._channel.on("close", () => this._onClose());
|
||||
this._channel.on("page", ({ page }) => this._onPage(import_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("serviceWorker", ({ worker }) => {
|
||||
const serviceWorker = import_worker.Worker.from(worker);
|
||||
serviceWorker._context = this;
|
||||
this._serviceWorkers.add(serviceWorker);
|
||||
this.emit(import_events.Events.BrowserContext.ServiceWorker, serviceWorker);
|
||||
});
|
||||
this._channel.on("console", (event) => {
|
||||
const worker = import_worker.Worker.fromNullable(event.worker);
|
||||
const page = import_page.Page.fromNullable(event.page);
|
||||
const consoleMessage = new import_consoleMessage.ConsoleMessage(this._platform, event, page, worker);
|
||||
worker?.emit(import_events.Events.Worker.Console, consoleMessage);
|
||||
page?.emit(import_events.Events.Page.Console, consoleMessage);
|
||||
if (worker && this._serviceWorkers.has(worker)) {
|
||||
const scope = this._serviceWorkerScope(worker);
|
||||
for (const page2 of this._pages) {
|
||||
if (scope && page2.url().startsWith(scope))
|
||||
page2.emit(import_events.Events.Page.Console, consoleMessage);
|
||||
}
|
||||
}
|
||||
this.emit(import_events.Events.BrowserContext.Console, consoleMessage);
|
||||
});
|
||||
this._channel.on("pageError", ({ error, page }) => {
|
||||
const pageObject = import_page.Page.from(page);
|
||||
const parsedError = (0, import_errors.parseError)(error);
|
||||
this.emit(import_events.Events.BrowserContext.WebError, new import_webError.WebError(pageObject, parsedError));
|
||||
if (pageObject)
|
||||
pageObject.emit(import_events.Events.Page.PageError, parsedError);
|
||||
});
|
||||
this._channel.on("dialog", ({ dialog }) => {
|
||||
const dialogObject = import_dialog.Dialog.from(dialog);
|
||||
let hasListeners = this.emit(import_events.Events.BrowserContext.Dialog, dialogObject);
|
||||
const page = dialogObject.page();
|
||||
if (page)
|
||||
hasListeners = page.emit(import_events.Events.Page.Dialog, dialogObject) || hasListeners;
|
||||
if (!hasListeners) {
|
||||
if (dialogObject.type() === "beforeunload")
|
||||
dialog.accept({}).catch(() => {
|
||||
});
|
||||
else
|
||||
dialog.dismiss().catch(() => {
|
||||
});
|
||||
}
|
||||
});
|
||||
this._channel.on("request", ({ request, page }) => this._onRequest(network.Request.from(request), import_page.Page.fromNullable(page)));
|
||||
this._channel.on("requestFailed", ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, import_page.Page.fromNullable(page)));
|
||||
this._channel.on("requestFinished", (params) => this._onRequestFinished(params));
|
||||
this._channel.on("response", ({ response, page }) => this._onResponse(network.Response.from(response), import_page.Page.fromNullable(page)));
|
||||
this._channel.on("recorderEvent", ({ event, data, page, code }) => {
|
||||
if (event === "actionAdded")
|
||||
this._onRecorderEventSink?.actionAdded?.(import_page.Page.from(page), data, code);
|
||||
else if (event === "actionUpdated")
|
||||
this._onRecorderEventSink?.actionUpdated?.(import_page.Page.from(page), data, code);
|
||||
else if (event === "signalAdded")
|
||||
this._onRecorderEventSink?.signalAdded?.(import_page.Page.from(page), data);
|
||||
});
|
||||
this._closedPromise = new Promise((f) => this.once(import_events.Events.BrowserContext.Close, f));
|
||||
this._setEventToSubscriptionMapping(/* @__PURE__ */ new Map([
|
||||
[import_events.Events.BrowserContext.Console, "console"],
|
||||
[import_events.Events.BrowserContext.Dialog, "dialog"],
|
||||
[import_events.Events.BrowserContext.Request, "request"],
|
||||
[import_events.Events.BrowserContext.Response, "response"],
|
||||
[import_events.Events.BrowserContext.RequestFinished, "requestFinished"],
|
||||
[import_events.Events.BrowserContext.RequestFailed, "requestFailed"]
|
||||
]));
|
||||
}
|
||||
static from(context) {
|
||||
return context._object;
|
||||
}
|
||||
static fromNullable(context) {
|
||||
return context ? BrowserContext.from(context) : null;
|
||||
}
|
||||
async _initializeHarFromOptions(recordHar) {
|
||||
if (!recordHar)
|
||||
return;
|
||||
const defaultContent = recordHar.path.endsWith(".zip") ? "attach" : "embed";
|
||||
await this._recordIntoHAR(recordHar.path, null, {
|
||||
url: recordHar.urlFilter,
|
||||
updateContent: recordHar.content ?? (recordHar.omitContent ? "omit" : defaultContent),
|
||||
updateMode: recordHar.mode ?? "full"
|
||||
});
|
||||
}
|
||||
_onPage(page) {
|
||||
this._pages.add(page);
|
||||
this.emit(import_events.Events.BrowserContext.Page, page);
|
||||
if (page._opener && !page._opener.isClosed())
|
||||
page._opener.emit(import_events.Events.Page.Popup, page);
|
||||
}
|
||||
_onRequest(request, page) {
|
||||
this.emit(import_events.Events.BrowserContext.Request, request);
|
||||
if (page)
|
||||
page.emit(import_events.Events.Page.Request, request);
|
||||
}
|
||||
_onResponse(response, page) {
|
||||
this.emit(import_events.Events.BrowserContext.Response, response);
|
||||
if (page)
|
||||
page.emit(import_events.Events.Page.Response, response);
|
||||
}
|
||||
_onRequestFailed(request, responseEndTiming, failureText, page) {
|
||||
request._failureText = failureText || null;
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(import_events.Events.BrowserContext.RequestFailed, request);
|
||||
if (page)
|
||||
page.emit(import_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 = import_page.Page.fromNullable(params.page);
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(import_events.Events.BrowserContext.RequestFinished, request);
|
||||
if (page)
|
||||
page.emit(import_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 (page?._closeWasCalled || this.isClosed())
|
||||
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._updateInterceptionPatterns({ internal: true }).catch(() => {
|
||||
});
|
||||
if (handled)
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
_serviceWorkerScope(serviceWorker) {
|
||||
try {
|
||||
let url = new URL(".", serviceWorker.url()).href;
|
||||
if (!url.endsWith("/"))
|
||||
url += "/";
|
||||
return url;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
browser() {
|
||||
return this._browser;
|
||||
}
|
||||
pages() {
|
||||
return [...this._pages];
|
||||
}
|
||||
isClosed() {
|
||||
return this._closingStatus !== "none";
|
||||
}
|
||||
async newPage() {
|
||||
if (this._ownerPage)
|
||||
throw new Error("Please use browser.newContext()");
|
||||
return import_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 })).cookies;
|
||||
}
|
||||
async addCookies(cookies) {
|
||||
await this._channel.addCookies({ cookies });
|
||||
}
|
||||
async clearCookies(options = {}) {
|
||||
await this._channel.clearCookies({
|
||||
name: (0, import_rtti.isString)(options.name) ? options.name : void 0,
|
||||
nameRegexSource: (0, import_rtti.isRegExp)(options.name) ? options.name.source : void 0,
|
||||
nameRegexFlags: (0, import_rtti.isRegExp)(options.name) ? options.name.flags : void 0,
|
||||
domain: (0, import_rtti.isString)(options.domain) ? options.domain : void 0,
|
||||
domainRegexSource: (0, import_rtti.isRegExp)(options.domain) ? options.domain.source : void 0,
|
||||
domainRegexFlags: (0, import_rtti.isRegExp)(options.domain) ? options.domain.flags : void 0,
|
||||
path: (0, import_rtti.isString)(options.path) ? options.path : void 0,
|
||||
pathRegexSource: (0, import_rtti.isRegExp)(options.path) ? options.path.source : void 0,
|
||||
pathRegexFlags: (0, import_rtti.isRegExp)(options.path) ? options.path.flags : void 0
|
||||
});
|
||||
}
|
||||
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 || void 0 });
|
||||
}
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
network.validateHeaders(headers);
|
||||
await this._channel.setExtraHTTPHeaders({ headers: (0, import_headers.headersObjectToArray)(headers) });
|
||||
}
|
||||
async setOffline(offline) {
|
||||
await this._channel.setOffline({ offline });
|
||||
}
|
||||
async setHTTPCredentials(httpCredentials) {
|
||||
await this._channel.setHTTPCredentials({ httpCredentials: httpCredentials || void 0 });
|
||||
}
|
||||
async addInitScript(script, arg) {
|
||||
const source = await (0, import_clientHelper.evaluationScript)(this._platform, script, arg);
|
||||
return import_disposable.DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
|
||||
}
|
||||
async exposeBinding(name, callback, options = {}) {
|
||||
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
|
||||
this._bindings.set(name, callback);
|
||||
return import_disposable.DisposableObject.from(result.disposable);
|
||||
}
|
||||
async exposeFunction(name, callback) {
|
||||
const result = await this._channel.exposeBinding({ name });
|
||||
const binding = (source, ...args) => callback(...args);
|
||||
this._bindings.set(name, binding);
|
||||
return import_disposable.DisposableObject.from(result.disposable);
|
||||
}
|
||||
async route(url, handler, options = {}) {
|
||||
this._routes.unshift(new network.RouteHandler(this._platform, this._options.baseURL, url, handler, options.times));
|
||||
await this._updateInterceptionPatterns({ title: "Route requests" });
|
||||
return new import_disposable.DisposableStub(() => this.unroute(url, handler));
|
||||
}
|
||||
async routeWebSocket(url, handler) {
|
||||
this._webSocketRoutes.unshift(new network.WebSocketRouteHandler(this._options.baseURL, url, handler));
|
||||
await this._updateWebSocketInterceptionPatterns({ title: "Route WebSockets" });
|
||||
}
|
||||
async _recordIntoHAR(har, page, options = {}) {
|
||||
const { harId } = await this._channel.harStart({
|
||||
page: page?._channel,
|
||||
options: {
|
||||
zip: har.endsWith(".zip"),
|
||||
content: options.updateContent ?? "attach",
|
||||
urlGlob: (0, import_rtti.isString)(options.url) ? options.url : void 0,
|
||||
urlRegexSource: (0, import_rtti.isRegExp)(options.url) ? options.url.source : void 0,
|
||||
urlRegexFlags: (0, import_rtti.isRegExp)(options.url) ? options.url.flags : void 0,
|
||||
mode: options.updateMode ?? "minimal"
|
||||
}
|
||||
});
|
||||
this._harRecorders.set(harId, { path: har, content: options.updateContent ?? "attach" });
|
||||
}
|
||||
async routeFromHAR(har, options = {}) {
|
||||
const localUtils = this._connection.localUtils();
|
||||
if (!localUtils)
|
||||
throw new Error("Route from har is not supported in thin clients");
|
||||
if (options.update) {
|
||||
await this._recordIntoHAR(har, null, options);
|
||||
return;
|
||||
}
|
||||
const harRouter = await import_harRouter.HarRouter.create(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?.behavior);
|
||||
this._disposeHarRouters();
|
||||
}
|
||||
async unroute(url, handler) {
|
||||
const removed = [];
|
||||
const remaining = [];
|
||||
for (const route of this._routes) {
|
||||
if ((0, import_urlMatch.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;
|
||||
if (behavior && behavior !== "default") {
|
||||
const promises = removed.map((routeHandler) => routeHandler.stop(behavior));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
await this._updateInterceptionPatterns({ title: "Unroute requests" });
|
||||
}
|
||||
async _updateInterceptionPatterns(options) {
|
||||
const patterns = network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||
await this._wrapApiCall(() => this._channel.setNetworkInterceptionPatterns({ patterns }), options);
|
||||
}
|
||||
async _updateWebSocketInterceptionPatterns(options) {
|
||||
const patterns = network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||
await this._wrapApiCall(() => this._channel.setWebSocketInterceptionPatterns({ patterns }), options);
|
||||
}
|
||||
_effectiveCloseReason() {
|
||||
return this._closeReason || this._browser?._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 = import_waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== import_events.Events.BrowserContext.Close)
|
||||
waiter.rejectOnEvent(this, import_events.Events.BrowserContext.Close, () => new import_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({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, void 0, 2), "utf8");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
async setStorageState(storageState) {
|
||||
const state = await prepareStorageState(this._platform, storageState);
|
||||
await this._channel.setStorageState({ storageState: state });
|
||||
}
|
||||
backgroundPages() {
|
||||
return [];
|
||||
}
|
||||
serviceWorkers() {
|
||||
return [...this._serviceWorkers];
|
||||
}
|
||||
async newCDPSession(page) {
|
||||
if (!(page instanceof import_page.Page) && !(page instanceof import_frame.Frame))
|
||||
throw new Error("page: expected Page or Frame");
|
||||
const result = await this._channel.newCDPSession(page instanceof import_page.Page ? { page: page._channel } : { frame: page._channel });
|
||||
return import_cdpSession.CDPSession.from(result.session);
|
||||
}
|
||||
_onClose() {
|
||||
this._closingStatus = "closed";
|
||||
this._browser?._contexts.delete(this);
|
||||
this._browser?._browserType._contexts.delete(this);
|
||||
this._browser?._browserType._playwright.selectors._contextsForSelectors.delete(this);
|
||||
this._disposeHarRouters();
|
||||
this.tracing._resetStackCounter();
|
||||
this.emit(import_events.Events.BrowserContext.Close, this);
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.close();
|
||||
}
|
||||
async close(options = {}) {
|
||||
if (this.isClosed())
|
||||
return;
|
||||
this._closeReason = options.reason;
|
||||
this._closingStatus = "closing";
|
||||
await this.request.dispose(options);
|
||||
await this._instrumentation.runBeforeCloseBrowserContext(this);
|
||||
await this._wrapApiCall(async () => {
|
||||
for (const [harId, harParams] of this._harRecorders) {
|
||||
const har = await this._channel.harExport({ harId });
|
||||
const artifact = import_artifact.Artifact.from(har.artifact);
|
||||
const isCompressed = harParams.content === "attach" || harParams.path.endsWith(".zip");
|
||||
const needCompressed = harParams.path.endsWith(".zip");
|
||||
if (isCompressed && !needCompressed) {
|
||||
const localUtils = this._connection.localUtils();
|
||||
if (!localUtils)
|
||||
throw new Error("Uncompressed har is not supported in thin clients");
|
||||
await artifact.saveAs(harParams.path + ".tmp");
|
||||
await localUtils.harUnzip({ zipFile: harParams.path + ".tmp", harFile: harParams.path });
|
||||
} else {
|
||||
await artifact.saveAs(harParams.path);
|
||||
}
|
||||
await artifact.delete();
|
||||
}
|
||||
}, { internal: true });
|
||||
await this._channel.close(options);
|
||||
await this._closedPromise;
|
||||
}
|
||||
async _enableRecorder(params, eventSink) {
|
||||
if (eventSink)
|
||||
this._onRecorderEventSink = eventSink;
|
||||
await this._channel.enableRecorder(params);
|
||||
}
|
||||
async _disableRecorder() {
|
||||
this._onRecorderEventSink = void 0;
|
||||
await this._channel.disableRecorder();
|
||||
}
|
||||
async _exposeConsoleApi() {
|
||||
await this._channel.exposeConsoleApi();
|
||||
}
|
||||
}
|
||||
async function prepareStorageState(platform, storageState) {
|
||||
if (typeof storageState !== "string")
|
||||
return storageState;
|
||||
try {
|
||||
return JSON.parse(await platform.fs().promises.readFile(storageState, "utf8"));
|
||||
} catch (e) {
|
||||
(0, import_stackTrace.rewriteErrorMessage)(e, `Error reading storage state from ${storageState}:
|
||||
` + e.message);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async function prepareBrowserContextParams(platform, 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 ? void 0 : options.viewport,
|
||||
noDefaultViewport: options.viewport === null,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? (0, import_headers.headersObjectToArray)(options.extraHTTPHeaders) : void 0,
|
||||
storageState: options.storageState ? await prepareStorageState(platform, options.storageState) : void 0,
|
||||
serviceWorkers: options.serviceWorkers,
|
||||
colorScheme: options.colorScheme === null ? "no-override" : options.colorScheme,
|
||||
reducedMotion: options.reducedMotion === null ? "no-override" : options.reducedMotion,
|
||||
forcedColors: options.forcedColors === null ? "no-override" : options.forcedColors,
|
||||
contrast: options.contrast === null ? "no-override" : options.contrast,
|
||||
acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads),
|
||||
clientCertificates: await toClientCertificatesProtocol(platform, options.clientCertificates)
|
||||
};
|
||||
if (!contextParams.recordVideo && options.videosPath) {
|
||||
contextParams.recordVideo = {
|
||||
dir: options.videosPath,
|
||||
size: options.videoSize
|
||||
};
|
||||
}
|
||||
if (contextParams.recordVideo && contextParams.recordVideo.dir)
|
||||
contextParams.recordVideo.dir = platform.path().resolve(contextParams.recordVideo.dir);
|
||||
return contextParams;
|
||||
}
|
||||
function toAcceptDownloadsProtocol(acceptDownloads) {
|
||||
if (acceptDownloads === void 0)
|
||||
return void 0;
|
||||
if (acceptDownloads)
|
||||
return "accept";
|
||||
return "deny";
|
||||
}
|
||||
async function toClientCertificatesProtocol(platform, certs) {
|
||||
if (!certs)
|
||||
return void 0;
|
||||
const bufferizeContent = async (value, path) => {
|
||||
if (value)
|
||||
return value;
|
||||
if (path)
|
||||
return await platform.fs().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
|
||||
})));
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
BrowserContext,
|
||||
prepareBrowserContextParams,
|
||||
toClientCertificatesProtocol
|
||||
});
|
||||
Generated
Vendored
+153
@@ -0,0 +1,153 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var browserType_exports = {};
|
||||
__export(browserType_exports, {
|
||||
BrowserType: () => BrowserType
|
||||
});
|
||||
module.exports = __toCommonJS(browserType_exports);
|
||||
var import_browser = require("./browser");
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_clientHelper = require("./clientHelper");
|
||||
var import_assert = require("../utils/isomorphic/assert");
|
||||
var import_headers = require("../utils/isomorphic/headers");
|
||||
var import_connect = require("./connect");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
class BrowserType extends import_channelOwner.ChannelOwner {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this._contexts = /* @__PURE__ */ new Set();
|
||||
}
|
||||
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 = {}) {
|
||||
(0, import_assert.assert)(!options.userDataDir, "userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead");
|
||||
(0, import_assert.assert)(!options.port, "Cannot specify a port without launching as a server.");
|
||||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...options };
|
||||
const launchOptions = {
|
||||
...options,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? (0, import_clientHelper.envObjectToArray)(options.env) : void 0,
|
||||
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).launchTimeout(options)
|
||||
};
|
||||
return await this._wrapApiCall(async () => {
|
||||
const browser = import_browser.Browser.from((await this._channel.launch(launchOptions)).browser);
|
||||
browser._connectToBrowserType(this, options, logger);
|
||||
return browser;
|
||||
});
|
||||
}
|
||||
async launchServer(options = {}) {
|
||||
if (!this._serverLauncher)
|
||||
throw new Error("Launching server is not supported");
|
||||
options = { ...this._playwright._defaultLaunchOptions, ...options };
|
||||
return await this._serverLauncher.launchServer(options);
|
||||
}
|
||||
async launchPersistentContext(userDataDir, options = {}) {
|
||||
(0, import_assert.assert)(!options.port, "Cannot specify a port without launching as a server.");
|
||||
options = this._playwright.selectors._withSelectorOptions({
|
||||
...this._playwright._defaultLaunchOptions,
|
||||
...options
|
||||
});
|
||||
await this._instrumentation.runBeforeCreateBrowserContext(options);
|
||||
const logger = options.logger || this._playwright._defaultLaunchOptions?.logger;
|
||||
const contextParams = await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options);
|
||||
const persistentParams = {
|
||||
...contextParams,
|
||||
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : void 0,
|
||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||
env: options.env ? (0, import_clientHelper.envObjectToArray)(options.env) : void 0,
|
||||
channel: options.channel,
|
||||
userDataDir: this._platform.path().isAbsolute(userDataDir) || !userDataDir ? userDataDir : this._platform.path().resolve(userDataDir),
|
||||
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).launchTimeout(options)
|
||||
};
|
||||
const context = await this._wrapApiCall(async () => {
|
||||
const result = await this._channel.launchPersistentContext(persistentParams);
|
||||
const browser = import_browser.Browser.from(result.browser);
|
||||
browser._connectToBrowserType(this, options, logger);
|
||||
const context2 = import_browserContext.BrowserContext.from(result.context);
|
||||
await context2._initializeHarFromOptions(options.recordHar);
|
||||
return context2;
|
||||
});
|
||||
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||
return context;
|
||||
}
|
||||
async connect(optionsOrEndpoint, options) {
|
||||
if (typeof optionsOrEndpoint === "string")
|
||||
return await this._connect({ ...options, endpoint: optionsOrEndpoint });
|
||||
(0, import_assert.assert)(optionsOrEndpoint.wsEndpoint, "options.wsEndpoint is required");
|
||||
return await this._connect({ ...options, endpoint: optionsOrEndpoint.wsEndpoint });
|
||||
}
|
||||
async _connect(params) {
|
||||
const logger = params.logger;
|
||||
return await this._wrapApiCall(async () => {
|
||||
const browser = await (0, import_connect.connectToBrowser)(this._playwright, { browserName: this.name(), ...params });
|
||||
browser._connectToBrowserType(this, {}, logger);
|
||||
return browser;
|
||||
});
|
||||
}
|
||||
async connectOverCDP(endpointURLOrOptions, options) {
|
||||
if (typeof endpointURLOrOptions === "string")
|
||||
return await this._connectOverCDP(endpointURLOrOptions, options);
|
||||
const endpointURL = "endpointURL" in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
|
||||
(0, import_assert.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, import_headers.headersObjectToArray)(params.headers) : void 0;
|
||||
const result = await this._channel.connectOverCDP({
|
||||
endpointURL,
|
||||
headers,
|
||||
slowMo: params.slowMo,
|
||||
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).timeout(params),
|
||||
isLocal: params.isLocal
|
||||
});
|
||||
const browser = import_browser.Browser.from(result.browser);
|
||||
browser._connectToBrowserType(this, {}, params.logger);
|
||||
if (result.defaultContext)
|
||||
await this._instrumentation.runAfterCreateBrowserContext(import_browserContext.BrowserContext.from(result.defaultContext));
|
||||
return browser;
|
||||
}
|
||||
async _connectOverCDPTransport(transport) {
|
||||
if (this.name() !== "chromium")
|
||||
throw new Error("Connecting over CDP is only supported in Chromium.");
|
||||
const result = await this._channel.connectOverCDPTransport({ transport });
|
||||
const browser = import_browser.Browser.from(result.browser);
|
||||
browser._connectToBrowserType(this, {}, void 0);
|
||||
if (result.defaultContext)
|
||||
await this._instrumentation.runAfterCreateBrowserContext(import_browserContext.BrowserContext.from(result.defaultContext));
|
||||
return browser;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
BrowserType
|
||||
});
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var cdpSession_exports = {};
|
||||
__export(cdpSession_exports, {
|
||||
CDPSession: () => CDPSession
|
||||
});
|
||||
module.exports = __toCommonJS(cdpSession_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
class CDPSession extends import_channelOwner.ChannelOwner {
|
||||
static from(cdpSession) {
|
||||
return cdpSession._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._channel.on("event", (event) => {
|
||||
this.emit(event.method, event.params);
|
||||
this.emit("event", event);
|
||||
});
|
||||
this._channel.on("close", () => {
|
||||
this.emit("close", this);
|
||||
});
|
||||
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();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
CDPSession
|
||||
});
|
||||
Generated
Vendored
+194
@@ -0,0 +1,194 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var channelOwner_exports = {};
|
||||
__export(channelOwner_exports, {
|
||||
ChannelOwner: () => ChannelOwner
|
||||
});
|
||||
module.exports = __toCommonJS(channelOwner_exports);
|
||||
var import_eventEmitter = require("./eventEmitter");
|
||||
var import_validator = require("../protocol/validator");
|
||||
var import_protocolMetainfo = require("../utils/isomorphic/protocolMetainfo");
|
||||
var import_clientStackTrace = require("./clientStackTrace");
|
||||
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||
class ChannelOwner extends import_eventEmitter.EventEmitter {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
const connection = parent instanceof ChannelOwner ? parent._connection : parent;
|
||||
super(connection._platform);
|
||||
this._objects = /* @__PURE__ */ new Map();
|
||||
this._eventToSubscriptionMapping = /* @__PURE__ */ new Map();
|
||||
this._wasCollected = false;
|
||||
this.setMaxListeners(0);
|
||||
this._connection = connection;
|
||||
this._type = type;
|
||||
this._guid = guid;
|
||||
this._parent = parent instanceof ChannelOwner ? parent : void 0;
|
||||
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 import_eventEmitter.EventEmitter(connection._platform));
|
||||
this._initializer = initializer;
|
||||
}
|
||||
_setEventToSubscriptionMapping(mapping) {
|
||||
this._eventToSubscriptionMapping = mapping;
|
||||
}
|
||||
_updateSubscription(event, enabled) {
|
||||
const protocolEvent = this._eventToSubscriptionMapping.get(String(event));
|
||||
if (protocolEvent)
|
||||
this._channel.updateSubscription({ event: protocolEvent, enabled }).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) {
|
||||
if (this._parent)
|
||||
this._parent._objects.delete(this._guid);
|
||||
this._connection._objects.delete(this._guid);
|
||||
this._wasCollected = reason === "gc";
|
||||
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())
|
||||
};
|
||||
}
|
||||
_validatorToWireContext() {
|
||||
return {
|
||||
tChannelImpl: tChannelImplToWire,
|
||||
binary: this._connection.rawBuffers() ? "buffer" : "toBase64",
|
||||
isUnderTest: () => this._platform.isUnderTest()
|
||||
};
|
||||
}
|
||||
_createChannel(base) {
|
||||
const channel = new Proxy(base, {
|
||||
get: (obj, prop) => {
|
||||
if (typeof prop === "string") {
|
||||
const validator = (0, import_validator.maybeFindValidator)(this._type, prop, "Params");
|
||||
const { internal } = (0, import_protocolMetainfo.getMetainfo)({ type: this._type, method: prop }) || {};
|
||||
if (validator) {
|
||||
return async (params) => {
|
||||
return await this._wrapApiCall(async (apiZone) => {
|
||||
const validatedParams = validator(params, "", this._validatorToWireContext());
|
||||
if (!apiZone.internal && !apiZone.reported) {
|
||||
apiZone.reported = true;
|
||||
this._instrumentation.onApiCallBegin(apiZone, { type: this._type, method: prop, params });
|
||||
logApiCall(this._platform, this._logger, `=> ${apiZone.apiName} started`);
|
||||
return await this._connection.sendMessageToServer(this, prop, validatedParams, apiZone);
|
||||
}
|
||||
return await this._connection.sendMessageToServer(this, prop, validatedParams, { internal: true });
|
||||
}, { internal });
|
||||
};
|
||||
}
|
||||
}
|
||||
return obj[prop];
|
||||
}
|
||||
});
|
||||
channel._object = this;
|
||||
return channel;
|
||||
}
|
||||
async _wrapApiCall(func, options) {
|
||||
const logger = this._logger;
|
||||
const existingApiZone = this._platform.zones.current().data();
|
||||
if (existingApiZone)
|
||||
return await func(existingApiZone);
|
||||
const stackTrace = (0, import_clientStackTrace.captureLibraryStackTrace)(this._platform);
|
||||
const apiZone = { title: options?.title, apiName: stackTrace.apiName, frames: stackTrace.frames, internal: options?.internal ?? false, reported: false, userData: void 0, stepId: void 0 };
|
||||
try {
|
||||
const result = await this._platform.zones.current().push(apiZone).run(async () => await func(apiZone));
|
||||
if (!options?.internal) {
|
||||
logApiCall(this._platform, logger, `<= ${apiZone.apiName} succeeded`);
|
||||
this._instrumentation.onApiCallEnd(apiZone);
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
const innerError = (this._platform.showInternalStackFrames() || this._platform.isUnderTest()) && e.stack ? "\n<inner error>\n" + e.stack : "";
|
||||
if (apiZone.apiName && !apiZone.apiName.includes("<anonymous>"))
|
||||
e.message = apiZone.apiName + ": " + e.message;
|
||||
const stackFrames = "\n" + (0, import_stackTrace.stringifyStackFrames)(stackTrace.frames).join("\n") + innerError;
|
||||
if (stackFrames.trim())
|
||||
e.stack = e.message + stackFrames;
|
||||
else
|
||||
e.stack = "";
|
||||
if (!options?.internal) {
|
||||
apiZone.error = e;
|
||||
logApiCall(this._platform, logger, `<= ${apiZone.apiName} failed`);
|
||||
this._instrumentation.onApiCallEnd(apiZone);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
toJSON() {
|
||||
return {
|
||||
_type: this._type,
|
||||
_guid: this._guid
|
||||
};
|
||||
}
|
||||
}
|
||||
function logApiCall(platform, logger, message) {
|
||||
if (logger && logger.isEnabled("api", "info"))
|
||||
logger.log("api", "info", message, [], { color: "cyan" });
|
||||
platform.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 import_validator.ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
ChannelOwner
|
||||
});
|
||||
Generated
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var clientHelper_exports = {};
|
||||
__export(clientHelper_exports, {
|
||||
addSourceUrlToScript: () => addSourceUrlToScript,
|
||||
envObjectToArray: () => envObjectToArray,
|
||||
evaluationScript: () => evaluationScript
|
||||
});
|
||||
module.exports = __toCommonJS(clientHelper_exports);
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
function envObjectToArray(env) {
|
||||
const result = [];
|
||||
for (const name in env) {
|
||||
if (!Object.is(env[name], void 0))
|
||||
result.push({ name, value: String(env[name]) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function evaluationScript(platform, fun, arg, addSourceUrl = true) {
|
||||
if (typeof fun === "function") {
|
||||
const source = fun.toString();
|
||||
const argString = Object.is(arg, void 0) ? "undefined" : JSON.stringify(arg);
|
||||
return `(${source})(${argString})`;
|
||||
}
|
||||
if (arg !== void 0)
|
||||
throw new Error("Cannot evaluate a string with arguments");
|
||||
if ((0, import_rtti.isString)(fun))
|
||||
return fun;
|
||||
if (fun.content !== void 0)
|
||||
return fun.content;
|
||||
if (fun.path !== void 0) {
|
||||
let source = await platform.fs().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}
|
||||
//# sourceURL=${path.replace(/\n/g, "")}`;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
addSourceUrlToScript,
|
||||
envObjectToArray,
|
||||
evaluationScript
|
||||
});
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var clientInstrumentation_exports = {};
|
||||
__export(clientInstrumentation_exports, {
|
||||
createInstrumentation: () => createInstrumentation
|
||||
});
|
||||
module.exports = __toCommonJS(clientInstrumentation_exports);
|
||||
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)
|
||||
await listener[prop]?.(...params);
|
||||
};
|
||||
}
|
||||
if (prop.startsWith("on")) {
|
||||
return (...params) => {
|
||||
for (const listener of listeners)
|
||||
listener[prop]?.(...params);
|
||||
};
|
||||
}
|
||||
return obj[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
createInstrumentation
|
||||
});
|
||||
Generated
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var clientStackTrace_exports = {};
|
||||
__export(clientStackTrace_exports, {
|
||||
captureLibraryStackTrace: () => captureLibraryStackTrace
|
||||
});
|
||||
module.exports = __toCommonJS(clientStackTrace_exports);
|
||||
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||
function captureLibraryStackTrace(platform) {
|
||||
const stack = (0, import_stackTrace.captureRawStack)();
|
||||
let parsedFrames = stack.map((line) => {
|
||||
const frame = (0, import_stackTrace.parseStackFrame)(line, platform.pathSeparator, platform.showInternalStackFrames());
|
||||
if (!frame || !frame.file)
|
||||
return null;
|
||||
const isPlaywrightLibrary = !!platform.coreDir && frame.file.startsWith(platform.coreDir);
|
||||
const parsed = {
|
||||
frame,
|
||||
frameText: line,
|
||||
isPlaywrightLibrary
|
||||
};
|
||||
return parsed;
|
||||
}).filter(Boolean);
|
||||
let apiName = "";
|
||||
for (let i = 0; i < parsedFrames.length - 1; i++) {
|
||||
const parsedFrame = parsedFrames[i];
|
||||
if (parsedFrame.isPlaywrightLibrary && !parsedFrames[i + 1].isPlaywrightLibrary) {
|
||||
apiName = apiName || normalizeAPIName(parsedFrame.frame.function);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function normalizeAPIName(name) {
|
||||
if (!name)
|
||||
return "";
|
||||
const match = name.match(/(API|JS|CDP|[A-Z])(.*)/);
|
||||
if (!match)
|
||||
return name;
|
||||
return match[1].toLowerCase() + match[2];
|
||||
}
|
||||
const filterPrefixes = platform.boxedStackPrefixes();
|
||||
parsedFrames = parsedFrames.filter((f) => {
|
||||
if (filterPrefixes.some((prefix) => f.frame.file.startsWith(prefix)))
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
return {
|
||||
frames: parsedFrames.map((p) => p.frame),
|
||||
apiName
|
||||
};
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
captureLibraryStackTrace
|
||||
});
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var clock_exports = {};
|
||||
__export(clock_exports, {
|
||||
Clock: () => Clock
|
||||
});
|
||||
module.exports = __toCommonJS(clock_exports);
|
||||
class Clock {
|
||||
constructor(browserContext) {
|
||||
this._browserContext = browserContext;
|
||||
}
|
||||
async install(options = {}) {
|
||||
await this._browserContext._channel.clockInstall(options.time !== void 0 ? 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));
|
||||
}
|
||||
}
|
||||
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 : void 0,
|
||||
ticksString: typeof ticks === "string" ? ticks : void 0
|
||||
};
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Clock
|
||||
});
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var connect_exports = {};
|
||||
__export(connect_exports, {
|
||||
connectToBrowser: () => connectToBrowser,
|
||||
connectToEndpoint: () => connectToEndpoint
|
||||
});
|
||||
module.exports = __toCommonJS(connect_exports);
|
||||
var import_time = require("../utils/isomorphic/time");
|
||||
var import_timeoutRunner = require("../utils/isomorphic/timeoutRunner");
|
||||
var import_browser = require("./browser");
|
||||
var import_connection = require("./connection");
|
||||
var import_events = require("./events");
|
||||
async function connectToBrowser(playwright, params) {
|
||||
const deadline = params.timeout ? (0, import_time.monotonicTime)() + params.timeout : 0;
|
||||
const nameParam = params.browserName ? { "x-playwright-browser": params.browserName } : {};
|
||||
const headers = { ...nameParam, ...params.headers };
|
||||
const connectParams = {
|
||||
endpoint: params.endpoint,
|
||||
headers,
|
||||
exposeNetwork: params.exposeNetwork,
|
||||
slowMo: params.slowMo,
|
||||
timeout: params.timeout || 0
|
||||
};
|
||||
if (params.__testHookRedirectPortForwarding)
|
||||
connectParams.socksProxyRedirectPortForTest = params.__testHookRedirectPortForwarding;
|
||||
const connection = await connectToEndpoint(playwright._connection, connectParams);
|
||||
let browser;
|
||||
connection.on("close", () => {
|
||||
for (const context of browser?.contexts() || []) {
|
||||
for (const page of context.pages())
|
||||
page._onClose();
|
||||
context._onClose();
|
||||
}
|
||||
setTimeout(() => browser?._didClose(), 0);
|
||||
});
|
||||
const result = await (0, import_timeoutRunner.raceAgainstDeadline)(async () => {
|
||||
if (params.__testHookBeforeCreateBrowser)
|
||||
await params.__testHookBeforeCreateBrowser();
|
||||
const playwright2 = await connection.initializePlaywright();
|
||||
if (!playwright2._initializer.preLaunchedBrowser) {
|
||||
connection.close();
|
||||
throw new Error("Malformed endpoint. Did you use BrowserType.launchServer method?");
|
||||
}
|
||||
playwright2.selectors = playwright2.selectors;
|
||||
browser = import_browser.Browser.from(playwright2._initializer.preLaunchedBrowser);
|
||||
browser._shouldCloseConnectionOnClose = true;
|
||||
browser.on(import_events.Events.Browser.Disconnected, () => connection.close());
|
||||
return browser;
|
||||
}, deadline);
|
||||
if (!result.timedOut) {
|
||||
return result.result;
|
||||
} else {
|
||||
connection.close();
|
||||
throw new Error(`Timeout ${params.timeout}ms exceeded`);
|
||||
}
|
||||
}
|
||||
async function connectToEndpoint(parentConnection, params) {
|
||||
const localUtils = parentConnection.localUtils();
|
||||
const transport = localUtils ? new JsonPipeTransport(localUtils) : new WebSocketTransport();
|
||||
const connectHeaders = await transport.connect(params);
|
||||
const connection = new import_connection.Connection(parentConnection._platform, localUtils, parentConnection._instrumentation, connectHeaders);
|
||||
connection.markAsRemote();
|
||||
connection.on("close", () => transport.close());
|
||||
let closeError;
|
||||
const onTransportClosed = (reason) => {
|
||||
connection.close(reason || closeError);
|
||||
};
|
||||
transport.onClose((reason) => onTransportClosed(reason));
|
||||
connection.onmessage = (message) => transport.send(message).catch(() => onTransportClosed());
|
||||
transport.onMessage((message) => {
|
||||
try {
|
||||
connection.dispatch(message);
|
||||
} catch (e) {
|
||||
closeError = String(e);
|
||||
transport.close().catch(() => {
|
||||
});
|
||||
}
|
||||
});
|
||||
return connection;
|
||||
}
|
||||
class JsonPipeTransport {
|
||||
constructor(owner) {
|
||||
this._owner = owner;
|
||||
}
|
||||
async connect(params) {
|
||||
const { pipe, headers: connectHeaders } = await this._owner._channel.connect(params);
|
||||
this._pipe = pipe;
|
||||
return connectHeaders;
|
||||
}
|
||||
async send(message) {
|
||||
await this._pipe.send({ message });
|
||||
}
|
||||
onMessage(callback) {
|
||||
this._pipe.on("message", ({ message }) => callback(message));
|
||||
}
|
||||
onClose(callback) {
|
||||
this._pipe.on("closed", ({ reason }) => callback(reason));
|
||||
}
|
||||
async close() {
|
||||
await this._pipe.close().catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
class WebSocketTransport {
|
||||
async connect(params) {
|
||||
this._ws = new window.WebSocket(params.endpoint);
|
||||
return [];
|
||||
}
|
||||
async send(message) {
|
||||
this._ws.send(JSON.stringify(message));
|
||||
}
|
||||
onMessage(callback) {
|
||||
this._ws.addEventListener("message", (event) => callback(JSON.parse(event.data)));
|
||||
}
|
||||
onClose(callback) {
|
||||
this._ws.addEventListener("close", () => callback());
|
||||
}
|
||||
async close() {
|
||||
this._ws.close();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
connectToBrowser,
|
||||
connectToEndpoint
|
||||
});
|
||||
Generated
Vendored
+322
@@ -0,0 +1,322 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var connection_exports = {};
|
||||
__export(connection_exports, {
|
||||
Connection: () => Connection
|
||||
});
|
||||
module.exports = __toCommonJS(connection_exports);
|
||||
var import_eventEmitter = require("./eventEmitter");
|
||||
var import_android = require("./android");
|
||||
var import_artifact = require("./artifact");
|
||||
var import_browser = require("./browser");
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_browserType = require("./browserType");
|
||||
var import_cdpSession = require("./cdpSession");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_clientInstrumentation = require("./clientInstrumentation");
|
||||
var import_debugger = require("./debugger");
|
||||
var import_dialog = require("./dialog");
|
||||
var import_disposable = require("./disposable");
|
||||
var import_electron = require("./electron");
|
||||
var import_elementHandle = require("./elementHandle");
|
||||
var import_errors = require("./errors");
|
||||
var import_fetch = require("./fetch");
|
||||
var import_frame = require("./frame");
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
var import_jsonPipe = require("./jsonPipe");
|
||||
var import_localUtils = require("./localUtils");
|
||||
var import_network = require("./network");
|
||||
var import_page = require("./page");
|
||||
var import_playwright = require("./playwright");
|
||||
var import_stream = require("./stream");
|
||||
var import_tracing = require("./tracing");
|
||||
var import_worker = require("./worker");
|
||||
var import_writableStream = require("./writableStream");
|
||||
var import_validator = require("../protocol/validator");
|
||||
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||
class Root extends import_channelOwner.ChannelOwner {
|
||||
constructor(connection) {
|
||||
super(connection, "Root", "", {});
|
||||
}
|
||||
async initialize() {
|
||||
return import_playwright.Playwright.from((await this._channel.initialize({
|
||||
sdkLanguage: "javascript"
|
||||
})).playwright);
|
||||
}
|
||||
}
|
||||
class DummyChannelOwner extends import_channelOwner.ChannelOwner {
|
||||
}
|
||||
class Connection extends import_eventEmitter.EventEmitter {
|
||||
constructor(platform, localUtils, instrumentation, headers = []) {
|
||||
super(platform);
|
||||
this._objects = /* @__PURE__ */ new Map();
|
||||
this.onmessage = (message) => {
|
||||
};
|
||||
this._lastId = 0;
|
||||
this._callbacks = /* @__PURE__ */ new Map();
|
||||
this._isRemote = false;
|
||||
this._rawBuffers = false;
|
||||
this._tracingCount = 0;
|
||||
this._instrumentation = instrumentation || (0, import_clientInstrumentation.createInstrumentation)();
|
||||
this._localUtils = localUtils;
|
||||
this._rootObject = new Root(this);
|
||||
this.headers = headers;
|
||||
}
|
||||
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, options) {
|
||||
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 (this._platform.isLogEnabled("channel")) {
|
||||
this._platform.log("channel", "SEND> " + JSON.stringify(message));
|
||||
}
|
||||
const location = options.frames?.[0] ? { file: options.frames[0].file, line: options.frames[0].line, column: options.frames[0].column } : void 0;
|
||||
const metadata = { title: options.title, location, internal: options.internal, stepId: options.stepId };
|
||||
if (this._tracingCount && options.frames && type !== "LocalUtils")
|
||||
this._localUtils?.addStackToTracingNoReply({ callData: { stack: options.frames ?? [], id } }).catch(() => {
|
||||
});
|
||||
this._platform.zones.empty.run(() => this.onmessage({ ...message, metadata }));
|
||||
return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, title: options.title, type, method }));
|
||||
}
|
||||
_validatorFromWireContext() {
|
||||
return {
|
||||
tChannelImpl: this._tChannelImplFromWire.bind(this),
|
||||
binary: this._rawBuffers ? "buffer" : "fromBase64",
|
||||
isUnderTest: () => this._platform.isUnderTest()
|
||||
};
|
||||
}
|
||||
dispatch(message) {
|
||||
if (this._closedError)
|
||||
return;
|
||||
const { id, guid, method, params, result, error, log } = message;
|
||||
if (id) {
|
||||
if (this._platform.isLogEnabled("channel"))
|
||||
this._platform.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, import_errors.parseError)(error);
|
||||
(0, import_stackTrace.rewriteErrorMessage)(parsedError, parsedError.message + formatCallLog(this._platform, log));
|
||||
callback.reject(parsedError);
|
||||
} else {
|
||||
const validator2 = (0, import_validator.findValidator)(callback.type, callback.method, "Result");
|
||||
callback.resolve(validator2(result, "", this._validatorFromWireContext()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._platform.isLogEnabled("channel"))
|
||||
this._platform.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, import_validator.findValidator)(object._type, method, "Event");
|
||||
object._channel.emit(method, validator(params, "", this._validatorFromWireContext()));
|
||||
}
|
||||
close(cause) {
|
||||
if (this._closedError)
|
||||
return;
|
||||
this._closedError = new import_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 import_validator.ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
return object._channel;
|
||||
}
|
||||
throw new import_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, import_validator.findValidator)(type, "", "Initializer");
|
||||
initializer = validator(initializer, "", this._validatorFromWireContext());
|
||||
switch (type) {
|
||||
case "Android":
|
||||
result = new import_android.Android(parent, type, guid, initializer);
|
||||
break;
|
||||
case "AndroidSocket":
|
||||
result = new import_android.AndroidSocket(parent, type, guid, initializer);
|
||||
break;
|
||||
case "AndroidDevice":
|
||||
result = new import_android.AndroidDevice(parent, type, guid, initializer);
|
||||
break;
|
||||
case "APIRequestContext":
|
||||
result = new import_fetch.APIRequestContext(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Artifact":
|
||||
result = new import_artifact.Artifact(parent, type, guid, initializer);
|
||||
break;
|
||||
case "BindingCall":
|
||||
result = new import_page.BindingCall(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Browser":
|
||||
result = new import_browser.Browser(parent, type, guid, initializer);
|
||||
break;
|
||||
case "BrowserContext":
|
||||
result = new import_browserContext.BrowserContext(parent, type, guid, initializer);
|
||||
break;
|
||||
case "BrowserType":
|
||||
result = new import_browserType.BrowserType(parent, type, guid, initializer);
|
||||
break;
|
||||
case "CDPSession":
|
||||
result = new import_cdpSession.CDPSession(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Debugger":
|
||||
result = new import_debugger.Debugger(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Dialog":
|
||||
result = new import_dialog.Dialog(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Disposable":
|
||||
result = new import_disposable.DisposableObject(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Electron":
|
||||
result = new import_electron.Electron(parent, type, guid, initializer);
|
||||
break;
|
||||
case "ElectronApplication":
|
||||
result = new import_electron.ElectronApplication(parent, type, guid, initializer);
|
||||
break;
|
||||
case "ElementHandle":
|
||||
result = new import_elementHandle.ElementHandle(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Frame":
|
||||
result = new import_frame.Frame(parent, type, guid, initializer);
|
||||
break;
|
||||
case "JSHandle":
|
||||
result = new import_jsHandle.JSHandle(parent, type, guid, initializer);
|
||||
break;
|
||||
case "JsonPipe":
|
||||
result = new import_jsonPipe.JsonPipe(parent, type, guid, initializer);
|
||||
break;
|
||||
case "LocalUtils":
|
||||
result = new import_localUtils.LocalUtils(parent, type, guid, initializer);
|
||||
if (!this._localUtils)
|
||||
this._localUtils = result;
|
||||
break;
|
||||
case "Page":
|
||||
result = new import_page.Page(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Playwright":
|
||||
result = new import_playwright.Playwright(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Request":
|
||||
result = new import_network.Request(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Response":
|
||||
result = new import_network.Response(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Route":
|
||||
result = new import_network.Route(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Stream":
|
||||
result = new import_stream.Stream(parent, type, guid, initializer);
|
||||
break;
|
||||
case "SocksSupport":
|
||||
result = new DummyChannelOwner(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Tracing":
|
||||
result = new import_tracing.Tracing(parent, type, guid, initializer);
|
||||
break;
|
||||
case "WebSocket":
|
||||
result = new import_network.WebSocket(parent, type, guid, initializer);
|
||||
break;
|
||||
case "WebSocketRoute":
|
||||
result = new import_network.WebSocketRoute(parent, type, guid, initializer);
|
||||
break;
|
||||
case "Worker":
|
||||
result = new import_worker.Worker(parent, type, guid, initializer);
|
||||
break;
|
||||
case "WritableStream":
|
||||
result = new import_writableStream.WritableStream(parent, type, guid, initializer);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Missing type " + type);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
function formatCallLog(platform, log) {
|
||||
if (!log || !log.some((l) => !!l))
|
||||
return "";
|
||||
return `
|
||||
Call log:
|
||||
${platform.colors.dim(log.join("\n"))}
|
||||
`;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Connection
|
||||
});
|
||||
Generated
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var consoleMessage_exports = {};
|
||||
__export(consoleMessage_exports, {
|
||||
ConsoleMessage: () => ConsoleMessage
|
||||
});
|
||||
module.exports = __toCommonJS(consoleMessage_exports);
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
class ConsoleMessage {
|
||||
constructor(platform, event, page, worker) {
|
||||
this._page = page;
|
||||
this._worker = worker;
|
||||
this._event = event;
|
||||
if (platform.inspectCustom)
|
||||
this[platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
worker() {
|
||||
return this._worker;
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
type() {
|
||||
return this._event.type;
|
||||
}
|
||||
text() {
|
||||
return this._event.text;
|
||||
}
|
||||
args() {
|
||||
return this._event.args.map(import_jsHandle.JSHandle.from);
|
||||
}
|
||||
location() {
|
||||
return this._event.location;
|
||||
}
|
||||
timestamp() {
|
||||
return this._event.timestamp;
|
||||
}
|
||||
_inspect() {
|
||||
return this.text();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
ConsoleMessage
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var coverage_exports = {};
|
||||
__export(coverage_exports, {
|
||||
Coverage: () => Coverage
|
||||
});
|
||||
module.exports = __toCommonJS(coverage_exports);
|
||||
class Coverage {
|
||||
constructor(channel) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Coverage
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var debugger_exports = {};
|
||||
__export(debugger_exports, {
|
||||
Debugger: () => Debugger
|
||||
});
|
||||
module.exports = __toCommonJS(debugger_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_events = require("./events");
|
||||
class Debugger extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._pausedDetails = null;
|
||||
this._channel.on("pausedStateChanged", ({ pausedDetails }) => {
|
||||
this._pausedDetails = pausedDetails ?? null;
|
||||
this.emit(import_events.Events.Debugger.PausedStateChanged);
|
||||
});
|
||||
}
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
async requestPause() {
|
||||
await this._channel.requestPause();
|
||||
}
|
||||
async resume() {
|
||||
await this._channel.resume();
|
||||
}
|
||||
async next() {
|
||||
await this._channel.next();
|
||||
}
|
||||
async runTo(location) {
|
||||
await this._channel.runTo({ location });
|
||||
}
|
||||
pausedDetails() {
|
||||
return this._pausedDetails;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Debugger
|
||||
});
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var dialog_exports = {};
|
||||
__export(dialog_exports, {
|
||||
Dialog: () => Dialog
|
||||
});
|
||||
module.exports = __toCommonJS(dialog_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_page = require("./page");
|
||||
var import_errors = require("./errors");
|
||||
class Dialog extends import_channelOwner.ChannelOwner {
|
||||
static from(dialog) {
|
||||
return dialog._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._page = import_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() {
|
||||
try {
|
||||
await this._channel.dismiss();
|
||||
} catch (e) {
|
||||
if ((0, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Dialog
|
||||
});
|
||||
Generated
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var disposable_exports = {};
|
||||
__export(disposable_exports, {
|
||||
DisposableObject: () => DisposableObject,
|
||||
DisposableStub: () => DisposableStub,
|
||||
disposeAll: () => disposeAll
|
||||
});
|
||||
module.exports = __toCommonJS(disposable_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_errors = require("./errors");
|
||||
class DisposableObject extends import_channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose() {
|
||||
try {
|
||||
await this._channel.dispose();
|
||||
} catch (e) {
|
||||
if ((0, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
class DisposableStub {
|
||||
constructor(dispose) {
|
||||
this._dispose = dispose;
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose() {
|
||||
if (!this._dispose)
|
||||
return;
|
||||
try {
|
||||
const dispose = this._dispose;
|
||||
this._dispose = void 0;
|
||||
await dispose();
|
||||
} catch (e) {
|
||||
if ((0, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
async function disposeAll(disposables) {
|
||||
const copy = [...disposables];
|
||||
disposables.length = 0;
|
||||
await Promise.all(copy.map((d) => d.dispose()));
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
DisposableObject,
|
||||
DisposableStub,
|
||||
disposeAll
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var download_exports = {};
|
||||
__export(download_exports, {
|
||||
Download: () => Download
|
||||
});
|
||||
module.exports = __toCommonJS(download_exports);
|
||||
class Download {
|
||||
constructor(page, url, suggestedFilename, artifact) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Download
|
||||
});
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var electron_exports = {};
|
||||
__export(electron_exports, {
|
||||
Electron: () => Electron,
|
||||
ElectronApplication: () => ElectronApplication
|
||||
});
|
||||
module.exports = __toCommonJS(electron_exports);
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_clientHelper = require("./clientHelper");
|
||||
var import_consoleMessage = require("./consoleMessage");
|
||||
var import_errors = require("./errors");
|
||||
var import_events = require("./events");
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
var import_waiter = require("./waiter");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
class Electron extends import_channelOwner.ChannelOwner {
|
||||
static from(electron) {
|
||||
return electron._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
async launch(options = {}) {
|
||||
options = this._playwright.selectors._withSelectorOptions(options);
|
||||
const params = {
|
||||
...await (0, import_browserContext.prepareBrowserContextParams)(this._platform, options),
|
||||
env: (0, import_clientHelper.envObjectToArray)(options.env ? options.env : this._platform.env),
|
||||
tracesDir: options.tracesDir,
|
||||
artifactsDir: options.artifactsDir,
|
||||
timeout: new import_timeoutSettings.TimeoutSettings(this._platform).launchTimeout(options)
|
||||
};
|
||||
const app = ElectronApplication.from((await this._channel.launch(params)).electronApplication);
|
||||
this._playwright.selectors._contextsForSelectors.add(app._context);
|
||||
app.once(import_events.Events.ElectronApplication.Close, () => this._playwright.selectors._contextsForSelectors.delete(app._context));
|
||||
await app._context._initializeHarFromOptions(options.recordHar);
|
||||
app._context.tracing._tracesDir = options.tracesDir;
|
||||
return app;
|
||||
}
|
||||
}
|
||||
class ElectronApplication extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._windows = /* @__PURE__ */ new Set();
|
||||
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||
this._context = import_browserContext.BrowserContext.from(initializer.context);
|
||||
for (const page of this._context._pages)
|
||||
this._onPage(page);
|
||||
this._context.on(import_events.Events.BrowserContext.Page, (page) => this._onPage(page));
|
||||
this._channel.on("close", () => {
|
||||
this.emit(import_events.Events.ElectronApplication.Close);
|
||||
});
|
||||
this._channel.on("console", (event) => this.emit(import_events.Events.ElectronApplication.Console, new import_consoleMessage.ConsoleMessage(this._platform, event, null, null)));
|
||||
this._setEventToSubscriptionMapping(/* @__PURE__ */ new Map([
|
||||
[import_events.Events.ElectronApplication.Console, "console"]
|
||||
]));
|
||||
}
|
||||
static from(electronApplication) {
|
||||
return electronApplication._object;
|
||||
}
|
||||
process() {
|
||||
return this._connection.toImpl?.(this)?.process();
|
||||
}
|
||||
_onPage(page) {
|
||||
this._windows.add(page);
|
||||
this.emit(import_events.Events.ElectronApplication.Window, page);
|
||||
page.once(import_events.Events.Page.Close, () => this._windows.delete(page));
|
||||
}
|
||||
windows() {
|
||||
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, import_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 = import_waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== import_events.Events.ElectronApplication.Close)
|
||||
waiter.rejectOnEvent(this, import_events.Events.ElectronApplication.Close, () => new import_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 import_jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return import_jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Electron,
|
||||
ElectronApplication
|
||||
});
|
||||
Generated
Vendored
+281
@@ -0,0 +1,281 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var elementHandle_exports = {};
|
||||
__export(elementHandle_exports, {
|
||||
ElementHandle: () => ElementHandle,
|
||||
convertInputFiles: () => convertInputFiles,
|
||||
convertSelectOptionValues: () => convertSelectOptionValues,
|
||||
determineScreenshotType: () => determineScreenshotType
|
||||
});
|
||||
module.exports = __toCommonJS(elementHandle_exports);
|
||||
var import_frame = require("./frame");
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
var import_assert = require("../utils/isomorphic/assert");
|
||||
var import_fileUtils = require("./fileUtils");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_writableStream = require("./writableStream");
|
||||
var import_mimeType = require("../utils/isomorphic/mimeType");
|
||||
class ElementHandle extends import_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._frame = parent;
|
||||
this._elementChannel = this._channel;
|
||||
}
|
||||
asElement() {
|
||||
return this;
|
||||
}
|
||||
async ownerFrame() {
|
||||
return import_frame.Frame.fromNullable((await this._elementChannel.ownerFrame()).frame);
|
||||
}
|
||||
async contentFrame() {
|
||||
return import_frame.Frame.fromNullable((await this._elementChannel.contentFrame()).frame);
|
||||
}
|
||||
async getAttribute(name) {
|
||||
const value = (await this._elementChannel.getAttribute({ name })).value;
|
||||
return value === void 0 ? null : value;
|
||||
}
|
||||
async inputValue() {
|
||||
return (await this._elementChannel.inputValue()).value;
|
||||
}
|
||||
async textContent() {
|
||||
const value = (await this._elementChannel.textContent()).value;
|
||||
return value === void 0 ? 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, import_jsHandle.serializeArgument)(eventInit) });
|
||||
}
|
||||
async scrollIntoViewIfNeeded(options = {}) {
|
||||
await this._elementChannel.scrollIntoViewIfNeeded({ ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async hover(options = {}) {
|
||||
await this._elementChannel.hover({ ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async click(options = {}) {
|
||||
return await this._elementChannel.click({ ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async dblclick(options = {}) {
|
||||
return await this._elementChannel.dblclick({ ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async tap(options = {}) {
|
||||
return await this._elementChannel.tap({ ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async selectOption(values, options = {}) {
|
||||
const result = await this._elementChannel.selectOption({ ...convertSelectOptionValues(values), ...options, timeout: this._frame._timeout(options) });
|
||||
return result.values;
|
||||
}
|
||||
async fill(value, options = {}) {
|
||||
return await this._elementChannel.fill({ value, ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async selectText(options = {}) {
|
||||
await this._elementChannel.selectText({ ...options, timeout: this._frame._timeout(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(this._platform, files, frame.page().context());
|
||||
await this._elementChannel.setInputFiles({ ...converted, ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async focus() {
|
||||
await this._elementChannel.focus();
|
||||
}
|
||||
async type(text, options = {}) {
|
||||
await this._elementChannel.type({ text, ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async press(key, options = {}) {
|
||||
await this._elementChannel.press({ key, ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async check(options = {}) {
|
||||
return await this._elementChannel.check({ ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async uncheck(options = {}) {
|
||||
return await this._elementChannel.uncheck({ ...options, timeout: this._frame._timeout(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 === void 0 ? null : value;
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
const mask = options.mask;
|
||||
const copy = { ...options, mask: void 0, timeout: this._frame._timeout(options) };
|
||||
if (!copy.type)
|
||||
copy.type = determineScreenshotType(options);
|
||||
if (mask) {
|
||||
copy.mask = mask.map((locator) => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector
|
||||
}));
|
||||
}
|
||||
const result = await this._elementChannel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
|
||||
await this._platform.fs().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, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_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, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async waitForElementState(state, options = {}) {
|
||||
return await this._elementChannel.waitForElementState({ state, ...options, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async waitForSelector(selector, options = {}) {
|
||||
const result = await this._elementChannel.waitForSelector({ selector, ...options, timeout: this._frame._timeout(options) });
|
||||
return ElementHandle.fromNullable(result.element);
|
||||
}
|
||||
}
|
||||
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, import_assert.assert)(values[i] !== null, `options[${i}]: expected object, got null`);
|
||||
if (values[0] instanceof ElementHandle)
|
||||
return { elements: values.map((v) => v._elementChannel) };
|
||||
if ((0, import_rtti.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) >= import_fileUtils.fileUploadSizeLimit;
|
||||
}
|
||||
async function resolvePathsAndDirectoryForInputFiles(platform, items) {
|
||||
let localPaths;
|
||||
let localDirectory;
|
||||
for (const item of items) {
|
||||
const stat = await platform.fs().promises.stat(item);
|
||||
if (stat.isDirectory()) {
|
||||
if (localDirectory)
|
||||
throw new Error("Multiple directories are not supported");
|
||||
localDirectory = platform.path().resolve(item);
|
||||
} else {
|
||||
localPaths ??= [];
|
||||
localPaths.push(platform.path().resolve(item));
|
||||
}
|
||||
}
|
||||
if (localPaths?.length && localDirectory)
|
||||
throw new Error("File paths must be all files or a single directory");
|
||||
return [localPaths, localDirectory];
|
||||
}
|
||||
async function convertInputFiles(platform, 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(platform, items);
|
||||
if (context._connection.isRemote()) {
|
||||
const files2 = localDirectory ? (await platform.fs().promises.readdir(localDirectory, { withFileTypes: true, recursive: true })).filter((f) => f.isFile()).map((f) => platform.path().join(f.parentPath, f.name)) : localPaths;
|
||||
const { writableStreams, rootDir } = await context._wrapApiCall(async () => context._channel.createTempFiles({
|
||||
rootDirName: localDirectory ? platform.path().basename(localDirectory) : void 0,
|
||||
items: await Promise.all(files2.map(async (file) => {
|
||||
const lastModifiedMs = (await platform.fs().promises.stat(file)).mtimeMs;
|
||||
return {
|
||||
name: localDirectory ? platform.path().relative(localDirectory, file) : platform.path().basename(file),
|
||||
lastModifiedMs
|
||||
};
|
||||
}))
|
||||
}), { internal: true });
|
||||
for (let i = 0; i < files2.length; i++) {
|
||||
const writable = import_writableStream.WritableStream.from(writableStreams[i]);
|
||||
await platform.streamFile(files2[i], writable.stream());
|
||||
}
|
||||
return {
|
||||
directoryStream: rootDir,
|
||||
streams: localDirectory ? void 0 : 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 = (0, import_mimeType.getMimeTypeForPath)(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;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
ElementHandle,
|
||||
convertInputFiles,
|
||||
convertSelectOptionValues,
|
||||
determineScreenshotType
|
||||
});
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var errors_exports = {};
|
||||
__export(errors_exports, {
|
||||
TargetClosedError: () => TargetClosedError,
|
||||
TimeoutError: () => TimeoutError,
|
||||
isTargetClosedError: () => isTargetClosedError,
|
||||
parseError: () => parseError,
|
||||
serializeError: () => serializeError
|
||||
});
|
||||
module.exports = __toCommonJS(errors_exports);
|
||||
var import_serializers = require("../protocol/serializers");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
class TimeoutError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "TimeoutError";
|
||||
}
|
||||
}
|
||||
class TargetClosedError extends Error {
|
||||
constructor(cause) {
|
||||
super(cause || "Target page, context or browser has been closed");
|
||||
}
|
||||
}
|
||||
function isTargetClosedError(error) {
|
||||
return error instanceof TargetClosedError;
|
||||
}
|
||||
function serializeError(e) {
|
||||
if ((0, import_rtti.isError)(e))
|
||||
return { error: { message: e.message, stack: e.stack, name: e.name } };
|
||||
return { value: (0, import_serializers.serializeValue)(e, (value) => ({ fallThrough: value })) };
|
||||
}
|
||||
function parseError(error) {
|
||||
if (!error.error) {
|
||||
if (error.value === void 0)
|
||||
throw new Error("Serialized error must have either an error or a value");
|
||||
return (0, import_serializers.parseSerializedValue)(error.value, void 0);
|
||||
}
|
||||
if (error.error.name === "TimeoutError") {
|
||||
const e2 = new TimeoutError(error.error.message);
|
||||
e2.stack = error.error.stack || "";
|
||||
return e2;
|
||||
}
|
||||
if (error.error.name === "TargetClosedError") {
|
||||
const e2 = new TargetClosedError(error.error.message);
|
||||
e2.stack = error.error.stack || "";
|
||||
return e2;
|
||||
}
|
||||
const e = new Error(error.error.message);
|
||||
e.stack = error.error.stack || "";
|
||||
e.name = error.error.name;
|
||||
return e;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
TargetClosedError,
|
||||
TimeoutError,
|
||||
isTargetClosedError,
|
||||
parseError,
|
||||
serializeError
|
||||
});
|
||||
Generated
Vendored
+314
@@ -0,0 +1,314 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var eventEmitter_exports = {};
|
||||
__export(eventEmitter_exports, {
|
||||
EventEmitter: () => EventEmitter
|
||||
});
|
||||
module.exports = __toCommonJS(eventEmitter_exports);
|
||||
class EventEmitter {
|
||||
constructor(platform) {
|
||||
this._events = void 0;
|
||||
this._eventsCount = 0;
|
||||
this._maxListeners = void 0;
|
||||
this._pendingHandlers = /* @__PURE__ */ new Map();
|
||||
this._platform = platform;
|
||||
if (this._events === void 0 || this._events === Object.getPrototypeOf(this)._events) {
|
||||
this._events = /* @__PURE__ */ Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
}
|
||||
this._maxListeners = this._maxListeners || void 0;
|
||||
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 === void 0 ? this._platform.defaultMaxListeners() : this._maxListeners;
|
||||
}
|
||||
emit(type, ...args) {
|
||||
const events = this._events;
|
||||
if (events === void 0)
|
||||
return false;
|
||||
const handler = events?.[type];
|
||||
if (handler === void 0)
|
||||
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 = /* @__PURE__ */ 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 === void 0) {
|
||||
events = this._events = /* @__PURE__ */ Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
} else {
|
||||
if (events.newListener !== void 0) {
|
||||
this.emit("newListener", type, unwrapListener(listener));
|
||||
events = this._events;
|
||||
}
|
||||
existing = events[type];
|
||||
}
|
||||
if (existing === void 0) {
|
||||
existing = events[type] = listener;
|
||||
++this._eventsCount;
|
||||
} else {
|
||||
if (typeof existing === "function") {
|
||||
existing = events[type] = prepend ? [listener, existing] : [existing, listener];
|
||||
} else if (prepend) {
|
||||
existing.unshift(listener);
|
||||
} else {
|
||||
existing.push(listener);
|
||||
}
|
||||
const m = this.getMaxListeners();
|
||||
if (m > 0 && existing.length > m && !existing.warned) {
|
||||
existing.warned = true;
|
||||
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 (!this._platform.isUnderTest()) {
|
||||
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 === void 0)
|
||||
return this;
|
||||
const list = events[type];
|
||||
if (list === void 0)
|
||||
return this;
|
||||
if (list === listener || list.listener === listener) {
|
||||
if (--this._eventsCount === 0) {
|
||||
this._events = /* @__PURE__ */ Object.create(null);
|
||||
} else {
|
||||
delete events[type];
|
||||
if (events.removeListener)
|
||||
this.emit("removeListener", type, list.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 !== void 0)
|
||||
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);
|
||||
return this._waitFor(type).then(() => {
|
||||
if (errors.length)
|
||||
throw errors[0];
|
||||
});
|
||||
}
|
||||
if (options.behavior === "ignoreErrors")
|
||||
this._rejectionHandler = () => {
|
||||
};
|
||||
return Promise.resolve();
|
||||
}
|
||||
_removeAllListeners(type) {
|
||||
const events = this._events;
|
||||
if (!events)
|
||||
return;
|
||||
if (!events.removeListener) {
|
||||
if (type === void 0) {
|
||||
this._events = /* @__PURE__ */ Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
} else if (events[type] !== void 0) {
|
||||
if (--this._eventsCount === 0)
|
||||
this._events = /* @__PURE__ */ Object.create(null);
|
||||
else
|
||||
delete events[type];
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === void 0) {
|
||||
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 = /* @__PURE__ */ Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
return;
|
||||
}
|
||||
const listeners = events[type];
|
||||
if (typeof listeners === "function") {
|
||||
this.removeListener(type, listeners);
|
||||
} else if (listeners !== void 0) {
|
||||
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 !== void 0) {
|
||||
const listener = events[type];
|
||||
if (typeof listener === "function")
|
||||
return 1;
|
||||
if (listener !== void 0)
|
||||
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 === void 0)
|
||||
return [];
|
||||
const listener = events[type];
|
||||
if (listener === void 0)
|
||||
return [];
|
||||
if (typeof listener === "function")
|
||||
return unwrap ? [unwrapListener(listener)] : [listener];
|
||||
return unwrap ? unwrapListeners(listener) : listener.slice();
|
||||
}
|
||||
}
|
||||
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._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) {
|
||||
return wrappedListener(l) ?? l;
|
||||
}
|
||||
function unwrapListeners(arr) {
|
||||
return arr.map((l) => wrappedListener(l) ?? l);
|
||||
}
|
||||
function wrappedListener(l) {
|
||||
return l.listener;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
EventEmitter
|
||||
});
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var events_exports = {};
|
||||
__export(events_exports, {
|
||||
Events: () => Events
|
||||
});
|
||||
module.exports = __toCommonJS(events_exports);
|
||||
const Events = {
|
||||
AndroidDevice: {
|
||||
WebView: "webview",
|
||||
Close: "close"
|
||||
},
|
||||
AndroidSocket: {
|
||||
Data: "data",
|
||||
Close: "close"
|
||||
},
|
||||
AndroidWebView: {
|
||||
Close: "close"
|
||||
},
|
||||
Browser: {
|
||||
Disconnected: "disconnected"
|
||||
},
|
||||
Debugger: {
|
||||
PausedStateChanged: "pausedstatechanged"
|
||||
},
|
||||
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",
|
||||
// Deprecated in v1.56, never emitted anymore.
|
||||
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",
|
||||
Console: "console"
|
||||
},
|
||||
ElectronApplication: {
|
||||
Close: "close",
|
||||
Console: "console",
|
||||
Window: "window"
|
||||
}
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Events
|
||||
});
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var fetch_exports = {};
|
||||
__export(fetch_exports, {
|
||||
APIRequest: () => APIRequest,
|
||||
APIRequestContext: () => APIRequestContext,
|
||||
APIResponse: () => APIResponse
|
||||
});
|
||||
module.exports = __toCommonJS(fetch_exports);
|
||||
var import_browserContext = require("./browserContext");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_errors = require("./errors");
|
||||
var import_network = require("./network");
|
||||
var import_tracing = require("./tracing");
|
||||
var import_assert = require("../utils/isomorphic/assert");
|
||||
var import_fileUtils = require("./fileUtils");
|
||||
var import_headers = require("../utils/isomorphic/headers");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
class APIRequest {
|
||||
constructor(playwright) {
|
||||
this._contexts = /* @__PURE__ */ new Set();
|
||||
this._playwright = playwright;
|
||||
}
|
||||
async newContext(options = {}) {
|
||||
options = { ...options };
|
||||
await this._playwright._instrumentation.runBeforeCreateRequestContext(options);
|
||||
const storageState = typeof options.storageState === "string" ? JSON.parse(await this._playwright._platform.fs().promises.readFile(options.storageState, "utf8")) : options.storageState;
|
||||
const context = APIRequestContext.from((await this._playwright._channel.newRequest({
|
||||
...options,
|
||||
extraHTTPHeaders: options.extraHTTPHeaders ? (0, import_headers.headersObjectToArray)(options.extraHTTPHeaders) : void 0,
|
||||
storageState,
|
||||
tracesDir: this._playwright._defaultLaunchOptions?.tracesDir,
|
||||
// We do not expose tracesDir in the API, so do not allow options to accidentally override it.
|
||||
clientCertificates: await (0, import_browserContext.toClientCertificatesProtocol)(this._playwright._platform, options.clientCertificates)
|
||||
})).request);
|
||||
this._contexts.add(context);
|
||||
context._request = this;
|
||||
context._timeoutSettings.setDefaultTimeout(options.timeout ?? this._playwright._defaultContextTimeout);
|
||||
context._tracing._tracesDir = this._playwright._defaultLaunchOptions?.tracesDir;
|
||||
await context._instrumentation.runAfterCreateRequestContext(context);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
class APIRequestContext extends import_channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._tracing = import_tracing.Tracing.from(initializer.tracing);
|
||||
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose(options = {}) {
|
||||
this._closeReason = options.reason;
|
||||
await this._instrumentation.runBeforeCloseRequestContext(this);
|
||||
try {
|
||||
await this._channel.dispose(options);
|
||||
} catch (e) {
|
||||
if ((0, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
this._tracing._resetStackCounter();
|
||||
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, import_rtti.isString)(urlOrRequest) ? urlOrRequest : void 0;
|
||||
const request = (0, import_rtti.isString)(urlOrRequest) ? void 0 : urlOrRequest;
|
||||
return await this._innerFetch({ url, request, ...options });
|
||||
}
|
||||
async _innerFetch(options = {}) {
|
||||
return await this._wrapApiCall(async () => {
|
||||
if (this._closeReason)
|
||||
throw new import_errors.TargetClosedError(this._closeReason);
|
||||
(0, import_assert.assert)(options.request || typeof options.url === "string", "First argument must be either URL string or Request");
|
||||
(0, import_assert.assert)((options.data === void 0 ? 0 : 1) + (options.form === void 0 ? 0 : 1) + (options.multipart === void 0 ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||
(0, import_assert.assert)(options.maxRedirects === void 0 || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
|
||||
(0, import_assert.assert)(options.maxRetries === void 0 || options.maxRetries >= 0, `'maxRetries' must be greater than or equal to '0'`);
|
||||
const url = options.url !== void 0 ? options.url : options.request.url();
|
||||
const method = options.method || options.request?.method();
|
||||
let encodedParams = void 0;
|
||||
if (typeof options.params === "string")
|
||||
encodedParams = options.params;
|
||||
else if (options.params instanceof URLSearchParams)
|
||||
encodedParams = options.params.toString();
|
||||
const headersObj = options.headers || options.request?.headers();
|
||||
const headers = headersObj ? (0, import_headers.headersObjectToArray)(headersObj) : void 0;
|
||||
let jsonData;
|
||||
let formData;
|
||||
let multipartData;
|
||||
let postDataBuffer;
|
||||
if (options.data !== void 0) {
|
||||
if ((0, import_rtti.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, import_rtti.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 {
|
||||
for (const [name, value] of Object.entries(options.multipart))
|
||||
multipartData.push(await toFormField(this._platform, name, value));
|
||||
}
|
||||
}
|
||||
if (postDataBuffer === void 0 && jsonData === void 0 && formData === void 0 && multipartData === void 0)
|
||||
postDataBuffer = options.request?.postDataBuffer() || void 0;
|
||||
const fixtures = {
|
||||
__testHookLookup: options.__testHookLookup
|
||||
};
|
||||
const result = await this._channel.fetch({
|
||||
url,
|
||||
params: typeof options.params === "object" ? objectToArray(options.params) : void 0,
|
||||
encodedParams,
|
||||
method,
|
||||
headers,
|
||||
postData: postDataBuffer,
|
||||
jsonData,
|
||||
formData,
|
||||
multipartData,
|
||||
timeout: this._timeoutSettings.timeout(options),
|
||||
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({ indexedDB: options.indexedDB });
|
||||
if (options.path) {
|
||||
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, JSON.stringify(state, void 0, 2), "utf8");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
}
|
||||
async function toFormField(platform, name, value) {
|
||||
const typeOfValue = typeof 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 (typeOfValue === "string" || typeOfValue === "number" || typeOfValue === "boolean") {
|
||||
return { name, value: String(value) };
|
||||
} else {
|
||||
return { name, file: await readStreamToJson(platform, 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;
|
||||
}
|
||||
}
|
||||
class APIResponse {
|
||||
constructor(context, initializer) {
|
||||
this._request = context;
|
||||
this._initializer = initializer;
|
||||
this._headers = new import_network.RawHeaders(this._initializer.headers);
|
||||
if (context._platform.inspectCustom)
|
||||
this[context._platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
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() {
|
||||
return await this._request._wrapApiCall(async () => {
|
||||
try {
|
||||
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
|
||||
if (result.binary === void 0)
|
||||
throw new Error("Response has been disposed");
|
||||
return result.binary;
|
||||
} catch (e) {
|
||||
if ((0, import_errors.isTargetClosedError)(e))
|
||||
throw new Error("Response has been disposed");
|
||||
throw e;
|
||||
}
|
||||
}, { internal: true });
|
||||
}
|
||||
async text() {
|
||||
const content = await this.body();
|
||||
return content.toString("utf8");
|
||||
}
|
||||
async json() {
|
||||
const content = await this.text();
|
||||
return JSON.parse(content);
|
||||
}
|
||||
async [Symbol.asyncDispose]() {
|
||||
await this.dispose();
|
||||
}
|
||||
async dispose() {
|
||||
await this._request._channel.disposeAPIResponse({ fetchUid: this._fetchUid() });
|
||||
}
|
||||
_inspect() {
|
||||
const headers = this.headersArray().map(({ name, value }) => ` ${name}: ${value}`);
|
||||
return `APIResponse: ${this.status()} ${this.statusText()}
|
||||
${headers.join("\n")}`;
|
||||
}
|
||||
_fetchUid() {
|
||||
return this._initializer.fetchUid;
|
||||
}
|
||||
async _fetchLog() {
|
||||
const { log } = await this._request._channel.fetchLog({ fetchUid: this._fetchUid() });
|
||||
return log;
|
||||
}
|
||||
}
|
||||
function filePayloadToJson(payload) {
|
||||
return {
|
||||
name: payload.name,
|
||||
mimeType: payload.mimeType,
|
||||
buffer: payload.buffer
|
||||
};
|
||||
}
|
||||
async function readStreamToJson(platform, 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: platform.path().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 void 0;
|
||||
const result = [];
|
||||
for (const [name, value] of Object.entries(map)) {
|
||||
if (value !== void 0)
|
||||
result.push({ name, value: String(value) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function isFilePayload(value) {
|
||||
return typeof value === "object" && value["name"] && value["mimeType"] && value["buffer"];
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
APIRequest,
|
||||
APIRequestContext,
|
||||
APIResponse
|
||||
});
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var fileChooser_exports = {};
|
||||
__export(fileChooser_exports, {
|
||||
FileChooser: () => FileChooser
|
||||
});
|
||||
module.exports = __toCommonJS(fileChooser_exports);
|
||||
class FileChooser {
|
||||
constructor(page, elementHandle, isMultiple) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
FileChooser
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var fileUtils_exports = {};
|
||||
__export(fileUtils_exports, {
|
||||
fileUploadSizeLimit: () => fileUploadSizeLimit,
|
||||
mkdirIfNeeded: () => mkdirIfNeeded
|
||||
});
|
||||
module.exports = __toCommonJS(fileUtils_exports);
|
||||
const fileUploadSizeLimit = 50 * 1024 * 1024;
|
||||
async function mkdirIfNeeded(platform, filePath) {
|
||||
await platform.fs().promises.mkdir(platform.path().dirname(filePath), { recursive: true }).catch(() => {
|
||||
});
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
fileUploadSizeLimit,
|
||||
mkdirIfNeeded
|
||||
});
|
||||
+404
@@ -0,0 +1,404 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var frame_exports = {};
|
||||
__export(frame_exports, {
|
||||
Frame: () => Frame,
|
||||
verifyLoadState: () => verifyLoadState
|
||||
});
|
||||
module.exports = __toCommonJS(frame_exports);
|
||||
var import_eventEmitter = require("./eventEmitter");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_clientHelper = require("./clientHelper");
|
||||
var import_elementHandle = require("./elementHandle");
|
||||
var import_events = require("./events");
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
var import_locator = require("./locator");
|
||||
var network = __toESM(require("./network"));
|
||||
var import_types = require("./types");
|
||||
var import_waiter = require("./waiter");
|
||||
var import_assert = require("../utils/isomorphic/assert");
|
||||
var import_locatorUtils = require("../utils/isomorphic/locatorUtils");
|
||||
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
class Frame extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._parentFrame = null;
|
||||
this._url = "";
|
||||
this._name = "";
|
||||
this._detached = false;
|
||||
this._childFrames = /* @__PURE__ */ new Set();
|
||||
this._eventEmitter = new import_eventEmitter.EventEmitter(parent._platform);
|
||||
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(import_events.Events.Page.Load, this._page);
|
||||
if (!this._parentFrame && event.add === "domcontentloaded" && this._page)
|
||||
this._page.emit(import_events.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(import_events.Events.Page.FrameNavigated, this);
|
||||
});
|
||||
}
|
||||
static from(frame) {
|
||||
return frame._object;
|
||||
}
|
||||
static fromNullable(frame) {
|
||||
return frame ? Frame.from(frame) : null;
|
||||
}
|
||||
page() {
|
||||
return this._page;
|
||||
}
|
||||
_timeout(options) {
|
||||
const timeoutSettings = this._page?._timeoutSettings || new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||
return timeoutSettings.timeout(options || {});
|
||||
}
|
||||
_navigationTimeout(options) {
|
||||
const timeoutSettings = this._page?._timeoutSettings || new import_timeoutSettings.TimeoutSettings(this._platform);
|
||||
return timeoutSettings.navigationTimeout(options || {});
|
||||
}
|
||||
async goto(url, options = {}) {
|
||||
const waitUntil = verifyLoadState("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||
return network.Response.fromNullable((await this._channel.goto({ url, ...options, waitUntil, timeout: this._navigationTimeout(options) })).response);
|
||||
}
|
||||
_setupNavigationWaiter(options) {
|
||||
const waiter = new import_waiter.Waiter(this._page, "");
|
||||
if (this._page.isClosed())
|
||||
waiter.rejectImmediately(this._page._closeErrorWithReason());
|
||||
waiter.rejectOnEvent(this._page, import_events.Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||
waiter.rejectOnEvent(this._page, import_events.Events.Page.Crash, new Error("Navigation failed because page crashed!"));
|
||||
waiter.rejectOnEvent(this._page, import_events.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 === void 0 ? "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) => {
|
||||
if (event.error)
|
||||
return true;
|
||||
waiter.log(` navigated to "${event.url}"`);
|
||||
return (0, import_urlMatch.urlMatches)(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;
|
||||
}, { title: "Wait for navigation" });
|
||||
}
|
||||
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();
|
||||
}, { title: `Wait for load state "${state}"` });
|
||||
}
|
||||
async waitForURL(url, options = {}) {
|
||||
if ((0, import_urlMatch.urlMatches)(this._page?.context()._options.baseURL, this.url(), url))
|
||||
return await this.waitForLoadState(options.waitUntil, options);
|
||||
await this.waitForNavigation({ url, ...options });
|
||||
}
|
||||
async frameElement() {
|
||||
return import_elementHandle.ElementHandle.from((await this._channel.frameElement()).element);
|
||||
}
|
||||
async evaluateHandle(pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpressionHandle({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return import_jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async _evaluateExposeUtilityScript(pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
const result = await this._channel.evaluateExpression({ expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $(selector, options) {
|
||||
const result = await this._channel.querySelector({ selector, ...options });
|
||||
return import_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, timeout: this._timeout(options) });
|
||||
return import_elementHandle.ElementHandle.fromNullable(result.element);
|
||||
}
|
||||
async dispatchEvent(selector, type, eventInit, options = {}) {
|
||||
await this._channel.dispatchEvent({ selector, type, eventInit: (0, import_jsHandle.serializeArgument)(eventInit), ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async $eval(selector, pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
const result = await this._channel.evalOnSelector({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $$eval(selector, pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
const result = await this._channel.evalOnSelectorAll({ selector, expression: String(pageFunction), isFunction: typeof pageFunction === "function", arg: (0, import_jsHandle.serializeArgument)(arg) });
|
||||
return (0, import_jsHandle.parseResult)(result.value);
|
||||
}
|
||||
async $$(selector) {
|
||||
const result = await this._channel.querySelectorAll({ selector });
|
||||
return result.elements.map((e) => import_elementHandle.ElementHandle.from(e));
|
||||
}
|
||||
async _queryCount(selector, options) {
|
||||
return (await this._channel.queryCount({ selector, ...options })).value;
|
||||
}
|
||||
async content() {
|
||||
return (await this._channel.content()).value;
|
||||
}
|
||||
async setContent(html, options = {}) {
|
||||
const waitUntil = verifyLoadState("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||
await this._channel.setContent({ html, ...options, waitUntil, timeout: this._navigationTimeout(options) });
|
||||
}
|
||||
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 this._platform.fs().promises.readFile(copy.path)).toString();
|
||||
copy.content = (0, import_clientHelper.addSourceUrlToScript)(copy.content, copy.path);
|
||||
}
|
||||
return import_elementHandle.ElementHandle.from((await this._channel.addScriptTag({ ...copy })).element);
|
||||
}
|
||||
async addStyleTag(options = {}) {
|
||||
const copy = { ...options };
|
||||
if (copy.path) {
|
||||
copy.content = (await this._platform.fs().promises.readFile(copy.path)).toString();
|
||||
copy.content += "/*# sourceURL=" + copy.path.replace(/\n/g, "") + "*/";
|
||||
}
|
||||
return import_elementHandle.ElementHandle.from((await this._channel.addStyleTag({ ...copy })).element);
|
||||
}
|
||||
async click(selector, options = {}) {
|
||||
return await this._channel.click({ selector, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async dblclick(selector, options = {}) {
|
||||
return await this._channel.dblclick({ selector, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async dragAndDrop(source, target, options = {}) {
|
||||
return await this._channel.dragAndDrop({ source, target, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async tap(selector, options = {}) {
|
||||
return await this._channel.tap({ selector, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async fill(selector, value, options = {}) {
|
||||
return await this._channel.fill({ selector, value, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async _highlight(selector) {
|
||||
return await this._channel.highlight({ selector });
|
||||
}
|
||||
locator(selector, options) {
|
||||
return new import_locator.Locator(this, selector, options);
|
||||
}
|
||||
getByTestId(testId) {
|
||||
return this.locator((0, import_locatorUtils.getByTestIdSelector)((0, import_locator.testIdAttributeName)(), testId));
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByAltTextSelector)(text, options));
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByLabelSelector)(text, options));
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByPlaceholderSelector)(text, options));
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByTextSelector)(text, options));
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByTitleSelector)(text, options));
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.locator((0, import_locatorUtils.getByRoleSelector)(role, options));
|
||||
}
|
||||
frameLocator(selector) {
|
||||
return new import_locator.FrameLocator(this, selector);
|
||||
}
|
||||
async focus(selector, options = {}) {
|
||||
await this._channel.focus({ selector, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async textContent(selector, options = {}) {
|
||||
const value = (await this._channel.textContent({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
return value === void 0 ? null : value;
|
||||
}
|
||||
async innerText(selector, options = {}) {
|
||||
return (await this._channel.innerText({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
}
|
||||
async innerHTML(selector, options = {}) {
|
||||
return (await this._channel.innerHTML({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
}
|
||||
async getAttribute(selector, name, options = {}) {
|
||||
const value = (await this._channel.getAttribute({ selector, name, ...options, timeout: this._timeout(options) })).value;
|
||||
return value === void 0 ? null : value;
|
||||
}
|
||||
async inputValue(selector, options = {}) {
|
||||
return (await this._channel.inputValue({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
}
|
||||
async isChecked(selector, options = {}) {
|
||||
return (await this._channel.isChecked({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
}
|
||||
async isDisabled(selector, options = {}) {
|
||||
return (await this._channel.isDisabled({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
}
|
||||
async isEditable(selector, options = {}) {
|
||||
return (await this._channel.isEditable({ selector, ...options, timeout: this._timeout(options) })).value;
|
||||
}
|
||||
async isEnabled(selector, options = {}) {
|
||||
return (await this._channel.isEnabled({ selector, ...options, timeout: this._timeout(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, timeout: this._timeout(options) });
|
||||
}
|
||||
async selectOption(selector, values, options = {}) {
|
||||
return (await this._channel.selectOption({ selector, ...(0, import_elementHandle.convertSelectOptionValues)(values), ...options, timeout: this._timeout(options) })).values;
|
||||
}
|
||||
async setInputFiles(selector, files, options = {}) {
|
||||
const converted = await (0, import_elementHandle.convertInputFiles)(this._platform, files, this.page().context());
|
||||
await this._channel.setInputFiles({ selector, ...converted, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async type(selector, text, options = {}) {
|
||||
await this._channel.type({ selector, text, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async press(selector, key, options = {}) {
|
||||
await this._channel.press({ selector, key, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async check(selector, options = {}) {
|
||||
await this._channel.check({ selector, ...options, timeout: this._timeout(options) });
|
||||
}
|
||||
async uncheck(selector, options = {}) {
|
||||
await this._channel.uncheck({ selector, ...options, timeout: this._timeout(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({ waitTimeout: timeout });
|
||||
}
|
||||
async waitForFunction(pageFunction, arg, options = {}) {
|
||||
if (typeof options.polling === "string")
|
||||
(0, import_assert.assert)(options.polling === "raf", "Unknown polling option: " + options.polling);
|
||||
const result = await this._channel.waitForFunction({
|
||||
...options,
|
||||
pollingInterval: options.polling === "raf" ? void 0 : options.polling,
|
||||
expression: String(pageFunction),
|
||||
isFunction: typeof pageFunction === "function",
|
||||
arg: (0, import_jsHandle.serializeArgument)(arg),
|
||||
timeout: this._timeout(options)
|
||||
});
|
||||
return import_jsHandle.JSHandle.from(result.handle);
|
||||
}
|
||||
async title() {
|
||||
return (await this._channel.title()).value;
|
||||
}
|
||||
async _expect(expression, options) {
|
||||
const params = { expression, ...options, isNot: !!options.isNot };
|
||||
params.expectedValue = (0, import_jsHandle.serializeArgument)(options.expectedValue);
|
||||
const result = await this._channel.expect(params);
|
||||
if (result.received !== void 0)
|
||||
result.received = (0, import_jsHandle.parseResult)(result.received);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
function verifyLoadState(name, waitUntil) {
|
||||
if (waitUntil === "networkidle0")
|
||||
waitUntil = "networkidle";
|
||||
if (!import_types.kLifecycleEvents.has(waitUntil))
|
||||
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
|
||||
return waitUntil;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Frame,
|
||||
verifyLoadState
|
||||
});
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var harRouter_exports = {};
|
||||
__export(harRouter_exports, {
|
||||
HarRouter: () => HarRouter
|
||||
});
|
||||
module.exports = __toCommonJS(harRouter_exports);
|
||||
class HarRouter {
|
||||
static async create(localUtils, file, notFoundAction, options) {
|
||||
const { harId, error } = await localUtils.harOpen({ file });
|
||||
if (error)
|
||||
throw new Error(error);
|
||||
return new HarRouter(localUtils, harId, notFoundAction, options);
|
||||
}
|
||||
constructor(localUtils, harId, notFoundAction, options) {
|
||||
this._localUtils = localUtils;
|
||||
this._harId = harId;
|
||||
this._options = options;
|
||||
this._notFoundAction = notFoundAction;
|
||||
}
|
||||
async _handle(route) {
|
||||
const request = route.request();
|
||||
const response = await this._localUtils.harLookup({
|
||||
harId: this._harId,
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
headers: await request.headersArray(),
|
||||
postData: request.postDataBuffer() || void 0,
|
||||
isNavigationRequest: request.isNavigationRequest()
|
||||
});
|
||||
if (response.action === "redirect") {
|
||||
route._platform.log("api", `HAR: ${route.request().url()} redirected to ${response.redirectURL}`);
|
||||
await route._redirectNavigationRequest(response.redirectURL);
|
||||
return;
|
||||
}
|
||||
if (response.action === "fulfill") {
|
||||
if (response.status === -1)
|
||||
return;
|
||||
const transformedHeaders = response.headers.reduce((headersMap, { name, value }) => {
|
||||
if (name.toLowerCase() !== "set-cookie") {
|
||||
headersMap[name] = value;
|
||||
} else {
|
||||
if (!headersMap["set-cookie"])
|
||||
headersMap["set-cookie"] = value;
|
||||
else
|
||||
headersMap["set-cookie"] += `
|
||||
${value}`;
|
||||
}
|
||||
return headersMap;
|
||||
}, {});
|
||||
await route.fulfill({
|
||||
status: response.status,
|
||||
headers: transformedHeaders,
|
||||
body: response.body
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (response.action === "error")
|
||||
route._platform.log("api", "HAR: " + response.message);
|
||||
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.harClose({ harId: this._harId }).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
HarRouter
|
||||
});
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var input_exports = {};
|
||||
__export(input_exports, {
|
||||
Keyboard: () => Keyboard,
|
||||
Mouse: () => Mouse,
|
||||
Touchscreen: () => Touchscreen
|
||||
});
|
||||
module.exports = __toCommonJS(input_exports);
|
||||
class Keyboard {
|
||||
constructor(page) {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
class Mouse {
|
||||
constructor(page) {
|
||||
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._page._wrapApiCall(async () => {
|
||||
await this.click(x, y, { ...options, clickCount: 2 });
|
||||
}, { title: "Double click" });
|
||||
}
|
||||
async wheel(deltaX, deltaY) {
|
||||
await this._page._channel.mouseWheel({ deltaX, deltaY });
|
||||
}
|
||||
}
|
||||
class Touchscreen {
|
||||
constructor(page) {
|
||||
this._page = page;
|
||||
}
|
||||
async tap(x, y) {
|
||||
await this._page._channel.touchscreenTap({ x, y });
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Keyboard,
|
||||
Mouse,
|
||||
Touchscreen
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var jsHandle_exports = {};
|
||||
__export(jsHandle_exports, {
|
||||
JSHandle: () => JSHandle,
|
||||
assertMaxArguments: () => assertMaxArguments,
|
||||
parseResult: () => parseResult,
|
||||
serializeArgument: () => serializeArgument
|
||||
});
|
||||
module.exports = __toCommonJS(jsHandle_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_errors = require("./errors");
|
||||
var import_serializers = require("../protocol/serializers");
|
||||
class JSHandle extends import_channelOwner.ChannelOwner {
|
||||
static from(handle) {
|
||||
return handle._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
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 = /* @__PURE__ */ 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, import_errors.isTargetClosedError)(e))
|
||||
return;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
toString() {
|
||||
return this._preview;
|
||||
}
|
||||
}
|
||||
function serializeArgument(arg) {
|
||||
const handles = [];
|
||||
const pushHandle = (channel) => {
|
||||
handles.push(channel);
|
||||
return handles.length - 1;
|
||||
};
|
||||
const value = (0, import_serializers.serializeValue)(arg, (value2) => {
|
||||
if (value2 instanceof JSHandle)
|
||||
return { h: pushHandle(value2._channel) };
|
||||
return { fallThrough: value2 };
|
||||
});
|
||||
return { value, handles };
|
||||
}
|
||||
function parseResult(value) {
|
||||
return (0, import_serializers.parseSerializedValue)(value, void 0);
|
||||
}
|
||||
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.");
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
JSHandle,
|
||||
assertMaxArguments,
|
||||
parseResult,
|
||||
serializeArgument
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var jsonPipe_exports = {};
|
||||
__export(jsonPipe_exports, {
|
||||
JsonPipe: () => JsonPipe
|
||||
});
|
||||
module.exports = __toCommonJS(jsonPipe_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
class JsonPipe extends import_channelOwner.ChannelOwner {
|
||||
static from(jsonPipe) {
|
||||
return jsonPipe._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
channel() {
|
||||
return this._channel;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
JsonPipe
|
||||
});
|
||||
Generated
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var localUtils_exports = {};
|
||||
__export(localUtils_exports, {
|
||||
LocalUtils: () => LocalUtils
|
||||
});
|
||||
module.exports = __toCommonJS(localUtils_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
class LocalUtils extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.devices = {};
|
||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
this.devices[name] = descriptor;
|
||||
}
|
||||
async zip(params) {
|
||||
return await this._channel.zip(params);
|
||||
}
|
||||
async harOpen(params) {
|
||||
return await this._channel.harOpen(params);
|
||||
}
|
||||
async harLookup(params) {
|
||||
return await this._channel.harLookup(params);
|
||||
}
|
||||
async harClose(params) {
|
||||
return await this._channel.harClose(params);
|
||||
}
|
||||
async harUnzip(params) {
|
||||
return await this._channel.harUnzip(params);
|
||||
}
|
||||
async tracingStarted(params) {
|
||||
return await this._channel.tracingStarted(params);
|
||||
}
|
||||
async traceDiscarded(params) {
|
||||
return await this._channel.traceDiscarded(params);
|
||||
}
|
||||
async addStackToTracingNoReply(params) {
|
||||
return await this._channel.addStackToTracingNoReply(params);
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
LocalUtils
|
||||
});
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var locator_exports = {};
|
||||
__export(locator_exports, {
|
||||
FrameLocator: () => FrameLocator,
|
||||
Locator: () => Locator,
|
||||
setTestIdAttribute: () => setTestIdAttribute,
|
||||
testIdAttributeName: () => testIdAttributeName
|
||||
});
|
||||
module.exports = __toCommonJS(locator_exports);
|
||||
var import_elementHandle = require("./elementHandle");
|
||||
var import_locatorGenerators = require("../utils/isomorphic/locatorGenerators");
|
||||
var import_locatorUtils = require("../utils/isomorphic/locatorUtils");
|
||||
var import_stringUtils = require("../utils/isomorphic/stringUtils");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_time = require("../utils/isomorphic/time");
|
||||
class Locator {
|
||||
constructor(frame, selector, options) {
|
||||
this._frame = frame;
|
||||
this._selector = selector;
|
||||
if (options?.hasText)
|
||||
this._selector += ` >> internal:has-text=${(0, import_stringUtils.escapeForTextSelector)(options.hasText, false)}`;
|
||||
if (options?.hasNotText)
|
||||
this._selector += ` >> internal:has-not-text=${(0, import_stringUtils.escapeForTextSelector)(options.hasNotText, false)}`;
|
||||
if (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?.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);
|
||||
}
|
||||
if (options?.visible !== void 0)
|
||||
this._selector += ` >> visible=${options.visible ? "true" : "false"}`;
|
||||
if (this._frame._platform.inspectCustom)
|
||||
this[this._frame._platform.inspectCustom] = () => this._inspect();
|
||||
}
|
||||
async _withElement(task, options) {
|
||||
const timeout = this._frame._timeout({ timeout: options.timeout });
|
||||
const deadline = timeout ? (0, import_time.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 = import_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, import_time.monotonicTime)() : 0);
|
||||
} finally {
|
||||
await handle.dispose();
|
||||
}
|
||||
}, { title: options.title, internal: options.internal });
|
||||
}
|
||||
_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(), { title: "Bounding box", timeout: 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 = {}) {
|
||||
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), { title: "Evaluate", timeout: 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), { title: "Evaluate", timeout: options?.timeout });
|
||||
}
|
||||
async fill(value, options = {}) {
|
||||
return await this._frame.fill(this._selector, value, { strict: true, ...options });
|
||||
}
|
||||
async clear(options = {}) {
|
||||
await this._frame._wrapApiCall(() => this.fill("", options), { title: "Clear" });
|
||||
}
|
||||
async _highlight() {
|
||||
return await this._frame._highlight(this._selector);
|
||||
}
|
||||
async highlight() {
|
||||
return await this._frame._highlight(this._selector);
|
||||
}
|
||||
locator(selectorOrLocator, options) {
|
||||
if ((0, import_rtti.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, import_locatorUtils.getByTestIdSelector)(testIdAttributeName(), testId));
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByAltTextSelector)(text, options));
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByLabelSelector)(text, options));
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByPlaceholderSelector)(text, options));
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByTextSelector)(text, options));
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByTitleSelector)(text, options));
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.locator((0, import_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);
|
||||
}
|
||||
describe(description) {
|
||||
return new Locator(this._frame, this._selector + " >> internal:describe=" + JSON.stringify(description));
|
||||
}
|
||||
description() {
|
||||
return (0, import_locatorGenerators.locatorCustomDescription)(this._selector) || null;
|
||||
}
|
||||
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, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
// options are only here for testing
|
||||
async count(_options) {
|
||||
return await this._frame._queryCount(this._selector, _options);
|
||||
}
|
||||
async normalize() {
|
||||
const { resolvedSelector } = await this._frame._channel.resolveSelector({ selector: this._selector });
|
||||
return new Locator(this._frame, resolvedSelector);
|
||||
}
|
||||
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 = {}) {
|
||||
const mask = options.mask;
|
||||
return await this._withElement((h, timeout) => h.screenshot({ ...options, mask, timeout }), { title: "Screenshot", timeout: options.timeout });
|
||||
}
|
||||
async ariaSnapshot(options = {}) {
|
||||
const result = await this._frame._channel.ariaSnapshot({ timeout: this._frame._timeout(options), mode: options.mode, selector: this._selector, depth: options.depth });
|
||||
return result.snapshot;
|
||||
}
|
||||
async scrollIntoViewIfNeeded(options = {}) {
|
||||
return await this._withElement((h, timeout) => h.scrollIntoViewIfNeeded({ ...options, timeout }), { title: "Scroll into view", 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 }), { title: "Select text", 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, timeout: this._frame._timeout(options) });
|
||||
}
|
||||
async _expect(expression, options) {
|
||||
return this._frame._expect(expression, {
|
||||
...options,
|
||||
selector: this._selector
|
||||
});
|
||||
}
|
||||
_inspect() {
|
||||
return this.toString();
|
||||
}
|
||||
toString() {
|
||||
return (0, import_locatorGenerators.asLocatorDescription)("javascript", this._selector);
|
||||
}
|
||||
}
|
||||
class FrameLocator {
|
||||
constructor(frame, selector) {
|
||||
this._frame = frame;
|
||||
this._frameSelector = selector;
|
||||
}
|
||||
locator(selectorOrLocator, options) {
|
||||
if ((0, import_rtti.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, import_locatorUtils.getByTestIdSelector)(testIdAttributeName(), testId));
|
||||
}
|
||||
getByAltText(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByAltTextSelector)(text, options));
|
||||
}
|
||||
getByLabel(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByLabelSelector)(text, options));
|
||||
}
|
||||
getByPlaceholder(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByPlaceholderSelector)(text, options));
|
||||
}
|
||||
getByText(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByTextSelector)(text, options));
|
||||
}
|
||||
getByTitle(text, options) {
|
||||
return this.locator((0, import_locatorUtils.getByTitleSelector)(text, options));
|
||||
}
|
||||
getByRole(role, options = {}) {
|
||||
return this.locator((0, import_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}`);
|
||||
}
|
||||
}
|
||||
let _testIdAttributeName = "data-testid";
|
||||
function testIdAttributeName() {
|
||||
return _testIdAttributeName;
|
||||
}
|
||||
function setTestIdAttribute(attributeName) {
|
||||
_testIdAttributeName = attributeName;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
FrameLocator,
|
||||
Locator,
|
||||
setTestIdAttribute,
|
||||
testIdAttributeName
|
||||
});
|
||||
+750
@@ -0,0 +1,750 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var network_exports = {};
|
||||
__export(network_exports, {
|
||||
RawHeaders: () => RawHeaders,
|
||||
Request: () => Request,
|
||||
Response: () => Response,
|
||||
Route: () => Route,
|
||||
RouteHandler: () => RouteHandler,
|
||||
WebSocket: () => WebSocket,
|
||||
WebSocketRoute: () => WebSocketRoute,
|
||||
WebSocketRouteHandler: () => WebSocketRouteHandler,
|
||||
validateHeaders: () => validateHeaders
|
||||
});
|
||||
module.exports = __toCommonJS(network_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_errors = require("./errors");
|
||||
var import_events = require("./events");
|
||||
var import_fetch = require("./fetch");
|
||||
var import_frame = require("./frame");
|
||||
var import_waiter = require("./waiter");
|
||||
var import_worker = require("./worker");
|
||||
var import_assert = require("../utils/isomorphic/assert");
|
||||
var import_headers = require("../utils/isomorphic/headers");
|
||||
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||
var import_manualPromise = require("../utils/isomorphic/manualPromise");
|
||||
var import_multimap = require("../utils/isomorphic/multimap");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||
var import_mimeType = require("../utils/isomorphic/mimeType");
|
||||
class Request extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._redirectedFrom = null;
|
||||
this._redirectedTo = null;
|
||||
this._failureText = null;
|
||||
this._response = null;
|
||||
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
|
||||
};
|
||||
}
|
||||
static from(request) {
|
||||
return request._object;
|
||||
}
|
||||
static fromNullable(request) {
|
||||
return request ? Request.from(request) : null;
|
||||
}
|
||||
url() {
|
||||
return this._fallbackOverrides.url || this._initializer.url;
|
||||
}
|
||||
resourceType() {
|
||||
return this._initializer.resourceType;
|
||||
}
|
||||
method() {
|
||||
return this._fallbackOverrides.method || this._initializer.method;
|
||||
}
|
||||
postData() {
|
||||
return (this._fallbackOverrides.postDataBuffer || this._initializer.postData)?.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?.includes("application/x-www-form-urlencoded")) {
|
||||
const entries = {};
|
||||
const parsed = new 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);
|
||||
}, { internal: true });
|
||||
}
|
||||
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 Response.fromNullable((await this._channel.response()).response);
|
||||
}
|
||||
existingResponse() {
|
||||
return this._response;
|
||||
}
|
||||
frame() {
|
||||
if (!this._initializer.frame) {
|
||||
(0, import_assert.assert)(this.serviceWorker());
|
||||
throw new Error("Service Worker requests do not have an associated frame.");
|
||||
}
|
||||
const frame = import_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() {
|
||||
return import_frame.Frame.fromNullable(this._initializer.frame)?._page || null;
|
||||
}
|
||||
serviceWorker() {
|
||||
return this._initializer.serviceWorker ? import_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, import_rtti.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() {
|
||||
return this.serviceWorker()?._closedScope || this._safePage()?._closedOrCrashedScope || new import_manualPromise.LongStandingScope();
|
||||
}
|
||||
}
|
||||
class Route extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._handlingPromise = null;
|
||||
this._didThrow = false;
|
||||
}
|
||||
static from(route) {
|
||||
return route._object;
|
||||
}
|
||||
request() {
|
||||
return Request.from(this._initializer.request);
|
||||
}
|
||||
async _raceWithTargetClose(promise) {
|
||||
return await this.request()._targetClosedScope().safeRace(promise);
|
||||
}
|
||||
async _startHandling() {
|
||||
this._handlingPromise = new import_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._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 !== void 0) {
|
||||
(0, import_assert.assert)(options.body === void 0, "Can specify either body or json parameters");
|
||||
body = JSON.stringify(options.json);
|
||||
}
|
||||
if (options.response instanceof import_fetch.APIResponse) {
|
||||
statusOption ??= options.response.status();
|
||||
headersOption ??= options.response.headers();
|
||||
if (body === void 0 && options.path === void 0) {
|
||||
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 this._platform.fs().promises.readFile(options.path);
|
||||
body = buffer.toString("base64");
|
||||
isBase64 = true;
|
||||
length = buffer.length;
|
||||
} else if ((0, import_rtti.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"] = (0, import_mimeType.getMimeTypeForPath)(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, import_headers.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, import_headers.headersObjectToArray)(options.headers) : void 0,
|
||||
postData: options.postDataBuffer,
|
||||
isFallback
|
||||
}));
|
||||
}
|
||||
}
|
||||
class WebSocketRoute extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._connected = false;
|
||||
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, import_rtti.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(() => {
|
||||
});
|
||||
});
|
||||
}
|
||||
static from(route) {
|
||||
return route._object;
|
||||
}
|
||||
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, import_rtti.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;
|
||||
await this._channel.ensureOpened().catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
class WebSocketRouteHandler {
|
||||
constructor(baseURL, url, handler) {
|
||||
this._baseURL = baseURL;
|
||||
this.url = url;
|
||||
this.handler = handler;
|
||||
}
|
||||
static prepareInterceptionPatterns(handlers) {
|
||||
const patterns = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
const serialized = (0, import_urlMatch.serializeURLMatch)(handler.url);
|
||||
if (serialized)
|
||||
patterns.push(serialized);
|
||||
else
|
||||
all = true;
|
||||
}
|
||||
if (all)
|
||||
return [{ glob: "**/*" }];
|
||||
return patterns;
|
||||
}
|
||||
matches(wsURL) {
|
||||
return (0, import_urlMatch.urlMatches)(this._baseURL, wsURL, this.url, true);
|
||||
}
|
||||
async handle(webSocketRoute) {
|
||||
const handler = this.handler;
|
||||
await handler(webSocketRoute);
|
||||
await webSocketRoute._afterHandle();
|
||||
}
|
||||
}
|
||||
class Response extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._finishedPromise = new import_manualPromise.ManualPromise();
|
||||
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||
this._request = Request.from(this._initializer.request);
|
||||
this._request._response = this;
|
||||
Object.assign(this._request._timing, this._initializer.timing);
|
||||
}
|
||||
static from(response) {
|
||||
return response._object;
|
||||
}
|
||||
static fromNullable(response) {
|
||||
return response ? Response.from(response) : null;
|
||||
}
|
||||
url() {
|
||||
return this._initializer.url;
|
||||
}
|
||||
ok() {
|
||||
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;
|
||||
}
|
||||
async httpVersion() {
|
||||
return (await this._channel.httpVersion()).value;
|
||||
}
|
||||
}
|
||||
class WebSocket extends import_channelOwner.ChannelOwner {
|
||||
static from(webSocket) {
|
||||
return webSocket._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._isClosed = false;
|
||||
this._page = parent;
|
||||
this._channel.on("frameSent", (event) => {
|
||||
if (event.opcode === 1)
|
||||
this.emit(import_events.Events.WebSocket.FrameSent, { payload: event.data });
|
||||
else if (event.opcode === 2)
|
||||
this.emit(import_events.Events.WebSocket.FrameSent, { payload: Buffer.from(event.data, "base64") });
|
||||
});
|
||||
this._channel.on("frameReceived", (event) => {
|
||||
if (event.opcode === 1)
|
||||
this.emit(import_events.Events.WebSocket.FrameReceived, { payload: event.data });
|
||||
else if (event.opcode === 2)
|
||||
this.emit(import_events.Events.WebSocket.FrameReceived, { payload: Buffer.from(event.data, "base64") });
|
||||
});
|
||||
this._channel.on("socketError", ({ error }) => this.emit(import_events.Events.WebSocket.Error, error));
|
||||
this._channel.on("close", () => {
|
||||
this._isClosed = true;
|
||||
this.emit(import_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 = import_waiter.Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== import_events.Events.WebSocket.Error)
|
||||
waiter.rejectOnEvent(this, import_events.Events.WebSocket.Error, new Error("Socket error"));
|
||||
if (event !== import_events.Events.WebSocket.Close)
|
||||
waiter.rejectOnEvent(this, import_events.Events.WebSocket.Close, new Error("Socket closed"));
|
||||
waiter.rejectOnEvent(this._page, import_events.Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
function validateHeaders(headers) {
|
||||
for (const key of Object.keys(headers)) {
|
||||
const value = headers[key];
|
||||
if (!Object.is(value, void 0) && !(0, import_rtti.isString)(value))
|
||||
throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
|
||||
}
|
||||
}
|
||||
class RouteHandler {
|
||||
constructor(platform, baseURL, url, handler, times = Number.MAX_SAFE_INTEGER) {
|
||||
this.handledCount = 0;
|
||||
this._ignoreException = false;
|
||||
this._activeInvocations = /* @__PURE__ */ new Set();
|
||||
this._baseURL = baseURL;
|
||||
this._times = times;
|
||||
this.url = url;
|
||||
this.handler = handler;
|
||||
this._savedZone = platform.zones.current().pop();
|
||||
}
|
||||
static prepareInterceptionPatterns(handlers) {
|
||||
const patterns = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
const serialized = (0, import_urlMatch.serializeURLMatch)(handler.url);
|
||||
if (serialized)
|
||||
patterns.push(serialized);
|
||||
else
|
||||
all = true;
|
||||
}
|
||||
if (all)
|
||||
return [{ glob: "**/*" }];
|
||||
return patterns;
|
||||
}
|
||||
matches(requestURL) {
|
||||
return (0, import_urlMatch.urlMatches)(this._baseURL, requestURL, this.url);
|
||||
}
|
||||
async handle(route) {
|
||||
return await this._savedZone.run(async () => this._handleImpl(route));
|
||||
}
|
||||
async _handleImpl(route) {
|
||||
const handlerInvocation = { complete: new import_manualPromise.ManualPromise(), route };
|
||||
this._activeInvocations.add(handlerInvocation);
|
||||
try {
|
||||
return await this._handleInternal(route);
|
||||
} catch (e) {
|
||||
if (this._ignoreException)
|
||||
return false;
|
||||
if ((0, import_errors.isTargetClosedError)(e)) {
|
||||
(0, import_stackTrace.rewriteErrorMessage)(e, `"${e.message}" while running route callback.
|
||||
Consider awaiting \`await page.unrouteAll({ behavior: 'ignoreErrors' })\`
|
||||
before the end of the test to ignore remaining routes in flight.`);
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
handlerInvocation.complete.resolve();
|
||||
this._activeInvocations.delete(handlerInvocation);
|
||||
}
|
||||
}
|
||||
async stop(behavior) {
|
||||
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();
|
||||
const handler = this.handler;
|
||||
const [handled] = await Promise.all([
|
||||
handledPromise,
|
||||
handler(route, route.request())
|
||||
]);
|
||||
return handled;
|
||||
}
|
||||
willExpire() {
|
||||
return this.handledCount + 1 >= this._times;
|
||||
}
|
||||
}
|
||||
class RawHeaders {
|
||||
constructor(headers) {
|
||||
this._headersMap = new import_multimap.MultiMap();
|
||||
this._headersArray = headers;
|
||||
for (const header of headers)
|
||||
this._headersMap.set(header.name.toLowerCase(), header.value);
|
||||
}
|
||||
static _fromHeadersObjectLossy(headers) {
|
||||
const headersArray = Object.entries(headers).map(([name, value]) => ({
|
||||
name,
|
||||
value
|
||||
})).filter((header) => header.value !== void 0);
|
||||
return new RawHeaders(headersArray);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
RawHeaders,
|
||||
Request,
|
||||
Response,
|
||||
Route,
|
||||
RouteHandler,
|
||||
WebSocket,
|
||||
WebSocketRoute,
|
||||
WebSocketRouteHandler,
|
||||
validateHeaders
|
||||
});
|
||||
+731
@@ -0,0 +1,731 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var page_exports = {};
|
||||
__export(page_exports, {
|
||||
BindingCall: () => BindingCall,
|
||||
Page: () => Page
|
||||
});
|
||||
module.exports = __toCommonJS(page_exports);
|
||||
var import_artifact = require("./artifact");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_clientHelper = require("./clientHelper");
|
||||
var import_coverage = require("./coverage");
|
||||
var import_disposable = require("./disposable");
|
||||
var import_download = require("./download");
|
||||
var import_elementHandle = require("./elementHandle");
|
||||
var import_errors = require("./errors");
|
||||
var import_events = require("./events");
|
||||
var import_fileChooser = require("./fileChooser");
|
||||
var import_frame = require("./frame");
|
||||
var import_harRouter = require("./harRouter");
|
||||
var import_input = require("./input");
|
||||
var import_jsHandle = require("./jsHandle");
|
||||
var import_network = require("./network");
|
||||
var import_video = require("./video");
|
||||
var import_screencast = require("./screencast");
|
||||
var import_waiter = require("./waiter");
|
||||
var import_worker = require("./worker");
|
||||
var import_timeoutSettings = require("./timeoutSettings");
|
||||
var import_assert = require("../utils/isomorphic/assert");
|
||||
var import_fileUtils = require("./fileUtils");
|
||||
var import_headers = require("../utils/isomorphic/headers");
|
||||
var import_stringUtils = require("../utils/isomorphic/stringUtils");
|
||||
var import_urlMatch = require("../utils/isomorphic/urlMatch");
|
||||
var import_manualPromise = require("../utils/isomorphic/manualPromise");
|
||||
var import_rtti = require("../utils/isomorphic/rtti");
|
||||
var import_consoleMessage = require("./consoleMessage");
|
||||
class Page extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._frames = /* @__PURE__ */ new Set();
|
||||
this._workers = /* @__PURE__ */ new Set();
|
||||
this._closed = false;
|
||||
this._closedOrCrashedScope = new import_manualPromise.LongStandingScope();
|
||||
this._routes = [];
|
||||
this._webSocketRoutes = [];
|
||||
this._bindings = /* @__PURE__ */ new Map();
|
||||
this._closeWasCalled = false;
|
||||
this._harRouters = [];
|
||||
this._locatorHandlers = /* @__PURE__ */ new Map();
|
||||
this._instrumentation.onPage(this);
|
||||
this._browserContext = parent;
|
||||
this._timeoutSettings = new import_timeoutSettings.TimeoutSettings(this._platform, this._browserContext._timeoutSettings);
|
||||
this.keyboard = new import_input.Keyboard(this);
|
||||
this.mouse = new import_input.Mouse(this);
|
||||
this.request = this._browserContext.request;
|
||||
this.touchscreen = new import_input.Touchscreen(this);
|
||||
this.clock = this._browserContext.clock;
|
||||
this._mainFrame = import_frame.Frame.from(initializer.mainFrame);
|
||||
this._mainFrame._page = this;
|
||||
this._frames.add(this._mainFrame);
|
||||
this._viewportSize = initializer.viewportSize;
|
||||
this._closed = initializer.isClosed;
|
||||
this._opener = Page.fromNullable(initializer.opener);
|
||||
this._video = new import_video.Video(this, this._connection, initializer.video ? import_artifact.Artifact.from(initializer.video) : void 0);
|
||||
this.screencast = new import_screencast.Screencast(this);
|
||||
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 = import_artifact.Artifact.from(artifact);
|
||||
this.emit(import_events.Events.Page.Download, new import_download.Download(this, url, suggestedFilename, artifactObject));
|
||||
});
|
||||
this._channel.on("fileChooser", ({ element, isMultiple }) => this.emit(import_events.Events.Page.FileChooser, new import_fileChooser.FileChooser(this, import_elementHandle.ElementHandle.from(element), isMultiple)));
|
||||
this._channel.on("frameAttached", ({ frame }) => this._onFrameAttached(import_frame.Frame.from(frame)));
|
||||
this._channel.on("frameDetached", ({ frame }) => this._onFrameDetached(import_frame.Frame.from(frame)));
|
||||
this._channel.on("locatorHandlerTriggered", ({ uid }) => this._onLocatorHandlerTriggered(uid));
|
||||
this._channel.on("route", ({ route }) => this._onRoute(import_network.Route.from(route)));
|
||||
this._channel.on("webSocketRoute", ({ webSocketRoute }) => this._onWebSocketRoute(import_network.WebSocketRoute.from(webSocketRoute)));
|
||||
this._channel.on("viewportSizeChanged", ({ viewportSize }) => this._viewportSize = viewportSize);
|
||||
this._channel.on("webSocket", ({ webSocket }) => this.emit(import_events.Events.Page.WebSocket, import_network.WebSocket.from(webSocket)));
|
||||
this._channel.on("worker", ({ worker }) => this._onWorker(import_worker.Worker.from(worker)));
|
||||
this.coverage = new import_coverage.Coverage(this._channel);
|
||||
this.once(import_events.Events.Page.Close, () => this._closedOrCrashedScope.close(this._closeErrorWithReason()));
|
||||
this.once(import_events.Events.Page.Crash, () => this._closedOrCrashedScope.close(new import_errors.TargetClosedError()));
|
||||
this._setEventToSubscriptionMapping(/* @__PURE__ */ new Map([
|
||||
[import_events.Events.Page.Console, "console"],
|
||||
[import_events.Events.Page.Dialog, "dialog"],
|
||||
[import_events.Events.Page.Request, "request"],
|
||||
[import_events.Events.Page.Response, "response"],
|
||||
[import_events.Events.Page.RequestFinished, "requestFinished"],
|
||||
[import_events.Events.Page.RequestFailed, "requestFailed"],
|
||||
[import_events.Events.Page.FileChooser, "fileChooser"]
|
||||
]));
|
||||
}
|
||||
static from(page) {
|
||||
return page._object;
|
||||
}
|
||||
static fromNullable(page) {
|
||||
return page ? Page.from(page) : null;
|
||||
}
|
||||
_onFrameAttached(frame) {
|
||||
frame._page = this;
|
||||
this._frames.add(frame);
|
||||
if (frame._parentFrame)
|
||||
frame._parentFrame._childFrames.add(frame);
|
||||
this.emit(import_events.Events.Page.FrameAttached, frame);
|
||||
}
|
||||
_onFrameDetached(frame) {
|
||||
this._frames.delete(frame);
|
||||
frame._detached = true;
|
||||
if (frame._parentFrame)
|
||||
frame._parentFrame._childFrames.delete(frame);
|
||||
this.emit(import_events.Events.Page.FrameDetached, frame);
|
||||
}
|
||||
async _onRoute(route) {
|
||||
route._context = this.context();
|
||||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
if (this._closeWasCalled || this._browserContext.isClosed())
|
||||
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._updateInterceptionPatterns({ internal: 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(import_events.Events.Page.Worker, worker);
|
||||
}
|
||||
_onClose() {
|
||||
this._closed = true;
|
||||
this._browserContext._pages.delete(this);
|
||||
this._disposeHarRouters();
|
||||
this.emit(import_events.Events.Page.Close, this);
|
||||
}
|
||||
_onCrash() {
|
||||
this.emit(import_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, import_rtti.isString)(frameSelector) ? frameSelector : frameSelector.name;
|
||||
const url = (0, import_rtti.isObject)(frameSelector) ? frameSelector.url : void 0;
|
||||
(0, import_assert.assert)(name || url, "Either name or url matcher should be specified");
|
||||
return this.frames().find((f) => {
|
||||
if (name)
|
||||
return f.name() === name;
|
||||
return (0, import_urlMatch.urlMatches)(this._browserContext._options.baseURL, f.url(), url);
|
||||
}) || null;
|
||||
}
|
||||
frames() {
|
||||
return [...this._frames];
|
||||
}
|
||||
setDefaultNavigationTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultNavigationTimeout(timeout);
|
||||
}
|
||||
setDefaultTimeout(timeout) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
}
|
||||
video() {
|
||||
if (!this._browserContext._options.recordVideo)
|
||||
return null;
|
||||
return this._video;
|
||||
}
|
||||
async pickLocator() {
|
||||
const { selector } = await this._channel.pickLocator({});
|
||||
return this.locator(selector);
|
||||
}
|
||||
async cancelPickLocator() {
|
||||
await this._channel.cancelPickLocator({});
|
||||
}
|
||||
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, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
return await this._mainFrame.evaluateHandle(pageFunction, arg);
|
||||
}
|
||||
async $eval(selector, pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 3);
|
||||
return await this._mainFrame.$eval(selector, pageFunction, arg);
|
||||
}
|
||||
async $$eval(selector, pageFunction, arg) {
|
||||
(0, import_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) {
|
||||
const result = await this._channel.exposeBinding({ name });
|
||||
const binding = (source, ...args) => callback(...args);
|
||||
this._bindings.set(name, binding);
|
||||
return import_disposable.DisposableObject.from(result.disposable);
|
||||
}
|
||||
async exposeBinding(name, callback, options = {}) {
|
||||
const result = await this._channel.exposeBinding({ name, needsHandle: options.handle });
|
||||
this._bindings.set(name, callback);
|
||||
return import_disposable.DisposableObject.from(result.disposable);
|
||||
}
|
||||
async setExtraHTTPHeaders(headers) {
|
||||
(0, import_network.validateHeaders)(headers);
|
||||
await this._channel.setExtraHTTPHeaders({ headers: (0, import_headers.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, import_frame.verifyLoadState)("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||
return import_network.Response.fromNullable((await this._channel.reload({ ...options, waitUntil, timeout: this._timeoutSettings.navigationTimeout(options) })).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 !== void 0)
|
||||
handler.times--;
|
||||
await handler.handler(handler.locator);
|
||||
}
|
||||
remove = handler?.times === 0;
|
||||
} finally {
|
||||
if (remove)
|
||||
this._locatorHandlers.delete(uid);
|
||||
this._channel.resolveLocatorHandlerNoReply({ uid, remove }).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, import_rtti.isString)(urlOrPredicate) || (0, import_rtti.isRegExp)(urlOrPredicate))
|
||||
return (0, import_urlMatch.urlMatches)(this._browserContext._options.baseURL, request.url(), urlOrPredicate);
|
||||
return await urlOrPredicate(request);
|
||||
};
|
||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||
const logLine = trimmedUrl ? `waiting for request ${trimmedUrl}` : void 0;
|
||||
return await this._waitForEvent(import_events.Events.Page.Request, { predicate, timeout: options.timeout }, logLine);
|
||||
}
|
||||
async waitForResponse(urlOrPredicate, options = {}) {
|
||||
const predicate = async (response) => {
|
||||
if ((0, import_rtti.isString)(urlOrPredicate) || (0, import_rtti.isRegExp)(urlOrPredicate))
|
||||
return (0, import_urlMatch.urlMatches)(this._browserContext._options.baseURL, response.url(), urlOrPredicate);
|
||||
return await urlOrPredicate(response);
|
||||
};
|
||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||
const logLine = trimmedUrl ? `waiting for response ${trimmedUrl}` : void 0;
|
||||
return await this._waitForEvent(import_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 import_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 = import_waiter.Waiter.createForEvent(this, event);
|
||||
if (logLine)
|
||||
waiter.log(logLine);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== import_events.Events.Page.Crash)
|
||||
waiter.rejectOnEvent(this, import_events.Events.Page.Crash, new Error("Page crashed"));
|
||||
if (event !== import_events.Events.Page.Close)
|
||||
waiter.rejectOnEvent(this, import_events.Events.Page.Close, () => this._closeErrorWithReason());
|
||||
const result = await waiter.waitForEvent(this, event, predicate);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
});
|
||||
}
|
||||
async goBack(options = {}) {
|
||||
const waitUntil = (0, import_frame.verifyLoadState)("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||
return import_network.Response.fromNullable((await this._channel.goBack({ ...options, waitUntil, timeout: this._timeoutSettings.navigationTimeout(options) })).response);
|
||||
}
|
||||
async goForward(options = {}) {
|
||||
const waitUntil = (0, import_frame.verifyLoadState)("waitUntil", options.waitUntil === void 0 ? "load" : options.waitUntil);
|
||||
return import_network.Response.fromNullable((await this._channel.goForward({ ...options, waitUntil, timeout: this._timeoutSettings.navigationTimeout(options) })).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,
|
||||
contrast: options.contrast === null ? "no-override" : options.contrast
|
||||
});
|
||||
}
|
||||
async setViewportSize(viewportSize) {
|
||||
this._viewportSize = viewportSize;
|
||||
await this._channel.setViewportSize({ viewportSize });
|
||||
}
|
||||
viewportSize() {
|
||||
return this._viewportSize || null;
|
||||
}
|
||||
async evaluate(pageFunction, arg) {
|
||||
(0, import_jsHandle.assertMaxArguments)(arguments.length, 2);
|
||||
return await this._mainFrame.evaluate(pageFunction, arg);
|
||||
}
|
||||
async addInitScript(script, arg) {
|
||||
const source = await (0, import_clientHelper.evaluationScript)(this._platform, script, arg);
|
||||
return import_disposable.DisposableObject.from((await this._channel.addInitScript({ source })).disposable);
|
||||
}
|
||||
async route(url, handler, options = {}) {
|
||||
this._routes.unshift(new import_network.RouteHandler(this._platform, this._browserContext._options.baseURL, url, handler, options.times));
|
||||
await this._updateInterceptionPatterns({ title: "Route requests" });
|
||||
return new import_disposable.DisposableStub(() => this.unroute(url, handler));
|
||||
}
|
||||
async routeFromHAR(har, options = {}) {
|
||||
const localUtils = this._connection.localUtils();
|
||||
if (!localUtils)
|
||||
throw new Error("Route from har is not supported in thin clients");
|
||||
if (options.update) {
|
||||
await this._browserContext._recordIntoHAR(har, this, options);
|
||||
return;
|
||||
}
|
||||
const harRouter = await import_harRouter.HarRouter.create(localUtils, har, options.notFound || "abort", { urlMatch: options.url });
|
||||
this._harRouters.push(harRouter);
|
||||
await harRouter.addPageRoute(this);
|
||||
}
|
||||
async routeWebSocket(url, handler) {
|
||||
this._webSocketRoutes.unshift(new import_network.WebSocketRouteHandler(this._browserContext._options.baseURL, url, handler));
|
||||
await this._updateWebSocketInterceptionPatterns({ title: "Route WebSockets" });
|
||||
}
|
||||
_disposeHarRouters() {
|
||||
this._harRouters.forEach((router) => router.dispose());
|
||||
this._harRouters = [];
|
||||
}
|
||||
async unrouteAll(options) {
|
||||
await this._unrouteInternal(this._routes, [], options?.behavior);
|
||||
this._disposeHarRouters();
|
||||
}
|
||||
async unroute(url, handler) {
|
||||
const removed = [];
|
||||
const remaining = [];
|
||||
for (const route of this._routes) {
|
||||
if ((0, import_urlMatch.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;
|
||||
if (behavior && behavior !== "default") {
|
||||
const promises = removed.map((routeHandler) => routeHandler.stop(behavior));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
await this._updateInterceptionPatterns({ title: "Unroute requests" });
|
||||
}
|
||||
async _updateInterceptionPatterns(options) {
|
||||
const patterns = import_network.RouteHandler.prepareInterceptionPatterns(this._routes);
|
||||
await this._wrapApiCall(() => this._channel.setNetworkInterceptionPatterns({ patterns }), options);
|
||||
}
|
||||
async _updateWebSocketInterceptionPatterns(options) {
|
||||
const patterns = import_network.WebSocketRouteHandler.prepareInterceptionPatterns(this._webSocketRoutes);
|
||||
await this._wrapApiCall(() => this._channel.setWebSocketInterceptionPatterns({ patterns }), options);
|
||||
}
|
||||
async screenshot(options = {}) {
|
||||
const mask = options.mask;
|
||||
const copy = { ...options, mask: void 0, timeout: this._timeoutSettings.timeout(options) };
|
||||
if (!copy.type)
|
||||
copy.type = (0, import_elementHandle.determineScreenshotType)(options);
|
||||
if (mask) {
|
||||
copy.mask = mask.map((locator) => ({
|
||||
frame: locator._frame._channel,
|
||||
selector: locator._selector
|
||||
}));
|
||||
}
|
||||
const result = await this._channel.screenshot(copy);
|
||||
if (options.path) {
|
||||
await (0, import_fileUtils.mkdirIfNeeded)(this._platform, options.path);
|
||||
await this._platform.fs().promises.writeFile(options.path, result.binary);
|
||||
}
|
||||
return result.binary;
|
||||
}
|
||||
async _expectScreenshot(options) {
|
||||
const mask = options?.mask ? options?.mask.map((locator2) => ({
|
||||
frame: locator2._frame._channel,
|
||||
selector: locator2._selector
|
||||
})) : void 0;
|
||||
const locator = options.locator ? {
|
||||
frame: options.locator._frame._channel,
|
||||
selector: options.locator._selector
|
||||
} : void 0;
|
||||
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;
|
||||
if (!options.runBeforeUnload)
|
||||
this._closeWasCalled = true;
|
||||
try {
|
||||
if (this._ownedContext)
|
||||
await this._ownedContext.close();
|
||||
else
|
||||
await this._channel.close(options);
|
||||
} catch (e) {
|
||||
if ((0, import_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) {
|
||||
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);
|
||||
}
|
||||
async clearConsoleMessages() {
|
||||
await this._channel.clearConsoleMessages();
|
||||
}
|
||||
async consoleMessages(options) {
|
||||
const { messages } = await this._channel.consoleMessages({ filter: options?.filter });
|
||||
return messages.map((message) => new import_consoleMessage.ConsoleMessage(this._platform, message, this, null));
|
||||
}
|
||||
async clearPageErrors() {
|
||||
await this._channel.clearPageErrors();
|
||||
}
|
||||
async pageErrors(options) {
|
||||
const { errors } = await this._channel.pageErrors({ filter: options?.filter });
|
||||
return errors.map((error) => (0, import_errors.parseError)(error));
|
||||
}
|
||||
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);
|
||||
}
|
||||
async requests() {
|
||||
const { requests } = await this._channel.requests();
|
||||
return requests.map((request) => import_network.Request.from(request));
|
||||
}
|
||||
workers() {
|
||||
return [...this._workers];
|
||||
}
|
||||
async pause(_options) {
|
||||
if (this._platform.isJSDebuggerAttached())
|
||||
return;
|
||||
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
|
||||
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();
|
||||
this._browserContext.setDefaultNavigationTimeout(0);
|
||||
this._browserContext.setDefaultTimeout(0);
|
||||
this._instrumentation?.onWillPause({ keepTestTimeout: !!_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) {
|
||||
const platform = this._platform;
|
||||
await platform.fs().promises.mkdir(platform.path().dirname(options.path), { recursive: true });
|
||||
await platform.fs().promises.writeFile(options.path, result.pdf);
|
||||
}
|
||||
return result.pdf;
|
||||
}
|
||||
async ariaSnapshot(options = {}) {
|
||||
const result = await this.mainFrame()._channel.ariaSnapshot({ timeout: this._timeoutSettings.timeout(options), track: options._track, mode: options.mode, depth: options.depth });
|
||||
return result.snapshot;
|
||||
}
|
||||
async _setDockTile(image) {
|
||||
await this._channel.setDockTile({ image });
|
||||
}
|
||||
}
|
||||
class BindingCall extends import_channelOwner.ChannelOwner {
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
async call(func) {
|
||||
try {
|
||||
const frame = import_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, import_jsHandle.JSHandle.from(this._initializer.handle));
|
||||
else
|
||||
result = await func(source, ...this._initializer.args.map(import_jsHandle.parseResult));
|
||||
this._channel.resolve({ result: (0, import_jsHandle.serializeArgument)(result) }).catch(() => {
|
||||
});
|
||||
} catch (e) {
|
||||
this._channel.reject({ error: (0, import_errors.serializeError)(e) }).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
function trimUrl(param) {
|
||||
if ((0, import_rtti.isRegExp)(param))
|
||||
return `/${(0, import_stringUtils.trimStringWithEllipsis)(param.source, 50)}/${param.flags}`;
|
||||
if ((0, import_rtti.isString)(param))
|
||||
return `"${(0, import_stringUtils.trimStringWithEllipsis)(param, 50)}"`;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
BindingCall,
|
||||
Page
|
||||
});
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var platform_exports = {};
|
||||
__export(platform_exports, {
|
||||
emptyPlatform: () => emptyPlatform
|
||||
});
|
||||
module.exports = __toCommonJS(platform_exports);
|
||||
var import_colors = require("../utils/isomorphic/colors");
|
||||
const noopZone = {
|
||||
push: () => noopZone,
|
||||
pop: () => noopZone,
|
||||
run: (func) => func(),
|
||||
data: () => void 0
|
||||
};
|
||||
const emptyPlatform = {
|
||||
name: "empty",
|
||||
boxedStackPrefixes: () => [],
|
||||
calculateSha1: async () => {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
colors: import_colors.webColors,
|
||||
createGuid: () => {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
defaultMaxListeners: () => 10,
|
||||
env: {},
|
||||
fs: () => {
|
||||
throw new Error("Not implemented");
|
||||
},
|
||||
inspectCustom: void 0,
|
||||
isDebugMode: () => false,
|
||||
isJSDebuggerAttached: () => false,
|
||||
isLogEnabled(name) {
|
||||
return false;
|
||||
},
|
||||
isUnderTest: () => false,
|
||||
log(name, message) {
|
||||
},
|
||||
path: () => {
|
||||
throw new Error("Function not implemented.");
|
||||
},
|
||||
pathSeparator: "/",
|
||||
showInternalStackFrames: () => false,
|
||||
streamFile(path, writable) {
|
||||
throw new Error("Streams are not available");
|
||||
},
|
||||
streamReadable: (channel) => {
|
||||
throw new Error("Streams are not available");
|
||||
},
|
||||
streamWritable: (channel) => {
|
||||
throw new Error("Streams are not available");
|
||||
},
|
||||
zones: { empty: noopZone, current: () => noopZone }
|
||||
};
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
emptyPlatform
|
||||
});
|
||||
Generated
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var playwright_exports = {};
|
||||
__export(playwright_exports, {
|
||||
Playwright: () => Playwright
|
||||
});
|
||||
module.exports = __toCommonJS(playwright_exports);
|
||||
var import_android = require("./android");
|
||||
var import_browser = require("./browser");
|
||||
var import_browserType = require("./browserType");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_electron = require("./electron");
|
||||
var import_errors = require("./errors");
|
||||
var import_fetch = require("./fetch");
|
||||
var import_selectors = require("./selectors");
|
||||
class Playwright extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.request = new import_fetch.APIRequest(this);
|
||||
this.chromium = import_browserType.BrowserType.from(initializer.chromium);
|
||||
this.chromium._playwright = this;
|
||||
this.firefox = import_browserType.BrowserType.from(initializer.firefox);
|
||||
this.firefox._playwright = this;
|
||||
this.webkit = import_browserType.BrowserType.from(initializer.webkit);
|
||||
this.webkit._playwright = this;
|
||||
this._android = import_android.Android.from(initializer.android);
|
||||
this._android._playwright = this;
|
||||
this._electron = import_electron.Electron.from(initializer.electron);
|
||||
this._electron._playwright = this;
|
||||
this.devices = this._connection.localUtils()?.devices ?? {};
|
||||
this.selectors = new import_selectors.Selectors(this._connection._platform);
|
||||
this.errors = { TimeoutError: import_errors.TimeoutError };
|
||||
}
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
_browserTypes() {
|
||||
return [this.chromium, this.firefox, this.webkit];
|
||||
}
|
||||
_preLaunchedBrowser() {
|
||||
const browser = import_browser.Browser.from(this._initializer.preLaunchedBrowser);
|
||||
browser._connectToBrowserType(this[browser._name], {}, void 0);
|
||||
return browser;
|
||||
}
|
||||
_allContexts() {
|
||||
return this._browserTypes().flatMap((type) => [...type._contexts]);
|
||||
}
|
||||
_allPages() {
|
||||
return this._allContexts().flatMap((context) => context.pages());
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Playwright
|
||||
});
|
||||
Generated
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var screencast_exports = {};
|
||||
__export(screencast_exports, {
|
||||
Screencast: () => Screencast
|
||||
});
|
||||
module.exports = __toCommonJS(screencast_exports);
|
||||
var import_artifact = require("./artifact");
|
||||
var import_disposable = require("./disposable");
|
||||
class Screencast {
|
||||
constructor(page) {
|
||||
this._started = false;
|
||||
this._onFrame = null;
|
||||
this._page = page;
|
||||
this._page._channel.on("screencastFrame", ({ data }) => {
|
||||
void this._onFrame?.({ data });
|
||||
});
|
||||
}
|
||||
async start(options = {}) {
|
||||
if (this._started)
|
||||
throw new Error("Screencast is already started");
|
||||
this._started = true;
|
||||
if (options.onFrame)
|
||||
this._onFrame = options.onFrame;
|
||||
const result = await this._page._channel.screencastStart({
|
||||
size: options.size,
|
||||
quality: options.quality,
|
||||
sendFrames: !!options.onFrame,
|
||||
record: !!options.path
|
||||
});
|
||||
if (result.artifact) {
|
||||
this._artifact = import_artifact.Artifact.from(result.artifact);
|
||||
this._savePath = options.path;
|
||||
}
|
||||
return new import_disposable.DisposableStub(() => this.stop());
|
||||
}
|
||||
async stop() {
|
||||
await this._page._wrapApiCall(async () => {
|
||||
this._started = false;
|
||||
this._onFrame = null;
|
||||
await this._page._channel.screencastStop();
|
||||
if (this._savePath)
|
||||
await this._artifact?.saveAs(this._savePath);
|
||||
this._artifact = void 0;
|
||||
this._savePath = void 0;
|
||||
});
|
||||
}
|
||||
async showActions(options) {
|
||||
await this._page._channel.screencastShowActions({ duration: options?.duration, position: options?.position, fontSize: options?.fontSize });
|
||||
return new import_disposable.DisposableStub(() => this._page._channel.screencastHideActions());
|
||||
}
|
||||
async hideActions() {
|
||||
await this._page._channel.screencastHideActions();
|
||||
}
|
||||
async showOverlay(html, options) {
|
||||
const { id } = await this._page._channel.screencastShowOverlay({ html, duration: options?.duration });
|
||||
return new import_disposable.DisposableStub(() => this._page._channel.screencastRemoveOverlay({ id }));
|
||||
}
|
||||
async showChapter(title, options) {
|
||||
await this._page._channel.screencastChapter({ title, ...options });
|
||||
}
|
||||
async showOverlays() {
|
||||
await this._page._channel.screencastSetOverlayVisible({ visible: true });
|
||||
}
|
||||
async hideOverlays() {
|
||||
await this._page._channel.screencastSetOverlayVisible({ visible: false });
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Screencast
|
||||
});
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var selectors_exports = {};
|
||||
__export(selectors_exports, {
|
||||
Selectors: () => Selectors
|
||||
});
|
||||
module.exports = __toCommonJS(selectors_exports);
|
||||
var import_clientHelper = require("./clientHelper");
|
||||
var import_locator = require("./locator");
|
||||
class Selectors {
|
||||
constructor(platform) {
|
||||
this._selectorEngines = [];
|
||||
this._contextsForSelectors = /* @__PURE__ */ new Set();
|
||||
this._platform = platform;
|
||||
}
|
||||
async register(name, script, options = {}) {
|
||||
if (this._selectorEngines.some((engine) => engine.name === name))
|
||||
throw new Error(`selectors.register: "${name}" selector engine has been already registered`);
|
||||
const source = await (0, import_clientHelper.evaluationScript)(this._platform, script, void 0, false);
|
||||
const selectorEngine = { ...options, name, source };
|
||||
for (const context of this._contextsForSelectors)
|
||||
await context._channel.registerSelectorEngine({ selectorEngine });
|
||||
this._selectorEngines.push(selectorEngine);
|
||||
}
|
||||
setTestIdAttribute(attributeName) {
|
||||
this._testIdAttributeName = attributeName;
|
||||
(0, import_locator.setTestIdAttribute)(attributeName);
|
||||
for (const context of this._contextsForSelectors) {
|
||||
context._options.testIdAttributeName = attributeName;
|
||||
context._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
_withSelectorOptions(options) {
|
||||
return { ...options, selectorEngines: this._selectorEngines, testIdAttributeName: this._testIdAttributeName };
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Selectors
|
||||
});
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var stream_exports = {};
|
||||
__export(stream_exports, {
|
||||
Stream: () => Stream
|
||||
});
|
||||
module.exports = __toCommonJS(stream_exports);
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
class Stream extends import_channelOwner.ChannelOwner {
|
||||
static from(Stream2) {
|
||||
return Stream2._object;
|
||||
}
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
}
|
||||
stream() {
|
||||
return this._platform.streamReadable(this._channel);
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Stream
|
||||
});
|
||||
Generated
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var timeoutSettings_exports = {};
|
||||
__export(timeoutSettings_exports, {
|
||||
TimeoutSettings: () => TimeoutSettings
|
||||
});
|
||||
module.exports = __toCommonJS(timeoutSettings_exports);
|
||||
var import_time = require("../utils/isomorphic/time");
|
||||
class TimeoutSettings {
|
||||
constructor(platform, parent) {
|
||||
this._parent = parent;
|
||||
this._platform = platform;
|
||||
}
|
||||
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 !== void 0)
|
||||
return this._defaultNavigationTimeout;
|
||||
if (this._platform.isDebugMode())
|
||||
return 0;
|
||||
if (this._defaultTimeout !== void 0)
|
||||
return this._defaultTimeout;
|
||||
if (this._parent)
|
||||
return this._parent.navigationTimeout(options);
|
||||
return import_time.DEFAULT_PLAYWRIGHT_TIMEOUT;
|
||||
}
|
||||
timeout(options) {
|
||||
if (typeof options.timeout === "number")
|
||||
return options.timeout;
|
||||
if (this._platform.isDebugMode())
|
||||
return 0;
|
||||
if (this._defaultTimeout !== void 0)
|
||||
return this._defaultTimeout;
|
||||
if (this._parent)
|
||||
return this._parent.timeout(options);
|
||||
return import_time.DEFAULT_PLAYWRIGHT_TIMEOUT;
|
||||
}
|
||||
launchTimeout(options) {
|
||||
if (typeof options.timeout === "number")
|
||||
return options.timeout;
|
||||
if (this._platform.isDebugMode())
|
||||
return 0;
|
||||
if (this._parent)
|
||||
return this._parent.launchTimeout(options);
|
||||
return import_time.DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
TimeoutSettings
|
||||
});
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var tracing_exports = {};
|
||||
__export(tracing_exports, {
|
||||
Tracing: () => Tracing
|
||||
});
|
||||
module.exports = __toCommonJS(tracing_exports);
|
||||
var import_artifact = require("./artifact");
|
||||
var import_channelOwner = require("./channelOwner");
|
||||
var import_disposable = require("./disposable");
|
||||
class Tracing extends import_channelOwner.ChannelOwner {
|
||||
constructor(parent, type, guid, initializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._includeSources = false;
|
||||
this._additionalSources = /* @__PURE__ */ new Set();
|
||||
this._isLive = false;
|
||||
this._isTracing = false;
|
||||
}
|
||||
static from(channel) {
|
||||
return channel._object;
|
||||
}
|
||||
async start(options = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
this._includeSources = !!options.sources;
|
||||
this._isLive = !!options.live;
|
||||
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, this._isLive);
|
||||
});
|
||||
}
|
||||
async startChunk(options = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
const { traceName } = await this._channel.tracingStartChunk(options);
|
||||
await this._startCollectingStacks(traceName, this._isLive);
|
||||
});
|
||||
}
|
||||
async group(name, options = {}) {
|
||||
if (options.location)
|
||||
this._additionalSources.add(options.location.file);
|
||||
await this._channel.tracingGroup({ name, location: options.location });
|
||||
return new import_disposable.DisposableStub(() => this.groupEnd());
|
||||
}
|
||||
async groupEnd() {
|
||||
await this._channel.tracingGroupEnd();
|
||||
}
|
||||
async _startCollectingStacks(traceName, live) {
|
||||
if (!this._isTracing) {
|
||||
this._isTracing = true;
|
||||
this._connection.setIsTracing(true);
|
||||
}
|
||||
const result = await this._connection.localUtils()?.tracingStarted({ tracesDir: this._tracesDir, traceName, live });
|
||||
this._stacksId = result?.stacksId;
|
||||
}
|
||||
async stopChunk(options = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._doStopChunk(options.path);
|
||||
});
|
||||
}
|
||||
async stop(options = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._doStopChunk(options.path);
|
||||
await this._channel.tracingStop();
|
||||
});
|
||||
}
|
||||
async _doStopChunk(filePath) {
|
||||
this._resetStackCounter();
|
||||
const additionalSources = [...this._additionalSources];
|
||||
this._additionalSources.clear();
|
||||
if (!filePath) {
|
||||
await this._channel.tracingStopChunk({ mode: "discard" });
|
||||
if (this._stacksId)
|
||||
await this._connection.localUtils().traceDiscarded({ stacksId: this._stacksId });
|
||||
return;
|
||||
}
|
||||
const localUtils = this._connection.localUtils();
|
||||
if (!localUtils)
|
||||
throw new Error("Cannot save trace in thin clients");
|
||||
const isLocal = !this._connection.isRemote();
|
||||
if (isLocal) {
|
||||
const result2 = await this._channel.tracingStopChunk({ mode: "entries" });
|
||||
await localUtils.zip({ zipFile: filePath, entries: result2.entries, mode: "write", stacksId: this._stacksId, includeSources: this._includeSources, additionalSources });
|
||||
return;
|
||||
}
|
||||
const result = await this._channel.tracingStopChunk({ mode: "archive" });
|
||||
if (!result.artifact) {
|
||||
if (this._stacksId)
|
||||
await localUtils.traceDiscarded({ stacksId: this._stacksId });
|
||||
return;
|
||||
}
|
||||
const artifact = import_artifact.Artifact.from(result.artifact);
|
||||
await artifact.saveAs(filePath);
|
||||
await artifact.delete();
|
||||
await localUtils.zip({ zipFile: filePath, entries: [], mode: "append", stacksId: this._stacksId, includeSources: this._includeSources, additionalSources });
|
||||
}
|
||||
_resetStackCounter() {
|
||||
if (this._isTracing) {
|
||||
this._isTracing = false;
|
||||
this._connection.setIsTracing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Tracing
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var types_exports = {};
|
||||
__export(types_exports, {
|
||||
kLifecycleEvents: () => kLifecycleEvents
|
||||
});
|
||||
module.exports = __toCommonJS(types_exports);
|
||||
const kLifecycleEvents = /* @__PURE__ */ new Set(["load", "domcontentloaded", "networkidle", "commit"]);
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
kLifecycleEvents
|
||||
});
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var video_exports = {};
|
||||
__export(video_exports, {
|
||||
Video: () => Video
|
||||
});
|
||||
module.exports = __toCommonJS(video_exports);
|
||||
var import_eventEmitter = require("./eventEmitter");
|
||||
class Video extends import_eventEmitter.EventEmitter {
|
||||
constructor(page, connection, artifact) {
|
||||
super(page._platform);
|
||||
this._isRemote = false;
|
||||
this._isRemote = connection.isRemote();
|
||||
this._artifact = artifact;
|
||||
}
|
||||
async path() {
|
||||
if (this._isRemote)
|
||||
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
|
||||
if (!this._artifact)
|
||||
throw new Error("Video recording has not been started.");
|
||||
return this._artifact._initializer.absolutePath;
|
||||
}
|
||||
async saveAs(path) {
|
||||
if (!this._artifact)
|
||||
throw new Error("Video recording has not been started.");
|
||||
return await this._artifact.saveAs(path);
|
||||
}
|
||||
async delete() {
|
||||
if (this._artifact)
|
||||
await this._artifact.delete();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Video
|
||||
});
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var waiter_exports = {};
|
||||
__export(waiter_exports, {
|
||||
Waiter: () => Waiter
|
||||
});
|
||||
module.exports = __toCommonJS(waiter_exports);
|
||||
var import_errors = require("./errors");
|
||||
var import_stackTrace = require("../utils/isomorphic/stackTrace");
|
||||
class Waiter {
|
||||
constructor(channelOwner, event) {
|
||||
this._failures = [];
|
||||
this._logs = [];
|
||||
this._waitId = channelOwner._platform.createGuid();
|
||||
this._channelOwner = channelOwner;
|
||||
this._savedZone = channelOwner._platform.zones.current().pop();
|
||||
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 } });
|
||||
}, { internal: 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 import_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, import_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 } });
|
||||
}, { internal: true }).catch(() => {
|
||||
});
|
||||
}
|
||||
_rejectOn(promise, dispose) {
|
||||
this._failures.push(promise);
|
||||
if (dispose)
|
||||
this._dispose.push(dispose);
|
||||
}
|
||||
}
|
||||
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 `
|
||||
${"=".repeat(leftLength)}${header}${"=".repeat(rightLength)}
|
||||
${log.join("\n")}
|
||||
${"=".repeat(headerLength)}`;
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Waiter
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user