fix(runtime): e2e 发现的 3 个 bug 修复
- download: desktop_download_apps SingleChildScrollView 加 primary:true (Scrollbar 缺 ScrollPosition) - contact-stores: checkDomainAvailability async 后 setState 加 mounted 保护 (dispose 后 setState) - buy-service: _loadData 加 data 结构校验 + mounted 保护 (API 400 无效参数时 data 为错误结构 -> RangeError)
13
tools/e2e/api_buy.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const { chromium } = require('playwright');
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await (await browser.newContext()).newPage();
|
||||
await page.goto('http://127.0.0.1:8099/', { waitUntil: 'load' });
|
||||
await page.waitForTimeout(3000);
|
||||
const data = await page.evaluate(async () => {
|
||||
const r = await fetch('https://api.minipos.us/v1/get-buy-service-info/pos/1?store_id=0');
|
||||
return JSON.stringify(await r.json());
|
||||
}).catch(e => 'ERR: ' + e.message);
|
||||
console.log('buy-service API:', data.slice(0, 600));
|
||||
await browser.close();
|
||||
})();
|
||||
22
tools/e2e/capture_api.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// 拦截 buy-service 的 API 请求/响应,看实际数据结构
|
||||
const { chromium } = require('playwright');
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
|
||||
page.on('response', async (resp) => {
|
||||
const url = resp.url();
|
||||
if (url.includes('get-buy-service-info')) {
|
||||
console.log('=== buy-service API 响应 ===');
|
||||
console.log('状态:', resp.status());
|
||||
try {
|
||||
const body = await resp.text();
|
||||
console.log('body:', body.slice(0, 700));
|
||||
} catch(e) { console.log('读 body 失败'); }
|
||||
}
|
||||
});
|
||||
page.on('pageerror', e => console.log('PAGEERR:', e.message.slice(0,150)));
|
||||
page.on('console', m => { if (m.text().startsWith('data:') || m.text().includes('RangeError')) console.log('CONSOLE:', m.text().slice(0,200)); });
|
||||
await page.goto('http://127.0.0.1:8099/buy-service/1/pos', { waitUntil: 'load' }).catch(()=>{});
|
||||
await page.waitForTimeout(8000);
|
||||
await browser.close();
|
||||
})();
|
||||
12
tools/e2e/capture_data.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const { chromium } = require('playwright');
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
|
||||
const logs = [];
|
||||
page.on('console', m => logs.push(m.text()));
|
||||
await page.goto('http://127.0.0.1:8099/buy-service/1/pos', { waitUntil: 'load' }).catch(()=>{});
|
||||
await page.waitForTimeout(6000);
|
||||
const dataLog = logs.filter(l => l.startsWith('data: {') || l.startsWith('data:{'));
|
||||
console.log('应用 data:', dataLog[0] ? dataLog[0].slice(0, 500) : '(未捕获)');
|
||||
await browser.close();
|
||||
})();
|
||||
21
tools/e2e/capture_data2.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const { chromium } = require('playwright');
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await (await browser.newContext({viewport:{width:1280,height:900}})).newPage();
|
||||
const logs = [];
|
||||
page.on('console', m => logs.push(m.text()));
|
||||
page.on('pageerror', e => logs.push('PAGEERR: ' + e.message));
|
||||
await page.goto('http://127.0.0.1:8099/buy-service/1/pos', { waitUntil: 'load' }).catch(()=>{});
|
||||
await page.waitForTimeout(7000);
|
||||
// 找 data 打印 + RangeError
|
||||
const dataLog = logs.find(l => l.startsWith('data:'));
|
||||
console.log('=== data ===', dataLog ? dataLog.slice(0,400) : '(无)');
|
||||
// 看 service_selections / options 部分
|
||||
if (dataLog) {
|
||||
const m = dataLog.match(/service_selections:[^,}]+/);
|
||||
if (m) console.log('service_selections:', m[0].slice(0,200));
|
||||
const o = dataLog.match(/options:[^]]*\]/);
|
||||
if (o) console.log('options:', o[0].slice(0,200));
|
||||
}
|
||||
await browser.close();
|
||||
})();
|
||||
33
tools/e2e/diag.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// 诊断:对失败路由打印完整错误 + CK_STACK
|
||||
const { chromium } = require('playwright');
|
||||
const BASE = 'http://127.0.0.1:8099';
|
||||
const ROUTES = ['/contact-stores', '/download', '/buy-service/1/pos'];
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
for (const route of ROUTES) {
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const allLogs = [];
|
||||
page.on('pageerror', e => allLogs.push(`PAGEERROR: ${e.message}`));
|
||||
page.on('console', m => allLogs.push(m.text()));
|
||||
await page.goto(BASE + route, { waitUntil: 'load', timeout: 30000 }).catch(e => allLogs.push('GOTO: ' + e.message.slice(0,80)));
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
console.log(`\n########## ${route} ##########`);
|
||||
// 只打印 CK_ERROR + CK_STACK + PAGEERROR
|
||||
for (let i = 0; i < allLogs.length; i++) {
|
||||
const l = allLogs[i];
|
||||
if (l.startsWith('CK_ERROR:') || l.startsWith('PAGEERROR')) {
|
||||
console.log(l.slice(0, 300));
|
||||
// 找紧跟的 CK_STACK
|
||||
if (allLogs[i+1] && allLogs[i+1].startsWith('CK_STACK:')) {
|
||||
const proj = allLogs[i+1].match(/flutter_wisetronic\/[^\s]+:\d+/g);
|
||||
console.log(' → 项目位置:', proj ? [...new Set(proj)].slice(0,4) : '(无项目帧)');
|
||||
}
|
||||
}
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await browser.close();
|
||||
})();
|
||||
32
tools/e2e/diag2.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// 诊断 v2:打印完整 CK_STACK(定位 framework 抛的错误的源头)
|
||||
const { chromium } = require('playwright');
|
||||
const BASE = 'http://127.0.0.1:8099';
|
||||
const ROUTES = ['/buy-service/1/pos', '/download'];
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
for (const route of ROUTES) {
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const page = await ctx.newPage();
|
||||
const allLogs = [];
|
||||
page.on('pageerror', e => allLogs.push(`PAGEERROR: ${e.stack || e.message}`));
|
||||
page.on('console', m => allLogs.push(m.text()));
|
||||
await page.goto(BASE + route, { waitUntil: 'load', timeout: 30000 }).catch(()=>{});
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
console.log(`\n########## ${route} ##########`);
|
||||
for (let i = 0; i < allLogs.length; i++) {
|
||||
const l = allLogs[i];
|
||||
if (l.startsWith('CK_ERROR:') || l.startsWith('PAGEERROR')) {
|
||||
console.log('--- ' + l.slice(0, 150));
|
||||
if (allLogs[i+1] && allLogs[i+1].startsWith('CK_STACK:')) {
|
||||
// 打印堆栈里所有 flutter_wisetronic 帧
|
||||
const proj = allLogs[i+1].match(/flutter_wisetronic\/[^\s]+/g) || [];
|
||||
console.log(' 项目帧: ' + (proj.length ? [...new Set(proj)].join('\n ') : '(无)'));
|
||||
}
|
||||
}
|
||||
}
|
||||
await ctx.close();
|
||||
}
|
||||
await browser.close();
|
||||
})();
|
||||
47
tools/e2e/dump_idb.js
Normal file
@@ -0,0 +1,47 @@
|
||||
// dump IndexedDB 结构 + Hive entry 格式
|
||||
const { chromium } = require('playwright');
|
||||
const BASE = 'http://127.0.0.1:8099';
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const page = await (await browser.newContext()).newPage();
|
||||
await page.goto(BASE + '/', { waitUntil: 'load', timeout: 60000 });
|
||||
await page.waitForTimeout(7000);
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const dbs = await indexedDB.databases();
|
||||
out.databases = dbs.map(d => d.name);
|
||||
for (const db of dbs) {
|
||||
out[db.name] = [];
|
||||
await new Promise((resolve) => {
|
||||
const req = indexedDB.open(db.name);
|
||||
req.onsuccess = () => {
|
||||
const d = req.result;
|
||||
const stores = [...d.objectStoreNames];
|
||||
out[db.name + '_stores'] = stores;
|
||||
if (stores.length === 0) { d.close(); resolve(); return; }
|
||||
const tx = d.transaction(stores[0], 'readonly');
|
||||
const store = tx.objectStore(stores[0]);
|
||||
const allReq = store.getAll();
|
||||
const keysReq = store.getAllKeys();
|
||||
allReq.onsuccess = () => {
|
||||
out[db.name] = allReq.result.map((v, i) => {
|
||||
let hex = '';
|
||||
if (v instanceof ArrayBuffer) {
|
||||
const u = new Uint8Array(v);
|
||||
hex = Array.from(u.slice(0, 40)).map(b => b.toString(16).padStart(2,'0')).join(' ');
|
||||
return { key: keysReq.result[i], valueBytes: v.byteLength, hexFirst40: hex };
|
||||
}
|
||||
return { key: keysReq.result[i], value: typeof v === 'string' ? v.slice(0,80) : v };
|
||||
});
|
||||
};
|
||||
tx.oncomplete = () => { d.close(); resolve(); };
|
||||
};
|
||||
req.onerror = () => resolve();
|
||||
});
|
||||
}
|
||||
return out;
|
||||
});
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
await browser.close();
|
||||
})().catch(e => { console.error('失败:', e.message); process.exit(1); });
|
||||
BIN
tools/e2e/login.png
Normal file
|
After Width: | Height: | Size: 813 KiB |
BIN
tools/e2e/login_semantics.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
1
tools/e2e/node_modules/.bin/playwright
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../playwright/cli.js
|
||||
1
tools/e2e/node_modules/.bin/playwright-core
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../playwright-core/cli.js
|
||||
38
tools/e2e/node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "e2e",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/playwright": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
202
tools/e2e/node_modules/playwright-core/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Portions Copyright (c) Microsoft Corporation.
|
||||
Portions Copyright 2017 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
5
tools/e2e/node_modules/playwright-core/NOTICE
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Playwright
|
||||
Copyright (c) Microsoft Corporation
|
||||
|
||||
This software contains code derived from the Puppeteer project (https://github.com/puppeteer/puppeteer),
|
||||
available under the Apache 2.0 license (https://github.com/puppeteer/puppeteer/blob/master/LICENSE).
|
||||
3
tools/e2e/node_modules/playwright-core/README.md
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# playwright-core
|
||||
|
||||
This package contains the no-browser flavor of [Playwright](http://github.com/microsoft/playwright).
|
||||
13
tools/e2e/node_modules/playwright-core/ThirdPartyNotices.txt
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
microsoft/playwright-core
|
||||
|
||||
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
|
||||
|
||||
This package bundles third-party software inside individual files under
|
||||
`lib/`. Each bundled output has a sidecar `<bundle>.js.LICENSE` file next
|
||||
to it listing every npm package whose source was inlined into that
|
||||
bundle, together with the full license text for each.
|
||||
|
||||
For example:
|
||||
- lib/utilsBundle.js.LICENSE
|
||||
|
||||
This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise.
|
||||
5
tools/e2e/node_modules/playwright-core/bin/install_media_pack.ps1
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
$osInfo = Get-WmiObject -Class Win32_OperatingSystem
|
||||
# check if running on Windows Server
|
||||
if ($osInfo.ProductType -eq 3) {
|
||||
Install-WindowsFeature Server-Media-Foundation
|
||||
}
|
||||
33
tools/e2e/node_modules/playwright-core/bin/install_webkit_wsl.ps1
generated
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# This script sets up a WSL distribution that will be used to run WebKit.
|
||||
|
||||
$Distribution = "playwright"
|
||||
$Username = "pwuser"
|
||||
|
||||
$distributions = (wsl --list --quiet) -split "\r?\n"
|
||||
if ($distributions -contains $Distribution) {
|
||||
Write-Host "WSL distribution '$Distribution' already exists. Skipping installation."
|
||||
} else {
|
||||
Write-Host "Installing new WSL distribution '$Distribution'..."
|
||||
$VhdSize = "10GB"
|
||||
wsl --install -d Ubuntu-24.04 --name $Distribution --no-launch --vhd-size $VhdSize
|
||||
wsl -d $Distribution -u root adduser --gecos GECOS --disabled-password $Username
|
||||
}
|
||||
|
||||
$pwshDirname = (Resolve-Path -Path $PSScriptRoot).Path;
|
||||
$playwrightCoreRoot = Resolve-Path (Join-Path $pwshDirname "..")
|
||||
|
||||
$initScript = @"
|
||||
if [ ! -f "/home/$Username/node/bin/node" ]; then
|
||||
mkdir -p /home/$Username/node
|
||||
curl -fsSL https://nodejs.org/dist/v22.17.0/node-v22.17.0-linux-x64.tar.xz -o /home/$Username/node/node-v22.17.0-linux-x64.tar.xz
|
||||
tar -xJf /home/$Username/node/node-v22.17.0-linux-x64.tar.xz -C /home/$Username/node --strip-components=1
|
||||
sudo -u $Username echo 'export PATH=/home/$Username/node/bin:\`$PATH' >> /home/$Username/.profile
|
||||
fi
|
||||
/home/$Username/node/bin/node cli.js install-deps webkit
|
||||
sudo -u $Username PLAYWRIGHT_SKIP_BROWSER_GC=1 /home/$Username/node/bin/node cli.js install webkit
|
||||
"@ -replace "\r\n", "`n"
|
||||
|
||||
wsl -d $Distribution --cd $playwrightCoreRoot -u root -- bash -c "$initScript"
|
||||
Write-Host "Done!"
|
||||
42
tools/e2e/node_modules/playwright-core/bin/reinstall_chrome_beta_linux.sh
generated
vendored
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [[ $(arch) == "aarch64" ]]; then
|
||||
echo "ERROR: not supported on Linux Arm64"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old beta if any.
|
||||
if dpkg --get-selections | grep -q "^google-chrome-beta[[:space:]]*install$" >/dev/null; then
|
||||
apt-get remove -y google-chrome-beta
|
||||
fi
|
||||
|
||||
# 2. Update apt lists (needed to install curl and chrome dependencies)
|
||||
apt-get update
|
||||
|
||||
# 3. Install curl to download chrome
|
||||
if ! command -v curl >/dev/null; then
|
||||
apt-get install -y curl
|
||||
fi
|
||||
|
||||
# 4. download chrome beta from dl.google.com and install it.
|
||||
cd /tmp
|
||||
curl -L -O https://dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb
|
||||
apt-get install -y ./google-chrome-beta_current_amd64.deb
|
||||
rm -rf ./google-chrome-beta_current_amd64.deb
|
||||
cd -
|
||||
google-chrome-beta --version
|
||||
13
tools/e2e/node_modules/playwright-core/bin/reinstall_chrome_beta_mac.sh
generated
vendored
Executable file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
rm -rf "/Applications/Google Chrome Beta.app"
|
||||
cd /tmp
|
||||
curl -L --retry 3 -o ./googlechromebeta.dmg https://dl.google.com/chrome/mac/universal/beta/googlechromebeta.dmg
|
||||
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechromebeta.dmg ./googlechromebeta.dmg
|
||||
cp -pR "/Volumes/googlechromebeta.dmg/Google Chrome Beta.app" /Applications
|
||||
hdiutil detach /Volumes/googlechromebeta.dmg
|
||||
rm -rf /tmp/googlechromebeta.dmg
|
||||
|
||||
/Applications/Google\ Chrome\ Beta.app/Contents/MacOS/Google\ Chrome\ Beta --version
|
||||
24
tools/e2e/node_modules/playwright-core/bin/reinstall_chrome_beta_win.ps1
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$url = 'https://dl.google.com/tag/s/dl/chrome/install/beta/googlechromebetastandaloneenterprise64.msi'
|
||||
|
||||
Write-Host "Downloading Google Chrome Beta"
|
||||
$wc = New-Object net.webclient
|
||||
$msiInstaller = "$env:temp\google-chrome-beta.msi"
|
||||
$wc.Downloadfile($url, $msiInstaller)
|
||||
|
||||
Write-Host "Installing Google Chrome Beta"
|
||||
$arguments = "/i `"$msiInstaller`" /quiet"
|
||||
Start-Process msiexec.exe -ArgumentList $arguments -Wait
|
||||
Remove-Item $msiInstaller
|
||||
|
||||
$suffix = "\\Google\\Chrome Beta\\Application\\chrome.exe"
|
||||
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
|
||||
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
|
||||
} else {
|
||||
Write-Host "ERROR: Failed to install Google Chrome Beta."
|
||||
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
|
||||
exit 1
|
||||
}
|
||||
42
tools/e2e/node_modules/playwright-core/bin/reinstall_chrome_stable_linux.sh
generated
vendored
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [[ $(arch) == "aarch64" ]]; then
|
||||
echo "ERROR: not supported on Linux Arm64"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old stable if any.
|
||||
if dpkg --get-selections | grep -q "^google-chrome[[:space:]]*install$" >/dev/null; then
|
||||
apt-get remove -y google-chrome
|
||||
fi
|
||||
|
||||
# 2. Update apt lists (needed to install curl and chrome dependencies)
|
||||
apt-get update
|
||||
|
||||
# 3. Install curl to download chrome
|
||||
if ! command -v curl >/dev/null; then
|
||||
apt-get install -y curl
|
||||
fi
|
||||
|
||||
# 4. download chrome stable from dl.google.com and install it.
|
||||
cd /tmp
|
||||
curl -L -O https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
apt-get install -y ./google-chrome-stable_current_amd64.deb
|
||||
rm -rf ./google-chrome-stable_current_amd64.deb
|
||||
cd -
|
||||
google-chrome --version
|
||||
12
tools/e2e/node_modules/playwright-core/bin/reinstall_chrome_stable_mac.sh
generated
vendored
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
rm -rf "/Applications/Google Chrome.app"
|
||||
cd /tmp
|
||||
curl -L --retry 3 -o ./googlechrome.dmg https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg
|
||||
hdiutil attach -nobrowse -quiet -noautofsck -noautoopen -mountpoint /Volumes/googlechrome.dmg ./googlechrome.dmg
|
||||
cp -pR "/Volumes/googlechrome.dmg/Google Chrome.app" /Applications
|
||||
hdiutil detach /Volumes/googlechrome.dmg
|
||||
rm -rf /tmp/googlechrome.dmg
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --version
|
||||
24
tools/e2e/node_modules/playwright-core/bin/reinstall_chrome_stable_win.ps1
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$url = 'https://dl.google.com/tag/s/dl/chrome/install/googlechromestandaloneenterprise64.msi'
|
||||
|
||||
$wc = New-Object net.webclient
|
||||
$msiInstaller = "$env:temp\google-chrome.msi"
|
||||
Write-Host "Downloading Google Chrome"
|
||||
$wc.Downloadfile($url, $msiInstaller)
|
||||
|
||||
Write-Host "Installing Google Chrome"
|
||||
$arguments = "/i `"$msiInstaller`" /quiet"
|
||||
Start-Process msiexec.exe -ArgumentList $arguments -Wait
|
||||
Remove-Item $msiInstaller
|
||||
|
||||
|
||||
$suffix = "\\Google\\Chrome\\Application\\chrome.exe"
|
||||
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
|
||||
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
|
||||
} else {
|
||||
Write-Host "ERROR: Failed to install Google Chrome."
|
||||
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
|
||||
exit 1
|
||||
}
|
||||
48
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_beta_linux.sh
generated
vendored
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [[ $(arch) == "aarch64" ]]; then
|
||||
echo "ERROR: not supported on Linux Arm64"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old beta if any.
|
||||
if dpkg --get-selections | grep -q "^microsoft-edge-beta[[:space:]]*install$" >/dev/null; then
|
||||
apt-get remove -y microsoft-edge-beta
|
||||
fi
|
||||
|
||||
# 2. Install curl to download Microsoft gpg key
|
||||
if ! command -v curl >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
fi
|
||||
|
||||
# GnuPG is not preinstalled in slim images
|
||||
if ! command -v gpg >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y gpg
|
||||
fi
|
||||
|
||||
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
|
||||
curl -L https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
|
||||
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/
|
||||
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list'
|
||||
rm /tmp/microsoft.gpg
|
||||
apt-get update && apt-get install -y microsoft-edge-beta
|
||||
|
||||
microsoft-edge-beta --version
|
||||
11
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_beta_mac.sh
generated
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
cd /tmp
|
||||
curl -L --retry 3 -o ./msedge_beta.pkg "$1"
|
||||
# Note: there's no way to uninstall previously installed MSEdge.
|
||||
# However, running PKG again seems to update installation.
|
||||
sudo installer -pkg /tmp/msedge_beta.pkg -target /
|
||||
rm -rf /tmp/msedge_beta.pkg
|
||||
/Applications/Microsoft\ Edge\ Beta.app/Contents/MacOS/Microsoft\ Edge\ Beta --version
|
||||
23
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_beta_win.ps1
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$url = $args[0]
|
||||
|
||||
Write-Host "Downloading Microsoft Edge Beta"
|
||||
$wc = New-Object net.webclient
|
||||
$msiInstaller = "$env:temp\microsoft-edge-beta.msi"
|
||||
$wc.Downloadfile($url, $msiInstaller)
|
||||
|
||||
Write-Host "Installing Microsoft Edge Beta"
|
||||
$arguments = "/i `"$msiInstaller`" /quiet"
|
||||
Start-Process msiexec.exe -ArgumentList $arguments -Wait
|
||||
Remove-Item $msiInstaller
|
||||
|
||||
$suffix = "\\Microsoft\\Edge Beta\\Application\\msedge.exe"
|
||||
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
|
||||
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
|
||||
} else {
|
||||
Write-Host "ERROR: Failed to install Microsoft Edge Beta."
|
||||
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
|
||||
exit 1
|
||||
}
|
||||
48
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_dev_linux.sh
generated
vendored
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [[ $(arch) == "aarch64" ]]; then
|
||||
echo "ERROR: not supported on Linux Arm64"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old dev if any.
|
||||
if dpkg --get-selections | grep -q "^microsoft-edge-dev[[:space:]]*install$" >/dev/null; then
|
||||
apt-get remove -y microsoft-edge-dev
|
||||
fi
|
||||
|
||||
# 2. Install curl to download Microsoft gpg key
|
||||
if ! command -v curl >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
fi
|
||||
|
||||
# GnuPG is not preinstalled in slim images
|
||||
if ! command -v gpg >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y gpg
|
||||
fi
|
||||
|
||||
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
|
||||
curl -L https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
|
||||
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/
|
||||
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-dev.list'
|
||||
rm /tmp/microsoft.gpg
|
||||
apt-get update && apt-get install -y microsoft-edge-dev
|
||||
|
||||
microsoft-edge-dev --version
|
||||
11
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_dev_mac.sh
generated
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
cd /tmp
|
||||
curl -L --retry 3 -o ./msedge_dev.pkg "$1"
|
||||
# Note: there's no way to uninstall previously installed MSEdge.
|
||||
# However, running PKG again seems to update installation.
|
||||
sudo installer -pkg /tmp/msedge_dev.pkg -target /
|
||||
rm -rf /tmp/msedge_dev.pkg
|
||||
/Applications/Microsoft\ Edge\ Dev.app/Contents/MacOS/Microsoft\ Edge\ Dev --version
|
||||
23
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_dev_win.ps1
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$url = $args[0]
|
||||
|
||||
Write-Host "Downloading Microsoft Edge Dev"
|
||||
$wc = New-Object net.webclient
|
||||
$msiInstaller = "$env:temp\microsoft-edge-dev.msi"
|
||||
$wc.Downloadfile($url, $msiInstaller)
|
||||
|
||||
Write-Host "Installing Microsoft Edge Dev"
|
||||
$arguments = "/i `"$msiInstaller`" /quiet"
|
||||
Start-Process msiexec.exe -ArgumentList $arguments -Wait
|
||||
Remove-Item $msiInstaller
|
||||
|
||||
$suffix = "\\Microsoft\\Edge Dev\\Application\\msedge.exe"
|
||||
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
|
||||
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
|
||||
} else {
|
||||
Write-Host "ERROR: Failed to install Microsoft Edge Dev."
|
||||
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
|
||||
exit 1
|
||||
}
|
||||
48
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_stable_linux.sh
generated
vendored
Executable file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
if [[ $(arch) == "aarch64" ]]; then
|
||||
echo "ERROR: not supported on Linux Arm64"
|
||||
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
|
||||
fi
|
||||
fi
|
||||
|
||||
# 1. make sure to remove old stable if any.
|
||||
if dpkg --get-selections | grep -q "^microsoft-edge-stable[[:space:]]*install$" >/dev/null; then
|
||||
apt-get remove -y microsoft-edge-stable
|
||||
fi
|
||||
|
||||
# 2. Install curl to download Microsoft gpg key
|
||||
if ! command -v curl >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y curl
|
||||
fi
|
||||
|
||||
# GnuPG is not preinstalled in slim images
|
||||
if ! command -v gpg >/dev/null; then
|
||||
apt-get update
|
||||
apt-get install -y gpg
|
||||
fi
|
||||
|
||||
# 3. Add the GPG key, the apt repo, update the apt cache, and install the package
|
||||
curl -L https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > /tmp/microsoft.gpg
|
||||
install -o root -g root -m 644 /tmp/microsoft.gpg /etc/apt/trusted.gpg.d/
|
||||
sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/edge stable main" > /etc/apt/sources.list.d/microsoft-edge-stable.list'
|
||||
rm /tmp/microsoft.gpg
|
||||
apt-get update && apt-get install -y microsoft-edge-stable
|
||||
|
||||
microsoft-edge-stable --version
|
||||
11
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_stable_mac.sh
generated
vendored
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
cd /tmp
|
||||
curl -L --retry 3 -o ./msedge_stable.pkg "$1"
|
||||
# Note: there's no way to uninstall previously installed MSEdge.
|
||||
# However, running PKG again seems to update installation.
|
||||
sudo installer -pkg /tmp/msedge_stable.pkg -target /
|
||||
rm -rf /tmp/msedge_stable.pkg
|
||||
/Applications/Microsoft\ Edge.app/Contents/MacOS/Microsoft\ Edge --version
|
||||
24
tools/e2e/node_modules/playwright-core/bin/reinstall_msedge_stable_win.ps1
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$url = $args[0]
|
||||
|
||||
Write-Host "Downloading Microsoft Edge"
|
||||
$wc = New-Object net.webclient
|
||||
$msiInstaller = "$env:temp\microsoft-edge-stable.msi"
|
||||
$wc.Downloadfile($url, $msiInstaller)
|
||||
|
||||
Write-Host "Installing Microsoft Edge"
|
||||
$arguments = "/i `"$msiInstaller`" /quiet"
|
||||
Start-Process msiexec.exe -ArgumentList $arguments -Wait
|
||||
Remove-Item $msiInstaller
|
||||
|
||||
$suffix = "\\Microsoft\\Edge\\Application\\msedge.exe"
|
||||
if (Test-Path "${env:ProgramFiles(x86)}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles(x86)}$suffix").VersionInfo
|
||||
} elseif (Test-Path "${env:ProgramFiles}$suffix") {
|
||||
(Get-Item "${env:ProgramFiles}$suffix").VersionInfo
|
||||
} else {
|
||||
Write-Host "ERROR: Failed to install Microsoft Edge."
|
||||
Write-Host "ERROR: This could be due to insufficient privileges, in which case re-running as Administrator may help."
|
||||
exit 1
|
||||
}
|
||||
75
tools/e2e/node_modules/playwright-core/browsers.json
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"comment": "Do not edit this file, use utils/roll_browser.js",
|
||||
"browsers": [
|
||||
{
|
||||
"name": "chromium",
|
||||
"revision": "1234",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "151.0.7922.34",
|
||||
"title": "Chrome for Testing"
|
||||
},
|
||||
{
|
||||
"name": "chromium-headless-shell",
|
||||
"revision": "1234",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "151.0.7922.34",
|
||||
"title": "Chrome Headless Shell"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree",
|
||||
"revision": "1433",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "151.0.7893.0",
|
||||
"title": "Chrome Canary for Testing"
|
||||
},
|
||||
{
|
||||
"name": "chromium-tip-of-tree-headless-shell",
|
||||
"revision": "1433",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "151.0.7893.0",
|
||||
"title": "Chrome Canary Headless Shell"
|
||||
},
|
||||
{
|
||||
"name": "firefox",
|
||||
"revision": "1538",
|
||||
"installByDefault": true,
|
||||
"browserVersion": "153.0",
|
||||
"title": "Firefox"
|
||||
},
|
||||
{
|
||||
"name": "firefox-beta",
|
||||
"revision": "1526",
|
||||
"installByDefault": false,
|
||||
"browserVersion": "152.0b1",
|
||||
"title": "Firefox Beta"
|
||||
},
|
||||
{
|
||||
"name": "webkit",
|
||||
"revision": "2336",
|
||||
"installByDefault": true,
|
||||
"revisionOverrides": {
|
||||
"mac14": "2251",
|
||||
"mac14-arm64": "2251",
|
||||
"ubuntu20.04-x64": "2092",
|
||||
"ubuntu20.04-arm64": "2092"
|
||||
},
|
||||
"browserVersion": "26.5",
|
||||
"title": "WebKit"
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg",
|
||||
"revision": "1011",
|
||||
"installByDefault": true
|
||||
},
|
||||
{
|
||||
"name": "winldd",
|
||||
"revision": "1007",
|
||||
"installByDefault": false
|
||||
},
|
||||
{
|
||||
"name": "android",
|
||||
"revision": "1001",
|
||||
"installByDefault": false
|
||||
}
|
||||
]
|
||||
}
|
||||
21
tools/e2e/node_modules/playwright-core/cli.js
generated
vendored
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const { libCli, libCliTestStub } = require('./lib/coreBundle');
|
||||
const { program } = require('./lib/utilsBundle');
|
||||
libCli.decorateProgram(program);
|
||||
libCliTestStub.decorateProgram(program);
|
||||
program.parse(process.argv);
|
||||
17
tools/e2e/node_modules/playwright-core/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './types/types';
|
||||
17
tools/e2e/node_modules/playwright-core/index.js
generated
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
require('./lib/bootstrap');
|
||||
module.exports = require('./lib/coreBundle').inprocess.playwright;
|
||||
28
tools/e2e/node_modules/playwright-core/index.mjs
generated
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import playwright from './index.js';
|
||||
|
||||
export const chromium = playwright.chromium;
|
||||
export const firefox = playwright.firefox;
|
||||
export const webkit = playwright.webkit;
|
||||
export const selectors = playwright.selectors;
|
||||
export const devices = playwright.devices;
|
||||
export const errors = playwright.errors;
|
||||
export const request = playwright.request;
|
||||
export const _electron = playwright._electron;
|
||||
export const _android = playwright._android;
|
||||
export default playwright;
|
||||
88
tools/e2e/node_modules/playwright-core/lib/bootstrap.js
generated
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
"use strict";
|
||||
const minimumMajorNodeVersion = 20;
|
||||
const currentNodeVersion = process.versions.node;
|
||||
const major = +currentNodeVersion.split(".")[0];
|
||||
if (major < minimumMajorNodeVersion) {
|
||||
console.error(
|
||||
"You are running Node.js " + currentNodeVersion + `.
|
||||
Playwright requires Node.js ${minimumMajorNodeVersion} or higher.
|
||||
Please update your version of Node.js.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
if (process.env.PW_INSTRUMENT_MODULES) {
|
||||
const Module = require("module");
|
||||
const originalLoad = Module._load;
|
||||
const root = { name: "<root>", selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
|
||||
let current = root;
|
||||
const stack = [];
|
||||
Module._load = function(request, _parent, _isMain) {
|
||||
const node = { name: request, selfMs: 0, totalMs: 0, childrenMs: 0, children: [] };
|
||||
current.children.push(node);
|
||||
stack.push(current);
|
||||
current = node;
|
||||
const start = performance.now();
|
||||
let result;
|
||||
try {
|
||||
result = originalLoad.apply(this, arguments);
|
||||
} catch (e) {
|
||||
current = stack.pop();
|
||||
current.children.pop();
|
||||
throw e;
|
||||
}
|
||||
const duration = performance.now() - start;
|
||||
node.totalMs = duration;
|
||||
node.selfMs = Math.max(0, duration - node.childrenMs);
|
||||
current = stack.pop();
|
||||
current.childrenMs += duration;
|
||||
return result;
|
||||
};
|
||||
process.on("exit", () => {
|
||||
function printTree(node, prefix, isLast, lines2, depth) {
|
||||
if (node.totalMs < 1 && depth > 0)
|
||||
return;
|
||||
const connector = depth === 0 ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
||||
const time = `${node.totalMs.toFixed(1).padStart(8)}ms`;
|
||||
const self = node.children.length ? ` (self: ${node.selfMs.toFixed(1)}ms)` : "";
|
||||
lines2.push(`${time} ${prefix}${connector}${node.name}${self}`);
|
||||
const childPrefix = prefix + (depth === 0 ? "" : isLast ? " " : "\u2502 ");
|
||||
const sorted2 = node.children.slice().sort((a, b) => b.totalMs - a.totalMs);
|
||||
for (let i = 0; i < sorted2.length; i++)
|
||||
printTree(sorted2[i], childPrefix, i === sorted2.length - 1, lines2, depth + 1);
|
||||
}
|
||||
let totalModules = 0;
|
||||
function count(n) {
|
||||
totalModules++;
|
||||
n.children.forEach(count);
|
||||
}
|
||||
root.children.forEach(count);
|
||||
const lines = [];
|
||||
const sorted = root.children.slice().sort((a, b) => b.totalMs - a.totalMs);
|
||||
for (let i = 0; i < sorted.length; i++)
|
||||
printTree(sorted[i], "", i === sorted.length - 1, lines, 0);
|
||||
const totalMs = root.children.reduce((s, c) => s + c.totalMs, 0);
|
||||
process.stderr.write(`
|
||||
--- Module load tree: ${totalModules} modules, ${totalMs.toFixed(0)}ms total ---
|
||||
` + lines.join("\n") + "\n");
|
||||
const flat = /* @__PURE__ */ new Map();
|
||||
function gather(n) {
|
||||
const existing = flat.get(n.name);
|
||||
if (existing) {
|
||||
existing.selfMs += n.selfMs;
|
||||
existing.totalMs += n.totalMs;
|
||||
existing.count++;
|
||||
} else {
|
||||
flat.set(n.name, { selfMs: n.selfMs, totalMs: n.totalMs, count: 1 });
|
||||
}
|
||||
n.children.forEach(gather);
|
||||
}
|
||||
root.children.forEach(gather);
|
||||
const top50 = [...flat.entries()].sort((a, b) => b[1].selfMs - a[1].selfMs).slice(0, 50);
|
||||
const flatLines = top50.map(
|
||||
([mod, { selfMs, totalMs: totalMs2, count: count2 }]) => `${selfMs.toFixed(1).padStart(8)}ms self ${totalMs2.toFixed(1).padStart(8)}ms total (x${String(count2).padStart(3)}) ${mod}`
|
||||
);
|
||||
process.stderr.write(`
|
||||
--- Top 50 modules by self time ---
|
||||
` + flatLines.join("\n") + "\n");
|
||||
});
|
||||
}
|
||||
74830
tools/e2e/node_modules/playwright-core/lib/coreBundle.js
generated
vendored
Normal file
5
tools/e2e/node_modules/playwright-core/lib/entry/cliDaemon.js
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"use strict";
|
||||
var import_coreBundle = require("../coreBundle");
|
||||
const { program } = require("../utilsBundle");
|
||||
import_coreBundle.tools.decorateCliDaemonProgram(program);
|
||||
void program.parseAsync();
|
||||
3
tools/e2e/node_modules/playwright-core/lib/entry/dashboardApp.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
var import_coreBundle = require("../coreBundle");
|
||||
import_coreBundle.tools.openDashboardApp();
|
||||
10
tools/e2e/node_modules/playwright-core/lib/entry/mcp.js
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"use strict";
|
||||
var import_coreBundle = require("../coreBundle");
|
||||
var import_package = require("../package");
|
||||
const { program } = require("../utilsBundle");
|
||||
const p = program.version("Version " + import_package.packageJSON.version).name("Playwright MCP");
|
||||
import_coreBundle.tools.decorateMCPCommand(p);
|
||||
program.parseAsync(process.argv).catch((e) => {
|
||||
console.error(e.message);
|
||||
import_coreBundle.utils.gracefullyProcessExitDoNotHang(1);
|
||||
});
|
||||
3
tools/e2e/node_modules/playwright-core/lib/entry/oopBrowserDownload.js
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
var import_coreBundle = require("../coreBundle");
|
||||
import_coreBundle.registry.runOopDownloadBrowserMain();
|
||||
50
tools/e2e/node_modules/playwright-core/lib/package.js
generated
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var package_exports = {};
|
||||
__export(package_exports, {
|
||||
binPath: () => binPath,
|
||||
libPath: () => libPath,
|
||||
packageJSON: () => packageJSON,
|
||||
packageRoot: () => packageRoot
|
||||
});
|
||||
module.exports = __toCommonJS(package_exports);
|
||||
var import_path = __toESM(require("path"));
|
||||
const packageRoot = import_path.default.join(__dirname, "..");
|
||||
const packageJSON = require(import_path.default.join(packageRoot, "package.json"));
|
||||
const binPath = import_path.default.join(packageRoot, "bin");
|
||||
function libPath(...parts) {
|
||||
return import_path.default.join(packageRoot, "lib", ...parts);
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
binPath,
|
||||
libPath,
|
||||
packageJSON,
|
||||
packageRoot
|
||||
});
|
||||
BIN
tools/e2e/node_modules/playwright-core/lib/server/chromium/appIcon.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 16 KiB |
118
tools/e2e/node_modules/playwright-core/lib/server/electron/loader.js
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
"use strict";
|
||||
|
||||
// packages/playwright-core/src/server/electron/loader.ts
|
||||
var import_electron = require("electron");
|
||||
|
||||
// packages/playwright-core/src/server/chromium/chromiumSwitches.ts
|
||||
var disabledFeatures = [
|
||||
// See https://github.com/microsoft/playwright/issues/14047
|
||||
"AvoidUnnecessaryBeforeUnloadCheckSync",
|
||||
// See https://github.com/microsoft/playwright/issues/38568
|
||||
"BoundaryEventDispatchTracksNodeRemoval",
|
||||
"DestroyProfileOnBrowserClose",
|
||||
// See https://github.com/microsoft/playwright/pull/13854
|
||||
"DialMediaRouteProvider",
|
||||
"GlobalMediaControls",
|
||||
// See https://github.com/microsoft/playwright/pull/27605
|
||||
"HttpsUpgrades",
|
||||
// Hides the Lens feature in the URL address bar. Its not working in unofficial builds.
|
||||
"LensOverlay",
|
||||
// See https://github.com/microsoft/playwright/pull/8162
|
||||
"MediaRouter",
|
||||
// See https://github.com/microsoft/playwright/issues/28023
|
||||
"PaintHolding",
|
||||
// See https://github.com/microsoft/playwright/issues/32230
|
||||
"ThirdPartyStoragePartitioning",
|
||||
// Chromium 149 rejects re-applying the `origin` header on a redirect (as request interception
|
||||
// does) with net::ERR_INVALID_ARGUMENT. See https://github.com/microsoft/playwright/issues/41690
|
||||
"BlockOriginHeaderModificationOnRedirect",
|
||||
// See https://github.com/microsoft/playwright/issues/16126
|
||||
"Translate",
|
||||
// See https://issues.chromium.org/u/1/issues/435410220
|
||||
"AutoDeElevate",
|
||||
// Prevents downloading optimization hints on startup.
|
||||
"OptimizationHints",
|
||||
// Disables forced sign-in in Edge.
|
||||
"msForceBrowserSignIn",
|
||||
// Disables updating the preferred version in LaunchServices preferences on mac.
|
||||
"msEdgeUpdateLaunchServicesPreferredVersion"
|
||||
].filter(Boolean);
|
||||
var chromiumSwitches = (options) => [
|
||||
"--disable-field-trial-config",
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:testing/variations/README.md
|
||||
"--disable-background-networking",
|
||||
"--disable-background-timer-throttling",
|
||||
"--disable-backgrounding-occluded-windows",
|
||||
"--disable-back-forward-cache",
|
||||
// Avoids surprises like main request not being intercepted during page.goBack().
|
||||
"--disable-breakpad",
|
||||
"--disable-client-side-phishing-detection",
|
||||
"--disable-component-extensions-with-background-pages",
|
||||
"--disable-component-update",
|
||||
// Avoids unneeded network activity after startup.
|
||||
"--no-default-browser-check",
|
||||
"--disable-default-apps",
|
||||
"--disable-dev-shm-usage",
|
||||
"--disable-edgeupdater",
|
||||
// Disables Edge-specific updater on mac.
|
||||
"--disable-extensions",
|
||||
"--disable-features=" + disabledFeatures.join(","),
|
||||
process.env.PLAYWRIGHT_LEGACY_SCREENSHOT ? "" : "--enable-features=CDPScreenshotNewSurface",
|
||||
"--allow-pre-commit-input",
|
||||
"--disable-hang-monitor",
|
||||
"--disable-ipc-flooding-protection",
|
||||
"--disable-popup-blocking",
|
||||
"--disable-prompt-on-repost",
|
||||
"--disable-renderer-backgrounding",
|
||||
"--disable-updater-scheduler",
|
||||
// Prevents Edge-specific updater from being launched when Edge is launched on mac.
|
||||
"--force-color-profile=srgb",
|
||||
"--metrics-recording-only",
|
||||
"--no-first-run",
|
||||
"--password-store=basic",
|
||||
"--use-mock-keychain",
|
||||
// See https://chromium-review.googlesource.com/c/chromium/src/+/2436773
|
||||
"--no-service-autorun",
|
||||
"--export-tagged-pdf",
|
||||
// https://chromium-review.googlesource.com/c/chromium/src/+/4853540
|
||||
"--disable-search-engine-choice-screen",
|
||||
// https://issues.chromium.org/41491762
|
||||
"--unsafely-disable-devtools-self-xss-warnings",
|
||||
// Edge can potentially restart on Windows (msRelaunchNoCompatLayer) which looses its file descriptors (stdout/stderr) and CDP (3/4). Disable until fixed upstream.
|
||||
"--edge-skip-compat-layer-relaunch",
|
||||
// This disables Chrome for Testing infobar that is visible in the persistent context.
|
||||
// The switch is ignored everywhere else, including Chromium/Chrome/Edge.
|
||||
"--disable-infobars",
|
||||
// Less annoying popups.
|
||||
"--disable-search-engine-choice-screen",
|
||||
// Prevents the "three dots" menu crash in IdentityManager::HasPrimaryAccount for ephemeral contexts.
|
||||
options?.android ? "" : "--disable-sync"
|
||||
].filter(Boolean);
|
||||
|
||||
// packages/playwright-core/src/server/electron/loader.ts
|
||||
process.argv.splice(1, process.argv.indexOf("--remote-debugging-port=0"));
|
||||
for (const arg of chromiumSwitches()) {
|
||||
const match = arg.match(/--([^=]*)=?(.*)/);
|
||||
import_electron.app.commandLine.appendSwitch(match[1], match[2]);
|
||||
}
|
||||
var originalWhenReady = import_electron.app.whenReady();
|
||||
var originalEmit = import_electron.app.emit.bind(import_electron.app);
|
||||
var readyEventArgs;
|
||||
import_electron.app.emit = (event, ...args) => {
|
||||
if (event === "ready") {
|
||||
readyEventArgs = args;
|
||||
return import_electron.app.listenerCount("ready") > 0;
|
||||
}
|
||||
return originalEmit(event, ...args);
|
||||
};
|
||||
var isReady = false;
|
||||
var whenReadyCallback;
|
||||
var whenReadyPromise = new Promise((f) => whenReadyCallback = f);
|
||||
import_electron.app.isReady = () => isReady;
|
||||
import_electron.app.whenReady = () => whenReadyPromise;
|
||||
globalThis.__playwright_run = async () => {
|
||||
const event = await originalWhenReady;
|
||||
isReady = true;
|
||||
whenReadyCallback(event);
|
||||
originalEmit("ready", ...readyEventArgs);
|
||||
};
|
||||
7347
tools/e2e/node_modules/playwright-core/lib/serverRegistry.js
generated
vendored
Normal file
354
tools/e2e/node_modules/playwright-core/lib/serverRegistry.js.LICENSE
generated
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
packages/playwright-core/lib/serverRegistry.js
|
||||
|
||||
THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
|
||||
|
||||
The following npm packages are inlined into this bundle.
|
||||
|
||||
- anymatch@3.1.3 (https://github.com/micromatch/anymatch)
|
||||
- binary-extensions@2.3.0 (https://github.com/sindresorhus/binary-extensions)
|
||||
- braces@3.0.3 (https://github.com/micromatch/braces)
|
||||
- chokidar@3.6.0 (https://github.com/paulmillr/chokidar)
|
||||
- fill-range@7.1.1 (https://github.com/jonschlinkert/fill-range)
|
||||
- glob-parent@5.1.2 (https://github.com/gulpjs/glob-parent)
|
||||
- is-binary-path@2.1.0 (https://github.com/sindresorhus/is-binary-path)
|
||||
- is-extglob@2.1.1 (https://github.com/jonschlinkert/is-extglob)
|
||||
- is-glob@4.0.3 (https://github.com/micromatch/is-glob)
|
||||
- is-number@7.0.0 (https://github.com/jonschlinkert/is-number)
|
||||
- normalize-path@3.0.0 (https://github.com/jonschlinkert/normalize-path)
|
||||
- picomatch@2.3.2 (https://github.com/micromatch/picomatch)
|
||||
- readdirp@3.6.0 (https://github.com/paulmillr/readdirp)
|
||||
- to-regex-range@5.0.1 (https://github.com/micromatch/to-regex-range)
|
||||
|
||||
%% anymatch@3.1.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The ISC License
|
||||
|
||||
Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com)
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
=========================================
|
||||
END OF anymatch@3.1.3 NOTICES AND INFORMATION
|
||||
|
||||
%% binary-extensions@2.3.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
Copyright (c) Paul Miller (https://paulmillr.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=========================================
|
||||
END OF binary-extensions@2.3.0 NOTICES AND INFORMATION
|
||||
|
||||
%% braces@3.0.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-present, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF braces@3.0.3 NOTICES AND INFORMATION
|
||||
|
||||
%% chokidar@3.6.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com), Elan Shanker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF chokidar@3.6.0 NOTICES AND INFORMATION
|
||||
|
||||
%% fill-range@7.1.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-present, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF fill-range@7.1.1 NOTICES AND INFORMATION
|
||||
|
||||
%% glob-parent@5.1.2 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The ISC License
|
||||
|
||||
Copyright (c) 2015, 2019 Elan Shanker
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
=========================================
|
||||
END OF glob-parent@5.1.2 NOTICES AND INFORMATION
|
||||
|
||||
%% is-binary-path@2.1.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com), Paul Miller (https://paulmillr.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
=========================================
|
||||
END OF is-binary-path@2.1.0 NOTICES AND INFORMATION
|
||||
|
||||
%% is-extglob@2.1.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2016, Jon Schlinkert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF is-extglob@2.1.1 NOTICES AND INFORMATION
|
||||
|
||||
%% is-glob@4.0.3 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2017, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF is-glob@4.0.3 NOTICES AND INFORMATION
|
||||
|
||||
%% is-number@7.0.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-present, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF is-number@7.0.0 NOTICES AND INFORMATION
|
||||
|
||||
%% normalize-path@3.0.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2018, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF normalize-path@3.0.0 NOTICES AND INFORMATION
|
||||
|
||||
%% picomatch@2.3.2 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-present, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF picomatch@2.3.2 NOTICES AND INFORMATION
|
||||
|
||||
%% readdirp@3.6.0 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2012-2019 Thorsten Lorenz, Paul Miller (https://paulmillr.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
=========================================
|
||||
END OF readdirp@3.6.0 NOTICES AND INFORMATION
|
||||
|
||||
%% to-regex-range@5.0.1 NOTICES AND INFORMATION BEGIN HERE
|
||||
=========================================
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present, Jon Schlinkert.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
=========================================
|
||||
END OF to-regex-range@5.0.1 NOTICES AND INFORMATION
|
||||
|
||||
SUMMARY
|
||||
=========================================
|
||||
Total Packages: 14
|
||||
=========================================
|
||||
141
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/channelSessions.js
generated
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var channelSessions_exports = {};
|
||||
__export(channelSessions_exports, {
|
||||
isKnownChannel: () => isKnownChannel,
|
||||
listChannelSessions: () => listChannelSessions
|
||||
});
|
||||
module.exports = __toCommonJS(channelSessions_exports);
|
||||
var import_fs = __toESM(require("fs"));
|
||||
var import_net = __toESM(require("net"));
|
||||
var import_os = __toESM(require("os"));
|
||||
var import_path = __toESM(require("path"));
|
||||
var import_extension = require("../utils/extension");
|
||||
function isKnownChannel(name) {
|
||||
return channelToUserDataDir.has(name);
|
||||
}
|
||||
async function listChannelSessions() {
|
||||
if (process.env.PWTEST_CLI_CHANNEL_SCAN_DISABLED_FOR_TEST)
|
||||
return [];
|
||||
const result = [];
|
||||
for (const [channel, dirs] of channelToUserDataDir) {
|
||||
const userDataDir = dirs[process.platform];
|
||||
if (!userDataDir)
|
||||
continue;
|
||||
if (!await pathExists(userDataDir))
|
||||
continue;
|
||||
const [endpoint, extensionInstalled] = await Promise.all([
|
||||
readEndpoint(userDataDir),
|
||||
(0, import_extension.isPlaywrightExtensionInstalled)(userDataDir)
|
||||
]);
|
||||
result.push({ channel, userDataDir, endpoint, extensionInstalled });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async function pathExists(p) {
|
||||
try {
|
||||
await import_fs.default.promises.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function readEndpoint(userDataDir) {
|
||||
let contents;
|
||||
try {
|
||||
contents = await import_fs.default.promises.readFile(import_path.default.join(userDataDir, "DevToolsActivePort"), "utf-8");
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
const port = parseInt(contents.trim().split("\n")[0], 10);
|
||||
if (!Number.isFinite(port))
|
||||
return void 0;
|
||||
if (!await isPortOpen(port))
|
||||
return void 0;
|
||||
return `http://localhost:${port}`;
|
||||
}
|
||||
async function isPortOpen(port) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = import_net.default.createConnection(port, "127.0.0.1");
|
||||
const done = (value) => {
|
||||
socket.destroy();
|
||||
resolve(value);
|
||||
};
|
||||
socket.once("connect", () => done(true));
|
||||
socket.once("error", () => done(false));
|
||||
socket.setTimeout(250, () => done(false));
|
||||
});
|
||||
}
|
||||
const channelToUserDataDir = /* @__PURE__ */ new Map([
|
||||
["chrome", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "google-chrome"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Google", "Chrome"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Google", "Chrome", "User Data")
|
||||
}],
|
||||
["chrome-beta", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "google-chrome-beta"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Google", "Chrome Beta"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Google", "Chrome Beta", "User Data")
|
||||
}],
|
||||
["chrome-dev", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "google-chrome-unstable"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Google", "Chrome Dev"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Google", "Chrome Dev", "User Data")
|
||||
}],
|
||||
["chrome-canary", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "google-chrome-canary"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Google", "Chrome Canary"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Google", "Chrome SxS", "User Data")
|
||||
}],
|
||||
["msedge", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "microsoft-edge"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Microsoft Edge"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Microsoft", "Edge", "User Data")
|
||||
}],
|
||||
["msedge-beta", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "microsoft-edge-beta"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Microsoft Edge Beta"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Microsoft", "Edge Beta", "User Data")
|
||||
}],
|
||||
["msedge-dev", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "microsoft-edge-dev"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Microsoft Edge Dev"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Microsoft", "Edge Dev", "User Data")
|
||||
}],
|
||||
["msedge-canary", {
|
||||
"linux": import_path.default.join(import_os.default.homedir(), ".config", "microsoft-edge-canary"),
|
||||
"darwin": import_path.default.join(import_os.default.homedir(), "Library", "Application Support", "Microsoft Edge Canary"),
|
||||
"win32": import_path.default.join(process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local"), "Microsoft", "Edge SxS", "User Data")
|
||||
}]
|
||||
]);
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
isKnownChannel,
|
||||
listChannelSessions
|
||||
});
|
||||
6
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/cli.js
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
"use strict";
|
||||
var import_program = require("./program");
|
||||
(0, import_program.program)().catch((e) => {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
708
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/help.json
generated
vendored
Normal file
128
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/minimist.js
generated
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var minimist_exports = {};
|
||||
__export(minimist_exports, {
|
||||
minimist: () => minimist
|
||||
});
|
||||
module.exports = __toCommonJS(minimist_exports);
|
||||
function minimist(args, opts) {
|
||||
if (!opts)
|
||||
opts = {};
|
||||
const bools = {};
|
||||
const strings = {};
|
||||
for (const key of toArray(opts.boolean))
|
||||
bools[key] = true;
|
||||
for (const key of toArray(opts.string))
|
||||
strings[key] = true;
|
||||
const argv = { _: [] };
|
||||
function setArg(key, val) {
|
||||
if (argv[key] === void 0 || bools[key] || typeof argv[key] === "boolean")
|
||||
argv[key] = val;
|
||||
else if (Array.isArray(argv[key]))
|
||||
argv[key].push(val);
|
||||
else
|
||||
argv[key] = [argv[key], val];
|
||||
}
|
||||
let notFlags = [];
|
||||
const doubleDashIndex = args.indexOf("--");
|
||||
if (doubleDashIndex !== -1) {
|
||||
notFlags = args.slice(doubleDashIndex + 1);
|
||||
args = args.slice(0, doubleDashIndex);
|
||||
}
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
let key;
|
||||
let next;
|
||||
if (/^--.+=/.test(arg)) {
|
||||
const m = arg.match(/^--([^=]+)=([\s\S]*)$/);
|
||||
key = m[1];
|
||||
if (bools[key])
|
||||
throw new Error(`boolean option '--${key}' should not be passed with '=value', use '--${key}' or '--no-${key}' instead`);
|
||||
setArg(key, m[2]);
|
||||
} else if (/^--no-.+/.test(arg)) {
|
||||
key = arg.match(/^--no-(.+)/)[1];
|
||||
setArg(key, false);
|
||||
} else if (/^--.+/.test(arg)) {
|
||||
key = arg.match(/^--(.+)/)[1];
|
||||
next = args[i + 1];
|
||||
if (next !== void 0 && !/^(-|--)[^-]/.test(next) && !bools[key]) {
|
||||
setArg(key, next);
|
||||
i += 1;
|
||||
} else if (/^(true|false)$/.test(next)) {
|
||||
setArg(key, next === "true");
|
||||
i += 1;
|
||||
} else {
|
||||
setArg(key, strings[key] ? "" : true);
|
||||
}
|
||||
} else if (/^-[^-]+/.test(arg)) {
|
||||
const letters = arg.slice(1, -1).split("");
|
||||
let broken = false;
|
||||
for (let j = 0; j < letters.length; j++) {
|
||||
next = arg.slice(j + 2);
|
||||
if (next === "-") {
|
||||
setArg(letters[j], next);
|
||||
continue;
|
||||
}
|
||||
if (/[A-Za-z]/.test(letters[j]) && next[0] === "=") {
|
||||
setArg(letters[j], next.slice(1));
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
if (/[A-Za-z]/.test(letters[j]) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
|
||||
setArg(letters[j], next);
|
||||
broken = true;
|
||||
break;
|
||||
}
|
||||
if (letters[j + 1] && letters[j + 1].match(/\W/)) {
|
||||
setArg(letters[j], arg.slice(j + 2));
|
||||
broken = true;
|
||||
break;
|
||||
} else {
|
||||
setArg(letters[j], strings[letters[j]] ? "" : true);
|
||||
}
|
||||
}
|
||||
key = arg.slice(-1)[0];
|
||||
if (!broken && key !== "-") {
|
||||
if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) && !bools[key]) {
|
||||
setArg(key, args[i + 1]);
|
||||
i += 1;
|
||||
} else if (args[i + 1] && /^(true|false)$/.test(args[i + 1])) {
|
||||
setArg(key, args[i + 1] === "true");
|
||||
i += 1;
|
||||
} else {
|
||||
setArg(key, strings[key] ? "" : true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
argv._.push(arg);
|
||||
}
|
||||
}
|
||||
for (const k of notFlags)
|
||||
argv._.push(k);
|
||||
return argv;
|
||||
}
|
||||
function toArray(value) {
|
||||
if (!value)
|
||||
return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
minimist
|
||||
});
|
||||
343
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/output.js
generated
vendored
Normal file
@@ -0,0 +1,343 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var output_exports = {};
|
||||
__export(output_exports, {
|
||||
JsonOutput: () => JsonOutput,
|
||||
TextOutput: () => TextOutput
|
||||
});
|
||||
module.exports = __toCommonJS(output_exports);
|
||||
var import_path = __toESM(require("path"));
|
||||
var import_extension = require("../utils/extension");
|
||||
class TextOutput {
|
||||
constructor() {
|
||||
this.json = false;
|
||||
}
|
||||
version(v) {
|
||||
console.log(v);
|
||||
}
|
||||
help(text) {
|
||||
console.log(text);
|
||||
}
|
||||
errorUnknownCommand(name, globalHelp) {
|
||||
console.error(`Unknown command: ${name}
|
||||
`);
|
||||
console.log(globalHelp);
|
||||
return process.exit(1);
|
||||
}
|
||||
errorUnknownOption(opts, commandHelp) {
|
||||
console.error(`Unknown option${opts.length > 1 ? "s" : ""}: ${opts.map((f) => `--${f}`).join(", ")}`);
|
||||
console.log("");
|
||||
console.log(commandHelp);
|
||||
return process.exit(1);
|
||||
}
|
||||
errorTooManyArguments(expected, received, commandHelp) {
|
||||
console.error(`error: too many arguments: expected ${expected}, received ${received}`);
|
||||
console.log("");
|
||||
console.log(commandHelp);
|
||||
return process.exit(1);
|
||||
}
|
||||
errorAttachConflict() {
|
||||
console.error(`Error: only one of [name], --cdp, --endpoint, or --extension can be specified`);
|
||||
return process.exit(1);
|
||||
}
|
||||
errorDetachNotAttached(session) {
|
||||
console.error(`Error: session '${session}' was not attached; use \`playwright-cli${session !== "default" ? ` -s=${session}` : ""} close\` to stop it.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
errorBrowserNotOpenForTool(session) {
|
||||
console.log(`The browser '${session}' is not open, please run open first`);
|
||||
console.log("");
|
||||
console.log(` playwright-cli${session !== "default" ? ` -s=${session}` : ""} open [params]`);
|
||||
return process.exit(1);
|
||||
}
|
||||
errorAttachNoTarget() {
|
||||
console.error(`Error: no target specified for attach command; use one of [name], --cdp, --endpoint, or --extension to specify the target to attach to.`);
|
||||
return process.exit(1);
|
||||
}
|
||||
list({ all, browsers, servers, channelSessions }) {
|
||||
const byWorkspace = /* @__PURE__ */ new Map();
|
||||
for (const browser of browsers) {
|
||||
let list = byWorkspace.get(browser.workspace);
|
||||
if (!list) {
|
||||
list = [];
|
||||
byWorkspace.set(browser.workspace, list);
|
||||
}
|
||||
list.push(browser);
|
||||
}
|
||||
let count = 0;
|
||||
for (const [workspaceKey, list] of byWorkspace) {
|
||||
if (count === 0)
|
||||
console.log("### Browsers");
|
||||
if (all)
|
||||
console.log(`${import_path.default.relative(process.cwd(), workspaceKey) || "/"}:`);
|
||||
for (const browser of list)
|
||||
console.log(renderBrowser(browser));
|
||||
count += list.length;
|
||||
}
|
||||
if (!all) {
|
||||
if (!count)
|
||||
console.log(" (no browsers)");
|
||||
return;
|
||||
}
|
||||
if (servers?.length) {
|
||||
if (count)
|
||||
console.log("");
|
||||
console.log("### Browser servers available for attach");
|
||||
const serversByWorkspace = /* @__PURE__ */ new Map();
|
||||
for (const server of servers) {
|
||||
let list = serversByWorkspace.get(server.workspaceDir ?? "");
|
||||
if (!list) {
|
||||
list = [];
|
||||
serversByWorkspace.set(server.workspaceDir ?? "", list);
|
||||
}
|
||||
list.push(server);
|
||||
}
|
||||
for (const [workspaceKey, list] of serversByWorkspace) {
|
||||
if (workspaceKey)
|
||||
console.log(`${import_path.default.relative(process.cwd(), workspaceKey) || "/"}:`);
|
||||
for (const server of list)
|
||||
console.log(renderServer(server));
|
||||
}
|
||||
count += servers.length;
|
||||
}
|
||||
if (!count)
|
||||
console.log(" (no browsers)");
|
||||
if (channelSessions?.length) {
|
||||
console.log("");
|
||||
console.log("### Browsers available to attach via CDP");
|
||||
for (const session of channelSessions)
|
||||
console.log(renderChannelSession(session));
|
||||
}
|
||||
}
|
||||
closeAll(_sessions) {
|
||||
}
|
||||
deleteData(session, result) {
|
||||
if (!result.existed) {
|
||||
console.log(`No user data found for browser '${session}'.`);
|
||||
return;
|
||||
}
|
||||
if (result.deletedUserDataDir)
|
||||
console.log(`Deleted user data for browser '${session}'.`);
|
||||
}
|
||||
killAll(pids) {
|
||||
for (const pid of pids)
|
||||
console.log(`Killed daemon process ${pid}`);
|
||||
if (pids.length === 0)
|
||||
console.log("No daemon processes found.");
|
||||
else
|
||||
console.log(`Killed ${pids.length} daemon process${pids.length === 1 ? "" : "es"}.`);
|
||||
}
|
||||
open(session, pid, toolResult) {
|
||||
console.log(`### Browser \`${session}\` opened with pid ${pid}.`);
|
||||
if (toolResult)
|
||||
console.log(toolResult);
|
||||
}
|
||||
attach(session, pid, endpoint, toolResult) {
|
||||
if (endpoint) {
|
||||
console.log(`### Session \`${session}\` created, attached to \`${endpoint}\`.`);
|
||||
console.log(`Run commands with: playwright-cli --s=${session} <command>`);
|
||||
console.log("");
|
||||
} else {
|
||||
console.log(`### Browser \`${session}\` opened with pid ${pid}.`);
|
||||
}
|
||||
if (toolResult)
|
||||
console.log(toolResult);
|
||||
}
|
||||
close(session, wasOpen) {
|
||||
if (!wasOpen) {
|
||||
console.log(`Browser '${session}' is not open.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Browser '${session}' closed
|
||||
`);
|
||||
}
|
||||
detach(session, wasAttached) {
|
||||
if (!wasAttached) {
|
||||
console.log(`Browser '${session}' is not attached.`);
|
||||
return;
|
||||
}
|
||||
console.log(`Browser '${session}' detached
|
||||
`);
|
||||
}
|
||||
installed() {
|
||||
}
|
||||
show(_session, pid) {
|
||||
if (process.env.PWTEST_PRINT_DASHBOARD_PID_FOR_TEST)
|
||||
console.log(`### Dashboard opened with pid ${pid}.`);
|
||||
}
|
||||
toolResult(text) {
|
||||
console.log(text);
|
||||
}
|
||||
installStdio() {
|
||||
return "inherit";
|
||||
}
|
||||
}
|
||||
class JsonOutput {
|
||||
constructor() {
|
||||
this.json = true;
|
||||
}
|
||||
version(v) {
|
||||
this._emit({ version: v });
|
||||
}
|
||||
help(text) {
|
||||
this._emit({ help: text });
|
||||
}
|
||||
errorUnknownCommand(name, _globalHelp) {
|
||||
this._emit({ isError: true, error: `Unknown command: ${name}` });
|
||||
return process.exit(1);
|
||||
}
|
||||
errorUnknownOption(opts, _commandHelp) {
|
||||
this._emit({ isError: true, error: `Unknown option${opts.length > 1 ? "s" : ""}: ${opts.map((f) => `--${f}`).join(", ")}` });
|
||||
return process.exit(1);
|
||||
}
|
||||
errorTooManyArguments(expected, received, _commandHelp) {
|
||||
this._emit({ isError: true, error: `error: too many arguments: expected ${expected}, received ${received}` });
|
||||
return process.exit(1);
|
||||
}
|
||||
errorAttachConflict() {
|
||||
this._emit({ isError: true, error: `only one of [name], --cdp, --endpoint, or --extension can be specified` });
|
||||
return process.exit(1);
|
||||
}
|
||||
errorDetachNotAttached(session) {
|
||||
this._emit({ isError: true, error: `session '${session}' was not attached; use close to stop it.` });
|
||||
return process.exit(1);
|
||||
}
|
||||
errorBrowserNotOpenForTool(session) {
|
||||
this._emit({ isError: true, error: `The browser '${session}' is not open, please run open first` });
|
||||
return process.exit(1);
|
||||
}
|
||||
errorAttachNoTarget() {
|
||||
this._emit({ isError: true, error: `no target specified for attach command; use one of [name], --cdp, --endpoint, or --extension to specify the target to attach to.` });
|
||||
return process.exit(1);
|
||||
}
|
||||
list({ all, browsers, servers, channelSessions }) {
|
||||
const payload = { browsers };
|
||||
if (all) {
|
||||
payload.servers = servers ?? [];
|
||||
payload.channelSessions = channelSessions ?? [];
|
||||
}
|
||||
this._emit(payload);
|
||||
}
|
||||
closeAll(sessions) {
|
||||
this._emit({ closed: sessions });
|
||||
}
|
||||
deleteData(session, result) {
|
||||
this._emit({ session, deleted: result.existed });
|
||||
}
|
||||
killAll(pids) {
|
||||
this._emit({ killed: pids.length, pids });
|
||||
}
|
||||
open(session, pid, toolResult) {
|
||||
this._emit({ session, pid, result: parseJsonText(toolResult) });
|
||||
}
|
||||
attach(session, pid, endpoint, toolResult) {
|
||||
this._emit({
|
||||
session,
|
||||
pid,
|
||||
...endpoint ? { endpoint } : {},
|
||||
result: parseJsonText(toolResult)
|
||||
});
|
||||
}
|
||||
close(session, wasOpen) {
|
||||
this._emit({ session, status: wasOpen ? "closed" : "not-open" });
|
||||
}
|
||||
detach(session, wasAttached) {
|
||||
this._emit({ session, status: wasAttached ? "detached" : "not-attached" });
|
||||
}
|
||||
installed() {
|
||||
this._emit({ installed: true });
|
||||
}
|
||||
show(session, pid) {
|
||||
this._emit({ session, pid });
|
||||
}
|
||||
toolResult(text) {
|
||||
console.log(text);
|
||||
}
|
||||
installStdio() {
|
||||
return "ignore";
|
||||
}
|
||||
_emit(value) {
|
||||
console.log(JSON.stringify(value, null, 2));
|
||||
}
|
||||
}
|
||||
function parseJsonText(text) {
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
function renderBrowser(browser) {
|
||||
const lines = [`- ${browser.name}:`];
|
||||
lines.push(` - status: ${browser.status}`);
|
||||
if (browser.status === "open" && !browser.compatible)
|
||||
lines.push(` - version: v${browser.version} [incompatible please re-open]`);
|
||||
if (browser.browserType)
|
||||
lines.push(` - browser-type: ${browser.browserType}${browser.attached ? " (attached)" : ""}`);
|
||||
if (!browser.attached) {
|
||||
if (browser.userDataDir === null)
|
||||
lines.push(` - user-data-dir: <in-memory>`);
|
||||
else
|
||||
lines.push(` - user-data-dir: ${browser.userDataDir}`);
|
||||
if (browser.headed !== void 0)
|
||||
lines.push(` - headed: ${browser.headed}`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
function renderServer(server) {
|
||||
const lines = [`- browser "${server.title}":`];
|
||||
lines.push(` - browser: ${server.browser.browserName}`);
|
||||
lines.push(` - version: v${server.playwrightVersion}`);
|
||||
if (server.browser.userDataDir)
|
||||
lines.push(` - data-dir: ${server.browser.userDataDir}`);
|
||||
else
|
||||
lines.push(` - data-dir: <in-memory>`);
|
||||
lines.push(` - run \`playwright-cli attach "${server.title}"\` to attach`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
function renderChannelSession(session) {
|
||||
const lines = [`- ${session.channel}:`];
|
||||
lines.push(` - data-dir: ${session.userDataDir}`);
|
||||
if (session.extensionInstalled)
|
||||
lines.push(` - attach (extension): \`playwright-cli attach --extension=${session.channel}\``);
|
||||
else
|
||||
lines.push(` - attach (extension): install at ${import_extension.playwrightExtensionInstallUrl}`);
|
||||
if (session.endpoint) {
|
||||
lines.push(` - attach (remote debugging): \`playwright-cli attach --cdp=${session.channel}\``);
|
||||
} else {
|
||||
const inspectScheme = session.channel.startsWith("msedge") ? "edge" : "chrome";
|
||||
lines.push(` - attach (remote debugging): enable at ${inspectScheme}://inspect/#remote-debugging`);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
JsonOutput,
|
||||
TextOutput
|
||||
});
|
||||
404
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/program.js
generated
vendored
Normal file
@@ -0,0 +1,404 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var program_exports = {};
|
||||
__export(program_exports, {
|
||||
calculateSha1: () => calculateSha1,
|
||||
program: () => program
|
||||
});
|
||||
module.exports = __toCommonJS(program_exports);
|
||||
var import_child_process = require("child_process");
|
||||
var import_crypto = __toESM(require("crypto"));
|
||||
var import_os = __toESM(require("os"));
|
||||
var import_path = __toESM(require("path"));
|
||||
var import_channelSessions = require("./channelSessions");
|
||||
var import_output = require("./output");
|
||||
var import_registry = require("./registry");
|
||||
var import_session = require("./session");
|
||||
var import_package = require("../../package");
|
||||
var import_serverRegistry = require("../../serverRegistry");
|
||||
var import_minimist = require("./minimist");
|
||||
const globalOptions = [
|
||||
"json",
|
||||
"raw",
|
||||
"session"
|
||||
];
|
||||
const booleanOptions = [
|
||||
"all",
|
||||
"help",
|
||||
"json",
|
||||
"raw",
|
||||
"version"
|
||||
];
|
||||
async function program(options) {
|
||||
const clientInfo = (0, import_registry.createClientInfo)();
|
||||
const help = require((0, import_package.libPath)("tools", "cli-client", "help.json"));
|
||||
const argv = process.argv.slice(2);
|
||||
const boolean = [...help.booleanOptions, ...booleanOptions];
|
||||
const args = (0, import_minimist.minimist)(argv, { boolean, string: ["_"] });
|
||||
if (args.s) {
|
||||
args.session = args.s;
|
||||
delete args.s;
|
||||
}
|
||||
const output = args.json ? new import_output.JsonOutput() : new import_output.TextOutput();
|
||||
const commandName = args._?.[0];
|
||||
if (args.version || args.v) {
|
||||
output.version(options?.embedderVersion ?? clientInfo.version);
|
||||
process.exit(0);
|
||||
}
|
||||
const command = commandName && help.commands[commandName];
|
||||
if (args.help || args.h || !commandName) {
|
||||
if (command) {
|
||||
output.help(command.help);
|
||||
} else {
|
||||
const lines = ["playwright-cli - run playwright mcp commands from terminal"];
|
||||
if (process.env.CLAUDECODE || process.env.COPILOT_CLI)
|
||||
lines.push(`Agent skill: ${import_path.default.relative(process.cwd(), (0, import_package.libPath)("tools", "skills", "playwright-cli", "SKILL.md"))}`);
|
||||
lines.push(help.global);
|
||||
output.help(lines.join("\n\n"));
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
if (!command)
|
||||
output.errorUnknownCommand(commandName, help.global);
|
||||
validateFlags(args, command, output);
|
||||
validateArgs(args, command, output);
|
||||
const registry = await import_registry.Registry.load();
|
||||
const sessionName = (0, import_registry.resolveSessionName)(args.session);
|
||||
switch (commandName) {
|
||||
case "list": {
|
||||
const data = await collectList(registry, clientInfo, !!args.all);
|
||||
output.list(data);
|
||||
return;
|
||||
}
|
||||
case "close-all": {
|
||||
const entries = registry.entries(clientInfo);
|
||||
const closed = [];
|
||||
for (const entry of entries) {
|
||||
await new import_session.Session(entry).stop();
|
||||
closed.push(entry.config.name);
|
||||
}
|
||||
output.closeAll(closed);
|
||||
return;
|
||||
}
|
||||
case "delete-data": {
|
||||
const entry = registry.entry(clientInfo, sessionName);
|
||||
if (!entry) {
|
||||
output.deleteData(sessionName, { existed: false, deletedUserDataDir: false });
|
||||
return;
|
||||
}
|
||||
const result = await new import_session.Session(entry).deleteData();
|
||||
output.deleteData(sessionName, result);
|
||||
return;
|
||||
}
|
||||
case "kill-all": {
|
||||
const pids = await killAllDaemons();
|
||||
output.killAll(pids);
|
||||
return;
|
||||
}
|
||||
case "open": {
|
||||
const { pid } = await startSession(sessionName, registry, clientInfo, args, "open");
|
||||
const newEntry = await registry.loadEntry(clientInfo, sessionName);
|
||||
const params = args._.slice(1);
|
||||
const toolText = await runInSessionOrStop(newEntry, clientInfo, { _: ["goto", ...params.length ? params : ["about:blank"]] }, output);
|
||||
output.open(sessionName, pid, toolText);
|
||||
return;
|
||||
}
|
||||
case "attach": {
|
||||
const attachTarget = args._[1];
|
||||
const targetCount = (attachTarget ? 1 : 0) + (args.cdp ? 1 : 0) + (args.endpoint ? 1 : 0) + (args.extension ? 1 : 0);
|
||||
if (targetCount > 1)
|
||||
output.errorAttachConflict();
|
||||
if (attachTarget)
|
||||
args.endpoint = attachTarget;
|
||||
const extensionChannel = typeof args.extension === "string" ? args.extension : void 0;
|
||||
if (extensionChannel) {
|
||||
args.browser = extensionChannel;
|
||||
args.extension = true;
|
||||
}
|
||||
const cdpChannel = typeof args.cdp === "string" && (0, import_channelSessions.isKnownChannel)(args.cdp) ? args.cdp : void 0;
|
||||
const targetName = attachTarget ?? cdpChannel ?? extensionChannel ?? args.endpoint ?? args.cdp;
|
||||
if (!targetName)
|
||||
output.errorAttachNoTarget();
|
||||
const attachSessionName = (0, import_registry.explicitSessionName)(args.session) ?? attachTarget ?? cdpChannel ?? extensionChannel ?? sessionName;
|
||||
args.session = attachSessionName;
|
||||
const { pid } = await startSession(attachSessionName, registry, clientInfo, args, "attach");
|
||||
const newEntry = await registry.loadEntry(clientInfo, attachSessionName);
|
||||
const toolText = await runInSessionOrStop(newEntry, clientInfo, { _: ["snapshot"], filename: "<auto>" }, output);
|
||||
output.attach(attachSessionName, pid, targetName, toolText);
|
||||
return;
|
||||
}
|
||||
case "close": {
|
||||
const closeEntry = registry.entry(clientInfo, sessionName);
|
||||
const { wasOpen } = closeEntry ? await new import_session.Session(closeEntry).stop() : { wasOpen: false };
|
||||
output.close(sessionName, wasOpen);
|
||||
return;
|
||||
}
|
||||
case "detach": {
|
||||
const detachEntry = registry.entry(clientInfo, sessionName);
|
||||
if (detachEntry && !detachEntry.config.attached)
|
||||
output.errorDetachNotAttached(sessionName);
|
||||
const { wasOpen } = detachEntry ? await new import_session.Session(detachEntry).stop() : { wasOpen: false };
|
||||
output.detach(sessionName, wasOpen);
|
||||
return;
|
||||
}
|
||||
case "install":
|
||||
await runInitWorkspace(args, output);
|
||||
output.installed();
|
||||
return;
|
||||
case "install-browser":
|
||||
await installBrowser();
|
||||
output.installed();
|
||||
return;
|
||||
case "show": {
|
||||
const daemonScript = (0, import_package.libPath)("entry", "dashboardApp.js");
|
||||
const daemonArgs = [
|
||||
daemonScript,
|
||||
`--workspaceDir=${clientInfo.workspaceDir ?? ""}`
|
||||
];
|
||||
const explicit = (0, import_registry.explicitSessionName)(args.session);
|
||||
if (explicit)
|
||||
daemonArgs.push(`--sessionName=${explicit}`);
|
||||
if (args.port !== void 0)
|
||||
daemonArgs.push(`--port=${args.port}`);
|
||||
if (args.host !== void 0)
|
||||
daemonArgs.push(`--host=${args.host}`);
|
||||
if (args.kill) {
|
||||
daemonArgs.push(`--kill`);
|
||||
const child2 = (0, import_child_process.spawn)(process.execPath, daemonArgs, { stdio: "ignore" });
|
||||
await new Promise((resolve) => child2.on("exit", () => resolve()));
|
||||
return;
|
||||
}
|
||||
if (args.annotate) {
|
||||
const entry = registry.entry(clientInfo, sessionName);
|
||||
if (!entry)
|
||||
output.errorBrowserNotOpenForTool(sessionName);
|
||||
args.raw = true;
|
||||
const text = await runInSession(entry, clientInfo, args, output);
|
||||
output.toolResult(text);
|
||||
return;
|
||||
}
|
||||
const foreground = args.port !== void 0;
|
||||
const child = (0, import_child_process.spawn)(process.execPath, daemonArgs, {
|
||||
detached: !foreground,
|
||||
stdio: foreground ? "inherit" : ["pipe", "pipe", "ignore"]
|
||||
});
|
||||
if (foreground) {
|
||||
await new Promise((resolve) => child.on("exit", () => resolve()));
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => child.stdin.destroy(), 6e4);
|
||||
child.unref();
|
||||
let daemonPid;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
let outLog = "";
|
||||
child.stdout.on("data", (data) => {
|
||||
outLog += data.toString();
|
||||
const match = outLog.match(/Dashboard is running pid=(\d+)/);
|
||||
if (match) {
|
||||
daemonPid = Number(match[1]);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
child.once("exit", (code, signal) => reject(new Error(`Dashboard daemon exited (code=${code}, signal=${signal}) before signaling READY${outLog ? "\n" + outLog : ""}`)));
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
child.removeAllListeners("exit");
|
||||
child.stdin.destroy();
|
||||
child.stdout.destroy();
|
||||
}
|
||||
output.show(sessionName, daemonPid);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
const entry = registry.entry(clientInfo, sessionName);
|
||||
if (!entry)
|
||||
output.errorBrowserNotOpenForTool(sessionName);
|
||||
if (command.raw)
|
||||
args.raw = true;
|
||||
const text = await runInSession(entry, clientInfo, args, output);
|
||||
output.toolResult(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function startSession(sessionName, registry, clientInfo, args, mode) {
|
||||
const entry = registry.entry(clientInfo, sessionName);
|
||||
if (entry)
|
||||
await new import_session.Session(entry).stop();
|
||||
return await import_session.Session.startDaemon(clientInfo, args, mode);
|
||||
}
|
||||
async function runInSession(entry, clientInfo, args, output) {
|
||||
const raw = !!args.raw;
|
||||
for (const globalOption of globalOptions)
|
||||
delete args[globalOption];
|
||||
const session = new import_session.Session(entry);
|
||||
const result = await session.run(clientInfo, args, { raw, json: output.json });
|
||||
return result.text;
|
||||
}
|
||||
async function runInSessionOrStop(entry, clientInfo, args, output) {
|
||||
try {
|
||||
return await runInSession(entry, clientInfo, args, output);
|
||||
} catch (e) {
|
||||
await new import_session.Session(entry).stop().catch(() => {
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
async function runInitWorkspace(args, output) {
|
||||
const cliPath = (0, import_package.libPath)("entry", "cliDaemon.js");
|
||||
const daemonArgs = [cliPath, "--init-workspace", ...args.skills ? ["--init-skills", String(args.skills)] : []];
|
||||
await new Promise((resolve, reject) => {
|
||||
const child = (0, import_child_process.spawn)(process.execPath, daemonArgs, {
|
||||
stdio: output.installStdio(),
|
||||
cwd: process.cwd()
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (code === 0)
|
||||
resolve();
|
||||
else
|
||||
reject(new Error(`Workspace initialization failed with exit code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
async function installBrowser() {
|
||||
const argv = process.argv.map((arg) => arg === "install-browser" ? "install" : arg);
|
||||
const { libCli } = require("../../coreBundle.js");
|
||||
const { program: program2 } = require("../../utilsBundle.js");
|
||||
if (!program2.version())
|
||||
libCli.decorateProgram(program2);
|
||||
program2.parse(argv);
|
||||
}
|
||||
const daemonProcessPatterns = ["run-mcp-server", "run-cli-server", "cli-daemon", "cliDaemon.js", "dashboardApp.js"];
|
||||
async function killAllDaemons() {
|
||||
const platform = import_os.default.platform();
|
||||
const pidFilterEnv = process.env.PWTEST_KILL_ALL_PID_FILTER_FOR_TEST;
|
||||
const pidFilter = pidFilterEnv ? new Set(pidFilterEnv.split(",").map((p) => parseInt(p, 10)).filter((n) => !isNaN(n))) : void 0;
|
||||
const killed = [];
|
||||
try {
|
||||
if (platform === "win32") {
|
||||
const clauses = [`(${daemonProcessPatterns.map((p) => `$_.CommandLine -like '*${p}*'`).join(" -or ")})`];
|
||||
if (pidFilter)
|
||||
clauses.push(`(${[...pidFilter].map((p) => `$_.ProcessId -eq ${p}`).join(" -or ")})`);
|
||||
const whereClause = clauses.join(" -and ");
|
||||
const result = (0, import_child_process.execSync)(
|
||||
`powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { ${whereClause} } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue; $_.ProcessId }"`,
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
const pids = result.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/.test(line));
|
||||
for (const pid of pids)
|
||||
killed.push(parseInt(pid, 10));
|
||||
} else {
|
||||
const result = (0, import_child_process.execSync)("ps auxww", { encoding: "utf-8" });
|
||||
const lines = result.split("\n");
|
||||
for (const line of lines) {
|
||||
if (daemonProcessPatterns.some((p) => line.includes(p))) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
const pid = parts[1];
|
||||
if (pid && /^\d+$/.test(pid)) {
|
||||
const numericPid = parseInt(pid, 10);
|
||||
if (pidFilter && !pidFilter.has(numericPid))
|
||||
continue;
|
||||
try {
|
||||
process.kill(numericPid, "SIGKILL");
|
||||
killed.push(numericPid);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
async function collectList(registry, clientInfo, all) {
|
||||
const browsers = [];
|
||||
const entries = registry.entryMap();
|
||||
const serverEntries = await import_serverRegistry.serverRegistry.list();
|
||||
const key = (0, import_registry.clientKey)(clientInfo);
|
||||
for (const [workspaceKey, list] of entries) {
|
||||
if (!all && workspaceKey !== key)
|
||||
continue;
|
||||
for (const entry of list) {
|
||||
const session = new import_session.Session(entry);
|
||||
const canConnect = await session.canConnect();
|
||||
if (!canConnect) {
|
||||
await session.deleteSessionConfig();
|
||||
continue;
|
||||
}
|
||||
const config = session.config;
|
||||
const channel = config.browser?.launchOptions.channel ?? config.browser?.browserName;
|
||||
browsers.push({
|
||||
name: session.name,
|
||||
workspace: workspaceKey,
|
||||
status: canConnect ? "open" : "closed",
|
||||
browserType: channel,
|
||||
userDataDir: config.browser?.userDataDir ?? null,
|
||||
headed: config.browser ? !config.browser.launchOptions.headless : void 0,
|
||||
persistent: !!config.cli.persistent,
|
||||
attached: !!config.attached,
|
||||
compatible: session.isCompatible(clientInfo),
|
||||
version: config.version
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!all)
|
||||
return { all, browsers };
|
||||
const servers = [...serverEntries.values()].flat();
|
||||
return { all, browsers, servers, channelSessions: await (0, import_channelSessions.listChannelSessions)() };
|
||||
}
|
||||
function validateFlags(args, command, output) {
|
||||
const unknownFlags = [];
|
||||
for (const key of Object.keys(args)) {
|
||||
if (key === "_")
|
||||
continue;
|
||||
if (globalOptions.includes(key))
|
||||
continue;
|
||||
if (!(key in command.flags))
|
||||
unknownFlags.push(key);
|
||||
}
|
||||
if (unknownFlags.length)
|
||||
output.errorUnknownOption(unknownFlags, command.help);
|
||||
}
|
||||
function validateArgs(args, command, output) {
|
||||
const positional = args._.slice(1);
|
||||
if (positional.length > command.args.length)
|
||||
output.errorTooManyArguments(command.args.length, positional.length, command.help);
|
||||
}
|
||||
function calculateSha1(buffer) {
|
||||
const hash = import_crypto.default.createHash("sha1");
|
||||
hash.update(buffer);
|
||||
return hash.digest("hex");
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
calculateSha1,
|
||||
program
|
||||
});
|
||||
176
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/registry.js
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var registry_exports = {};
|
||||
__export(registry_exports, {
|
||||
Registry: () => Registry,
|
||||
baseDaemonDir: () => baseDaemonDir,
|
||||
clientKey: () => clientKey,
|
||||
createClientInfo: () => createClientInfo,
|
||||
explicitSessionName: () => explicitSessionName,
|
||||
resolveSessionName: () => resolveSessionName
|
||||
});
|
||||
module.exports = __toCommonJS(registry_exports);
|
||||
var import_crypto = __toESM(require("crypto"));
|
||||
var import_fs = __toESM(require("fs"));
|
||||
var import_os = __toESM(require("os"));
|
||||
var import_path = __toESM(require("path"));
|
||||
var import_package = require("../../package");
|
||||
function clientKey(clientInfo) {
|
||||
return clientInfo.workspaceDir || clientInfo.workspaceDirHash;
|
||||
}
|
||||
class Registry {
|
||||
constructor(files) {
|
||||
this._files = files;
|
||||
}
|
||||
entry(clientInfo, sessionName) {
|
||||
const key = clientKey(clientInfo);
|
||||
const entries = this._files.get(key) || [];
|
||||
return entries.find((entry) => entry.config.name === sessionName);
|
||||
}
|
||||
entries(clientInfo) {
|
||||
return this._files.get(clientKey(clientInfo)) || [];
|
||||
}
|
||||
entryMap() {
|
||||
return this._files;
|
||||
}
|
||||
async loadEntry(clientInfo, sessionName) {
|
||||
const entry = await Registry._loadSessionEntry(clientInfo.daemonProfilesDir, sessionName + ".session");
|
||||
if (!entry)
|
||||
throw new Error(`Could not start the session "${sessionName}"`);
|
||||
const key = clientKey(clientInfo);
|
||||
let list = this._files.get(key);
|
||||
if (!list) {
|
||||
list = [];
|
||||
this._files.set(key, list);
|
||||
}
|
||||
const oldIndex = list.findIndex((e) => e.config.name === sessionName);
|
||||
if (oldIndex !== -1)
|
||||
list.splice(oldIndex, 1);
|
||||
list.push(entry);
|
||||
return entry;
|
||||
}
|
||||
static async _loadSessionEntry(daemonDir, file) {
|
||||
try {
|
||||
const fileName = import_path.default.join(daemonDir, file);
|
||||
const data = await import_fs.default.promises.readFile(fileName, "utf-8");
|
||||
const config = JSON.parse(data);
|
||||
if (!config.name)
|
||||
config.name = import_path.default.basename(file, ".session");
|
||||
if (!config.timestamp)
|
||||
config.timestamp = 0;
|
||||
return { file: fileName, config, daemonDir };
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
static async load() {
|
||||
const sessions = /* @__PURE__ */ new Map();
|
||||
const hashDirs = await import_fs.default.promises.readdir(baseDaemonDir).catch(() => []);
|
||||
for (const workspaceDirHash of hashDirs) {
|
||||
const daemonDir = import_path.default.join(baseDaemonDir, workspaceDirHash);
|
||||
const stat = await import_fs.default.promises.stat(daemonDir);
|
||||
if (!stat.isDirectory())
|
||||
continue;
|
||||
const files = await import_fs.default.promises.readdir(daemonDir).catch(() => []);
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".session"))
|
||||
continue;
|
||||
const entry = await Registry._loadSessionEntry(daemonDir, file);
|
||||
if (!entry)
|
||||
continue;
|
||||
const key = entry.config.workspaceDir || workspaceDirHash;
|
||||
let list = sessions.get(key);
|
||||
if (!list) {
|
||||
list = [];
|
||||
sessions.set(key, list);
|
||||
}
|
||||
list.push(entry);
|
||||
}
|
||||
}
|
||||
return new Registry(sessions);
|
||||
}
|
||||
}
|
||||
const baseDaemonDir = (() => {
|
||||
if (process.env.PWTEST_DAEMON_SESSION_DIR)
|
||||
return process.env.PWTEST_DAEMON_SESSION_DIR;
|
||||
let localCacheDir;
|
||||
if (process.platform === "linux")
|
||||
localCacheDir = process.env.XDG_CACHE_HOME || import_path.default.join(import_os.default.homedir(), ".cache");
|
||||
if (process.platform === "darwin")
|
||||
localCacheDir = import_path.default.join(import_os.default.homedir(), "Library", "Caches");
|
||||
if (process.platform === "win32")
|
||||
localCacheDir = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
|
||||
if (!localCacheDir)
|
||||
throw new Error("Unsupported platform: " + process.platform);
|
||||
return import_path.default.join(localCacheDir, "ms-playwright", "daemon");
|
||||
})();
|
||||
function createClientInfo() {
|
||||
const workspaceDir = findWorkspaceDir(process.cwd());
|
||||
const version = process.env.PLAYWRIGHT_CLI_VERSION_FOR_TEST || import_package.packageJSON.version;
|
||||
const hash = import_crypto.default.createHash("sha1");
|
||||
hash.update(workspaceDir || import_package.packageRoot);
|
||||
const workspaceDirHash = hash.digest("hex").substring(0, 16);
|
||||
return {
|
||||
version,
|
||||
workspaceDir,
|
||||
workspaceDirHash,
|
||||
daemonProfilesDir: daemonProfilesDir(workspaceDirHash),
|
||||
homeDir: import_os.default.homedir()
|
||||
};
|
||||
}
|
||||
function findWorkspaceDir(startDir) {
|
||||
let dir = startDir;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (import_fs.default.existsSync(import_path.default.join(dir, ".playwright")))
|
||||
return dir;
|
||||
const parentDir = import_path.default.dirname(dir);
|
||||
if (parentDir === dir)
|
||||
break;
|
||||
dir = parentDir;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
const daemonProfilesDir = (workspaceDirHash) => {
|
||||
return import_path.default.join(baseDaemonDir, workspaceDirHash);
|
||||
};
|
||||
function explicitSessionName(sessionName) {
|
||||
return sessionName || process.env.PLAYWRIGHT_CLI_SESSION;
|
||||
}
|
||||
function resolveSessionName(sessionName) {
|
||||
return explicitSessionName(sessionName) || "default";
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Registry,
|
||||
baseDaemonDir,
|
||||
clientKey,
|
||||
createClientInfo,
|
||||
explicitSessionName,
|
||||
resolveSessionName
|
||||
});
|
||||
258
tools/e2e/node_modules/playwright-core/lib/tools/cli-client/session.js
generated
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var session_exports = {};
|
||||
__export(session_exports, {
|
||||
Session: () => Session
|
||||
});
|
||||
module.exports = __toCommonJS(session_exports);
|
||||
var import_child_process = require("child_process");
|
||||
var import_fs = __toESM(require("fs"));
|
||||
var import_net = __toESM(require("net"));
|
||||
var import_os = __toESM(require("os"));
|
||||
var import_path = __toESM(require("path"));
|
||||
var import_package = require("../../package");
|
||||
var import_socketConnection = require("../utils/socketConnection");
|
||||
var import_registry = require("./registry");
|
||||
class Session {
|
||||
constructor(sessionFile) {
|
||||
this.config = sessionFile.config;
|
||||
this.name = this.config.name;
|
||||
this._sessionFile = sessionFile;
|
||||
}
|
||||
isCompatible(clientInfo) {
|
||||
return (0, import_socketConnection.compareSemver)(clientInfo.version, this.config.version) >= 0;
|
||||
}
|
||||
async run(clientInfo, args, options) {
|
||||
if (!this.isCompatible(clientInfo))
|
||||
throw new Error(`Client is v${clientInfo.version}, session '${this.name}' is v${this.config.version}. Run
|
||||
|
||||
playwright-cli${this.name !== "default" ? ` -s=${this.name}` : ""} open
|
||||
|
||||
to restart the browser session.`);
|
||||
const { socket } = await this._connect();
|
||||
if (!socket)
|
||||
throw new Error(`Browser '${this.name}' is not open. Run
|
||||
|
||||
playwright-cli${this.name !== "default" ? ` -s=${this.name}` : ""} open
|
||||
|
||||
to start the browser session.`);
|
||||
return await SocketConnectionClient.sendAndClose(socket, "run", { args, cwd: process.cwd(), raw: options?.raw, json: options?.json });
|
||||
}
|
||||
async stop() {
|
||||
if (!await this.canConnect())
|
||||
return { wasOpen: false };
|
||||
await this._stopDaemon();
|
||||
return { wasOpen: true };
|
||||
}
|
||||
async deleteData() {
|
||||
await this.stop();
|
||||
const dataDirs = await import_fs.default.promises.readdir(this._sessionFile.daemonDir).catch(() => []);
|
||||
const matchingEntries = dataDirs.filter((file) => file === `${this.name}.session` || file.startsWith(`ud-${this.name}-`));
|
||||
if (matchingEntries.length === 0)
|
||||
return { existed: false, deletedUserDataDir: false };
|
||||
let deletedUserDataDir = false;
|
||||
for (const entry of matchingEntries) {
|
||||
const userDataDir = import_path.default.resolve(this._sessionFile.daemonDir, entry);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
try {
|
||||
await import_fs.default.promises.rm(userDataDir, { recursive: true });
|
||||
if (entry.startsWith("ud-"))
|
||||
deletedUserDataDir = true;
|
||||
break;
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT")
|
||||
break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
||||
if (i === 4)
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { existed: true, deletedUserDataDir };
|
||||
}
|
||||
async _connect() {
|
||||
return await new Promise((resolve) => {
|
||||
const socket = import_net.default.createConnection(this.config.socketPath, () => {
|
||||
resolve({ socket });
|
||||
});
|
||||
socket.on("error", (error) => {
|
||||
if (import_os.default.platform() !== "win32")
|
||||
void import_fs.default.promises.unlink(this.config.socketPath).catch(() => {
|
||||
}).then(() => resolve({ error }));
|
||||
else
|
||||
resolve({ error });
|
||||
});
|
||||
});
|
||||
}
|
||||
async canConnect() {
|
||||
const { socket } = await this._connect();
|
||||
if (socket) {
|
||||
socket.destroy();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static async startDaemon(clientInfo, cliArgs, mode) {
|
||||
await import_fs.default.promises.mkdir(clientInfo.daemonProfilesDir, { recursive: true });
|
||||
const cliPath = (0, import_package.libPath)("entry", "cliDaemon.js");
|
||||
const sessionName = (0, import_registry.resolveSessionName)(cliArgs.session);
|
||||
const errLog = import_path.default.join(clientInfo.daemonProfilesDir, sessionName + ".err");
|
||||
const err = import_fs.default.openSync(errLog, "w");
|
||||
const args = [
|
||||
cliPath,
|
||||
sessionName
|
||||
];
|
||||
if (cliArgs.headed)
|
||||
args.push("--headed");
|
||||
if (cliArgs.mobile)
|
||||
args.push("--mobile");
|
||||
if (cliArgs.device)
|
||||
args.push(`--device=${cliArgs.device}`);
|
||||
if (cliArgs.browser)
|
||||
args.push(`--browser=${cliArgs.browser}`);
|
||||
if (cliArgs.persistent)
|
||||
args.push("--persistent");
|
||||
if (cliArgs.profile)
|
||||
args.push(`--profile=${cliArgs.profile}`);
|
||||
if (cliArgs.config)
|
||||
args.push(`--config=${cliArgs.config}`);
|
||||
if (cliArgs.extension)
|
||||
args.push("--extension");
|
||||
else if (cliArgs.cdp)
|
||||
args.push(`--cdp=${cliArgs.cdp}`);
|
||||
else if (cliArgs.endpoint)
|
||||
args.push(`--endpoint=${cliArgs.endpoint}`);
|
||||
const child = (0, import_child_process.spawn)(process.execPath, args, {
|
||||
detached: true,
|
||||
stdio: ["ignore", "pipe", err],
|
||||
cwd: process.cwd()
|
||||
// Will be used as root.
|
||||
});
|
||||
let signalled = false;
|
||||
const sigintHandler = () => {
|
||||
signalled = true;
|
||||
child.kill("SIGINT");
|
||||
};
|
||||
const sigtermHandler = () => {
|
||||
signalled = true;
|
||||
child.kill("SIGTERM");
|
||||
};
|
||||
process.on("SIGINT", sigintHandler);
|
||||
process.on("SIGTERM", sigtermHandler);
|
||||
let outLog = "";
|
||||
const rejectWithPid = (reject, message) => reject(Object.assign(new Error(`Daemon pid=${child.pid}: ${message}`), { daemonPid: child.pid }));
|
||||
await new Promise((resolve, reject) => {
|
||||
child.stdout.on("data", (data) => {
|
||||
outLog += data.toString();
|
||||
if (outLog.includes("Daemon listening on"))
|
||||
resolve();
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
if (!signalled) {
|
||||
const errLogContent = import_fs.default.readFileSync(errLog, "utf-8");
|
||||
rejectWithPid(reject, `Daemon process exited with code ${code}` + (outLog ? "\n" + outLog : "") + (errLogContent ? "\n" + errLogContent : ""));
|
||||
}
|
||||
});
|
||||
});
|
||||
process.off("SIGINT", sigintHandler);
|
||||
process.off("SIGTERM", sigtermHandler);
|
||||
child.stdout.destroy();
|
||||
child.unref();
|
||||
return { pid: child.pid, sessionName, endpoint: cliArgs.endpoint };
|
||||
}
|
||||
async _stopDaemon() {
|
||||
const { socket } = await this._connect();
|
||||
if (!socket)
|
||||
return;
|
||||
let error;
|
||||
await SocketConnectionClient.sendAndClose(socket, "stop", {}).catch((e) => error = e);
|
||||
if (error && !error?.message?.includes("Session closed"))
|
||||
throw error;
|
||||
}
|
||||
async deleteSessionConfig() {
|
||||
await import_fs.default.promises.rm(this._sessionFile.file).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
class SocketConnectionClient {
|
||||
constructor(socket) {
|
||||
this._nextMessageId = 1;
|
||||
this._callbacks = /* @__PURE__ */ new Map();
|
||||
this._connection = new import_socketConnection.SocketConnection(socket);
|
||||
this._connection.onmessage = (message) => this._onMessage(message);
|
||||
this._connection.onclose = () => this._rejectCallbacks();
|
||||
}
|
||||
async send(method, params = {}) {
|
||||
const messageId = this._nextMessageId++;
|
||||
const message = {
|
||||
id: messageId,
|
||||
method,
|
||||
params
|
||||
};
|
||||
const responsePromise = new Promise((resolve, reject) => {
|
||||
this._callbacks.set(messageId, { resolve, reject, method, params });
|
||||
});
|
||||
const [result] = await Promise.all([responsePromise, this._connection.send(message)]);
|
||||
return result;
|
||||
}
|
||||
static async sendAndClose(socket, method, params = {}) {
|
||||
const connection = new SocketConnectionClient(socket);
|
||||
try {
|
||||
return await connection.send(method, params);
|
||||
} finally {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
close() {
|
||||
this._connection.close();
|
||||
}
|
||||
_onMessage(object) {
|
||||
if (object.id && this._callbacks.has(object.id)) {
|
||||
const callback = this._callbacks.get(object.id);
|
||||
this._callbacks.delete(object.id);
|
||||
if (object.error)
|
||||
callback.reject(new Error(object.error));
|
||||
else
|
||||
callback.resolve(object.result);
|
||||
} else if (object.id) {
|
||||
throw new Error(`Unexpected message id: ${object.id}`);
|
||||
} else {
|
||||
throw new Error(`Unexpected message without id: ${JSON.stringify(object)}`);
|
||||
}
|
||||
}
|
||||
_rejectCallbacks() {
|
||||
for (const callback of this._callbacks.values())
|
||||
callback.reject(new Error("Session closed"));
|
||||
this._callbacks.clear();
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
Session
|
||||
});
|
||||
BIN
tools/e2e/node_modules/playwright-core/lib/tools/dashboard/appIcon.png
generated
vendored
Normal file
|
After Width: | Height: | Size: 18 KiB |
420
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/SKILL.md
generated
vendored
Normal file
@@ -0,0 +1,420 @@
|
||||
---
|
||||
name: playwright-cli
|
||||
description: Automate browser interactions, test web pages and work with Playwright tests.
|
||||
allowed-tools: Bash(playwright-cli:*) Bash(npx:*) Bash(npm:*)
|
||||
---
|
||||
|
||||
# Browser Automation with playwright-cli
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
# open new browser
|
||||
playwright-cli open
|
||||
# navigate to a page
|
||||
playwright-cli goto https://playwright.dev
|
||||
# interact with the page using refs from the snapshot
|
||||
playwright-cli click e15
|
||||
playwright-cli type "page.click"
|
||||
playwright-cli press Enter
|
||||
# take a screenshot (rarely used, as snapshot is more common)
|
||||
playwright-cli screenshot
|
||||
# close the browser
|
||||
playwright-cli close
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### Core
|
||||
|
||||
```bash
|
||||
playwright-cli open
|
||||
# open and navigate right away
|
||||
playwright-cli open https://example.com/
|
||||
playwright-cli goto https://playwright.dev
|
||||
playwright-cli type "search query"
|
||||
playwright-cli click e3
|
||||
playwright-cli dblclick e7
|
||||
# --submit presses Enter after filling the element
|
||||
playwright-cli fill e5 "user@example.com" --submit
|
||||
playwright-cli drag e2 e8
|
||||
# drop files or data onto an element (from outside the page)
|
||||
playwright-cli drop e4 --path=./image.png
|
||||
playwright-cli drop e4 --data="text/plain=hello world"
|
||||
playwright-cli hover e4
|
||||
playwright-cli select e9 "option-value"
|
||||
playwright-cli upload ./document.pdf
|
||||
playwright-cli check e12
|
||||
playwright-cli uncheck e12
|
||||
playwright-cli snapshot
|
||||
# search the snapshot for text or a regexp, returns matching nodes with surrounding context
|
||||
playwright-cli find "Sign in"
|
||||
playwright-cli find --regex "Sign (in|up)"
|
||||
# wrap the regexp in slashes to add flags, e.g. /i for case-insensitive
|
||||
playwright-cli find --regex "/sign (in|up)/i"
|
||||
playwright-cli eval "document.title"
|
||||
playwright-cli eval "el => el.textContent" e5
|
||||
# get element id, class, or any attribute not visible in the snapshot
|
||||
playwright-cli eval "el => el.id" e5
|
||||
playwright-cli eval "el => el.getAttribute('data-testid')" e5
|
||||
playwright-cli dialog-accept
|
||||
playwright-cli dialog-accept "confirmation text"
|
||||
playwright-cli dialog-dismiss
|
||||
playwright-cli resize 1920 1080
|
||||
playwright-cli close
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
```bash
|
||||
playwright-cli go-back
|
||||
playwright-cli go-forward
|
||||
playwright-cli reload
|
||||
```
|
||||
|
||||
### Keyboard
|
||||
|
||||
```bash
|
||||
playwright-cli press Enter
|
||||
playwright-cli press ArrowDown
|
||||
playwright-cli keydown Shift
|
||||
playwright-cli keyup Shift
|
||||
```
|
||||
|
||||
### Mouse
|
||||
|
||||
```bash
|
||||
playwright-cli mousemove 150 300
|
||||
playwright-cli mousedown
|
||||
playwright-cli mousedown right
|
||||
playwright-cli mouseup
|
||||
playwright-cli mouseup right
|
||||
playwright-cli mousewheel 0 100
|
||||
```
|
||||
|
||||
### Save as
|
||||
|
||||
```bash
|
||||
playwright-cli screenshot
|
||||
playwright-cli screenshot e5
|
||||
playwright-cli screenshot --filename=page.png
|
||||
playwright-cli screenshot --hires
|
||||
playwright-cli pdf --filename=page.pdf
|
||||
```
|
||||
|
||||
### Tabs
|
||||
|
||||
```bash
|
||||
playwright-cli tab-list
|
||||
playwright-cli tab-new
|
||||
playwright-cli tab-new https://example.com/page
|
||||
playwright-cli tab-close
|
||||
playwright-cli tab-close 2
|
||||
playwright-cli tab-select 0
|
||||
```
|
||||
|
||||
### Storage
|
||||
|
||||
```bash
|
||||
playwright-cli state-save
|
||||
playwright-cli state-save auth.json
|
||||
playwright-cli state-load auth.json
|
||||
|
||||
# Cookies
|
||||
playwright-cli cookie-list
|
||||
playwright-cli cookie-list --domain=example.com
|
||||
playwright-cli cookie-get session_id
|
||||
playwright-cli cookie-set session_id abc123
|
||||
playwright-cli cookie-set session_id abc123 --domain=example.com --httpOnly --secure
|
||||
playwright-cli cookie-delete session_id
|
||||
playwright-cli cookie-clear
|
||||
|
||||
# LocalStorage
|
||||
playwright-cli localstorage-list
|
||||
playwright-cli localstorage-get theme
|
||||
playwright-cli localstorage-set theme dark
|
||||
playwright-cli localstorage-delete theme
|
||||
playwright-cli localstorage-clear
|
||||
|
||||
# SessionStorage
|
||||
playwright-cli sessionstorage-list
|
||||
playwright-cli sessionstorage-get step
|
||||
playwright-cli sessionstorage-set step 3
|
||||
playwright-cli sessionstorage-delete step
|
||||
playwright-cli sessionstorage-clear
|
||||
```
|
||||
|
||||
### Network
|
||||
|
||||
```bash
|
||||
playwright-cli route "**/*.jpg" --status=404
|
||||
playwright-cli route "https://api.example.com/**" --body='{"mock": true}'
|
||||
playwright-cli route-list
|
||||
playwright-cli unroute "**/*.jpg"
|
||||
playwright-cli unroute
|
||||
```
|
||||
|
||||
### DevTools
|
||||
|
||||
```bash
|
||||
playwright-cli console
|
||||
playwright-cli console warning
|
||||
playwright-cli requests
|
||||
playwright-cli request 5
|
||||
playwright-cli run-code "async page => await page.context().grantPermissions(['geolocation'])"
|
||||
playwright-cli run-code --filename=script.js
|
||||
playwright-cli tracing-start
|
||||
playwright-cli tracing-stop
|
||||
playwright-cli video-start video.webm
|
||||
playwright-cli video-chapter "Chapter Title" --description="Details" --duration=2000
|
||||
playwright-cli video-stop
|
||||
|
||||
# annotate each subsequent action (click, type, ...) with a callout naming the action and highlighting the target
|
||||
playwright-cli video-show-actions --duration=600 --position=top-right
|
||||
playwright-cli video-hide-actions
|
||||
|
||||
# launch the dashboard for UI review / design feedback — user annotates the page, you receive the annotated screenshot, snapshot, and notes
|
||||
playwright-cli show --annotate
|
||||
|
||||
# generate a Playwright locator for an element from its ref or selector
|
||||
playwright-cli generate-locator e5 --raw
|
||||
|
||||
# show a persistent highlight overlay for an element, optionally with a custom style
|
||||
playwright-cli highlight e5
|
||||
playwright-cli highlight e5 --style="outline: 3px dashed red"
|
||||
# hide a single element highlight, or all page highlights when no target is given
|
||||
playwright-cli highlight e5 --hide
|
||||
playwright-cli highlight --hide
|
||||
```
|
||||
|
||||
## Raw output
|
||||
|
||||
The global `--raw` option strips page status, generated code, and snapshot sections from the output, returning only the result value. Use it to pipe command output into other tools. Commands that don't produce output return nothing.
|
||||
|
||||
```bash
|
||||
playwright-cli --raw eval "JSON.stringify(performance.timing)" | jq '.loadEventEnd - .navigationStart'
|
||||
playwright-cli --raw eval "JSON.stringify([...document.querySelectorAll('a')].map(a => a.href))" > links.json
|
||||
playwright-cli --raw snapshot > before.yml
|
||||
playwright-cli click e5
|
||||
playwright-cli --raw snapshot > after.yml
|
||||
diff before.yml after.yml
|
||||
TOKEN=$(playwright-cli --raw cookie-get session_id)
|
||||
playwright-cli --raw localstorage-get theme
|
||||
```
|
||||
|
||||
For structured output wrapping every reply as JSON, pass --json
|
||||
```bash
|
||||
playwright-cli list --json
|
||||
```
|
||||
|
||||
## Open parameters
|
||||
```bash
|
||||
# Use specific browser when creating session
|
||||
playwright-cli open --browser=chrome
|
||||
playwright-cli open --browser=firefox
|
||||
playwright-cli open --browser=webkit
|
||||
playwright-cli open --browser=msedge
|
||||
|
||||
# Emulate a generic mobile device (Pixel 10 for Chromium, iPhone 17 for WebKit).
|
||||
# Prefer this when a mobile layout is acceptable: mobile pages are usually
|
||||
# lighter, so snapshots are smaller and cheaper.
|
||||
playwright-cli open --mobile
|
||||
playwright-cli open --device="iPhone 15"
|
||||
|
||||
# Use persistent profile (by default profile is in-memory)
|
||||
playwright-cli open --persistent
|
||||
# Use persistent profile with custom directory
|
||||
playwright-cli open --profile=/path/to/profile
|
||||
|
||||
# Connect to browser via Playwright Extension
|
||||
playwright-cli attach --extension=chrome
|
||||
|
||||
# Connect to a running Chrome or Edge by channel name
|
||||
playwright-cli attach --cdp=chrome
|
||||
playwright-cli attach --cdp=msedge
|
||||
|
||||
# Connect to a running browser via CDP endpoint
|
||||
playwright-cli attach --cdp=http://localhost:9222
|
||||
|
||||
# Start with config file
|
||||
playwright-cli open --config=my-config.json
|
||||
|
||||
# Close the browser
|
||||
playwright-cli close
|
||||
# Detach from an attached browser (leaves the external browser running)
|
||||
playwright-cli -s=msedge detach
|
||||
# Delete user data for the default session
|
||||
playwright-cli delete-data
|
||||
```
|
||||
|
||||
## URLs with `&` on Windows
|
||||
|
||||
On Windows, `cmd.exe` and PowerShell treat `&` as a command separator, so URLs with multiple query parameters get truncated before `playwright-cli` runs. Escape `&` with `^&` in `cmd.exe`, or use `--%` in PowerShell:
|
||||
|
||||
```batch
|
||||
playwright-cli goto "https://example.com/?a=1^&b=2"
|
||||
```
|
||||
|
||||
```powershell
|
||||
playwright-cli --% goto "https://example.com/?a=1&b=2"
|
||||
```
|
||||
|
||||
## Snapshots
|
||||
|
||||
After each command, playwright-cli provides a snapshot of the current browser state.
|
||||
|
||||
```bash
|
||||
> playwright-cli goto https://example.com
|
||||
### Page
|
||||
- Page URL: https://example.com/
|
||||
- Page Title: Example Domain
|
||||
### Snapshot
|
||||
[Snapshot](.playwright-cli/page-2026-02-14T19-22-42-679Z.yml)
|
||||
```
|
||||
|
||||
You can also take a snapshot on demand using `playwright-cli snapshot` command. All the options below can be combined as needed.
|
||||
|
||||
```bash
|
||||
# default - save to a file with timestamp-based name
|
||||
playwright-cli snapshot
|
||||
|
||||
# save to file, use when snapshot is a part of the workflow result
|
||||
playwright-cli snapshot --filename=after-click.yaml
|
||||
|
||||
# snapshot an element instead of the whole page
|
||||
playwright-cli snapshot "#main"
|
||||
|
||||
# limit snapshot depth for efficiency, take a partial snapshot afterwards
|
||||
playwright-cli snapshot --depth=4
|
||||
playwright-cli snapshot e34
|
||||
|
||||
# include each element's bounding box as [box=x,y,width,height]
|
||||
playwright-cli snapshot --boxes
|
||||
|
||||
# search a large snapshot instead of capturing it all — returns matching nodes
|
||||
# with 3 lines of context around each match (like grep -C)
|
||||
playwright-cli find "Add to cart"
|
||||
playwright-cli find --regex "\\$[0-9]+\\.[0-9]{2}"
|
||||
```
|
||||
|
||||
## Targeting elements
|
||||
|
||||
By default, use refs from the snapshot to interact with page elements.
|
||||
|
||||
```bash
|
||||
# get snapshot with refs
|
||||
playwright-cli snapshot
|
||||
|
||||
# interact using a ref
|
||||
playwright-cli click e15
|
||||
```
|
||||
|
||||
You can also use css selectors or Playwright locators.
|
||||
|
||||
```bash
|
||||
# css selector
|
||||
playwright-cli click "#main > button.submit"
|
||||
|
||||
# role locator
|
||||
playwright-cli click "getByRole('button', { name: 'Submit' })"
|
||||
|
||||
# test id
|
||||
playwright-cli click "getByTestId('submit-button')"
|
||||
```
|
||||
|
||||
## Browser Sessions
|
||||
|
||||
```bash
|
||||
# create new browser session named "mysession" with persistent profile
|
||||
playwright-cli -s=mysession open example.com --persistent
|
||||
# same with manually specified profile directory (use when requested explicitly)
|
||||
playwright-cli -s=mysession open example.com --profile=/path/to/profile
|
||||
playwright-cli -s=mysession click e6
|
||||
playwright-cli -s=mysession close # stop a named browser
|
||||
playwright-cli -s=mysession delete-data # delete user data for persistent session
|
||||
|
||||
playwright-cli list
|
||||
# Close all browsers
|
||||
playwright-cli close-all
|
||||
# Forcefully kill all browser processes
|
||||
playwright-cli kill-all
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
If global `playwright-cli` command is not available, try a local version via `npx playwright cli`:
|
||||
|
||||
```bash
|
||||
npx --no-install playwright --version
|
||||
```
|
||||
|
||||
When local version is available, use `npx playwright cli` in all commands. Otherwise, install `playwright-cli` as a global command:
|
||||
|
||||
```bash
|
||||
npm install -g @playwright/cli@latest
|
||||
```
|
||||
|
||||
## Example: Form submission
|
||||
|
||||
```bash
|
||||
playwright-cli open https://example.com/form
|
||||
playwright-cli snapshot
|
||||
|
||||
playwright-cli fill e1 "user@example.com"
|
||||
playwright-cli fill e2 "password123"
|
||||
playwright-cli click e3
|
||||
playwright-cli snapshot
|
||||
playwright-cli close
|
||||
```
|
||||
|
||||
## Example: Multi-tab workflow
|
||||
|
||||
```bash
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli tab-new https://example.com/other
|
||||
playwright-cli tab-list
|
||||
playwright-cli tab-select 0
|
||||
playwright-cli snapshot
|
||||
playwright-cli close
|
||||
```
|
||||
|
||||
## Example: Debugging with DevTools
|
||||
|
||||
```bash
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli click e4
|
||||
playwright-cli fill e7 "test"
|
||||
playwright-cli console
|
||||
playwright-cli requests
|
||||
playwright-cli close
|
||||
```
|
||||
|
||||
```bash
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli tracing-start
|
||||
playwright-cli click e4
|
||||
playwright-cli fill e7 "test"
|
||||
playwright-cli tracing-stop
|
||||
playwright-cli close
|
||||
```
|
||||
|
||||
## Example: Interactive session
|
||||
|
||||
Ask the user for UI review or design feedback. The user draws boxes on the live page and types comments; you receive the annotated screenshot, the snapshot of the marked region, and the user's notes. Use this whenever the user asks for "UI review", "design feedback", or to "ask the user what they think / want / mean":
|
||||
|
||||
```bash
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli show --annotate
|
||||
```
|
||||
|
||||
## Specific tasks
|
||||
|
||||
* **Running and Debugging Playwright tests** [references/playwright-tests.md](references/playwright-tests.md)
|
||||
* **Request mocking** [references/request-mocking.md](references/request-mocking.md)
|
||||
* **Running Playwright code** [references/running-code.md](references/running-code.md)
|
||||
* **Browser session management** [references/session-management.md](references/session-management.md)
|
||||
* **Storage state (cookies, localStorage)** [references/storage-state.md](references/storage-state.md)
|
||||
* **Test generation (plan / generate / heal)** [references/test-generation.md](references/test-generation.md)
|
||||
* **Tracing** [references/tracing.md](references/tracing.md)
|
||||
* **Video recording** [references/video-recording.md](references/video-recording.md)
|
||||
* **Inspecting element attributes** [references/element-attributes.md](references/element-attributes.md)
|
||||
23
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/element-attributes.md
generated
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Inspecting Element Attributes
|
||||
|
||||
When the snapshot doesn't show an element's `id`, `class`, `data-*` attributes, or other DOM properties, use `eval` to inspect them.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
playwright-cli snapshot
|
||||
# snapshot shows a button as e7 but doesn't reveal its id or data attributes
|
||||
|
||||
# get the element's id
|
||||
playwright-cli eval "el => el.id" e7
|
||||
|
||||
# get all CSS classes
|
||||
playwright-cli eval "el => el.className" e7
|
||||
|
||||
# get a specific attribute
|
||||
playwright-cli eval "el => el.getAttribute('data-testid')" e7
|
||||
playwright-cli eval "el => el.getAttribute('aria-label')" e7
|
||||
|
||||
# get a computed style property
|
||||
playwright-cli eval "el => getComputedStyle(el).display" e7
|
||||
```
|
||||
39
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/playwright-tests.md
generated
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
# Running Playwright Tests
|
||||
|
||||
To run Playwright tests, use the `npx playwright test` command, or a package manager script. To avoid opening the interactive html report, use `PLAYWRIGHT_HTML_OPEN=never` environment variable.
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test
|
||||
|
||||
# Run all tests through a custom npm script
|
||||
PLAYWRIGHT_HTML_OPEN=never npm run special-test-command
|
||||
```
|
||||
|
||||
# Debugging Playwright Tests
|
||||
|
||||
To debug a failing Playwright test, run it with `--debug=cli` option. This command will pause the test at the start and print the debugging instructions.
|
||||
|
||||
**IMPORTANT**: run the command in the background and check the output until "Debugging Instructions" is printed. Make sure to stop the command after you have finished.
|
||||
|
||||
Once instructions containing a session name are printed, use `playwright-cli` to attach the session and explore the page.
|
||||
|
||||
```bash
|
||||
# Run the test
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test --debug=cli
|
||||
# ...
|
||||
# ... debugging instructions for "tw-abcdef" session ...
|
||||
# ...
|
||||
|
||||
# Attach to the test
|
||||
playwright-cli attach tw-abcdef
|
||||
```
|
||||
|
||||
Keep the test running in the background while you explore and look for a fix.
|
||||
The test is paused at the start, so you should step over or pause at a particular location
|
||||
where the problem is most likely to be.
|
||||
|
||||
Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code.
|
||||
This code appears in the output and can be copied directly into the test. Most of the time, a specific locator or an expectation should be updated, but it could also be a bug in the app. Use your judgement.
|
||||
|
||||
After fixing the test, stop the background test run. Rerun to check that test passes.
|
||||
87
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/request-mocking.md
generated
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
# Request Mocking
|
||||
|
||||
Intercept, mock, modify, and block network requests.
|
||||
|
||||
## CLI Route Commands
|
||||
|
||||
```bash
|
||||
# Mock with custom status
|
||||
playwright-cli route "**/*.jpg" --status=404
|
||||
|
||||
# Mock with JSON body
|
||||
playwright-cli route "**/api/users" --body='[{"id":1,"name":"Alice"}]' --content-type=application/json
|
||||
|
||||
# Mock with custom headers
|
||||
playwright-cli route "**/api/data" --body='{"ok":true}' --header="X-Custom: value"
|
||||
|
||||
# Remove headers from requests
|
||||
playwright-cli route "**/*" --remove-header=cookie,authorization
|
||||
|
||||
# List active routes
|
||||
playwright-cli route-list
|
||||
|
||||
# Remove a route or all routes
|
||||
playwright-cli unroute "**/*.jpg"
|
||||
playwright-cli unroute
|
||||
```
|
||||
|
||||
## URL Patterns
|
||||
|
||||
```
|
||||
**/api/users - Exact path match
|
||||
**/api/*/details - Wildcard in path
|
||||
**/*.{png,jpg,jpeg} - Match file extensions
|
||||
**/search?q=* - Match query parameters
|
||||
```
|
||||
|
||||
## Advanced Mocking with run-code
|
||||
|
||||
For conditional responses, request body inspection, response modification, or delays:
|
||||
|
||||
### Conditional Response Based on Request
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.route('**/api/login', route => {
|
||||
const body = route.request().postDataJSON();
|
||||
if (body.username === 'admin') {
|
||||
route.fulfill({ body: JSON.stringify({ token: 'mock-token' }) });
|
||||
} else {
|
||||
route.fulfill({ status: 401, body: JSON.stringify({ error: 'Invalid' }) });
|
||||
}
|
||||
});
|
||||
}"
|
||||
```
|
||||
|
||||
### Modify Real Response
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.route('**/api/user', async route => {
|
||||
const response = await route.fetch();
|
||||
const json = await response.json();
|
||||
json.isPremium = true;
|
||||
await route.fulfill({ response, json });
|
||||
});
|
||||
}"
|
||||
```
|
||||
|
||||
### Simulate Network Failures
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.route('**/api/offline', route => route.abort('internetdisconnected'));
|
||||
}"
|
||||
# Options: connectionrefused, timedout, connectionreset, internetdisconnected
|
||||
```
|
||||
|
||||
### Delayed Response
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.route('**/api/slow', async route => {
|
||||
await new Promise(r => setTimeout(r, 3000));
|
||||
route.fulfill({ body: JSON.stringify({ data: 'loaded' }) });
|
||||
});
|
||||
}"
|
||||
```
|
||||
241
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/running-code.md
generated
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
# Running Custom Playwright Code
|
||||
|
||||
Use `run-code` to execute arbitrary Playwright code for advanced scenarios not covered by CLI commands.
|
||||
|
||||
## Syntax
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
// Your Playwright code here
|
||||
// Access page.context() for browser context operations
|
||||
}"
|
||||
```
|
||||
|
||||
You can also load the function from a file:
|
||||
|
||||
```bash
|
||||
playwright-cli run-code --filename=./my-script.js
|
||||
```
|
||||
|
||||
|
||||
The code must be a single function expression, it is wrapped in `(...)` and evaluated.
|
||||
import/export/require syntax is not supported.
|
||||
|
||||
## Geolocation
|
||||
|
||||
```bash
|
||||
# Grant geolocation permission and set location
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().grantPermissions(['geolocation']);
|
||||
await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });
|
||||
}"
|
||||
|
||||
# Set location to London
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().grantPermissions(['geolocation']);
|
||||
await page.context().setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
|
||||
}"
|
||||
|
||||
# Clear geolocation override
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().clearPermissions();
|
||||
}"
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
```bash
|
||||
# Grant multiple permissions
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().grantPermissions([
|
||||
'geolocation',
|
||||
'notifications',
|
||||
'camera',
|
||||
'microphone'
|
||||
]);
|
||||
}"
|
||||
|
||||
# Grant permissions for specific origin
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().grantPermissions(['clipboard-read'], {
|
||||
origin: 'https://example.com'
|
||||
});
|
||||
}"
|
||||
```
|
||||
|
||||
## Media Emulation
|
||||
|
||||
```bash
|
||||
# Emulate dark color scheme
|
||||
playwright-cli run-code "async page => {
|
||||
await page.emulateMedia({ colorScheme: 'dark' });
|
||||
}"
|
||||
|
||||
# Emulate light color scheme
|
||||
playwright-cli run-code "async page => {
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
}"
|
||||
|
||||
# Emulate reduced motion
|
||||
playwright-cli run-code "async page => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
}"
|
||||
|
||||
# Emulate print media
|
||||
playwright-cli run-code "async page => {
|
||||
await page.emulateMedia({ media: 'print' });
|
||||
}"
|
||||
```
|
||||
|
||||
## Wait Strategies
|
||||
|
||||
```bash
|
||||
# Wait for network idle
|
||||
playwright-cli run-code "async page => {
|
||||
await page.waitForLoadState('networkidle');
|
||||
}"
|
||||
|
||||
# Wait for specific element
|
||||
playwright-cli run-code "async page => {
|
||||
await page.locator('.loading').waitFor({ state: 'hidden' });
|
||||
}"
|
||||
|
||||
# Wait for function to return true
|
||||
playwright-cli run-code "async page => {
|
||||
await page.waitForFunction(() => window.appReady === true);
|
||||
}"
|
||||
|
||||
# Wait with timeout
|
||||
playwright-cli run-code "async page => {
|
||||
await page.locator('.result').waitFor({ timeout: 10000 });
|
||||
}"
|
||||
```
|
||||
|
||||
## Frames and Iframes
|
||||
|
||||
```bash
|
||||
# Work with iframe
|
||||
playwright-cli run-code "async page => {
|
||||
const frame = page.locator('iframe#my-iframe').contentFrame();
|
||||
await frame.locator('button').click();
|
||||
}"
|
||||
|
||||
# Get all frames
|
||||
playwright-cli run-code "async page => {
|
||||
const frames = page.frames();
|
||||
return frames.map(f => f.url());
|
||||
}"
|
||||
```
|
||||
|
||||
## File Downloads
|
||||
|
||||
```bash
|
||||
# Handle file download
|
||||
playwright-cli run-code "async page => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page.getByRole('link', { name: 'Download' }).click();
|
||||
const download = await downloadPromise;
|
||||
await download.saveAs('./downloaded-file.pdf');
|
||||
return download.suggestedFilename();
|
||||
}"
|
||||
```
|
||||
|
||||
## Clipboard
|
||||
|
||||
```bash
|
||||
# Read clipboard (requires permission)
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().grantPermissions(['clipboard-read']);
|
||||
return await page.evaluate(() => navigator.clipboard.readText());
|
||||
}"
|
||||
|
||||
# Write to clipboard
|
||||
playwright-cli run-code "async page => {
|
||||
await page.evaluate(text => navigator.clipboard.writeText(text), 'Hello clipboard!');
|
||||
}"
|
||||
```
|
||||
|
||||
## Page Information
|
||||
|
||||
```bash
|
||||
# Get page title
|
||||
playwright-cli run-code "async page => {
|
||||
return await page.title();
|
||||
}"
|
||||
|
||||
# Get current URL
|
||||
playwright-cli run-code "async page => {
|
||||
return page.url();
|
||||
}"
|
||||
|
||||
# Get page content
|
||||
playwright-cli run-code "async page => {
|
||||
return await page.content();
|
||||
}"
|
||||
|
||||
# Get viewport size
|
||||
playwright-cli run-code "async page => {
|
||||
return page.viewportSize();
|
||||
}"
|
||||
```
|
||||
|
||||
## JavaScript Execution
|
||||
|
||||
```bash
|
||||
# Execute JavaScript and return result
|
||||
playwright-cli run-code "async page => {
|
||||
return await page.evaluate(() => {
|
||||
return {
|
||||
userAgent: navigator.userAgent,
|
||||
language: navigator.language,
|
||||
cookiesEnabled: navigator.cookieEnabled
|
||||
};
|
||||
});
|
||||
}"
|
||||
|
||||
# Pass arguments to evaluate
|
||||
playwright-cli run-code "async page => {
|
||||
const multiplier = 5;
|
||||
return await page.evaluate(m => document.querySelectorAll('li').length * m, multiplier);
|
||||
}"
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```bash
|
||||
# Try-catch in run-code
|
||||
playwright-cli run-code "async page => {
|
||||
try {
|
||||
await page.getByRole('button', { name: 'Submit' }).click({ timeout: 1000 });
|
||||
return 'clicked';
|
||||
} catch (e) {
|
||||
return 'element not found';
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
## Complex Workflows
|
||||
|
||||
```bash
|
||||
# Login and save state
|
||||
playwright-cli run-code "async page => {
|
||||
await page.goto('https://example.com/login');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill('secret');
|
||||
await page.getByRole('button', { name: 'Sign in' }).click();
|
||||
await page.waitForURL('**/dashboard');
|
||||
await page.context().storageState({ path: 'auth.json' });
|
||||
return 'Login successful';
|
||||
}"
|
||||
|
||||
# Scrape data from multiple pages
|
||||
playwright-cli run-code "async page => {
|
||||
const results = [];
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
await page.goto(\`https://example.com/page/\${i}\`);
|
||||
const items = await page.locator('.item').allTextContents();
|
||||
results.push(...items);
|
||||
}
|
||||
return results;
|
||||
}"
|
||||
```
|
||||
225
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/session-management.md
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
# Browser Session Management
|
||||
|
||||
Run multiple isolated browser sessions concurrently with state persistence.
|
||||
|
||||
## Named Browser Sessions
|
||||
|
||||
Use `-s` flag to isolate browser contexts:
|
||||
|
||||
```bash
|
||||
# Browser 1: Authentication flow
|
||||
playwright-cli -s=auth open https://app.example.com/login
|
||||
|
||||
# Browser 2: Public browsing (separate cookies, storage)
|
||||
playwright-cli -s=public open https://example.com
|
||||
|
||||
# Commands are isolated by browser session
|
||||
playwright-cli -s=auth fill e1 "user@example.com"
|
||||
playwright-cli -s=public snapshot
|
||||
```
|
||||
|
||||
## Browser Session Isolation Properties
|
||||
|
||||
Each browser session has independent:
|
||||
- Cookies
|
||||
- LocalStorage / SessionStorage
|
||||
- IndexedDB
|
||||
- Cache
|
||||
- Browsing history
|
||||
- Open tabs
|
||||
|
||||
## Browser Session Commands
|
||||
|
||||
```bash
|
||||
# List all browser sessions
|
||||
playwright-cli list
|
||||
|
||||
# Stop a browser session (close the browser)
|
||||
playwright-cli close # stop the default browser
|
||||
playwright-cli -s=mysession close # stop a named browser
|
||||
|
||||
# Stop all browser sessions
|
||||
playwright-cli close-all
|
||||
|
||||
# Forcefully kill all daemon processes (for stale/zombie processes)
|
||||
playwright-cli kill-all
|
||||
|
||||
# Delete browser session user data (profile directory)
|
||||
playwright-cli delete-data # delete default browser data
|
||||
playwright-cli -s=mysession delete-data # delete named browser data
|
||||
```
|
||||
|
||||
## Environment Variable
|
||||
|
||||
Set a default browser session name via environment variable:
|
||||
|
||||
```bash
|
||||
export PLAYWRIGHT_CLI_SESSION="mysession"
|
||||
playwright-cli open example.com # Uses "mysession" automatically
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Concurrent Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Scrape multiple sites concurrently
|
||||
|
||||
# Start all browsers
|
||||
playwright-cli -s=site1 open https://site1.com &
|
||||
playwright-cli -s=site2 open https://site2.com &
|
||||
playwright-cli -s=site3 open https://site3.com &
|
||||
wait
|
||||
|
||||
# Take snapshots from each
|
||||
playwright-cli -s=site1 snapshot
|
||||
playwright-cli -s=site2 snapshot
|
||||
playwright-cli -s=site3 snapshot
|
||||
|
||||
# Cleanup
|
||||
playwright-cli close-all
|
||||
```
|
||||
|
||||
### A/B Testing Sessions
|
||||
|
||||
```bash
|
||||
# Test different user experiences
|
||||
playwright-cli -s=variant-a open "https://app.com?variant=a"
|
||||
playwright-cli -s=variant-b open "https://app.com?variant=b"
|
||||
|
||||
# Compare
|
||||
playwright-cli -s=variant-a screenshot
|
||||
playwright-cli -s=variant-b screenshot
|
||||
```
|
||||
|
||||
### Persistent Profile
|
||||
|
||||
By default, browser profile is kept in memory only. Use `--persistent` flag on `open` to persist the browser profile to disk:
|
||||
|
||||
```bash
|
||||
# Use persistent profile (auto-generated location)
|
||||
playwright-cli open https://example.com --persistent
|
||||
|
||||
# Use persistent profile with custom directory
|
||||
playwright-cli open https://example.com --profile=/path/to/profile
|
||||
```
|
||||
|
||||
## Attaching to a Running Browser
|
||||
|
||||
Use `attach` to connect to a browser that is already running, instead of launching a new one.
|
||||
|
||||
### Attach by channel name
|
||||
|
||||
Connect to a running Chrome or Edge instance by its channel name. The browser must have remote debugging enabled — navigate to `chrome://inspect/#remote-debugging` in the target browser and check "Allow remote debugging for this browser instance".
|
||||
|
||||
```bash
|
||||
# Attach to Chrome
|
||||
playwright-cli attach --cdp=chrome
|
||||
|
||||
# Attach to Chrome Canary
|
||||
playwright-cli attach --cdp=chrome-canary
|
||||
|
||||
# Attach to Microsoft Edge
|
||||
playwright-cli attach --cdp=msedge
|
||||
|
||||
# Attach to Edge Dev
|
||||
playwright-cli attach --cdp=msedge-dev
|
||||
```
|
||||
|
||||
Supported channels: `chrome`, `chrome-beta`, `chrome-dev`, `chrome-canary`, `msedge`, `msedge-beta`, `msedge-dev`, `msedge-canary`.
|
||||
|
||||
When `--session` is not provided, the session is named after the channel (e.g. `--cdp=msedge` creates a session called `msedge`), so parallel attaches to Chrome and Edge don't collide on `default`. Pass `--session=<name>` to override.
|
||||
|
||||
### Attach via CDP endpoint
|
||||
|
||||
Connect to a browser that exposes a Chrome DevTools Protocol endpoint:
|
||||
|
||||
```bash
|
||||
playwright-cli attach --cdp=http://localhost:9222
|
||||
```
|
||||
|
||||
### Attach via browser extension
|
||||
|
||||
Connect to a browser with the Playwright extension installed:
|
||||
|
||||
```bash
|
||||
playwright-cli attach --extension
|
||||
```
|
||||
|
||||
### Detach
|
||||
|
||||
Tear down an attached session without affecting the external browser:
|
||||
|
||||
```bash
|
||||
# Detach the default attached session
|
||||
playwright-cli detach
|
||||
|
||||
# Detach a specific attached session
|
||||
playwright-cli -s=msedge detach
|
||||
```
|
||||
|
||||
`detach` only works on sessions created via `attach`. For sessions created via `open`, use `close`.
|
||||
|
||||
## Default Browser Session
|
||||
|
||||
When `-s` is omitted, commands use the default browser session:
|
||||
|
||||
```bash
|
||||
# These use the same default browser session
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli snapshot
|
||||
playwright-cli close # Stops default browser
|
||||
```
|
||||
|
||||
## Browser Session Configuration
|
||||
|
||||
Configure a browser session with specific settings when opening:
|
||||
|
||||
```bash
|
||||
# Open with config file
|
||||
playwright-cli open https://example.com --config=.playwright/my-cli.json
|
||||
|
||||
# Open with specific browser
|
||||
playwright-cli open https://example.com --browser=firefox
|
||||
|
||||
# Open in headed mode
|
||||
playwright-cli open https://example.com --headed
|
||||
|
||||
# Open with persistent profile
|
||||
playwright-cli open https://example.com --persistent
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Name Browser Sessions Semantically
|
||||
|
||||
```bash
|
||||
# GOOD: Clear purpose
|
||||
playwright-cli -s=github-auth open https://github.com
|
||||
playwright-cli -s=docs-scrape open https://docs.example.com
|
||||
|
||||
# AVOID: Generic names
|
||||
playwright-cli -s=s1 open https://github.com
|
||||
```
|
||||
|
||||
### 2. Always Clean Up
|
||||
|
||||
```bash
|
||||
# Stop browsers when done
|
||||
playwright-cli -s=auth close
|
||||
playwright-cli -s=scrape close
|
||||
|
||||
# Or stop all at once
|
||||
playwright-cli close-all
|
||||
|
||||
# If browsers become unresponsive or zombie processes remain
|
||||
playwright-cli kill-all
|
||||
```
|
||||
|
||||
### 3. Delete Stale Browser Data
|
||||
|
||||
```bash
|
||||
# Remove old browser data to free disk space
|
||||
playwright-cli -s=oldsession delete-data
|
||||
```
|
||||
275
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/storage-state.md
generated
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
# Storage Management
|
||||
|
||||
Manage cookies, localStorage, sessionStorage, and browser storage state.
|
||||
|
||||
## Storage State
|
||||
|
||||
Save and restore complete browser state including cookies and storage.
|
||||
|
||||
### Save Storage State
|
||||
|
||||
```bash
|
||||
# Save to auto-generated filename (storage-state-{timestamp}.json)
|
||||
playwright-cli state-save
|
||||
|
||||
# Save to specific filename
|
||||
playwright-cli state-save my-auth-state.json
|
||||
```
|
||||
|
||||
### Restore Storage State
|
||||
|
||||
```bash
|
||||
# Load storage state from file
|
||||
playwright-cli state-load my-auth-state.json
|
||||
|
||||
# Reload page to apply cookies
|
||||
playwright-cli open https://example.com
|
||||
```
|
||||
|
||||
### Storage State File Format
|
||||
|
||||
The saved file contains:
|
||||
|
||||
```json
|
||||
{
|
||||
"cookies": [
|
||||
{
|
||||
"name": "session_id",
|
||||
"value": "abc123",
|
||||
"domain": "example.com",
|
||||
"path": "/",
|
||||
"expires": 1893456000,
|
||||
"httpOnly": true,
|
||||
"secure": true,
|
||||
"sameSite": "Lax"
|
||||
}
|
||||
],
|
||||
"origins": [
|
||||
{
|
||||
"origin": "https://example.com",
|
||||
"localStorage": [
|
||||
{ "name": "theme", "value": "dark" },
|
||||
{ "name": "user_id", "value": "12345" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Cookies
|
||||
|
||||
### List All Cookies
|
||||
|
||||
```bash
|
||||
playwright-cli cookie-list
|
||||
```
|
||||
|
||||
### Filter Cookies by Domain
|
||||
|
||||
```bash
|
||||
playwright-cli cookie-list --domain=example.com
|
||||
```
|
||||
|
||||
### Filter Cookies by Path
|
||||
|
||||
```bash
|
||||
playwright-cli cookie-list --path=/api
|
||||
```
|
||||
|
||||
### Get Specific Cookie
|
||||
|
||||
```bash
|
||||
playwright-cli cookie-get session_id
|
||||
```
|
||||
|
||||
### Set a Cookie
|
||||
|
||||
```bash
|
||||
# Basic cookie
|
||||
playwright-cli cookie-set session abc123
|
||||
|
||||
# Cookie with options
|
||||
playwright-cli cookie-set session abc123 --domain=example.com --path=/ --httpOnly --secure --sameSite=Lax
|
||||
|
||||
# Cookie with expiration (Unix timestamp)
|
||||
playwright-cli cookie-set remember_me token123 --expires=1893456000
|
||||
```
|
||||
|
||||
### Delete a Cookie
|
||||
|
||||
```bash
|
||||
playwright-cli cookie-delete session_id
|
||||
```
|
||||
|
||||
### Clear All Cookies
|
||||
|
||||
```bash
|
||||
playwright-cli cookie-clear
|
||||
```
|
||||
|
||||
### Advanced: Multiple Cookies or Custom Options
|
||||
|
||||
For complex scenarios like adding multiple cookies at once, use `run-code`:
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.context().addCookies([
|
||||
{ name: 'session_id', value: 'sess_abc123', domain: 'example.com', path: '/', httpOnly: true },
|
||||
{ name: 'preferences', value: JSON.stringify({ theme: 'dark' }), domain: 'example.com', path: '/' }
|
||||
]);
|
||||
}"
|
||||
```
|
||||
|
||||
## Local Storage
|
||||
|
||||
### List All localStorage Items
|
||||
|
||||
```bash
|
||||
playwright-cli localstorage-list
|
||||
```
|
||||
|
||||
### Get Single Value
|
||||
|
||||
```bash
|
||||
playwright-cli localstorage-get token
|
||||
```
|
||||
|
||||
### Set Value
|
||||
|
||||
```bash
|
||||
playwright-cli localstorage-set theme dark
|
||||
```
|
||||
|
||||
### Set JSON Value
|
||||
|
||||
```bash
|
||||
playwright-cli localstorage-set user_settings '{"theme":"dark","language":"en"}'
|
||||
```
|
||||
|
||||
### Delete Single Item
|
||||
|
||||
```bash
|
||||
playwright-cli localstorage-delete token
|
||||
```
|
||||
|
||||
### Clear All localStorage
|
||||
|
||||
```bash
|
||||
playwright-cli localstorage-clear
|
||||
```
|
||||
|
||||
### Advanced: Multiple Operations
|
||||
|
||||
For complex scenarios like setting multiple values at once, use `run-code`:
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('token', 'jwt_abc123');
|
||||
localStorage.setItem('user_id', '12345');
|
||||
localStorage.setItem('expires_at', Date.now() + 3600000);
|
||||
});
|
||||
}"
|
||||
```
|
||||
|
||||
## Session Storage
|
||||
|
||||
### List All sessionStorage Items
|
||||
|
||||
```bash
|
||||
playwright-cli sessionstorage-list
|
||||
```
|
||||
|
||||
### Get Single Value
|
||||
|
||||
```bash
|
||||
playwright-cli sessionstorage-get form_data
|
||||
```
|
||||
|
||||
### Set Value
|
||||
|
||||
```bash
|
||||
playwright-cli sessionstorage-set step 3
|
||||
```
|
||||
|
||||
### Delete Single Item
|
||||
|
||||
```bash
|
||||
playwright-cli sessionstorage-delete step
|
||||
```
|
||||
|
||||
### Clear sessionStorage
|
||||
|
||||
```bash
|
||||
playwright-cli sessionstorage-clear
|
||||
```
|
||||
|
||||
## IndexedDB
|
||||
|
||||
### List Databases
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
return await page.evaluate(async () => {
|
||||
const databases = await indexedDB.databases();
|
||||
return databases;
|
||||
});
|
||||
}"
|
||||
```
|
||||
|
||||
### Delete Database
|
||||
|
||||
```bash
|
||||
playwright-cli run-code "async page => {
|
||||
await page.evaluate(() => {
|
||||
indexedDB.deleteDatabase('myDatabase');
|
||||
});
|
||||
}"
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Authentication State Reuse
|
||||
|
||||
```bash
|
||||
# Step 1: Login and save state
|
||||
playwright-cli open https://app.example.com/login
|
||||
playwright-cli snapshot
|
||||
playwright-cli fill e1 "user@example.com"
|
||||
playwright-cli fill e2 "password123"
|
||||
playwright-cli click e3
|
||||
|
||||
# Save the authenticated state
|
||||
playwright-cli state-save auth.json
|
||||
|
||||
# Step 2: Later, restore state and skip login
|
||||
playwright-cli state-load auth.json
|
||||
playwright-cli open https://app.example.com/dashboard
|
||||
# Already logged in!
|
||||
```
|
||||
|
||||
### Save and Restore Roundtrip
|
||||
|
||||
```bash
|
||||
# Set up authentication state
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli eval "() => { document.cookie = 'session=abc123'; localStorage.setItem('user', 'john'); }"
|
||||
|
||||
# Save state to file
|
||||
playwright-cli state-save my-session.json
|
||||
|
||||
# ... later, in a new session ...
|
||||
|
||||
# Restore state
|
||||
playwright-cli state-load my-session.json
|
||||
playwright-cli open https://example.com
|
||||
# Cookies and localStorage are restored!
|
||||
```
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit storage state files containing auth tokens
|
||||
- Add `*.auth-state.json` to `.gitignore`
|
||||
- Delete state files after automation completes
|
||||
- Use environment variables for sensitive data
|
||||
- By default, sessions run in-memory mode which is safer for sensitive operations
|
||||
433
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/test-generation.md
generated
vendored
Normal file
@@ -0,0 +1,433 @@
|
||||
# Test generation (plan → generate → heal)
|
||||
|
||||
End-to-end workflow for authoring and maintaining Playwright tests with `playwright-cli`. Every `playwright-cli` action emits the equivalent Playwright TypeScript, and that generated code is the raw material for every test. The sections below can be used independently:
|
||||
|
||||
- **How generation works** — the core mechanic everything else relies on: actions become TypeScript, plus how to add assertions.
|
||||
- **Plan** — explore the app, produce a spec file describing what to test.
|
||||
- **Generate** — turn a spec into Playwright test files. Update the spec if it's vague or stale.
|
||||
- **Heal** — diagnose failing tests, fix the code, reconcile the spec with reality.
|
||||
|
||||
Plan / generate / heal lean on the same mechanic: run `npx playwright test --debug=cli` in the background, then `playwright-cli attach tw-XXXX` to drive the paused page interactively. See [playwright-tests.md](playwright-tests.md) for the debug/attach mechanics.
|
||||
|
||||
---
|
||||
|
||||
## 0. How generation works
|
||||
|
||||
Every action you perform with `playwright-cli` generates corresponding Playwright TypeScript code. This code appears in the output and can be copied directly into your test files.
|
||||
|
||||
```bash
|
||||
# Start a session
|
||||
playwright-cli open https://example.com/login
|
||||
|
||||
# Take a snapshot to see elements
|
||||
playwright-cli snapshot
|
||||
# Output shows: e1 [textbox "Email"], e2 [textbox "Password"], e3 [button "Sign In"]
|
||||
|
||||
# Fill form fields - generates code automatically
|
||||
playwright-cli fill e1 "user@example.com"
|
||||
# Ran Playwright code:
|
||||
# await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
||||
|
||||
playwright-cli fill e2 "password123"
|
||||
# Ran Playwright code:
|
||||
# await page.getByRole('textbox', { name: 'Password' }).fill('password123');
|
||||
|
||||
playwright-cli click e3
|
||||
# Ran Playwright code:
|
||||
# await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
```
|
||||
|
||||
### Building a test file
|
||||
|
||||
Collect the generated code into a Playwright test:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('login flow', async ({ page }) => {
|
||||
// Generated code from playwright-cli session:
|
||||
await page.goto('https://example.com/login');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill('password123');
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
|
||||
// Add assertions
|
||||
await expect(page).toHaveURL(/.*dashboard/);
|
||||
});
|
||||
```
|
||||
|
||||
### Use semantic locators
|
||||
|
||||
The generated code uses role-based locators when possible, which are more resilient:
|
||||
|
||||
```typescript
|
||||
// Generated (good - semantic)
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Avoid (fragile - CSS selectors)
|
||||
await page.locator('#submit-btn').click();
|
||||
```
|
||||
|
||||
### Explore before recording
|
||||
|
||||
Take snapshots to understand the page structure before recording actions:
|
||||
|
||||
```bash
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli snapshot
|
||||
# Review the element structure
|
||||
playwright-cli click e5
|
||||
```
|
||||
|
||||
### Add assertions manually
|
||||
|
||||
Generated code captures actions but not assertions. Add expectations in your test using one of the recommended matchers:
|
||||
|
||||
- `toBeVisible()` — element is rendered and visible
|
||||
- `toHaveText(text)` — element text content matches
|
||||
- `toHaveValue(value) / toBeEmpty()` — input/select value matches
|
||||
- `toBeChecked() / toBeUnchecked()` — checkbox state matches
|
||||
- `toMatchAriaSnapshot(snapshot)` — page (or locator) matches a partial accessibility snapshot
|
||||
|
||||
Use `playwright-cli generate-locator <target>` to produce the locator expression for the assertion, and the snapshot/eval commands to capture the expected value.
|
||||
|
||||
When asserting text content, make sure that generated locator does not contain text from the element itself. `getByTestId()` or `getByLabel()` usually work well with asserting text. When locator is text-based, prefer `toBeVisible()` instead.
|
||||
|
||||
Snapshot to be matched does not have to contain all the information - only capture what's necessary for the assertion. You can use regular expressions for unstable values.
|
||||
|
||||
```bash
|
||||
# Get a stable locator for an element ref to use in the assertion
|
||||
playwright-cli --raw generate-locator e5
|
||||
# getByRole('button', { name: 'Submit' })
|
||||
|
||||
# Capture expected text content for toHaveText
|
||||
playwright-cli --raw eval "el => el.textContent" e5
|
||||
|
||||
# Capture expected input value for toHaveValue/toBeEmpty
|
||||
playwright-cli --raw eval "el => el.value" e5
|
||||
|
||||
# Capture expected aria snapshot for toMatchAriaSnapshot/toBeChecked
|
||||
# (whole page, or use a ref to scope to a region)
|
||||
playwright-cli --raw snapshot
|
||||
playwright-cli --raw snapshot e5
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Generated action
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
|
||||
// Manual assertions using the outputs above:
|
||||
await expect(page.getByRole('alert', { name: 'Success' })).toBeVisible();
|
||||
await expect(page.getByTestId('main-header')).toHaveText('Welcome, user');
|
||||
await expect(page.getByRole('textbox', { name: 'Email' })).toHaveValue('user@example.com');
|
||||
await expect(page.getByRole('checkbox', { name: 'Enable notifications' })).toBeChecked();
|
||||
|
||||
// toMatchAriaSnapshot on the whole page, finds a matching region
|
||||
await expect(page).toMatchAriaSnapshot(`
|
||||
- heading "Welcome, user"
|
||||
- link /\\d+ new messages?/
|
||||
- button "Sign out"
|
||||
`);
|
||||
|
||||
// toMatchAriaSnapshot scoped to a region
|
||||
await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`
|
||||
- link "Home"
|
||||
- link /\\d+ new messages?/
|
||||
- link "Profile"
|
||||
`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Planning
|
||||
|
||||
Goal: produce a spec file (e.g. `specs/<feature>.plan.md`) that enumerates the scenarios to test. **Always** write the spec to a file.
|
||||
|
||||
### 1.1 Prerequisite: workspace
|
||||
|
||||
Check the workspace has Playwright installed before anything else:
|
||||
|
||||
```bash
|
||||
# Either of these confirms a workspace:
|
||||
test -f playwright.config.ts || test -f playwright.config.js
|
||||
npx --no-install playwright --version
|
||||
```
|
||||
|
||||
If there is no Playwright install, bootstrap one and let the user pick the defaults:
|
||||
|
||||
```bash
|
||||
npm init playwright@latest
|
||||
```
|
||||
|
||||
### 1.2 Prerequisite: seed test
|
||||
|
||||
A **seed test** is a minimal test that lands the page in the state every scenario starts from: navigation to the app, any required login, feature flags, etc. Scenarios assume a fresh start *after* the seed. `--debug=cli` pauses *inside* this test, so the seed is where every planning and generation session begins.
|
||||
|
||||
Minimum viable seed:
|
||||
|
||||
```ts
|
||||
// tests/seed.spec.ts
|
||||
import { test } from '@playwright/test';
|
||||
|
||||
test('seed', async ({ page }) => {
|
||||
await page.goto('https://example.com/');
|
||||
});
|
||||
```
|
||||
|
||||
Preferred — push navigation into a fixture so scenario tests reuse it:
|
||||
|
||||
```ts
|
||||
// tests/fixtures.ts
|
||||
import { test as baseTest } from '@playwright/test';
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
export const test = baseTest.extend({
|
||||
page: async ({ page }, use) => {
|
||||
await page.goto('https://example.com/');
|
||||
await use(page);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```ts
|
||||
// tests/seed.spec.ts
|
||||
import { test } from './fixtures';
|
||||
|
||||
test('seed', async ({ page }) => {
|
||||
// Fixture already navigates. This empty body tells agents where to start.
|
||||
});
|
||||
```
|
||||
|
||||
If no seed exists, create one that at least navigates to the app.
|
||||
|
||||
### 1.3 Explore the app
|
||||
|
||||
Launch the app via the seed in the background and attach:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test tests/seed.spec.ts --debug=cli
|
||||
# wait for "Debugging Instructions" and the session name tw-XXXX
|
||||
playwright-cli attach tw-XXXX
|
||||
```
|
||||
|
||||
Resume so the seed runs, then probe the app:
|
||||
|
||||
```bash
|
||||
playwright-cli resume # resume so that seed test runs fully
|
||||
playwright-cli snapshot # inventory of interactive elements
|
||||
playwright-cli click e5 # follow a flow
|
||||
playwright-cli eval "location.href" # read URL / state
|
||||
playwright-cli show --annotate # ask the user to point at something
|
||||
```
|
||||
|
||||
Map out:
|
||||
|
||||
- Interactive surfaces (forms, buttons, lists, filters, modals).
|
||||
- Primary user journeys end-to-end.
|
||||
- Edge cases: empty states, validation errors, very long input, boundary values.
|
||||
- Persistence: reload, local/session storage, URL fragments.
|
||||
- Navigation: which controls change the URL, back/forward behaviour.
|
||||
|
||||
**Important**: Do not just open the app url with playwright-cli, always go through the test to capture any custom setup done there.
|
||||
**Important**: Stop the background test when done exploring.
|
||||
|
||||
### 1.4 Write the spec file
|
||||
|
||||
Save under `specs/<feature>.plan.md`. Use this structure:
|
||||
|
||||
```markdown
|
||||
# <Feature> Test Plan
|
||||
|
||||
## Application Overview
|
||||
|
||||
<One paragraph describing what the feature does and why it matters.>
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. <Group Name>
|
||||
|
||||
**Seed:** `tests/seed.spec.ts`
|
||||
|
||||
#### 1.1. <kebab-case-scenario-name>
|
||||
|
||||
**File:** `tests/<group>/<kebab-case-scenario-name>.spec.ts`
|
||||
|
||||
**Steps:**
|
||||
1. <Concrete user step>
|
||||
- expect: <observable outcome>
|
||||
- expect: <another observable outcome>
|
||||
2. <Next step>
|
||||
- expect: <outcome>
|
||||
|
||||
#### 1.2. <next-scenario>
|
||||
...
|
||||
|
||||
### 2. <Next Group>
|
||||
|
||||
**Seed:** `tests/seed.spec.ts`
|
||||
...
|
||||
```
|
||||
|
||||
Guidelines:
|
||||
|
||||
- Each scenario is independent and starts from the seed's fresh state — never chain scenarios.
|
||||
- Scenario names are kebab-case and match the test file name (`should-add-single-todo` → `should-add-single-todo.spec.ts`).
|
||||
- Cover happy path, edge cases, validation, negative flows, persistence.
|
||||
- Write steps at the user level ("Type 'Buy milk' into the input"), not the API level ("call `fill`").
|
||||
- Put observable outcomes in `- expect:` bullets; each becomes an assertion during generation.
|
||||
|
||||
---
|
||||
|
||||
## 2. Generate
|
||||
|
||||
Goal: take a spec file and produce Playwright test files. Optionally update the spec if it has drifted.
|
||||
|
||||
### 2.1 Inputs
|
||||
|
||||
- **Spec file**, e.g. `specs/basic-operations.plan.md`.
|
||||
- **Target**: either a single scenario (e.g. `1.2`), a whole group (`1`), or all.
|
||||
- **Seed file**, read from the `**Seed:**` line of the scenario's group.
|
||||
|
||||
### 2.2 Generate one scenario
|
||||
|
||||
For each target scenario, in sequence (never in parallel — scenarios share the seed session):
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test <seed-file> --debug=cli # background
|
||||
playwright-cli attach tw-XXXX
|
||||
# resume
|
||||
```
|
||||
|
||||
**Do not** just open the app url with playwright-cli, always go through the test to capture any custom setup done there.
|
||||
|
||||
Walk the scenario's `Steps:` one by one with `playwright-cli`, treating the spec as the plan and the live app as the source of truth. If a step is vague ("click the button" — which button?), references an element that no longer exists, or contradicts the app's actual behaviour, use your judgement: update the spec to match what the app really does, then keep going. Editing the spec mid-generation is expected.
|
||||
|
||||
Every action prints the equivalent Playwright TypeScript (see [How generation works](#0-how-generation-works)):
|
||||
|
||||
```bash
|
||||
playwright-cli snapshot # find refs
|
||||
playwright-cli fill e3 "John Doe" # -> page.getByRole('textbox', {...}).fill(...)
|
||||
playwright-cli press Enter
|
||||
playwright-cli click e7
|
||||
```
|
||||
|
||||
For each `- expect:` bullet, add an explicit assertion. See [How generation works](#0-how-generation-works) for details.
|
||||
|
||||
Collect the generated code and write the test file at the path given in the spec:
|
||||
|
||||
```ts
|
||||
// spec: specs/basic-operations.plan.md
|
||||
// seed: tests/seed.spec.ts
|
||||
import { test, expect } from './fixtures'; // or '@playwright/test' if no fixtures file
|
||||
|
||||
test.describe('Signing in and out', () => {
|
||||
test('should sign in', async ({ page }) => {
|
||||
// 1. Navigate to the application
|
||||
// (handled by the seed fixture)
|
||||
|
||||
// 2. Type 'John Doe' into the username field
|
||||
await page.getByRole('textbox', { name: 'username' }).fill('John Doe');
|
||||
|
||||
// 3. Type password
|
||||
await page.getByRole('textbox', { name: 'password' }).fill('TestPassword');
|
||||
|
||||
// 4. Press Enter to submit
|
||||
await page.getByRole('textbox', { name: 'password' }).press('Enter');
|
||||
|
||||
await expect(page.getByRole('heading')).toContainText('Welcome, John Doe!');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- **One test per file.** File path, describe name, and test name come verbatim from the spec (minus the ordinal).
|
||||
- Prefix each numbered step with a `// N. <step text>` comment before its actions.
|
||||
- Use the describe group name verbatim from the spec (no `1.` ordinal).
|
||||
- Import from `./fixtures` if the project has one; otherwise `@playwright/test`.
|
||||
- **Important**: close the CLI session and stop the background test before moving to the next scenario.
|
||||
|
||||
### 2.3 Generate multiple scenarios
|
||||
|
||||
Loop 2.2 over the targeted scenarios one at a time, restarting the seed between each so every test starts from a clean page. This is safe to parallelise due to unique generated session names - just make sure each test run is stopped.
|
||||
|
||||
### 2.4 Run generated tests
|
||||
|
||||
After generation, run the new tests once:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test tests/<group>/<scenario>.spec.ts
|
||||
```
|
||||
|
||||
Any failure goes to Section 3.
|
||||
|
||||
---
|
||||
|
||||
## 3. Heal
|
||||
|
||||
Goal: fix failing tests, and update the spec if the app's intended behaviour changed.
|
||||
|
||||
### 3.1 Find failing tests
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test
|
||||
```
|
||||
|
||||
Record the list of failing `<file>:<line>` entries and process them one at a time. Do not attempt parallel fixes — shared state and the single CLI session make that fragile.
|
||||
|
||||
### 3.2 Debug one failure
|
||||
|
||||
Run the single failing test in debug mode in the background, then attach:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_HTML_OPEN=never npx playwright test tests/<group>/<scenario>.spec.ts:<line> --debug=cli
|
||||
# wait for "Debugging Instructions" and the tw-XXXX session name
|
||||
playwright-cli attach tw-XXXX
|
||||
```
|
||||
|
||||
The test is paused at the start. Step forward or run to until just before the failing action or assertion, then diagnose:
|
||||
|
||||
```bash
|
||||
playwright-cli snapshot # did the element change / move / rename?
|
||||
playwright-cli console # app-side errors?
|
||||
playwright-cli requests # failed request? wrong payload?
|
||||
playwright-cli show --annotate # ask the user to point somewhere
|
||||
```
|
||||
|
||||
Common causes: selector drift, new wrapper element, label/ARIA rename, timing (transition, async load), assertion text updated in the app, test data leaking between runs.
|
||||
|
||||
Rehearse the corrected interaction with `playwright-cli` — the generated code in the output is what you paste back into the test.
|
||||
|
||||
### 3.3 Apply the fix
|
||||
|
||||
Edit the test file: update the locator, assertion, step order, or inputs to match the corrected behaviour. Stop the background debug run. Rerun the single test to confirm green.
|
||||
|
||||
Never skip hooks or add sleeps as a fix. Never use `networkidle`.
|
||||
|
||||
### 3.4 Reconcile with the spec
|
||||
|
||||
Open the spec referenced by the `// spec:` header in the test file and locate the scenario that matches the test.
|
||||
|
||||
- **Fix was purely technical** (locator drift, better assertion shape) and the spec's user-level behaviour still matches the app → leave the spec alone.
|
||||
- **Fix changed user-visible steps, inputs, order, or expected outcomes** that the spec describes → update the spec to match reality. Keep the scenario id and file path stable; only the step / expect lines change.
|
||||
- **Unclear whether the app change is intentional** (spec is stale) **or a regression** (test was right, app is wrong) → **stop and ask the user**. Provide:
|
||||
- the scenario id (e.g. `2.3`),
|
||||
- the spec lines that no longer match,
|
||||
- the observed app behaviour (quote a snapshot excerpt or a concrete outcome).
|
||||
|
||||
Only after the user answers, either update the spec (intentional change) or file/flag the test as covering a bug (regression).
|
||||
|
||||
### 3.5 Iteration and giving up
|
||||
|
||||
- Fix failures one at a time; rerun after each.
|
||||
- If after thorough investigation you are confident the test is correct but the app is wrong *and* the user has confirmed it's a bug: mark the test `test.fixme(...)` with a comment pointing at the user's decision or issue link. Never silently skip.
|
||||
|
||||
---
|
||||
|
||||
## Cross-references
|
||||
|
||||
| For... | See |
|
||||
|---|---|
|
||||
| `--debug=cli` / attach mechanics | [playwright-tests.md](playwright-tests.md) |
|
||||
| Mocking requests during exploration/generation | [request-mocking.md](request-mocking.md) |
|
||||
| Managing the CLI browser session | [session-management.md](session-management.md) |
|
||||
139
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/tracing.md
generated
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
# Tracing
|
||||
|
||||
Capture detailed execution traces for debugging and analysis. Traces include DOM snapshots, screenshots, network activity, and console logs.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
# Start trace recording
|
||||
playwright-cli tracing-start
|
||||
|
||||
# Perform actions
|
||||
playwright-cli open https://example.com
|
||||
playwright-cli click e1
|
||||
playwright-cli fill e2 "test"
|
||||
|
||||
# Stop trace recording
|
||||
playwright-cli tracing-stop
|
||||
```
|
||||
|
||||
## Trace Output Files
|
||||
|
||||
When you start tracing, Playwright creates a `traces/` directory with several files:
|
||||
|
||||
### `trace-{timestamp}.trace`
|
||||
|
||||
**Action log** - The main trace file containing:
|
||||
- Every action performed (clicks, fills, navigations)
|
||||
- DOM snapshots before and after each action
|
||||
- Screenshots at each step
|
||||
- Timing information
|
||||
- Console messages
|
||||
- Source locations
|
||||
|
||||
### `trace-{timestamp}.network`
|
||||
|
||||
**Network log** - Complete network activity:
|
||||
- All HTTP requests and responses
|
||||
- Request headers and bodies
|
||||
- Response headers and bodies
|
||||
- Timing (DNS, connect, TLS, TTFB, download)
|
||||
- Resource sizes
|
||||
- Failed requests and errors
|
||||
|
||||
### `resources/`
|
||||
|
||||
**Resources directory** - Cached resources:
|
||||
- Images, fonts, stylesheets, scripts
|
||||
- Response bodies for replay
|
||||
- Assets needed to reconstruct page state
|
||||
|
||||
## What Traces Capture
|
||||
|
||||
| Category | Details |
|
||||
|----------|---------|
|
||||
| **Actions** | Clicks, fills, hovers, keyboard input, navigations |
|
||||
| **DOM** | Full DOM snapshot before/after each action |
|
||||
| **Screenshots** | Visual state at each step |
|
||||
| **Network** | All requests, responses, headers, bodies, timing |
|
||||
| **Console** | All console.log, warn, error messages |
|
||||
| **Timing** | Precise timing for each operation |
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Debugging Failed Actions
|
||||
|
||||
```bash
|
||||
playwright-cli tracing-start
|
||||
playwright-cli open https://app.example.com
|
||||
|
||||
# This click fails - why?
|
||||
playwright-cli click e5
|
||||
|
||||
playwright-cli tracing-stop
|
||||
# Open trace to see DOM state when click was attempted
|
||||
```
|
||||
|
||||
### Analyzing Performance
|
||||
|
||||
```bash
|
||||
playwright-cli tracing-start
|
||||
playwright-cli open https://slow-site.com
|
||||
playwright-cli tracing-stop
|
||||
|
||||
# View network waterfall to identify slow resources
|
||||
```
|
||||
|
||||
### Capturing Evidence
|
||||
|
||||
```bash
|
||||
# Record a complete user flow for documentation
|
||||
playwright-cli tracing-start
|
||||
|
||||
playwright-cli open https://app.example.com/checkout
|
||||
playwright-cli fill e1 "4111111111111111"
|
||||
playwright-cli fill e2 "12/25"
|
||||
playwright-cli fill e3 "123"
|
||||
playwright-cli click e4
|
||||
|
||||
playwright-cli tracing-stop
|
||||
# Trace shows exact sequence of events
|
||||
```
|
||||
|
||||
## Trace vs Video vs Screenshot
|
||||
|
||||
| Feature | Trace | Video | Screenshot |
|
||||
|---------|-------|-------|------------|
|
||||
| **Format** | .trace file | .webm video | .png/.jpeg image |
|
||||
| **DOM inspection** | Yes | No | No |
|
||||
| **Network details** | Yes | No | No |
|
||||
| **Step-by-step replay** | Yes | Continuous | Single frame |
|
||||
| **File size** | Medium | Large | Small |
|
||||
| **Best for** | Debugging | Demos | Quick capture |
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Start Tracing Before the Problem
|
||||
|
||||
```bash
|
||||
# Trace the entire flow, not just the failing step
|
||||
playwright-cli tracing-start
|
||||
playwright-cli open https://example.com
|
||||
# ... all steps leading to the issue ...
|
||||
playwright-cli tracing-stop
|
||||
```
|
||||
|
||||
### 2. Clean Up Old Traces
|
||||
|
||||
Traces can consume significant disk space:
|
||||
|
||||
```bash
|
||||
# Remove traces older than 7 days
|
||||
find .playwright-cli/traces -mtime +7 -delete
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- Traces add overhead to automation
|
||||
- Large traces can consume significant disk space
|
||||
- Some dynamic content may not replay perfectly
|
||||
143
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-cli/references/video-recording.md
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
# Video Recording
|
||||
|
||||
Capture browser automation sessions as video for debugging, documentation, or verification. Produces WebM (VP8/VP9 codec).
|
||||
|
||||
## Basic Recording
|
||||
|
||||
```bash
|
||||
# Open browser first
|
||||
playwright-cli open
|
||||
|
||||
# Start recording
|
||||
playwright-cli video-start demo.webm
|
||||
|
||||
# Add a chapter marker for section transitions
|
||||
playwright-cli video-chapter "Getting Started" --description="Opening the homepage" --duration=2000
|
||||
|
||||
# Navigate and perform actions
|
||||
playwright-cli goto https://example.com
|
||||
playwright-cli snapshot
|
||||
playwright-cli click e1
|
||||
|
||||
# Add another chapter
|
||||
playwright-cli video-chapter "Filling Form" --description="Entering test data" --duration=2000
|
||||
playwright-cli fill e2 "test input"
|
||||
|
||||
# Stop and save
|
||||
playwright-cli video-stop
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Use Descriptive Filenames
|
||||
|
||||
```bash
|
||||
# Include context in filename
|
||||
playwright-cli video-start recordings/login-flow-2024-01-15.webm
|
||||
playwright-cli video-start recordings/checkout-test-run-42.webm
|
||||
```
|
||||
|
||||
### 2. Record entire hero scripts.
|
||||
|
||||
When recording a video for the user or as a proof of work, it is best to create a code snippet and execute it with run-code.
|
||||
It allows inserting appropriate pauses between the actions and annotating the video. There are new Playwright APIs for that.
|
||||
|
||||
1) Perform scenario using CLI and take note of all locators and actions. You'll need those locators to request their bounding boxes for highlight.
|
||||
2) Create a file with the intended script for video (below). Use pressSequentially w/ delay for nice typing, make reasonable pauses.
|
||||
3) Use playwright-cli run-code --filename your-script.js
|
||||
|
||||
**Important**: Overlays are `pointer-events: none` — they do not interfere with page interactions. You can safely keep sticky overlays visible while clicking, filling, or performing any actions on the page.
|
||||
|
||||
```js
|
||||
async page => {
|
||||
await page.screencast.start({ path: 'video.webm', size: { width: 1280, height: 800 } });
|
||||
await page.goto('https://demo.playwright.dev/todomvc');
|
||||
|
||||
// Show a chapter card — blurs the page and shows a dialog.
|
||||
// Blocks until duration expires, then auto-removes.
|
||||
// Use this for simple use cases, but always feel free to hand-craft your own beautiful
|
||||
// overlay via await page.screencast.showOverlay().
|
||||
await page.screencast.showChapter('Adding Todo Items', {
|
||||
description: 'We will add several items to the todo list.',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// Perform action
|
||||
await page.getByRole('textbox', { name: 'What needs to be done?' }).pressSequentially('Walk the dog', { delay: 60 });
|
||||
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Show next chapter
|
||||
await page.screencast.showChapter('Verifying Results', {
|
||||
description: 'Checking the item appeared in the list.',
|
||||
duration: 2000,
|
||||
});
|
||||
|
||||
// Add a sticky annotation that stays while you perform actions.
|
||||
// Overlays are pointer-events: none, so they won't block clicks.
|
||||
const annotation = await page.screencast.showOverlay(`
|
||||
<div style="position: absolute; top: 8px; right: 8px;
|
||||
padding: 6px 12px; background: rgba(0,0,0,0.7);
|
||||
border-radius: 8px; font-size: 13px; color: white;">
|
||||
✓ Item added successfully
|
||||
</div>
|
||||
`);
|
||||
|
||||
// Perform more actions while the annotation is visible
|
||||
await page.getByRole('textbox', { name: 'What needs to be done?' }).pressSequentially('Buy groceries', { delay: 60 });
|
||||
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Remove the annotation when done
|
||||
await annotation.dispose();
|
||||
|
||||
// You can also highlight relevant locators and provide contextual annotations.
|
||||
const bounds = await page.getByText('Walk the dog').boundingBox();
|
||||
await page.screencast.showOverlay(`
|
||||
<div style="position: absolute;
|
||||
top: ${bounds.y}px;
|
||||
left: ${bounds.x}px;
|
||||
width: ${bounds.width}px;
|
||||
height: ${bounds.height}px;
|
||||
border: 1px solid red;">
|
||||
</div>
|
||||
<div style="position: absolute;
|
||||
top: ${bounds.y + bounds.height + 5}px;
|
||||
left: ${bounds.x + bounds.width / 2}px;
|
||||
transform: translateX(-50%);
|
||||
padding: 6px;
|
||||
background: #808080;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
color: white;">Check it out, it is right above this text
|
||||
</div>
|
||||
`, { duration: 2000 });
|
||||
|
||||
await page.screencast.stop();
|
||||
}
|
||||
```
|
||||
|
||||
Embrace creativity, overlays are powerful.
|
||||
|
||||
### Overlay API Summary
|
||||
|
||||
| Method | Use Case |
|
||||
|--------|----------|
|
||||
| `page.screencast.showChapter(title, { description?, duration?, styleSheet? })` | Full-screen chapter card with blurred backdrop — ideal for section transitions |
|
||||
| `page.screencast.showOverlay(html, { duration? })` | Custom HTML overlay — use for callouts, labels, highlights |
|
||||
| `disposable.dispose()` | Remove a sticky overlay added without duration |
|
||||
| `page.screencast.hideOverlays()` / `page.screencast.showOverlays()` | Temporarily hide/show all overlays |
|
||||
|
||||
## Tracing vs Video
|
||||
|
||||
| Feature | Video | Tracing |
|
||||
|---------|-------|---------|
|
||||
| Output | WebM file | Trace file (viewable in Trace Viewer) |
|
||||
| Shows | Visual recording | DOM snapshots, network, console, actions |
|
||||
| Use case | Demos, documentation | Debugging, analysis |
|
||||
| Size | Larger | Smaller |
|
||||
|
||||
## Limitations
|
||||
|
||||
- Recording adds slight overhead to automation
|
||||
- Large recordings can consume significant disk space
|
||||
143
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: playwright-component-testing
|
||||
description: Set up component testing with Playwright using a story gallery — scaffold stories and a gallery dev page driven by the built-in mount fixture, no dedicated component-testing runtime. Use when asked to test React or Vue components in isolation with Playwright, or to migrate off @playwright/experimental-ct-react / -vue.
|
||||
---
|
||||
|
||||
# Component Testing with Playwright
|
||||
|
||||
Test components with regular Playwright e2e tests against a small **story gallery** page hosted by the app's own dev server. No extra test runner, bundler integration or npm packages are required.
|
||||
|
||||
## Concept
|
||||
|
||||
- A **story** is a tiny wrapper component that embeds the component under test in one specific scenario: hard-coded props, mock data, providers, recorded callbacks. Stories live next to the component in `*.story.tsx` (or `.ts`/`.jsx`/`.js`/`.vue`) files; each named export is one story.
|
||||
- The **gallery** is a single page you implement to `references/gallery-spec.md`: it exposes `window.mount(params)` / `window.unmount()` that render a story — resolved from your story files (e.g. with `import.meta.glob`) — into `#root`. It is framework-specific and yours to own — there is no template to copy for it.
|
||||
- Tests are plain Playwright tests. The built-in **`mount(storyId, props?)` fixture** (from `@playwright/test`) drives the gallery's `window.mount` and returns a `Locator` for the gallery root (`#root`). Scope the queries from there — `component.getByRole('button').click()`, not `component.click()`. Nothing to scaffold for it.
|
||||
|
||||
Everything the component needs must be set up *inside the story* (it runs in the browser); everything the test asserts must be observable *through the page* (DOM, URL, network). Where the component takes callbacks, the story creates the state, provides the callbacks and records the state into a hidden form for the test to assert on. `mount(id, props)` passes plain serializable `props` to the story.
|
||||
|
||||
## Setup workflow
|
||||
|
||||
1. **Detect the framework and bundler.** React vs Vue decides the framework notes and example story to follow. Then:
|
||||
- **App runs on Vite** (has `vite.config.*`): the gallery is served by the existing dev server at `/playwright/gallery/index.html` — Vite serves any `.html` file under the project root, the app's plugins/aliases/CSS apply automatically, and `vite build` ignores it. No extra server needed.
|
||||
- **Anything else** (Next.js, webpack, no dev server): run a small standalone dev server (e.g. Vite) that serves the gallery page, and point `baseURL` at it. Requires `vite` and the framework plugin as devDependencies.
|
||||
2. **Implement the gallery** to `references/gallery-spec.md`: a page at `<project>/playwright/gallery/` that renders the requested story into `#root`. Start from the worked example in the spec and the framework notes in `references/react.md` / `references/vue.md`. Keep story discovery (`import.meta.glob`) and the framework mount here — this is the only framework-specific glue, so keep it small. Import the app's global CSS the same way the app's own entry does.
|
||||
3. **Configure Playwright** — add to `playwright.config.ts`:
|
||||
|
||||
```ts
|
||||
projects: [
|
||||
{
|
||||
name: 'components',
|
||||
testDir: './tests/components',
|
||||
use: { ...devices['Desktop Chrome'], baseURL: 'http://localhost:5173/playwright/gallery/index.html', serviceWorkers: 'block', reuseContext: true },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: 'npm run dev', // or: npx vite --config playwright/vite.config.ts
|
||||
url: 'http://localhost:5173/playwright/gallery/index.html', // standalone server: http://localhost:3100/playwright/gallery/index.html
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
```
|
||||
|
||||
Match the port to the dev server. `mount` navigates to `baseURL`, so set `baseURL` to the gallery's URL. `serviceWorkers: 'block'` keeps the app's own service worker from serving cached responses that would shadow your `page.route()` mocks. `reuseContext: true` reuses the browser context across tests in a worker (as the old component-testing runtime did) — a large speedup for component suites. If the config already has projects/webServer, merge instead of replacing.
|
||||
4. **Write a first story** next to an existing component, modeled on `templates/<react|vue>/Button.story.*`.
|
||||
5. **Write a first spec**, modeled on `templates/react/button.spec.ts`, importing `test`/`expect` from `@playwright/test`.
|
||||
6. **Run**: `npx playwright test --project=components`. Open `http://localhost:5173/playwright/gallery/index.html` in a browser to eyeball all stories.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Story id: path under `src/` without the `.story.*` extension, plus the export name — `src/components/Button.story.tsx` export `Primary` → `components/Button/Primary`. Any unique suffix works too: `mount('Button/Primary')`. A `.story.vue` single-file component is one story, addressable by its path alone (its `default` export).
|
||||
- One export per scenario. Prefer a new story export over parameterizing an existing one — stories are greppable, reviewable documentation of component states.
|
||||
|
||||
## Testing patterns
|
||||
|
||||
Examples are React; the Vue equivalents differ only in story syntax.
|
||||
|
||||
### Callbacks and events
|
||||
|
||||
**The story owns the state and provides the callbacks.** Where the component takes callbacks, create the state inside the story, wire the callbacks to it, and record the state into a hidden form next to the component. Tests perform operations and assert on the recorded values:
|
||||
|
||||
```tsx
|
||||
export const Stateful = () => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
return <>
|
||||
<Expandable expanded={expanded} setExpanded={setExpanded} title="Title">Details</Expandable>
|
||||
<form hidden><input data-testid="expanded" readOnly value={String(expanded)} /></form>
|
||||
</>;
|
||||
};
|
||||
```
|
||||
|
||||
```ts
|
||||
test('click should expand', async ({ mount }) => {
|
||||
const component = await mount('components/Expandable/Stateful');
|
||||
await component.locator('.codicon-chevron-right').click();
|
||||
await expect(component.getByTestId('expanded')).toHaveValue('true');
|
||||
});
|
||||
```
|
||||
|
||||
This keeps the whole scenario in the browser: no callback marshalling, the story doubles as documentation, and the recorded state is visible when eyeballing the gallery. Record each observed value in its own `data-testid` input (`String(...)` or `JSON.stringify(...)` for payloads) and assert with `toHaveValue()` — a web-first assertion that retries until the state lands. The negative direction works the same way: perform the operation, then assert the value did **not** change.
|
||||
|
||||
### Per-test props
|
||||
|
||||
When a scenario is genuinely parametric (e.g. a boundary-value sweep), pass props as the second argument to `mount`; the gallery hands them to the story as its props. Keep props to plain serializable data — callbacks belong inside the story.
|
||||
|
||||
```tsx
|
||||
export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
|
||||
<Button title={title} />;
|
||||
```
|
||||
|
||||
```ts
|
||||
const component = await mount('components/Button/WithTitle', { title: 'Hello' });
|
||||
```
|
||||
|
||||
`mount` is generic over the story: pass the story type as a template argument to type-check the props (and `update()`):
|
||||
|
||||
```ts
|
||||
import type { WithTitle } from './Button.story';
|
||||
|
||||
const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' });
|
||||
```
|
||||
|
||||
This works for React and Vue stories alike; Vue stories must additionally declare the props at runtime — see the `Typed props` sections in `references/react.md` / `references/vue.md`.
|
||||
|
||||
### Prop transitions with `update()`
|
||||
|
||||
To test how a component reacts to a prop change **without remounting** (state preserved), call `component.update(newProps)` — it re-renders the same story with new props on the existing root:
|
||||
|
||||
```ts
|
||||
const component = await mount('components/Counter/Default', { value: 1 });
|
||||
await expect(component.getByTestId('value')).toHaveText('1');
|
||||
await component.update({ value: 2 });
|
||||
await expect(component.getByTestId('value')).toHaveText('2');
|
||||
```
|
||||
|
||||
This requires the gallery to reuse its root/instance (`references/gallery-spec.md`); state survives as long as the story stays the same.
|
||||
|
||||
### Multiple states in one test
|
||||
|
||||
Each `mount()` navigates fresh, so tests are fully isolated and mounting several stories in one test is cheap:
|
||||
|
||||
```ts
|
||||
await expect(await mount('Button/Primary')).toHaveScreenshot('primary.png');
|
||||
await expect(await mount('Button/Disabled')).toHaveScreenshot('disabled.png');
|
||||
```
|
||||
|
||||
For visual comparison, screenshot the returned root locator (as above), not the page, to avoid asserting on browser chrome.
|
||||
|
||||
### Network mocking
|
||||
|
||||
Use `page.route()` as usual — register routes before `mount()`, since mounting navigates. `serviceWorkers: 'block'` (set in the config above) keeps the app's own service worker from serving cached responses that shadow the routes. Teams with MSW handler libraries can start the worker inside a story or decorator instead.
|
||||
|
||||
### Debugging stories
|
||||
|
||||
Open your gallery URL (`baseURL`) in a browser and call `await window.mount({ story: 'components/Button/Primary' })` from the devtools console — that is exactly what the `mount` fixture does. An unknown story rejects `window.mount`, which surfaces as the test's `mount()` throwing with a real stack. To browse without the console, give your gallery an optional index page.
|
||||
|
||||
## Decision points
|
||||
|
||||
- **Monorepos / non-`src` layouts**: change the glob and the id derivation in your gallery (`references/gallery-spec.md`) to match.
|
||||
- **Global providers** (theme, i18n, store, router): create a shared `decorator` helper next to the gallery and wrap components in stories; see `references/react.md` / `references/vue.md`.
|
||||
## References
|
||||
|
||||
- `references/gallery-spec.md` — the gallery endpoint contract to implement (**start here**).
|
||||
- `references/react.md` — React walkthrough: providers, StrictMode, CSS.
|
||||
- `references/vue.md` — Vue walkthrough: `.story.ts` and `.story.vue` stories, plugins.
|
||||
- `references/migration.md` — migrating off `@playwright/experimental-ct-react` / `-vue`.
|
||||
144
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md
generated
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
# Gallery contract
|
||||
|
||||
The **gallery** is a single page, served by your dev server at the URL you set as `baseURL` in your
|
||||
Playwright config, that exposes two methods on `window` for Playwright to drive:
|
||||
|
||||
- `window.mount(params)` — render a story.
|
||||
- `window.unmount()` — unmount the current story.
|
||||
|
||||
The built-in `mount` fixture navigates to the gallery, then calls `window.mount` via
|
||||
`page.evaluate()`. Keep props to plain serializable data — where the component takes callbacks,
|
||||
the story creates the state, provides the callbacks and records the state into a hidden form for
|
||||
the test to assert on.
|
||||
|
||||
## `window.mount(params)`
|
||||
|
||||
`params` is `{ story, props }`, straight from the test's `mount(story, props)` call:
|
||||
|
||||
- `story` — the story id (string). Resolve it (see id grammar) to a component.
|
||||
- `props` — the plain serializable props object passed to the component.
|
||||
|
||||
Render the resolved component with `props` into `#root`. Return a `Promise` that resolves once the
|
||||
component is mounted and **rejects on failure** (unknown story, render throw). The rejection
|
||||
surfaces as the test's `await mount(...)` throwing, with a real stack — there is no HTTP-status or
|
||||
DOM-attribute signalling.
|
||||
|
||||
**Reuse the root across calls.** The test's `component.update(props)` calls `window.mount` again
|
||||
with the same story and new props, **without navigating**. If you render into the same root /
|
||||
instance rather than recreating it, the framework reconciles and component-internal state is
|
||||
preserved — that is CT's `update()`. Recreating the root each call (or navigating) resets state, so
|
||||
reuse it: create it on first mount and render into it on every call. The framework reconciles,
|
||||
remounting on its own only when the story (component type) changes.
|
||||
|
||||
**`window.mount` is your setup/teardown hook.** It is the browser-side equivalent of CT's
|
||||
`beforeMount` / `afterMount`: install providers or plugins, seed a store, start an in-browser mock
|
||||
server *before* you render, and run post-render work *after* — all inside this one function,
|
||||
branched on the `story` / `props` the test passed. There is no separate hook registry; the function
|
||||
you own is the hook.
|
||||
|
||||
## `window.unmount()`
|
||||
|
||||
Unmount the current story from `#root` and return a `Promise`. The test calls it via
|
||||
`component.unmount()`. Needed only to assert teardown/cleanup effects — each `mount` navigates
|
||||
fresh, so tests are already isolated.
|
||||
|
||||
## `#root`
|
||||
|
||||
Render the component into an element with `id="root"`. `mount` returns a `Locator` for `#root`
|
||||
itself, so tests scope their queries from there — `component.getByRole('button').click()`, not
|
||||
`component.click()`. Stories are free to render fragments, e.g. the component plus a hidden form
|
||||
recording its state.
|
||||
|
||||
## Story id grammar (recommended)
|
||||
|
||||
The gallery owns resolution; `mount` passes the id through untouched. Recommended scheme:
|
||||
|
||||
- `<path under src, without the .story.* extension>/<ExportName>` — e.g.
|
||||
`src/components/Button.story.tsx` export `Primary` → `components/Button/Primary`.
|
||||
- Any unique trailing suffix resolves too: `Button/Primary`.
|
||||
- A single-file-component story (`Button.story.vue`) is one story, addressed by its path alone
|
||||
(its default export): `components/Button`.
|
||||
|
||||
## Worked example (React + Vite SPA)
|
||||
|
||||
An illustration of the contract, **not** a file to copy — implement the equivalent for your stack.
|
||||
`import.meta.glob` stays inline here: Vite analyzes it statically, relative to this file, so it
|
||||
cannot be moved into shared/shipped code. That is exactly why the gallery is yours to own.
|
||||
|
||||
```tsx
|
||||
// playwright/gallery/main.tsx
|
||||
import { flushSync } from 'react-dom';
|
||||
import { createRoot, type Root } from 'react-dom/client';
|
||||
|
||||
const stories = import.meta.glob('../../src/**/*.story.{tsx,jsx}');
|
||||
const id = (f: string) => f.replace(/^(\.\.\/)+src\//, '').replace(/\.story\.\w+$/, '');
|
||||
|
||||
async function resolve(storyId: string) {
|
||||
const sep = storyId.lastIndexOf('/');
|
||||
const [path, name] = [storyId.slice(0, sep), storyId.slice(sep + 1)];
|
||||
const file = Object.keys(stories).find(f => id(f) === path || id(f).endsWith('/' + path));
|
||||
const mod = (file && await stories[file]()) as Record<string, any> | undefined;
|
||||
return mod?.[name] ?? mod?.default;
|
||||
}
|
||||
|
||||
const rootEl = document.getElementById('root')!;
|
||||
let root: Root | undefined;
|
||||
|
||||
(window as any).mount = async ({ story, props }: { story: string, props?: Record<string, any> }) => {
|
||||
const Story = await resolve(story);
|
||||
if (!Story)
|
||||
throw new Error(`Unknown story: ${story}`);
|
||||
root ??= createRoot(rootEl); // reuse the root so update() reconciles and preserves state
|
||||
// flushSync so a render error rejects the promise instead of being swallowed.
|
||||
flushSync(() => root!.render(<Story {...props} />));
|
||||
};
|
||||
|
||||
(window as any).unmount = async () => {
|
||||
root?.unmount();
|
||||
root = undefined;
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- playwright/gallery/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
```
|
||||
|
||||
## Vue variant (state-preserving)
|
||||
|
||||
Vue's `createApp(...).mount()` builds a fresh instance each call, so mount a small **reactive host**
|
||||
once and update its refs — updating them re-renders in place, which is what preserves state across
|
||||
`update()`:
|
||||
|
||||
```ts
|
||||
// playwright/gallery/main.ts
|
||||
import { createApp, h, shallowRef, type App, type Component } from 'vue';
|
||||
|
||||
// resolve() and the import.meta.glob are the same as the React example.
|
||||
const story = shallowRef<Component | null>(null);
|
||||
const props = shallowRef<Record<string, any>>({});
|
||||
const host = { render: () => (story.value ? h(story.value, props.value) : null) };
|
||||
let app: App | undefined;
|
||||
|
||||
(window as any).mount = async ({ story: id, props: next }: { story: string, props?: Record<string, any> }) => {
|
||||
const resolved = await resolve(id);
|
||||
if (!resolved)
|
||||
throw new Error(`Unknown story: ${id}`);
|
||||
story.value = resolved;
|
||||
props.value = next ?? {};
|
||||
if (!app) { // mount once; the ref updates above re-render in place
|
||||
app = createApp(host);
|
||||
app.mount('#root');
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).unmount = async () => {
|
||||
app?.unmount();
|
||||
app = undefined;
|
||||
};
|
||||
```
|
||||
|
||||
Keep the story-resolution glob and the framework mount in this file; everything
|
||||
else lives in your stories and tests.
|
||||
85
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
# Migrating from @playwright/experimental-ct-react / -vue
|
||||
|
||||
The CT packages compiled JSX in the test file and marshalled it into the browser. The gallery
|
||||
pattern moves the scenario into a story export that runs natively in the browser: structure (which
|
||||
component, its children, providers) plus behavior (state and callbacks, recorded into a hidden
|
||||
form for the test to assert on). Plain data props travel through `mount(storyId, props)`;
|
||||
`update()` and `unmount()` work as before.
|
||||
|
||||
## Concept mapping
|
||||
|
||||
| `@playwright/experimental-ct-*` | Gallery pattern |
|
||||
|---|---|
|
||||
| `mount(<Button title="…" onClick={spy} />)` | Stateful story: the story provides `onClick`, records the effect into a hidden form input; the test asserts `toHaveValue()` |
|
||||
| Plain data props from the test | Unchanged in spirit: `mount(id, props)` |
|
||||
| JSX children / slots from the test | Cannot cross — bake each composition into its own story export (Vue: `.story.vue` for slot-heavy scenarios) |
|
||||
| `component.update(<Button count={2} />)` | `component.update({ count: 2 })` — state-preserving, needs the gallery to reuse its root (`gallery-spec.md`) |
|
||||
| `component.unmount()` | `component.unmount()` — backed by the gallery's `window.unmount()` |
|
||||
| `beforeMount`/`afterMount` in `playwright/index.ts` | The body of the gallery's `window.mount` (global), or story decorators (per-story) |
|
||||
| `hooksConfig` per-test variation | Props: `mount('App/Routing', { route: '/dashboard' })` — the story/decorator interprets them |
|
||||
| `router` fixture / MSW handlers in Node | `page.route()` in the test, or MSW `setupWorker` inside a story/decorator |
|
||||
| `playwright/index.html` (styles, fonts, theme) | The gallery's `index.html` / entry module imports |
|
||||
| `ctViteConfig`, `ctPort`, `ctTemplateDir`, `ctCacheDir` | Gone — the gallery runs through the app's own dev server; port lives in `webServer` + `baseURL`; location is `playwright/gallery/` |
|
||||
| `defineConfig` from `@playwright/experimental-ct-react` | Plain `defineConfig` from `@playwright/test`, with `baseURL` = gallery URL, `serviceWorkers: 'block'`, `reuseContext: true` (see `SKILL.md`) |
|
||||
|
||||
## Steps
|
||||
|
||||
1. Set up the gallery and config per `SKILL.md`. Keep the old CT project running until the last
|
||||
spec is migrated.
|
||||
2. For each CT spec, split every `mount(<…/>)` call: JSX structure becomes a story export next to
|
||||
the component; plain data props stay in the test as `mount`'s second argument. Callback spies
|
||||
become story state recorded into a hidden form. A call site that only varies data props usually
|
||||
needs just one generic story that spreads them:
|
||||
`export const Default = (props: ButtonProps) => <Button title="Submit" {...props} />`.
|
||||
3. Rewrite the spec: import `test`/`expect` from `@playwright/test`; `mount(<X a={1}/>)`
|
||||
→ `mount('X/Default', { a: 1 })`; `update(<X a={2}/>)` → `update({ a: 2 })`;
|
||||
`unmount()` unchanged. `mount` returns a locator for the gallery root — scope the queries:
|
||||
`component.getByRole('button').click()`. Spy assertions become `toHaveValue()` on the story's
|
||||
recorded state.
|
||||
4. Port `beforeMount` hooks: app-wide setup into the gallery's `window.mount`; per-test
|
||||
`hooksConfig` branches into props interpreted by a story or decorator.
|
||||
5. When all specs are green, delete the CT project from the config, drop the
|
||||
`@playwright/experimental-ct-*` dependency, and remove `playwright/index.html`,
|
||||
`playwright/index.ts*` and `playwright/.cache`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Story ids are strings.** Renaming or moving a story breaks specs at runtime, not compile time
|
||||
— and the suffix-matching resolution can silently match a different story after a rename.
|
||||
- **Per-test JSX is gone.** Any test that built a different JSX tree per test (children matrices,
|
||||
inline wrappers) becomes one story export per composition.
|
||||
|
||||
## Before / after
|
||||
|
||||
```tsx
|
||||
// Before (CT)
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import Button from '../src/components/Button';
|
||||
|
||||
test('click', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount(<Button title="Submit" onClick={data => messages.push(data)} />);
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
});
|
||||
```
|
||||
|
||||
```tsx
|
||||
// After: src/components/Button.story.tsx
|
||||
import Button from './Button';
|
||||
|
||||
export const Default = (props: { onClick?: (data: string) => void }) =>
|
||||
<Button title="Submit" {...props} />;
|
||||
```
|
||||
|
||||
```ts
|
||||
// After: src/components/Button.spec.ts
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('click', async ({ mount }) => {
|
||||
const messages: string[] = [];
|
||||
const component = await mount('components/Button/Default', { onClick: (data: string) => messages.push(data) });
|
||||
await component.click();
|
||||
expect(messages).toEqual(['hello']);
|
||||
});
|
||||
```
|
||||
67
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# React setup
|
||||
|
||||
Follow the setup workflow in `SKILL.md`. Implement the gallery per `references/gallery-spec.md` (which has a React worked example); this page covers React-specific details.
|
||||
|
||||
## Files
|
||||
|
||||
- `playwright/gallery/` — the gallery you implement to `references/gallery-spec.md` (an `index.html` + a `main.tsx` module). Requires `react` and `react-dom` 18+ (`createRoot`).
|
||||
- Stories: `src/**/*.story.tsx` (the glob also picks up `.story.jsx`); example in `templates/react/Button.story.tsx`.
|
||||
|
||||
## StrictMode
|
||||
|
||||
Wrap the rendered story in `<React.StrictMode>` in your gallery to match how most apps render. In development builds StrictMode intentionally double-invokes render functions and effects. This matters for stories that record events with counters set in effects — recording via state updates from event handlers (like the `CountsClicks` example story) is unaffected. If a story misbehaves under StrictMode, that is usually a real finding about the component; drop the wrapper only if the app itself does not use StrictMode.
|
||||
|
||||
## Global providers
|
||||
|
||||
If components require context (theme, store, i18n, router), create one shared decorator and use it in stories, so each story states its scenario and nothing more:
|
||||
|
||||
```tsx
|
||||
// src/stories/decorators.tsx
|
||||
import { ThemeProvider } from '../theme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export function AppScaffold({ children, route = '/' }: { children: React.ReactNode, route?: string }) {
|
||||
return (
|
||||
<ThemeProvider theme="light">
|
||||
<MemoryRouter initialEntries={[route]}>{children}</MemoryRouter>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// src/components/ProfilePage.story.tsx
|
||||
export const LoggedIn = () => (
|
||||
<AppScaffold route="/profile/42">
|
||||
<ProfilePage user={{ id: 42, name: 'Test User' }} />
|
||||
</AppScaffold>
|
||||
);
|
||||
```
|
||||
|
||||
Do not build the decorator into the gallery — keeping it in story files makes the wrapping visible and lets stories opt out.
|
||||
|
||||
## Typed props
|
||||
|
||||
`mount` is generic over the story: pass the story type as a template argument to type-check per-test props (and `update()`). Props are inferred from the component signature; function and class components both work.
|
||||
|
||||
```tsx
|
||||
// src/components/Button.story.tsx
|
||||
export const WithTitle = ({ title = 'Default' }: { title?: string }) =>
|
||||
<Button title={title} />;
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/components/button.spec.ts
|
||||
import type { WithTitle } from './Button.story';
|
||||
|
||||
const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' });
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
- Global stylesheets: import them in your gallery entry (`playwright/gallery/main.tsx`, e.g. `import '../../src/index.css'`), mirroring the app's own entry point.
|
||||
- Tailwind: if content scanning is path-based, make sure `*.story.tsx` files are covered.
|
||||
|
||||
## Data fetching
|
||||
|
||||
For libraries with client objects (React Query, Apollo), create the client inside the story or decorator so each navigation starts fresh.
|
||||
75
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
# Vue setup
|
||||
|
||||
Follow the setup workflow in `SKILL.md`. Implement the gallery per `references/gallery-spec.md`, exposing `window.mount`/`window.unmount` — in Vue, mount with `app = createApp(h(story, props)); app.mount('#root')` and unmount with `app.unmount()`; this page covers Vue-specific details.
|
||||
|
||||
## Files
|
||||
|
||||
- `playwright/gallery/` — the gallery you implement to `references/gallery-spec.md` (an `index.html` + a `main.ts` module). Requires Vue 3.
|
||||
- Stories: `src/**/*.story.{ts,js,vue}`; example in `templates/vue/Button.story.ts`.
|
||||
|
||||
## Two ways to write stories
|
||||
|
||||
**Render-function stories** (`Button.story.ts`) — several scenarios per file, one named export each; see `templates/vue/Button.story.ts`. Uses `defineComponent` + `h()`, no SFC compilation involved.
|
||||
|
||||
**Single-file-component stories** (`Button.primary.story.vue`) — one story per file, full template syntax:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import Button from './Button.vue';
|
||||
const clicks = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button title="Submit" @click="clicks++" />
|
||||
<form hidden><input data-testid="click-count" readonly :value="String(clicks)" /></form>
|
||||
</template>
|
||||
```
|
||||
|
||||
An SFC story is addressed by its path without the extension: `mount('components/Button.primary')`. Prefer SFC stories when the scenario needs slots or non-trivial templates.
|
||||
|
||||
## Global plugins
|
||||
|
||||
Apps that rely on plugins (Pinia, vue-router, i18n) should wrap components with a decorator story helper that creates a fresh instance per story:
|
||||
|
||||
```ts
|
||||
// src/stories/decorators.ts
|
||||
import { defineComponent, h, type Component } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
export function withStore(story: Component) {
|
||||
return defineComponent(() => {
|
||||
const pinia = createPinia();
|
||||
return () => h(story, { pinia });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
For plugins that must be installed on the app instance (`app.use(...)`), add them in your gallery right after `createApp(...)` — that is the equivalent of the app's own bootstrap.
|
||||
|
||||
## Typed props
|
||||
|
||||
A story that takes per-test props declares them twice: in the setup signature (for the type) and in the `props` option (so Vue delivers them as props rather than `attrs`):
|
||||
|
||||
```ts
|
||||
// src/components/Button.story.ts
|
||||
export const WithTitle = defineComponent(
|
||||
(props: { title?: string }) => () => h(Button, { title: props.title ?? 'Default' }),
|
||||
{ props: ['title'] },
|
||||
);
|
||||
```
|
||||
|
||||
`mount` is generic over the story: pass the story type as a template argument to type-check the props (and `update()`):
|
||||
|
||||
```ts
|
||||
// src/components/button.spec.ts
|
||||
import type { WithTitle } from './Button.story';
|
||||
|
||||
const component = await mount<typeof WithTitle>('components/Button/WithTitle', { title: 'Hello' });
|
||||
```
|
||||
|
||||
Options-API stories (`defineComponent({ props: { ... } })`) infer props the same way. For `.story.vue` SFC stories, prop types are only inferable when the setup generates SFC types (Volar/vue-tsc); otherwise pass the props type directly: `mount<{ title?: string }>('components/Button.primary', { title: 'Hello' })`.
|
||||
|
||||
## CSS
|
||||
|
||||
Import global stylesheets in your gallery entry (`playwright/gallery/main.ts`, e.g. `import '../../src/assets/main.css'`), mirroring the app's entry point.
|
||||
171
tools/e2e/node_modules/playwright-core/lib/tools/skills/playwright-trace/SKILL.md
generated
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
---
|
||||
name: playwright-trace
|
||||
description: Inspect Playwright trace files from the command line — list actions, view requests, console, errors, snapshots and screenshots.
|
||||
allowed-tools: Bash(npx:*)
|
||||
---
|
||||
|
||||
# Playwright Trace CLI
|
||||
|
||||
Inspect `.zip` trace files produced by Playwright tests without opening a browser.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Start with `trace open <trace.zip>` to extract the trace and see its metadata.
|
||||
2. Use `trace actions` to see all actions with their action IDs.
|
||||
3. Use `trace action <action-id>` to drill into a specific action — see parameters, logs, source location, and available snapshots.
|
||||
4. Use `trace requests`, `trace console`, or `trace errors` for cross-cutting views.
|
||||
5. Use `trace snapshot <action-id>` to get the DOM snapshot, or run a browser command against it.
|
||||
6. Use `trace close` to remove the extracted trace data when done.
|
||||
|
||||
All commands after `open` operate on the currently opened trace — no need to pass the trace file again. Opening a new trace replaces the previous one.
|
||||
|
||||
## Commands
|
||||
|
||||
### Open a trace
|
||||
|
||||
```bash
|
||||
# Extract trace and show metadata: browser, viewport, duration, action/error counts
|
||||
npx playwright trace open <trace.zip>
|
||||
```
|
||||
|
||||
### Close a trace
|
||||
|
||||
```bash
|
||||
# Remove extracted trace data
|
||||
npx playwright trace close
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
```bash
|
||||
# List all actions as a tree with action IDs and timing
|
||||
npx playwright trace actions
|
||||
|
||||
# Filter by action title (regex, case-insensitive)
|
||||
npx playwright trace actions --grep "click"
|
||||
|
||||
# Only failed actions
|
||||
npx playwright trace actions --errors-only
|
||||
```
|
||||
|
||||
### Action details
|
||||
|
||||
```bash
|
||||
# Show full details for one action: params, result, logs, source, snapshots
|
||||
npx playwright trace action <action-id>
|
||||
```
|
||||
|
||||
The `action` command displays available snapshot phases (before, input, after) and the exact command to extract them.
|
||||
|
||||
### Requests
|
||||
|
||||
```bash
|
||||
# All network requests: method, status, URL, duration, size
|
||||
npx playwright trace requests
|
||||
|
||||
# Filter by URL pattern
|
||||
npx playwright trace requests --grep "api"
|
||||
|
||||
# Filter by HTTP method
|
||||
npx playwright trace requests --method POST
|
||||
|
||||
# Only failed requests (status >= 400)
|
||||
npx playwright trace requests --failed
|
||||
```
|
||||
|
||||
### Request details
|
||||
|
||||
```bash
|
||||
# Show full details for one request: headers, body, security
|
||||
npx playwright trace request <request-id>
|
||||
```
|
||||
|
||||
### Console
|
||||
|
||||
```bash
|
||||
# All console messages and stdout/stderr
|
||||
npx playwright trace console
|
||||
|
||||
# Only errors
|
||||
npx playwright trace console --errors-only
|
||||
|
||||
# Only browser console (no stdout/stderr)
|
||||
npx playwright trace console --browser
|
||||
|
||||
# Only stdout/stderr (no browser console)
|
||||
npx playwright trace console --stdio
|
||||
```
|
||||
|
||||
### Errors
|
||||
|
||||
```bash
|
||||
# All errors with stack traces and associated actions
|
||||
npx playwright trace errors
|
||||
```
|
||||
|
||||
### Snapshots
|
||||
|
||||
The `snapshot` command loads the DOM snapshot for an action into a headless browser and runs a single browser command against it. Without a browser command, it returns the accessibility snapshot.
|
||||
|
||||
```bash
|
||||
# Get the accessibility snapshot (default)
|
||||
npx playwright trace snapshot <action-id>
|
||||
|
||||
# Use a specific phase
|
||||
npx playwright trace snapshot <action-id> --name before
|
||||
|
||||
# Run eval to query the DOM
|
||||
npx playwright trace snapshot <action-id> -- eval "document.title"
|
||||
npx playwright trace snapshot <action-id> -- eval "document.querySelector('#error').textContent"
|
||||
|
||||
# Eval on a specific element ref (from the snapshot)
|
||||
npx playwright trace snapshot <action-id> -- eval "el => el.getAttribute('data-testid')" e5
|
||||
|
||||
# Take a screenshot of the snapshot
|
||||
npx playwright trace snapshot <action-id> -- screenshot
|
||||
|
||||
# Redirect output to a file
|
||||
npx playwright trace snapshot <action-id> -- eval "document.body.outerHTML" --filename=page.html
|
||||
npx playwright trace snapshot <action-id> -- screenshot --filename=screenshot.png
|
||||
```
|
||||
|
||||
Only three browser commands are useful on a frozen snapshot: `snapshot`, `eval`, and `screenshot`.
|
||||
|
||||
### Attachments
|
||||
|
||||
```bash
|
||||
# List all trace attachments
|
||||
npx playwright trace attachments
|
||||
|
||||
# Extract an attachment by its number
|
||||
npx playwright trace attachment 1
|
||||
npx playwright trace attachment 1 -o out.png
|
||||
```
|
||||
|
||||
## Typical investigation
|
||||
|
||||
```bash
|
||||
# 1. Open the trace and see what's inside
|
||||
npx playwright trace open test-results/my-test/trace.zip
|
||||
|
||||
# 2. What actions ran?
|
||||
npx playwright trace actions
|
||||
|
||||
# 3. Which action failed?
|
||||
npx playwright trace actions --errors-only
|
||||
|
||||
# 4. What went wrong?
|
||||
npx playwright trace action 12
|
||||
|
||||
# 5. What did the page look like at that moment?
|
||||
npx playwright trace snapshot 12
|
||||
|
||||
# 6. Query the DOM for more detail
|
||||
npx playwright trace snapshot 12 -- eval "document.querySelector('.error-message').textContent"
|
||||
|
||||
# 7. Any relevant network failures?
|
||||
npx playwright trace requests --failed
|
||||
|
||||
# 8. Any console errors?
|
||||
npx playwright trace console --errors-only
|
||||
```
|
||||
101
tools/e2e/node_modules/playwright-core/lib/tools/utils/extension.js
generated
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
"use strict";
|
||||
var __create = Object.create;
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __getProtoOf = Object.getPrototypeOf;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||||
// If the importer is in node compatibility mode or this is not an ESM
|
||||
// file that has been converted to a CommonJS file using a Babel-
|
||||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||||
mod
|
||||
));
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var extension_exports = {};
|
||||
__export(extension_exports, {
|
||||
findPlaywrightExtensionProfile: () => findPlaywrightExtensionProfile,
|
||||
isPlaywrightExtensionInstalled: () => isPlaywrightExtensionInstalled,
|
||||
playwrightExtensionId: () => playwrightExtensionId,
|
||||
playwrightExtensionInstallUrl: () => playwrightExtensionInstallUrl
|
||||
});
|
||||
module.exports = __toCommonJS(extension_exports);
|
||||
var import_fs = __toESM(require("fs"));
|
||||
var import_path = __toESM(require("path"));
|
||||
const playwrightExtensionId = "mmlmfjhmonkocbjadbfplnigmagldckm";
|
||||
const playwrightExtensionInstallUrl = `https://chromewebstore.google.com/detail/playwright-extension/${playwrightExtensionId}`;
|
||||
async function findPlaywrightExtensionProfile(userDataDir) {
|
||||
const profiles = await listProfileDirectories(userDataDir);
|
||||
const lastUsed = await readLastUsedProfile(userDataDir);
|
||||
const ordered = lastUsed && profiles.includes(lastUsed) ? [lastUsed, ...profiles.filter((profile) => profile !== lastUsed)] : profiles;
|
||||
for (const profile of ordered) {
|
||||
if (await isExtensionInstalledInProfile(import_path.default.join(userDataDir, profile)))
|
||||
return profile;
|
||||
}
|
||||
return void 0;
|
||||
}
|
||||
async function listProfileDirectories(userDataDir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = await import_fs.default.promises.readdir(userDataDir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const profiles = entries.filter((entry) => entry === "Default" || /^Profile \d+$/.test(entry));
|
||||
profiles.sort((a, b) => profileRank(a) - profileRank(b));
|
||||
return profiles;
|
||||
}
|
||||
function profileRank(profile) {
|
||||
return profile === "Default" ? -1 : parseInt(profile.slice("Profile ".length), 10);
|
||||
}
|
||||
async function readLastUsedProfile(userDataDir) {
|
||||
try {
|
||||
const localState = JSON.parse(await import_fs.default.promises.readFile(import_path.default.join(userDataDir, "Local State"), "utf-8"));
|
||||
const lastUsed = localState?.profile?.last_used;
|
||||
return typeof lastUsed === "string" ? lastUsed : void 0;
|
||||
} catch {
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
async function isPlaywrightExtensionInstalled(userDataDir) {
|
||||
return await findPlaywrightExtensionProfile(userDataDir) !== void 0;
|
||||
}
|
||||
async function isExtensionInstalledInProfile(profileDir) {
|
||||
if (await pathExists(import_path.default.join(profileDir, "Extensions", playwrightExtensionId)))
|
||||
return true;
|
||||
try {
|
||||
const prefs = await import_fs.default.promises.readFile(import_path.default.join(profileDir, "Preferences"), "utf-8");
|
||||
return prefs.includes(`"${playwrightExtensionId}"`);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function pathExists(p) {
|
||||
try {
|
||||
await import_fs.default.promises.access(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
findPlaywrightExtensionProfile,
|
||||
isPlaywrightExtensionInstalled,
|
||||
playwrightExtensionId,
|
||||
playwrightExtensionInstallUrl
|
||||
});
|
||||
108
tools/e2e/node_modules/playwright-core/lib/tools/utils/socketConnection.js
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
"use strict";
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
var socketConnection_exports = {};
|
||||
__export(socketConnection_exports, {
|
||||
SocketConnection: () => SocketConnection,
|
||||
compareSemver: () => compareSemver
|
||||
});
|
||||
module.exports = __toCommonJS(socketConnection_exports);
|
||||
class SocketConnection {
|
||||
constructor(socket) {
|
||||
this._pendingBuffers = [];
|
||||
this._socket = socket;
|
||||
socket.on("data", (buffer) => this._onData(buffer));
|
||||
socket.on("close", () => {
|
||||
this.onclose?.();
|
||||
});
|
||||
socket.on("error", (e) => console.error(`error: ${e.message}`));
|
||||
}
|
||||
async send(message) {
|
||||
await new Promise((resolve, reject) => {
|
||||
this._socket.write(`${JSON.stringify(message)}
|
||||
`, (error) => {
|
||||
if (error)
|
||||
reject(error);
|
||||
else
|
||||
resolve(void 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
close() {
|
||||
this._socket.destroy();
|
||||
}
|
||||
_onData(buffer) {
|
||||
let end = buffer.indexOf("\n");
|
||||
if (end === -1) {
|
||||
this._pendingBuffers.push(buffer);
|
||||
return;
|
||||
}
|
||||
this._pendingBuffers.push(buffer.slice(0, end));
|
||||
const message = Buffer.concat(this._pendingBuffers).toString();
|
||||
this._dispatchMessage(message);
|
||||
let start = end + 1;
|
||||
end = buffer.indexOf("\n", start);
|
||||
while (end !== -1) {
|
||||
const message2 = buffer.toString(void 0, start, end);
|
||||
this._dispatchMessage(message2);
|
||||
start = end + 1;
|
||||
end = buffer.indexOf("\n", start);
|
||||
}
|
||||
this._pendingBuffers = [buffer.slice(start)];
|
||||
}
|
||||
_dispatchMessage(message) {
|
||||
try {
|
||||
this.onmessage?.(JSON.parse(message));
|
||||
} catch (e) {
|
||||
console.error("failed to dispatch message", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
function compareSemver(a, b) {
|
||||
const aBase = a.replace(/-.*$/, "");
|
||||
const bBase = b.replace(/-.*$/, "");
|
||||
const aParts = aBase.split(".").map(Number);
|
||||
const bParts = bBase.split(".").map(Number);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (aParts[i] > bParts[i])
|
||||
return 1;
|
||||
if (aParts[i] < bParts[i])
|
||||
return -1;
|
||||
}
|
||||
const aTimestamp = parseSuffixTimestamp(a);
|
||||
const bTimestamp = parseSuffixTimestamp(b);
|
||||
if (aTimestamp > bTimestamp)
|
||||
return 1;
|
||||
if (aTimestamp < bTimestamp)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
function parseSuffixTimestamp(version) {
|
||||
const match = version.match(/^\d+\.\d+\.\d+-(?:alpha|beta)-(.+)$/);
|
||||
if (!match)
|
||||
return Infinity;
|
||||
const suffix = match[1];
|
||||
if (/^\d{4}-\d{2}-\d{2}$/.test(suffix))
|
||||
return new Date(suffix).getTime();
|
||||
return Number(suffix);
|
||||
}
|
||||
// Annotate the CommonJS export names for ESM import in node:
|
||||
0 && (module.exports = {
|
||||
SocketConnection,
|
||||
compareSemver
|
||||
});
|
||||
90764
tools/e2e/node_modules/playwright-core/lib/utilsBundle.js
generated
vendored
Normal file
2179
tools/e2e/node_modules/playwright-core/lib/utilsBundle.js.LICENSE
generated
vendored
Normal file
BIN
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf
generated
vendored
Normal file
1
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg
generated
vendored
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
1
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg
generated
vendored
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
1
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg
generated
vendored
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
11
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js
generated
vendored
Normal file
1
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css
generated
vendored
Normal file
1
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/assets/safari-na3_-uQk.svg
generated
vendored
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
29
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/index.html
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
|
||||
<title>Playwright Dashboard</title>
|
||||
<script type="module" crossorigin src="/assets/index-CyWAfh-p.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DhC616m4.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
24
tools/e2e/node_modules/playwright-core/lib/vite/dashboard/playwright-logo.svg
generated
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<mask id="bezel-mask">
|
||||
<rect width="400" height="400" fill="white"/>
|
||||
<rect x="32" y="32" width="336" height="302" rx="6" fill="black"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<!-- Monitor bezel (ring, transparent screen area) -->
|
||||
<rect x="20" y="20" width="360" height="326" rx="13" fill="#2D4552" mask="url(#bezel-mask)"/>
|
||||
<!-- Stand neck -->
|
||||
<rect x="190" y="346" width="20" height="20" fill="#2D4552"/>
|
||||
<!-- Stand base -->
|
||||
<rect x="152" y="366" width="96" height="14" rx="7" fill="#2D4552"/>
|
||||
<!-- Playwright logo inscribed on screen (scale 0.755, centered) -->
|
||||
<g transform="translate(49, 32) scale(0.755)">
|
||||
<path d="M136.444 221.556C123.558 225.213 115.104 231.625 109.535 238.032C114.869 233.364 122.014 229.08 131.652 226.348C141.51 223.554 149.92 223.574 156.869 224.915V219.481C150.941 218.939 144.145 219.371 136.444 221.556ZM108.946 175.876L61.0895 188.484C61.0895 188.484 61.9617 189.716 63.5767 191.36L104.153 180.668C104.153 180.668 103.578 188.077 98.5847 194.705C108.03 187.559 108.946 175.876 108.946 175.876ZM149.005 288.347C81.6582 306.486 46.0272 228.438 35.2396 187.928C30.2556 169.229 28.0799 155.067 27.5 145.928C27.4377 144.979 27.4665 144.179 27.5336 143.446C24.04 143.657 22.3674 145.473 22.7077 150.721C23.2876 159.855 25.4633 174.016 30.4473 192.721C41.2301 233.225 76.8659 311.273 144.213 293.134C158.872 289.185 169.885 281.992 178.152 272.81C170.532 279.692 160.995 285.112 149.005 288.347ZM161.661 128.11V132.903H188.077C187.535 131.206 186.989 129.677 186.447 128.11H161.661Z" fill="#2D4552"/>
|
||||
<path d="M193.981 167.584C205.861 170.958 212.144 179.287 215.465 186.658L228.711 190.42C228.711 190.42 226.904 164.623 203.57 157.995C181.741 151.793 168.308 170.124 166.674 172.496C173.024 167.972 182.297 164.268 193.981 167.584ZM299.422 186.777C277.573 180.547 264.145 198.916 262.535 201.255C268.89 196.736 278.158 193.031 289.837 196.362C301.698 199.741 307.976 208.06 311.307 215.436L324.572 219.212C324.572 219.212 322.736 193.41 299.422 186.777ZM286.262 254.795L176.072 223.99C176.072 223.99 177.265 230.038 181.842 237.869L274.617 263.805C282.255 259.386 286.262 254.795 286.262 254.795ZM209.867 321.102C122.618 297.71 133.166 186.543 147.284 133.865C153.097 112.156 159.073 96.0203 164.029 85.204C161.072 84.5953 158.623 86.1529 156.203 91.0746C150.941 101.747 144.212 119.124 137.7 143.45C123.586 196.127 113.038 307.29 200.283 330.682C241.406 341.699 273.442 324.955 297.323 298.659C274.655 319.19 245.714 330.701 209.867 321.102Z" fill="#2D4552"/>
|
||||
<path d="M161.661 262.296V239.863L99.3324 257.537C99.3324 257.537 103.938 230.777 136.444 221.556C146.302 218.762 154.713 218.781 161.661 220.123V128.11H192.869C189.471 117.61 186.184 109.526 183.423 103.909C178.856 94.612 174.174 100.775 163.545 109.665C156.059 115.919 137.139 129.261 108.668 136.933C80.1966 144.61 57.179 142.574 47.5752 140.911C33.9601 138.562 26.8387 135.572 27.5049 145.928C28.0847 155.062 30.2605 169.224 35.2445 187.928C46.0272 228.433 81.663 306.481 149.01 288.342C166.602 283.602 179.019 274.233 187.626 262.291H161.661V262.296ZM61.0848 188.484L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6614 203.743 61.0848 188.484 61.0848 188.484Z" fill="#E2574C"/>
|
||||
<path d="M341.786 129.174C329.345 131.355 299.498 134.072 262.612 124.185C225.716 114.304 201.236 97.0224 191.537 88.8994C177.788 77.3834 171.74 69.3802 165.788 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.098C297.093 344.47 343.53 242.92 357.644 190.238C364.157 165.917 367.013 147.5 367.799 135.625C368.695 122.173 359.455 126.078 341.786 129.174ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756ZM223.42 268.713C182.403 256.698 176.077 223.99 176.077 223.99L286.262 254.796C286.262 254.791 264.021 280.578 223.42 268.713ZM262.377 201.495C262.377 201.495 276.107 180.126 299.422 186.773C322.736 193.411 324.572 219.208 324.572 219.208L262.377 201.495Z" fill="#2EAD33"/>
|
||||
<path d="M139.88 246.04L99.3324 257.532C99.3324 257.532 103.737 232.44 133.607 222.496L110.647 136.33L108.663 136.933C80.1918 144.611 57.1742 142.574 47.5704 140.911C33.9554 138.563 26.834 135.572 27.5001 145.929C28.08 155.063 30.2557 169.224 35.2397 187.929C46.0225 228.433 81.6583 306.481 149.005 288.342L150.989 287.719L139.88 246.04ZM61.0848 188.485L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6615 203.743 61.0848 188.485 61.0848 188.485Z" fill="#D65348"/>
|
||||
<path d="M225.27 269.163L223.415 268.712C182.398 256.698 176.072 223.99 176.072 223.99L232.89 239.872L262.971 124.281L262.607 124.185C225.711 114.304 201.232 97.0224 191.532 88.8994C177.783 77.3834 171.735 69.3802 165.783 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.097L211.655 321.5L225.27 269.163ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756Z" fill="#1D8D22"/>
|
||||
<path d="M141.946 245.451L131.072 248.537C133.641 263.019 138.169 276.917 145.276 289.195C146.513 288.922 147.74 288.687 149 288.342C152.302 287.451 155.364 286.348 158.312 285.145C150.371 273.361 145.118 259.789 141.946 245.451ZM137.7 143.451C132.112 164.307 127.113 194.326 128.489 224.436C130.952 223.367 133.554 222.371 136.444 221.551L138.457 221.101C136.003 188.939 141.308 156.165 147.284 133.866C148.799 128.225 150.318 122.978 151.832 118.085C149.393 119.637 146.767 121.228 143.776 122.867C141.759 129.093 139.722 135.898 137.7 143.451Z" fill="#C04B41"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
16
tools/e2e/node_modules/playwright-core/lib/vite/htmlReport/index.html
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html style='scrollbar-gutter: stable both-edges;'>
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='color-scheme' content='dark light'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<title>Playwright Test Report</title>
|
||||
<script type="module" crossorigin src="./report.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./report.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'></div>
|
||||
</body>
|
||||
</html>
|
||||
2
tools/e2e/node_modules/playwright-core/lib/vite/htmlReport/report.css
generated
vendored
Normal file
32
tools/e2e/node_modules/playwright-core/lib/vite/htmlReport/report.js
generated
vendored
Normal file
1
tools/e2e/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css
generated
vendored
Normal file
32
tools/e2e/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js
generated
vendored
Normal file
BIN
tools/e2e/node_modules/playwright-core/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf
generated
vendored
Normal file
129
tools/e2e/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js
generated
vendored
Normal file
1
tools/e2e/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css
generated
vendored
Normal file
29
tools/e2e/node_modules/playwright-core/lib/vite/recorder/index.html
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright (c) Microsoft Corporation.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" translate="no">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" href="/playwright-logo.svg" type="image/svg+xml">
|
||||
<title>Playwright Inspector</title>
|
||||
<script type="module" crossorigin src="/assets/index-DYjdXIbE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-l_lX622x.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
9
tools/e2e/node_modules/playwright-core/lib/vite/recorder/playwright-logo.svg
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M136.444 221.556C123.558 225.213 115.104 231.625 109.535 238.032C114.869 233.364 122.014 229.08 131.652 226.348C141.51 223.554 149.92 223.574 156.869 224.915V219.481C150.941 218.939 144.145 219.371 136.444 221.556ZM108.946 175.876L61.0895 188.484C61.0895 188.484 61.9617 189.716 63.5767 191.36L104.153 180.668C104.153 180.668 103.578 188.077 98.5847 194.705C108.03 187.559 108.946 175.876 108.946 175.876ZM149.005 288.347C81.6582 306.486 46.0272 228.438 35.2396 187.928C30.2556 169.229 28.0799 155.067 27.5 145.928C27.4377 144.979 27.4665 144.179 27.5336 143.446C24.04 143.657 22.3674 145.473 22.7077 150.721C23.2876 159.855 25.4633 174.016 30.4473 192.721C41.2301 233.225 76.8659 311.273 144.213 293.134C158.872 289.185 169.885 281.992 178.152 272.81C170.532 279.692 160.995 285.112 149.005 288.347ZM161.661 128.11V132.903H188.077C187.535 131.206 186.989 129.677 186.447 128.11H161.661Z" fill="#2D4552"/>
|
||||
<path d="M193.981 167.584C205.861 170.958 212.144 179.287 215.465 186.658L228.711 190.42C228.711 190.42 226.904 164.623 203.57 157.995C181.741 151.793 168.308 170.124 166.674 172.496C173.024 167.972 182.297 164.268 193.981 167.584ZM299.422 186.777C277.573 180.547 264.145 198.916 262.535 201.255C268.89 196.736 278.158 193.031 289.837 196.362C301.698 199.741 307.976 208.06 311.307 215.436L324.572 219.212C324.572 219.212 322.736 193.41 299.422 186.777ZM286.262 254.795L176.072 223.99C176.072 223.99 177.265 230.038 181.842 237.869L274.617 263.805C282.255 259.386 286.262 254.795 286.262 254.795ZM209.867 321.102C122.618 297.71 133.166 186.543 147.284 133.865C153.097 112.156 159.073 96.0203 164.029 85.204C161.072 84.5953 158.623 86.1529 156.203 91.0746C150.941 101.747 144.212 119.124 137.7 143.45C123.586 196.127 113.038 307.29 200.283 330.682C241.406 341.699 273.442 324.955 297.323 298.659C274.655 319.19 245.714 330.701 209.867 321.102Z" fill="#2D4552"/>
|
||||
<path d="M161.661 262.296V239.863L99.3324 257.537C99.3324 257.537 103.938 230.777 136.444 221.556C146.302 218.762 154.713 218.781 161.661 220.123V128.11H192.869C189.471 117.61 186.184 109.526 183.423 103.909C178.856 94.612 174.174 100.775 163.545 109.665C156.059 115.919 137.139 129.261 108.668 136.933C80.1966 144.61 57.179 142.574 47.5752 140.911C33.9601 138.562 26.8387 135.572 27.5049 145.928C28.0847 155.062 30.2605 169.224 35.2445 187.928C46.0272 228.433 81.663 306.481 149.01 288.342C166.602 283.602 179.019 274.233 187.626 262.291H161.661V262.296ZM61.0848 188.484L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6614 203.743 61.0848 188.484 61.0848 188.484Z" fill="#E2574C"/>
|
||||
<path d="M341.786 129.174C329.345 131.355 299.498 134.072 262.612 124.185C225.716 114.304 201.236 97.0224 191.537 88.8994C177.788 77.3834 171.74 69.3802 165.788 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.098C297.093 344.47 343.53 242.92 357.644 190.238C364.157 165.917 367.013 147.5 367.799 135.625C368.695 122.173 359.455 126.078 341.786 129.174ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756ZM223.42 268.713C182.403 256.698 176.077 223.99 176.077 223.99L286.262 254.796C286.262 254.791 264.021 280.578 223.42 268.713ZM262.377 201.495C262.377 201.495 276.107 180.126 299.422 186.773C322.736 193.411 324.572 219.208 324.572 219.208L262.377 201.495Z" fill="#2EAD33"/>
|
||||
<path d="M139.88 246.04L99.3324 257.532C99.3324 257.532 103.737 232.44 133.607 222.496L110.647 136.33L108.663 136.933C80.1918 144.611 57.1742 142.574 47.5704 140.911C33.9554 138.563 26.834 135.572 27.5001 145.929C28.08 155.063 30.2557 169.224 35.2397 187.929C46.0225 228.433 81.6583 306.481 149.005 288.342L150.989 287.719L139.88 246.04ZM61.0848 188.485L108.946 175.876C108.946 175.876 107.551 194.288 89.6087 199.018C71.6615 203.743 61.0848 188.485 61.0848 188.485Z" fill="#D65348"/>
|
||||
<path d="M225.27 269.163L223.415 268.712C182.398 256.698 176.072 223.99 176.072 223.99L232.89 239.872L262.971 124.281L262.607 124.185C225.711 114.304 201.232 97.0224 191.532 88.8994C177.783 77.3834 171.735 69.3802 165.783 81.4857C160.526 92.163 153.797 109.54 147.284 133.866C133.171 186.543 122.623 297.706 209.867 321.097L211.655 321.5L225.27 269.163ZM166.497 172.756C166.497 172.756 180.246 151.372 203.565 158C226.899 164.628 228.706 190.425 228.706 190.425L166.497 172.756Z" fill="#1D8D22"/>
|
||||
<path d="M141.946 245.451L131.072 248.537C133.641 263.019 138.169 276.917 145.276 289.195C146.513 288.922 147.74 288.687 149 288.342C152.302 287.451 155.364 286.348 158.312 285.145C150.371 273.361 145.118 259.789 141.946 245.451ZM137.7 143.451C132.112 164.307 127.113 194.326 128.489 224.436C130.952 223.367 133.554 222.371 136.444 221.551L138.457 221.101C136.003 188.939 141.308 156.165 147.284 133.866C148.799 128.225 150.318 122.978 151.832 118.085C149.393 119.637 146.767 121.228 143.776 122.867C141.759 129.093 139.722 135.898 137.7 143.451Z" fill="#C04B41"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
32
tools/e2e/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-rXmQmLUY.js
generated
vendored
Normal file
181
tools/e2e/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B-dXF5JN.js
generated
vendored
Normal file
1
tools/e2e/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e,t=`'`){let n=JSON.stringify(e),r=n.substring(1,n.length-1).replace(/\\"/g,`"`);if(t===`'`)return t+r.replace(/[']/g,`\\'`)+t;if(t===`"`)return t+r.replace(/["]/g,`\\"`)+t;if(t==="`")return t+r.replace(/[`]/g,"\\`")+t;throw Error(`Invalid escape char`)}function t(e){return e.charAt(0).toUpperCase()+e.substring(1)}function n(e){return e.replace(/([a-z0-9])([A-Z])/g,`$1_$2`).replace(/([A-Z])([A-Z][a-z])/g,`$1_$2`).toLowerCase()}function r(e){return`"${e.replace(/["\\]/g,e=>`\\`+e)}"`}var i;function a(){i=new Map}function o(e){let t=i?.get(e);return t===void 0&&(t=e.replace(/[\u200b\u00ad]/g,``).trim().replace(/\s+/g,` `),i?.set(e,t)),t}function s(e){return e.replace(/(^|[^\\])(\\\\)*\\(['"`])/g,`$1$2$3`)}function c(e){return e.unicode||e.unicodeSets?String(e):String(e).replace(/(^|[^\\])(\\\\)*(["'`])/g,`$1$2\\$3`).replace(/>>/g,`\\>\\>`)}function l(e,t){return typeof e==`string`?`${JSON.stringify(e)}${t?`s`:`i`}`:c(e)}function u(e,t){return typeof e==`string`?`"${e.replace(/\\/g,`\\\\`).replace(/["]/g,`\\"`)}"${t?`s`:`i`}`:c(e)}function d(e,t,n=``){if(e.length<=t)return e;let r=[...e];return r.length>t?r.slice(0,t-n.length).join(``)+n:r.join(``)}function f(e,t){return d(e,t,`…`)}function p(e){if(!e.startsWith(`data:`))return e;let t=e.indexOf(`,`);return t===-1?e:e.slice(0,t+1)+`…`}function m(e){return e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}function h(e,t){let n=e.length,r=t.length,i=0,a=0,o=Array(n+1).fill(null).map(()=>Array(r+1).fill(0));for(let s=1;s<=n;s++)for(let n=1;n<=r;n++)e[s-1]===t[n-1]&&(o[s][n]=o[s-1][n-1]+1,o[s][n]>i&&(i=o[s][n],a=s));return e.slice(a-i,a)}function g(e,t){try{return[`http:`,`https:`].includes(new URL(e,t).protocol)}catch{return!1}}export{m as a,s as c,n as d,t as f,l as i,o as l,p as m,a as n,e as o,f as p,u as r,h as s,g as t,r as u};
|
||||