Merge branch 'main' into sk-branch-4
This commit is contained in:
commit
579fbd25ba
2
.github/workflows/create_test_report.yml
vendored
2
.github/workflows/create_test_report.yml
vendored
|
|
@ -35,7 +35,7 @@ jobs:
|
|||
run: |
|
||||
npx playwright merge-reports --config .github/workflows/merge.config.ts ./all-blob-reports
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Azure Login
|
||||
uses: azure/login@v2
|
||||
|
|
|
|||
31
.github/workflows/tests_secondary.yml
vendored
31
.github/workflows/tests_secondary.yml
vendored
|
|
@ -268,40 +268,25 @@ jobs:
|
|||
- run: npx playwright install-deps
|
||||
- run: utils/build/build-playwright-driver.sh
|
||||
|
||||
test_linux_chromium_headless_new:
|
||||
name: Linux Chromium Headless New
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
browsers-to-install: chromium
|
||||
command: npm run ctest
|
||||
bot-name: "headless-new"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW: 1
|
||||
|
||||
test_linux_chromium_headless_shell:
|
||||
name: Chromium Headless Shell
|
||||
test_channel_chromium:
|
||||
name: Test channel=chromium
|
||||
environment: ${{ github.event_name == 'push' && 'allow-uploading-flakiness-results' || null }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runs-on: [ubuntu-latest, macos-latest, windows-latest]
|
||||
runs-on: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/run-test
|
||||
with:
|
||||
browsers-to-install: chromium-headless-shell
|
||||
# TODO: this should pass --no-shell.
|
||||
# However, codegen tests do not inherit the channel and try to launch headless shell.
|
||||
browsers-to-install: chromium
|
||||
command: npm run ctest
|
||||
bot-name: "headless-shell-${{ matrix.runs-on }}"
|
||||
bot-name: "channel-chromium-${{ matrix.runs-on }}"
|
||||
flakiness-client-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_CLIENT_ID }}
|
||||
flakiness-tenant-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_TENANT_ID }}
|
||||
flakiness-subscription-id: ${{ secrets.AZURE_FLAKINESS_DASHBOARD_SUBSCRIPTION_ID }}
|
||||
env:
|
||||
PWTEST_CHANNEL: chromium-headless-shell
|
||||
PWTEST_CHANNEL: chromium
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 🎭 Playwright
|
||||
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
[](https://www.npmjs.com/package/playwright) <!-- GEN:chromium-version-badge -->[](https://www.chromium.org/Home)<!-- GEN:stop --> <!-- GEN:firefox-version-badge -->[](https://www.mozilla.org/en-US/firefox/new/)<!-- GEN:stop --> <!-- GEN:webkit-version-badge -->[](https://webkit.org/)<!-- GEN:stop --> [](https://aka.ms/playwright/discord)
|
||||
|
||||
## [Documentation](https://playwright.dev) | [API reference](https://playwright.dev/docs/api/class-playwright)
|
||||
|
||||
|
|
@ -8,9 +8,9 @@ Playwright is a framework for Web Testing and Automation. It allows testing [Chr
|
|||
|
||||
| | Linux | macOS | Windows |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Chromium <!-- GEN:chromium-version -->131.0.6778.3<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->131.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Chromium <!-- GEN:chromium-version -->132.0.6834.6<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| WebKit <!-- GEN:webkit-version -->18.2<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
| Firefox <!-- GEN:firefox-version -->132.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
|
||||
|
||||
Headless execution is supported for all browsers on all platforms. Check out [system requirements](https://playwright.dev/docs/intro#system-requirements) for details.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
REMOTE_URL="https://github.com/mozilla/gecko-dev"
|
||||
BASE_BRANCH="release"
|
||||
BASE_REVISION="47bcb6d7d2013f9a3d864678675100e0b3d73c5e"
|
||||
BASE_REVISION="bc78b98043438d8ee2727a483b6e10dedfda883f"
|
||||
|
|
|
|||
|
|
@ -256,6 +256,13 @@ class PageHandler {
|
|||
return await this._contentPage.send('disposeObject', options);
|
||||
}
|
||||
|
||||
async ['Heap.collectGarbage']() {
|
||||
Services.obs.notifyObservers(null, "child-gc-request");
|
||||
Cu.forceGC();
|
||||
Services.obs.notifyObservers(null, "child-cc-request");
|
||||
Cu.forceCC();
|
||||
}
|
||||
|
||||
async ['Network.getResponseBody']({requestId}) {
|
||||
return this._pageNetwork.getResponseBody(requestId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -481,6 +481,17 @@ const Browser = {
|
|||
},
|
||||
};
|
||||
|
||||
const Heap = {
|
||||
targets: ['page'],
|
||||
types: {},
|
||||
events: {},
|
||||
methods: {
|
||||
'collectGarbage': {
|
||||
params: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Network = {
|
||||
targets: ['page'],
|
||||
types: networkTypes,
|
||||
|
|
@ -996,7 +1007,7 @@ const Accessibility = {
|
|||
}
|
||||
|
||||
this.protocol = {
|
||||
domains: {Browser, Page, Runtime, Network, Accessibility},
|
||||
domains: {Browser, Heap, Page, Runtime, Network, Accessibility},
|
||||
};
|
||||
this.checkScheme = checkScheme;
|
||||
this.EXPORTED_SYMBOLS = ['protocol', 'checkScheme'];
|
||||
|
|
|
|||
|
|
@ -57,10 +57,10 @@ index 8e9bf2b413585b5a3db9370eee5d57fb6c6716ed..5a3b194b54e3813c89989f13a214c989
|
|||
* Return XPCOM wrapper for the internal accessible.
|
||||
*/
|
||||
diff --git a/browser/app/winlauncher/LauncherProcessWin.cpp b/browser/app/winlauncher/LauncherProcessWin.cpp
|
||||
index b40e0fceb567c0d217adf284e13f434e49cc8467..2c4e6d5fbf8da40954ad6a5b15e412493e43b14e 100644
|
||||
index 8167d2b81c918e02ce757f7f448f22e07c29d140..3ae798880acfd8aa965ae08051f2f81855133711 100644
|
||||
--- a/browser/app/winlauncher/LauncherProcessWin.cpp
|
||||
+++ b/browser/app/winlauncher/LauncherProcessWin.cpp
|
||||
@@ -22,6 +22,7 @@
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "mozilla/WinHeaderOnlyUtils.h"
|
||||
#include "nsWindowsHelpers.h"
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ index b40e0fceb567c0d217adf284e13f434e49cc8467..2c4e6d5fbf8da40954ad6a5b15e41249
|
|||
#include <windows.h>
|
||||
#include <processthreadsapi.h>
|
||||
|
||||
@@ -421,8 +422,18 @@ Maybe<int> LauncherMain(int& argc, wchar_t* argv[],
|
||||
@@ -422,8 +423,18 @@ Maybe<int> LauncherMain(int& argc, wchar_t* argv[],
|
||||
HANDLE stdHandles[] = {::GetStdHandle(STD_INPUT_HANDLE),
|
||||
::GetStdHandle(STD_OUTPUT_HANDLE),
|
||||
::GetStdHandle(STD_ERROR_HANDLE)};
|
||||
|
|
@ -167,7 +167,7 @@ index d49c6fbf1bf83b832795fa674f6b41f223eef812..7ea3540947ff5f61b15f27fbf4b95564
|
|||
const transportProvider = {
|
||||
setListener(upgradeListener) {
|
||||
diff --git a/docshell/base/BrowsingContext.cpp b/docshell/base/BrowsingContext.cpp
|
||||
index db5b5b990727aefcbaa47f89e0f53f4048e60038..bcd2321f46d9bca719fc530054984a2163c21f86 100644
|
||||
index e1721f31d491aa8a7977eaca3d2f7f8a048546de..b3bc2d575dc3f794cbc08c603e70d34bbe69efed 100644
|
||||
--- a/docshell/base/BrowsingContext.cpp
|
||||
+++ b/docshell/base/BrowsingContext.cpp
|
||||
@@ -106,8 +106,15 @@ struct ParamTraits<mozilla::dom::DisplayMode>
|
||||
|
|
@ -188,7 +188,7 @@ index db5b5b990727aefcbaa47f89e0f53f4048e60038..bcd2321f46d9bca719fc530054984a21
|
|||
|
||||
template <>
|
||||
struct ParamTraits<mozilla::dom::ExplicitActiveStatus>
|
||||
@@ -2807,6 +2814,40 @@ void BrowsingContext::DidSet(FieldIndex<IDX_PrefersColorSchemeOverride>,
|
||||
@@ -2818,6 +2825,40 @@ void BrowsingContext::DidSet(FieldIndex<IDX_PrefersColorSchemeOverride>,
|
||||
PresContextAffectingFieldChanged();
|
||||
}
|
||||
|
||||
|
|
@ -297,7 +297,7 @@ index 61135ab0d7894c500c3c5d80d107e283c01b6830..cc8eb043f1f78214843ec7b335dd9932
|
|||
|
||||
bool CanSet(FieldIndex<IDX_SuspendMediaWhenInactive>, bool, ContentParent*) {
|
||||
diff --git a/docshell/base/CanonicalBrowsingContext.cpp b/docshell/base/CanonicalBrowsingContext.cpp
|
||||
index 18b2bde3da2b1e17938fddda486b1bc4ddcf0e79..793a3d002b10298f7a19a2eae4d377f6f022fd36 100644
|
||||
index f0d8cb25398472d8720fcacc47081d95d3e9887c..a680d4458360c8515712ef0a986415113ae8a4e0 100644
|
||||
--- a/docshell/base/CanonicalBrowsingContext.cpp
|
||||
+++ b/docshell/base/CanonicalBrowsingContext.cpp
|
||||
@@ -324,6 +324,8 @@ void CanonicalBrowsingContext::ReplacedBy(
|
||||
|
|
@ -323,7 +323,7 @@ index 18b2bde3da2b1e17938fddda486b1bc4ddcf0e79..793a3d002b10298f7a19a2eae4d377f6
|
|||
}
|
||||
|
||||
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
|
||||
index 60cbd5d5b8d202fc30d5ac931ac66030bade65e7..f552a695880c5838c89ce918f61d051577cc5598 100644
|
||||
index c15a424a05d23287ee21726a5fb21ff5691e4c2b..fa9989e313bbb7bf049ce1519733c4032e9f9b4b 100644
|
||||
--- a/docshell/base/nsDocShell.cpp
|
||||
+++ b/docshell/base/nsDocShell.cpp
|
||||
@@ -15,6 +15,12 @@
|
||||
|
|
@ -609,7 +609,7 @@ index 60cbd5d5b8d202fc30d5ac931ac66030bade65e7..f552a695880c5838c89ce918f61d0515
|
|||
if (RefPtr<PresShell> presShell = GetPresShell()) {
|
||||
presShell->ActivenessMaybeChanged();
|
||||
}
|
||||
@@ -6681,6 +6906,10 @@ bool nsDocShell::CanSavePresentation(uint32_t aLoadType,
|
||||
@@ -6688,6 +6913,10 @@ bool nsDocShell::CanSavePresentation(uint32_t aLoadType,
|
||||
return false; // no entry to save into
|
||||
}
|
||||
|
||||
|
|
@ -620,7 +620,7 @@ index 60cbd5d5b8d202fc30d5ac931ac66030bade65e7..f552a695880c5838c89ce918f61d0515
|
|||
MOZ_ASSERT(!mozilla::SessionHistoryInParent(),
|
||||
"mOSHE cannot be non-null with SHIP");
|
||||
nsCOMPtr<nsIDocumentViewer> viewer = mOSHE->GetDocumentViewer();
|
||||
@@ -8413,6 +8642,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) {
|
||||
@@ -8420,6 +8649,12 @@ nsresult nsDocShell::PerformRetargeting(nsDocShellLoadState* aLoadState) {
|
||||
true, // aForceNoOpener
|
||||
getter_AddRefs(newBC));
|
||||
MOZ_ASSERT(!newBC);
|
||||
|
|
@ -633,7 +633,7 @@ index 60cbd5d5b8d202fc30d5ac931ac66030bade65e7..f552a695880c5838c89ce918f61d0515
|
|||
return rv;
|
||||
}
|
||||
|
||||
@@ -9549,6 +9784,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
|
||||
@@ -9556,6 +9791,16 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
|
||||
nsINetworkPredictor::PREDICT_LOAD, attrs, nullptr);
|
||||
|
||||
nsCOMPtr<nsIRequest> req;
|
||||
|
|
@ -650,7 +650,7 @@ index 60cbd5d5b8d202fc30d5ac931ac66030bade65e7..f552a695880c5838c89ce918f61d0515
|
|||
rv = DoURILoad(aLoadState, aCacheKey, getter_AddRefs(req));
|
||||
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
@@ -12747,6 +12992,9 @@ class OnLinkClickEvent : public Runnable {
|
||||
@@ -12754,6 +12999,9 @@ class OnLinkClickEvent : public Runnable {
|
||||
mHandler->OnLinkClickSync(mContent, mLoadState, mNoOpenerImplied,
|
||||
mTriggeringPrincipal);
|
||||
}
|
||||
|
|
@ -660,7 +660,7 @@ index 60cbd5d5b8d202fc30d5ac931ac66030bade65e7..f552a695880c5838c89ce918f61d0515
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
@@ -12836,6 +13084,8 @@ nsresult nsDocShell::OnLinkClick(
|
||||
@@ -12843,6 +13091,8 @@ nsresult nsDocShell::OnLinkClick(
|
||||
nsCOMPtr<nsIRunnable> ev =
|
||||
new OnLinkClickEvent(this, aContent, loadState, noOpenerImplied,
|
||||
aIsTrusted, aTriggeringPrincipal);
|
||||
|
|
@ -781,10 +781,10 @@ index fdc04f16c6f547077ad8c872f9357d85d4513c50..199f8fdb0670265c715f99f5cac1a2b2
|
|||
* This attempts to save any applicable layout history state (like
|
||||
* scroll position) in the nsISHEntry. This is normally done
|
||||
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
|
||||
index 235e2fcfccda18b4e923d1c1b02b5e1d9b02b089..e81abc3e18d82fa235a69911eb117ad0dcf54fd2 100644
|
||||
index 79f3524037e954eb693e2882d91a7632e6e1df41..2b75a1eaff4d166f68ca4a943e10cf9c6ab28bbf 100644
|
||||
--- a/dom/base/Document.cpp
|
||||
+++ b/dom/base/Document.cpp
|
||||
@@ -3757,6 +3757,9 @@ void Document::SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages) {
|
||||
@@ -3783,6 +3783,9 @@ void Document::SendToConsole(nsCOMArray<nsISecurityConsoleMessage>& aMessages) {
|
||||
}
|
||||
|
||||
void Document::ApplySettingsFromCSP(bool aSpeculative) {
|
||||
|
|
@ -794,7 +794,7 @@ index 235e2fcfccda18b4e923d1c1b02b5e1d9b02b089..e81abc3e18d82fa235a69911eb117ad0
|
|||
nsresult rv = NS_OK;
|
||||
if (!aSpeculative) {
|
||||
// 1) apply settings from regular CSP
|
||||
@@ -3814,6 +3817,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) {
|
||||
@@ -3840,6 +3843,11 @@ nsresult Document::InitCSP(nsIChannel* aChannel) {
|
||||
MOZ_ASSERT(!mScriptGlobalObject,
|
||||
"CSP must be initialized before mScriptGlobalObject is set!");
|
||||
|
||||
|
|
@ -806,7 +806,7 @@ index 235e2fcfccda18b4e923d1c1b02b5e1d9b02b089..e81abc3e18d82fa235a69911eb117ad0
|
|||
// If this is a data document - no need to set CSP.
|
||||
if (mLoadedAsData) {
|
||||
return NS_OK;
|
||||
@@ -4613,6 +4621,10 @@ bool Document::HasFocus(ErrorResult& rv) const {
|
||||
@@ -4641,6 +4649,10 @@ bool Document::HasFocus(ErrorResult& rv) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -817,7 +817,7 @@ index 235e2fcfccda18b4e923d1c1b02b5e1d9b02b089..e81abc3e18d82fa235a69911eb117ad0
|
|||
if (!fm->IsInActiveWindow(bc)) {
|
||||
return false;
|
||||
}
|
||||
@@ -19080,6 +19092,66 @@ ColorScheme Document::PreferredColorScheme(IgnoreRFP aIgnoreRFP) const {
|
||||
@@ -19139,6 +19151,66 @@ ColorScheme Document::PreferredColorScheme(IgnoreRFP aIgnoreRFP) const {
|
||||
return PreferenceSheet::PrefsFor(*this).mColorScheme;
|
||||
}
|
||||
|
||||
|
|
@ -885,10 +885,10 @@ index 235e2fcfccda18b4e923d1c1b02b5e1d9b02b089..e81abc3e18d82fa235a69911eb117ad0
|
|||
if (!sLoadingForegroundTopLevelContentDocument) {
|
||||
return false;
|
||||
diff --git a/dom/base/Document.h b/dom/base/Document.h
|
||||
index 0021e452414f9b7dc7b32a1065a82986d12dfdd7..2325b7d65bc1fb98b1dce994724c8e75c902834e 100644
|
||||
index 7a8d8f2a716fc613c4095eaf1a18017887b9b924..e030e6b7ad63ad7c95227ed8f54e946190a638d8 100644
|
||||
--- a/dom/base/Document.h
|
||||
+++ b/dom/base/Document.h
|
||||
@@ -4053,6 +4053,9 @@ class Document : public nsINode,
|
||||
@@ -4077,6 +4077,9 @@ class Document : public nsINode,
|
||||
// color-scheme meta tag.
|
||||
ColorScheme DefaultColorScheme() const;
|
||||
|
||||
|
|
@ -962,10 +962,10 @@ index 6abf6cef230c97815f17f6b7abf9f1b1de274a6f..46ead1f32e0d710b5b32e61dff72a4f7
|
|||
dom::MediaCapabilities* MediaCapabilities();
|
||||
dom::MediaSession* MediaSession();
|
||||
diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp
|
||||
index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a69257799fec 100644
|
||||
index 8518005d2938d35da7681c1b4230cacbe8fbaf03..e501e7e3351b6f5bdd07020dea658b9f8508b126 100644
|
||||
--- a/dom/base/nsContentUtils.cpp
|
||||
+++ b/dom/base/nsContentUtils.cpp
|
||||
@@ -8829,7 +8829,8 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
@@ -8809,7 +8809,8 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
bool aIgnoreRootScrollFrame, float aPressure,
|
||||
unsigned short aInputSourceArg, uint32_t aIdentifier, bool aToWindow,
|
||||
PreventDefaultResult* aPreventDefault, bool aIsDOMEventSynthesized,
|
||||
|
|
@ -975,7 +975,7 @@ index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a692
|
|||
nsPoint offset;
|
||||
nsCOMPtr<nsIWidget> widget = GetWidget(aPresShell, &offset);
|
||||
if (!widget) return NS_ERROR_FAILURE;
|
||||
@@ -8837,6 +8838,7 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
@@ -8817,6 +8818,7 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
EventMessage msg;
|
||||
Maybe<WidgetMouseEvent::ExitFrom> exitFrom;
|
||||
bool contextMenuKey = false;
|
||||
|
|
@ -983,7 +983,7 @@ index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a692
|
|||
if (aType.EqualsLiteral("mousedown")) {
|
||||
msg = eMouseDown;
|
||||
} else if (aType.EqualsLiteral("mouseup")) {
|
||||
@@ -8861,6 +8863,12 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
@@ -8841,6 +8843,12 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
msg = eMouseHitTest;
|
||||
} else if (aType.EqualsLiteral("MozMouseExploreByTouch")) {
|
||||
msg = eMouseExploreByTouch;
|
||||
|
|
@ -996,7 +996,7 @@ index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a692
|
|||
} else {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
@@ -8871,7 +8879,14 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
@@ -8851,7 +8859,14 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
|
||||
Maybe<WidgetPointerEvent> pointerEvent;
|
||||
Maybe<WidgetMouseEvent> mouseEvent;
|
||||
|
|
@ -1012,7 +1012,7 @@ index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a692
|
|||
MOZ_ASSERT(!aIsWidgetEventSynthesized,
|
||||
"The event shouldn't be dispatched as a synthesized event");
|
||||
if (MOZ_UNLIKELY(aIsWidgetEventSynthesized)) {
|
||||
@@ -8890,8 +8905,11 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
@@ -8870,8 +8885,11 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
contextMenuKey ? WidgetMouseEvent::eContextMenuKey
|
||||
: WidgetMouseEvent::eNormal);
|
||||
}
|
||||
|
|
@ -1024,7 +1024,7 @@ index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a692
|
|||
mouseOrPointerEvent.pointerId = aIdentifier;
|
||||
mouseOrPointerEvent.mModifiers = GetWidgetModifiers(aModifiers);
|
||||
mouseOrPointerEvent.mButton = aButton;
|
||||
@@ -8904,6 +8922,8 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
@@ -8884,6 +8902,8 @@ nsresult nsContentUtils::SendMouseEvent(
|
||||
mouseOrPointerEvent.mClickCount = aClickCount;
|
||||
mouseOrPointerEvent.mFlags.mIsSynthesizedForTests = aIsDOMEventSynthesized;
|
||||
mouseOrPointerEvent.mExitFrom = exitFrom;
|
||||
|
|
@ -1034,10 +1034,10 @@ index 7b7deca251cf20fa4896e63e32d17303dd603263..151dd519433de858673dc1620094a692
|
|||
nsPresContext* presContext = aPresShell->GetPresContext();
|
||||
if (!presContext) return NS_ERROR_FAILURE;
|
||||
diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h
|
||||
index 3837cce20cfb7cc3c5a93e7b595dee632739de5c..81ccfbe139e7041eb862ab3b085f1dae76bf0a5c 100644
|
||||
index b4b2244ddfbe43efa055788297a103c49989d921..2d22cdf8b25d9ce0e0daabb09f315d61a214a2be 100644
|
||||
--- a/dom/base/nsContentUtils.h
|
||||
+++ b/dom/base/nsContentUtils.h
|
||||
@@ -3093,7 +3093,8 @@ class nsContentUtils {
|
||||
@@ -3047,7 +3047,8 @@ class nsContentUtils {
|
||||
int32_t aModifiers, bool aIgnoreRootScrollFrame, float aPressure,
|
||||
unsigned short aInputSourceArg, uint32_t aIdentifier, bool aToWindow,
|
||||
mozilla::PreventDefaultResult* aPreventDefault,
|
||||
|
|
@ -1048,10 +1048,10 @@ index 3837cce20cfb7cc3c5a93e7b595dee632739de5c..81ccfbe139e7041eb862ab3b085f1dae
|
|||
static void FirePageShowEventForFrameLoaderSwap(
|
||||
nsIDocShellTreeItem* aItem,
|
||||
diff --git a/dom/base/nsDOMWindowUtils.cpp b/dom/base/nsDOMWindowUtils.cpp
|
||||
index e2de2b30c094e30db4d33e6cf8e5fbf83f219876..f937f561c0524e04563129f2cb762ae4127e6462 100644
|
||||
index c77bf80d5e1fc6db342ab47e85b7950f8a15a2d8..2f61c71cdb82b73c1de1a357315d9243a0b8c639 100644
|
||||
--- a/dom/base/nsDOMWindowUtils.cpp
|
||||
+++ b/dom/base/nsDOMWindowUtils.cpp
|
||||
@@ -684,6 +684,26 @@ nsDOMWindowUtils::GetPresShellId(uint32_t* aPresShellId) {
|
||||
@@ -685,6 +685,26 @@ nsDOMWindowUtils::GetPresShellId(uint32_t* aPresShellId) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
|
|
@ -1078,7 +1078,7 @@ index e2de2b30c094e30db4d33e6cf8e5fbf83f219876..f937f561c0524e04563129f2cb762ae4
|
|||
NS_IMETHODIMP
|
||||
nsDOMWindowUtils::SendMouseEvent(
|
||||
const nsAString& aType, float aX, float aY, int32_t aButton,
|
||||
@@ -698,7 +718,7 @@ nsDOMWindowUtils::SendMouseEvent(
|
||||
@@ -699,7 +719,7 @@ nsDOMWindowUtils::SendMouseEvent(
|
||||
aOptionalArgCount >= 7 ? aIdentifier : DEFAULT_MOUSE_POINTER_ID, false,
|
||||
aPreventDefault, aOptionalArgCount >= 4 ? aIsDOMEventSynthesized : true,
|
||||
aOptionalArgCount >= 5 ? aIsWidgetEventSynthesized : false,
|
||||
|
|
@ -1087,7 +1087,7 @@ index e2de2b30c094e30db4d33e6cf8e5fbf83f219876..f937f561c0524e04563129f2cb762ae4
|
|||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
@@ -716,7 +736,7 @@ nsDOMWindowUtils::SendMouseEventToWindow(
|
||||
@@ -717,7 +737,7 @@ nsDOMWindowUtils::SendMouseEventToWindow(
|
||||
aOptionalArgCount >= 7 ? aIdentifier : DEFAULT_MOUSE_POINTER_ID, true,
|
||||
nullptr, aOptionalArgCount >= 4 ? aIsDOMEventSynthesized : true,
|
||||
aOptionalArgCount >= 5 ? aIsWidgetEventSynthesized : false,
|
||||
|
|
@ -1096,7 +1096,7 @@ index e2de2b30c094e30db4d33e6cf8e5fbf83f219876..f937f561c0524e04563129f2cb762ae4
|
|||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
@@ -725,13 +745,13 @@ nsDOMWindowUtils::SendMouseEventCommon(
|
||||
@@ -726,13 +746,13 @@ nsDOMWindowUtils::SendMouseEventCommon(
|
||||
int32_t aClickCount, int32_t aModifiers, bool aIgnoreRootScrollFrame,
|
||||
float aPressure, unsigned short aInputSourceArg, uint32_t aPointerId,
|
||||
bool aToWindow, bool* aPreventDefault, bool aIsDOMEventSynthesized,
|
||||
|
|
@ -1126,10 +1126,10 @@ index 47ff326b202266b1d7d6af8bdfb72776df8a6a93..b8e084b0c788c46345b1455b8257f171
|
|||
MOZ_CAN_RUN_SCRIPT
|
||||
nsresult SendTouchEventCommon(
|
||||
diff --git a/dom/base/nsFocusManager.cpp b/dom/base/nsFocusManager.cpp
|
||||
index 22c175c93ef7bc81640b0ad71bd6ca9c1082fea6..7d77e91afbfe7aebe0c94793c2e0606715e3acdb 100644
|
||||
index cbd5cb8e4525454cac0470a14bdc63d45bf53b9a..a73297f3faafe5895453f0a6996aa30a77a97267 100644
|
||||
--- a/dom/base/nsFocusManager.cpp
|
||||
+++ b/dom/base/nsFocusManager.cpp
|
||||
@@ -1684,6 +1684,10 @@ Maybe<uint64_t> nsFocusManager::SetFocusInner(Element* aNewContent,
|
||||
@@ -1697,6 +1697,10 @@ Maybe<uint64_t> nsFocusManager::SetFocusInner(Element* aNewContent,
|
||||
(GetActiveBrowsingContext() == newRootBrowsingContext);
|
||||
}
|
||||
|
||||
|
|
@ -1140,7 +1140,7 @@ index 22c175c93ef7bc81640b0ad71bd6ca9c1082fea6..7d77e91afbfe7aebe0c94793c2e06067
|
|||
// Exit fullscreen if a website focuses another window
|
||||
if (StaticPrefs::full_screen_api_exit_on_windowRaise() &&
|
||||
!isElementInActiveWindow && (aFlags & FLAG_RAISE)) {
|
||||
@@ -2269,6 +2273,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear,
|
||||
@@ -2282,6 +2286,7 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear,
|
||||
bool aIsLeavingDocument, bool aAdjustWidget,
|
||||
bool aRemainActive, Element* aElementToFocus,
|
||||
uint64_t aActionId) {
|
||||
|
|
@ -1148,7 +1148,7 @@ index 22c175c93ef7bc81640b0ad71bd6ca9c1082fea6..7d77e91afbfe7aebe0c94793c2e06067
|
|||
LOGFOCUS(("<<Blur begin actionid: %" PRIu64 ">>", aActionId));
|
||||
|
||||
// hold a reference to the focused content, which may be null
|
||||
@@ -2315,6 +2320,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear,
|
||||
@@ -2328,6 +2333,11 @@ bool nsFocusManager::BlurImpl(BrowsingContext* aBrowsingContextToClear,
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1160,7 +1160,7 @@ index 22c175c93ef7bc81640b0ad71bd6ca9c1082fea6..7d77e91afbfe7aebe0c94793c2e06067
|
|||
// Keep a ref to presShell since dispatching the DOM event may cause
|
||||
// the document to be destroyed.
|
||||
RefPtr<PresShell> presShell = docShell->GetPresShell();
|
||||
@@ -2992,7 +3002,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow,
|
||||
@@ -3005,7 +3015,9 @@ void nsFocusManager::RaiseWindow(nsPIDOMWindowOuter* aWindow,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1172,10 +1172,10 @@ index 22c175c93ef7bc81640b0ad71bd6ca9c1082fea6..7d77e91afbfe7aebe0c94793c2e06067
|
|||
// care of lowering the present active window. This happens in
|
||||
// a separate runnable to avoid touching multiple windows in
|
||||
diff --git a/dom/base/nsGlobalWindowOuter.cpp b/dom/base/nsGlobalWindowOuter.cpp
|
||||
index e47d4979078343102f00e93df913ff778b841804..360ab27a8f3394d18b558de80b5d0bbb963c1391 100644
|
||||
index f2aa07e2c1e6df28e165b1868ad9717248360972..2b1b406c4fdf6d0716b9c29c3e640de210eae749 100644
|
||||
--- a/dom/base/nsGlobalWindowOuter.cpp
|
||||
+++ b/dom/base/nsGlobalWindowOuter.cpp
|
||||
@@ -2514,10 +2514,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument,
|
||||
@@ -2516,10 +2516,16 @@ nsresult nsGlobalWindowOuter::SetNewDocument(Document* aDocument,
|
||||
}();
|
||||
|
||||
if (!isContentAboutBlankInChromeDocshell) {
|
||||
|
|
@ -1196,7 +1196,7 @@ index e47d4979078343102f00e93df913ff778b841804..360ab27a8f3394d18b558de80b5d0bbb
|
|||
}
|
||||
}
|
||||
|
||||
@@ -2637,6 +2643,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() {
|
||||
@@ -2639,6 +2645,19 @@ void nsGlobalWindowOuter::DispatchDOMWindowCreated() {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1217,10 +1217,10 @@ index e47d4979078343102f00e93df913ff778b841804..360ab27a8f3394d18b558de80b5d0bbb
|
|||
|
||||
void nsGlobalWindowOuter::SetDocShell(nsDocShell* aDocShell) {
|
||||
diff --git a/dom/base/nsGlobalWindowOuter.h b/dom/base/nsGlobalWindowOuter.h
|
||||
index 0039d6d91b23953afbd6aec2b4d1f064db3c3b1c..7a6c5da16651d34ea60c69331365d94886da1993 100644
|
||||
index e2a2b560b565e6eb3cd5b4e77eb30051afe7a418..81eaca3fb0acfe90bf07acb4115a0db0786c6998 100644
|
||||
--- a/dom/base/nsGlobalWindowOuter.h
|
||||
+++ b/dom/base/nsGlobalWindowOuter.h
|
||||
@@ -314,6 +314,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget,
|
||||
@@ -317,6 +317,7 @@ class nsGlobalWindowOuter final : public mozilla::dom::EventTarget,
|
||||
|
||||
// Outer windows only.
|
||||
void DispatchDOMWindowCreated();
|
||||
|
|
@ -1229,7 +1229,7 @@ index 0039d6d91b23953afbd6aec2b4d1f064db3c3b1c..7a6c5da16651d34ea60c69331365d948
|
|||
// Outer windows only.
|
||||
virtual void EnsureSizeAndPositionUpToDate() override;
|
||||
diff --git a/dom/base/nsINode.cpp b/dom/base/nsINode.cpp
|
||||
index 4b54dcd5b4fc9c575552ae82d5ed66f313cdeb72..e75b5f148d55d8f7d7e098a84930fec0e28aa01d 100644
|
||||
index 091d04dd79da3acc33aa22405ddb984ba486dfe2..40bb124fd735c2d214221c1a5fac442e26f7564e 100644
|
||||
--- a/dom/base/nsINode.cpp
|
||||
+++ b/dom/base/nsINode.cpp
|
||||
@@ -1402,6 +1402,61 @@ void nsINode::GetBoxQuadsFromWindowOrigin(const BoxQuadOptions& aOptions,
|
||||
|
|
@ -1295,10 +1295,10 @@ index 4b54dcd5b4fc9c575552ae82d5ed66f313cdeb72..e75b5f148d55d8f7d7e098a84930fec0
|
|||
DOMQuad& aQuad, const GeometryNode& aFrom,
|
||||
const ConvertCoordinateOptions& aOptions, CallerType aCallerType,
|
||||
diff --git a/dom/base/nsINode.h b/dom/base/nsINode.h
|
||||
index 6f980f472aefe147de47212717ca300e62e02952..3d60daf88196ed502fc647cc7b51d2eb70a281ef 100644
|
||||
index 3bc7ff8a3d9e7f3148f51da13f34ea1b3ca2ba77..dcb47740ca93ab237e4f55d4225f77283f5f404b 100644
|
||||
--- a/dom/base/nsINode.h
|
||||
+++ b/dom/base/nsINode.h
|
||||
@@ -2303,6 +2303,10 @@ class nsINode : public mozilla::dom::EventTarget {
|
||||
@@ -2317,6 +2317,10 @@ class nsINode : public mozilla::dom::EventTarget {
|
||||
nsTArray<RefPtr<DOMQuad>>& aResult,
|
||||
ErrorResult& aRv);
|
||||
|
||||
|
|
@ -1310,10 +1310,10 @@ index 6f980f472aefe147de47212717ca300e62e02952..3d60daf88196ed502fc647cc7b51d2eb
|
|||
DOMQuad& aQuad, const TextOrElementOrDocument& aFrom,
|
||||
const ConvertCoordinateOptions& aOptions, CallerType aCallerType,
|
||||
diff --git a/dom/base/nsJSUtils.cpp b/dom/base/nsJSUtils.cpp
|
||||
index cf8037cd580013efe5eb578c43f45c0d21946c6a..583460796fdef633e8075013597f7c315ce4ab06 100644
|
||||
index 48df3ae2d30b975269d06e6354b143abd3e5fcd8..87c8d237355668b0ff324f49be879219b1761083 100644
|
||||
--- a/dom/base/nsJSUtils.cpp
|
||||
+++ b/dom/base/nsJSUtils.cpp
|
||||
@@ -177,6 +177,11 @@ bool nsJSUtils::GetScopeChainForElement(
|
||||
@@ -149,6 +149,11 @@ bool nsJSUtils::GetScopeChainForElement(
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -1326,10 +1326,10 @@ index cf8037cd580013efe5eb578c43f45c0d21946c6a..583460796fdef633e8075013597f7c31
|
|||
void nsJSUtils::ResetTimeZone() { JS::ResetTimeZone(); }
|
||||
|
||||
diff --git a/dom/base/nsJSUtils.h b/dom/base/nsJSUtils.h
|
||||
index cceb725d393d5e5f83c8f87491089c3fa1d57cc3..e906a7fb7c3fd72554613f640dcc272e6984d929 100644
|
||||
index 8b4c1492c64884d83eb1553bc40b921e0da601b7..ee66eaa21d8e8c208204ef73fca5b3d78abefb24 100644
|
||||
--- a/dom/base/nsJSUtils.h
|
||||
+++ b/dom/base/nsJSUtils.h
|
||||
@@ -79,6 +79,7 @@ class nsJSUtils {
|
||||
@@ -71,6 +71,7 @@ class nsJSUtils {
|
||||
JSContext* aCx, mozilla::dom::Element* aElement,
|
||||
JS::MutableHandleVector<JSObject*> aScopeChain);
|
||||
|
||||
|
|
@ -1479,18 +1479,18 @@ index 7e1af00d05fbafa2d828e2c7e4dcc5c82d115f5b..e85af9718d064e4d2865bc944e9d4ba1
|
|||
~Geolocation();
|
||||
|
||||
diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp
|
||||
index e2a77a11435a80abbb6381ffabbb5711eca0ac0d..a614efef052ca7c39457726d1f1e66f7cff777f8 100644
|
||||
index d40c2a230c8c86f585935061d05e20b405c906fe..29547e7a0d75fdc8b8b30344db32287424e65fba 100644
|
||||
--- a/dom/html/HTMLInputElement.cpp
|
||||
+++ b/dom/html/HTMLInputElement.cpp
|
||||
@@ -59,6 +59,7 @@
|
||||
@@ -60,6 +60,7 @@
|
||||
#include "mozilla/dom/Document.h"
|
||||
#include "mozilla/dom/HTMLDataListElement.h"
|
||||
#include "mozilla/dom/HTMLOptionElement.h"
|
||||
+#include "nsDocShell.h"
|
||||
#include "nsIFormControlFrame.h"
|
||||
#include "nsITextControlFrame.h"
|
||||
#include "nsIFrame.h"
|
||||
@@ -784,6 +785,13 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
|
||||
#include "nsRangeFrame.h"
|
||||
#include "nsError.h"
|
||||
@@ -783,6 +784,13 @@ nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) {
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
|
||||
|
|
@ -1505,7 +1505,7 @@ index e2a77a11435a80abbb6381ffabbb5711eca0ac0d..a614efef052ca7c39457726d1f1e66f7
|
|||
return NS_OK;
|
||||
}
|
||||
diff --git a/dom/interfaces/base/nsIDOMWindowUtils.idl b/dom/interfaces/base/nsIDOMWindowUtils.idl
|
||||
index ac0251b4989799e9bb370a8066d10f13154bbc7c..184f4d980c35652c67da06e917e9d0b85ff34cea 100644
|
||||
index 89202fa1ff22593e7cb5e20fc40b3b3b8e114449..61ed40c8454c6e85876cbc7c240496cc96f77239 100644
|
||||
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
|
||||
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
|
||||
@@ -374,6 +374,26 @@ interface nsIDOMWindowUtils : nsISupports {
|
||||
|
|
@ -1536,7 +1536,7 @@ index ac0251b4989799e9bb370a8066d10f13154bbc7c..184f4d980c35652c67da06e917e9d0b8
|
|||
* touchstart, touchend, touchmove, and touchcancel
|
||||
*
|
||||
diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp
|
||||
index 204ee71ece1afa8b416caafcb4bdd242344f1a26..8597f2d0c4bd7a6fbfed9f29d002d0c59c8f8ae9 100644
|
||||
index 0335a887fe157210f9eef58bf63be879f7d5de2b..dfbb8dae406f9d9276a2719f515ac5a51f8a671d 100644
|
||||
--- a/dom/ipc/BrowserChild.cpp
|
||||
+++ b/dom/ipc/BrowserChild.cpp
|
||||
@@ -1656,6 +1656,21 @@ void BrowserChild::HandleRealMouseButtonEvent(const WidgetMouseEvent& aEvent,
|
||||
|
|
@ -1822,7 +1822,7 @@ index 3b39538e51840cd9b1685b2efd2ff2e9ec83608a..c7bf4f2d53b58bbacb22b3ebebf6f3fc
|
|||
|
||||
return aGlobalOrNull;
|
||||
diff --git a/dom/security/nsCSPUtils.cpp b/dom/security/nsCSPUtils.cpp
|
||||
index 4eafb2247d5aa8e989c0359d6d9d864edf73759d..e0d0b5bc78537c6fa8d0cf02cc6c772993008b91 100644
|
||||
index ff2e907c0d8fc05c6e39fae612eceed405b62712..40ec25b5588a1628f9d9bd16886bed83dad49b90 100644
|
||||
--- a/dom/security/nsCSPUtils.cpp
|
||||
+++ b/dom/security/nsCSPUtils.cpp
|
||||
@@ -22,6 +22,7 @@
|
||||
|
|
@ -1869,10 +1869,10 @@ index 2f71b284ee5f7e11f117c447834b48355784448c..2640bd57123c2b03bf4b06a2419cd020
|
|||
* returned quads are further translated relative to the window
|
||||
* origin -- which is not the layout origin. Further translation
|
||||
diff --git a/dom/workers/RuntimeService.cpp b/dom/workers/RuntimeService.cpp
|
||||
index 6085248083194be05e85c3be7f0e69fd1928bf3d..23b72e2d0030496d5b05c88f06ed1a30ed33396b 100644
|
||||
index 1ba2051ed316956a5a71f85ed5fa0735d54716e5..c0d6f45ce14040a79cfe134a4f8254434a4c53cc 100644
|
||||
--- a/dom/workers/RuntimeService.cpp
|
||||
+++ b/dom/workers/RuntimeService.cpp
|
||||
@@ -998,7 +998,7 @@ void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) {
|
||||
@@ -1007,7 +1007,7 @@ void PrefLanguagesChanged(const char* /* aPrefName */, void* /* aClosure */) {
|
||||
AssertIsOnMainThread();
|
||||
|
||||
nsTArray<nsString> languages;
|
||||
|
|
@ -1881,7 +1881,7 @@ index 6085248083194be05e85c3be7f0e69fd1928bf3d..23b72e2d0030496d5b05c88f06ed1a30
|
|||
|
||||
RuntimeService* runtime = RuntimeService::GetService();
|
||||
if (runtime) {
|
||||
@@ -1185,8 +1185,7 @@ bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) {
|
||||
@@ -1194,8 +1194,7 @@ bool RuntimeService::RegisterWorker(WorkerPrivate& aWorkerPrivate) {
|
||||
}
|
||||
|
||||
// The navigator overridden properties should have already been read.
|
||||
|
|
@ -1891,7 +1891,7 @@ index 6085248083194be05e85c3be7f0e69fd1928bf3d..23b72e2d0030496d5b05c88f06ed1a30
|
|||
mNavigatorPropertiesLoaded = true;
|
||||
}
|
||||
|
||||
@@ -1808,6 +1807,13 @@ void RuntimeService::PropagateStorageAccessPermissionGranted(
|
||||
@@ -1817,6 +1816,13 @@ void RuntimeService::PropagateStorageAccessPermissionGranted(
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1905,7 +1905,7 @@ index 6085248083194be05e85c3be7f0e69fd1928bf3d..23b72e2d0030496d5b05c88f06ed1a30
|
|||
template <typename Func>
|
||||
void RuntimeService::BroadcastAllWorkers(const Func& aFunc) {
|
||||
AssertIsOnMainThread();
|
||||
@@ -2333,6 +2339,14 @@ void PropagateStorageAccessPermissionGrantedToWorkers(
|
||||
@@ -2342,6 +2348,14 @@ void PropagateStorageAccessPermissionGrantedToWorkers(
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1947,7 +1947,7 @@ index 58894a8361c7ef1dddd481ca5877a209a8b8ff5c..c481d40d79b6397b7f1d571bd9f6ae5c
|
|||
|
||||
bool IsWorkerGlobal(JSObject* global);
|
||||
diff --git a/dom/workers/WorkerPrivate.cpp b/dom/workers/WorkerPrivate.cpp
|
||||
index 089f42307becf7c6f81199d970fb8870db494818..63fb760ac831bc88415aee1cddf8b59662e55f37 100644
|
||||
index 2b48cc2980165ce51ded62faef96b93b781ede32..d8dc90983353c2f5cc1db56e327c4533d524cc1d 100644
|
||||
--- a/dom/workers/WorkerPrivate.cpp
|
||||
+++ b/dom/workers/WorkerPrivate.cpp
|
||||
@@ -700,6 +700,18 @@ class UpdateContextOptionsRunnable final : public WorkerControlRunnable {
|
||||
|
|
@ -1969,7 +1969,7 @@ index 089f42307becf7c6f81199d970fb8870db494818..63fb760ac831bc88415aee1cddf8b596
|
|||
class UpdateLanguagesRunnable final : public WorkerThreadRunnable {
|
||||
nsTArray<nsString> mLanguages;
|
||||
|
||||
@@ -2108,6 +2120,16 @@ void WorkerPrivate::UpdateContextOptions(
|
||||
@@ -2113,6 +2125,16 @@ void WorkerPrivate::UpdateContextOptions(
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1986,7 +1986,7 @@ index 089f42307becf7c6f81199d970fb8870db494818..63fb760ac831bc88415aee1cddf8b596
|
|||
void WorkerPrivate::UpdateLanguages(const nsTArray<nsString>& aLanguages) {
|
||||
AssertIsOnParentThread();
|
||||
|
||||
@@ -5736,6 +5758,15 @@ void WorkerPrivate::UpdateContextOptionsInternal(
|
||||
@@ -5740,6 +5762,15 @@ void WorkerPrivate::UpdateContextOptionsInternal(
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2003,7 +2003,7 @@ index 089f42307becf7c6f81199d970fb8870db494818..63fb760ac831bc88415aee1cddf8b596
|
|||
const nsTArray<nsString>& aLanguages) {
|
||||
WorkerGlobalScope* globalScope = GlobalScope();
|
||||
diff --git a/dom/workers/WorkerPrivate.h b/dom/workers/WorkerPrivate.h
|
||||
index dfb96b7b798785d7b75c683bc0969e39487137a3..a463eec618af51fdbc25db509870598846c0fd66 100644
|
||||
index da25a495a8930f7b88958553164d86fe2869416f..38f92829438ead78d73d96ee48a3fcab46c337d7 100644
|
||||
--- a/dom/workers/WorkerPrivate.h
|
||||
+++ b/dom/workers/WorkerPrivate.h
|
||||
@@ -432,6 +432,8 @@ class WorkerPrivate final
|
||||
|
|
@ -2015,7 +2015,7 @@ index dfb96b7b798785d7b75c683bc0969e39487137a3..a463eec618af51fdbc25db5098705988
|
|||
void UpdateLanguagesInternal(const nsTArray<nsString>& aLanguages);
|
||||
|
||||
void UpdateJSWorkerMemoryParameterInternal(JSContext* aCx, JSGCParamKey key,
|
||||
@@ -1059,6 +1061,8 @@ class WorkerPrivate final
|
||||
@@ -1069,6 +1071,8 @@ class WorkerPrivate final
|
||||
|
||||
void UpdateContextOptions(const JS::ContextOptions& aContextOptions);
|
||||
|
||||
|
|
@ -2247,10 +2247,10 @@ index 0ec6ee3eb37c6493d8a25352fd0e54e1927bceab..885dba71bc5815e5f6f3ec2700c376aa
|
|||
// No boxes to return
|
||||
return;
|
||||
diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp
|
||||
index f154e05a8c2e2ebf07565d087a42436feeb17f53..0af8c24c0f391c52fe2acfeb01cacb32358e6861 100644
|
||||
index 2cc3c5673e246ac38cdaddb457ce36ee4c7356ce..61093cd52fc049ad77d84dd837731071fdace69d 100644
|
||||
--- a/layout/base/PresShell.cpp
|
||||
+++ b/layout/base/PresShell.cpp
|
||||
@@ -11064,7 +11064,9 @@ bool PresShell::ComputeActiveness() const {
|
||||
@@ -11163,7 +11163,9 @@ bool PresShell::ComputeActiveness() const {
|
||||
if (!browserChild->IsVisible()) {
|
||||
MOZ_LOG(gLog, LogLevel::Debug,
|
||||
(" > BrowserChild %p is not visible", browserChild));
|
||||
|
|
@ -2262,7 +2262,7 @@ index f154e05a8c2e2ebf07565d087a42436feeb17f53..0af8c24c0f391c52fe2acfeb01cacb32
|
|||
|
||||
// If the browser is visible but just due to be preserving layers
|
||||
diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp
|
||||
index 0011b1a1a36da0dec7cc6afa6fd689a4c8710d37..25bebd7b03b0b8dc595607bae07f360f3be3f284 100644
|
||||
index d8995d6d94f5861d8ed839613768eb269b44e362..b946de49a0ecab449a9d1aff24c38f69a431eec0 100644
|
||||
--- a/layout/base/nsLayoutUtils.cpp
|
||||
+++ b/layout/base/nsLayoutUtils.cpp
|
||||
@@ -698,6 +698,10 @@ bool nsLayoutUtils::AllowZoomingForDocument(
|
||||
|
|
@ -2276,7 +2276,7 @@ index 0011b1a1a36da0dec7cc6afa6fd689a4c8710d37..25bebd7b03b0b8dc595607bae07f360f
|
|||
// True if we allow zooming for all documents on this platform, or if we are
|
||||
// in RDM.
|
||||
BrowsingContext* bc = aDocument->GetBrowsingContext();
|
||||
@@ -9791,6 +9795,9 @@ void nsLayoutUtils::ComputeSystemFont(nsFont* aSystemFont,
|
||||
@@ -9768,6 +9772,9 @@ void nsLayoutUtils::ComputeSystemFont(nsFont* aSystemFont,
|
||||
|
||||
/* static */
|
||||
bool nsLayoutUtils::ShouldHandleMetaViewport(const Document* aDocument) {
|
||||
|
|
@ -2287,10 +2287,10 @@ index 0011b1a1a36da0dec7cc6afa6fd689a4c8710d37..25bebd7b03b0b8dc595607bae07f360f
|
|||
return StaticPrefs::dom_meta_viewport_enabled() || (bc && bc->InRDMPane());
|
||||
}
|
||||
diff --git a/layout/style/GeckoBindings.h b/layout/style/GeckoBindings.h
|
||||
index d273793fc8d92b5c19ec0562730eab249cc41eb8..46b4078c6031318265a8338e01f52ab60bd9c0e8 100644
|
||||
index c18d38d8ad2f80bb0d3512d1a9ae965c594bb356..22736c86eb5e3d0a44563c312e34032c157f3abe 100644
|
||||
--- a/layout/style/GeckoBindings.h
|
||||
+++ b/layout/style/GeckoBindings.h
|
||||
@@ -596,6 +596,7 @@ float Gecko_MediaFeatures_GetResolution(const mozilla::dom::Document*);
|
||||
@@ -595,6 +595,7 @@ float Gecko_MediaFeatures_GetResolution(const mozilla::dom::Document*);
|
||||
bool Gecko_MediaFeatures_PrefersReducedMotion(const mozilla::dom::Document*);
|
||||
bool Gecko_MediaFeatures_PrefersReducedTransparency(
|
||||
const mozilla::dom::Document*);
|
||||
|
|
@ -2351,7 +2351,7 @@ index 21d5a5e1b4193d058c30268ab73c8d595436b381..11b960ec0ff3ea77857cb915d05bbdbb
|
|||
+
|
||||
} // namespace mozilla::net
|
||||
diff --git a/netwerk/base/LoadInfo.h b/netwerk/base/LoadInfo.h
|
||||
index 52d867196a459578cbea1a4f626afbe51dd1abd5..2904832cbcad476fdebb54c7e24d5f14b1c0fb4b 100644
|
||||
index 6ba1d8e11efbbf75f4a44d4977587429ad1371f8..d834f1f5528264f59c4547f00825e0a3b433d9dd 100644
|
||||
--- a/netwerk/base/LoadInfo.h
|
||||
+++ b/netwerk/base/LoadInfo.h
|
||||
@@ -413,9 +413,10 @@ class LoadInfo final : public nsILoadInfo {
|
||||
|
|
@ -2360,7 +2360,7 @@ index 52d867196a459578cbea1a4f626afbe51dd1abd5..2904832cbcad476fdebb54c7e24d5f14
|
|||
bool mWasSchemelessInput = false;
|
||||
-
|
||||
nsILoadInfo::HTTPSUpgradeTelemetryType mHttpsUpgradeTelemetry =
|
||||
nsILoadInfo::NO_UPGRADE;
|
||||
nsILoadInfo::NOT_INITIALIZED;
|
||||
+
|
||||
+ uint64_t mJugglerLoadIdentifier = 0;
|
||||
};
|
||||
|
|
@ -2387,10 +2387,10 @@ index 9dc2bb0da6871b905abd17d931e555429977c6c2..b71cf6393492346f16417b3ba745a235
|
|||
} // namespace net
|
||||
} // namespace mozilla
|
||||
diff --git a/netwerk/base/nsILoadInfo.idl b/netwerk/base/nsILoadInfo.idl
|
||||
index 12f43b911006d5b0bbfa9936070dc0d561bc7bb4..94d20cdca548534ad5e4ef4f937e287c58768870 100644
|
||||
index daccd1dc75fb1f6ba88c8f734e10c14cbdbffe8f..9621ca5dc05f12a8d81da787fa479fe03ea99e4c 100644
|
||||
--- a/netwerk/base/nsILoadInfo.idl
|
||||
+++ b/netwerk/base/nsILoadInfo.idl
|
||||
@@ -1586,4 +1586,5 @@ interface nsILoadInfo : nsISupports
|
||||
@@ -1590,4 +1590,5 @@ interface nsILoadInfo : nsISupports
|
||||
*/
|
||||
[infallible] attribute nsILoadInfo_HTTPSUpgradeTelemetryType httpsUpgradeTelemetry;
|
||||
|
||||
|
|
@ -2409,7 +2409,7 @@ index 7f91d2df6f8bb4020c75c132dc8f6bf26625fa1e..ba6569f4be8fc54ec96ee44d5de45a09
|
|||
/**
|
||||
* Set the status and reason for the forthcoming synthesized response.
|
||||
diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp
|
||||
index 10f65a549ce886bf7f19de02714482e28a8931a5..f41d32ce90f7345ad5a9bd90e420354865f35235 100644
|
||||
index ef946929c9bbd7903c8e3b32bcb373d5096aed52..a2814c5c891e2877aca9fdb4698385282b8743a9 100644
|
||||
--- a/netwerk/ipc/DocumentLoadListener.cpp
|
||||
+++ b/netwerk/ipc/DocumentLoadListener.cpp
|
||||
@@ -171,6 +171,7 @@ static auto CreateDocumentLoadInfo(CanonicalBrowsingContext* aBrowsingContext,
|
||||
|
|
@ -2459,10 +2459,10 @@ index e81a4538fd45c13aa60d933de5f4f32ce69fb5f2..d7945f81295c497485a09696f06ce041
|
|||
if (mPump && mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) {
|
||||
mPump->PeekStream(CallTypeSniffers, static_cast<nsIChannel*>(this));
|
||||
diff --git a/parser/html/nsHtml5TreeOpExecutor.cpp b/parser/html/nsHtml5TreeOpExecutor.cpp
|
||||
index f25949e6cc907ff18a76d68fc2e8005bd40146ea..9be4cb34517b06b94c6e145aef8a8ea5d2687d97 100644
|
||||
index 071ed8da4135102b0b1fedce32326bdc0657c3fd..063b516001a674b558046f9191f08352eb671801 100644
|
||||
--- a/parser/html/nsHtml5TreeOpExecutor.cpp
|
||||
+++ b/parser/html/nsHtml5TreeOpExecutor.cpp
|
||||
@@ -1389,6 +1389,10 @@ void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta(
|
||||
@@ -1391,6 +1391,10 @@ void nsHtml5TreeOpExecutor::UpdateReferrerInfoFromMeta(
|
||||
void nsHtml5TreeOpExecutor::AddSpeculationCSP(const nsAString& aCSP) {
|
||||
NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
|
||||
|
||||
|
|
@ -2474,10 +2474,10 @@ index f25949e6cc907ff18a76d68fc2e8005bd40146ea..9be4cb34517b06b94c6e145aef8a8ea5
|
|||
nsCOMPtr<nsIContentSecurityPolicy> preloadCsp = mDocument->GetPreloadCsp();
|
||||
if (!preloadCsp) {
|
||||
diff --git a/security/manager/ssl/nsCertOverrideService.cpp b/security/manager/ssl/nsCertOverrideService.cpp
|
||||
index fcc2a45e6de8eaeb1af2404a69bd3df58cf2aec8..d4c1df007bf5993cf9e0dadbe91aa2c38afc42ec 100644
|
||||
index b2e328e7c7d7a89be34b84fd176c306a3620c77c..54f24b213bcdc78c702e15d4d45a3943bc082281 100644
|
||||
--- a/security/manager/ssl/nsCertOverrideService.cpp
|
||||
+++ b/security/manager/ssl/nsCertOverrideService.cpp
|
||||
@@ -437,7 +437,12 @@ nsCertOverrideService::HasMatchingOverride(
|
||||
@@ -439,7 +439,12 @@ nsCertOverrideService::HasMatchingOverride(
|
||||
bool disableAllSecurityCheck = false;
|
||||
{
|
||||
MutexAutoLock lock(mMutex);
|
||||
|
|
@ -2491,7 +2491,7 @@ index fcc2a45e6de8eaeb1af2404a69bd3df58cf2aec8..d4c1df007bf5993cf9e0dadbe91aa2c3
|
|||
}
|
||||
if (disableAllSecurityCheck) {
|
||||
*aIsTemporary = false;
|
||||
@@ -649,14 +654,24 @@ static bool IsDebugger() {
|
||||
@@ -651,14 +656,24 @@ static bool IsDebugger() {
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsCertOverrideService::
|
||||
|
|
@ -2625,10 +2625,10 @@ index 00a5381133f8cec0de452c31c7151801a1acc0b9..5d3e3d6f566dc724f257beaeb994ceda
|
|||
|
||||
if (provider.failed) {
|
||||
diff --git a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp
|
||||
index 144628a310662eb393d8c1a4fffbec3cf5fd1dff..69fa66f27d0533f3d90801acbfa23039ef81f7df 100644
|
||||
index 6a40d032449e780bfeb77934ba141317b94e7189..1468d38355058b985f18613bd6e3bc84086fae40 100644
|
||||
--- a/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp
|
||||
+++ b/toolkit/components/resistfingerprinting/nsUserCharacteristics.cpp
|
||||
@@ -632,7 +632,7 @@ void PopulateLanguages() {
|
||||
@@ -553,7 +553,7 @@ void PopulateLanguages() {
|
||||
// sufficient to only collect this information as the other properties are
|
||||
// just reformats of Navigator::GetAcceptLanguages.
|
||||
nsTArray<nsString> languages;
|
||||
|
|
@ -2666,10 +2666,10 @@ index 654903fadb709be976b72f36f155e23bc0622152..815b3dc24c9fda6b1db6c4666ac68904
|
|||
int32_t aMaxSelfProgress,
|
||||
int32_t aCurTotalProgress,
|
||||
diff --git a/toolkit/components/windowwatcher/nsWindowWatcher.cpp b/toolkit/components/windowwatcher/nsWindowWatcher.cpp
|
||||
index cdba76dc8ae2206a58d7e5eb6eba97c2c3732513..266fdc6235363eafc6c7b587d5c0f597deee6e59 100644
|
||||
index e3f616c4efd5d7e10ed372afa4b5c4d2d93e6a67..abb7772184c9baf23025c1577d1284b6ed1959fb 100644
|
||||
--- a/toolkit/components/windowwatcher/nsWindowWatcher.cpp
|
||||
+++ b/toolkit/components/windowwatcher/nsWindowWatcher.cpp
|
||||
@@ -1865,7 +1865,11 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForContent(
|
||||
@@ -1881,7 +1881,11 @@ uint32_t nsWindowWatcher::CalculateChromeFlagsForContent(
|
||||
|
||||
// Open a minimal popup.
|
||||
*aIsPopupRequested = true;
|
||||
|
|
@ -2683,10 +2683,10 @@ index cdba76dc8ae2206a58d7e5eb6eba97c2c3732513..266fdc6235363eafc6c7b587d5c0f597
|
|||
|
||||
/**
|
||||
diff --git a/toolkit/mozapps/update/UpdateService.sys.mjs b/toolkit/mozapps/update/UpdateService.sys.mjs
|
||||
index be01248253ee1bcc9435c3e8223ed032f498a023..0f05923c29a023511b72a81ec527300cafa17760 100644
|
||||
index 6c2b400952492266a184c96b4a1ce71e87df7ffe..6e1fb4f59b6a6d70100e1eb1d15c937d8480988a 100644
|
||||
--- a/toolkit/mozapps/update/UpdateService.sys.mjs
|
||||
+++ b/toolkit/mozapps/update/UpdateService.sys.mjs
|
||||
@@ -3888,6 +3888,8 @@ export class UpdateService {
|
||||
@@ -3894,6 +3894,8 @@ export class UpdateService {
|
||||
}
|
||||
|
||||
get disabledForTesting() {
|
||||
|
|
@ -2696,10 +2696,10 @@ index be01248253ee1bcc9435c3e8223ed032f498a023..0f05923c29a023511b72a81ec527300c
|
|||
}
|
||||
|
||||
diff --git a/toolkit/toolkit.mozbuild b/toolkit/toolkit.mozbuild
|
||||
index 8c2b2bf996bd889651dc7fac1dc351b4c47b567e..07d237eb17a657ce051fd0aa5e53449c0c3159f6 100644
|
||||
index f42ed17a4a75689ae9c8e769d7b6e2c3f654b8ee..5af0877335339407160dd7d10b89bd3d62adff9f 100644
|
||||
--- a/toolkit/toolkit.mozbuild
|
||||
+++ b/toolkit/toolkit.mozbuild
|
||||
@@ -155,6 +155,7 @@ if CONFIG["ENABLE_WEBDRIVER"]:
|
||||
@@ -156,6 +156,7 @@ if CONFIG["ENABLE_WEBDRIVER"]:
|
||||
"/remote",
|
||||
"/testing/firefox-ui",
|
||||
"/testing/marionette",
|
||||
|
|
@ -2760,7 +2760,7 @@ index fe72a2715da8846146377e719559c16e6ef1f7ff..a5959143bac8f62ee359fa3883a844f3
|
|||
// nsDocumentViewer::LoadComplete that doesn't do various things
|
||||
// that are not relevant here because this wasn't an actual
|
||||
diff --git a/uriloader/exthandler/nsExternalHelperAppService.cpp b/uriloader/exthandler/nsExternalHelperAppService.cpp
|
||||
index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790eafcbc6a 100644
|
||||
index 139a43a1780dac34a6d8b135accac9cf39beef3f..2a855c3ae87e4e5a431cb1547cda0ee88490ca33 100644
|
||||
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
|
||||
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
|
||||
@@ -112,6 +112,7 @@
|
||||
|
|
@ -2771,7 +2771,7 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
#include "mozilla/Preferences.h"
|
||||
#include "mozilla/ipc/URIUtils.h"
|
||||
|
||||
@@ -831,6 +832,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
|
||||
@@ -872,6 +873,12 @@ NS_IMETHODIMP nsExternalHelperAppService::ApplyDecodingForExtension(
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
|
@ -2784,7 +2784,7 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
nsresult nsExternalHelperAppService::GetFileTokenForPath(
|
||||
const char16_t* aPlatformAppPath, nsIFile** aFile) {
|
||||
nsDependentString platformAppPath(aPlatformAppPath);
|
||||
@@ -1441,7 +1448,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
|
||||
@@ -1494,7 +1501,12 @@ nsresult nsExternalAppHandler::SetUpTempFile(nsIChannel* aChannel) {
|
||||
// Strip off the ".part" from mTempLeafName
|
||||
mTempLeafName.Truncate(mTempLeafName.Length() - ArrayLength(".part") + 1);
|
||||
|
||||
|
|
@ -2797,7 +2797,7 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
mSaver =
|
||||
do_CreateInstance(NS_BACKGROUNDFILESAVERSTREAMLISTENER_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
@@ -1630,7 +1642,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
|
||||
@@ -1683,7 +1695,36 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
|
@ -2835,7 +2835,7 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
if (NS_FAILED(rv)) {
|
||||
nsresult transferError = rv;
|
||||
|
||||
@@ -1682,6 +1723,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
|
||||
@@ -1744,6 +1785,9 @@ NS_IMETHODIMP nsExternalAppHandler::OnStartRequest(nsIRequest* request) {
|
||||
|
||||
bool alwaysAsk = true;
|
||||
mMimeInfo->GetAlwaysAskBeforeHandling(&alwaysAsk);
|
||||
|
|
@ -2845,7 +2845,7 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
if (alwaysAsk) {
|
||||
// But we *don't* ask if this mimeInfo didn't come from
|
||||
// our user configuration datastore and the user has said
|
||||
@@ -2198,6 +2242,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
|
||||
@@ -2260,6 +2304,16 @@ nsExternalAppHandler::OnSaveComplete(nsIBackgroundFileSaver* aSaver,
|
||||
NotifyTransfer(aStatus);
|
||||
}
|
||||
|
||||
|
|
@ -2862,7 +2862,7 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
@@ -2679,6 +2733,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
|
||||
@@ -2743,6 +2797,15 @@ NS_IMETHODIMP nsExternalAppHandler::Cancel(nsresult aReason) {
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2879,10 +2879,10 @@ index ad769a235b6a7ccf7791a3d9680f3bf373b30a86..ff18e7516a2bc3258b00d958cbaa4790
|
|||
// OnStartRequest)
|
||||
mDialog = nullptr;
|
||||
diff --git a/uriloader/exthandler/nsExternalHelperAppService.h b/uriloader/exthandler/nsExternalHelperAppService.h
|
||||
index 1f77e095dbfa3acc046779114007d83fc1cfa087..2354abbab7af6f6bdc3bd628722f03ea401d236a 100644
|
||||
index e880b90b2df85fb3b1ab3ba8d2fc181b824e2272..dbadd74dea9b245d68da3b2856e16b7b98c655d0 100644
|
||||
--- a/uriloader/exthandler/nsExternalHelperAppService.h
|
||||
+++ b/uriloader/exthandler/nsExternalHelperAppService.h
|
||||
@@ -257,6 +257,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
|
||||
@@ -258,6 +258,8 @@ class nsExternalHelperAppService : public nsIExternalHelperAppService,
|
||||
mozilla::dom::BrowsingContext* aContentContext, bool aForceSave,
|
||||
nsIInterfaceRequestor* aWindowContext,
|
||||
nsIStreamListener** aStreamListener);
|
||||
|
|
@ -2891,7 +2891,7 @@ index 1f77e095dbfa3acc046779114007d83fc1cfa087..2354abbab7af6f6bdc3bd628722f03ea
|
|||
};
|
||||
|
||||
/**
|
||||
@@ -462,6 +464,9 @@ class nsExternalAppHandler final : public nsIStreamListener,
|
||||
@@ -463,6 +465,9 @@ class nsExternalAppHandler final : public nsIStreamListener,
|
||||
* Upon successful return, both mTempFile and mSaver will be valid.
|
||||
*/
|
||||
nsresult SetUpTempFile(nsIChannel* aChannel);
|
||||
|
|
@ -2902,7 +2902,7 @@ index 1f77e095dbfa3acc046779114007d83fc1cfa087..2354abbab7af6f6bdc3bd628722f03ea
|
|||
* When we download a helper app, we are going to retarget all load
|
||||
* notifications into our own docloader and load group instead of
|
||||
diff --git a/uriloader/exthandler/nsIExternalHelperAppService.idl b/uriloader/exthandler/nsIExternalHelperAppService.idl
|
||||
index 4a399acb72d4fd475c9ae43e9eadbc32f261e290..97ace81c82b16a9a993166dd4b0ddb3a721c9872 100644
|
||||
index 53ea934dd4876e4b491b724385c8fbf7d00ee6cd..0b7b88c853b21ce778d8e87fea0a2bfe839ad412 100644
|
||||
--- a/uriloader/exthandler/nsIExternalHelperAppService.idl
|
||||
+++ b/uriloader/exthandler/nsIExternalHelperAppService.idl
|
||||
@@ -6,8 +6,11 @@
|
||||
|
|
@ -2935,10 +2935,11 @@ index 4a399acb72d4fd475c9ae43e9eadbc32f261e290..97ace81c82b16a9a993166dd4b0ddb3a
|
|||
/**
|
||||
* The external helper app service is used for finding and launching
|
||||
* platform specific external applications for a given mime content type.
|
||||
@@ -76,6 +90,7 @@ interface nsIExternalHelperAppService : nsISupports
|
||||
boolean applyDecodingForExtension(in AUTF8String aExtension,
|
||||
in ACString aEncodingType);
|
||||
|
||||
@@ -87,6 +101,8 @@ interface nsIExternalHelperAppService : nsISupports
|
||||
* `DownloadIntegration.sys.mjs`, which is implemented on all platforms.
|
||||
*/
|
||||
nsIFile getPreferredDownloadsDirectory();
|
||||
+
|
||||
+ void setDownloadInterceptor(in nsIDownloadInterceptor interceptor);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
REMOTE_URL="https://github.com/WebKit/WebKit.git"
|
||||
BASE_BRANCH="main"
|
||||
BASE_REVISION="2ea46ab90e6511139bfb94415205038b672381e0"
|
||||
BASE_REVISION="8ceb1da47e75a488ae4c12017a861636904acd4f"
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ const NSActivityOptions ActivityOptions =
|
|||
|
||||
for (NSString *argument in subArray) {
|
||||
if (![argument hasPrefix:@"--"])
|
||||
_initialURL = argument;
|
||||
_initialURL = [argument copy];
|
||||
if ([argument hasPrefix:@"--user-data-dir="]) {
|
||||
NSRange range = NSMakeRange(16, [argument length] - 16);
|
||||
_userDataDir = [[argument substringWithRange:range] copy];
|
||||
|
|
@ -230,7 +230,7 @@ const NSActivityOptions ActivityOptions =
|
|||
configuration = [[WKWebViewConfiguration alloc] init];
|
||||
configuration.websiteDataStore = [self persistentDataStore];
|
||||
configuration._controlledByAutomation = true;
|
||||
configuration.preferences._fullScreenEnabled = YES;
|
||||
configuration.preferences.elementFullscreenEnabled = YES;
|
||||
configuration.preferences._developerExtrasEnabled = YES;
|
||||
configuration.preferences._mediaDevicesEnabled = YES;
|
||||
configuration.preferences._mockCaptureDevicesEnabled = YES;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -104,38 +104,23 @@ await page.Keyboard.PressAsync("Shift+A");
|
|||
An example to trigger select-all with the keyboard
|
||||
|
||||
```js
|
||||
// on Windows and Linux
|
||||
await page.keyboard.press('Control+A');
|
||||
// on macOS
|
||||
await page.keyboard.press('Meta+A');
|
||||
await page.keyboard.press('ControlOrMeta+A');
|
||||
```
|
||||
|
||||
```java
|
||||
// on Windows and Linux
|
||||
page.keyboard().press("Control+A");
|
||||
// on macOS
|
||||
page.keyboard().press("Meta+A");
|
||||
page.keyboard().press("ControlOrMeta+A");
|
||||
```
|
||||
|
||||
```python async
|
||||
# on windows and linux
|
||||
await page.keyboard.press("Control+A")
|
||||
# on mac_os
|
||||
await page.keyboard.press("Meta+A")
|
||||
await page.keyboard.press("ControlOrMeta+A")
|
||||
```
|
||||
|
||||
```python sync
|
||||
# on windows and linux
|
||||
page.keyboard.press("Control+A")
|
||||
# on mac_os
|
||||
page.keyboard.press("Meta+A")
|
||||
page.keyboard.press("ControlOrMeta+A")
|
||||
```
|
||||
|
||||
```csharp
|
||||
// on Windows and Linux
|
||||
await page.Keyboard.PressAsync("Control+A");
|
||||
// on macOS
|
||||
await page.Keyboard.PressAsync("Meta+A");
|
||||
await page.Keyboard.PressAsync("ControlOrMeta+A");
|
||||
```
|
||||
|
||||
## async method: Keyboard.down
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ Additional locator to match.
|
|||
* since: v1.49
|
||||
- returns: <[string]>
|
||||
|
||||
Captures the aria snapshot of the given element. See [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
|
||||
Captures the aria snapshot of the given element.
|
||||
Read more about [aria snapshots](../aria-snapshots.md) and [`method: LocatorAssertions.toMatchAriaSnapshot`] for the corresponding assertion.
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
@ -205,6 +206,9 @@ Below is the HTML markup and the respective ARIA snapshot:
|
|||
- link "About"
|
||||
```
|
||||
|
||||
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: Locator.ariaSnapshot.timeout = %%-input-timeout-js-%%
|
||||
* since: v1.49
|
||||
|
||||
|
|
|
|||
|
|
@ -701,7 +701,7 @@ expect(locator).to_be_enabled()
|
|||
|
||||
```csharp
|
||||
var locator = Page.Locator("button.submit");
|
||||
await Expect(locator).toBeEnabledAsync();
|
||||
await Expect(locator).ToBeEnabledAsync();
|
||||
```
|
||||
|
||||
### option: LocatorAssertions.toBeEnabled.enabled
|
||||
|
|
@ -1181,7 +1181,7 @@ expect(locator).to_have_accessible_description("Save results to disk")
|
|||
|
||||
```csharp
|
||||
var locator = Page.GetByTestId("save-button");
|
||||
await Expect(locator).toHaveAccessibleDescriptionAsync("Save results to disk");
|
||||
await Expect(locator).ToHaveAccessibleDescriptionAsync("Save results to disk");
|
||||
```
|
||||
|
||||
### param: LocatorAssertions.toHaveAccessibleDescription.description
|
||||
|
|
@ -1231,12 +1231,12 @@ expect(locator).to_have_accessible_name("Save to disk")
|
|||
|
||||
```csharp
|
||||
var locator = Page.GetByTestId("save-button");
|
||||
await Expect(locator).toHaveAccessibleNameAsync("Save to disk");
|
||||
await Expect(locator).ToHaveAccessibleNameAsync("Save to disk");
|
||||
```
|
||||
|
||||
### param: LocatorAssertions.toHaveAccessibleName.name
|
||||
* since: v1.44
|
||||
- `name` <[string]|[RegExp]>
|
||||
- `name` <[string]|[RegExp]|[Array]<[string]|[RegExp]>>
|
||||
|
||||
Expected accessible name.
|
||||
|
||||
|
|
@ -2109,7 +2109,7 @@ Expected options currently selected.
|
|||
* langs:
|
||||
- alias-java: matchesAriaSnapshot
|
||||
|
||||
Asserts that the target element matches the given accessibility snapshot.
|
||||
Asserts that the target element matches the given [accessibility snapshot](../aria-snapshots.md).
|
||||
|
||||
**Usage**
|
||||
|
||||
|
|
@ -2159,3 +2159,6 @@ assertThat(page.locator("body")).matchesAriaSnapshot("""
|
|||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-js-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
||||
### option: LocatorAssertions.toMatchAriaSnapshot.timeout = %%-csharp-java-python-assertions-timeout-%%
|
||||
* since: v1.49
|
||||
|
|
|
|||
|
|
@ -281,6 +281,80 @@ given name prefix inside the [`option: BrowserType.launch.tracesDir`] directory
|
|||
To specify the final trace zip file name, you need to pass `path` option to
|
||||
[`method: Tracing.stopChunk`] instead.
|
||||
|
||||
## async method: Tracing.group
|
||||
* since: v1.49
|
||||
|
||||
:::caution
|
||||
Use `test.step` instead when available.
|
||||
:::
|
||||
|
||||
Creates a new group within the trace, assigning any subsequent API calls to this group, until [`method: Tracing.groupEnd`] is called. Groups can be nested and will be visible in the trace viewer.
|
||||
|
||||
**Usage**
|
||||
|
||||
```js
|
||||
// use test.step instead
|
||||
await test.step('Log in', async () => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
// All actions between group and groupEnd
|
||||
// will be shown in the trace viewer as a group.
|
||||
page.context().tracing.group("Open Playwright.dev > API");
|
||||
page.navigate("https://playwright.dev/");
|
||||
page.getByRole(AriaRole.LINK, new Page.GetByRoleOptions().setName("API")).click();
|
||||
page.context().tracing.groupEnd();
|
||||
```
|
||||
|
||||
```python sync
|
||||
# All actions between group and group_end
|
||||
# will be shown in the trace viewer as a group.
|
||||
page.context.tracing.group("Open Playwright.dev > API")
|
||||
page.goto("https://playwright.dev/")
|
||||
page.get_by_role("link", name="API").click()
|
||||
page.context.tracing.group_end()
|
||||
```
|
||||
|
||||
```python async
|
||||
# All actions between group and group_end
|
||||
# will be shown in the trace viewer as a group.
|
||||
await page.context.tracing.group("Open Playwright.dev > API")
|
||||
await page.goto("https://playwright.dev/")
|
||||
await page.get_by_role("link", name="API").click()
|
||||
await page.context.tracing.group_end()
|
||||
```
|
||||
|
||||
```csharp
|
||||
// All actions between GroupAsync and GroupEndAsync
|
||||
// will be shown in the trace viewer as a group.
|
||||
await Page.Context().Tracing.GroupAsync("Open Playwright.dev > API");
|
||||
await Page.GotoAsync("https://playwright.dev/");
|
||||
await Page.GetByRole(AriaRole.Link, new() { Name = "API" }).ClickAsync();
|
||||
await Page.Context().Tracing.GroupEndAsync();
|
||||
```
|
||||
|
||||
### param: Tracing.group.name
|
||||
* since: v1.49
|
||||
- `name` <[string]>
|
||||
|
||||
Group name shown in the trace viewer.
|
||||
|
||||
### option: Tracing.group.location
|
||||
* since: v1.49
|
||||
- `location` ?<[Object]>
|
||||
- `file` <[string]>
|
||||
- `line` ?<[int]>
|
||||
- `column` ?<[int]>
|
||||
|
||||
Specifies a custom location for the group to be shown in the trace viewer. Defaults to the location of the [`method: Tracing.group`] call.
|
||||
|
||||
## async method: Tracing.groupEnd
|
||||
* since: v1.49
|
||||
|
||||
Closes the last group created by [`method: Tracing.group`].
|
||||
|
||||
## async method: Tracing.stop
|
||||
* since: v1.12
|
||||
|
||||
|
|
|
|||
392
docs/src/aria-snapshots.md
Normal file
392
docs/src/aria-snapshots.md
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
---
|
||||
id: aria-snapshots
|
||||
title: "Aria snapshots"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
In Playwright, aria snapshots provide a YAML representation of the accessibility tree of a page.
|
||||
These snapshots can be stored and compared later to verify if the page structure remains consistent or meets defined
|
||||
expectations.
|
||||
|
||||
The YAML format describes the hierarchical structure of accessible elements on the page, detailing **roles**, **attributes**, **values**, and **text content**.
|
||||
The structure follows a tree-like syntax, where each node represents an accessible element, and indentation indicates
|
||||
nested elements.
|
||||
|
||||
Following is a simple example of an aria snapshot for the playwright.dev homepage:
|
||||
|
||||
```yaml
|
||||
- banner:
|
||||
- heading /Playwright enables reliable/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- main:
|
||||
- img "Browsers (Chromium, Firefox, WebKit)"
|
||||
- heading "Any browser • Any platform • One API"
|
||||
```
|
||||
|
||||
Each accessible element in the tree is represented as a YAML node:
|
||||
|
||||
```yaml
|
||||
- role "name" [attribute=value]
|
||||
```
|
||||
|
||||
- **role**: Specifies the ARIA or HTML role of the element (e.g., `heading`, `list`, `listitem`, `button`).
|
||||
- **"name"**: Accessible name of the element. Quoted strings indicate exact values, `/patterns/` are used for regular expression.
|
||||
- **[attribute=value]**: Attributes and values, in square brackets, represent specific ARIA attributes, such
|
||||
as `checked`, `disabled`, `expanded`, `level`, `pressed`, or `selected`.
|
||||
|
||||
These values are derived from ARIA attributes or calculated based on HTML semantics. To inspect the accessibility tree
|
||||
structure of a page, use the [Chrome DevTools Accessibility Pane](https://developer.chrome.com/docs/devtools/accessibility/reference#pane).
|
||||
|
||||
|
||||
## Snapshot matching
|
||||
|
||||
The [`method: LocatorAssertions.toMatchAriaSnapshot`] assertion method in Playwright compares the accessible
|
||||
structure of the locator scope with a predefined aria snapshot template, helping validate the page's state against
|
||||
testing requirements.
|
||||
|
||||
For the following DOM:
|
||||
|
||||
```html
|
||||
<h1>title</h1>
|
||||
```
|
||||
|
||||
You can match it using the following snapshot template:
|
||||
|
||||
```js
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(`
|
||||
- heading "title"
|
||||
`);
|
||||
```
|
||||
|
||||
```python sync
|
||||
page.locator("body").to_match_aria_snapshot("""
|
||||
- heading "title"
|
||||
""")
|
||||
```
|
||||
|
||||
```python async
|
||||
await page.locator("body").to_match_aria_snapshot("""
|
||||
- heading "title"
|
||||
""")
|
||||
```
|
||||
|
||||
```java
|
||||
page.locator("body").expect().toMatchAriaSnapshot("""
|
||||
- heading "title"
|
||||
""");
|
||||
```
|
||||
|
||||
```csharp
|
||||
await Expect(page.Locator("body")).ToMatchAriaSnapshotAsync(@"
|
||||
- heading ""title""
|
||||
");
|
||||
```
|
||||
|
||||
When matching, the snapshot template is compared to the current accessibility tree of the page:
|
||||
|
||||
* If the tree structure matches the template, the test passes; otherwise, it fails, indicating a mismatch between
|
||||
expected and actual accessibility states.
|
||||
* The comparison is case-sensitive and collapses whitespace, so indentation and line breaks are ignored.
|
||||
* The comparison is order-sensitive, meaning the order of elements in the snapshot template must match the order in the
|
||||
page's accessibility tree.
|
||||
|
||||
|
||||
### Partial matching
|
||||
|
||||
You can perform partial matches on nodes by omitting attributes or accessible names, enabling verification of specific
|
||||
parts of the accessibility tree without requiring exact matches. This flexibility is helpful for dynamic or irrelevant
|
||||
attributes.
|
||||
|
||||
```html
|
||||
<button>Submit</button>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- button
|
||||
```
|
||||
|
||||
In this example, the button role is matched, but the accessible name ("Submit") is not specified, allowing the test to
|
||||
pass regardless of the button’s label.
|
||||
|
||||
<hr/>
|
||||
|
||||
For elements with ARIA attributes like `checked` or `disabled`, omitting these attributes allows partial matching,
|
||||
focusing solely on role and hierarchy.
|
||||
|
||||
```html
|
||||
<input type="checkbox" checked>
|
||||
```
|
||||
|
||||
*aria snapshot for partial match*
|
||||
|
||||
```yaml
|
||||
- checkbox
|
||||
```
|
||||
|
||||
In this partial match, the `checked` attribute is ignored, so the test will pass regardless of the checkbox state.
|
||||
|
||||
<hr/>
|
||||
|
||||
Similarly, you can partially match children in lists or groups by omitting specific list items or nested elements.
|
||||
|
||||
```html
|
||||
<ul>
|
||||
<li>Feature A</li>
|
||||
<li>Feature B</li>
|
||||
<li>Feature C</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
*aria snapshot for partial match*
|
||||
|
||||
```yaml
|
||||
- list
|
||||
- listitem: Feature B
|
||||
```
|
||||
|
||||
Partial matches let you create flexible snapshot tests that verify essential page structure without enforcing
|
||||
specific content or attributes.
|
||||
|
||||
### Matching with regular expressions
|
||||
|
||||
Regular expressions allow flexible matching for elements with dynamic or variable text. Accessible names and text can
|
||||
support regex patterns.
|
||||
|
||||
```html
|
||||
<h1>Issues 12</h1>
|
||||
```
|
||||
|
||||
*aria snapshot with regular expression*
|
||||
|
||||
```yaml
|
||||
- heading /Issues \d+/
|
||||
```
|
||||
|
||||
## Generating snapshots
|
||||
|
||||
Creating aria snapshots in Playwright helps ensure and maintain your application’s structure.
|
||||
You can generate snapshots in various ways depending on your testing setup and workflow.
|
||||
|
||||
### 1. Generating snapshots with the Playwright code generator
|
||||
|
||||
If you’re using Playwright’s [Code Generator](./codegen.md), generating aria snapshots is streamlined with its
|
||||
interactive interface:
|
||||
|
||||
- **"Assert snapshot" Action**: In the code generator, you can use the "Assert snapshot" action to automatically create
|
||||
a snapshot assertion for the selected elements. This is a quick way to capture the aria snapshot as part of your
|
||||
recorded test flow.
|
||||
|
||||
- **"Aria snapshot" Tab**: The "Aria snapshot" tab within the code generator interface visually represents the
|
||||
aria snapshot for a selected locator, letting you explore, inspect, and verify element roles, attributes, and
|
||||
accessible names to aid snapshot creation and review.
|
||||
|
||||
### 2. Updating snapshots with `@playwright/test` and the `--update-snapshots` flag
|
||||
|
||||
When using the Playwright test runner (`@playwright/test`), you can automatically update snapshots by running tests with
|
||||
the `--update-snapshots` flag:
|
||||
|
||||
```bash
|
||||
npx playwright test --update-snapshots
|
||||
```
|
||||
|
||||
This command regenerates snapshots for assertions, including aria snapshots, replacing outdated ones. It’s
|
||||
useful when application structure changes require new snapshots as a baseline. Note that Playwright will wait for the
|
||||
maximum expect timeout specified in the test runner configuration to ensure the
|
||||
page is settled before taking the snapshot. It might be necessary to adjust the `--timeout` if the test hits the timeout
|
||||
while generating snapshots.
|
||||
|
||||
#### Empty template for snapshot generation
|
||||
|
||||
Passing an empty string as the template in an assertion generates a snapshot on-the-fly:
|
||||
|
||||
```js
|
||||
await expect(locator).toMatchAriaSnapshot('');
|
||||
```
|
||||
|
||||
Note that Playwright will wait for the maximum expect timeout specified in the test runner configuration to ensure the
|
||||
page is settled before taking the snapshot. It might be necessary to adjust the `--timeout` if the test hits the timeout
|
||||
while generating snapshots.
|
||||
|
||||
#### Snapshot patch files
|
||||
|
||||
When updating snapshots, Playwright creates patch files that capture differences. These patch files can be reviewed,
|
||||
applied, and committed to source control, allowing teams to track structural changes over time and ensure updates are
|
||||
consistent with application requirements.
|
||||
|
||||
### 3. Using the `Locator.ariaSnapshot` method
|
||||
|
||||
The [`method: Locator.ariaSnapshot`] method allows you to programmatically create a YAML representation of accessible
|
||||
elements within a locator’s scope, especially helpful for generating snapshots dynamically during test execution.
|
||||
|
||||
**Example**:
|
||||
|
||||
```js
|
||||
const snapshot = await page.locator('body').ariaSnapshot();
|
||||
console.log(snapshot);
|
||||
```
|
||||
|
||||
```python sync
|
||||
snapshot = page.locator("body").aria_snapshot()
|
||||
print(snapshot)
|
||||
```
|
||||
|
||||
```python async
|
||||
snapshot = await page.locator("body").aria_snapshot()
|
||||
print(snapshot)
|
||||
```
|
||||
|
||||
```java
|
||||
String snapshot = page.locator("body").ariaSnapshot();
|
||||
System.out.println(snapshot);
|
||||
```
|
||||
|
||||
```csharp
|
||||
var snapshot = await page.Locator("body").AriaSnapshotAsync();
|
||||
Console.WriteLine(snapshot);
|
||||
```
|
||||
|
||||
This command outputs the aria snapshot within the specified locator’s scope in YAML format, which you can validate
|
||||
or store as needed.
|
||||
|
||||
## Accessibility tree examples
|
||||
|
||||
### Headings with level attributes
|
||||
|
||||
Headings can include a `level` attribute indicating their heading level.
|
||||
|
||||
```html
|
||||
<h1>Title</h1>
|
||||
<h2>Subtitle</h2>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- heading "Title" [level=1]
|
||||
- heading "Subtitle" [level=2]
|
||||
```
|
||||
|
||||
### Text nodes
|
||||
|
||||
Standalone or descriptive text elements appear as text nodes.
|
||||
|
||||
```html
|
||||
<div>Sample accessible name</div>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- text: Sample accessible name
|
||||
```
|
||||
|
||||
### Inline multiline text
|
||||
|
||||
Multiline text, such as paragraphs, is normalized in the aria snapshot.
|
||||
|
||||
```html
|
||||
<p>Line 1<br>Line 2</p>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- paragraph: Line 1 Line 2
|
||||
```
|
||||
|
||||
### Links
|
||||
|
||||
Links display their text or composed content from pseudo-elements.
|
||||
|
||||
```html
|
||||
<a href="#more-info">Read more about Accessibility</a>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- link "Read more about Accessibility"
|
||||
```
|
||||
|
||||
### Text boxes
|
||||
|
||||
Input elements of type `text` show their `value` attribute content.
|
||||
|
||||
```html
|
||||
<input type="text" value="Enter your name">
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- textbox: Enter your name
|
||||
```
|
||||
|
||||
### Lists with items
|
||||
|
||||
Ordered and unordered lists include their list items.
|
||||
|
||||
```html
|
||||
<ul aria-label="Main Features">
|
||||
<li>Feature 1</li>
|
||||
<li>Feature 2</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- list "Main Features":
|
||||
- listitem: Feature 1
|
||||
- listitem: Feature 2
|
||||
```
|
||||
|
||||
### Grouped elements
|
||||
|
||||
Groups capture nested elements, such as `<details>` elements with summary content.
|
||||
|
||||
```html
|
||||
<details>
|
||||
<summary>Summary</summary>
|
||||
<p>Detail content here</p>
|
||||
</details>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- group: Summary
|
||||
```
|
||||
|
||||
### Attributes and states
|
||||
|
||||
Commonly used ARIA attributes, like `checked`, `disabled`, `expanded`, `level`, `pressed`, and `selected`, represent
|
||||
control states.
|
||||
|
||||
#### Checkbox with `checked` attribute
|
||||
|
||||
```html
|
||||
<input type="checkbox" checked>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- checkbox [checked]
|
||||
```
|
||||
|
||||
#### Button with `pressed` attribute
|
||||
|
||||
```html
|
||||
<button aria-pressed="true">Toggle</button>
|
||||
```
|
||||
|
||||
*aria snapshot*
|
||||
|
||||
```yaml
|
||||
- button "Toggle" [pressed=true]
|
||||
```
|
||||
|
|
@ -90,7 +90,7 @@ await page
|
|||
|
||||
#### Prefer user-facing attributes to XPath or CSS selectors
|
||||
|
||||
Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests. For example consider selecting this button by its CSS classes. Should the designer change something then the class might change breaking your test.
|
||||
Your DOM can easily change so having your tests depend on your DOM structure can lead to failing tests. For example consider selecting this button by its CSS classes. Should the designer change something then the class might change, thus breaking your test.
|
||||
|
||||
|
||||
```js
|
||||
|
|
@ -475,6 +475,21 @@ Setup CI/CD and run your tests frequently. The more often you run your tests the
|
|||
|
||||
Use Linux when running your tests on CI as it is cheaper. Developers can use whatever environment when running locally but use linux on CI. Consider setting up [Sharding](./test-sharding.md) to make CI faster.
|
||||
|
||||
|
||||
#### Optimize browser downloads on CI
|
||||
|
||||
Only install the browsers that you actually need, especially on CI. For example, if you're only testing with Chromium, install just Chromium.
|
||||
|
||||
```bash title=".github/workflows/playwright.yml"
|
||||
# Instead of installing all browsers
|
||||
npx playwright install --with-deps
|
||||
|
||||
# Install only Chromium
|
||||
npx playwright install chromium --with-deps
|
||||
```
|
||||
|
||||
This saves both download time and disk space on your CI machines.
|
||||
|
||||
### Lint your tests
|
||||
|
||||
We recommend TypeScript and linting with ESLint for your tests to catch errors early. Use [`@typescript-eslint/no-floating-promises`](https://typescript-eslint.io/rules/no-floating-promises/) [ESLint](https://eslint.org) rule to make sure there are no missing awaits before the asynchronous calls to the Playwright API. On your CI you can run `tsc --noEmit` to ensure that functions are called with the right signature.
|
||||
|
|
|
|||
|
|
@ -338,6 +338,87 @@ dotnet test --settings:webkit.runsettings
|
|||
|
||||
For Google Chrome, Microsoft Edge and other Chromium-based browsers, by default, Playwright uses open source Chromium builds. Since the Chromium project is ahead of the branded browsers, when the world is on Google Chrome N, Playwright already supports Chromium N+1 that will be released in Google Chrome and Microsoft Edge a few weeks later.
|
||||
|
||||
Playwright ships a regular Chromium build for headed operations and a separate [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) for headless mode. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
|
||||
|
||||
#### Optimize download size on CI
|
||||
|
||||
If you are only running tests in headless mode, for example on CI, you can avoid downloading a regular version of Chromium by passing `--only-shell` during installation.
|
||||
|
||||
```bash js
|
||||
# only running tests headlessly
|
||||
npx playwright install --with-deps --only-shell
|
||||
```
|
||||
|
||||
```bash java
|
||||
# only running tests headlessly
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps --only-shell"
|
||||
```
|
||||
|
||||
```bash python
|
||||
# only running tests headlessly
|
||||
playwright install --with-deps --only-shell
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
# only running tests headlessly
|
||||
pwsh bin/Debug/netX/playwright.ps1 install --with-deps --only-shell
|
||||
```
|
||||
|
||||
#### Opt-in to new headless mode
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for details.
|
||||
|
||||
```js
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
```java
|
||||
import com.microsoft.playwright.*;
|
||||
|
||||
public class Example {
|
||||
public static void main(String[] args) {
|
||||
try (Playwright playwright = Playwright.create()) {
|
||||
Browser browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setChannel("chromium"));
|
||||
Page page = browser.newPage();
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash python
|
||||
pytest test_login.py --browser-channel chromium
|
||||
```
|
||||
|
||||
```xml csharp
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<Playwright>
|
||||
<BrowserName>chromium</BrowserName>
|
||||
<LaunchOptions>
|
||||
<Channel>chromium</Channel>
|
||||
</LaunchOptions>
|
||||
</Playwright>
|
||||
</RunSettings>
|
||||
```
|
||||
|
||||
```bash csharp
|
||||
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=chromium
|
||||
```
|
||||
|
||||
### Google Chrome & Microsoft Edge
|
||||
|
||||
While Playwright can download and use the recent Chromium build, it can operate against the branded Google Chrome and Microsoft Edge browsers available on the machine (note that Playwright doesn't install them by default). In particular, the current Playwright version will support Stable and Beta channels of these browsers.
|
||||
|
|
@ -348,6 +429,10 @@ Available channels are `chrome`, `msedge`, `chrome-beta`, `msedge-beta` or `msed
|
|||
Certain Enterprise Browser Policies may impact Playwright's ability to launch and control Google Chrome and Microsoft Edge. Running in an environment with browser policies is outside of the Playwright project's scope.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
Google Chrome and Microsoft Edge have switched to a [new headless mode](https://developer.chrome.com/docs/chromium/headless) implementation that is closer to a regular headed mode. This differs from [chromium headless shell](https://developer.chrome.com/blog/chrome-headless-shell) that is used in Playwright by default when running headless, so expect different behavior in some cases. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) fore details.
|
||||
:::
|
||||
|
||||
```js
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
|
|
@ -401,6 +486,23 @@ pytest test_login.py --browser-channel msedge
|
|||
dotnet test -- Playwright.BrowserName=chromium Playwright.LaunchOptions.Channel=msedge
|
||||
```
|
||||
|
||||
######
|
||||
* langs: python
|
||||
|
||||
Alternatively when using the library directly, you can specify the browser [`option: BrowserType.launch.channel`] when launching the browser:
|
||||
|
||||
```python
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
with sync_playwright() as p:
|
||||
# Channel can be "chrome", "msedge", "chrome-beta", "msedge-beta" or "msedge-dev".
|
||||
browser = p.chromium.launch(channel="msedge")
|
||||
page = browser.new_page()
|
||||
page.goto("http://playwright.dev")
|
||||
print(page.title())
|
||||
browser.close()
|
||||
```
|
||||
|
||||
#### Installing Google Chrome & Microsoft Edge
|
||||
|
||||
If Google Chrome or Microsoft Edge is not available on your machine, you can install
|
||||
|
|
|
|||
|
|
@ -214,10 +214,6 @@ def test_popup_page(page: Page, extension_id: str) -> None:
|
|||
|
||||
## Headless mode
|
||||
|
||||
:::danger
|
||||
`headless=new` mode is not officially supported by Playwright and might result in unexpected behavior.
|
||||
:::
|
||||
|
||||
By default, Chrome's headless mode in Playwright does not support Chrome extensions. To overcome this limitation, you can run Chrome's persistent context with a new headless mode by using the following code:
|
||||
|
||||
```js title="fixtures.ts"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Playwright tests can be run on any CI provider. This guide covers one way of run
|
|||
## Introduction
|
||||
* langs: python, java, csharp
|
||||
|
||||
Playwright tests can be ran on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration.
|
||||
Playwright tests can be run on any CI provider. In this section we will cover running tests on GitHub using GitHub actions. If you would like to see how to configure other CI providers check out our detailed doc on Continuous Integration.
|
||||
|
||||
#### You will learn
|
||||
* langs: python, java, csharp
|
||||
|
|
|
|||
|
|
@ -164,11 +164,11 @@ await Page.GotoAsync("http://localhost:3333");
|
|||
await Page.Clock.PauseAtAsync(new DateTime(2024, 2, 2, 10, 0, 0));
|
||||
|
||||
// Assert the page state.
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:00:00 AM");
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:00:00 AM");
|
||||
|
||||
// Close the laptop lid again and open it at 10:30am.
|
||||
await Page.Clock.FastForwardAsync("30:00");
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveText("2/2/2024, 10:30:00 AM");
|
||||
await Expect(Page.GetByTestId("current-time")).ToHaveTextAsync("2/2/2024, 10:30:00 AM");
|
||||
```
|
||||
|
||||
## Test inactivity monitoring
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ The `tests` folder contains a basic example test to help you get started with te
|
|||
|
||||
## Running the Example Test
|
||||
|
||||
By default tests will be run on all 3 browsers, chromium, firefox and webkit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
|
||||
By default tests will be run on all 3 browsers, Chromium, Firefox and WebKit using 3 workers. This can be configured in the [playwright.config file](./test-configuration.md). Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal.
|
||||
|
||||
<Tabs
|
||||
defaultValue="npm"
|
||||
|
|
|
|||
|
|
@ -788,7 +788,7 @@ All the same methods are also available on [Locator], [FrameLocator] and [Frame]
|
|||
- [`method: LocatorAssertions.toHaveAttribute`] with an empty value does not match missing attribute anymore. For example, the following snippet will succeed when `button` **does not** have a `disabled` attribute.
|
||||
|
||||
```csharp
|
||||
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttribute("disabled", "");
|
||||
await Expect(Page.GetByRole(AriaRole.Button)).ToHaveAttributeAsync("disabled", "");
|
||||
```
|
||||
|
||||
### Browser Versions
|
||||
|
|
|
|||
|
|
@ -6,6 +6,93 @@ toc_max_heading_level: 2
|
|||
|
||||
import LiteYouTube from '@site/src/components/LiteYouTube';
|
||||
|
||||
## Version 1.49
|
||||
|
||||
### Aria snapshots
|
||||
|
||||
New assertion [`method: LocatorAssertions.toMatchAriaSnapshot`] verifies page structure by comparing to an expected accessibility tree, represented as YAML.
|
||||
|
||||
```js
|
||||
await page.goto('https://playwright.dev');
|
||||
await expect(page.locator('body')).toMatchAriaSnapshot(`
|
||||
- banner:
|
||||
- heading /Playwright enables reliable/ [level=1]
|
||||
- link "Get started"
|
||||
- link "Star microsoft/playwright on GitHub"
|
||||
- main:
|
||||
- img "Browsers (Chromium, Firefox, WebKit)"
|
||||
- heading "Any browser • Any platform • One API"
|
||||
`);
|
||||
```
|
||||
|
||||
You can generate this assertion with [Test Generator](./codegen) and update the expected snapshot with `--update-snapshots` command line flag.
|
||||
|
||||
Learn more in the [aria snapshots guide](./aria-snapshots).
|
||||
|
||||
### Test runner
|
||||
|
||||
- New option [`property: TestConfig.tsconfig`] allows to specify a single `tsconfig` to be used for all tests.
|
||||
- New method [`method: Test.fail.only`] to focus on a failing test.
|
||||
- Options [`property: TestConfig.globalSetup`] and [`property: TestConfig.globalTeardown`] now support multiple setups/teardowns.
|
||||
- New value `'on-first-failure'` for [`property: TestOptions.screenshot`].
|
||||
- Added "previous" and "next" buttons to the HTML report to quickly switch between test cases.
|
||||
- New properties [`property: TestInfoError.cause`] and [`property: TestError.cause`] mirroring [`Error.cause`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause).
|
||||
|
||||
### Breaking: `chrome` and `msedge` channels switch to new headless mode
|
||||
|
||||
This change affects you if you're using one of the following channels in your `playwright.config.ts`:
|
||||
- `chrome`, `chrome-dev`, `chrome-beta`, or `chrome-canary`
|
||||
- `msedge`, `msedge-dev`, `msedge-beta`, or `msedge-canary`
|
||||
|
||||
#### What do I need to do?
|
||||
|
||||
After updating to Playwright v1.49, run your test suite. If it still passes, you're good to go. If not, you will probably need to update your snapshots, and adapt some of your test code around PDF viewers and extensions. See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for more details.
|
||||
|
||||
### Other breaking changes
|
||||
|
||||
- There will be no more updates for WebKit on Ubuntu 20.04 and Debian 11. We recommend updating your OS to a later version.
|
||||
- Package `@playwright/experimental-ct-vue2` will no longer be updated.
|
||||
- Package `@playwright/experimental-ct-solid` will no longer be updated.
|
||||
|
||||
### Try new Chromium headless
|
||||
|
||||
You can opt into the new headless mode by using `'chromium'` channel. As [official Chrome documentation puts it](https://developer.chrome.com/blog/chrome-headless-shell):
|
||||
|
||||
> New Headless on the other hand is the real Chrome browser, and is thus more authentic, reliable, and offers more features. This makes it more suitable for high-accuracy end-to-end web app testing or browser extension testing.
|
||||
|
||||
See [issue #33566](https://github.com/microsoft/playwright/issues/33566) for the list of possible breakages you could encounter and more details on Chromium headless. Please file an issue if you see any problems after opting in.
|
||||
|
||||
```js
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Miscellaneous
|
||||
|
||||
- `<canvas>` elements inside a snapshot now draw a preview.
|
||||
- New method [`method: Tracing.group`] to visually group actions in the trace.
|
||||
- Playwright docker images switched from Node.js v20 to Node.js v22 LTS.
|
||||
|
||||
### Browser Versions
|
||||
|
||||
- Chromium 131.0.6778.33
|
||||
- Mozilla Firefox 132.0
|
||||
- WebKit 18.2
|
||||
|
||||
This version was also tested against the following stable channels:
|
||||
|
||||
- Google Chrome 130
|
||||
- Microsoft Edge 130
|
||||
|
||||
|
||||
## Version 1.48
|
||||
|
||||
<LiteYouTube
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ npx playwright test --ui
|
|||
|
||||

|
||||
|
||||
Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about it's features.
|
||||
Check out or [detailed guide on UI Mode](./test-ui-mode.md) to learn more about its features.
|
||||
|
||||
### Run tests in headed mode
|
||||
|
||||
|
|
@ -112,11 +112,11 @@ npx playwright test --ui
|
|||
|
||||

|
||||
|
||||
While debugging you can use the Pick Locator button to select an element on the page and see the locator that Playwright would use to find that element. You can also edit the locator in the locator playground and see it highlighting live on the Browser window. Use the Copy Locator button to copy the locator to your clipboard and then paste it into you test.
|
||||
While debugging you can use the Pick Locator button to select an element on the page and see the locator that Playwright would use to find that element. You can also edit the locator in the locator playground and see it highlighting live on the Browser window. Use the Copy Locator button to copy the locator to your clipboard and then paste it into your test.
|
||||
|
||||

|
||||
|
||||
Check out our [detailed guide on UI Mode](./test-ui-mode.md) to learn more about it's features.
|
||||
Check out our [detailed guide on UI Mode](./test-ui-mode.md) to learn more about its features.
|
||||
|
||||
### Debug tests with the Playwright Inspector
|
||||
|
||||
|
|
|
|||
|
|
@ -1767,6 +1767,12 @@ Whether to box the step in the report. Defaults to `false`. When the step is box
|
|||
|
||||
Specifies a custom location for the step to be shown in test reports and trace viewer. By default, location of the [`method: Test.step`] call is shown.
|
||||
|
||||
### option: Test.step.timeout
|
||||
* since: v1.50
|
||||
- `timeout` <[float]>
|
||||
|
||||
Maximum time in milliseconds for the step to finish. Defaults to `0` (no timeout).
|
||||
|
||||
## method: Test.use
|
||||
* since: v1.10
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
|
||||
Information about an error thrown during test execution.
|
||||
|
||||
## property: TestInfoError.cause
|
||||
* since: v1.49
|
||||
- type: ?<[TestInfoError]>
|
||||
|
||||
Error cause. Set when there is a [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
|
||||
|
||||
## property: TestInfoError.message
|
||||
* since: v1.10
|
||||
- type: ?<[string]>
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ Note that by default `toPass` has timeout 0 and does not respect custom [expect
|
|||
|
||||
You can extend Playwright assertions by providing custom matchers. These matchers will be available on the `expect` object.
|
||||
|
||||
In this example we add a custom `toHaveAmount` function. Custom matcher should return a `message` callback and a `pass` flag indicating whether the assertion passed.
|
||||
In this example we add a custom `toHaveAmount` function. Custom matcher should return a `pass` flag indicating whether the assertion passed, and a `message` callback that's used when the assertion fails.
|
||||
|
||||
```js title="fixtures.ts"
|
||||
import { expect as baseExpect } from '@playwright/test';
|
||||
|
|
@ -279,7 +279,7 @@ export const expect = baseExpect.extend({
|
|||
? () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
|
||||
'\n\n' +
|
||||
`Locator: ${locator}\n` +
|
||||
`Expected: ${this.isNot ? 'not' : ''}${this.utils.printExpected(expected)}\n` +
|
||||
`Expected: not ${this.utils.printExpected(expected)}\n` +
|
||||
(matcherResult ? `Received: ${this.utils.printReceived(matcherResult.actual)}` : '')
|
||||
: () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) +
|
||||
'\n\n' +
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ test('event should work', async ({ mount }) => {
|
|||
|
||||
## How to get started
|
||||
|
||||
Adding Playwright Test to an existing project is easy. Below are the steps to enable Playwright Test for a React, Vue, Svelte or Solid project.
|
||||
Adding Playwright Test to an existing project is easy. Below are the steps to enable Playwright Test for a React, Vue or Svelte project.
|
||||
|
||||
### Step 1: Install Playwright Test for components for your respective framework
|
||||
|
||||
|
|
@ -106,7 +106,6 @@ component is mounted using this script. It can be either a `.js`, `.ts`, `.jsx`
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -168,20 +167,6 @@ test('should work', async ({ mount }) => {
|
|||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="app.spec.tsx"
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import App from './App';
|
||||
|
||||
test('should work', async ({ mount }) => {
|
||||
const component = await mount(<App />);
|
||||
await expect(component).toContainText('Learn Solid');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### Step 3. Run the tests
|
||||
|
|
@ -309,7 +294,6 @@ Provide props to a component when mounted.
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -325,17 +309,6 @@ test('props', async ({ mount }) => {
|
|||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('props', async ({ mount }) => {
|
||||
const component = await mount(<Component msg="greetings" />);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
|
|
@ -379,7 +352,6 @@ Provide callbacks/events to a component when mounted.
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -395,17 +367,6 @@ test('callback', async ({ mount }) => {
|
|||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('callback', async ({ mount }) => {
|
||||
const component = await mount(<Component onClick={() => {}} />);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
|
|
@ -449,7 +410,6 @@ Provide children/slots to a component when mounted.
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -465,17 +425,6 @@ test('children', async ({ mount }) => {
|
|||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('children', async ({ mount }) => {
|
||||
const component = await mount(<Component>Child</Component>);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
|
|
@ -519,9 +468,7 @@ You can use `beforeMount` and `afterMount` hooks to configure your app. This let
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Vue3', value: 'vue3'},
|
||||
{label: 'Vue2', value: 'vue2'},
|
||||
]
|
||||
}>
|
||||
<TabItem value="react">
|
||||
|
|
@ -555,37 +502,6 @@ You can use `beforeMount` and `afterMount` hooks to configure your app. This let
|
|||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="playwright/index.tsx"
|
||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-solid/hooks';
|
||||
import { Router } from '@solidjs/router';
|
||||
|
||||
export type HooksConfig = {
|
||||
enableRouting?: boolean;
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ App, hooksConfig }) => {
|
||||
if (hooksConfig?.enableRouting)
|
||||
return <Router><App /></Router>;
|
||||
});
|
||||
```
|
||||
|
||||
```js title="src/pages/ProductsPage.spec.tsx"
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import type { HooksConfig } from '../playwright';
|
||||
import { ProductsPage } from './pages/ProductsPage';
|
||||
|
||||
test('configure routing through hooks config', async ({ page, mount }) => {
|
||||
const component = await mount<HooksConfig>(<ProductsPage />, {
|
||||
hooksConfig: { enableRouting: true },
|
||||
});
|
||||
await expect(component.getByRole('link')).toHaveAttribute('href', '/products/42');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="vue3">
|
||||
|
||||
```js title="playwright/index.ts"
|
||||
|
|
@ -617,40 +533,6 @@ You can use `beforeMount` and `afterMount` hooks to configure your app. This let
|
|||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="vue2">
|
||||
|
||||
```js title="playwright/index.ts"
|
||||
import { beforeMount, afterMount } from '@playwright/experimental-ct-vue2/hooks';
|
||||
import Router from 'vue-router';
|
||||
import { router } from '../src/router';
|
||||
|
||||
export type HooksConfig = {
|
||||
enableRouting?: boolean;
|
||||
}
|
||||
|
||||
beforeMount<HooksConfig>(async ({ app, hooksConfig }) => {
|
||||
if (hooksConfig?.enableRouting) {
|
||||
Vue.use(Router);
|
||||
return { router }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
```js title="src/pages/ProductsPage.spec.ts"
|
||||
import { test, expect } from '@playwright/experimental-ct-vue2';
|
||||
import type { HooksConfig } from '../playwright';
|
||||
import ProductsPage from './pages/ProductsPage.vue';
|
||||
|
||||
test('configure routing through hooks config', async ({ page, mount }) => {
|
||||
const component = await mount<HooksConfig>(ProductsPage, {
|
||||
hooksConfig: { enableRouting: true },
|
||||
});
|
||||
await expect(component.getByRole('link')).toHaveAttribute('href', '/products/42');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
### unmount
|
||||
|
|
@ -661,7 +543,6 @@ Unmount the mounted component from the DOM. This is useful for testing the compo
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -678,18 +559,6 @@ test('unmount', async ({ mount }) => {
|
|||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('unmount', async ({ mount }) => {
|
||||
const component = await mount(<Component/>);
|
||||
await component.unmount();
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
|
|
@ -735,7 +604,6 @@ Update props, slots/children, and/or events/callbacks of a mounted component. Th
|
|||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -754,20 +622,6 @@ test('update', async ({ mount }) => {
|
|||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="solid">
|
||||
|
||||
```js title="component.spec.tsx"
|
||||
import { test } from '@playwright/experimental-ct-solid';
|
||||
|
||||
test('update', async ({ mount }) => {
|
||||
const component = await mount(<Component/>);
|
||||
await component.update(
|
||||
<Component msg="greetings" onClick={() => {}}>Child</Component>
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="svelte">
|
||||
|
||||
|
|
@ -855,7 +709,7 @@ test('example test', async ({ mount, router }) => {
|
|||
|
||||
## Frequently asked questions
|
||||
|
||||
### What's the difference between `@playwright/test` and `@playwright/experimental-ct-{react,svelte,vue,solid}`?
|
||||
### What's the difference between `@playwright/test` and `@playwright/experimental-ct-{react,svelte,vue}`?
|
||||
|
||||
```js
|
||||
test('…', async ({ mount, page, context }) => {
|
||||
|
|
@ -863,13 +717,12 @@ test('…', async ({ mount, page, context }) => {
|
|||
});
|
||||
```
|
||||
|
||||
`@playwright/experimental-ct-{react,svelte,vue,solid}` wrap `@playwright/test` to provide an additional built-in component-testing specific fixture called `mount`:
|
||||
`@playwright/experimental-ct-{react,svelte,vue}` wrap `@playwright/test` to provide an additional built-in component-testing specific fixture called `mount`:
|
||||
|
||||
<Tabs
|
||||
defaultValue="react"
|
||||
values={[
|
||||
{label: 'React', value: 'react'},
|
||||
{label: 'Solid', value: 'solid'},
|
||||
{label: 'Svelte', value: 'svelte'},
|
||||
{label: 'Vue', value: 'vue'},
|
||||
]
|
||||
|
|
@ -930,22 +783,6 @@ test('should work', async ({ mount }) => {
|
|||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="solid">
|
||||
|
||||
```js
|
||||
import { test, expect } from '@playwright/experimental-ct-solid';
|
||||
import HelloWorld from './HelloWorld';
|
||||
|
||||
test.use({ viewport: { width: 500, height: 500 } });
|
||||
|
||||
test('should work', async ({ mount }) => {
|
||||
const component = await mount(<HelloWorld msg="greetings" />);
|
||||
await expect(component).toContainText('Greetings');
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
||||
</Tabs>
|
||||
|
||||
Additionally, it adds some config options you can use in your `playwright-ct.config.{ts,js}`.
|
||||
|
|
|
|||
|
|
@ -4,29 +4,11 @@
|
|||
|
||||
Information about an error thrown during test execution.
|
||||
|
||||
## property: TestError.expected
|
||||
## property: TestError.cause
|
||||
* since: v1.49
|
||||
- type: ?<[string]>
|
||||
- type: ?<[TestError]>
|
||||
|
||||
Expected value formatted as a human-readable string.
|
||||
|
||||
## property: TestError.locator
|
||||
* since: v1.49
|
||||
- type: ?<[string]>
|
||||
|
||||
Receiver's locator.
|
||||
|
||||
## property: TestError.log
|
||||
* since: v1.49
|
||||
- type: ?<[Array]<[string]>>
|
||||
|
||||
Call log.
|
||||
|
||||
## property: TestError.matcherName
|
||||
* since: v1.49
|
||||
- type: ?<[string]>
|
||||
|
||||
Expect matcher name.
|
||||
Error cause. Set when there is a [cause](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause) for the error. Will be `undefined` if there is no cause or if the cause is not an instance of [Error].
|
||||
|
||||
## property: TestError.message
|
||||
* since: v1.10
|
||||
|
|
@ -34,24 +16,12 @@ Expect matcher name.
|
|||
|
||||
Error message. Set when [Error] (or its subclass) has been thrown.
|
||||
|
||||
## property: TestError.received
|
||||
* since: v1.49
|
||||
- type: ?<[string]>
|
||||
|
||||
Received value formatted as a human-readable string.
|
||||
|
||||
## property: TestError.stack
|
||||
* since: v1.10
|
||||
- type: ?<[string]>
|
||||
|
||||
Error stack. Set when [Error] (or its subclass) has been thrown.
|
||||
|
||||
## property: TestError.timeout
|
||||
* since: v1.49
|
||||
- type: ?<[int]>
|
||||
|
||||
Timeout in milliseconds, if the error was caused by a timeout.
|
||||
|
||||
## property: TestError.value
|
||||
* since: v1.10
|
||||
- type: ?<[string]>
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ See [Running Tests](./running-tests.md) for general information on `pytest` opti
|
|||
|
||||
## Examples
|
||||
|
||||
### Configure Mypy typings for auto-completion
|
||||
### Configure typings for auto-completion
|
||||
|
||||
```py title="test_my_application.py"
|
||||
from playwright.sync_api import Page
|
||||
|
|
@ -109,16 +109,23 @@ def test_visit_admin_dashboard(page: Page):
|
|||
# ...
|
||||
```
|
||||
|
||||
### Configure slow mo
|
||||
If you're using VSCode with Pylance, these types can be inferred by enabling the `python.testing.pytestEnabled` setting so you don't need the type annotation.
|
||||
|
||||
Run tests with slow mo with the `--slowmo` argument.
|
||||
### Using multiple contexts
|
||||
|
||||
```bash
|
||||
pytest --slowmo 100
|
||||
In order to simulate multiple users, you can create multiple [`BrowserContext`](./browser-contexts) instances.
|
||||
|
||||
```py title="test_my_application.py"
|
||||
from playwright.sync_api import Page, BrowserContext
|
||||
from pytest_playwright.pytest_playwright import CreateContextCallback
|
||||
|
||||
def test_foo(page: Page, new_context: CreateContextCallback) -> None:
|
||||
page.goto("https://example.com")
|
||||
context = new_context()
|
||||
page2 = context.new_page()
|
||||
# page and page2 are in different contexts
|
||||
```
|
||||
|
||||
Slows down Playwright operations by 100 milliseconds.
|
||||
|
||||
### Skip test by browser
|
||||
|
||||
```py title="test_my_application.py"
|
||||
|
|
@ -196,7 +203,7 @@ def browser_context_args(browser_context_args):
|
|||
}
|
||||
```
|
||||
|
||||
### Device emulation
|
||||
### Device emulation / BrowserContext option overrides
|
||||
|
||||
```py title="conftest.py"
|
||||
import pytest
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ test('example test', async ({ page }) => {
|
|||
});
|
||||
```
|
||||
|
||||
:::warning
|
||||
Browser rendering can vary based on the host OS, version, settings, hardware, power source (battery vs. power adapter), headless mode, and other factors. For consistent screenshots, run tests in the same environment where the baseline screenshots were generated.
|
||||
:::
|
||||
|
||||
## Generating screenshots
|
||||
|
||||
When you run above for the first time, test runner will say:
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ npx playwright test --trace on
|
|||
|
||||
## Opening the HTML report
|
||||
|
||||
The HTML report shows you a report of all your tests that have been ran and on which browsers as well as how long they took. Tests can be filtered by passed tests, failed, flakey or skipped tests. You can also search for a particular test. Clicking on a test will open the detailed view where you can see more information on your tests such as the errors, the test steps and the trace.
|
||||
The HTML report shows you a report of all your tests that have been run and on which browsers as well as how long they took. Tests can be filtered by passed tests, failed, flaky or skipped tests. You can also search for a particular test. Clicking on a test will open the detailed view where you can see more information on your tests such as the errors, the test steps and the trace.
|
||||
|
||||
```bash
|
||||
npx playwright show-report
|
||||
|
|
|
|||
382
package-lock.json
generated
382
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "playwright-internal",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
"vite": "^5.4.6",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.5.1"
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -207,6 +207,7 @@
|
|||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
|
||||
"integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
|
|
@ -233,6 +234,7 @@
|
|||
"version": "7.23.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.7.tgz",
|
||||
"integrity": "sha512-xCoqR/8+BoNnXOY7RVSgv6X+o7pmT5q1d+gGcRlXYkI+9B31glE4jeejhKVpA04O1AtzOt7OSQ6VYKP5FcRl9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.22.5",
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
|
|
@ -286,6 +288,7 @@
|
|||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz",
|
||||
"integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.23.0"
|
||||
},
|
||||
|
|
@ -326,6 +329,7 @@
|
|||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz",
|
||||
"integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
|
|
@ -345,6 +349,7 @@
|
|||
"version": "7.22.20",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz",
|
||||
"integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-environment-visitor": "^7.22.20",
|
||||
"@babel/helper-member-expression-to-functions": "^7.22.15",
|
||||
|
|
@ -372,6 +377,7 @@
|
|||
"version": "7.22.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz",
|
||||
"integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.22.5"
|
||||
},
|
||||
|
|
@ -467,6 +473,7 @@
|
|||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz",
|
||||
"integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
},
|
||||
|
|
@ -517,6 +524,7 @@
|
|||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz",
|
||||
"integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5"
|
||||
},
|
||||
|
|
@ -579,6 +587,7 @@
|
|||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz",
|
||||
"integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-module-transforms": "^7.23.3",
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
|
|
@ -721,6 +730,7 @@
|
|||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.6.tgz",
|
||||
"integrity": "sha512-6cBG5mBvUu4VUD04OHKnYzbuHNP8huDsD3EDqqpIpsswTDoqHCjLoHb6+QgsV1WsT2nipRqCPgxD3LXnEO7XfA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.22.5",
|
||||
"@babel/helper-create-class-features-plugin": "^7.23.6",
|
||||
|
|
@ -754,24 +764,6 @@
|
|||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-typescript": {
|
||||
"version": "7.23.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz",
|
||||
"integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.22.5",
|
||||
"@babel/helper-validator-option": "^7.22.15",
|
||||
"@babel/plugin-syntax-jsx": "^7.23.3",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.23.3",
|
||||
"@babel/plugin-transform-typescript": "^7.23.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.8.tgz",
|
||||
|
|
@ -1496,10 +1488,6 @@
|
|||
"resolved": "packages/playwright-ct-react17",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@playwright/experimental-ct-solid": {
|
||||
"resolved": "packages/playwright-ct-solid",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@playwright/experimental-ct-svelte": {
|
||||
"resolved": "packages/playwright-ct-svelte",
|
||||
"link": true
|
||||
|
|
@ -1508,10 +1496,6 @@
|
|||
"resolved": "packages/playwright-ct-vue",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@playwright/experimental-ct-vue2": {
|
||||
"resolved": "packages/playwright-ct-vue2",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"resolved": "packages/playwright-test",
|
||||
"link": true
|
||||
|
|
@ -2416,28 +2400,6 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-to-html": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz",
|
||||
"integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==",
|
||||
"dependencies": {
|
||||
"entities": "^2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"ansi-to-html": "bin/ansi-to-html"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-to-html/node_modules/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
|
|
@ -2642,43 +2604,6 @@
|
|||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jsx-dom-expressions": {
|
||||
"version": "0.37.13",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.37.13.tgz",
|
||||
"integrity": "sha512-oAEMMIgU0h1DmHn4ZDaBBFc08nsVJciLq9pF7g0ZdpeIDKfY4zXjXr8+/oBjKhXG8nyomhnTodPjeG+/ZXcWXQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "7.18.6",
|
||||
"@babel/plugin-syntax-jsx": "^7.18.6",
|
||||
"@babel/types": "^7.20.7",
|
||||
"html-entities": "2.3.3",
|
||||
"validate-html-nesting": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.20.12"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.18.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz",
|
||||
"integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-preset-solid": {
|
||||
"version": "1.8.9",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.8.9.tgz",
|
||||
"integrity": "sha512-1awR1QCoryXtAdnjsrx/eVBTYz+tpHUDOdBXqG9oVV7S0ojf2MV/woR0+8BG+LMXVzIr60oKYzCZ9UZGafxmpg==",
|
||||
"dependencies": {
|
||||
"babel-plugin-jsx-dom-expressions": "^0.37.13"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
|
|
@ -2936,10 +2861,11 @@
|
|||
"periscopic": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror-shadow-1": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror-shadow-1/-/codemirror-shadow-1-0.0.1.tgz",
|
||||
"integrity": "sha512-kD3OZpCCHr3LHRKfbGx5IogHTWq4Uo9jH2bXPVa7/n6ppkgI66rx4tniQY1BpqWp/JNhQmQsXhQoaZ1TH6t0xQ=="
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.65.18",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.18.tgz",
|
||||
"integrity": "sha512-Gaz4gHnkbHMGgahNt3CA5HBk5lLQBqmD/pBgeB4kQU6OedZmqMBjlRF0LSrp2tJ4wlLNPm2FfaUd1pDy0mdlpA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
|
|
@ -3076,10 +3002,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
|
|
@ -4590,11 +4517,6 @@
|
|||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-entities": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
|
||||
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="
|
||||
},
|
||||
"node_modules/html-reporter": {
|
||||
"resolved": "packages/html-reporter",
|
||||
"link": true
|
||||
|
|
@ -5063,17 +4985,6 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
|
||||
|
|
@ -5387,20 +5298,6 @@
|
|||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/merge-anything": {
|
||||
"version": "5.1.7",
|
||||
"resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz",
|
||||
"integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==",
|
||||
"dependencies": {
|
||||
"is-what": "^4.1.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
|
|
@ -5964,21 +5861,6 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.8.8",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
|
||||
|
|
@ -6462,25 +6344,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.0.4.tgz",
|
||||
"integrity": "sha512-qQs/N+KfJu83rmszFQaTxcoJoPn6KNUruX4KmnmyD0oZkUoiNvJ1rpdYKDf4YHM05k+HOgCxa3yvf15QbVijGg==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/seroval-plugins": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.0.4.tgz",
|
||||
"integrity": "sha512-DQ2IK6oQVvy8k+c2V5x5YCtUa/GGGsUwUBNN9UqohrZ0rWdUapBFpNMYP1bCyRHoxOJjdKGl+dieacFIpU/i1A==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"seroval": "^1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
|
@ -6570,37 +6433,6 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-js": {
|
||||
"version": "1.8.11",
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.11.tgz",
|
||||
"integrity": "sha512-WdwmER+TwBJiN4rVQTVBxocg+9pKlOs41KzPYntrC86xO5sek8TzBYozPEZPL1IRWDouf2lMrvSbIs3CanlPvQ==",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "^1.0.3",
|
||||
"seroval-plugins": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/solid-refresh": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.6.3.tgz",
|
||||
"integrity": "sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==",
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.23.6",
|
||||
"@babel/helper-module-imports": "^7.22.15",
|
||||
"@babel/types": "^7.23.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
|
|
@ -7168,11 +7000,6 @@
|
|||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-html-nesting": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/validate-html-nesting/-/validate-html-nesting-1.2.2.tgz",
|
||||
"integrity": "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg=="
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
|
|
@ -7241,24 +7068,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-solid": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.8.2.tgz",
|
||||
"integrity": "sha512-HcvMs6DTxBaO4kE3psnirPQBCUUdYeQkCNKuB2TpEkJsxb6BGP6/7qkbbCSMxn25PyNdjvzVi1WXi0ou8KPgHw==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@types/babel__core": "^7.20.4",
|
||||
"babel-preset-solid": "^1.8.4",
|
||||
"merge-anything": "^5.1.7",
|
||||
"solid-refresh": "^0.6.3",
|
||||
"vitefu": "^0.2.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.7.2",
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite/node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
|
|
@ -7852,10 +7661,10 @@
|
|||
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
|
||||
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
|
||||
"dev": true,
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz",
|
||||
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
|
@ -7922,16 +7731,13 @@
|
|||
}
|
||||
},
|
||||
"packages/html-reporter": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"ansi-to-html": "^0.7.2"
|
||||
}
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"packages/playwright": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7945,11 +7751,11 @@
|
|||
},
|
||||
"packages/playwright-browser-chromium": {
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7957,11 +7763,11 @@
|
|||
},
|
||||
"packages/playwright-browser-firefox": {
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
@ -7969,22 +7775,22 @@
|
|||
},
|
||||
"packages/playwright-browser-webkit": {
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-chromium": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -7994,7 +7800,7 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-core": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
|
|
@ -8005,11 +7811,11 @@
|
|||
},
|
||||
"packages/playwright-ct-core": {
|
||||
"name": "@playwright/experimental-ct-core",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.0-next",
|
||||
"playwright-core": "1.49.0-next",
|
||||
"playwright": "1.50.0-next",
|
||||
"playwright-core": "1.50.0-next",
|
||||
"vite": "^5.2.8"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -8018,10 +7824,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react": {
|
||||
"name": "@playwright/experimental-ct-react",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -8033,10 +7839,10 @@
|
|||
},
|
||||
"packages/playwright-ct-react17": {
|
||||
"name": "@playwright/experimental-ct-react17",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@vitejs/plugin-react": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -8046,30 +7852,12 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-solid": {
|
||||
"name": "@playwright/experimental-ct-solid",
|
||||
"version": "1.49.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"vite-plugin-solid": "^2.7.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"solid-js": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-svelte": {
|
||||
"name": "@playwright/experimental-ct-svelte",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -8084,10 +7872,10 @@
|
|||
},
|
||||
"packages/playwright-ct-vue": {
|
||||
"name": "@playwright/experimental-ct-vue",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@playwright/experimental-ct-core": "1.50.0-next",
|
||||
"@vitejs/plugin-vue": "^4.2.1"
|
||||
},
|
||||
"bin": {
|
||||
|
|
@ -8097,65 +7885,12 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-vue2": {
|
||||
"name": "@playwright/experimental-ct-vue2",
|
||||
"version": "1.49.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@playwright/experimental-ct-core": "1.49.0-next",
|
||||
"@vitejs/plugin-vue2": "^2.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vue": "^2.7.14"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-vue2/node_modules/@vitejs/plugin-vue2": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue2/-/plugin-vue2-2.3.1.tgz",
|
||||
"integrity": "sha512-/ksaaz2SRLN11JQhLdEUhDzOn909WEk99q9t9w+N12GjQCljzv7GyvAbD/p20aBUjHkvpGOoQ+FCOkG+mjDF4A==",
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >= 16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0",
|
||||
"vue": "^2.7.0-0"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-vue2/node_modules/@vue/compiler-sfc": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.16.tgz",
|
||||
"integrity": "sha512-KWhJ9k5nXuNtygPU7+t1rX6baZeqOYLEforUPjgNDBnLicfHCoi48H87Q8XyLZOrNNsmhuwKqtpDQWjEFe6Ekg==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.23.5",
|
||||
"postcss": "^8.4.14",
|
||||
"source-map": "^0.6.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"prettier": "^1.18.2 || ^2.0.0"
|
||||
}
|
||||
},
|
||||
"packages/playwright-ct-vue2/node_modules/vue": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.7.16.tgz",
|
||||
"integrity": "sha512-4gCtFXaAA3zYZdTp5s4Hl2sozuySsgz4jy1EnpBHNfpMa9dK1ZCG7viqBPCwXtmgc8nHqUsAu3G4gtmXkkY3Sw==",
|
||||
"deprecated": "Vue 2 has reached EOL and is no longer actively maintained. See https://v2.vuejs.org/eol/ for more details.",
|
||||
"dependencies": {
|
||||
"@vue/compiler-sfc": "2.7.16",
|
||||
"csstype": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"packages/playwright-firefox": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -8166,10 +7901,10 @@
|
|||
},
|
||||
"packages/playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.49.0-next"
|
||||
"playwright": "1.50.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -8179,11 +7914,11 @@
|
|||
}
|
||||
},
|
||||
"packages/playwright-webkit": {
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
|
|
@ -8206,7 +7941,10 @@
|
|||
}
|
||||
},
|
||||
"packages/recorder": {
|
||||
"version": "0.0.0"
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"yaml": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"packages/trace-viewer": {
|
||||
"version": "0.0.0"
|
||||
|
|
@ -8214,7 +7952,7 @@
|
|||
"packages/web": {
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"codemirror-shadow-1": "0.0.1",
|
||||
"codemirror": "5.65.18",
|
||||
"xterm": "^5.1.0",
|
||||
"xterm-addon-fit": "^0.7.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -103,6 +103,6 @@
|
|||
"vite": "^5.4.6",
|
||||
"ws": "^8.17.1",
|
||||
"xml2js": "^0.5.0",
|
||||
"yaml": "^2.5.1"
|
||||
"yaml": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,5 @@
|
|||
"dev": "vite",
|
||||
"build": "vite build && tsc",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-to-html": "^0.7.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,3 +48,14 @@ test('setExpanded is called', async ({ mount }) => {
|
|||
await component.getByText('Title').click();
|
||||
expect(expandedValues).toEqual([true]);
|
||||
});
|
||||
|
||||
test('setExpanded should work', async ({ mount }) => {
|
||||
const component = await mount(<AutoChip header='Title' initialExpanded={false}>
|
||||
Body
|
||||
</AutoChip>);
|
||||
await component.getByText('Title').click();
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- button "Title" [expanded]
|
||||
- region: Body
|
||||
`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import './colors.css';
|
|||
import './common.css';
|
||||
import * as icons from './icons';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
import { useAnchor } from './links';
|
||||
|
||||
export const Chip: React.FC<{
|
||||
header: JSX.Element | string,
|
||||
|
|
@ -28,10 +29,13 @@ export const Chip: React.FC<{
|
|||
setExpanded?: (expanded: boolean) => void,
|
||||
children?: any,
|
||||
dataTestId?: string,
|
||||
targetRef?: React.RefObject<HTMLDivElement>,
|
||||
}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId, targetRef }) => {
|
||||
return <div className='chip' data-testid={dataTestId} ref={targetRef}>
|
||||
}> = ({ header, expanded, setExpanded, children, noInsets, dataTestId }) => {
|
||||
const id = React.useId();
|
||||
return <div className='chip' data-testid={dataTestId}>
|
||||
<div
|
||||
role='button'
|
||||
aria-expanded={!!expanded}
|
||||
aria-controls={id}
|
||||
className={clsx('chip-header', setExpanded && ' expanded-' + expanded)}
|
||||
onClick={() => setExpanded?.(!expanded)}
|
||||
title={typeof header === 'string' ? header : undefined}>
|
||||
|
|
@ -39,7 +43,7 @@ export const Chip: React.FC<{
|
|||
{setExpanded && !expanded && icons.rightArrow()}
|
||||
{header}
|
||||
</div>
|
||||
{(!setExpanded || expanded) && <div className={clsx('chip-body', noInsets && 'chip-body-no-insets')}>{children}</div>}
|
||||
{(!setExpanded || expanded) && <div id={id} role='region' className={clsx('chip-body', noInsets && 'chip-body-no-insets')}>{children}</div>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
|
@ -49,16 +53,17 @@ export const AutoChip: React.FC<{
|
|||
noInsets?: boolean,
|
||||
children?: any,
|
||||
dataTestId?: string,
|
||||
targetRef?: React.RefObject<HTMLDivElement>,
|
||||
}> = ({ header, initialExpanded, noInsets, children, dataTestId, targetRef }) => {
|
||||
const [expanded, setExpanded] = React.useState(initialExpanded || initialExpanded === undefined);
|
||||
revealOnAnchorId?: string,
|
||||
}> = ({ header, initialExpanded, noInsets, children, dataTestId, revealOnAnchorId }) => {
|
||||
const [expanded, setExpanded] = React.useState(initialExpanded ?? true);
|
||||
const onReveal = React.useCallback(() => setExpanded(true), []);
|
||||
useAnchor(revealOnAnchorId, onReveal);
|
||||
return <Chip
|
||||
header={header}
|
||||
expanded={expanded}
|
||||
setExpanded={setExpanded}
|
||||
noInsets={noInsets}
|
||||
dataTestId={dataTestId}
|
||||
targetRef={targetRef}
|
||||
>
|
||||
{children}
|
||||
</Chip>;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ svg {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.d-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ test('should render counters', async ({ mount }) => {
|
|||
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
|
||||
await expect(component.locator('a', { hasText: 'Flaky' }).locator('.counter')).toHaveText('17');
|
||||
await expect(component.locator('a', { hasText: 'Skipped' }).locator('.counter')).toHaveText('10');
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- navigation:
|
||||
- link "All 90"
|
||||
- text: Passed 42 Failed 31 Flaky 17 Skipped 10
|
||||
`);
|
||||
});
|
||||
|
||||
test('should toggle filters', async ({ page, mount }) => {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import './colors.css';
|
|||
import './common.css';
|
||||
import './headerView.css';
|
||||
import * as icons from './icons';
|
||||
import { Link, navigate } from './links';
|
||||
import { Link, navigate, SearchParamsContext } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import { filterWithToken } from './filter';
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ export const HeaderView: React.FC<React.PropsWithChildren<{
|
|||
const StatsNavView: React.FC<{
|
||||
stats: Stats
|
||||
}> = ({ stats }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const q = searchParams.get('q')?.toString() || '';
|
||||
const tokens = q.split(' ');
|
||||
return <nav>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import { ReportView } from './reportView';
|
|||
const zipjs = zipImport as typeof zip;
|
||||
|
||||
import logo from '@web/assets/playwright-logo.svg';
|
||||
import { SearchParamsProvider } from './links';
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'shortcut icon';
|
||||
link.href = logo;
|
||||
|
|
@ -40,7 +41,9 @@ const ReportLoader: React.FC = () => {
|
|||
const zipReport = new ZipReport();
|
||||
zipReport.load().then(() => setReport(zipReport));
|
||||
}, [report]);
|
||||
return <ReportView report={report}></ReportView>;
|
||||
return <SearchParamsProvider>
|
||||
<ReportView report={report} />
|
||||
</SearchParamsProvider>;
|
||||
};
|
||||
|
||||
window.onload = () => {
|
||||
|
|
@ -52,7 +55,14 @@ class ZipReport implements LoadedReport {
|
|||
private _json!: HTMLReport;
|
||||
|
||||
async load() {
|
||||
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(window.playwrightReportBase64!), { useWebWorkers: false });
|
||||
const zipURI = await new Promise<string>(resolve => {
|
||||
if (window.playwrightReportBase64)
|
||||
return resolve(window.playwrightReportBase64);
|
||||
window.addEventListener('message', event => event.source === window.opener && resolve(event.data), { once: true });
|
||||
window.opener.postMessage('ready', '*');
|
||||
});
|
||||
|
||||
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(zipURI), { useWebWorkers: false });
|
||||
for (const entry of await zipReader.getEntries())
|
||||
this._entries.set(entry.filename, entry);
|
||||
this._json = await this.entry('report.json') as HTMLReport;
|
||||
|
|
|
|||
|
|
@ -33,13 +33,8 @@ export const Route: React.FunctionComponent<{
|
|||
predicate: (params: URLSearchParams) => boolean,
|
||||
children: any
|
||||
}> = ({ predicate, children }) => {
|
||||
const [matches, setMatches] = React.useState(predicate(new URLSearchParams(window.location.hash.slice(1))));
|
||||
React.useEffect(() => {
|
||||
const listener = () => setMatches(predicate(new URLSearchParams(window.location.hash.slice(1))));
|
||||
window.addEventListener('popstate', listener);
|
||||
return () => window.removeEventListener('popstate', listener);
|
||||
}, [predicate]);
|
||||
return matches ? children : null;
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
return predicate(searchParams) ? children : null;
|
||||
};
|
||||
|
||||
export const Link: React.FunctionComponent<{
|
||||
|
|
@ -90,6 +85,20 @@ export const AttachmentLink: React.FunctionComponent<{
|
|||
} : undefined} depth={0} style={{ lineHeight: '32px' }}></TreeItem>;
|
||||
};
|
||||
|
||||
export const SearchParamsContext = React.createContext<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||
|
||||
export const SearchParamsProvider: React.FunctionComponent<React.PropsWithChildren> = ({ children }) => {
|
||||
const [searchParams, setSearchParams] = React.useState<URLSearchParams>(new URLSearchParams(window.location.hash.slice(1)));
|
||||
|
||||
React.useEffect(() => {
|
||||
const listener = () => setSearchParams(new URLSearchParams(window.location.hash.slice(1)));
|
||||
window.addEventListener('popstate', listener);
|
||||
return () => window.removeEventListener('popstate', listener);
|
||||
}, []);
|
||||
|
||||
return <SearchParamsContext.Provider value={searchParams}>{children}</SearchParamsContext.Provider>;
|
||||
};
|
||||
|
||||
function downloadFileNameForAttachment(attachment: TestAttachment): string {
|
||||
if (attachment.name.includes('.') || !attachment.path)
|
||||
return attachment.name;
|
||||
|
|
@ -104,3 +113,32 @@ export function generateTraceUrl(traces: TestAttachment[]) {
|
|||
}
|
||||
|
||||
const kMissingContentType = 'x-playwright/missing';
|
||||
|
||||
type AnchorID = string | ((id: string | null) => boolean) | undefined;
|
||||
|
||||
export function useAnchor(id: AnchorID, onReveal: () => void) {
|
||||
React.useEffect(() => {
|
||||
if (typeof id === 'undefined')
|
||||
return;
|
||||
|
||||
const listener = () => {
|
||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||
const anchor = params.get('anchor');
|
||||
const isRevealed = typeof id === 'function' ? id(anchor) : anchor === id;
|
||||
if (isRevealed)
|
||||
onReveal();
|
||||
};
|
||||
window.addEventListener('popstate', listener);
|
||||
return () => window.removeEventListener('popstate', listener);
|
||||
}, [id, onReveal]);
|
||||
}
|
||||
|
||||
export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID }>) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const onAnchorReveal = React.useCallback(() => {
|
||||
requestAnimationFrame(() => ref.current?.scrollIntoView({ block: 'start', inline: 'start' }));
|
||||
}, []);
|
||||
useAnchor(id, onAnchorReveal);
|
||||
|
||||
return <div ref={ref}>{children}</div>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,19 +14,19 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { FilteredStats, TestCase, TestFile, TestFileSummary } from './types';
|
||||
import type { FilteredStats, TestCase, TestCaseSummary, TestFile, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import './colors.css';
|
||||
import './common.css';
|
||||
import { Filter } from './filter';
|
||||
import { HeaderView } from './headerView';
|
||||
import { Route } from './links';
|
||||
import { Route, SearchParamsContext } from './links';
|
||||
import type { LoadedReport } from './loadedReport';
|
||||
import './reportView.css';
|
||||
import type { Metainfo } from './metadataView';
|
||||
import { MetadataView } from './metadataView';
|
||||
import { TestCaseView } from './testCaseView';
|
||||
import { TestFilesView } from './testFilesView';
|
||||
import { TestFilesHeader, TestFilesView } from './testFilesView';
|
||||
import './theme.css';
|
||||
|
||||
declare global {
|
||||
|
|
@ -39,32 +39,55 @@ declare global {
|
|||
const testFilesRoutePredicate = (params: URLSearchParams) => !params.has('testId');
|
||||
const testCaseRoutePredicate = (params: URLSearchParams) => params.has('testId');
|
||||
|
||||
type TestModelSummary = {
|
||||
files: TestFileSummary[];
|
||||
tests: TestCaseSummary[];
|
||||
};
|
||||
|
||||
export const ReportView: React.FC<{
|
||||
report: LoadedReport | undefined,
|
||||
}> = ({ report }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const [expandedFiles, setExpandedFiles] = React.useState<Map<string, boolean>>(new Map());
|
||||
const [filterText, setFilterText] = React.useState(searchParams.get('q') || '');
|
||||
|
||||
const testIdToFileIdMap = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const file of report?.json().files || []) {
|
||||
for (const test of file.tests)
|
||||
map.set(test.testId, file.fileId);
|
||||
}
|
||||
return map;
|
||||
}, [report]);
|
||||
|
||||
const filter = React.useMemo(() => Filter.parse(filterText), [filterText]);
|
||||
const filteredStats = React.useMemo(() => computeStats(report?.json().files || [], filter), [report, filter]);
|
||||
const filteredStats = React.useMemo(() => filter.empty() ? undefined : computeStats(report?.json().files || [], filter), [report, filter]);
|
||||
const filteredTests = React.useMemo(() => {
|
||||
const result: TestModelSummary = { files: [], tests: [] };
|
||||
for (const file of report?.json().files || []) {
|
||||
const tests = file.tests.filter(t => filter.matches(t));
|
||||
if (tests.length)
|
||||
result.files.push({ ...file, tests });
|
||||
result.tests.push(...tests);
|
||||
}
|
||||
return result;
|
||||
}, [report, filter]);
|
||||
|
||||
return <div className='htmlreport vbox px-4 pb-4'>
|
||||
<main>
|
||||
{report?.json() && <HeaderView stats={report.json().stats} filterText={filterText} setFilterText={setFilterText}></HeaderView>}
|
||||
{report?.json().metadata && <MetadataView {...report?.json().metadata as Metainfo} />}
|
||||
<Route predicate={testFilesRoutePredicate}>
|
||||
<TestFilesHeader report={report?.json()} filteredStats={filteredStats} />
|
||||
<TestFilesView
|
||||
report={report?.json()}
|
||||
filter={filter}
|
||||
tests={filteredTests.files}
|
||||
expandedFiles={expandedFiles}
|
||||
setExpandedFiles={setExpandedFiles}
|
||||
projectNames={report?.json().projectNames || []}
|
||||
filteredStats={filteredStats}
|
||||
/>
|
||||
</Route>
|
||||
<Route predicate={testCaseRoutePredicate}>
|
||||
{!!report && <TestCaseViewLoader report={report}></TestCaseViewLoader>}
|
||||
{!!report && <TestCaseViewLoader report={report} tests={filteredTests.tests} testIdToFileIdMap={testIdToFileIdMap} />}
|
||||
</Route>
|
||||
</main>
|
||||
</div>;
|
||||
|
|
@ -72,21 +95,20 @@ export const ReportView: React.FC<{
|
|||
|
||||
const TestCaseViewLoader: React.FC<{
|
||||
report: LoadedReport,
|
||||
}> = ({ report }) => {
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
tests: TestCaseSummary[],
|
||||
testIdToFileIdMap: Map<string, string>,
|
||||
}> = ({ report, testIdToFileIdMap, tests }) => {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const [test, setTest] = React.useState<TestCase | undefined>();
|
||||
const testId = searchParams.get('testId');
|
||||
const anchor = (searchParams.get('anchor') || '') as 'video' | 'diff' | '';
|
||||
const run = +(searchParams.get('run') || '0');
|
||||
|
||||
const testIdToFileIdMap = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const file of report.json().files) {
|
||||
for (const test of file.tests)
|
||||
map.set(test.testId, file.fileId);
|
||||
}
|
||||
return map;
|
||||
}, [report]);
|
||||
const { prev, next } = React.useMemo(() => {
|
||||
const index = tests.findIndex(t => t.testId === testId);
|
||||
const prev = index > 0 ? tests[index - 1] : undefined;
|
||||
const next = index < tests.length - 1 ? tests[index + 1] : undefined;
|
||||
return { prev, next };
|
||||
}, [testId, tests]);
|
||||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
|
|
@ -104,7 +126,14 @@ const TestCaseViewLoader: React.FC<{
|
|||
}
|
||||
})();
|
||||
}, [test, report, testId, testIdToFileIdMap]);
|
||||
return <TestCaseView projectNames={report.json().projectNames} test={test} anchor={anchor} run={run}></TestCaseView>;
|
||||
|
||||
return <TestCaseView
|
||||
projectNames={report.json().projectNames}
|
||||
next={next}
|
||||
prev={prev}
|
||||
test={test}
|
||||
run={run}
|
||||
/>;
|
||||
};
|
||||
|
||||
function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
|
||||
|
|
@ -119,4 +148,4 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
|
|||
stats.duration += test.duration;
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
.test-case-column {
|
||||
border-radius: 6px;
|
||||
margin: 24px 0;
|
||||
margin: 12px 0 24px 0;
|
||||
}
|
||||
|
||||
.test-case-column .tab-element.selected {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import { TestCaseView } from './testCaseView';
|
||||
import type { TestCase, TestResult } from './types';
|
||||
import type { TestCase, TestCaseSummary, TestResult } from './types';
|
||||
|
||||
test.use({ viewport: { width: 800, height: 600 } });
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ const testCase: TestCase = {
|
|||
};
|
||||
|
||||
test('should render test case', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await expect(component.getByText('Hidden annotation')).toBeHidden();
|
||||
await component.getByText('Annotations').click();
|
||||
|
|
@ -79,7 +79,7 @@ test('should render test case', async ({ mount }) => {
|
|||
test('should render copy buttons for annotations', async ({ mount, page, context }) => {
|
||||
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={testCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await expect(component.getByText('Annotation text', { exact: false }).first()).toBeVisible();
|
||||
await component.getByText('Annotation text', { exact: false }).first().hover();
|
||||
await expect(component.locator('.test-case-annotation').getByLabel('Copy to clipboard').first()).toBeVisible();
|
||||
|
|
@ -108,7 +108,7 @@ const annotationLinkRenderingTestCase: TestCase = {
|
|||
};
|
||||
|
||||
test('should correctly render links in annotations', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={annotationLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
|
||||
const firstLink = await component.getByText('https://playwright.dev/docs/intro').first();
|
||||
await expect(firstLink).toBeVisible();
|
||||
|
|
@ -154,6 +154,20 @@ const resultWithAttachment: TestResult = {
|
|||
const attachmentLinkRenderingTestCase: TestCase = {
|
||||
testId: 'testid',
|
||||
title: 'My test',
|
||||
path: ['group'],
|
||||
projectName: 'chromium',
|
||||
location: { file: 'test.spec.ts', line: 42, column: 0 },
|
||||
tags: [],
|
||||
outcome: 'expected',
|
||||
duration: 10,
|
||||
ok: true,
|
||||
annotations: [],
|
||||
results: [resultWithAttachment]
|
||||
};
|
||||
|
||||
const testCaseSummary: TestCaseSummary = {
|
||||
testId: 'nextTestId',
|
||||
title: 'next test',
|
||||
path: [],
|
||||
projectName: 'chromium',
|
||||
location: { file: 'test.spec.ts', line: 42, column: 0 },
|
||||
|
|
@ -165,18 +179,36 @@ const attachmentLinkRenderingTestCase: TestCase = {
|
|||
results: [resultWithAttachment]
|
||||
};
|
||||
|
||||
|
||||
test('should correctly render links in attachments', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
await component.getByText('first attachment').click();
|
||||
const body = await component.getByText('The body with https://playwright.dev/docs/intro link');
|
||||
await expect(body).toBeVisible();
|
||||
await expect(body.locator('a').filter({ hasText: 'playwright.dev' })).toHaveAttribute('href', 'https://playwright.dev/docs/intro');
|
||||
await expect(body.locator('a').filter({ hasText: 'github.com' })).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- link "https://playwright.dev/docs/intro"
|
||||
- link "https://github.com/microsoft/playwright/issues/31284"
|
||||
`);
|
||||
});
|
||||
|
||||
test('should correctly render links in attachment name', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} run={0} anchor=''></TestCaseView>);
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={undefined} next={undefined} run={0}></TestCaseView>);
|
||||
const link = component.getByText('attachment with inline link').locator('a');
|
||||
await expect(link).toHaveAttribute('href', 'https://github.com/microsoft/playwright/issues/31284');
|
||||
await expect(link).toHaveText('https://github.com/microsoft/playwright/issues/31284');
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- link /https:\\/\\/github\\.com\\/microsoft\\/playwright\\/issues\\/\\d+/
|
||||
`);
|
||||
});
|
||||
|
||||
test('should correctly render prev and next', async ({ mount }) => {
|
||||
const component = await mount(<TestCaseView projectNames={['chromium', 'webkit']} test={attachmentLinkRenderingTestCase} prev={testCaseSummary} next={testCaseSummary} run={0}></TestCaseView>);
|
||||
await expect(component).toMatchAriaSnapshot(`
|
||||
- text: group
|
||||
- link "« previous"
|
||||
- link "next »"
|
||||
- text: "My test test.spec.ts:42 10ms"
|
||||
`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { TestCase, TestCaseAnnotation } from './types';
|
||||
import type { TestCase, TestCaseAnnotation, TestCaseSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import { TabbedPane } from './tabbedPane';
|
||||
import { AutoChip } from './chip';
|
||||
import './common.css';
|
||||
import { ProjectLink } from './links';
|
||||
import { Link, ProjectLink, SearchParamsContext } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testCaseView.css';
|
||||
import { TestResultView } from './testResultView';
|
||||
|
|
@ -31,10 +31,13 @@ import { CopyToClipboardContainer } from './copyToClipboard';
|
|||
export const TestCaseView: React.FC<{
|
||||
projectNames: string[],
|
||||
test: TestCase | undefined,
|
||||
anchor: 'video' | 'diff' | '',
|
||||
next: TestCaseSummary | undefined,
|
||||
prev: TestCaseSummary | undefined,
|
||||
run: number,
|
||||
}> = ({ projectNames, test, run, anchor }) => {
|
||||
}> = ({ projectNames, test, run, next, prev }) => {
|
||||
const [selectedResultIndex, setSelectedResultIndex] = React.useState(run);
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
|
||||
|
||||
const labels = React.useMemo(() => {
|
||||
if (!test)
|
||||
|
|
@ -47,7 +50,13 @@ export const TestCaseView: React.FC<{
|
|||
}, [test?.annotations]);
|
||||
|
||||
return <div className='test-case-column vbox'>
|
||||
{test && <div className='test-case-path'>{test.path.join(' › ')}</div>}
|
||||
{test && <div className='hbox'>
|
||||
<div className='test-case-path'>{test.path.join(' › ')}</div>
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div className={clsx(!prev && 'hidden')}><Link href={`#?testId=${prev?.testId}${filterParam}`}>« previous</Link></div>
|
||||
<div style={{ width: 10 }}></div>
|
||||
<div className={clsx(!next && 'hidden')}><Link href={`#?testId=${next?.testId}${filterParam}`}>next »</Link></div>
|
||||
</div>}
|
||||
{test && <div className='test-case-title'>{test?.title}</div>}
|
||||
{test && <div className='hbox'>
|
||||
<div className='test-case-location'>
|
||||
|
|
@ -69,7 +78,7 @@ export const TestCaseView: React.FC<{
|
|||
test.results.map((result, index) => ({
|
||||
id: String(index),
|
||||
title: <div style={{ display: 'flex', alignItems: 'center' }}>{statusIcon(result.status)} {retryLabel(index)}</div>,
|
||||
render: () => <TestResultView test={test!} result={result} anchor={anchor}></TestResultView>
|
||||
render: () => <TestResultView test={test!} result={result} />
|
||||
})) || []} selectedTab={String(selectedResultIndex)} setSelectedTab={id => setSelectedResultIndex(+id)} />}
|
||||
</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
@import '@web/third_party/vscode/colors.css';
|
||||
|
||||
.test-error-view {
|
||||
white-space: pre;
|
||||
overflow: auto;
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import ansi2html from 'ansi-to-html';
|
||||
import { ansi2html } from '@web/ansi2html';
|
||||
import * as React from 'react';
|
||||
import './testErrorView.css';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
|
|
@ -25,7 +25,7 @@ export const TestErrorView: React.FC<{
|
|||
testId?: string;
|
||||
}> = ({ error, testId }) => {
|
||||
const html = React.useMemo(() => ansiErrorToHtml(error), [error]);
|
||||
return <div className='test-error-view test-error-text' data-testId={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
return <div className='test-error-view test-error-text' data-testid={testId} dangerouslySetInnerHTML={{ __html: html || '' }}></div>;
|
||||
};
|
||||
|
||||
export const TestScreenshotErrorView: React.FC<{
|
||||
|
|
@ -43,33 +43,9 @@ export const TestScreenshotErrorView: React.FC<{
|
|||
};
|
||||
|
||||
function ansiErrorToHtml(text?: string): string {
|
||||
const config: any = {
|
||||
const defaultColors = {
|
||||
bg: 'var(--color-canvas-subtle)',
|
||||
fg: 'var(--color-fg-default)',
|
||||
};
|
||||
config.colors = ansiColors;
|
||||
return new ansi2html(config).toHtml(escapeHTML(text || ''));
|
||||
}
|
||||
|
||||
const ansiColors = {
|
||||
0: '#000',
|
||||
1: '#C00',
|
||||
2: '#0C0',
|
||||
3: '#C50',
|
||||
4: '#00C',
|
||||
5: '#C0C',
|
||||
6: '#0CC',
|
||||
7: '#CCC',
|
||||
8: '#555',
|
||||
9: '#F55',
|
||||
10: '#5F5',
|
||||
11: '#FF5',
|
||||
12: '#55F',
|
||||
13: '#F5F',
|
||||
14: '#5FF',
|
||||
15: '#FFF'
|
||||
};
|
||||
|
||||
function escapeHTML(text: string): string {
|
||||
return text.replace(/[&"<>]/g, c => ({ '&': '&', '"': '"', '<': '<', '>': '>' }[c]!));
|
||||
return ansi2html(text || '', defaultColors);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,24 +14,25 @@
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type { HTMLReport, TestCaseSummary, TestFileSummary } from './types';
|
||||
import type { TestCaseSummary, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import { hashStringToInt, msToString } from './utils';
|
||||
import { Chip } from './chip';
|
||||
import { filterWithToken, type Filter } from './filter';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink } from './links';
|
||||
import { filterWithToken } from './filter';
|
||||
import { generateTraceUrl, Link, navigate, ProjectLink, SearchParamsContext } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import './testFileView.css';
|
||||
import { video, image, trace } from './icons';
|
||||
import { clsx } from '@web/uiUtils';
|
||||
|
||||
export const TestFileView: React.FC<React.PropsWithChildren<{
|
||||
report: HTMLReport;
|
||||
file: TestFileSummary;
|
||||
projectNames: string[];
|
||||
isFileExpanded: (fileId: string) => boolean;
|
||||
setFileExpanded: (fileId: string, expanded: boolean) => void;
|
||||
filter: Filter;
|
||||
}>> = ({ file, report, isFileExpanded, setFileExpanded, filter }) => {
|
||||
}>> = ({ file, projectNames, isFileExpanded, setFileExpanded }) => {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
|
||||
return <Chip
|
||||
expanded={isFileExpanded(file.fileId)}
|
||||
noInsets={true}
|
||||
|
|
@ -39,7 +40,7 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
header={<span>
|
||||
{file.fileName}
|
||||
</span>}>
|
||||
{file.tests.filter(t => filter.matches(t)).map(test =>
|
||||
{file.tests.map(test =>
|
||||
<div key={`test-${test.testId}`} className={clsx('test-file-test', 'test-file-test-outcome-' + test.outcome)}>
|
||||
<div className='hbox' style={{ alignItems: 'flex-start' }}>
|
||||
<div className='hbox'>
|
||||
|
|
@ -47,11 +48,11 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
{statusIcon(test.outcome)}
|
||||
</span>
|
||||
<span>
|
||||
<Link href={`#?testId=${test.testId}`} title={[...test.path, test.title].join(' › ')}>
|
||||
<Link href={`#?testId=${test.testId}${filterParam}`} title={[...test.path, test.title].join(' › ')}>
|
||||
<span className='test-file-title'>{[...test.path, test.title].join(' › ')}</span>
|
||||
</Link>
|
||||
{report.projectNames.length > 1 && !!test.projectName &&
|
||||
<ProjectLink projectNames={report.projectNames} projectName={test.projectName} />}
|
||||
{projectNames.length > 1 && !!test.projectName &&
|
||||
<ProjectLink projectNames={projectNames} projectName={test.projectName} />}
|
||||
<LabelsClickView labels={test.tags} />
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -74,12 +75,12 @@ function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
|
|||
const resultWithImageDiff = test.results.find(result => result.attachments.some(attachment => {
|
||||
return attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/);
|
||||
}));
|
||||
return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
|
||||
return resultWithImageDiff ? <Link href={`#?testId=${test.testId}&anchor=diff-0&run=${test.results.indexOf(resultWithImageDiff)}`} title='View images' className='test-file-badge'>{image()}</Link> : undefined;
|
||||
}
|
||||
|
||||
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
|
||||
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=video&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||
return resultWithVideo ? <Link href={`#?testId=${test.testId}&anchor=videos&run=${test.results.indexOf(resultWithVideo)}`} title='View video' className='test-file-badge'>{video()}</Link> : undefined;
|
||||
}
|
||||
|
||||
function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
|
|
@ -90,10 +91,10 @@ function traceBadge(test: TestCaseSummary): JSX.Element | undefined {
|
|||
const LabelsClickView: React.FC<React.PropsWithChildren<{
|
||||
labels: string[],
|
||||
}>> = ({ labels }) => {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
|
||||
const onClickHandle = (e: React.MouseEvent, label: string) => {
|
||||
e.preventDefault();
|
||||
const searchParams = new URLSearchParams(window.location.hash.slice(1));
|
||||
const q = searchParams.get('q')?.toString() || '';
|
||||
const tokens = q.split(' ');
|
||||
navigate(filterWithToken(tokens, label, e.metaKey || e.ctrlKey));
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
import type { FilteredStats, HTMLReport, TestFileSummary } from './types';
|
||||
import * as React from 'react';
|
||||
import type { Filter } from './filter';
|
||||
import { TestFileView } from './testFileView';
|
||||
import './testFileView.css';
|
||||
import { msToString } from './utils';
|
||||
|
|
@ -24,40 +23,26 @@ import { AutoChip } from './chip';
|
|||
import { TestErrorView } from './testErrorView';
|
||||
|
||||
export const TestFilesView: React.FC<{
|
||||
report?: HTMLReport,
|
||||
tests: TestFileSummary[],
|
||||
expandedFiles: Map<string, boolean>,
|
||||
setExpandedFiles: (value: Map<string, boolean>) => void,
|
||||
filter: Filter,
|
||||
filteredStats: FilteredStats,
|
||||
projectNames: string[],
|
||||
}> = ({ report, filter, expandedFiles, setExpandedFiles, projectNames, filteredStats }) => {
|
||||
}> = ({ tests, expandedFiles, setExpandedFiles, projectNames }) => {
|
||||
const filteredFiles = React.useMemo(() => {
|
||||
const result: { file: TestFileSummary, defaultExpanded: boolean }[] = [];
|
||||
let visibleTests = 0;
|
||||
for (const file of report?.files || []) {
|
||||
const tests = file.tests.filter(t => filter.matches(t));
|
||||
visibleTests += tests.length;
|
||||
if (tests.length)
|
||||
result.push({ file, defaultExpanded: visibleTests < 200 });
|
||||
for (const file of tests) {
|
||||
visibleTests += file.tests.length;
|
||||
result.push({ file, defaultExpanded: visibleTests < 200 });
|
||||
}
|
||||
return result;
|
||||
}, [report, filter]);
|
||||
}, [tests]);
|
||||
return <>
|
||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
||||
{projectNames.length === 1 && !!projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {projectNames[0]}</div>}
|
||||
{!filter.empty() && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report?.duration ?? 0)}</div>
|
||||
</div>
|
||||
{report && !!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
{report && filteredFiles.map(({ file, defaultExpanded }) => {
|
||||
{filteredFiles.map(({ file, defaultExpanded }) => {
|
||||
return <TestFileView
|
||||
key={`file-${file.fileId}`}
|
||||
report={report}
|
||||
file={file}
|
||||
projectNames={projectNames}
|
||||
isFileExpanded={fileId => {
|
||||
const value = expandedFiles.get(fileId);
|
||||
if (value === undefined)
|
||||
|
|
@ -68,9 +53,28 @@ export const TestFilesView: React.FC<{
|
|||
const newExpanded = new Map(expandedFiles);
|
||||
newExpanded.set(fileId, expanded);
|
||||
setExpandedFiles(newExpanded);
|
||||
}}
|
||||
filter={filter}>
|
||||
}}>
|
||||
</TestFileView>;
|
||||
})}
|
||||
</>;
|
||||
};
|
||||
|
||||
export const TestFilesHeader: React.FC<{
|
||||
report: HTMLReport | undefined,
|
||||
filteredStats?: FilteredStats,
|
||||
}> = ({ report, filteredStats }) => {
|
||||
if (!report)
|
||||
return;
|
||||
return <>
|
||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||
{filteredStats && <div data-testid='filtered-tests-count' style={{ color: 'var(--color-fg-subtle)', padding: '0 10px' }}>Filtered: {filteredStats.total} {!!filteredStats.total && ('(' + msToString(filteredStats.duration) + ')')}</div>}
|
||||
<div style={{ flex: 'auto' }}></div>
|
||||
<div data-testid='overall-time' style={{ color: 'var(--color-fg-subtle)', marginRight: '10px' }}>{report ? new Date(report.startTime).toLocaleString() : ''}</div>
|
||||
<div data-testid='overall-duration' style={{ color: 'var(--color-fg-subtle)' }}>Total time: {msToString(report.duration ?? 0)}</div>
|
||||
</div>
|
||||
{!!report.errors.length && <AutoChip header='Errors' dataTestId='report-errors'>
|
||||
{report.errors.map((error, index) => <TestErrorView key={'test-report-error-message-' + index} error={error}></TestErrorView>)}
|
||||
</AutoChip>}
|
||||
</>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import { TreeItem } from './treeItem';
|
|||
import { msToString } from './utils';
|
||||
import { AutoChip } from './chip';
|
||||
import { traceImage } from './images';
|
||||
import { AttachmentLink, generateTraceUrl } from './links';
|
||||
import { Anchor, AttachmentLink, generateTraceUrl } from './links';
|
||||
import { statusIcon } from './statusIcon';
|
||||
import type { ImageDiff } from '@web/shared/imageDiffView';
|
||||
import { ImageDiffView } from '@web/shared/imageDiffView';
|
||||
|
|
@ -65,9 +65,7 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiff[] {
|
|||
export const TestResultView: React.FC<{
|
||||
test: TestCase,
|
||||
result: TestResult,
|
||||
anchor: 'video' | 'diff' | '',
|
||||
}> = ({ result, anchor }) => {
|
||||
|
||||
}> = ({ result }) => {
|
||||
const { screenshots, videos, traces, otherAttachments, diffs, errors, htmls } = React.useMemo(() => {
|
||||
const attachments = result?.attachments || [];
|
||||
const screenshots = new Set(attachments.filter(a => a.contentType.startsWith('image/')));
|
||||
|
|
@ -81,20 +79,6 @@ export const TestResultView: React.FC<{
|
|||
return { screenshots: [...screenshots], videos, traces, otherAttachments, diffs, errors, htmls };
|
||||
}, [result]);
|
||||
|
||||
const videoRef = React.useRef<HTMLDivElement>(null);
|
||||
const imageDiffRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const [scrolled, setScrolled] = React.useState(false);
|
||||
React.useEffect(() => {
|
||||
if (scrolled)
|
||||
return;
|
||||
setScrolled(true);
|
||||
if (anchor === 'video')
|
||||
videoRef.current?.scrollIntoView({ block: 'start', inline: 'start' });
|
||||
if (anchor === 'diff')
|
||||
imageDiffRef.current?.scrollIntoView({ block: 'start', inline: 'start' });
|
||||
}, [scrolled, anchor, setScrolled, videoRef]);
|
||||
|
||||
return <div className='test-result'>
|
||||
{!!errors.length && <AutoChip header='Errors'>
|
||||
{errors.map((error, index) => {
|
||||
|
|
@ -108,9 +92,11 @@ export const TestResultView: React.FC<{
|
|||
</AutoChip>}
|
||||
|
||||
{diffs.map((diff, index) =>
|
||||
<AutoChip key={`diff-${index}`} dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} targetRef={imageDiffRef}>
|
||||
<ImageDiffView key='image-diff' diff={diff}></ImageDiffView>
|
||||
</AutoChip>
|
||||
<Anchor key={`diff-${index}`} id={`diff-${index}`}>
|
||||
<AutoChip dataTestId='test-results-image-diff' header={`Image mismatch: ${diff.name}`} revealOnAnchorId={`diff-${index}`}>
|
||||
<ImageDiffView diff={diff}/>
|
||||
</AutoChip>
|
||||
</Anchor>
|
||||
)}
|
||||
|
||||
{!!screenshots.length && <AutoChip header='Screenshots'>
|
||||
|
|
@ -124,23 +110,23 @@ export const TestResultView: React.FC<{
|
|||
})}
|
||||
</AutoChip>}
|
||||
|
||||
{!!traces.length && <AutoChip header='Traces'>
|
||||
{!!traces.length && <Anchor id='traces'><AutoChip header='Traces' revealOnAnchorId='traces'>
|
||||
{<div>
|
||||
<a href={generateTraceUrl(traces)}>
|
||||
<img className='screenshot' src={traceImage} style={{ width: 192, height: 117, marginLeft: 20 }} />
|
||||
</a>
|
||||
{traces.map((a, i) => <AttachmentLink key={`trace-${i}`} attachment={a} linkName={traces.length === 1 ? 'trace' : `trace-${i + 1}`}></AttachmentLink>)}
|
||||
</div>}
|
||||
</AutoChip>}
|
||||
</AutoChip></Anchor>}
|
||||
|
||||
{!!videos.length && <AutoChip header='Videos' targetRef={videoRef}>
|
||||
{!!videos.length && <Anchor id='videos'><AutoChip header='Videos' revealOnAnchorId='videos'>
|
||||
{videos.map((a, i) => <div key={`video-${i}`}>
|
||||
<video controls>
|
||||
<source src={a.path} type={a.contentType}/>
|
||||
</video>
|
||||
<AttachmentLink attachment={a}></AttachmentLink>
|
||||
</div>)}
|
||||
</AutoChip>}
|
||||
</AutoChip></Anchor>}
|
||||
|
||||
{!!(otherAttachments.size + htmls.length) && <AutoChip header='Attachments'>
|
||||
{[...htmls].map((a, i) => (
|
||||
|
|
@ -153,7 +139,8 @@ export const TestResultView: React.FC<{
|
|||
|
||||
function classifyErrors(testErrors: string[], diffs: ImageDiff[]) {
|
||||
return testErrors.map(error => {
|
||||
if (error.includes('Screenshot comparison failed:')) {
|
||||
const firstLine = error.split('\n')[0];
|
||||
if (firstLine.includes('toHaveScreenshot') || firstLine.includes('toMatchSnapshot')) {
|
||||
const matchingDiff = diffs.find(diff => {
|
||||
const attachmentName = diff.actual?.attachment.name;
|
||||
return attachmentName && error.includes(attachmentName);
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ try {
|
|||
}
|
||||
|
||||
if (install)
|
||||
install(['chromium', 'ffmpeg']);
|
||||
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-chromium",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"description": "Playwright package that automatically installs Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-firefox",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"description": "Playwright package that automatically installs Firefox",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/browser-webkit",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"description": "Playwright package that automatically installs WebKit",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -27,6 +27,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,4 +24,4 @@ try {
|
|||
}
|
||||
|
||||
if (install)
|
||||
install(['chromium', 'ffmpeg']);
|
||||
install(['chromium', 'chromium-headless-shell', 'ffmpeg']);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -30,6 +30,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "1.49.0-next"
|
||||
"playwright-core": "1.50.0-next"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,13 @@ This project incorporates components from the projects listed below. The origina
|
|||
- balanced-match@1.0.2 (https://github.com/juliangruber/balanced-match)
|
||||
- brace-expansion@1.1.11 (https://github.com/juliangruber/brace-expansion)
|
||||
- buffer-crc32@0.2.13 (https://github.com/brianloveswords/buffer-crc32)
|
||||
- codemirror-shadow-1@0.0.1 (https://github.com/codemirror/CodeMirror)
|
||||
- codemirror@5.65.18 (https://github.com/codemirror/CodeMirror)
|
||||
- colors@1.4.0 (https://github.com/Marak/colors.js)
|
||||
- commander@8.3.0 (https://github.com/tj/commander.js)
|
||||
- concat-map@0.0.1 (https://github.com/substack/node-concat-map)
|
||||
- debug@4.3.4 (https://github.com/debug-js/debug)
|
||||
- define-lazy-prop@2.0.0 (https://github.com/sindresorhus/define-lazy-prop)
|
||||
- diff-match-patch@1.0.5 (https://github.com/JackuB/diff-match-patch)
|
||||
- diff@7.0.0 (https://github.com/kpdecker/jsdiff)
|
||||
- dotenv@16.4.5 (https://github.com/motdotla/dotenv)
|
||||
- end-of-stream@1.4.4 (https://github.com/mafintosh/end-of-stream)
|
||||
- escape-string-regexp@2.0.0 (https://github.com/sindresorhus/escape-string-regexp)
|
||||
|
|
@ -208,7 +208,7 @@ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEAL
|
|||
=========================================
|
||||
END OF buffer-crc32@0.2.13 AND INFORMATION
|
||||
|
||||
%% codemirror-shadow-1@0.0.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% codemirror@5.65.18 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
MIT License
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF codemirror-shadow-1@0.0.1 AND INFORMATION
|
||||
END OF codemirror@5.65.18 AND INFORMATION
|
||||
|
||||
%% colors@1.4.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
@ -352,211 +352,39 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|||
=========================================
|
||||
END OF define-lazy-prop@2.0.0 AND INFORMATION
|
||||
|
||||
%% diff-match-patch@1.0.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
%% diff@7.0.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
BSD 3-Clause License
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (c) 2009-2015, Kevin Decker <kpdecker@gmail.com>
|
||||
All rights reserved.
|
||||
|
||||
1. Definitions.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
"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.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
"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.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
=========================================
|
||||
END OF diff-match-patch@1.0.5 AND INFORMATION
|
||||
END OF diff@7.0.0 AND INFORMATION
|
||||
|
||||
%% dotenv@16.4.5 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@ if [[ $(arch) == "aarch64" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old beta if any.
|
||||
|
|
|
|||
|
|
@ -7,15 +7,17 @@ if [[ $(arch) == "aarch64" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old stable if any.
|
||||
|
|
|
|||
|
|
@ -8,15 +8,17 @@ if [[ $(arch) == "aarch64" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old beta if any.
|
||||
|
|
|
|||
|
|
@ -8,15 +8,17 @@ if [[ $(arch) == "aarch64" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old dev if any.
|
||||
|
|
|
|||
|
|
@ -8,15 +8,17 @@ if [[ $(arch) == "aarch64" ]]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
if [ -z "$PLAYWRIGHT_HOST_PLATFORM_OVERRIDE" ]; then
|
||||
if [[ ! -f "/etc/os-release" ]]; then
|
||||
echo "ERROR: cannot install on unknown linux distribution (/etc/os-release is missing)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
ID=$(bash -c 'source /etc/os-release && echo $ID')
|
||||
if [[ "${ID}" != "ubuntu" && "${ID}" != "debian" ]]; then
|
||||
echo "ERROR: cannot install on $ID distribution - only Ubuntu and Debian are supported"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old stable if any.
|
||||
|
|
|
|||
|
|
@ -3,31 +3,37 @@
|
|||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1143",
|
||||
"revision": "1149",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "131.0.6778.3"
|
||||
"browserVersion": "132.0.6834.6"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1149",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "132.0.6834.6"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1271",
|
||||
"revision": "1279",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "132.0.6791.0"
|
||||
"browserVersion": "133.0.6846.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1465",
|
||||
"revision": "1466",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "131.0"
|
||||
"browserVersion": "132.0"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1465",
|
||||
"revision": "1466",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "132.0b8"
|
||||
"browserVersion": "133.0b9"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2095",
|
||||
"revision": "2104",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac10.14": "1446",
|
||||
|
|
@ -39,7 +45,7 @@
|
|||
"ubuntu20.04-x64": "2092",
|
||||
"ubuntu20.04-arm64": "2092"
|
||||
},
|
||||
"browserVersion": "18.0"
|
||||
"browserVersion": "18.2"
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
|
|
@ -27,11 +27,11 @@
|
|||
"socks-proxy-agent": "8.0.4",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.5.1"
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/minimatch": "^3.0.5",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
|
|
@ -51,11 +51,12 @@
|
|||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
|
||||
"dev": true
|
||||
"node_modules/@types/diff": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz",
|
||||
"integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "2.0.3",
|
||||
|
|
@ -209,10 +210,14 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
|
||||
"node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.5",
|
||||
|
|
@ -469,10 +474,10 @@
|
|||
"@types/ms": "*"
|
||||
}
|
||||
},
|
||||
"@types/diff-match-patch": {
|
||||
"version": "1.0.36",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz",
|
||||
"integrity": "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==",
|
||||
"@types/diff": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/diff/-/diff-6.0.0.tgz",
|
||||
"integrity": "sha512-dhVCYGv3ZSbzmQaBSagrv1WJ6rXCdkyTcDyoNu1MD8JohI7pR7k8wdZEm+mvdxRKXyHVwckFzWU1vJc+Z29MlA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
|
|
@ -606,10 +611,10 @@
|
|||
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
|
||||
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="
|
||||
},
|
||||
"diff-match-patch": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz",
|
||||
"integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw=="
|
||||
"diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.4.5",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"colors": "1.4.0",
|
||||
"commander": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"diff": "^7.0.0",
|
||||
"dotenv": "^16.4.5",
|
||||
"graceful-fs": "4.2.10",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
|
|
@ -28,11 +28,11 @@
|
|||
"socks-proxy-agent": "8.0.4",
|
||||
"stack-utils": "2.0.5",
|
||||
"ws": "8.17.1",
|
||||
"yaml": "^2.5.1"
|
||||
"yaml": "^2.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/diff-match-patch": "^1.0.36",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/mime": "^2.0.3",
|
||||
"@types/minimatch": "^3.0.5",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ export const colors = colorsLibrary;
|
|||
import debugLibrary from 'debug';
|
||||
export const debug = debugLibrary;
|
||||
|
||||
import diffMatchPatchLibrary from 'diff-match-patch';
|
||||
export const diffMatchPatch = diffMatchPatchLibrary;
|
||||
import * as diffLibrary from 'diff';
|
||||
export const diff = diffLibrary;
|
||||
|
||||
import dotenvLibrary from 'dotenv';
|
||||
export const dotenv = dotenvLibrary;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.49.0-next",
|
||||
"version": "1.50.0-next",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -96,16 +96,42 @@ function suggestedBrowsersToInstall() {
|
|||
return registry.executables().filter(e => e.installType !== 'none' && e.type !== 'tool').map(e => e.name).join(', ');
|
||||
}
|
||||
|
||||
function checkBrowsersToInstall(args: string[]): Executable[] {
|
||||
function defaultBrowsersToInstall(options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
|
||||
let executables = registry.defaultExecutables();
|
||||
if (options.noShell)
|
||||
executables = executables.filter(e => e.name !== 'chromium-headless-shell');
|
||||
if (options.onlyShell)
|
||||
executables = executables.filter(e => e.name !== 'chromium');
|
||||
return executables;
|
||||
}
|
||||
|
||||
function checkBrowsersToInstall(args: string[], options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
|
||||
if (options.noShell && options.onlyShell)
|
||||
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
|
||||
|
||||
const faultyArguments: string[] = [];
|
||||
const executables: Executable[] = [];
|
||||
for (const arg of args) {
|
||||
const handleArgument = (arg: string) => {
|
||||
const executable = registry.findExecutable(arg);
|
||||
if (!executable || executable.installType === 'none')
|
||||
faultyArguments.push(arg);
|
||||
else
|
||||
executables.push(executable);
|
||||
if (executable?.browserName === 'chromium')
|
||||
executables.push(registry.findExecutable('ffmpeg')!);
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === 'chromium') {
|
||||
if (!options.onlyShell)
|
||||
handleArgument('chromium');
|
||||
if (!options.noShell)
|
||||
handleArgument('chromium-headless-shell');
|
||||
} else {
|
||||
handleArgument(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (faultyArguments.length)
|
||||
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
|
||||
return executables;
|
||||
|
|
@ -118,7 +144,12 @@ program
|
|||
.option('--with-deps', 'install system dependencies for browsers')
|
||||
.option('--dry-run', 'do not execute installation, only print information')
|
||||
.option('--force', 'force reinstall of stable browser channels')
|
||||
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean }) {
|
||||
.option('--only-shell', 'only install headless shell when installing chromium')
|
||||
.option('--no-shell', 'do not install chromium headless shell')
|
||||
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean, shell?: boolean, noShell?: boolean, onlyShell?: boolean }) {
|
||||
// For '--no-shell' option, commander sets `shell: false` instead.
|
||||
if (options.shell === false)
|
||||
options.noShell = true;
|
||||
if (isLikelyNpxGlobal()) {
|
||||
console.error(wrapInASCIIBox([
|
||||
`WARNING: It looks like you are running 'npx playwright install' without first`,
|
||||
|
|
@ -141,7 +172,7 @@ program
|
|||
}
|
||||
try {
|
||||
const hasNoArguments = !args.length;
|
||||
const executables = hasNoArguments ? registry.defaultExecutables() : checkBrowsersToInstall(args);
|
||||
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
|
||||
if (options.withDeps)
|
||||
await registry.installDeps(executables, !!options.dryRun);
|
||||
if (options.dryRun) {
|
||||
|
|
@ -199,9 +230,9 @@ program
|
|||
.action(async function(args: string[], options: { dryRun?: boolean }) {
|
||||
try {
|
||||
if (!args.length)
|
||||
await registry.installDeps(registry.defaultExecutables(), !!options.dryRun);
|
||||
await registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
|
||||
else
|
||||
await registry.installDeps(checkBrowsersToInstall(args), !!options.dryRun);
|
||||
await registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browser dependencies\n${e}`);
|
||||
gracefullyProcessExitDoNotHang(1);
|
||||
|
|
@ -554,6 +585,7 @@ async function open(options: Options, url: string | undefined, language: string)
|
|||
contextOptions,
|
||||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
handleSIGINT: false,
|
||||
});
|
||||
await openPage(context, url);
|
||||
}
|
||||
|
|
@ -577,6 +609,7 @@ async function codegen(options: Options & { target: string, output?: string, tes
|
|||
codegenMode: process.env.PW_RECORDER_IS_TRACE_VIEWER ? 'trace-events' : 'actions',
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||
handleSIGINT: false,
|
||||
});
|
||||
await openPage(context, url);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
return channel;
|
||||
}
|
||||
|
||||
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal = false): Promise<R> {
|
||||
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
|
||||
const logger = this._logger;
|
||||
const apiZone = zones.zoneData<ApiZone>('apiZone');
|
||||
if (apiZone)
|
||||
|
|
@ -178,7 +178,8 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
let apiName: string | undefined = stackTrace.apiName;
|
||||
const frames: channels.StackFrame[] = stackTrace.frames;
|
||||
|
||||
isInternal = isInternal || this._isInternalType;
|
||||
if (isInternal === undefined)
|
||||
isInternal = this._isInternalType;
|
||||
if (isInternal)
|
||||
apiName = undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export interface ClientInstrumentation {
|
|||
removeAllListeners(): void;
|
||||
onApiCallBegin(apiCall: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void;
|
||||
onApiCallEnd(userData: any, error?: Error): void;
|
||||
onWillPause(): void;
|
||||
onWillPause(options: { keepTestTimeout: boolean }): void;
|
||||
|
||||
runAfterCreateBrowserContext(context: BrowserContext): Promise<void>;
|
||||
runAfterCreateRequestContext(context: APIRequestContext): Promise<void>;
|
||||
|
|
@ -35,7 +35,7 @@ export interface ClientInstrumentation {
|
|||
export interface ClientInstrumentationListener {
|
||||
onApiCallBegin?(apiName: string, params: Record<string, any>, frames: StackFrame[], userData: any, out: { stepId?: string }): void;
|
||||
onApiCallEnd?(userData: any, error?: Error): void;
|
||||
onWillPause?(): void;
|
||||
onWillPause?(options: { keepTestTimeout: boolean }): void;
|
||||
|
||||
runAfterCreateBrowserContext?(context: BrowserContext): Promise<void>;
|
||||
runAfterCreateRequestContext?(context: APIRequestContext): Promise<void>;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import * as util from 'util';
|
|||
import { asLocator, isString, monotonicTime } from '../utils';
|
||||
import { ElementHandle } from './elementHandle';
|
||||
import type { Frame } from './frame';
|
||||
import type { FilePayload, FrameExpectOptions, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
|
||||
import type { FilePayload, FrameExpectParams, Rect, SelectOption, SelectOptionOptions, TimeoutOptions } from './types';
|
||||
import { parseResult, serializeArgument } from './jsHandle';
|
||||
import { escapeForTextSelector } from '../utils/isomorphic/stringUtils';
|
||||
import type { ByRoleOptions } from '../utils/isomorphic/locatorUtils';
|
||||
|
|
@ -354,7 +354,7 @@ export class Locator implements api.Locator {
|
|||
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options });
|
||||
}
|
||||
|
||||
async _expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
|
||||
async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
|
||||
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
||||
params.expectedValue = serializeArgument(options.expectedValue);
|
||||
const result = (await this._frame._channel.expect(params));
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ import { Worker } from './worker';
|
|||
import type { Headers, RemoteAddr, SecurityDetails, WaitForEventOptions } from './types';
|
||||
import fs from 'fs';
|
||||
import { mime } from '../utilsBundle';
|
||||
import { assert, isString, headersObjectToArray, isRegExp, rewriteErrorMessage } from '../utils';
|
||||
import { assert, isString, headersObjectToArray, isRegExp, rewriteErrorMessage, MultiMap, urlMatches, zones } from '../utils';
|
||||
import type { URLMatch, Zone } from '../utils';
|
||||
import { ManualPromise, LongStandingScope } from '../utils/manualPromise';
|
||||
import { Events } from './events';
|
||||
import type { Page } from './page';
|
||||
import { Waiter } from './waiter';
|
||||
import type * as api from '../../types/types';
|
||||
import type { HeadersArray } from '../common/types';
|
||||
import { MultiMap, urlMatches, type URLMatch } from '../utils';
|
||||
import { APIResponse } from './fetch';
|
||||
import type { Serializable } from '../../types/structs';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
|
|
@ -97,6 +97,7 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.RequestInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
|
||||
if (this._redirectedFrom)
|
||||
this._redirectedFrom._redirectedTo = this;
|
||||
|
|
@ -645,6 +646,7 @@ export class Response extends ChannelOwner<channels.ResponseChannel> implements
|
|||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ResponseInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||
this._request = Request.from(this._initializer.request);
|
||||
Object.assign(this._request._timing, this._initializer.timing);
|
||||
|
|
@ -811,12 +813,14 @@ export class RouteHandler {
|
|||
readonly handler: RouteHandlerCallback;
|
||||
private _ignoreException: boolean = false;
|
||||
private _activeInvocations: Set<{ complete: Promise<void>, route: Route }> = new Set();
|
||||
private _svedZone: Zone;
|
||||
|
||||
constructor(baseURL: string | undefined, url: URLMatch, handler: RouteHandlerCallback, times: number = Number.MAX_SAFE_INTEGER) {
|
||||
this._baseURL = baseURL;
|
||||
this._times = times;
|
||||
this.url = url;
|
||||
this.handler = handler;
|
||||
this._svedZone = zones.currentZone();
|
||||
}
|
||||
|
||||
static prepareInterceptionPatterns(handlers: RouteHandler[]) {
|
||||
|
|
@ -840,6 +844,10 @@ export class RouteHandler {
|
|||
}
|
||||
|
||||
public async handle(route: Route): Promise<boolean> {
|
||||
return await this._svedZone.run(async () => this._handleImpl(route));
|
||||
}
|
||||
|
||||
private async _handleImpl(route: Route): Promise<boolean> {
|
||||
const handlerInvocation = { complete: new ManualPromise(), route } ;
|
||||
this._activeInvocations.add(handlerInvocation);
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
|
|||
export type ExpectScreenshotOptions = Omit<channels.PageExpectScreenshotOptions, 'locator' | 'expected' | 'mask'> & {
|
||||
expected?: Buffer,
|
||||
locator?: api.Locator,
|
||||
timeout: number,
|
||||
isNot: boolean,
|
||||
mask?: api.Locator[],
|
||||
};
|
||||
|
|
@ -589,7 +590,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
return result.binary;
|
||||
}
|
||||
|
||||
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[]}> {
|
||||
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> {
|
||||
const mask = options?.mask ? options?.mask.map(locator => ({
|
||||
frame: (locator as Locator)._frame._channel,
|
||||
selector: (locator as Locator)._selector,
|
||||
|
|
@ -785,14 +786,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
return [...this._workers];
|
||||
}
|
||||
|
||||
async pause() {
|
||||
async pause(_options?: { __testHookKeepTestTimeout: boolean }) {
|
||||
if (require('inspector').url())
|
||||
return;
|
||||
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
|
||||
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();
|
||||
this._browserContext.setDefaultNavigationTimeout(0);
|
||||
this._browserContext.setDefaultTimeout(0);
|
||||
this._instrumentation?.onWillPause();
|
||||
this._instrumentation?.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
|
||||
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause());
|
||||
this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
|
||||
this._browserContext.setDefaultTimeout(defaultTimeout);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,18 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
await this._startCollectingStacks(traceName);
|
||||
}
|
||||
|
||||
async group(name: string, options: { location?: { file: string, line?: number, column?: number } } = {}) {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._channel.tracingGroup({ name, location: options.location });
|
||||
}, false);
|
||||
}
|
||||
|
||||
async groupEnd() {
|
||||
await this._wrapApiCall(async () => {
|
||||
await this._channel.tracingGroupEnd();
|
||||
}, false);
|
||||
}
|
||||
|
||||
private async _startCollectingStacks(traceName: string) {
|
||||
if (!this._isTracing) {
|
||||
this._isTracing = true;
|
||||
|
|
|
|||
|
|
@ -154,4 +154,4 @@ export type SelectorEngine = {
|
|||
export type RemoteAddr = channels.RemoteAddr;
|
||||
export type SecurityDetails = channels.SecurityDetails;
|
||||
|
||||
export type FrameExpectOptions = channels.FrameExpectOptions & { isNot?: boolean };
|
||||
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'selector'|'expression'|'expectedValue'> & { expectedValue?: any };
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@
|
|||
import type { EventEmitter } from 'events';
|
||||
import { rewriteErrorMessage } from '../utils/stackTrace';
|
||||
import { TimeoutError } from './errors';
|
||||
import { createGuid } from '../utils';
|
||||
import { createGuid, zones } from '../utils';
|
||||
import type { Zone } from '../utils';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { ChannelOwner } from './channelOwner';
|
||||
|
||||
|
|
@ -29,10 +30,13 @@ export class Waiter {
|
|||
private _channelOwner: ChannelOwner<channels.EventTargetChannel>;
|
||||
private _waitId: string;
|
||||
private _error: string | undefined;
|
||||
private _savedZone: Zone;
|
||||
|
||||
constructor(channelOwner: ChannelOwner<channels.EventTargetChannel>, event: string) {
|
||||
this._waitId = createGuid();
|
||||
this._channelOwner = channelOwner;
|
||||
this._savedZone = zones.currentZone();
|
||||
|
||||
this._channelOwner._channel.waitForEventInfo({ info: { waitId: this._waitId, phase: 'before', event } }).catch(() => {});
|
||||
this._dispose = [
|
||||
() => this._channelOwner._wrapApiCall(async () => {
|
||||
|
|
@ -46,12 +50,12 @@ export class Waiter {
|
|||
}
|
||||
|
||||
async waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): Promise<T> {
|
||||
const { promise, dispose } = waitForEvent(emitter, event, predicate);
|
||||
const { promise, dispose } = waitForEvent(emitter, event, this._savedZone, predicate);
|
||||
return await this.waitForPromise(promise, dispose);
|
||||
}
|
||||
|
||||
rejectOnEvent<T = void>(emitter: EventEmitter, event: string, error: Error | (() => Error), predicate?: (arg: T) => boolean | Promise<boolean>) {
|
||||
const { promise, dispose } = waitForEvent(emitter, event, predicate);
|
||||
const { promise, dispose } = waitForEvent(emitter, event, this._savedZone, predicate);
|
||||
this._rejectOn(promise.then(() => { throw (typeof error === 'function' ? error() : error); }), dispose);
|
||||
}
|
||||
|
||||
|
|
@ -103,19 +107,21 @@ export class Waiter {
|
|||
}
|
||||
}
|
||||
|
||||
function waitForEvent<T = void>(emitter: EventEmitter, event: string, predicate?: (arg: T) => boolean | Promise<boolean>): { promise: Promise<T>, dispose: () => void } {
|
||||
function waitForEvent<T = void>(emitter: EventEmitter, event: string, savedZone: Zone, predicate?: (arg: T) => boolean | Promise<boolean>): { promise: Promise<T>, dispose: () => void } {
|
||||
let listener: (eventArg: any) => void;
|
||||
const promise = new Promise<T>((resolve, reject) => {
|
||||
listener = async (eventArg: any) => {
|
||||
try {
|
||||
if (predicate && !(await predicate(eventArg)))
|
||||
return;
|
||||
emitter.removeListener(event, listener);
|
||||
resolve(eventArg);
|
||||
} catch (e) {
|
||||
emitter.removeListener(event, listener);
|
||||
reject(e);
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -422,7 +422,8 @@ scheme.DebugControllerSetRecorderModeParams = tObject({
|
|||
});
|
||||
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHighlightParams = tObject({
|
||||
selector: tString,
|
||||
selector: tOptional(tString),
|
||||
ariaTemplate: tOptional(tString),
|
||||
});
|
||||
scheme.DebugControllerHighlightResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerHideHighlightParams = tOptional(tObject({}));
|
||||
|
|
@ -976,6 +977,7 @@ scheme.BrowserContextEnableRecorderParams = tObject({
|
|||
device: tOptional(tString),
|
||||
saveStorage: tOptional(tString),
|
||||
outputFile: tOptional(tString),
|
||||
handleSIGINT: tOptional(tBoolean),
|
||||
omitCallTracking: tOptional(tBoolean),
|
||||
});
|
||||
scheme.BrowserContextEnableRecorderResult = tOptional(tObject({}));
|
||||
|
|
@ -1164,7 +1166,7 @@ scheme.PageReloadResult = tObject({
|
|||
});
|
||||
scheme.PageExpectScreenshotParams = tObject({
|
||||
expected: tOptional(tBinary),
|
||||
timeout: tOptional(tNumber),
|
||||
timeout: tNumber,
|
||||
isNot: tBoolean,
|
||||
locator: tOptional(tObject({
|
||||
frame: tChannel(['Frame']),
|
||||
|
|
@ -1192,6 +1194,7 @@ scheme.PageExpectScreenshotResult = tObject({
|
|||
errorMessage: tOptional(tString),
|
||||
actual: tOptional(tBinary),
|
||||
previous: tOptional(tBinary),
|
||||
timedOut: tOptional(tBoolean),
|
||||
log: tOptional(tArray(tString)),
|
||||
});
|
||||
scheme.PageScreenshotParams = tObject({
|
||||
|
|
@ -1768,7 +1771,7 @@ scheme.FrameExpectParams = tObject({
|
|||
expectedValue: tOptional(tType('SerializedArgument')),
|
||||
useInnerText: tOptional(tBoolean),
|
||||
isNot: tBoolean,
|
||||
timeout: tOptional(tNumber),
|
||||
timeout: tNumber,
|
||||
});
|
||||
scheme.FrameExpectResult = tObject({
|
||||
matches: tBoolean,
|
||||
|
|
@ -2295,6 +2298,17 @@ scheme.TracingTracingStartChunkParams = tObject({
|
|||
scheme.TracingTracingStartChunkResult = tObject({
|
||||
traceName: tString,
|
||||
});
|
||||
scheme.TracingTracingGroupParams = tObject({
|
||||
name: tString,
|
||||
location: tOptional(tObject({
|
||||
file: tString,
|
||||
line: tOptional(tNumber),
|
||||
column: tOptional(tNumber),
|
||||
})),
|
||||
});
|
||||
scheme.TracingTracingGroupResult = tOptional(tObject({}));
|
||||
scheme.TracingTracingGroupEndParams = tOptional(tObject({}));
|
||||
scheme.TracingTracingGroupEndResult = tOptional(tObject({}));
|
||||
scheme.TracingTracingStopChunkParams = tObject({
|
||||
mode: tEnum(['archive', 'discard', 'entries']),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,127 +14,17 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { AriaTemplateNode } from './injected/ariaSnapshot';
|
||||
import { parseYamlTemplate } from '../utils/isomorphic/ariaSnapshot';
|
||||
import type { AriaTemplateNode, ParsedYaml } from '@isomorphic/ariaSnapshot';
|
||||
import { yaml } from '../utilsBundle';
|
||||
import type { AriaRole } from '@injected/roleUtils';
|
||||
import { assert } from '../utils';
|
||||
|
||||
export function parseAriaSnapshot(text: string): AriaTemplateNode {
|
||||
const fragment = yaml.parse(text) as any[];
|
||||
const result: AriaTemplateNode = { role: 'fragment' };
|
||||
populateNode(result, fragment);
|
||||
return result;
|
||||
return parseYamlTemplate(parseYamlForAriaSnapshot(text));
|
||||
}
|
||||
|
||||
function populateNode(node: AriaTemplateNode, container: any[]) {
|
||||
for (const object of container) {
|
||||
if (typeof object === 'string') {
|
||||
const childNode = parseKey(object);
|
||||
node.children = node.children || [];
|
||||
node.children.push(childNode);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(object)) {
|
||||
const childNode = parseKey(key);
|
||||
const value = object[key];
|
||||
node.children = node.children || [];
|
||||
|
||||
if (childNode.role === 'text') {
|
||||
node.children.push(valueOrRegex(value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
node.children.push({ ...childNode, children: [valueOrRegex(value)] });
|
||||
continue;
|
||||
}
|
||||
|
||||
node.children.push(childNode);
|
||||
populateNode(childNode, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyAttribute(node: AriaTemplateNode, key: string, value: string) {
|
||||
if (key === 'checked') {
|
||||
assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "disabled" attribute must be a boolean or "mixed"');
|
||||
node.checked = value === 'true' ? true : value === 'false' ? false : 'mixed';
|
||||
return;
|
||||
}
|
||||
if (key === 'disabled') {
|
||||
assert(value === 'true' || value === 'false', 'Value of "disabled" attribute must be a boolean');
|
||||
node.disabled = value === 'true';
|
||||
return;
|
||||
}
|
||||
if (key === 'expanded') {
|
||||
assert(value === 'true' || value === 'false', 'Value of "expanded" attribute must be a boolean');
|
||||
node.expanded = value === 'true';
|
||||
return;
|
||||
}
|
||||
if (key === 'level') {
|
||||
assert(!isNaN(Number(value)), 'Value of "level" attribute must be a number');
|
||||
node.level = Number(value);
|
||||
return;
|
||||
}
|
||||
if (key === 'pressed') {
|
||||
assert(value === 'true' || value === 'false' || value === 'mixed', 'Value of "pressed" attribute must be a boolean or "mixed"');
|
||||
node.pressed = value === 'true' ? true : value === 'false' ? false : 'mixed';
|
||||
return;
|
||||
}
|
||||
if (key === 'selected') {
|
||||
assert(value === 'true' || value === 'false', 'Value of "selected" attribute must be a boolean');
|
||||
node.selected = value === 'true';
|
||||
return;
|
||||
}
|
||||
throw new Error(`Unsupported attribute [${key}] `);
|
||||
}
|
||||
|
||||
function parseKey(key: string): AriaTemplateNode {
|
||||
const tokenRegex = /\s*([a-z]+|"(?:[^"]*)"|\/(?:[^\/]*)\/|\[.*?\])/g;
|
||||
let match;
|
||||
const tokens = [];
|
||||
while ((match = tokenRegex.exec(key)) !== null)
|
||||
tokens.push(match[1]);
|
||||
|
||||
if (tokens.length === 0)
|
||||
throw new Error(`Invalid key ${key}`);
|
||||
|
||||
const role = tokens[0] as AriaRole | 'text';
|
||||
|
||||
let name: string | RegExp = '';
|
||||
let index = 1;
|
||||
if (tokens.length > 1 && (tokens[1].startsWith('"') || tokens[1].startsWith('/'))) {
|
||||
const nameToken = tokens[1];
|
||||
if (nameToken.startsWith('"')) {
|
||||
name = nameToken.slice(1, -1);
|
||||
} else {
|
||||
const pattern = nameToken.slice(1, -1);
|
||||
name = new RegExp(pattern);
|
||||
}
|
||||
index = 2;
|
||||
}
|
||||
|
||||
const result: AriaTemplateNode = { role, name };
|
||||
for (; index < tokens.length; index++) {
|
||||
const attrToken = tokens[index];
|
||||
if (attrToken.startsWith('[') && attrToken.endsWith(']')) {
|
||||
const attrContent = attrToken.slice(1, -1).trim();
|
||||
const [attrName, attrValue] = attrContent.split('=', 2);
|
||||
const value = attrValue !== undefined ? attrValue.trim() : 'true';
|
||||
applyAttribute(result, attrName, value);
|
||||
} else {
|
||||
throw new Error(`Invalid attribute token ${attrToken} in key ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeWhitespace(text: string) {
|
||||
return text.replace(/[\r\n\s\t]+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function valueOrRegex(value: string): string | RegExp {
|
||||
return value.startsWith('/') && value.endsWith('/') ? new RegExp(value.slice(1, -1)) : normalizeWhitespace(value);
|
||||
export function parseYamlForAriaSnapshot(text: string): ParsedYaml {
|
||||
const parsed = yaml.parse(text);
|
||||
if (!Array.isArray(parsed))
|
||||
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
|
||||
return parsed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,10 +112,7 @@ export class BidiChromium extends BrowserType {
|
|||
if (options.devtools)
|
||||
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||
if (options.headless) {
|
||||
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
||||
chromeArguments.push('--headless=new');
|
||||
else
|
||||
chromeArguments.push('--headless=old');
|
||||
chromeArguments.push('--headless');
|
||||
|
||||
chromeArguments.push(
|
||||
'--hide-scrollbars',
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ export abstract class BrowserType extends SdkObject {
|
|||
throw new Error(`Failed to launch ${this._name} because executable doesn't exist at ${executablePath}`);
|
||||
executable = executablePath;
|
||||
} else {
|
||||
const registryExecutable = registry.findExecutable(options.channel || this._name);
|
||||
const registryExecutable = registry.findExecutable(this.getExecutableName(options));
|
||||
if (!registryExecutable || registryExecutable.browserName !== this._name)
|
||||
throw new Error(`Unsupported ${this._name} channel "${options.channel}"`);
|
||||
executable = registryExecutable.executablePathOrDie(this.attribution.playwright.options.sdkLanguage);
|
||||
|
|
@ -332,6 +332,10 @@ export abstract class BrowserType extends SdkObject {
|
|||
async prepareUserDataDir(options: types.LaunchOptions, userDataDir: string): Promise<void> {
|
||||
}
|
||||
|
||||
getExecutableName(options: types.LaunchOptions): string {
|
||||
return options.channel || this._name;
|
||||
}
|
||||
|
||||
abstract defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[];
|
||||
abstract connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser>;
|
||||
abstract amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env;
|
||||
|
|
|
|||
|
|
@ -300,17 +300,14 @@ export class Chromium extends BrowserType {
|
|||
// See https://github.com/microsoft/playwright/issues/7362
|
||||
chromeArguments.push('--enable-use-zoom-for-dsf=false');
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
|
||||
if (options.headless)
|
||||
if (options.headless && (!options.channel || options.channel === 'chromium-headless-shell'))
|
||||
chromeArguments.push('--use-angle');
|
||||
}
|
||||
|
||||
if (options.devtools)
|
||||
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||
if (options.headless) {
|
||||
if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW)
|
||||
chromeArguments.push('--headless=new');
|
||||
else
|
||||
chromeArguments.push('--headless=old');
|
||||
chromeArguments.push('--headless');
|
||||
|
||||
chromeArguments.push(
|
||||
'--hide-scrollbars',
|
||||
|
|
@ -350,6 +347,12 @@ export class Chromium extends BrowserType {
|
|||
return new ChromiumReadyState();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override getExecutableName(options: types.LaunchOptions): string {
|
||||
if (options.channel)
|
||||
return options.channel;
|
||||
return options.headless ? 'chromium-headless-shell' : 'chromium';
|
||||
}
|
||||
}
|
||||
|
||||
class ChromiumReadyState extends BrowserReadyState {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export module Protocol {
|
|||
- from 'checked' to 'selected': states which apply to widgets
|
||||
- from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling.
|
||||
*/
|
||||
export type AXPropertyName = "busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url";
|
||||
export type AXPropertyName = "actions"|"busy"|"disabled"|"editable"|"focusable"|"focused"|"hidden"|"hiddenRoot"|"invalid"|"keyshortcuts"|"settable"|"roledescription"|"live"|"atomic"|"relevant"|"root"|"autocomplete"|"hasPopup"|"level"|"multiselectable"|"orientation"|"multiline"|"readonly"|"required"|"valuemin"|"valuemax"|"valuetext"|"checked"|"expanded"|"modal"|"pressed"|"selected"|"activedescendant"|"controls"|"describedby"|"details"|"errormessage"|"flowto"|"labelledby"|"owns"|"url";
|
||||
/**
|
||||
* A node in the accessibility tree.
|
||||
*/
|
||||
|
|
@ -694,7 +694,7 @@ percentage [0 - 100] for scroll driven animations
|
|||
export interface AffectedFrame {
|
||||
frameId: Page.FrameId;
|
||||
}
|
||||
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout";
|
||||
export type CookieExclusionReason = "ExcludeSameSiteUnspecifiedTreatedAsLax"|"ExcludeSameSiteNoneInsecure"|"ExcludeSameSiteLax"|"ExcludeSameSiteStrict"|"ExcludeInvalidSameParty"|"ExcludeSamePartyCrossPartyContext"|"ExcludeDomainNonASCII"|"ExcludeThirdPartyCookieBlockedInFirstPartySet"|"ExcludeThirdPartyPhaseout"|"ExcludePortMismatch"|"ExcludeSchemeMismatch";
|
||||
export type CookieWarningReason = "WarnSameSiteUnspecifiedCrossSiteContext"|"WarnSameSiteNoneInsecure"|"WarnSameSiteUnspecifiedLaxAllowUnsafe"|"WarnSameSiteStrictLaxDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeStrict"|"WarnSameSiteStrictCrossDowngradeLax"|"WarnSameSiteLaxCrossDowngradeStrict"|"WarnSameSiteLaxCrossDowngradeLax"|"WarnAttributeValueExceedsMaxSize"|"WarnDomainNonASCII"|"WarnThirdPartyPhaseout"|"WarnCrossSiteRedirectDowngradeChangesInclusion"|"WarnDeprecationTrialMetadata"|"WarnThirdPartyCookieHeuristic";
|
||||
export type CookieOperation = "SetCookie"|"ReadCookie";
|
||||
/**
|
||||
|
|
@ -2183,12 +2183,17 @@ The array enumerates @scope at-rules starting with the innermost one, going outw
|
|||
* The array keeps the types of ancestor CSSRules from the innermost going outwards.
|
||||
*/
|
||||
ruleTypes?: CSSRuleType[];
|
||||
/**
|
||||
* @starting-style CSS at-rule array.
|
||||
The array enumerates @starting-style at-rules starting with the innermost one, going outwards.
|
||||
*/
|
||||
startingStyles?: CSSStartingStyle[];
|
||||
}
|
||||
/**
|
||||
* Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors.
|
||||
This list only contains rule types that are collected during the ancestor rule collection.
|
||||
*/
|
||||
export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule";
|
||||
export type CSSRuleType = "MediaRule"|"SupportsRule"|"ContainerRule"|"LayerRule"|"ScopeRule"|"StyleRule"|"StartingStyleRule";
|
||||
/**
|
||||
* CSS coverage information.
|
||||
*/
|
||||
|
|
@ -2424,6 +2429,10 @@ available).
|
|||
* Optional logical axes queried for the container.
|
||||
*/
|
||||
logicalAxes?: DOM.LogicalAxes;
|
||||
/**
|
||||
* true if the query contains scroll-state() queries.
|
||||
*/
|
||||
queriesScrollState?: boolean;
|
||||
}
|
||||
/**
|
||||
* CSS Supports at-rule descriptor.
|
||||
|
|
@ -2475,6 +2484,20 @@ available).
|
|||
text: string;
|
||||
/**
|
||||
* The associated rule header range in the enclosing stylesheet (if
|
||||
available).
|
||||
*/
|
||||
range?: SourceRange;
|
||||
/**
|
||||
* Identifier of the stylesheet containing this object (if exists).
|
||||
*/
|
||||
styleSheetId?: StyleSheetId;
|
||||
}
|
||||
/**
|
||||
* CSS Starting Style at-rule descriptor.
|
||||
*/
|
||||
export interface CSSStartingStyle {
|
||||
/**
|
||||
* The associated rule header range in the enclosing stylesheet (if
|
||||
available).
|
||||
*/
|
||||
range?: SourceRange;
|
||||
|
|
@ -2779,6 +2802,12 @@ resized.) The current implementation considers only viewport-dependent media fea
|
|||
*/
|
||||
styleSheetId: StyleSheetId;
|
||||
}
|
||||
export type computedStyleUpdatedPayload = {
|
||||
/**
|
||||
* The node id that has updated computed styles.
|
||||
*/
|
||||
nodeId: DOM.NodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the
|
||||
|
|
@ -3039,6 +3068,19 @@ returns an array of locations of the CSS selector in the style sheet.
|
|||
export type getLocationForSelectorReturnValue = {
|
||||
ranges: SourceRange[];
|
||||
}
|
||||
/**
|
||||
* Starts tracking the given node for the computed style updates
|
||||
and whenever the computed style is updated for node, it queues
|
||||
a `computedStyleUpdated` event with throttling.
|
||||
There can only be 1 node tracked for computed style updates
|
||||
so passing a new node id removes tracking from the previous node.
|
||||
Pass `undefined` to disable tracking.
|
||||
*/
|
||||
export type trackComputedStyleUpdatesForNodeParameters = {
|
||||
nodeId?: DOM.NodeId;
|
||||
}
|
||||
export type trackComputedStyleUpdatesForNodeReturnValue = {
|
||||
}
|
||||
/**
|
||||
* Starts tracking the given computed styles for updates. The specified array of properties
|
||||
replaces the one previously specified. Pass empty array to disable tracking.
|
||||
|
|
@ -3561,7 +3603,7 @@ front-end.
|
|||
/**
|
||||
* Pseudo element type.
|
||||
*/
|
||||
export type PseudoType = "first-line"|"first-letter"|"before"|"after"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"select-fallback-button"|"select-fallback-button-text"|"picker";
|
||||
export type PseudoType = "first-line"|"first-letter"|"check"|"before"|"after"|"select-arrow"|"marker"|"backdrop"|"column"|"selection"|"search-text"|"target-text"|"spelling-error"|"grammar-error"|"highlight"|"first-line-inherited"|"scroll-marker"|"scroll-marker-group"|"scroll-next-button"|"scroll-prev-button"|"scrollbar"|"scrollbar-thumb"|"scrollbar-button"|"scrollbar-track"|"scrollbar-track-piece"|"scrollbar-corner"|"resizer"|"input-list-button"|"view-transition"|"view-transition-group"|"view-transition-image-pair"|"view-transition-old"|"view-transition-new"|"placeholder"|"file-selector-button"|"details-content"|"picker";
|
||||
/**
|
||||
* Shadow root type.
|
||||
*/
|
||||
|
|
@ -4876,15 +4918,17 @@ $x functions).
|
|||
}
|
||||
/**
|
||||
* Returns the query container of the given node based on container query
|
||||
conditions: containerName, physical, and logical axes. If no axes are
|
||||
provided, the style container is returned, which is the direct parent or the
|
||||
closest element with a matching container-name.
|
||||
conditions: containerName, physical and logical axes, and whether it queries
|
||||
scroll-state. If no axes are provided and queriesScrollState is false, the
|
||||
style container is returned, which is the direct parent or the closest
|
||||
element with a matching container-name.
|
||||
*/
|
||||
export type getContainerForNodeParameters = {
|
||||
nodeId: NodeId;
|
||||
containerName?: string;
|
||||
physicalAxes?: PhysicalAxes;
|
||||
logicalAxes?: LogicalAxes;
|
||||
queriesScrollState?: boolean;
|
||||
}
|
||||
export type getContainerForNodeReturnValue = {
|
||||
/**
|
||||
|
|
@ -8255,7 +8299,9 @@ file, data and other requests and responses, their headers, bodies, timing, etc.
|
|||
*/
|
||||
export type LoaderId = string;
|
||||
/**
|
||||
* Unique request identifier.
|
||||
* Unique network request identifier.
|
||||
Note that this does not identify individual HTTP requests that are part of
|
||||
a network request.
|
||||
*/
|
||||
export type RequestId = string;
|
||||
/**
|
||||
|
|
@ -8830,6 +8876,7 @@ If the opcode isn't 1, then payloadData is a base64 encoded string representing
|
|||
type: "parser"|"script"|"preload"|"SignedExchange"|"preflight"|"other";
|
||||
/**
|
||||
* Initiator JavaScript stack trace, set for Script only.
|
||||
Requires the Debugger domain to be enabled.
|
||||
*/
|
||||
stack?: Runtime.StackTrace;
|
||||
/**
|
||||
|
|
@ -8944,7 +8991,7 @@ This is a temporary ability and it will be removed in the future.
|
|||
/**
|
||||
* Types of reasons why a cookie may not be sent with a request.
|
||||
*/
|
||||
export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize";
|
||||
export type CookieBlockedReason = "SecureOnly"|"NotOnPath"|"DomainMismatch"|"SameSiteStrict"|"SameSiteLax"|"SameSiteUnspecifiedTreatedAsLax"|"SameSiteNoneInsecure"|"UserPreferences"|"ThirdPartyPhaseout"|"ThirdPartyBlockedInFirstPartySet"|"UnknownError"|"SchemefulSameSiteStrict"|"SchemefulSameSiteLax"|"SchemefulSameSiteUnspecifiedTreatedAsLax"|"SamePartyFromCrossPartyContext"|"NameValuePairExceedsMaxSize"|"PortMismatch"|"SchemeMismatch";
|
||||
/**
|
||||
* Types of reasons why a cookie should have been blocked by 3PCD but is exempted for the request.
|
||||
*/
|
||||
|
|
@ -11498,7 +11545,7 @@ as an ad.
|
|||
* All Permissions Policy features. This enum should match the one defined
|
||||
in third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.
|
||||
*/
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
export type PermissionsPolicyFeature = "accelerometer"|"all-screens-capture"|"ambient-light-sensor"|"attribution-reporting"|"autoplay"|"bluetooth"|"browsing-topics"|"camera"|"captured-surface-control"|"ch-dpr"|"ch-device-memory"|"ch-downlink"|"ch-ect"|"ch-prefers-color-scheme"|"ch-prefers-reduced-motion"|"ch-prefers-reduced-transparency"|"ch-rtt"|"ch-save-data"|"ch-ua"|"ch-ua-arch"|"ch-ua-bitness"|"ch-ua-platform"|"ch-ua-model"|"ch-ua-mobile"|"ch-ua-form-factors"|"ch-ua-full-version"|"ch-ua-full-version-list"|"ch-ua-platform-version"|"ch-ua-wow64"|"ch-viewport-height"|"ch-viewport-width"|"ch-width"|"clipboard-read"|"clipboard-write"|"compute-pressure"|"controlled-frame"|"cross-origin-isolated"|"deferred-fetch"|"digital-credentials-get"|"direct-sockets"|"direct-sockets-private"|"display-capture"|"document-domain"|"encrypted-media"|"execution-while-out-of-viewport"|"execution-while-not-rendered"|"fenced-unpartitioned-storage-read"|"focus-without-user-activation"|"fullscreen"|"frobulate"|"gamepad"|"geolocation"|"gyroscope"|"hid"|"identity-credentials-get"|"idle-detection"|"interest-cohort"|"join-ad-interest-group"|"keyboard-map"|"local-fonts"|"magnetometer"|"media-playback-while-not-visible"|"microphone"|"midi"|"otp-credentials"|"payment"|"picture-in-picture"|"popins"|"private-aggregation"|"private-state-token-issuance"|"private-state-token-redemption"|"publickey-credentials-create"|"publickey-credentials-get"|"run-ad-auction"|"screen-wake-lock"|"serial"|"shared-autofill"|"shared-storage"|"shared-storage-select-url"|"smart-card"|"speaker-selection"|"storage-access"|"sub-apps"|"sync-xhr"|"unload"|"usb"|"usb-unrestricted"|"vertical-scroll"|"web-app-installation"|"web-printing"|"web-share"|"window-management"|"xr-spatial-tracking";
|
||||
/**
|
||||
* Reason for a permissions policy feature to be disabled.
|
||||
*/
|
||||
|
|
@ -12384,7 +12431,8 @@ the page execution. Execution can be resumed via calling Page.handleJavaScriptDi
|
|||
defaultPrompt?: string;
|
||||
}
|
||||
/**
|
||||
* Fired for top level page lifecycle events such as navigation, load, paint, etc.
|
||||
* Fired for lifecycle events (navigation, load, paint, etc) in the current
|
||||
target (including local frames).
|
||||
*/
|
||||
export type lifecycleEventPayload = {
|
||||
/**
|
||||
|
|
@ -14339,6 +14387,7 @@ int
|
|||
destinationLimitPriority: SignedInt64AsBase10;
|
||||
aggregatableDebugReportingConfig: AttributionReportingAggregatableDebugReportingConfig;
|
||||
scopesData?: AttributionScopesData;
|
||||
maxEventLevelReports: number;
|
||||
}
|
||||
export type AttributionReportingSourceRegistrationResult = "success"|"internalError"|"insufficientSourceCapacity"|"insufficientUniqueDestinationCapacity"|"excessiveReportingOrigins"|"prohibitedByBrowserPolicy"|"successNoised"|"destinationReportingLimitReached"|"destinationGlobalLimitReached"|"destinationBothLimitsReached"|"reportingOriginsPerSiteLimitReached"|"exceedsMaxChannelCapacity"|"exceedsMaxScopesChannelCapacity"|"exceedsMaxTriggerStateCardinality"|"exceedsMaxEventStatesLimit"|"destinationPerDayReportingLimitReached";
|
||||
export type AttributionReportingSourceRegistrationTimeConfig = "include"|"exclude";
|
||||
|
|
@ -14386,7 +14435,7 @@ int
|
|||
scopes: string[];
|
||||
}
|
||||
export type AttributionReportingEventLevelResult = "success"|"successDroppedLowerPriority"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"deduplicated"|"excessiveAttributions"|"priorityTooLow"|"neverAttributedSource"|"excessiveReportingOrigins"|"noMatchingSourceFilterData"|"prohibitedByBrowserPolicy"|"noMatchingConfigurations"|"excessiveReports"|"falselyAttributedSource"|"reportWindowPassed"|"notRegistered"|"reportWindowNotStarted"|"noMatchingTriggerData";
|
||||
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
|
||||
export type AttributionReportingAggregatableResult = "success"|"internalError"|"noCapacityForAttributionDestination"|"noMatchingSources"|"excessiveAttributions"|"excessiveReportingOrigins"|"noHistograms"|"insufficientBudget"|"insufficientNamedBudget"|"noMatchingSourceFilterData"|"notRegistered"|"prohibitedByBrowserPolicy"|"deduplicated"|"reportWindowPassed"|"excessiveReports";
|
||||
/**
|
||||
* A single Related Website Set object.
|
||||
*/
|
||||
|
|
@ -15920,6 +15969,8 @@ are ignored.
|
|||
export module Fetch {
|
||||
/**
|
||||
* Unique request identifier.
|
||||
Note that this does not identify individual HTTP requests that are part of
|
||||
a network request.
|
||||
*/
|
||||
export type RequestId = string;
|
||||
/**
|
||||
|
|
@ -16302,7 +16353,7 @@ https://webaudio.github.io/web-audio-api/
|
|||
/**
|
||||
* Enum of AudioContextState from the spec
|
||||
*/
|
||||
export type ContextState = "suspended"|"running"|"closed";
|
||||
export type ContextState = "suspended"|"running"|"closed"|"interrupted";
|
||||
/**
|
||||
* Enum of AudioNode types
|
||||
*/
|
||||
|
|
@ -20213,6 +20264,7 @@ Error was thrown.
|
|||
"CSS.styleSheetAdded": CSS.styleSheetAddedPayload;
|
||||
"CSS.styleSheetChanged": CSS.styleSheetChangedPayload;
|
||||
"CSS.styleSheetRemoved": CSS.styleSheetRemovedPayload;
|
||||
"CSS.computedStyleUpdated": CSS.computedStyleUpdatedPayload;
|
||||
"Cast.sinksUpdated": Cast.sinksUpdatedPayload;
|
||||
"Cast.issueUpdated": Cast.issueUpdatedPayload;
|
||||
"DOM.attributeModified": DOM.attributeModifiedPayload;
|
||||
|
|
@ -20464,6 +20516,7 @@ Error was thrown.
|
|||
"CSS.getStyleSheetText": CSS.getStyleSheetTextParameters;
|
||||
"CSS.getLayersForNode": CSS.getLayersForNodeParameters;
|
||||
"CSS.getLocationForSelector": CSS.getLocationForSelectorParameters;
|
||||
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeParameters;
|
||||
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesParameters;
|
||||
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesParameters;
|
||||
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeParameters;
|
||||
|
|
@ -21075,6 +21128,7 @@ Error was thrown.
|
|||
"CSS.getStyleSheetText": CSS.getStyleSheetTextReturnValue;
|
||||
"CSS.getLayersForNode": CSS.getLayersForNodeReturnValue;
|
||||
"CSS.getLocationForSelector": CSS.getLocationForSelectorReturnValue;
|
||||
"CSS.trackComputedStyleUpdatesForNode": CSS.trackComputedStyleUpdatesForNodeReturnValue;
|
||||
"CSS.trackComputedStyleUpdates": CSS.trackComputedStyleUpdatesReturnValue;
|
||||
"CSS.takeComputedStyleUpdates": CSS.takeComputedStyleUpdatesReturnValue;
|
||||
"CSS.setEffectivePropertyValueForNode": CSS.setEffectivePropertyValueForNodeReturnValue;
|
||||
|
|
|
|||
|
|
@ -171,6 +171,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
using var playwright = await Playwright.CreateAsync();
|
||||
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatObject(options.launchOptions, ' ', 'BrowserTypeLaunchOptions')});
|
||||
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
|
||||
formatter.newLine();
|
||||
return formatter.format();
|
||||
}
|
||||
|
|
@ -196,6 +198,8 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
formatter.add(` [${this._mode === 'nunit' ? 'Test' : 'TestMethod'}]
|
||||
public async Task MyTest()
|
||||
{`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -261,32 +265,22 @@ function toPascal(value: string): string {
|
|||
return value[0].toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function convertContextOptions(options: BrowserContextOptions): any {
|
||||
const result: any = { ...options };
|
||||
if (options.recordHar) {
|
||||
result['recordHarPath'] = options.recordHar.path;
|
||||
result['recordHarContent'] = options.recordHar.content;
|
||||
result['recordHarMode'] = options.recordHar.mode;
|
||||
result['recordHarOmitContent'] = options.recordHar.omitContent;
|
||||
result['recordHarUrlFilter'] = options.recordHar.urlFilter;
|
||||
delete result.recordHar;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string {
|
||||
function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: string | undefined): string {
|
||||
let options = { ...contextOptions };
|
||||
// recordHAR is replaced with routeFromHAR in the generated code.
|
||||
delete options.recordHar;
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device) {
|
||||
if (!Object.entries(options).length)
|
||||
return '';
|
||||
return formatObject(convertContextOptions(options), ' ', 'BrowserNewContextOptions');
|
||||
return formatObject(options, ' ', 'BrowserNewContextOptions');
|
||||
}
|
||||
|
||||
options = sanitizeDeviceOptions(device, options);
|
||||
if (!Object.entries(options).length)
|
||||
return `playwright.Devices[${quote(deviceName!)}]`;
|
||||
|
||||
return formatObject(convertContextOptions(options), ' ', `BrowserNewContextOptions(playwright.Devices[${quote(deviceName!)}])`);
|
||||
return formatObject(options, ' ', `BrowserNewContextOptions(playwright.Devices[${quote(deviceName!)}])`);
|
||||
}
|
||||
|
||||
class CSharpFormatter {
|
||||
|
|
|
|||
|
|
@ -170,6 +170,8 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
try (Playwright playwright = Playwright.create()) {
|
||||
Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)});
|
||||
BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -240,16 +242,6 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
|
|||
lines.push(` .setLocale(${quote(options.locale)})`);
|
||||
if (options.proxy)
|
||||
lines.push(` .setProxy(new Proxy(${quote(options.proxy.server)}))`);
|
||||
if (options.recordHar?.content)
|
||||
lines.push(` .setRecordHarContent(HarContentPolicy.${options.recordHar?.content.toUpperCase()})`);
|
||||
if (options.recordHar?.mode)
|
||||
lines.push(` .setRecordHarMode(HarMode.${options.recordHar?.mode.toUpperCase()})`);
|
||||
if (options.recordHar?.omitContent)
|
||||
lines.push(` .setRecordHarOmitContent(true)`);
|
||||
if (options.recordHar?.path)
|
||||
lines.push(` .setRecordHarPath(Paths.get(${quote(options.recordHar.path)}))`);
|
||||
if (options.recordHar?.urlFilter)
|
||||
lines.push(` .setRecordHarUrlFilter(${quote(options.recordHar.urlFilter as string)})`);
|
||||
if (options.serviceWorkers)
|
||||
lines.push(` .setServiceWorkers(ServiceWorkerPolicy.${options.serviceWorkers.toUpperCase()})`);
|
||||
if (options.storageState)
|
||||
|
|
|
|||
|
|
@ -117,8 +117,10 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
const assertion = action.value ? `toHaveValue(${quote(action.value)})` : `toBeEmpty()`;
|
||||
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).${assertion};`;
|
||||
}
|
||||
case 'assertSnapshot':
|
||||
return `${this._isTest ? '' : '// '}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot)});`;
|
||||
case 'assertSnapshot': {
|
||||
const commentIfNeeded = this._isTest ? '' : '// ';
|
||||
return `${commentIfNeeded}await expect(${subject}.${this._asLocator(action.selector)}).toMatchAriaSnapshot(${quoteMultiline(action.snapshot, `${commentIfNeeded} `)});`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +147,8 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
|
||||
${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
||||
test('test', async ({ page }) => {`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` await page.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -160,6 +164,8 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
|||
(async () => {
|
||||
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
||||
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName, false)});`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` await context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -203,10 +209,8 @@ function formatObjectOrVoid(value: any, indent = ' '): string {
|
|||
|
||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined, isTest: boolean): string {
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (isTest) {
|
||||
// No recordHAR fixture in test.
|
||||
options = { ...options, recordHar: undefined };
|
||||
}
|
||||
// recordHAR is replaced with routeFromHAR in the generated code.
|
||||
options = { ...options, recordHar: undefined };
|
||||
if (!device)
|
||||
return formatObjectOrVoid(options);
|
||||
// Filter out all the properties from the device descriptor.
|
||||
|
|
@ -275,10 +279,13 @@ ${body}
|
|||
}
|
||||
|
||||
export function quoteMultiline(text: string, indent = ' ') {
|
||||
const escape = (text: string) => text.replace(/\\/g, '\\\\')
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/\$\{/g, '\\${');
|
||||
const lines = text.split('\n');
|
||||
if (lines.length === 1)
|
||||
return '`' + text.replace(/`/g, '\\`').replace(/\${/g, '\\${') + '`';
|
||||
return '`\n' + lines.map(line => indent + line.replace(/`/g, '\\`').replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
|
||||
return '`' + escape(text) + '`';
|
||||
return '`\n' + lines.map(line => indent + escape(line).replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
|
||||
}
|
||||
|
||||
function isMultilineString(text: string) {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,8 @@ from playwright.sync_api import Page, expect
|
|||
${fixture}
|
||||
|
||||
def test_example(page: Page) -> None {`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
||||
} else if (this._isAsync) {
|
||||
formatter.add(`
|
||||
import asyncio
|
||||
|
|
@ -161,6 +163,8 @@ from playwright.async_api import Playwright, async_playwright, expect
|
|||
async def run(playwright: Playwright) -> None {
|
||||
browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` await page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
||||
} else {
|
||||
formatter.add(`
|
||||
import re
|
||||
|
|
@ -170,6 +174,8 @@ from playwright.sync_api import Playwright, sync_playwright, expect
|
|||
def run(playwright: Playwright) -> None {
|
||||
browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||
context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||
if (options.contextOptions.recordHar)
|
||||
formatter.add(` context.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
|
@ -232,24 +238,13 @@ function formatOptions(value: any, hasArguments: boolean, asDict?: boolean): str
|
|||
}).join(', ');
|
||||
}
|
||||
|
||||
function convertContextOptions(options: BrowserContextOptions): any {
|
||||
const result: any = { ...options };
|
||||
if (options.recordHar) {
|
||||
result['record_har_path'] = options.recordHar.path;
|
||||
result['record_har_content'] = options.recordHar.content;
|
||||
result['record_har_mode'] = options.recordHar.mode;
|
||||
result['record_har_omit_content'] = options.recordHar.omitContent;
|
||||
result['record_har_url_filter'] = options.recordHar.urlFilter;
|
||||
delete result.recordHar;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined, asDict?: boolean): string {
|
||||
// recordHAR is replaced with routeFromHAR in the generated code.
|
||||
options = { ...options, recordHar: undefined };
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device)
|
||||
return formatOptions(convertContextOptions(options), false, asDict);
|
||||
return `**playwright.devices[${quote(deviceName!)}]` + formatOptions(convertContextOptions(sanitizeDeviceOptions(device, options)), true, asDict);
|
||||
return formatOptions(options, false, asDict);
|
||||
return `**playwright.devices[${quote(deviceName!)}]` + formatOptions(sanitizeDeviceOptions(device, options), true, asDict);
|
||||
}
|
||||
|
||||
class PythonFormatter {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import type { Playwright } from './playwright';
|
|||
import { Recorder } from './recorder';
|
||||
import { EmptyRecorderApp } from './recorder/recorderApp';
|
||||
import { asLocator, type Language } from '../utils';
|
||||
import { parseYamlForAriaSnapshot } from './ariaSnapshot';
|
||||
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
|
||||
|
|
@ -142,9 +143,13 @@ export class DebugController extends SdkObject {
|
|||
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
||||
}
|
||||
|
||||
async highlight(selector: string) {
|
||||
for (const recorder of await this._allRecorders())
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, selector);
|
||||
async highlight(params: { selector?: string, ariaTemplate?: string }) {
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
if (params.ariaTemplate)
|
||||
recorder.setHighlightedAriaTemplate(parseYamlForAriaSnapshot(params.ariaTemplate));
|
||||
else if (params.selector)
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
|
||||
}
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -68,7 +68,7 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
|
|||
}
|
||||
|
||||
async highlight(params: channels.DebugControllerHighlightParams) {
|
||||
await this._object.highlight(params.selector);
|
||||
await this._object.highlight(params);
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator';
|
||||
import { LongStandingScope, assert, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils';
|
||||
import { LongStandingScope, assert, compressCallLog, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils';
|
||||
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
|
||||
import type { CallMetadata } from '../instrumentation';
|
||||
import { SdkObject } from '../instrumentation';
|
||||
|
|
@ -357,7 +357,7 @@ export class DispatcherConnection {
|
|||
}
|
||||
|
||||
if (response.error)
|
||||
response.log = callMetadata.log;
|
||||
response.log = compressCallLog(callMetadata.log);
|
||||
this.onmessage(response);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import type * as channels from '@protocol/channels';
|
||||
import type { CallMetadata } from '@protocol/callMetadata';
|
||||
import type { Tracing } from '../trace/recorder/tracing';
|
||||
import { ArtifactDispatcher } from './artifactDispatcher';
|
||||
import { Dispatcher, existingDispatcher } from './dispatcher';
|
||||
|
|
@ -41,6 +42,15 @@ export class TracingDispatcher extends Dispatcher<Tracing, channels.TracingChann
|
|||
return await this._object.startChunk(params);
|
||||
}
|
||||
|
||||
async tracingGroup(params: channels.TracingTracingGroupParams, metadata: CallMetadata): Promise<channels.TracingTracingGroupResult> {
|
||||
const { name, location } = params;
|
||||
await this._object.group(name, location, metadata);
|
||||
}
|
||||
|
||||
async tracingGroupEnd(params: channels.TracingTracingGroupEndParams): Promise<channels.TracingTracingGroupEndResult> {
|
||||
await this._object.groupEnd();
|
||||
}
|
||||
|
||||
async tracingStopChunk(params: channels.TracingTracingStopChunkParams): Promise<channels.TracingTracingStopChunkResult> {
|
||||
const { artifact, entries } = await this._object.stopChunk(params);
|
||||
return { artifact: artifact ? ArtifactDispatcher.from(this, artifact) : undefined, entries };
|
||||
|
|
|
|||
|
|
@ -266,14 +266,22 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
const filtered = quads.map(quad => intersectQuadWithViewport(quad)).filter(quad => computeQuadArea(quad) > 0.99);
|
||||
if (!filtered.length)
|
||||
return 'error:notinviewport';
|
||||
// Return the middle point of the first quad.
|
||||
const result = { x: 0, y: 0 };
|
||||
for (const point of filtered[0]) {
|
||||
result.x += point.x / 4;
|
||||
result.y += point.y / 4;
|
||||
if (this._page._browserContext._browser.options.name === 'firefox') {
|
||||
// Firefox internally uses integer coordinates, so 8.x is converted to 8 or 9 when clicking.
|
||||
//
|
||||
// This does not work nicely for small elements. For example, 1x1 square with corners
|
||||
// (8;8) and (9;9) is targeted when clicking at (8;8) but not when clicking at (9;9).
|
||||
// So, clicking at (8.x;8.y) will sometimes click at (9;9) and miss the target.
|
||||
//
|
||||
// Therefore, we try to find an integer point within a quad to make sure we click inside the element.
|
||||
for (const quad of filtered) {
|
||||
const integerPoint = findIntegerPointInsideQuad(quad);
|
||||
if (integerPoint)
|
||||
return integerPoint;
|
||||
}
|
||||
}
|
||||
compensateHalfIntegerRoundingError(result);
|
||||
return result;
|
||||
// Return the middle point of the first quad.
|
||||
return quadMiddlePoint(filtered[0]);
|
||||
}
|
||||
|
||||
private async _offsetPoint(offset: types.Point): Promise<types.Point | 'error:notvisible' | 'error:notconnected'> {
|
||||
|
|
@ -299,7 +307,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
|
||||
while (progress.isRunning()) {
|
||||
if (retry) {
|
||||
progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}, attempt #${retry}`);
|
||||
progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}`);
|
||||
const timeout = waitTime[Math.min(retry - 1, waitTime.length - 1)];
|
||||
if (timeout) {
|
||||
progress.log(` waiting ${timeout}ms`);
|
||||
|
|
@ -920,24 +928,47 @@ function roundPoint(point: types.Point): types.Point {
|
|||
};
|
||||
}
|
||||
|
||||
function compensateHalfIntegerRoundingError(point: types.Point) {
|
||||
// Firefox internally uses integer coordinates, so 8.5 is converted to 9 when clicking.
|
||||
//
|
||||
// This does not work nicely for small elements. For example, 1x1 square with corners
|
||||
// (8;8) and (9;9) is targeted when clicking at (8;8) but not when clicking at (9;9).
|
||||
// So, clicking at (8.5;8.5) will effectively click at (9;9) and miss the target.
|
||||
//
|
||||
// Therefore, we skew half-integer values from the interval (8.49, 8.51) towards
|
||||
// (8.47, 8.49) that is rounded towards 8. This means clicking at (8.5;8.5) will
|
||||
// be replaced with (8.48;8.48) and will effectively click at (8;8).
|
||||
//
|
||||
// Other browsers use float coordinates, so this change should not matter.
|
||||
const remainderX = point.x - Math.floor(point.x);
|
||||
if (remainderX > 0.49 && remainderX < 0.51)
|
||||
point.x -= 0.02;
|
||||
const remainderY = point.y - Math.floor(point.y);
|
||||
if (remainderY > 0.49 && remainderY < 0.51)
|
||||
point.y -= 0.02;
|
||||
function quadMiddlePoint(quad: types.Quad): types.Point {
|
||||
const result = { x: 0, y: 0 };
|
||||
for (const point of quad) {
|
||||
result.x += point.x / 4;
|
||||
result.y += point.y / 4;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function triangleArea(p1: types.Point, p2: types.Point, p3: types.Point): number {
|
||||
return Math.abs(p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y)) / 2;
|
||||
}
|
||||
|
||||
function isPointInsideQuad(point: types.Point, quad: types.Quad): boolean {
|
||||
const area1 = triangleArea(point, quad[0], quad[1]) + triangleArea(point, quad[1], quad[2]) + triangleArea(point, quad[2], quad[3]) + triangleArea(point, quad[3], quad[0]);
|
||||
const area2 = triangleArea(quad[0], quad[1], quad[2]) + triangleArea(quad[1], quad[2], quad[3]);
|
||||
// Check that point is inside the quad.
|
||||
if (Math.abs(area1 - area2) > 0.1)
|
||||
return false;
|
||||
// Check that point is not on the right/bottom edge, because clicking
|
||||
// there does not actually click the element.
|
||||
return point.x < Math.max(quad[0].x, quad[1].x, quad[2].x, quad[3].x) &&
|
||||
point.y < Math.max(quad[0].y, quad[1].y, quad[2].y, quad[3].y);
|
||||
}
|
||||
|
||||
function findIntegerPointInsideQuad(quad: types.Quad): types.Point | undefined {
|
||||
// Try all four rounding directions of the middle point.
|
||||
const point = quadMiddlePoint(quad);
|
||||
point.x = Math.floor(point.x);
|
||||
point.y = Math.floor(point.y);
|
||||
if (isPointInsideQuad(point, quad))
|
||||
return point;
|
||||
point.x += 1;
|
||||
if (isPointInsideQuad(point, quad))
|
||||
return point;
|
||||
point.y += 1;
|
||||
if (isPointInsideQuad(point, quad))
|
||||
return point;
|
||||
point.x -= 1;
|
||||
if (isPointInsideQuad(point, quad))
|
||||
return point;
|
||||
}
|
||||
|
||||
export const kUnableToAdoptErrorMessage = 'Unable to adopt element handle from a different document';
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import * as types from './types';
|
|||
import { BrowserContext } from './browserContext';
|
||||
import type { Progress } from './progress';
|
||||
import { ProgressController } from './progress';
|
||||
import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, asLocator } from '../utils';
|
||||
import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, asLocator, compressCallLog } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
|
|
@ -296,7 +296,8 @@ export class FrameManager {
|
|||
if (request._documentId)
|
||||
frame.setPendingDocument({ documentId: request._documentId, request });
|
||||
if (request._isFavicon) {
|
||||
route?.continue({ isFallback: true }).catch(() => {});
|
||||
// Abort favicon requests to avoid network access in case of interception.
|
||||
route?.abort('aborted').catch(() => {});
|
||||
return;
|
||||
}
|
||||
this._page.emitOnContext(BrowserContext.Events.Request, request);
|
||||
|
|
@ -1452,7 +1453,7 @@ export class Frame extends SdkObject {
|
|||
timeout -= elapsed;
|
||||
}
|
||||
if (timeout < 0)
|
||||
return { matches: options.isNot, log: metadata.log, timedOut: true, received: lastIntermediateResult.received };
|
||||
return { matches: options.isNot, log: compressCallLog(metadata.log), timedOut: true, received: lastIntermediateResult.received };
|
||||
|
||||
// Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time.
|
||||
return await (new ProgressController(metadata, this)).run(async progress => {
|
||||
|
|
@ -1473,7 +1474,7 @@ export class Frame extends SdkObject {
|
|||
// A: We want user to receive a friendly message containing the last intermediate result.
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
||||
throw e;
|
||||
const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: metadata.log };
|
||||
const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: compressCallLog(metadata.log) };
|
||||
if (lastIntermediateResult.isSet)
|
||||
result.received = lastIntermediateResult.received;
|
||||
if (e instanceof TimeoutError)
|
||||
|
|
@ -1828,5 +1829,7 @@ function renderUnexpectedValue(expression: string, received: any): string {
|
|||
return received ? 'empty' : 'not empty';
|
||||
if (expression === 'to.be.focused')
|
||||
return received ? 'focused' : 'not focused';
|
||||
if (expression === 'to.match.aria')
|
||||
return received ? received.raw : received;
|
||||
return received;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ export {
|
|||
registry,
|
||||
registryDirectory,
|
||||
Registry,
|
||||
installDefaultBrowsersForNpmInstall,
|
||||
installBrowsersForNpmInstall,
|
||||
writeDockerVersion } from './registry';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,34 +14,47 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { escapeWithQuotes } from '@isomorphic/stringUtils';
|
||||
import * as roleUtils from './roleUtils';
|
||||
import { getElementComputedStyle } from './domUtils';
|
||||
import type { AriaRole } from './roleUtils';
|
||||
import { escapeRegExp, longestCommonSubstring } from '@isomorphic/stringUtils';
|
||||
import { yamlEscapeKeyIfNeeded, yamlEscapeValueIfNeeded } from './yaml';
|
||||
import type { AriaProps, AriaRole, AriaTemplateNode, AriaTemplateRoleNode, AriaTemplateTextNode } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
type AriaProps = {
|
||||
checked?: boolean | 'mixed';
|
||||
disabled?: boolean;
|
||||
expanded?: boolean;
|
||||
level?: number;
|
||||
pressed?: boolean | 'mixed';
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
type AriaNode = AriaProps & {
|
||||
export type AriaNode = AriaProps & {
|
||||
role: AriaRole | 'fragment';
|
||||
name: string;
|
||||
children: (AriaNode | string)[];
|
||||
element: Element;
|
||||
};
|
||||
|
||||
export type AriaTemplateNode = AriaProps & {
|
||||
role: AriaRole | 'fragment' | 'text';
|
||||
name?: RegExp | string;
|
||||
children?: (AriaTemplateNode | string | RegExp)[];
|
||||
export type AriaSnapshot = {
|
||||
root: AriaNode;
|
||||
elements: Map<number, Element>;
|
||||
ids: Map<Element, number>;
|
||||
};
|
||||
|
||||
export function generateAriaTree(rootElement: Element): AriaNode {
|
||||
export function generateAriaTree(rootElement: Element): AriaSnapshot {
|
||||
const visited = new Set<Node>();
|
||||
|
||||
const snapshot: AriaSnapshot = {
|
||||
root: { role: 'fragment', name: '', children: [], element: rootElement },
|
||||
elements: new Map<number, Element>(),
|
||||
ids: new Map<Element, number>(),
|
||||
};
|
||||
|
||||
const addElement = (element: Element) => {
|
||||
const id = snapshot.elements.size + 1;
|
||||
snapshot.elements.set(id, element);
|
||||
snapshot.ids.set(element, id);
|
||||
};
|
||||
|
||||
addElement(rootElement);
|
||||
|
||||
const visit = (ariaNode: AriaNode, node: Node) => {
|
||||
if (visited.has(node))
|
||||
return;
|
||||
visited.add(node);
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE && node.nodeValue) {
|
||||
const text = node.nodeValue;
|
||||
if (text)
|
||||
|
|
@ -56,13 +69,24 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
if (roleUtils.isElementHiddenForAria(element))
|
||||
return;
|
||||
|
||||
const ariaChildren: Element[] = [];
|
||||
if (element.hasAttribute('aria-owns')) {
|
||||
const ids = element.getAttribute('aria-owns')!.split(/\s+/);
|
||||
for (const id of ids) {
|
||||
const ownedElement = rootElement.ownerDocument.getElementById(id);
|
||||
if (ownedElement)
|
||||
ariaChildren.push(ownedElement);
|
||||
}
|
||||
}
|
||||
|
||||
addElement(element);
|
||||
const childAriaNode = toAriaNode(element);
|
||||
if (childAriaNode)
|
||||
ariaNode.children.push(childAriaNode);
|
||||
processChildNodes(childAriaNode || ariaNode, element);
|
||||
processElement(childAriaNode || ariaNode, element, ariaChildren);
|
||||
};
|
||||
|
||||
function processChildNodes(ariaNode: AriaNode, element: Element) {
|
||||
function processElement(ariaNode: AriaNode, element: Element, ariaChildren: Element[] = []) {
|
||||
// Surround every element with spaces for the sake of concatenated text nodes.
|
||||
const display = getElementComputedStyle(element)?.display || 'inline';
|
||||
const treatAsBlock = (display !== 'inline' || element.nodeName === 'BR') ? ' ' : '';
|
||||
|
|
@ -85,34 +109,36 @@ export function generateAriaTree(rootElement: Element): AriaNode {
|
|||
}
|
||||
}
|
||||
|
||||
for (const child of ariaChildren)
|
||||
visit(ariaNode, child);
|
||||
|
||||
ariaNode.children.push(roleUtils.getPseudoContent(element, '::after'));
|
||||
|
||||
if (treatAsBlock)
|
||||
ariaNode.children.push(treatAsBlock);
|
||||
|
||||
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
|
||||
if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0])
|
||||
ariaNode.children = [];
|
||||
}
|
||||
|
||||
roleUtils.beginAriaCaches();
|
||||
const ariaRoot: AriaNode = { role: 'fragment', name: '', children: [] };
|
||||
try {
|
||||
visit(ariaRoot, rootElement);
|
||||
visit(snapshot.root, rootElement);
|
||||
} finally {
|
||||
roleUtils.endAriaCaches();
|
||||
}
|
||||
|
||||
normalizeStringChildren(ariaRoot);
|
||||
return ariaRoot;
|
||||
normalizeStringChildren(snapshot.root);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function toAriaNode(element: Element): AriaNode | null {
|
||||
const role = roleUtils.getAriaRole(element);
|
||||
if (!role)
|
||||
if (!role || role === 'presentation' || role === 'none')
|
||||
return null;
|
||||
|
||||
const name = roleUtils.getElementAccessibleName(element, false) || '';
|
||||
const result: AriaNode = { role, name, children: [] };
|
||||
const result: AriaNode = { role, name, children: [], element };
|
||||
|
||||
if (roleUtils.kAriaCheckedRoles.includes(role))
|
||||
result.checked = roleUtils.getAriaChecked(element);
|
||||
|
|
@ -132,11 +158,10 @@ function toAriaNode(element: Element): AriaNode | null {
|
|||
if (roleUtils.kAriaSelectedRoles.includes(role))
|
||||
result.selected = roleUtils.getAriaSelected(element);
|
||||
|
||||
return result;
|
||||
}
|
||||
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)
|
||||
result.children = [element.value];
|
||||
|
||||
export function renderedAriaTree(rootElement: Element): string {
|
||||
return renderAriaTree(generateAriaTree(rootElement));
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeStringChildren(rootA11yNode: AriaNode) {
|
||||
|
|
@ -169,9 +194,9 @@ function normalizeStringChildren(rootA11yNode: AriaNode) {
|
|||
visit(rootA11yNode);
|
||||
}
|
||||
|
||||
const normalizeWhitespaceWithin = (text: string) => text.replace(/[\s\t\r\n]+/g, ' ');
|
||||
const normalizeWhitespaceWithin = (text: string) => text.replace(/[\u200b\s\t\r\n]+/g, ' ');
|
||||
|
||||
function matchesText(text: string | undefined, template: RegExp | string | undefined) {
|
||||
function matchesText(text: string, template: RegExp | string | undefined): boolean {
|
||||
if (!template)
|
||||
return true;
|
||||
if (!text)
|
||||
|
|
@ -181,17 +206,42 @@ function matchesText(text: string | undefined, template: RegExp | string | undef
|
|||
return !!text.match(template);
|
||||
}
|
||||
|
||||
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: boolean, received: string } {
|
||||
const root = generateAriaTree(rootElement);
|
||||
const matches = matchesNodeDeep(root, template);
|
||||
return { matches, received: renderAriaTree(root, { noText: true }) };
|
||||
function matchesTextNode(text: string, template: AriaTemplateTextNode) {
|
||||
return matchesText(text, template.text);
|
||||
}
|
||||
|
||||
function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegExp | string, depth: number): boolean {
|
||||
if (typeof node === 'string' && (typeof template === 'string' || template instanceof RegExp))
|
||||
return matchesText(node, template);
|
||||
function matchesName(text: string, template: AriaTemplateRoleNode) {
|
||||
return matchesText(text, template.name);
|
||||
}
|
||||
|
||||
if (typeof node === 'object' && typeof template === 'object' && !(template instanceof RegExp)) {
|
||||
export type MatcherReceived = {
|
||||
raw: string;
|
||||
regex: string;
|
||||
};
|
||||
|
||||
export function matchesAriaTree(rootElement: Element, template: AriaTemplateNode): { matches: AriaNode[], received: MatcherReceived } {
|
||||
const root = generateAriaTree(rootElement).root;
|
||||
const matches = matchesNodeDeep(root, template, false);
|
||||
return {
|
||||
matches,
|
||||
received: {
|
||||
raw: renderAriaTree(root, { mode: 'raw' }),
|
||||
regex: renderAriaTree(root, { mode: 'regex' }),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getAllByAria(rootElement: Element, template: AriaTemplateNode): Element[] {
|
||||
const root = generateAriaTree(rootElement).root;
|
||||
const matches = matchesNodeDeep(root, template, true);
|
||||
return matches.map(n => n.element);
|
||||
}
|
||||
|
||||
function matchesNode(node: AriaNode | string, template: AriaTemplateNode, depth: number): boolean {
|
||||
if (typeof node === 'string' && template.kind === 'text')
|
||||
return matchesTextNode(node, template);
|
||||
|
||||
if (typeof node === 'object' && template.kind === 'role') {
|
||||
if (template.role !== 'fragment' && template.role !== node.role)
|
||||
return false;
|
||||
if (template.checked !== undefined && template.checked !== node.checked)
|
||||
|
|
@ -206,7 +256,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx
|
|||
return false;
|
||||
if (template.selected !== undefined && template.selected !== node.selected)
|
||||
return false;
|
||||
if (!matchesText(node.name, template.name))
|
||||
if (!matchesName(node.name, template))
|
||||
return false;
|
||||
if (!containsList(node.children || [], template.children || [], depth))
|
||||
return false;
|
||||
|
|
@ -215,7 +265,7 @@ function matchesNode(node: AriaNode | string, template: AriaTemplateNode | RegEx
|
|||
return false;
|
||||
}
|
||||
|
||||
function containsList(children: (AriaNode | string)[], template: (AriaTemplateNode | RegExp | string)[], depth: number): boolean {
|
||||
function containsList(children: (AriaNode | string)[], template: AriaTemplateNode[], depth: number): boolean {
|
||||
if (template.length > children.length)
|
||||
return false;
|
||||
const cc = children.slice();
|
||||
|
|
@ -233,12 +283,12 @@ function containsList(children: (AriaNode | string)[], template: (AriaTemplateNo
|
|||
return true;
|
||||
}
|
||||
|
||||
function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
|
||||
const results: (AriaNode | string)[] = [];
|
||||
function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode, collectAll: boolean): AriaNode[] {
|
||||
const results: AriaNode[] = [];
|
||||
const visit = (node: AriaNode | string): boolean => {
|
||||
if (matchesNode(node, template, 0)) {
|
||||
results.push(node);
|
||||
return true;
|
||||
results.push(node as AriaNode);
|
||||
return !collectAll;
|
||||
}
|
||||
if (typeof node === 'string')
|
||||
return false;
|
||||
|
|
@ -249,57 +299,134 @@ function matchesNodeDeep(root: AriaNode, template: AriaTemplateNode): boolean {
|
|||
return false;
|
||||
};
|
||||
visit(root);
|
||||
return !!results.length;
|
||||
return results;
|
||||
}
|
||||
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: { noText?: boolean }): string {
|
||||
export function renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex', ids?: Map<Element, number> }): string {
|
||||
const lines: string[] = [];
|
||||
const visit = (ariaNode: AriaNode | string, indent: string) => {
|
||||
const includeText = options?.mode === 'regex' ? textContributesInfo : () => true;
|
||||
const renderString = options?.mode === 'regex' ? convertToBestGuessRegex : (str: string) => str;
|
||||
const visit = (ariaNode: AriaNode | string, parentAriaNode: AriaNode | null, indent: string) => {
|
||||
if (typeof ariaNode === 'string') {
|
||||
if (!options?.noText)
|
||||
lines.push(indent + '- text: ' + quoteYamlString(ariaNode));
|
||||
if (parentAriaNode && !includeText(parentAriaNode, ariaNode))
|
||||
return;
|
||||
const text = yamlEscapeValueIfNeeded(renderString(ariaNode));
|
||||
if (text)
|
||||
lines.push(indent + '- text: ' + text);
|
||||
return;
|
||||
}
|
||||
let line = `${indent}- ${ariaNode.role}`;
|
||||
if (ariaNode.name)
|
||||
line += ` ${escapeWithQuotes(ariaNode.name, '"')}`;
|
||||
|
||||
let key = ariaNode.role;
|
||||
// Yaml has a limit of 1024 characters per key, and we leave some space for role and attributes.
|
||||
if (ariaNode.name && ariaNode.name.length <= 900) {
|
||||
const name = renderString(ariaNode.name);
|
||||
if (name) {
|
||||
const stringifiedName = name.startsWith('/') && name.endsWith('/') ? name : JSON.stringify(name);
|
||||
key += ' ' + stringifiedName;
|
||||
}
|
||||
}
|
||||
if (ariaNode.checked === 'mixed')
|
||||
line += ` [checked=mixed]`;
|
||||
key += ` [checked=mixed]`;
|
||||
if (ariaNode.checked === true)
|
||||
line += ` [checked]`;
|
||||
key += ` [checked]`;
|
||||
if (ariaNode.disabled)
|
||||
line += ` [disabled]`;
|
||||
key += ` [disabled]`;
|
||||
if (ariaNode.expanded)
|
||||
line += ` [expanded]`;
|
||||
key += ` [expanded]`;
|
||||
if (ariaNode.level)
|
||||
line += ` [level=${ariaNode.level}]`;
|
||||
key += ` [level=${ariaNode.level}]`;
|
||||
if (ariaNode.pressed === 'mixed')
|
||||
line += ` [pressed=mixed]`;
|
||||
key += ` [pressed=mixed]`;
|
||||
if (ariaNode.pressed === true)
|
||||
line += ` [pressed]`;
|
||||
key += ` [pressed]`;
|
||||
if (ariaNode.selected === true)
|
||||
line += ` [selected]`;
|
||||
key += ` [selected]`;
|
||||
if (options?.ids) {
|
||||
const id = options?.ids.get(ariaNode.element);
|
||||
if (id)
|
||||
key += ` [id=${id}]`;
|
||||
}
|
||||
|
||||
lines.push(line + (ariaNode.children.length ? ':' : ''));
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, indent + ' ');
|
||||
const escapedKey = indent + '- ' + yamlEscapeKeyIfNeeded(key);
|
||||
if (!ariaNode.children.length) {
|
||||
lines.push(escapedKey);
|
||||
} else if (ariaNode.children.length === 1 && typeof ariaNode.children[0] === 'string') {
|
||||
const text = includeText(ariaNode, ariaNode.children[0]) ? renderString(ariaNode.children[0] as string) : null;
|
||||
if (text)
|
||||
lines.push(escapedKey + ': ' + yamlEscapeValueIfNeeded(text));
|
||||
else
|
||||
lines.push(escapedKey);
|
||||
} else {
|
||||
lines.push(escapedKey + ':');
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, ariaNode, indent + ' ');
|
||||
}
|
||||
};
|
||||
|
||||
if (ariaNode.role === 'fragment') {
|
||||
// Render fragment.
|
||||
for (const child of ariaNode.children || [])
|
||||
visit(child, '');
|
||||
visit(child, ariaNode, '');
|
||||
} else {
|
||||
visit(ariaNode, '');
|
||||
visit(ariaNode, null, '');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function quoteYamlString(str: string) {
|
||||
return `"${str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r')}"`;
|
||||
function convertToBestGuessRegex(text: string): string {
|
||||
const dynamicContent = [
|
||||
// 2mb
|
||||
{ regex: /\b[\d,.]+[bkmBKM]+\b/, replacement: '[\\d,.]+[bkmBKM]+' },
|
||||
// 2ms, 20s
|
||||
{ regex: /\b\d+[hmsp]+\b/, replacement: '\\d+[hmsp]+' },
|
||||
{ regex: /\b[\d,.]+[hmsp]+\b/, replacement: '[\\d,.]+[hmsp]+' },
|
||||
// Do not replace single digits with regex by default.
|
||||
// 2+ digits: [Issue 22, 22.3, 2.33, 2,333]
|
||||
{ regex: /\b\d+,\d+\b/, replacement: '\\d+,\\d+' },
|
||||
{ regex: /\b\d+\.\d{2,}\b/, replacement: '\\d+\\.\\d+' },
|
||||
{ regex: /\b\d{2,}\.\d+\b/, replacement: '\\d+\\.\\d+' },
|
||||
{ regex: /\b\d{2,}\b/, replacement: '\\d+' },
|
||||
];
|
||||
|
||||
let pattern = '';
|
||||
let lastIndex = 0;
|
||||
|
||||
const combinedRegex = new RegExp(dynamicContent.map(r => '(' + r.regex.source + ')').join('|'), 'g');
|
||||
text.replace(combinedRegex, (match, ...args) => {
|
||||
const offset = args[args.length - 2];
|
||||
const groups = args.slice(0, -2);
|
||||
pattern += escapeRegExp(text.slice(lastIndex, offset));
|
||||
for (let i = 0; i < groups.length; i++) {
|
||||
if (groups[i]) {
|
||||
const { replacement } = dynamicContent[i];
|
||||
pattern += replacement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
lastIndex = offset + match.length;
|
||||
return match;
|
||||
});
|
||||
if (!pattern)
|
||||
return text;
|
||||
|
||||
pattern += escapeRegExp(text.slice(lastIndex));
|
||||
return String(new RegExp(pattern));
|
||||
}
|
||||
|
||||
function textContributesInfo(node: AriaNode, text: string): boolean {
|
||||
if (!text.length)
|
||||
return false;
|
||||
|
||||
if (!node.name)
|
||||
return true;
|
||||
|
||||
if (node.name.length > text.length)
|
||||
return false;
|
||||
|
||||
// Figure out if text adds any value. "longestCommonSubstring" is expensive, so limit strings length.
|
||||
const substr = (text.length <= 200 && node.name.length <= 200) ? longestCommonSubstring(text, node.name) : '';
|
||||
let filtered = text;
|
||||
while (substr && filtered.includes(substr))
|
||||
filtered = filtered.replace(substr, '');
|
||||
return filtered.trim().length / text.length > 0.1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -90,7 +90,8 @@ export class Highlight {
|
|||
}
|
||||
|
||||
install() {
|
||||
if (!this._injectedScript.document.documentElement.contains(this._glassPaneElement))
|
||||
// NOTE: document.documentElement can be null: https://github.com/microsoft/TypeScript/issues/50078
|
||||
if (this._injectedScript.document.documentElement && !this._injectedScript.document.documentElement.contains(this._glassPaneElement))
|
||||
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,10 @@ import { kLayoutSelectorNames, type LayoutSelectorName, layoutSelectorScore } fr
|
|||
import { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||
import type { Language } from '../../utils/isomorphic/locatorGenerators';
|
||||
import { cacheNormalizedWhitespaces, normalizeWhiteSpace, trimStringWithEllipsis } from '../../utils/isomorphic/stringUtils';
|
||||
import { matchesAriaTree, renderedAriaTree } from './ariaSnapshot';
|
||||
import { matchesAriaTree, getAllByAria, generateAriaTree, renderAriaTree } from './ariaSnapshot';
|
||||
import type { AriaNode, AriaSnapshot } from './ariaSnapshot';
|
||||
import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot';
|
||||
import { parseYamlTemplate } from '@isomorphic/ariaSnapshot';
|
||||
|
||||
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue'> & { expectedValue?: any };
|
||||
|
||||
|
|
@ -82,6 +85,7 @@ export class InjectedScript {
|
|||
isElementVisible,
|
||||
isInsideScope,
|
||||
normalizeWhiteSpace,
|
||||
parseYamlTemplate,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
|
@ -212,10 +216,31 @@ export class InjectedScript {
|
|||
return new Set<Element>(result.map(r => r.element));
|
||||
}
|
||||
|
||||
ariaSnapshot(node: Node): string {
|
||||
ariaSnapshot(node: Node, options?: { mode?: 'raw' | 'regex', id?: boolean }): string {
|
||||
if (node.nodeType !== Node.ELEMENT_NODE)
|
||||
throw this.createStacklessError('Can only capture aria snapshot of Element nodes.');
|
||||
return renderedAriaTree(node as Element);
|
||||
const ariaSnapshot = generateAriaTree(node as Element);
|
||||
return renderAriaTree(ariaSnapshot.root, options);
|
||||
}
|
||||
|
||||
ariaSnapshotAsObject(node: Node): AriaSnapshot {
|
||||
return generateAriaTree(node as Element);
|
||||
}
|
||||
|
||||
ariaSnapshotElement(snapshot: AriaSnapshot, elementId: number): Element | null {
|
||||
return snapshot.elements.get(elementId) || null;
|
||||
}
|
||||
|
||||
renderAriaTree(ariaNode: AriaNode, options?: { mode?: 'raw' | 'regex', id?: boolean}): string {
|
||||
return renderAriaTree(ariaNode, options);
|
||||
}
|
||||
|
||||
renderAriaSnapshotWithIds(ariaSnapshot: AriaSnapshot): string {
|
||||
return renderAriaTree(ariaSnapshot.root, { ids: ariaSnapshot.ids });
|
||||
}
|
||||
|
||||
getAllByAria(document: Document, template: AriaTemplateNode): Element[] {
|
||||
return getAllByAria(document.documentElement, template);
|
||||
}
|
||||
|
||||
querySelectorAll(selector: ParsedSelector, root: Node): Element[] {
|
||||
|
|
@ -1263,8 +1288,13 @@ export class InjectedScript {
|
|||
}
|
||||
|
||||
{
|
||||
if (expression === 'to.match.aria')
|
||||
return matchesAriaTree(element, options.expectedValue);
|
||||
if (expression === 'to.match.aria') {
|
||||
const result = matchesAriaTree(element, options.expectedValue);
|
||||
return {
|
||||
received: result.received,
|
||||
matches: !!result.matches.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -1324,6 +1354,8 @@ export class InjectedScript {
|
|||
received = elements.map(e => options.useInnerText ? (e as HTMLElement).innerText : elementText(new Map(), e).full);
|
||||
else if (expression === 'to.have.class.array')
|
||||
received = elements.map(e => e.classList.toString());
|
||||
else if (expression === 'to.have.accessible.name.array')
|
||||
received = elements.map(e => getElementAccessibleName(e, false));
|
||||
|
||||
if (received && options.expectedText) {
|
||||
// "To match an array" is "to contain an array" + "equal length"
|
||||
|
|
|
|||
|
|
@ -207,9 +207,9 @@ class InspectTool implements RecorderTool {
|
|||
class RecordActionTool implements RecorderTool {
|
||||
private _recorder: Recorder;
|
||||
private _performingActions = new Set<actions.PerformOnRecordAction>();
|
||||
private _hoveredModel: HighlightModel | null = null;
|
||||
private _hoveredModel: HighlightModelWithSelector | null = null;
|
||||
private _hoveredElement: HTMLElement | null = null;
|
||||
private _activeModel: HighlightModel | null = null;
|
||||
private _activeModel: HighlightModelWithSelector | null = null;
|
||||
private _expectProgrammaticKeyUp = false;
|
||||
private _pendingClickAction: { action: actions.ClickAction, timeout: number } | undefined;
|
||||
|
||||
|
|
@ -492,9 +492,10 @@ class RecordActionTool implements RecorderTool {
|
|||
return;
|
||||
const result = activeElement ? this._recorder.injectedScript.generateSelector(activeElement, { testIdAttributeName: this._recorder.state.testIdAttributeName }) : null;
|
||||
this._activeModel = result && result.selector ? result : null;
|
||||
if (userGesture)
|
||||
if (userGesture) {
|
||||
this._hoveredElement = activeElement as HTMLElement | null;
|
||||
this._updateModelForHoveredElement();
|
||||
this._updateModelForHoveredElement();
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
|
||||
|
|
@ -589,6 +590,8 @@ class RecordActionTool implements RecorderTool {
|
|||
}
|
||||
|
||||
private _updateModelForHoveredElement() {
|
||||
if (this._performingActions.size)
|
||||
return;
|
||||
if (!this._hoveredElement || !this._hoveredElement.isConnected) {
|
||||
this._hoveredModel = null;
|
||||
this._hoveredElement = null;
|
||||
|
|
@ -605,7 +608,7 @@ class RecordActionTool implements RecorderTool {
|
|||
|
||||
class TextAssertionTool implements RecorderTool {
|
||||
private _recorder: Recorder;
|
||||
private _hoverHighlight: HighlightModel | null = null;
|
||||
private _hoverHighlight: HighlightModelWithSelector | null = null;
|
||||
private _action: actions.AssertAction | null = null;
|
||||
private _dialog: Dialog;
|
||||
private _textCache = new Map<Element | ShadowRoot, ElementText>();
|
||||
|
|
@ -715,7 +718,7 @@ class TextAssertionTool implements RecorderTool {
|
|||
name: 'assertSnapshot',
|
||||
selector: this._hoverHighlight.selector,
|
||||
signals: [],
|
||||
snapshot: this._recorder.injectedScript.ariaSnapshot(target),
|
||||
snapshot: this._recorder.injectedScript.ariaSnapshot(target, { mode: 'regex' }),
|
||||
};
|
||||
} else {
|
||||
this._hoverHighlight = this._recorder.injectedScript.generateSelector(target, { testIdAttributeName: this._recorder.state.testIdAttributeName, forTextExpect: true });
|
||||
|
|
@ -1018,7 +1021,8 @@ export class Recorder {
|
|||
private _listeners: (() => void)[] = [];
|
||||
private _currentTool: RecorderTool;
|
||||
private _tools: Record<Mode, RecorderTool>;
|
||||
private _actionSelectorModel: HighlightModel | null = null;
|
||||
private _lastHighlightedSelector: string | undefined = undefined;
|
||||
private _lastHighlightedAriaTemplateJSON: string = 'undefined';
|
||||
readonly highlight: Highlight;
|
||||
readonly overlay: Overlay | undefined;
|
||||
private _stylesheet: CSSStyleSheet;
|
||||
|
|
@ -1128,13 +1132,28 @@ export class Recorder {
|
|||
this._switchCurrentTool();
|
||||
this.overlay?.setUIState(state);
|
||||
|
||||
// Race or scroll.
|
||||
if (this._actionSelectorModel?.selector && !this._actionSelectorModel?.elements.length)
|
||||
this._actionSelectorModel = null;
|
||||
if (state.actionSelector !== this._actionSelectorModel?.selector)
|
||||
this._actionSelectorModel = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
|
||||
if (this.state.mode === 'none' || this.state.mode === 'standby')
|
||||
this.updateHighlight(this._actionSelectorModel, false);
|
||||
let highlight: HighlightModel | 'clear' | 'noop' = 'noop';
|
||||
if (state.actionSelector !== this._lastHighlightedSelector) {
|
||||
this._lastHighlightedSelector = state.actionSelector;
|
||||
const model = state.actionSelector ? querySelector(this.injectedScript, state.actionSelector, this.document) : null;
|
||||
highlight = model?.elements.length ? model : 'clear';
|
||||
}
|
||||
|
||||
const ariaTemplateJSON = JSON.stringify(state.ariaTemplate);
|
||||
if (this._lastHighlightedAriaTemplateJSON !== ariaTemplateJSON) {
|
||||
this._lastHighlightedAriaTemplateJSON = ariaTemplateJSON;
|
||||
const template = state.ariaTemplate ? this.injectedScript.utils.parseYamlTemplate(state.ariaTemplate) : undefined;
|
||||
const elements = template ? this.injectedScript.getAllByAria(this.document, template) : [];
|
||||
if (elements.length)
|
||||
highlight = { elements };
|
||||
else
|
||||
highlight = 'clear';
|
||||
}
|
||||
|
||||
if (highlight === 'clear')
|
||||
this.clearHighlight();
|
||||
else if (highlight !== 'noop')
|
||||
this.updateHighlight(highlight, false);
|
||||
}
|
||||
|
||||
clearHighlight() {
|
||||
|
|
@ -1249,6 +1268,8 @@ export class Recorder {
|
|||
private _onScroll(event: Event) {
|
||||
if (!event.isTrusted)
|
||||
return;
|
||||
this._lastHighlightedSelector = undefined;
|
||||
this._lastHighlightedAriaTemplateJSON = 'undefined';
|
||||
this.highlight.hideActionPoint();
|
||||
this._currentTool.onScroll?.(event);
|
||||
}
|
||||
|
|
@ -1439,10 +1460,14 @@ function consumeEvent(e: Event) {
|
|||
}
|
||||
|
||||
type HighlightModel = HighlightOptions & {
|
||||
selector: string;
|
||||
selector?: string;
|
||||
elements: Element[];
|
||||
};
|
||||
|
||||
type HighlightModelWithSelector = HighlightModel & {
|
||||
selector: string;
|
||||
};
|
||||
|
||||
function asCheckbox(node: Node | null): HTMLInputElement | null {
|
||||
if (!node || node.nodeName !== 'INPUT')
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { AriaRole } from '@isomorphic/ariaSnapshot';
|
||||
import { closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils';
|
||||
|
||||
function hasExplicitAccessibleName(e: Element) {
|
||||
|
|
@ -211,18 +212,6 @@ function getImplicitAriaRole(element: Element): AriaRole | null {
|
|||
return implicitRole;
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/wai-aria-1.2/#role_definitions
|
||||
// https://www.w3.org/TR/wai-aria-1.2/#abstract_roles
|
||||
// type AbstractRoles = 'command' | 'composite' | 'input' | 'landmark' | 'range' | 'roletype' | 'section' | 'sectionhead' | 'select' | 'structure' | 'widget' | 'window';
|
||||
|
||||
export type AriaRole = 'alert' | 'alertdialog' | 'application' | 'article' | 'banner' | 'blockquote' | 'button' | 'caption' | 'cell' | 'checkbox' | 'code' | 'columnheader' | 'combobox' |
|
||||
'complementary' | 'contentinfo' | 'definition' | 'deletion' | 'dialog' | 'directory' | 'document' | 'emphasis' | 'feed' | 'figure' | 'form' | 'generic' | 'grid' |
|
||||
'gridcell' | 'group' | 'heading' | 'img' | 'insertion' | 'link' | 'list' | 'listbox' | 'listitem' | 'log' | 'main' | 'mark' | 'marquee' | 'math' | 'meter' | 'menu' |
|
||||
'menubar' | 'menuitem' | 'menuitemcheckbox' | 'menuitemradio' | 'navigation' | 'none' | 'note' | 'option' | 'paragraph' | 'presentation' | 'progressbar' | 'radio' | 'radiogroup' |
|
||||
'region' | 'row' | 'rowgroup' | 'rowheader' | 'scrollbar' | 'search' | 'searchbox' | 'separator' | 'slider' |
|
||||
'spinbutton' | 'status' | 'strong' | 'subscript' | 'superscript' | 'switch' | 'tab' | 'table' | 'tablist' | 'tabpanel' | 'term' | 'textbox' | 'time' | 'timer' |
|
||||
'toolbar' | 'tooltip' | 'tree' | 'treegrid' | 'treeitem';
|
||||
|
||||
const validRoles: AriaRole[] = ['alert', 'alertdialog', 'application', 'article', 'banner', 'blockquote', 'button', 'caption', 'cell', 'checkbox', 'code', 'columnheader', 'combobox',
|
||||
'complementary', 'contentinfo', 'definition', 'deletion', 'dialog', 'directory', 'document', 'emphasis', 'feed', 'figure', 'form', 'generic', 'grid',
|
||||
'gridcell', 'group', 'heading', 'img', 'insertion', 'link', 'list', 'listbox', 'listitem', 'log', 'main', 'mark', 'marquee', 'math', 'meter', 'menu',
|
||||
|
|
@ -394,7 +383,11 @@ export function getAriaLabelledByElements(element: Element): Element[] | null {
|
|||
const ref = element.getAttribute('aria-labelledby');
|
||||
if (ref === null)
|
||||
return null;
|
||||
return getIdRefs(element, ref);
|
||||
const refs = getIdRefs(element, ref);
|
||||
// step 2b:
|
||||
// "if the current node has an aria-labelledby attribute that contains at least one valid IDREF"
|
||||
// Therefore, if none of the refs match an element, we consider aria-labelledby to be missing.
|
||||
return refs.length ? refs : null;
|
||||
}
|
||||
|
||||
function allowsNameFromContent(role: string, targetDescendant: boolean) {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue