If braces changes
This commit is contained in:
parent
ea592d32aa
commit
b77146632b
|
|
@ -27,17 +27,20 @@ export function bundle(): Plugin {
|
|||
},
|
||||
transformIndexHtml: {
|
||||
handler(html, ctx) {
|
||||
if (!ctx || !ctx.bundle)
|
||||
if (!ctx || !ctx.bundle) {
|
||||
return html;
|
||||
}
|
||||
html = html.replace(/(?=<!--)([\s\S]*?)-->/, '');
|
||||
for (const [name, value] of Object.entries(ctx.bundle)) {
|
||||
if (name.endsWith('.map'))
|
||||
if (name.endsWith('.map')) {
|
||||
continue;
|
||||
if ('code' in value)
|
||||
}
|
||||
if ('code' in value) {
|
||||
html = html.replace(/<script type="module".*<\/script>/, () => `<script type="module">${value.code}</script>`);
|
||||
else
|
||||
} else {
|
||||
html = html.replace(/<link rel="stylesheet"[^>]*>/, () => `<style type='text/css'>${value.source}</style>`);
|
||||
}
|
||||
}
|
||||
return html;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -98,8 +98,9 @@ export class Filter {
|
|||
}
|
||||
token.push(c);
|
||||
}
|
||||
if (token.length)
|
||||
if (token.length) {
|
||||
result.push(token.join('').toLowerCase());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -107,38 +108,45 @@ export class Filter {
|
|||
const searchValues = cacheSearchValues(test);
|
||||
if (this.project.length) {
|
||||
const matches = !!this.project.find(p => searchValues.project.includes(p));
|
||||
if (!matches)
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.status.length) {
|
||||
const matches = !!this.status.find(s => searchValues.status.includes(s));
|
||||
if (!matches)
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (searchValues.status === 'skipped')
|
||||
if (searchValues.status === 'skipped') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.text.length) {
|
||||
for (const text of this.text) {
|
||||
if (searchValues.text.includes(text))
|
||||
if (searchValues.text.includes(text)) {
|
||||
continue;
|
||||
}
|
||||
const [fileName, line, column] = text.split(':');
|
||||
if (searchValues.file.includes(fileName) && searchValues.line === line && (column === undefined || searchValues.column === column))
|
||||
if (searchValues.file.includes(fileName) && searchValues.line === line && (column === undefined || searchValues.column === column)) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.labels.length) {
|
||||
const matches = this.labels.every(l => searchValues.labels.includes(l));
|
||||
if (!matches)
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (this.annotations.length) {
|
||||
const matches = this.annotations.every(annotation =>
|
||||
searchValues.annotations.some(a => a.includes(annotation)));
|
||||
if (!matches)
|
||||
if (!matches) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -158,16 +166,20 @@ const searchValuesSymbol = Symbol('searchValues');
|
|||
|
||||
function cacheSearchValues(test: TestCaseSummary & { [searchValuesSymbol]?: SearchValues }): SearchValues {
|
||||
const cached = test[searchValuesSymbol];
|
||||
if (cached)
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
let status: SearchValues['status'] = 'passed';
|
||||
if (test.outcome === 'unexpected')
|
||||
if (test.outcome === 'unexpected') {
|
||||
status = 'failed';
|
||||
if (test.outcome === 'flaky')
|
||||
}
|
||||
if (test.outcome === 'flaky') {
|
||||
status = 'flaky';
|
||||
if (test.outcome === 'skipped')
|
||||
}
|
||||
if (test.outcome === 'skipped') {
|
||||
status = 'skipped';
|
||||
}
|
||||
const searchValues: SearchValues = {
|
||||
text: (status + ' ' + test.projectName + ' ' + test.tags.join(' ') + ' ' + test.location.file + ' ' + test.path.join(' ') + ' ' + test.title).toLowerCase(),
|
||||
project: test.projectName.toLowerCase(),
|
||||
|
|
@ -184,19 +196,23 @@ function cacheSearchValues(test: TestCaseSummary & { [searchValuesSymbol]?: Sear
|
|||
|
||||
export function filterWithToken(tokens: string[], token: string, append: boolean): string {
|
||||
if (append) {
|
||||
if (!tokens.includes(token))
|
||||
if (!tokens.includes(token)) {
|
||||
return '#?q=' + [...tokens, token].join(' ').trim();
|
||||
}
|
||||
return '#?q=' + tokens.filter(t => t !== token).join(' ').trim();
|
||||
}
|
||||
|
||||
// if metaKey or ctrlKey is not pressed, replace existing token with new token
|
||||
let prefix: 's:' | 'p:' | '@';
|
||||
if (token.startsWith('s:'))
|
||||
if (token.startsWith('s:')) {
|
||||
prefix = 's:';
|
||||
if (token.startsWith('p:'))
|
||||
}
|
||||
if (token.startsWith('p:')) {
|
||||
prefix = 'p:';
|
||||
if (token.startsWith('@'))
|
||||
}
|
||||
if (token.startsWith('@')) {
|
||||
prefix = '@';
|
||||
}
|
||||
|
||||
const newTokens = tokens.filter(t => !t.startsWith(prefix));
|
||||
newTokens.push(token);
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ document.head.appendChild(link);
|
|||
const ReportLoader: React.FC = () => {
|
||||
const [report, setReport] = React.useState<LoadedReport | undefined>();
|
||||
React.useEffect(() => {
|
||||
if (report)
|
||||
if (report) {
|
||||
return;
|
||||
}
|
||||
const zipReport = new ZipReport();
|
||||
zipReport.load().then(() => setReport(zipReport));
|
||||
}, [report]);
|
||||
|
|
@ -58,8 +59,9 @@ class ZipReport implements LoadedReport {
|
|||
|
||||
async load() {
|
||||
const zipURI = await new Promise<string>(resolve => {
|
||||
if (window.playwrightReportBase64)
|
||||
if (window.playwrightReportBase64) {
|
||||
return resolve(window.playwrightReportBase64);
|
||||
}
|
||||
if (window.opener) {
|
||||
window.addEventListener('message', event => {
|
||||
if (event.source === window.opener) {
|
||||
|
|
@ -70,15 +72,17 @@ class ZipReport implements LoadedReport {
|
|||
window.opener.postMessage('ready', '*');
|
||||
} else {
|
||||
const oldReport = localStorage.getItem(kPlaywrightReportStorageForHMR);
|
||||
if (oldReport)
|
||||
if (oldReport) {
|
||||
return resolve(oldReport);
|
||||
}
|
||||
alert('couldnt find report, something with HMR is broken');
|
||||
}
|
||||
});
|
||||
|
||||
const zipReader = new zipjs.ZipReader(new zipjs.Data64URIReader(zipURI), { useWebWorkers: false });
|
||||
for (const entry of await zipReader.getEntries())
|
||||
for (const entry of await zipReader.getEntries()) {
|
||||
this._entries.set(entry.filename, entry);
|
||||
}
|
||||
this._json = await this.entry('report.json') as HTMLReport;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,11 +101,13 @@ export const SearchParamsProvider: React.FunctionComponent<React.PropsWithChildr
|
|||
};
|
||||
|
||||
function downloadFileNameForAttachment(attachment: TestAttachment): string {
|
||||
if (attachment.name.includes('.') || !attachment.path)
|
||||
if (attachment.name.includes('.') || !attachment.path) {
|
||||
return attachment.name;
|
||||
}
|
||||
const firstDotIndex = attachment.path.indexOf('.');
|
||||
if (firstDotIndex === -1)
|
||||
if (firstDotIndex === -1) {
|
||||
return attachment.name;
|
||||
}
|
||||
return attachment.name + attachment.path.slice(firstDotIndex, attachment.path.length);
|
||||
}
|
||||
|
||||
|
|
@ -121,22 +123,27 @@ export function useAnchor(id: AnchorID, onReveal: () => void) {
|
|||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const isAnchored = useIsAnchored(id);
|
||||
React.useEffect(() => {
|
||||
if (isAnchored)
|
||||
if (isAnchored) {
|
||||
onReveal();
|
||||
}
|
||||
}, [isAnchored, onReveal, searchParams]);
|
||||
}
|
||||
|
||||
export function useIsAnchored(id: AnchorID) {
|
||||
const searchParams = React.useContext(SearchParamsContext);
|
||||
const anchor = searchParams.get('anchor');
|
||||
if (anchor === null)
|
||||
if (anchor === null) {
|
||||
return false;
|
||||
if (typeof id === 'undefined')
|
||||
}
|
||||
if (typeof id === 'undefined') {
|
||||
return false;
|
||||
if (typeof id === 'string')
|
||||
}
|
||||
if (typeof id === 'string') {
|
||||
return id === anchor;
|
||||
if (Array.isArray(id))
|
||||
}
|
||||
if (Array.isArray(id)) {
|
||||
return id.includes(anchor);
|
||||
}
|
||||
return id(anchor);
|
||||
}
|
||||
|
||||
|
|
@ -152,11 +159,14 @@ export function Anchor({ id, children }: React.PropsWithChildren<{ id: AnchorID
|
|||
|
||||
export function testResultHref({ test, result, anchor }: { test?: TestCase | TestCaseSummary, result?: TestResult | TestResultSummary, anchor?: string }) {
|
||||
const params = new URLSearchParams();
|
||||
if (test)
|
||||
if (test) {
|
||||
params.set('testId', test.testId);
|
||||
if (test && result)
|
||||
}
|
||||
if (test && result) {
|
||||
params.set('run', '' + test.results.indexOf(result as any));
|
||||
if (anchor)
|
||||
}
|
||||
if (anchor) {
|
||||
params.set('anchor', anchor);
|
||||
}
|
||||
return `#?` + params;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,8 +62,9 @@ class ErrorBoundary extends React.Component<React.PropsWithChildren<{}>, { error
|
|||
export const MetadataView: React.FC<Metainfo> = metadata => <ErrorBoundary><InnerMetadataView {...metadata} /></ErrorBoundary>;
|
||||
|
||||
const InnerMetadataView: React.FC<Metainfo> = metadata => {
|
||||
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.')))
|
||||
if (!Object.keys(metadata).find(k => k.startsWith('revision.') || k.startsWith('ci.'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AutoChip header={
|
||||
|
|
|
|||
|
|
@ -54,9 +54,10 @@ export const ReportView: React.FC<{
|
|||
const testIdToFileIdMap = React.useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const file of report?.json().files || []) {
|
||||
for (const test of file.tests)
|
||||
for (const test of file.tests) {
|
||||
map.set(test.testId, file.fileId);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [report]);
|
||||
|
||||
|
|
@ -66,8 +67,9 @@ export const ReportView: React.FC<{
|
|||
const result: TestModelSummary = { files: [], tests: [] };
|
||||
for (const file of report?.json().files || []) {
|
||||
const tests = file.tests.filter(t => filter.matches(t));
|
||||
if (tests.length)
|
||||
if (tests.length) {
|
||||
result.files.push({ ...file, tests });
|
||||
}
|
||||
result.tests.push(...tests);
|
||||
}
|
||||
return result;
|
||||
|
|
@ -112,11 +114,13 @@ const TestCaseViewLoader: React.FC<{
|
|||
|
||||
React.useEffect(() => {
|
||||
(async () => {
|
||||
if (!testId || testId === test?.testId)
|
||||
if (!testId || testId === test?.testId) {
|
||||
return;
|
||||
}
|
||||
const fileId = testIdToFileIdMap.get(testId);
|
||||
if (!fileId)
|
||||
if (!fileId) {
|
||||
return;
|
||||
}
|
||||
const file = await report.entry(`${fileId}.json`) as TestFile;
|
||||
for (const t of file.tests) {
|
||||
if (t.testId === testId) {
|
||||
|
|
@ -144,8 +148,9 @@ function computeStats(files: TestFileSummary[], filter: Filter): FilteredStats {
|
|||
for (const file of files) {
|
||||
const tests = file.tests.filter(t => filter.matches(t));
|
||||
stats.total += tests.length;
|
||||
for (const test of tests)
|
||||
for (const test of tests) {
|
||||
stats.duration += test.duration;
|
||||
}
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ export const TabbedPane: React.FunctionComponent<{
|
|||
</div>
|
||||
{
|
||||
tabs.map(tab => {
|
||||
if (selectedTab === tab.id)
|
||||
if (selectedTab === tab.id) {
|
||||
return <div key={tab.id} className='tab-content'>{tab.render()}</div>;
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -40,8 +40,9 @@ export const TestCaseView: React.FC<{
|
|||
const filterParam = searchParams.has('q') ? '&q=' + searchParams.get('q') : '';
|
||||
|
||||
const labels = React.useMemo(() => {
|
||||
if (!test)
|
||||
if (!test) {
|
||||
return undefined;
|
||||
}
|
||||
return test.tags;
|
||||
}, [test]);
|
||||
|
||||
|
|
@ -93,8 +94,9 @@ function TestCaseAnnotationView({ annotation: { type, description } }: { annotat
|
|||
}
|
||||
|
||||
function retryLabel(index: number) {
|
||||
if (!index)
|
||||
if (!index) {
|
||||
return 'Run';
|
||||
}
|
||||
return `Retry #${index}`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,11 +74,12 @@ export const TestFileView: React.FC<React.PropsWithChildren<{
|
|||
function imageDiffBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
for (const result of test.results) {
|
||||
for (const attachment of result.attachments) {
|
||||
if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/))
|
||||
if (attachment.contentType.startsWith('image/') && !!attachment.name.match(/-(expected|actual|diff)/)) {
|
||||
return <Link href={testResultHref({ test, result, anchor: `attachment-${attachment.name}` })} title='View images' className='test-file-badge'>{image()}</Link>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function videoBadge(test: TestCaseSummary): JSX.Element | undefined {
|
||||
const resultWithVideo = test.results.find(result => result.attachments.some(attachment => attachment.name === 'video'));
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ export const TestFilesView: React.FC<{
|
|||
projectNames={projectNames}
|
||||
isFileExpanded={fileId => {
|
||||
const value = expandedFiles.get(fileId);
|
||||
if (value === undefined)
|
||||
if (value === undefined) {
|
||||
return defaultExpanded;
|
||||
}
|
||||
return !!value;
|
||||
}}
|
||||
setFileExpanded={(fileId, expanded) => {
|
||||
|
|
@ -63,8 +64,9 @@ export const TestFilesHeader: React.FC<{
|
|||
report: HTMLReport | undefined,
|
||||
filteredStats?: FilteredStats,
|
||||
}> = ({ report, filteredStats }) => {
|
||||
if (!report)
|
||||
if (!report) {
|
||||
return;
|
||||
}
|
||||
return <>
|
||||
<div className='mt-2 mx-1' style={{ display: 'flex' }}>
|
||||
{report.projectNames.length === 1 && !!report.projectNames[0] && <div data-testid='project-name' style={{ color: 'var(--color-fg-subtle)' }}>Project: {report.projectNames[0]}</div>}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiffWithAnchors
|
|||
const snapshotNameToImageDiff = new Map<string, ImageDiffWithAnchors>();
|
||||
for (const attachment of screenshots) {
|
||||
const match = attachment.name.match(/^(.*)-(expected|actual|diff|previous)(\.[^.]+)?$/);
|
||||
if (!match)
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const [, name, category, extension = ''] = match;
|
||||
const snapshotName = name + extension;
|
||||
let imageDiff = snapshotNameToImageDiff.get(snapshotName);
|
||||
|
|
@ -46,15 +47,19 @@ function groupImageDiffs(screenshots: Set<TestAttachment>): ImageDiffWithAnchors
|
|||
snapshotNameToImageDiff.set(snapshotName, imageDiff);
|
||||
}
|
||||
imageDiff.anchors.push(`attachment-${attachment.name}`);
|
||||
if (category === 'actual')
|
||||
if (category === 'actual') {
|
||||
imageDiff.actual = { attachment };
|
||||
if (category === 'expected')
|
||||
}
|
||||
if (category === 'expected') {
|
||||
imageDiff.expected = { attachment, title: 'Expected' };
|
||||
if (category === 'previous')
|
||||
}
|
||||
if (category === 'previous') {
|
||||
imageDiff.expected = { attachment, title: 'Previous' };
|
||||
if (category === 'diff')
|
||||
}
|
||||
if (category === 'diff') {
|
||||
imageDiff.diff = { attachment };
|
||||
}
|
||||
}
|
||||
for (const [name, diff] of snapshotNameToImageDiff) {
|
||||
if (!diff.actual || !diff.expected) {
|
||||
snapshotNameToImageDiff.delete(name);
|
||||
|
|
@ -88,8 +93,9 @@ export const TestResultView: React.FC<{
|
|||
return <div className='test-result'>
|
||||
{!!errors.length && <AutoChip header='Errors'>
|
||||
{errors.map((error, index) => {
|
||||
if (error.type === 'screenshot')
|
||||
if (error.type === 'screenshot') {
|
||||
return <TestScreenshotErrorView key={'test-result-error-message-' + index} errorPrefix={error.errorPrefix} diff={error.diff!} errorSuffix={error.errorSuffix}></TestScreenshotErrorView>;
|
||||
}
|
||||
return <TestErrorView key={'test-result-error-message-' + index} error={error.error!}></TestErrorView>;
|
||||
})}
|
||||
</AutoChip>}
|
||||
|
|
@ -177,15 +183,18 @@ const StepTreeItem: React.FC<{
|
|||
const attachmentName = step.title.match(/^attach "(.*)"$/)?.[1];
|
||||
return <TreeItem title={<span aria-label={step.title}>
|
||||
<span style={{ float: 'right' }}>{msToString(step.duration)}</span>
|
||||
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={testResultHref({ test, result, anchor: `attachment-${attachmentName}` })} onClick={evt => { evt.stopPropagation(); }}>{icons.attachment()}</a>}
|
||||
{attachmentName && <a style={{ float: 'right' }} title='link to attachment' href={testResultHref({ test, result, anchor: `attachment-${attachmentName}` })} onClick={evt => {
|
||||
evt.stopPropagation();
|
||||
}}>{icons.attachment()}</a>}
|
||||
{statusIcon(step.error || step.duration === -1 ? 'failed' : 'passed')}
|
||||
<span>{step.title}</span>
|
||||
{step.count > 1 && <> ✕ <span className='test-result-counter'>{step.count}</span></>}
|
||||
{step.location && <span className='test-result-path'>— {step.location.file}:{step.location.line}</span>}
|
||||
</span>} loadChildren={step.steps.length + (step.snippet ? 1 : 0) ? () => {
|
||||
const children = step.steps.map((s, i) => <StepTreeItem key={i} step={s} depth={depth + 1} result={result} test={test} />);
|
||||
if (step.snippet)
|
||||
if (step.snippet) {
|
||||
children.unshift(<TestErrorView testId='test-snippet' key='line' error={step.snippet}/>);
|
||||
}
|
||||
return children;
|
||||
} : undefined} depth={depth}/>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ export const TreeItem: React.FunctionComponent<{
|
|||
}> = ({ title, loadChildren, onClick, expandByDefault, depth, selected, style }) => {
|
||||
const [expanded, setExpanded] = React.useState(expandByDefault || false);
|
||||
return <div className={'tree-item'} style={style}>
|
||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => { onClick?.(); setExpanded(!expanded); }} >
|
||||
<span className={clsx('tree-item-title', selected && 'selected')} style={{ whiteSpace: 'nowrap', paddingLeft: depth * 22 + 4 }} onClick={() => {
|
||||
onClick?.(); setExpanded(!expanded);
|
||||
}} >
|
||||
{loadChildren && !!expanded && icons.downArrow()}
|
||||
{loadChildren && !expanded && icons.rightArrow()}
|
||||
{!loadChildren && <span style={{ visibility: 'hidden' }}>{icons.rightArrow()}</span>}
|
||||
|
|
|
|||
|
|
@ -15,26 +15,32 @@
|
|||
*/
|
||||
|
||||
export function msToString(ms: number): string {
|
||||
if (!isFinite(ms))
|
||||
if (!isFinite(ms)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
if (ms === 0)
|
||||
if (ms === 0) {
|
||||
return '0ms';
|
||||
}
|
||||
|
||||
if (ms < 1000)
|
||||
if (ms < 1000) {
|
||||
return ms.toFixed(0) + 'ms';
|
||||
}
|
||||
|
||||
const seconds = ms / 1000;
|
||||
if (seconds < 60)
|
||||
if (seconds < 60) {
|
||||
return seconds.toFixed(1) + 's';
|
||||
}
|
||||
|
||||
const minutes = seconds / 60;
|
||||
if (minutes < 60)
|
||||
if (minutes < 60) {
|
||||
return minutes.toFixed(1) + 'm';
|
||||
}
|
||||
|
||||
const hours = minutes / 60;
|
||||
if (hours < 24)
|
||||
if (hours < 24) {
|
||||
return hours.toFixed(1) + 'h';
|
||||
}
|
||||
|
||||
const days = hours / 24;
|
||||
return days.toFixed(1) + 'd';
|
||||
|
|
@ -43,8 +49,9 @@ export function msToString(ms: number): string {
|
|||
// hash string to integer in range [0, 6] for color index, to get same color for same tag
|
||||
export function hashStringToInt(str: string) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++)
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 8) - hash);
|
||||
}
|
||||
return Math.abs(hash % 6);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -32,17 +32,20 @@ export class AndroidServerLauncherImpl {
|
|||
omitDriverInstall: options.omitDriverInstall,
|
||||
});
|
||||
|
||||
if (devices.length === 0)
|
||||
if (devices.length === 0) {
|
||||
throw new Error('No devices found');
|
||||
}
|
||||
|
||||
if (options.deviceSerialNumber) {
|
||||
devices = devices.filter(d => d.serial === options.deviceSerialNumber);
|
||||
if (devices.length === 0)
|
||||
if (devices.length === 0) {
|
||||
throw new Error(`No device with serial number '${options.deviceSerialNumber}' was found`);
|
||||
}
|
||||
}
|
||||
|
||||
if (devices.length > 1)
|
||||
if (devices.length > 1) {
|
||||
throw new Error(`More than one device found. Please specify deviceSerialNumber`);
|
||||
}
|
||||
|
||||
const device = devices[0];
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,8 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
|
||||
function toProtocolLogger(logger: Logger | undefined): ProtocolLogger | undefined {
|
||||
return logger ? (direction: 'send' | 'receive', message: object) => {
|
||||
if (logger.isEnabled('protocol', 'verbose'))
|
||||
if (logger.isEnabled('protocol', 'verbose')) {
|
||||
logger.log('protocol', 'verbose', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message), [], {});
|
||||
}
|
||||
} : undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ export function runDriver() {
|
|||
// Certain Language Binding JSON parsers (e.g. .NET) do not like strings with lone surrogates.
|
||||
const isJavaScriptLanguageBinding = !process.env.PW_LANG_NAME || process.env.PW_LANG_NAME === 'javascript';
|
||||
const replacer = !isJavaScriptLanguageBinding && (String.prototype as any).toWellFormed ? (key: string, value: any): any => {
|
||||
if (typeof value === 'string')
|
||||
if (typeof value === 'string') {
|
||||
return value.toWellFormed();
|
||||
}
|
||||
return value;
|
||||
} : undefined;
|
||||
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message, replacer));
|
||||
|
|
@ -85,8 +86,9 @@ export async function runServer(options: RunServerOptions) {
|
|||
|
||||
export async function launchBrowserServer(browserName: string, configFile?: string) {
|
||||
let options: LaunchServerOptions = {};
|
||||
if (configFile)
|
||||
if (configFile) {
|
||||
options = JSON.parse(fs.readFileSync(configFile).toString());
|
||||
}
|
||||
const browserType = (playwright as any)[browserName] as BrowserType;
|
||||
const server = await browserType.launchServer(options);
|
||||
console.log(server.wsEndpoint());
|
||||
|
|
|
|||
|
|
@ -82,42 +82,50 @@ function suggestedBrowsersToInstall() {
|
|||
|
||||
function defaultBrowsersToInstall(options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
|
||||
let executables = registry.defaultExecutables();
|
||||
if (options.noShell)
|
||||
if (options.noShell) {
|
||||
executables = executables.filter(e => e.name !== 'chromium-headless-shell');
|
||||
if (options.onlyShell)
|
||||
}
|
||||
if (options.onlyShell) {
|
||||
executables = executables.filter(e => e.name !== 'chromium');
|
||||
}
|
||||
return executables;
|
||||
}
|
||||
|
||||
function checkBrowsersToInstall(args: string[], options: { noShell?: boolean, onlyShell?: boolean }): Executable[] {
|
||||
if (options.noShell && options.onlyShell)
|
||||
if (options.noShell && options.onlyShell) {
|
||||
throw new Error(`Only one of --no-shell and --only-shell can be specified`);
|
||||
}
|
||||
|
||||
const faultyArguments: string[] = [];
|
||||
const executables: Executable[] = [];
|
||||
const handleArgument = (arg: string) => {
|
||||
const executable = registry.findExecutable(arg);
|
||||
if (!executable || executable.installType === 'none')
|
||||
if (!executable || executable.installType === 'none') {
|
||||
faultyArguments.push(arg);
|
||||
else
|
||||
} else {
|
||||
executables.push(executable);
|
||||
if (executable?.browserName === 'chromium')
|
||||
}
|
||||
if (executable?.browserName === 'chromium') {
|
||||
executables.push(registry.findExecutable('ffmpeg')!);
|
||||
}
|
||||
};
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg === 'chromium') {
|
||||
if (!options.onlyShell)
|
||||
if (!options.onlyShell) {
|
||||
handleArgument('chromium');
|
||||
if (!options.noShell)
|
||||
}
|
||||
if (!options.noShell) {
|
||||
handleArgument('chromium-headless-shell');
|
||||
}
|
||||
} else {
|
||||
handleArgument(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if (faultyArguments.length)
|
||||
if (faultyArguments.length) {
|
||||
throw new Error(`Invalid installation targets: ${faultyArguments.map(name => `'${name}'`).join(', ')}. Expecting one of: ${suggestedBrowsersToInstall()}`);
|
||||
}
|
||||
return executables;
|
||||
}
|
||||
|
||||
|
|
@ -132,8 +140,9 @@ program
|
|||
.option('--no-shell', 'do not install chromium headless shell')
|
||||
.action(async function(args: string[], options: { withDeps?: boolean, force?: boolean, dryRun?: boolean, shell?: boolean, noShell?: boolean, onlyShell?: boolean }) {
|
||||
// For '--no-shell' option, commander sets `shell: false` instead.
|
||||
if (options.shell === false)
|
||||
if (options.shell === false) {
|
||||
options.noShell = true;
|
||||
}
|
||||
if (isLikelyNpxGlobal()) {
|
||||
console.error(wrapInASCIIBox([
|
||||
`WARNING: It looks like you are running 'npx playwright install' without first`,
|
||||
|
|
@ -157,8 +166,9 @@ program
|
|||
try {
|
||||
const hasNoArguments = !args.length;
|
||||
const executables = hasNoArguments ? defaultBrowsersToInstall(options) : checkBrowsersToInstall(args, options);
|
||||
if (options.withDeps)
|
||||
if (options.withDeps) {
|
||||
await registry.installDeps(executables, !!options.dryRun);
|
||||
}
|
||||
if (options.dryRun) {
|
||||
for (const executable of executables) {
|
||||
const version = executable.browserVersion ? `version ` + executable.browserVersion : '';
|
||||
|
|
@ -167,9 +177,10 @@ program
|
|||
if (executable.downloadURLs?.length) {
|
||||
const [url, ...fallbacks] = executable.downloadURLs;
|
||||
console.log(` Download url: ${url}`);
|
||||
for (let i = 0; i < fallbacks.length; ++i)
|
||||
for (let i = 0; i < fallbacks.length; ++i) {
|
||||
console.log(` Download fallback ${i + 1}: ${fallbacks[i]}`);
|
||||
}
|
||||
}
|
||||
console.log(``);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -213,10 +224,11 @@ program
|
|||
.option('--dry-run', 'Do not execute installation commands, only print them')
|
||||
.action(async function(args: string[], options: { dryRun?: boolean }) {
|
||||
try {
|
||||
if (!args.length)
|
||||
if (!args.length) {
|
||||
await registry.installDeps(defaultBrowsersToInstall({}), !!options.dryRun);
|
||||
else
|
||||
} else {
|
||||
await registry.installDeps(checkBrowsersToInstall(args, {}), !!options.dryRun);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Failed to install browser dependencies\n${e}`);
|
||||
gracefullyProcessExitDoNotHang(1);
|
||||
|
|
@ -313,12 +325,15 @@ program
|
|||
.option('--stdin', 'Accept trace URLs over stdin to update the viewer')
|
||||
.description('show trace viewer')
|
||||
.action(function(traces, options) {
|
||||
if (options.browser === 'cr')
|
||||
if (options.browser === 'cr') {
|
||||
options.browser = 'chromium';
|
||||
if (options.browser === 'ff')
|
||||
}
|
||||
if (options.browser === 'ff') {
|
||||
options.browser = 'firefox';
|
||||
if (options.browser === 'wk')
|
||||
}
|
||||
if (options.browser === 'wk') {
|
||||
options.browser = 'webkit';
|
||||
}
|
||||
|
||||
const openOptions: TraceViewerServerOptions = {
|
||||
host: options.host,
|
||||
|
|
@ -326,10 +341,11 @@ program
|
|||
isServer: !!options.stdin,
|
||||
};
|
||||
|
||||
if (options.port !== undefined || options.host !== undefined)
|
||||
if (options.port !== undefined || options.host !== undefined) {
|
||||
runTraceInBrowser(traces, openOptions).catch(logErrorAndExit);
|
||||
else
|
||||
} else {
|
||||
runTraceViewerApp(traces, options.browser, openOptions, true).catch(logErrorAndExit);
|
||||
}
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
|
|
@ -367,8 +383,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
validateOptions(options);
|
||||
const browserType = lookupBrowserType(options);
|
||||
const launchOptions: LaunchOptions = extraOptions;
|
||||
if (options.channel)
|
||||
if (options.channel) {
|
||||
launchOptions.channel = options.channel as any;
|
||||
}
|
||||
launchOptions.handleSIGINT = false;
|
||||
|
||||
const contextOptions: BrowserContextOptions =
|
||||
|
|
@ -378,8 +395,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
// In headful mode, use host device scale factor for things to look nice.
|
||||
// In headless, keep things the way it works in Playwright by default.
|
||||
// Assume high-dpi on MacOS. TODO: this is not perfect.
|
||||
if (!extraOptions.headless)
|
||||
if (!extraOptions.headless) {
|
||||
contextOptions.deviceScaleFactor = os.platform() === 'darwin' ? 2 : 1;
|
||||
}
|
||||
|
||||
// Work around the WebKit GTK scrolling issue.
|
||||
if (browserType.name() === 'webkit' && process.platform === 'linux') {
|
||||
|
|
@ -387,11 +405,13 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
delete contextOptions.isMobile;
|
||||
}
|
||||
|
||||
if (contextOptions.isMobile && browserType.name() === 'firefox')
|
||||
if (contextOptions.isMobile && browserType.name() === 'firefox') {
|
||||
contextOptions.isMobile = undefined;
|
||||
}
|
||||
|
||||
if (options.blockServiceWorkers)
|
||||
if (options.blockServiceWorkers) {
|
||||
contextOptions.serviceWorkers = 'block';
|
||||
}
|
||||
|
||||
// Proxy
|
||||
|
||||
|
|
@ -399,9 +419,10 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
launchOptions.proxy = {
|
||||
server: options.proxyServer
|
||||
};
|
||||
if (options.proxyBypass)
|
||||
if (options.proxyBypass) {
|
||||
launchOptions.proxy.bypass = options.proxyBypass;
|
||||
}
|
||||
}
|
||||
|
||||
const browser = await browserType.launch(launchOptions);
|
||||
|
||||
|
|
@ -411,8 +432,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
process.stdout.write(text);
|
||||
process.stdout.write('\n-------------8<-------------\n');
|
||||
const autoExitCondition = process.env.PWTEST_CLI_AUTO_EXIT_WHEN;
|
||||
if (autoExitCondition && text.includes(autoExitCondition))
|
||||
if (autoExitCondition && text.includes(autoExitCondition)) {
|
||||
closeBrowser();
|
||||
}
|
||||
};
|
||||
// Make sure we exit abnormally when browser crashes.
|
||||
const logs: string[] = [];
|
||||
|
|
@ -434,8 +456,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
if (options.viewportSize) {
|
||||
try {
|
||||
const [width, height] = options.viewportSize.split(',').map(n => +n);
|
||||
if (isNaN(width) || isNaN(height))
|
||||
if (isNaN(width) || isNaN(height)) {
|
||||
throw new Error('bad values');
|
||||
}
|
||||
contextOptions.viewport = { width, height };
|
||||
} catch (e) {
|
||||
throw new Error('Invalid viewport size format: use "width,height", for example --viewport-size="800,600"');
|
||||
|
|
@ -459,38 +482,45 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
|
||||
// User agent
|
||||
|
||||
if (options.userAgent)
|
||||
if (options.userAgent) {
|
||||
contextOptions.userAgent = options.userAgent;
|
||||
}
|
||||
|
||||
// Lang
|
||||
|
||||
if (options.lang)
|
||||
if (options.lang) {
|
||||
contextOptions.locale = options.lang;
|
||||
}
|
||||
|
||||
// Color scheme
|
||||
|
||||
if (options.colorScheme)
|
||||
if (options.colorScheme) {
|
||||
contextOptions.colorScheme = options.colorScheme as 'dark' | 'light';
|
||||
}
|
||||
|
||||
// Timezone
|
||||
|
||||
if (options.timezone)
|
||||
if (options.timezone) {
|
||||
contextOptions.timezoneId = options.timezone;
|
||||
}
|
||||
|
||||
// Storage
|
||||
|
||||
if (options.loadStorage)
|
||||
if (options.loadStorage) {
|
||||
contextOptions.storageState = options.loadStorage;
|
||||
}
|
||||
|
||||
if (options.ignoreHttpsErrors)
|
||||
if (options.ignoreHttpsErrors) {
|
||||
contextOptions.ignoreHTTPSErrors = true;
|
||||
}
|
||||
|
||||
// HAR
|
||||
|
||||
if (options.saveHar) {
|
||||
contextOptions.recordHar = { path: path.resolve(process.cwd(), options.saveHar), mode: 'minimal' };
|
||||
if (options.saveHarGlob)
|
||||
if (options.saveHarGlob) {
|
||||
contextOptions.recordHar.urlFilter = options.saveHarGlob;
|
||||
}
|
||||
contextOptions.serviceWorkers = 'block';
|
||||
}
|
||||
|
||||
|
|
@ -502,15 +532,19 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
async function closeBrowser() {
|
||||
// We can come here multiple times. For example, saving storage creates
|
||||
// a temporary page and we call closeBrowser again when that page closes.
|
||||
if (closingBrowser)
|
||||
if (closingBrowser) {
|
||||
return;
|
||||
}
|
||||
closingBrowser = true;
|
||||
if (options.saveTrace)
|
||||
if (options.saveTrace) {
|
||||
await context.tracing.stop({ path: options.saveTrace });
|
||||
if (options.saveStorage)
|
||||
}
|
||||
if (options.saveStorage) {
|
||||
await context.storageState({ path: options.saveStorage }).catch(e => null);
|
||||
if (options.saveHar)
|
||||
}
|
||||
if (options.saveHar) {
|
||||
await context.close();
|
||||
}
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
|
|
@ -518,8 +552,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed.
|
||||
page.on('close', () => {
|
||||
const hasPage = browser.contexts().some(context => context.pages().length > 0);
|
||||
if (hasPage)
|
||||
if (hasPage) {
|
||||
return;
|
||||
}
|
||||
// Avoid the error when the last page is closed because the browser has been closed.
|
||||
closeBrowser().catch(() => {});
|
||||
});
|
||||
|
|
@ -533,8 +568,9 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
context.setDefaultTimeout(timeout);
|
||||
context.setDefaultNavigationTimeout(timeout);
|
||||
|
||||
if (options.saveTrace)
|
||||
if (options.saveTrace) {
|
||||
await context.tracing.start({ screenshots: true, snapshots: true });
|
||||
}
|
||||
|
||||
// Omit options that we add automatically for presentation purpose.
|
||||
delete launchOptions.headless;
|
||||
|
|
@ -547,10 +583,11 @@ async function launchContext(options: Options, extraOptions: LaunchOptions): Pro
|
|||
async function openPage(context: BrowserContext, url: string | undefined): Promise<Page> {
|
||||
const page = await context.newPage();
|
||||
if (url) {
|
||||
if (fs.existsSync(url))
|
||||
if (fs.existsSync(url)) {
|
||||
url = 'file://' + path.resolve(url);
|
||||
else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:'))
|
||||
} else if (!url.startsWith('http') && !url.startsWith('file://') && !url.startsWith('about:') && !url.startsWith('data:')) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
await page.goto(url).catch(error => {
|
||||
if (process.env.PWTEST_CLI_AUTO_EXIT_WHEN && isTargetClosedError(error)) {
|
||||
// Tests with PWTEST_CLI_AUTO_EXIT_WHEN might close page too fast, resulting
|
||||
|
|
@ -623,8 +660,9 @@ async function screenshot(options: Options, captureOptions: CaptureOptions, url:
|
|||
}
|
||||
|
||||
async function pdf(options: Options, captureOptions: CaptureOptions, url: string, path: string) {
|
||||
if (options.browser !== 'chromium')
|
||||
if (options.browser !== 'chromium') {
|
||||
throw new Error('PDF creation is only working with Chromium');
|
||||
}
|
||||
const { context } = await launchContext({ ...options, browser: 'chromium' }, { headless: true });
|
||||
console.log('Navigating to ' + url);
|
||||
const page = await openPage(context, url);
|
||||
|
|
@ -650,27 +688,31 @@ function lookupBrowserType(options: Options): BrowserType {
|
|||
case 'wk': browserType = playwright.webkit; break;
|
||||
case 'ff': browserType = playwright.firefox; break;
|
||||
}
|
||||
if (browserType)
|
||||
if (browserType) {
|
||||
return browserType;
|
||||
}
|
||||
program.help();
|
||||
}
|
||||
|
||||
function validateOptions(options: Options) {
|
||||
if (options.device && !(options.device in playwright.devices)) {
|
||||
const lines = [`Device descriptor not found: '${options.device}', available devices are:`];
|
||||
for (const name in playwright.devices)
|
||||
for (const name in playwright.devices) {
|
||||
lines.push(` "${name}"`);
|
||||
}
|
||||
throw new Error(lines.join('\n'));
|
||||
}
|
||||
if (options.colorScheme && !['light', 'dark'].includes(options.colorScheme))
|
||||
if (options.colorScheme && !['light', 'dark'].includes(options.colorScheme)) {
|
||||
throw new Error('Invalid color scheme, should be one of "light", "dark"');
|
||||
}
|
||||
}
|
||||
|
||||
function logErrorAndExit(e: Error) {
|
||||
if (process.env.PWDEBUGIMPL)
|
||||
if (process.env.PWDEBUGIMPL) {
|
||||
console.error(e);
|
||||
else
|
||||
} else {
|
||||
console.error(e.name + ': ' + e.message);
|
||||
}
|
||||
gracefullyProcessExitDoNotHang(1);
|
||||
}
|
||||
|
||||
|
|
@ -680,8 +722,9 @@ function codegenId(): string {
|
|||
|
||||
function commandWithOpenOptions(command: string, description: string, options: any[][]): Command {
|
||||
let result = program.command(command).description(description);
|
||||
for (const option of options)
|
||||
for (const option of options) {
|
||||
result = result.option(option[0], ...option.slice(1));
|
||||
}
|
||||
return result
|
||||
.option('-b, --browser <browserType>', 'browser to use, one of cr, chromium, ff, firefox, wk, webkit', 'chromium')
|
||||
.option('--block-service-workers', 'block service workers')
|
||||
|
|
|
|||
|
|
@ -29,8 +29,9 @@ function printPlaywrightTestError(command: string) {
|
|||
} catch (e) {
|
||||
}
|
||||
}
|
||||
if (!packages.length)
|
||||
if (!packages.length) {
|
||||
packages.push('playwright');
|
||||
}
|
||||
const packageManager = getPackageManager();
|
||||
if (packageManager === 'yarn') {
|
||||
console.error(`Please install @playwright/test package before running "yarn playwright ${command}"`);
|
||||
|
|
@ -63,5 +64,6 @@ function addExternalPlaywrightTestCommands() {
|
|||
}
|
||||
}
|
||||
|
||||
if (!process.env.PW_LANG_NAME)
|
||||
if (!process.env.PW_LANG_NAME) {
|
||||
addExternalPlaywrightTestCommands();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,8 +58,9 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
|
|||
}
|
||||
|
||||
async launchServer(options: types.LaunchServerOptions = {}): Promise<api.BrowserServer> {
|
||||
if (!this._serverLauncher)
|
||||
if (!this._serverLauncher) {
|
||||
throw new Error('Launching server is not supported');
|
||||
}
|
||||
return await this._serverLauncher.launchServer(options);
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +79,7 @@ export class Android extends ChannelOwner<channels.AndroidChannel> implements ap
|
|||
let device: AndroidDevice;
|
||||
let closeError: string | undefined;
|
||||
const onPipeClosed = () => {
|
||||
device?._didClose();
|
||||
device._didClose();
|
||||
connection.close(closeError);
|
||||
};
|
||||
pipe.on('closed', onPipeClosed);
|
||||
|
|
@ -143,9 +144,10 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
private _onWebViewRemoved(socketName: string) {
|
||||
const view = this._webViews.get(socketName);
|
||||
this._webViews.delete(socketName);
|
||||
if (view)
|
||||
if (view) {
|
||||
view.emit(Events.AndroidWebView.Close);
|
||||
}
|
||||
}
|
||||
|
||||
setDefaultTimeout(timeout: number) {
|
||||
this._timeoutSettings.setDefaultTimeout(timeout);
|
||||
|
|
@ -166,15 +168,18 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
|
||||
async webView(selector: { pkg?: string; socketName?: string; }, options?: types.TimeoutOptions): Promise<AndroidWebView> {
|
||||
const predicate = (v: AndroidWebView) => {
|
||||
if (selector.pkg)
|
||||
if (selector.pkg) {
|
||||
return v.pkg() === selector.pkg;
|
||||
if (selector.socketName)
|
||||
}
|
||||
if (selector.socketName) {
|
||||
return v._socketName() === selector.socketName;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const webView = [...this._webViews.values()].find(predicate);
|
||||
if (webView)
|
||||
if (webView) {
|
||||
return webView;
|
||||
}
|
||||
return await this.waitForEvent('webview', { ...options, predicate });
|
||||
}
|
||||
|
||||
|
|
@ -229,8 +234,9 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
|
||||
async screenshot(options: { path?: string } = {}): Promise<Buffer> {
|
||||
const { binary } = await this._channel.screenshot();
|
||||
if (options.path)
|
||||
if (options.path) {
|
||||
await fs.promises.writeFile(options.path, binary);
|
||||
}
|
||||
return binary;
|
||||
}
|
||||
|
||||
|
|
@ -240,13 +246,15 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
|
||||
async close() {
|
||||
try {
|
||||
if (this._shouldCloseConnectionOnClose)
|
||||
if (this._shouldCloseConnectionOnClose) {
|
||||
this._connection.close();
|
||||
else
|
||||
} else {
|
||||
await this._channel.close();
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
if (isTargetClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -286,8 +294,9 @@ export class AndroidDevice extends ChannelOwner<channels.AndroidDeviceChannel> i
|
|||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== Events.AndroidDevice.Close)
|
||||
if (event !== Events.AndroidDevice.Close) {
|
||||
waiter.rejectOnEvent(this, Events.AndroidDevice.Close, () => new TargetClosedError());
|
||||
}
|
||||
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
|
|
@ -320,8 +329,9 @@ export class AndroidSocket extends ChannelOwner<channels.AndroidSocketChannel> i
|
|||
}
|
||||
|
||||
async function loadFile(file: string | Buffer): Promise<Buffer> {
|
||||
if (isString(file))
|
||||
if (isString(file)) {
|
||||
return await fs.promises.readFile(file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
|
|
@ -375,10 +385,12 @@ function toSelectorChannel(selector: api.AndroidSelector): channels.AndroidSelec
|
|||
} = selector;
|
||||
|
||||
const toRegex = (value: RegExp | string | undefined): string | undefined => {
|
||||
if (value === undefined)
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
if (isRegExp(value))
|
||||
}
|
||||
if (isRegExp(value)) {
|
||||
return value.source;
|
||||
}
|
||||
return '^' + value.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') + '$';
|
||||
};
|
||||
|
||||
|
|
@ -427,8 +439,9 @@ export class AndroidWebView extends EventEmitter implements api.AndroidWebView {
|
|||
}
|
||||
|
||||
async page(): Promise<Page> {
|
||||
if (!this._pagePromise)
|
||||
if (!this._pagePromise) {
|
||||
this._pagePromise = this._fetchPage();
|
||||
}
|
||||
return await this._pagePromise;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ export class Artifact extends ChannelOwner<channels.ArtifactChannel> {
|
|||
}
|
||||
|
||||
async pathAfterFinished(): Promise<string> {
|
||||
if (this._connection.isRemote())
|
||||
if (this._connection.isRemote()) {
|
||||
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
|
||||
}
|
||||
return (await this._channel.pathAfterFinished()).value;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,9 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
return await this._wrapApiCall(async () => {
|
||||
for (const context of this._contexts) {
|
||||
await this._browserType._willCloseContext(context);
|
||||
for (const page of context.pages())
|
||||
for (const page of context.pages()) {
|
||||
page._onClose();
|
||||
}
|
||||
context._onClose();
|
||||
}
|
||||
return await this._innerNewContext(options, true);
|
||||
|
|
@ -138,14 +139,16 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
|
|||
async close(options: { reason?: string } = {}): Promise<void> {
|
||||
this._closeReason = options.reason;
|
||||
try {
|
||||
if (this._shouldCloseConnectionOnClose)
|
||||
if (this._shouldCloseConnectionOnClose) {
|
||||
this._connection.close();
|
||||
else
|
||||
} else {
|
||||
await this._channel.close(options);
|
||||
}
|
||||
await this._closedPromise;
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
if (isTargetClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,8 +79,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
|
||||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.BrowserContextInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
if (parent instanceof Browser)
|
||||
if (parent instanceof Browser) {
|
||||
this._browser = parent;
|
||||
}
|
||||
this._browser?._contexts.add(this);
|
||||
this._isChromium = this._browser?._name === 'chromium';
|
||||
this.tracing = Tracing.from(initializer.tracing);
|
||||
|
|
@ -107,32 +108,36 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const consoleMessage = new ConsoleMessage(event);
|
||||
this.emit(Events.BrowserContext.Console, consoleMessage);
|
||||
const page = consoleMessage.page();
|
||||
if (page)
|
||||
if (page) {
|
||||
page.emit(Events.Page.Console, consoleMessage);
|
||||
}
|
||||
});
|
||||
this._channel.on('pageError', ({ error, page }) => {
|
||||
const pageObject = Page.from(page);
|
||||
const parsedError = parseError(error);
|
||||
this.emit(Events.BrowserContext.WebError, new WebError(pageObject, parsedError));
|
||||
if (pageObject)
|
||||
if (pageObject) {
|
||||
pageObject.emit(Events.Page.PageError, parsedError);
|
||||
}
|
||||
});
|
||||
this._channel.on('dialog', ({ dialog }) => {
|
||||
const dialogObject = Dialog.from(dialog);
|
||||
let hasListeners = this.emit(Events.BrowserContext.Dialog, dialogObject);
|
||||
const page = dialogObject.page();
|
||||
if (page)
|
||||
if (page) {
|
||||
hasListeners = page.emit(Events.Page.Dialog, dialogObject) || hasListeners;
|
||||
}
|
||||
if (!hasListeners) {
|
||||
// Although we do similar handling on the server side, we still need this logic
|
||||
// on the client side due to a possible race condition between two async calls:
|
||||
// a) removing "dialog" listener subscription (client->server)
|
||||
// b) actual "dialog" event (server->client)
|
||||
if (dialogObject.type() === 'beforeunload')
|
||||
if (dialogObject.type() === 'beforeunload') {
|
||||
dialog.accept({}).catch(() => {});
|
||||
else
|
||||
} else {
|
||||
dialog.dismiss().catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
this._channel.on('request', ({ request, page }) => this._onRequest(network.Request.from(request), Page.fromNullable(page)));
|
||||
this._channel.on('requestFailed', ({ request, failureText, responseEndTiming, page }) => this._onRequestFailed(network.Request.from(request), responseEndTiming, failureText, Page.fromNullable(page)));
|
||||
|
|
@ -152,37 +157,42 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
|
||||
_setOptions(contextOptions: channels.BrowserNewContextParams, browserOptions: LaunchOptions) {
|
||||
this._options = contextOptions;
|
||||
if (this._options.recordHar)
|
||||
if (this._options.recordHar) {
|
||||
this._harRecorders.set('', { path: this._options.recordHar.path, content: this._options.recordHar.content });
|
||||
}
|
||||
this.tracing._tracesDir = browserOptions.tracesDir;
|
||||
}
|
||||
|
||||
private _onPage(page: Page): void {
|
||||
this._pages.add(page);
|
||||
this.emit(Events.BrowserContext.Page, page);
|
||||
if (page._opener && !page._opener.isClosed())
|
||||
if (page._opener && !page._opener.isClosed()) {
|
||||
page._opener.emit(Events.Page.Popup, page);
|
||||
}
|
||||
}
|
||||
|
||||
private _onRequest(request: network.Request, page: Page | null) {
|
||||
this.emit(Events.BrowserContext.Request, request);
|
||||
if (page)
|
||||
if (page) {
|
||||
page.emit(Events.Page.Request, request);
|
||||
}
|
||||
}
|
||||
|
||||
private _onResponse(response: network.Response, page: Page | null) {
|
||||
this.emit(Events.BrowserContext.Response, response);
|
||||
if (page)
|
||||
if (page) {
|
||||
page.emit(Events.Page.Response, response);
|
||||
}
|
||||
}
|
||||
|
||||
private _onRequestFailed(request: network.Request, responseEndTiming: number, failureText: string | undefined, page: Page | null) {
|
||||
request._failureText = failureText || null;
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(Events.BrowserContext.RequestFailed, request);
|
||||
if (page)
|
||||
if (page) {
|
||||
page.emit(Events.Page.RequestFailed, request);
|
||||
}
|
||||
}
|
||||
|
||||
private _onRequestFinished(params: channels.BrowserContextRequestFinishedEvent) {
|
||||
const { responseEndTiming } = params;
|
||||
|
|
@ -191,11 +201,13 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const page = Page.fromNullable(params.page);
|
||||
request._setResponseEndTiming(responseEndTiming);
|
||||
this.emit(Events.BrowserContext.RequestFinished, request);
|
||||
if (page)
|
||||
if (page) {
|
||||
page.emit(Events.Page.RequestFinished, request);
|
||||
if (response)
|
||||
}
|
||||
if (response) {
|
||||
response._finishedPromise.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
async _onRoute(route: network.Route) {
|
||||
route._context = this;
|
||||
|
|
@ -203,21 +215,27 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
// If the page or the context was closed we stall all requests right away.
|
||||
if (page?._closeWasCalled || this._closeWasCalled)
|
||||
if (page?._closeWasCalled || this._closeWasCalled) {
|
||||
return;
|
||||
if (!routeHandler.matches(route.request().url()))
|
||||
}
|
||||
if (!routeHandler.matches(route.request().url())) {
|
||||
continue;
|
||||
}
|
||||
const index = this._routes.indexOf(routeHandler);
|
||||
if (index === -1)
|
||||
if (index === -1) {
|
||||
continue;
|
||||
if (routeHandler.willExpire())
|
||||
}
|
||||
if (routeHandler.willExpire()) {
|
||||
this._routes.splice(index, 1);
|
||||
}
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length)
|
||||
if (!this._routes.length) {
|
||||
this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
|
||||
if (handled)
|
||||
}
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If the page is closed or unrouteAll() was called without waiting and interception disabled,
|
||||
// the method will throw an error - silence it.
|
||||
await route._innerContinue(true /* isFallback */).catch(() => {});
|
||||
|
|
@ -225,16 +243,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
|
||||
async _onWebSocketRoute(webSocketRoute: network.WebSocketRoute) {
|
||||
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
|
||||
if (routeHandler)
|
||||
if (routeHandler) {
|
||||
await routeHandler.handle(webSocketRoute);
|
||||
else
|
||||
} else {
|
||||
webSocketRoute.connectToServer();
|
||||
}
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
const func = this._bindings.get(bindingCall._initializer.name);
|
||||
if (!func)
|
||||
if (!func) {
|
||||
return;
|
||||
}
|
||||
await bindingCall.call(func);
|
||||
}
|
||||
|
||||
|
|
@ -261,16 +281,19 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async newPage(): Promise<Page> {
|
||||
if (this._ownerPage)
|
||||
if (this._ownerPage) {
|
||||
throw new Error('Please use browser.newContext()');
|
||||
}
|
||||
return Page.from((await this._channel.newPage()).page);
|
||||
}
|
||||
|
||||
async cookies(urls?: string | string[]): Promise<network.NetworkCookie[]> {
|
||||
if (!urls)
|
||||
if (!urls) {
|
||||
urls = [];
|
||||
if (urls && typeof urls === 'string')
|
||||
}
|
||||
if (urls && typeof urls === 'string') {
|
||||
urls = [urls];
|
||||
}
|
||||
return (await this._channel.cookies({ urls: urls as string[] })).cookies;
|
||||
}
|
||||
|
||||
|
|
@ -380,19 +403,21 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const removed = [];
|
||||
const remaining = [];
|
||||
for (const route of this._routes) {
|
||||
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler))
|
||||
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler)) {
|
||||
removed.push(route);
|
||||
else
|
||||
} else {
|
||||
remaining.push(route);
|
||||
}
|
||||
}
|
||||
await this._unrouteInternal(removed, remaining, 'default');
|
||||
}
|
||||
|
||||
private async _unrouteInternal(removed: network.RouteHandler[], remaining: network.RouteHandler[], behavior?: 'wait'|'ignoreErrors'|'default'): Promise<void> {
|
||||
this._routes = remaining;
|
||||
await this._updateInterceptionPatterns();
|
||||
if (!behavior || behavior === 'default')
|
||||
if (!behavior || behavior === 'default') {
|
||||
return;
|
||||
}
|
||||
const promises = removed.map(routeHandler => routeHandler.stop(behavior));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
|
@ -417,8 +442,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== Events.BrowserContext.Close)
|
||||
if (event !== Events.BrowserContext.Close) {
|
||||
waiter.rejectOnEvent(this, Events.BrowserContext.Close, () => new TargetClosedError(this._effectiveCloseReason()));
|
||||
}
|
||||
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
|
|
@ -444,16 +470,18 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
|
||||
async newCDPSession(page: Page | Frame): Promise<api.CDPSession> {
|
||||
// channelOwner.ts's validation messages don't handle the pseudo-union type, so we're explicit here
|
||||
if (!(page instanceof Page) && !(page instanceof Frame))
|
||||
if (!(page instanceof Page) && !(page instanceof Frame)) {
|
||||
throw new Error('page: expected Page or Frame');
|
||||
}
|
||||
const result = await this._channel.newCDPSession(page instanceof Page ? { page: page._channel } : { frame: page._channel });
|
||||
return CDPSession.from(result.session);
|
||||
}
|
||||
|
||||
_onClose() {
|
||||
if (this._browser)
|
||||
if (this._browser) {
|
||||
this._browser._contexts.delete(this);
|
||||
this._browserType?._contexts?.delete(this);
|
||||
}
|
||||
this._browserType?._contexts.delete(this);
|
||||
this._disposeHarRouters();
|
||||
this.tracing._resetStackCounter();
|
||||
this.emit(Events.BrowserContext.Close, this);
|
||||
|
|
@ -464,8 +492,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async close(options: { reason?: string } = {}): Promise<void> {
|
||||
if (this._closeWasCalled)
|
||||
if (this._closeWasCalled) {
|
||||
return;
|
||||
}
|
||||
this._closeReason = options.reason;
|
||||
this._closeWasCalled = true;
|
||||
await this._wrapApiCall(async () => {
|
||||
|
|
@ -498,8 +527,9 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
}
|
||||
|
||||
async function prepareStorageState(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams['storageState']> {
|
||||
if (typeof options.storageState !== 'string')
|
||||
if (typeof options.storageState !== 'string') {
|
||||
return options.storageState;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8'));
|
||||
} catch (e) {
|
||||
|
|
@ -509,8 +539,9 @@ async function prepareStorageState(options: BrowserContextOptions): Promise<chan
|
|||
}
|
||||
|
||||
function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): channels.RecordHarOptions | undefined {
|
||||
if (!options)
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
path: options.path,
|
||||
content: options.content || (options.omitContent ? 'omit' : undefined),
|
||||
|
|
@ -522,10 +553,12 @@ function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): c
|
|||
}
|
||||
|
||||
export async function prepareBrowserContextParams(options: BrowserContextOptions): Promise<channels.BrowserNewContextParams> {
|
||||
if (options.videoSize && !options.videosPath)
|
||||
if (options.videoSize && !options.videosPath) {
|
||||
throw new Error(`"videoSize" option requires "videosPath" to be specified`);
|
||||
if (options.extraHTTPHeaders)
|
||||
}
|
||||
if (options.extraHTTPHeaders) {
|
||||
network.validateHeaders(options.extraHTTPHeaders);
|
||||
}
|
||||
const contextParams: channels.BrowserNewContextParams = {
|
||||
...options,
|
||||
viewport: options.viewport === null ? undefined : options.viewport,
|
||||
|
|
@ -546,28 +579,34 @@ export async function prepareBrowserContextParams(options: BrowserContextOptions
|
|||
size: options.videoSize
|
||||
};
|
||||
}
|
||||
if (contextParams.recordVideo && contextParams.recordVideo.dir)
|
||||
if (contextParams.recordVideo && contextParams.recordVideo.dir) {
|
||||
contextParams.recordVideo.dir = path.resolve(process.cwd(), contextParams.recordVideo.dir);
|
||||
}
|
||||
return contextParams;
|
||||
}
|
||||
|
||||
function toAcceptDownloadsProtocol(acceptDownloads?: boolean) {
|
||||
if (acceptDownloads === undefined)
|
||||
if (acceptDownloads === undefined) {
|
||||
return undefined;
|
||||
if (acceptDownloads)
|
||||
}
|
||||
if (acceptDownloads) {
|
||||
return 'accept';
|
||||
}
|
||||
return 'deny';
|
||||
}
|
||||
|
||||
export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise<channels.PlaywrightNewRequestParams['clientCertificates']> {
|
||||
if (!certs)
|
||||
if (!certs) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const bufferizeContent = async (value?: Buffer, path?: string): Promise<Buffer | undefined> => {
|
||||
if (value)
|
||||
if (value) {
|
||||
return value;
|
||||
if (path)
|
||||
}
|
||||
if (path) {
|
||||
return await fs.promises.readFile(path);
|
||||
}
|
||||
};
|
||||
|
||||
return await Promise.all(certs.map(async cert => ({
|
||||
|
|
|
|||
|
|
@ -56,8 +56,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
}
|
||||
|
||||
executablePath(): string {
|
||||
if (!this._initializer.executablePath)
|
||||
if (!this._initializer.executablePath) {
|
||||
throw new Error('Browser is not supported on current platform');
|
||||
}
|
||||
return this._initializer.executablePath;
|
||||
}
|
||||
|
||||
|
|
@ -85,8 +86,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
}
|
||||
|
||||
async launchServer(options: LaunchServerOptions = {}): Promise<api.BrowserServer> {
|
||||
if (!this._serverLauncher)
|
||||
if (!this._serverLauncher) {
|
||||
throw new Error('Launching server is not supported');
|
||||
}
|
||||
options = { ...this._defaultLaunchOptions, ...options };
|
||||
return await this._serverLauncher.launchServer(options);
|
||||
}
|
||||
|
|
@ -115,8 +117,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise<api.Browser>;
|
||||
connect(wsEndpoint: string, options?: api.ConnectOptions): Promise<api.Browser>;
|
||||
async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint: string }), options?: api.ConnectOptions): Promise<Browser>{
|
||||
if (typeof optionsOrWsEndpoint === 'string')
|
||||
if (typeof optionsOrWsEndpoint === 'string') {
|
||||
return await this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint });
|
||||
}
|
||||
assert(optionsOrWsEndpoint.wsEndpoint, 'options.wsEndpoint is required');
|
||||
return await this._connect(optionsOrWsEndpoint);
|
||||
}
|
||||
|
|
@ -134,8 +137,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
slowMo: params.slowMo,
|
||||
timeout: params.timeout,
|
||||
};
|
||||
if ((params as any).__testHookRedirectPortForwarding)
|
||||
if ((params as any).__testHookRedirectPortForwarding) {
|
||||
connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding;
|
||||
}
|
||||
const { pipe, headers: connectHeaders } = await localUtils._channel.connect(connectParams);
|
||||
const closePipe = () => pipe.close().catch(() => {});
|
||||
const connection = new Connection(localUtils, this._instrumentation);
|
||||
|
|
@ -146,9 +150,10 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
let closeError: string | undefined;
|
||||
const onPipeClosed = (reason?: string) => {
|
||||
// Emulate all pages, contexts and the browser closing upon disconnect.
|
||||
for (const context of browser?.contexts() || []) {
|
||||
for (const page of context.pages())
|
||||
for (const context of browser.contexts() || []) {
|
||||
for (const page of context.pages()) {
|
||||
page._onClose();
|
||||
}
|
||||
context._onClose();
|
||||
}
|
||||
connection.close(reason || closeError);
|
||||
|
|
@ -158,7 +163,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
// here and promises did not have a chance to reject.
|
||||
// The order of rejects vs closure is a part of the API contract and our test runner
|
||||
// relies on it to attribute rejections to the right test.
|
||||
setTimeout(() => browser?._didClose(), 0);
|
||||
setTimeout(() => browser._didClose(), 0);
|
||||
};
|
||||
pipe.on('closed', params => onPipeClosed(params.reason));
|
||||
connection.onmessage = message => this._wrapApiCall(() => pipe.send({ message }).catch(() => onPipeClosed()), /* isInternal */ true);
|
||||
|
|
@ -174,8 +179,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
|
||||
const result = await raceAgainstDeadline(async () => {
|
||||
// For tests.
|
||||
if ((params as any).__testHookBeforeCreateBrowser)
|
||||
if ((params as any).__testHookBeforeCreateBrowser) {
|
||||
await (params as any).__testHookBeforeCreateBrowser();
|
||||
}
|
||||
|
||||
const playwright = await connection!.initializePlaywright();
|
||||
if (!playwright._initializer.preLaunchedBrowser) {
|
||||
|
|
@ -202,16 +208,18 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
async connectOverCDP(options: api.ConnectOverCDPOptions & { wsEndpoint?: string }): Promise<api.Browser>;
|
||||
async connectOverCDP(endpointURL: string, options?: api.ConnectOverCDPOptions): Promise<api.Browser>;
|
||||
async connectOverCDP(endpointURLOrOptions: (api.ConnectOverCDPOptions & { wsEndpoint?: string })|string, options?: api.ConnectOverCDPOptions) {
|
||||
if (typeof endpointURLOrOptions === 'string')
|
||||
if (typeof endpointURLOrOptions === 'string') {
|
||||
return await this._connectOverCDP(endpointURLOrOptions, options);
|
||||
}
|
||||
const endpointURL = 'endpointURL' in endpointURLOrOptions ? endpointURLOrOptions.endpointURL : endpointURLOrOptions.wsEndpoint;
|
||||
assert(endpointURL, 'Cannot connect over CDP without wsEndpoint.');
|
||||
return await this.connectOverCDP(endpointURL, endpointURLOrOptions);
|
||||
}
|
||||
|
||||
async _connectOverCDP(endpointURL: string, params: api.ConnectOverCDPOptions = {}): Promise<Browser> {
|
||||
if (this.name() !== 'chromium')
|
||||
if (this.name() !== 'chromium') {
|
||||
throw new Error('Connecting over CDP is only supported in Chromium.');
|
||||
}
|
||||
const headers = params.headers ? headersObjectToArray(params.headers) : undefined;
|
||||
const result = await this._channel.connectOverCDP({
|
||||
endpointURL,
|
||||
|
|
@ -221,8 +229,9 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
});
|
||||
const browser = Browser.from(result.browser);
|
||||
this._didLaunchBrowser(browser, {}, params.logger);
|
||||
if (result.defaultContext)
|
||||
if (result.defaultContext) {
|
||||
await this._didCreateContext(BrowserContext.from(result.defaultContext), {}, {}, params.logger);
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
|
|
@ -237,10 +246,12 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel> imple
|
|||
context._browserType = this;
|
||||
this._contexts.add(context);
|
||||
context._setOptions(contextOptions, browserOptions);
|
||||
if (this._defaultContextTimeout !== undefined)
|
||||
if (this._defaultContextTimeout !== undefined) {
|
||||
context.setDefaultTimeout(this._defaultContextTimeout);
|
||||
if (this._defaultContextNavigationTimeout !== undefined)
|
||||
}
|
||||
if (this._defaultContextNavigationTimeout !== undefined) {
|
||||
context.setDefaultNavigationTimeout(this._defaultContextNavigationTimeout);
|
||||
}
|
||||
await this._instrumentation.runAfterCreateBrowserContext(context);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,37 +80,42 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
}
|
||||
|
||||
override on(event: string | symbol, listener: Listener): this {
|
||||
if (!this.listenerCount(event))
|
||||
if (!this.listenerCount(event)) {
|
||||
this._updateSubscription(event, true);
|
||||
}
|
||||
super.on(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
override addListener(event: string | symbol, listener: Listener): this {
|
||||
if (!this.listenerCount(event))
|
||||
if (!this.listenerCount(event)) {
|
||||
this._updateSubscription(event, true);
|
||||
}
|
||||
super.addListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
override prependListener(event: string | symbol, listener: Listener): this {
|
||||
if (!this.listenerCount(event))
|
||||
if (!this.listenerCount(event)) {
|
||||
this._updateSubscription(event, true);
|
||||
}
|
||||
super.prependListener(event, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
override off(event: string | symbol, listener: Listener): this {
|
||||
super.off(event, listener);
|
||||
if (!this.listenerCount(event))
|
||||
if (!this.listenerCount(event)) {
|
||||
this._updateSubscription(event, false);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
override removeListener(event: string | symbol, listener: Listener): this {
|
||||
super.removeListener(event, listener);
|
||||
if (!this.listenerCount(event))
|
||||
if (!this.listenerCount(event)) {
|
||||
this._updateSubscription(event, false);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -122,14 +127,16 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
|
||||
_dispose(reason: 'gc' | undefined) {
|
||||
// Clean up from parent and connection.
|
||||
if (this._parent)
|
||||
if (this._parent) {
|
||||
this._parent._objects.delete(this._guid);
|
||||
}
|
||||
this._connection._objects.delete(this._guid);
|
||||
this._wasCollected = reason === 'gc';
|
||||
|
||||
// Dispose all children.
|
||||
for (const object of [...this._objects.values()])
|
||||
for (const object of [...this._objects.values()]) {
|
||||
object._dispose(reason);
|
||||
}
|
||||
this._objects.clear();
|
||||
}
|
||||
|
||||
|
|
@ -171,23 +178,27 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
async _wrapApiCall<R>(func: (apiZone: ApiZone) => Promise<R>, isInternal?: boolean): Promise<R> {
|
||||
const logger = this._logger;
|
||||
const apiZone = zones.zoneData<ApiZone>('apiZone');
|
||||
if (apiZone)
|
||||
if (apiZone) {
|
||||
return await func(apiZone);
|
||||
}
|
||||
|
||||
const stackTrace = captureLibraryStackTrace();
|
||||
let apiName: string | undefined = stackTrace.apiName;
|
||||
const frames: channels.StackFrame[] = stackTrace.frames;
|
||||
|
||||
if (isInternal === undefined)
|
||||
if (isInternal === undefined) {
|
||||
isInternal = this._isInternalType;
|
||||
if (isInternal)
|
||||
}
|
||||
if (isInternal) {
|
||||
apiName = undefined;
|
||||
}
|
||||
|
||||
// Enclosing zone could have provided the apiName and wallTime.
|
||||
const expectZone = zones.zoneData<ExpectZone>('expectZone');
|
||||
const stepId = expectZone?.stepId;
|
||||
if (!isInternal && expectZone)
|
||||
if (!isInternal && expectZone) {
|
||||
apiName = expectZone.title;
|
||||
}
|
||||
|
||||
// If we are coming from the expectZone, there is no need to generate a new
|
||||
// step for the API call, since it will be generated by the expect itself.
|
||||
|
|
@ -203,13 +214,15 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
return result;
|
||||
} catch (e) {
|
||||
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
|
||||
if (apiName && !apiName.includes('<anonymous>'))
|
||||
if (apiName && !apiName.includes('<anonymous>')) {
|
||||
e.message = apiName + ': ' + e.message;
|
||||
}
|
||||
const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError;
|
||||
if (stackFrames.trim())
|
||||
if (stackFrames.trim()) {
|
||||
e.stack = e.message + stackFrames;
|
||||
else
|
||||
} else {
|
||||
e.stack = '';
|
||||
}
|
||||
csi?.onApiCallEnd(callCookie, e);
|
||||
logApiCall(logger, `<= ${apiName} failed`, isInternal);
|
||||
throw e;
|
||||
|
|
@ -233,16 +246,19 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
|||
}
|
||||
|
||||
function logApiCall(logger: Logger | undefined, message: string, isNested: boolean) {
|
||||
if (isNested)
|
||||
if (isNested) {
|
||||
return;
|
||||
if (logger && logger.isEnabled('api', 'info'))
|
||||
}
|
||||
if (logger && logger.isEnabled('api', 'info')) {
|
||||
logger.log('api', 'info', message, [], { color: 'cyan' });
|
||||
}
|
||||
debugLogger.log('api', message);
|
||||
}
|
||||
|
||||
function tChannelImplToWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
|
||||
if (arg._object instanceof ChannelOwner && (names === '*' || names.includes(arg._object._type)))
|
||||
if (arg._object instanceof ChannelOwner && (names === '*' || names.includes(arg._object._type))) {
|
||||
return { guid: arg._object._guid };
|
||||
}
|
||||
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,9 +22,10 @@ import { isString } from '../utils';
|
|||
export function envObjectToArray(env: types.Env): { name: string, value: string }[] {
|
||||
const result: { name: string, value: string }[] = [];
|
||||
for (const name in env) {
|
||||
if (!Object.is(env[name], undefined))
|
||||
if (!Object.is(env[name], undefined)) {
|
||||
result.push({ name, value: String(env[name]) });
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -34,16 +35,20 @@ export async function evaluationScript(fun: Function | string | { path?: string,
|
|||
const argString = Object.is(arg, undefined) ? 'undefined' : JSON.stringify(arg);
|
||||
return `(${source})(${argString})`;
|
||||
}
|
||||
if (arg !== undefined)
|
||||
if (arg !== undefined) {
|
||||
throw new Error('Cannot evaluate a string with arguments');
|
||||
if (isString(fun))
|
||||
}
|
||||
if (isString(fun)) {
|
||||
return fun;
|
||||
if (fun.content !== undefined)
|
||||
}
|
||||
if (fun.content !== undefined) {
|
||||
return fun.content;
|
||||
}
|
||||
if (fun.path !== undefined) {
|
||||
let source = await fs.promises.readFile(fun.path, 'utf8');
|
||||
if (addSourceUrl)
|
||||
if (addSourceUrl) {
|
||||
source = addSourceUrlToScript(source, fun.path);
|
||||
}
|
||||
return source;
|
||||
}
|
||||
throw new Error('Either path or content property must be present');
|
||||
|
|
|
|||
|
|
@ -47,24 +47,30 @@ export function createInstrumentation(): ClientInstrumentation {
|
|||
const listeners: ClientInstrumentationListener[] = [];
|
||||
return new Proxy({}, {
|
||||
get: (obj: any, prop: string | symbol) => {
|
||||
if (typeof prop !== 'string')
|
||||
if (typeof prop !== 'string') {
|
||||
return obj[prop];
|
||||
if (prop === 'addListener')
|
||||
}
|
||||
if (prop === 'addListener') {
|
||||
return (listener: ClientInstrumentationListener) => listeners.push(listener);
|
||||
if (prop === 'removeListener')
|
||||
}
|
||||
if (prop === 'removeListener') {
|
||||
return (listener: ClientInstrumentationListener) => listeners.splice(listeners.indexOf(listener), 1);
|
||||
if (prop === 'removeAllListeners')
|
||||
}
|
||||
if (prop === 'removeAllListeners') {
|
||||
return () => listeners.splice(0, listeners.length);
|
||||
}
|
||||
if (prop.startsWith('run')) {
|
||||
return async (...params: any[]) => {
|
||||
for (const listener of listeners)
|
||||
for (const listener of listeners) {
|
||||
await (listener as any)[prop]?.(...params);
|
||||
}
|
||||
};
|
||||
}
|
||||
if (prop.startsWith('on')) {
|
||||
return (...params: any[]) => {
|
||||
for (const listener of listeners)
|
||||
for (const listener of listeners) {
|
||||
(listener as any)[prop]?.(...params);
|
||||
}
|
||||
};
|
||||
}
|
||||
return obj[prop];
|
||||
|
|
|
|||
|
|
@ -54,12 +54,15 @@ export class Clock implements api.Clock {
|
|||
}
|
||||
|
||||
function parseTime(time: string | number | Date): { timeNumber?: number, timeString?: string } {
|
||||
if (typeof time === 'number')
|
||||
if (typeof time === 'number') {
|
||||
return { timeNumber: time };
|
||||
if (typeof time === 'string')
|
||||
}
|
||||
if (typeof time === 'string') {
|
||||
return { timeString: time };
|
||||
if (!isFinite(time.getTime()))
|
||||
}
|
||||
if (!isFinite(time.getTime())) {
|
||||
throw new Error(`Invalid date: ${time}`);
|
||||
}
|
||||
return { timeNumber: time.getTime() };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -112,17 +112,20 @@ export class Connection extends EventEmitter {
|
|||
}
|
||||
|
||||
setIsTracing(isTracing: boolean) {
|
||||
if (isTracing)
|
||||
if (isTracing) {
|
||||
this._tracingCount++;
|
||||
else
|
||||
} else {
|
||||
this._tracingCount--;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], stepId?: string): Promise<any> {
|
||||
if (this._closedError)
|
||||
if (this._closedError) {
|
||||
throw this._closedError;
|
||||
if (object._wasCollected)
|
||||
}
|
||||
if (object._wasCollected) {
|
||||
throw new Error('The object has been collected to prevent unbounded heap growth.');
|
||||
}
|
||||
|
||||
const guid = object._guid;
|
||||
const type = object._type;
|
||||
|
|
@ -134,8 +137,9 @@ export class Connection extends EventEmitter {
|
|||
}
|
||||
const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined;
|
||||
const metadata: channels.Metadata = { apiName, location, internal: !apiName, stepId };
|
||||
if (this._tracingCount && frames && type !== 'LocalUtils')
|
||||
if (this._tracingCount && frames && type !== 'LocalUtils') {
|
||||
this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {});
|
||||
}
|
||||
// We need to exit zones before calling into the server, otherwise
|
||||
// when we receive events from the server, we would be in an API zone.
|
||||
zones.exitZones(() => this.onmessage({ ...message, metadata }));
|
||||
|
|
@ -143,16 +147,19 @@ export class Connection extends EventEmitter {
|
|||
}
|
||||
|
||||
dispatch(message: object) {
|
||||
if (this._closedError)
|
||||
if (this._closedError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, guid, method, params, result, error, log } = message as any;
|
||||
if (id) {
|
||||
if (debugLogger.isEnabled('channel'))
|
||||
if (debugLogger.isEnabled('channel')) {
|
||||
debugLogger.log('channel', '<RECV ' + JSON.stringify(message));
|
||||
}
|
||||
const callback = this._callbacks.get(id);
|
||||
if (!callback)
|
||||
if (!callback) {
|
||||
throw new Error(`Cannot find command to respond: ${id}`);
|
||||
}
|
||||
this._callbacks.delete(id);
|
||||
if (error && !result) {
|
||||
const parsedError = parseError(error);
|
||||
|
|
@ -165,21 +172,24 @@ export class Connection extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
if (debugLogger.isEnabled('channel'))
|
||||
if (debugLogger.isEnabled('channel')) {
|
||||
debugLogger.log('channel', '<EVENT ' + JSON.stringify(message));
|
||||
}
|
||||
if (method === '__create__') {
|
||||
this._createRemoteObject(guid, params.type, params.guid, params.initializer);
|
||||
return;
|
||||
}
|
||||
|
||||
const object = this._objects.get(guid);
|
||||
if (!object)
|
||||
if (!object) {
|
||||
throw new Error(`Cannot find object to "${method}": ${guid}`);
|
||||
}
|
||||
|
||||
if (method === '__adopt__') {
|
||||
const child = this._objects.get(params.guid);
|
||||
if (!child)
|
||||
if (!child) {
|
||||
throw new Error(`Unknown new child: ${params.guid}`);
|
||||
}
|
||||
object._adopt(child);
|
||||
return;
|
||||
}
|
||||
|
|
@ -194,11 +204,13 @@ export class Connection extends EventEmitter {
|
|||
}
|
||||
|
||||
close(cause?: string) {
|
||||
if (this._closedError)
|
||||
if (this._closedError) {
|
||||
return;
|
||||
}
|
||||
this._closedError = new TargetClosedError(cause);
|
||||
for (const callback of this._callbacks.values())
|
||||
for (const callback of this._callbacks.values()) {
|
||||
callback.reject(this._closedError);
|
||||
}
|
||||
this._callbacks.clear();
|
||||
this.emit('close');
|
||||
}
|
||||
|
|
@ -206,10 +218,12 @@ export class Connection extends EventEmitter {
|
|||
private _tChannelImplFromWire(names: '*' | string[], arg: any, path: string, context: ValidatorContext) {
|
||||
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
|
||||
const object = this._objects.get(arg.guid)!;
|
||||
if (!object)
|
||||
if (!object) {
|
||||
throw new Error(`Object with guid ${arg.guid} was not bound in the connection`);
|
||||
if (names !== '*' && !names.includes(object._type))
|
||||
}
|
||||
if (names !== '*' && !names.includes(object._type)) {
|
||||
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
}
|
||||
return object._channel;
|
||||
}
|
||||
throw new ValidationError(`${path}: expected channel ${names.toString()}`);
|
||||
|
|
@ -217,8 +231,9 @@ export class Connection extends EventEmitter {
|
|||
|
||||
private _createRemoteObject(parentGuid: string, type: string, guid: string, initializer: any): any {
|
||||
const parent = this._objects.get(parentGuid);
|
||||
if (!parent)
|
||||
if (!parent) {
|
||||
throw new Error(`Cannot find parent object ${parentGuid} to create ${guid}`);
|
||||
}
|
||||
let result: ChannelOwner<any>;
|
||||
const validator = findValidator(type, '', 'Initializer');
|
||||
initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' });
|
||||
|
|
@ -276,8 +291,9 @@ export class Connection extends EventEmitter {
|
|||
break;
|
||||
case 'LocalUtils':
|
||||
result = new LocalUtils(parent, type, guid, initializer);
|
||||
if (!this._localUtils)
|
||||
if (!this._localUtils) {
|
||||
this._localUtils = result as LocalUtils;
|
||||
}
|
||||
break;
|
||||
case 'Page':
|
||||
result = new Page(parent, type, guid, initializer);
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.ElectronApplicationInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._context = BrowserContext.from(initializer.context);
|
||||
for (const page of this._context._pages)
|
||||
for (const page of this._context._pages) {
|
||||
this._onPage(page);
|
||||
}
|
||||
this._context.on(Events.BrowserContext.Page, page => this._onPage(page));
|
||||
this._channel.on('close', () => {
|
||||
this.emit(Events.ElectronApplication.Close);
|
||||
|
|
@ -102,8 +103,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
}
|
||||
|
||||
async firstWindow(options?: { timeout?: number }): Promise<Page> {
|
||||
if (this._windows.size)
|
||||
if (this._windows.size) {
|
||||
return this._windows.values().next().value!;
|
||||
}
|
||||
return await this.waitForEvent('window', options);
|
||||
}
|
||||
|
||||
|
|
@ -119,8 +121,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
try {
|
||||
await this._context.close();
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
if (isTargetClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -131,8 +134,9 @@ export class ElectronApplication extends ChannelOwner<channels.ElectronApplicati
|
|||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== Events.ElectronApplication.Close)
|
||||
if (event !== Events.ElectronApplication.Close) {
|
||||
waiter.rejectOnEvent(this, Events.ElectronApplication.Close, () => new TargetClosedError());
|
||||
}
|
||||
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
|
|
|
|||
|
|
@ -147,8 +147,9 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
|
||||
async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) {
|
||||
const frame = await this.ownerFrame();
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
throw new Error('Cannot set input files to detached element');
|
||||
}
|
||||
const converted = await convertInputFiles(files, frame.page().context());
|
||||
await this._elementChannel.setInputFiles({ ...converted, ...options });
|
||||
}
|
||||
|
|
@ -174,11 +175,12 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
}
|
||||
|
||||
async setChecked(checked: boolean, options?: channels.ElementHandleCheckOptions) {
|
||||
if (checked)
|
||||
if (checked) {
|
||||
await this.check(options);
|
||||
else
|
||||
} else {
|
||||
await this.uncheck(options);
|
||||
}
|
||||
}
|
||||
|
||||
async boundingBox(): Promise<Rect | null> {
|
||||
const value = (await this._elementChannel.boundingBox()).value;
|
||||
|
|
@ -187,8 +189,9 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
|
||||
async screenshot(options: Omit<channels.ElementHandleScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
const copy: channels.ElementHandleScreenshotOptions = { ...options, mask: undefined };
|
||||
if (!copy.type)
|
||||
if (!copy.type) {
|
||||
copy.type = determineScreenshotType(options);
|
||||
}
|
||||
if (options.mask) {
|
||||
copy.mask = options.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
|
|
@ -235,18 +238,24 @@ export class ElementHandle<T extends Node = Node> extends JSHandle<T> implements
|
|||
}
|
||||
|
||||
export function convertSelectOptionValues(values: string | api.ElementHandle | SelectOption | string[] | api.ElementHandle[] | SelectOption[] | null): { elements?: channels.ElementHandleChannel[], options?: SelectOption[] } {
|
||||
if (values === null)
|
||||
if (values === null) {
|
||||
return {};
|
||||
if (!Array.isArray(values))
|
||||
}
|
||||
if (!Array.isArray(values)) {
|
||||
values = [values as any];
|
||||
if (!values.length)
|
||||
}
|
||||
if (!values.length) {
|
||||
return {};
|
||||
for (let i = 0; i < values.length; i++)
|
||||
}
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
assert(values[i] !== null, `options[${i}]: expected object, got null`);
|
||||
if (values[0] instanceof ElementHandle)
|
||||
}
|
||||
if (values[0] instanceof ElementHandle) {
|
||||
return { elements: (values as ElementHandle[]).map((v: ElementHandle) => v._elementChannel) };
|
||||
if (isString(values[0]))
|
||||
}
|
||||
if (isString(values[0])) {
|
||||
return { options: (values as string[]).map(valueOrLabel => ({ valueOrLabel })) };
|
||||
}
|
||||
return { options: values as SelectOption[] };
|
||||
}
|
||||
|
||||
|
|
@ -262,16 +271,18 @@ async function resolvePathsAndDirectoryForInputFiles(items: string[]): Promise<[
|
|||
for (const item of items) {
|
||||
const stat = await fs.promises.stat(item as string);
|
||||
if (stat.isDirectory()) {
|
||||
if (localDirectory)
|
||||
if (localDirectory) {
|
||||
throw new Error('Multiple directories are not supported');
|
||||
}
|
||||
localDirectory = path.resolve(item as string);
|
||||
} else {
|
||||
localPaths ??= [];
|
||||
localPaths.push(path.resolve(item as string));
|
||||
}
|
||||
}
|
||||
if (localPaths?.length && localDirectory)
|
||||
if (localPaths?.length && localDirectory) {
|
||||
throw new Error('File paths must be all files or a single directory');
|
||||
}
|
||||
return [localPaths, localDirectory];
|
||||
}
|
||||
|
||||
|
|
@ -279,8 +290,9 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
|
|||
const items: (string | FilePayload)[] = Array.isArray(files) ? files.slice() : [files];
|
||||
|
||||
if (items.some(item => typeof item === 'string')) {
|
||||
if (!items.every(item => typeof item === 'string'))
|
||||
if (!items.every(item => typeof item === 'string')) {
|
||||
throw new Error('File paths cannot be mixed with buffers');
|
||||
}
|
||||
|
||||
const [localPaths, localDirectory] = await resolvePathsAndDirectoryForInputFiles(items);
|
||||
|
||||
|
|
@ -312,18 +324,20 @@ export async function convertInputFiles(files: string | FilePayload | string[] |
|
|||
}
|
||||
|
||||
const payloads = items as FilePayload[];
|
||||
if (filePayloadExceedsSizeLimit(payloads))
|
||||
if (filePayloadExceedsSizeLimit(payloads)) {
|
||||
throw new Error('Cannot set buffer larger than 50Mb, please write it to a file and pass its path instead.');
|
||||
}
|
||||
return { payloads };
|
||||
}
|
||||
|
||||
export function determineScreenshotType(options: { path?: string, type?: 'png' | 'jpeg' }): 'png' | 'jpeg' | undefined {
|
||||
if (options.path) {
|
||||
const mimeType = mime.getType(options.path);
|
||||
if (mimeType === 'image/png')
|
||||
if (mimeType === 'image/png') {
|
||||
return 'png';
|
||||
else if (mimeType === 'image/jpeg')
|
||||
} else if (mimeType === 'image/jpeg') {
|
||||
return 'jpeg';
|
||||
}
|
||||
throw new Error(`path: unsupported mime type "${mimeType}"`);
|
||||
}
|
||||
return options.type;
|
||||
|
|
|
|||
|
|
@ -36,15 +36,17 @@ export function isTargetClosedError(error: Error) {
|
|||
}
|
||||
|
||||
export function serializeError(e: any): SerializedError {
|
||||
if (isError(e))
|
||||
if (isError(e)) {
|
||||
return { error: { message: e.message, stack: e.stack, name: e.name } };
|
||||
}
|
||||
return { value: serializeValue(e, value => ({ fallThrough: value })) };
|
||||
}
|
||||
|
||||
export function parseError(error: SerializedError): Error {
|
||||
if (!error.error) {
|
||||
if (error.value === undefined)
|
||||
if (error.value === undefined) {
|
||||
throw new Error('Serialized error must have either an error or a value');
|
||||
}
|
||||
return parseSerializedValue(error.value, undefined);
|
||||
}
|
||||
if (error.error.name === 'TimeoutError') {
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ export class EventEmitter implements EventEmitterType {
|
|||
}
|
||||
|
||||
setMaxListeners(n: number): this {
|
||||
if (typeof n !== 'number' || n < 0 || Number.isNaN(n))
|
||||
if (typeof n !== 'number' || n < 0 || Number.isNaN(n)) {
|
||||
throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received ' + n + '.');
|
||||
}
|
||||
this._maxListeners = n;
|
||||
return this;
|
||||
}
|
||||
|
|
@ -60,28 +61,32 @@ export class EventEmitter implements EventEmitterType {
|
|||
|
||||
emit(type: EventType, ...args: any[]): boolean {
|
||||
const events = this._events;
|
||||
if (events === undefined)
|
||||
if (events === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handler = events?.[type];
|
||||
if (handler === undefined)
|
||||
const handler = events[type];
|
||||
if (handler === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
this._callHandler(type, handler, args);
|
||||
} else {
|
||||
const len = handler.length;
|
||||
const listeners = handler.slice();
|
||||
for (let i = 0; i < len; ++i)
|
||||
for (let i = 0; i < len; ++i) {
|
||||
this._callHandler(type, listeners[i], args);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _callHandler(type: EventType, handler: Listener, args: any[]): void {
|
||||
const promise = Reflect.apply(handler, this, args);
|
||||
if (!(promise instanceof Promise))
|
||||
if (!(promise instanceof Promise)) {
|
||||
return;
|
||||
}
|
||||
let set = this._pendingHandlers.get(type);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
|
|
@ -89,10 +94,11 @@ export class EventEmitter implements EventEmitterType {
|
|||
}
|
||||
set.add(promise);
|
||||
promise.catch(e => {
|
||||
if (this._rejectionHandler)
|
||||
if (this._rejectionHandler) {
|
||||
this._rejectionHandler(e);
|
||||
else
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}).finally(() => set.delete(promise));
|
||||
}
|
||||
|
||||
|
|
@ -183,21 +189,24 @@ export class EventEmitter implements EventEmitterType {
|
|||
checkListener(listener);
|
||||
|
||||
const events = this._events;
|
||||
if (events === undefined)
|
||||
if (events === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
const list = events[type];
|
||||
if (list === undefined)
|
||||
if (list === undefined) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (list === listener || (list as any).listener === listener) {
|
||||
if (--this._eventsCount === 0) {
|
||||
this._events = Object.create(null);
|
||||
} else {
|
||||
delete events[type];
|
||||
if (events.removeListener)
|
||||
if (events.removeListener) {
|
||||
this.emit('removeListener', type, (list as any).listener ?? listener);
|
||||
}
|
||||
}
|
||||
} else if (typeof list !== 'function') {
|
||||
let position = -1;
|
||||
let originalListener;
|
||||
|
|
@ -210,20 +219,24 @@ export class EventEmitter implements EventEmitterType {
|
|||
}
|
||||
}
|
||||
|
||||
if (position < 0)
|
||||
if (position < 0) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (position === 0)
|
||||
if (position === 0) {
|
||||
list.shift();
|
||||
else
|
||||
} else {
|
||||
list.splice(position, 1);
|
||||
}
|
||||
|
||||
if (list.length === 1)
|
||||
if (list.length === 1) {
|
||||
events[type] = list[0];
|
||||
}
|
||||
|
||||
if (events.removeListener !== undefined)
|
||||
if (events.removeListener !== undefined) {
|
||||
this.emit('removeListener', type, originalListener || listener);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
|
||||
|
|
@ -237,21 +250,24 @@ export class EventEmitter implements EventEmitterType {
|
|||
removeAllListeners(type: EventType | undefined, options: { behavior?: 'wait'|'ignoreErrors'|'default' }): Promise<void>;
|
||||
removeAllListeners(type?: string, options?: { behavior?: 'wait'|'ignoreErrors'|'default' }): this | Promise<void> {
|
||||
this._removeAllListeners(type);
|
||||
if (!options)
|
||||
if (!options) {
|
||||
return this;
|
||||
}
|
||||
|
||||
if (options.behavior === 'wait') {
|
||||
const errors: Error[] = [];
|
||||
this._rejectionHandler = error => errors.push(error);
|
||||
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
|
||||
return this._waitFor(type).then(() => {
|
||||
if (errors.length)
|
||||
if (errors.length) {
|
||||
throw errors[0];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (options.behavior === 'ignoreErrors')
|
||||
if (options.behavior === 'ignoreErrors') {
|
||||
this._rejectionHandler = () => {};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line internal-playwright/await-promise-in-class-returns
|
||||
return Promise.resolve();
|
||||
|
|
@ -259,8 +275,9 @@ export class EventEmitter implements EventEmitterType {
|
|||
|
||||
private _removeAllListeners(type?: string) {
|
||||
const events = this._events;
|
||||
if (!events)
|
||||
if (!events) {
|
||||
return;
|
||||
}
|
||||
|
||||
// not listening for removeListener, no need to emit
|
||||
if (!events.removeListener) {
|
||||
|
|
@ -268,11 +285,12 @@ export class EventEmitter implements EventEmitterType {
|
|||
this._events = Object.create(null);
|
||||
this._eventsCount = 0;
|
||||
} else if (events[type] !== undefined) {
|
||||
if (--this._eventsCount === 0)
|
||||
if (--this._eventsCount === 0) {
|
||||
this._events = Object.create(null);
|
||||
else
|
||||
} else {
|
||||
delete events[type];
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -282,8 +300,9 @@ export class EventEmitter implements EventEmitterType {
|
|||
let key;
|
||||
for (let i = 0; i < keys.length; ++i) {
|
||||
key = keys[i];
|
||||
if (key === 'removeListener')
|
||||
if (key === 'removeListener') {
|
||||
continue;
|
||||
}
|
||||
this._removeAllListeners(key);
|
||||
}
|
||||
this._removeAllListeners('removeListener');
|
||||
|
|
@ -298,10 +317,11 @@ export class EventEmitter implements EventEmitterType {
|
|||
this.removeListener(type, listeners);
|
||||
} else if (listeners !== undefined) {
|
||||
// LIFO order
|
||||
for (let i = listeners.length - 1; i >= 0; i--)
|
||||
for (let i = listeners.length - 1; i >= 0; i--) {
|
||||
this.removeListener(type, listeners[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listeners(type: EventType): Listener[] {
|
||||
return this._listeners(this, type, true);
|
||||
|
|
@ -315,11 +335,13 @@ export class EventEmitter implements EventEmitterType {
|
|||
const events = this._events;
|
||||
if (events !== undefined) {
|
||||
const listener = events[type];
|
||||
if (typeof listener === 'function')
|
||||
if (typeof listener === 'function') {
|
||||
return 1;
|
||||
if (listener !== undefined)
|
||||
}
|
||||
if (listener !== undefined) {
|
||||
return listener.length;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -333,33 +355,38 @@ export class EventEmitter implements EventEmitterType {
|
|||
promises = [...(this._pendingHandlers.get(type) || [])];
|
||||
} else {
|
||||
promises = [];
|
||||
for (const [, pending] of this._pendingHandlers)
|
||||
for (const [, pending] of this._pendingHandlers) {
|
||||
promises.push(...pending);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private _listeners(target: EventEmitter, type: EventType, unwrap: boolean): Listener[] {
|
||||
const events = target._events;
|
||||
|
||||
if (events === undefined)
|
||||
if (events === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const listener = events[type];
|
||||
if (listener === undefined)
|
||||
if (listener === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof listener === 'function')
|
||||
if (typeof listener === 'function') {
|
||||
return unwrap ? [unwrapListener(listener)] : [listener];
|
||||
}
|
||||
|
||||
return unwrap ? unwrapListeners(listener) : listener.slice();
|
||||
}
|
||||
}
|
||||
|
||||
function checkListener(listener: any) {
|
||||
if (typeof listener !== 'function')
|
||||
if (typeof listener !== 'function') {
|
||||
throw new TypeError('The "listener" argument must be of type Function. Received type ' + typeof listener);
|
||||
}
|
||||
}
|
||||
|
||||
class OnceWrapper {
|
||||
private _fired = false;
|
||||
|
|
@ -377,8 +404,9 @@ class OnceWrapper {
|
|||
}
|
||||
|
||||
private _handle(...args: any[]) {
|
||||
if (this._fired)
|
||||
if (this._fired) {
|
||||
return;
|
||||
}
|
||||
this._fired = true;
|
||||
this._eventEmitter.removeListener(this._eventType, this.wrapperFunction);
|
||||
return this._listener.apply(this._eventEmitter, args);
|
||||
|
|
|
|||
|
|
@ -110,8 +110,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
try {
|
||||
await this._channel.dispose(options);
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
if (isTargetClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
this._tracing._resetStackCounter();
|
||||
|
|
@ -168,8 +169,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
|
||||
async _innerFetch(options: FetchOptions & { url?: string, request?: api.Request } = {}): Promise<APIResponse> {
|
||||
return await this._wrapApiCall(async () => {
|
||||
if (this._closeReason)
|
||||
if (this._closeReason) {
|
||||
throw new TargetClosedError(this._closeReason);
|
||||
}
|
||||
assert(options.request || typeof options.url === 'string', 'First argument must be either URL string or Request');
|
||||
assert((options.data === undefined ? 0 : 1) + (options.form === undefined ? 0 : 1) + (options.multipart === undefined ? 0 : 1) <= 1, `Only one of 'data', 'form' or 'multipart' can be specified`);
|
||||
assert(options.maxRedirects === undefined || options.maxRedirects >= 0, `'maxRedirects' must be greater than or equal to '0'`);
|
||||
|
|
@ -177,10 +179,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
const url = options.url !== undefined ? options.url : options.request!.url();
|
||||
const method = options.method || options.request?.method();
|
||||
let encodedParams = undefined;
|
||||
if (typeof options.params === 'string')
|
||||
if (typeof options.params === 'string') {
|
||||
encodedParams = options.params;
|
||||
else if (options.params instanceof URLSearchParams)
|
||||
} else if (options.params instanceof URLSearchParams) {
|
||||
encodedParams = options.params.toString();
|
||||
}
|
||||
// Cannot call allHeaders() here as the request may be paused inside route handler.
|
||||
const headersObj = options.headers || options.request?.headers();
|
||||
const headers = headersObj ? headersObjectToArray(headersObj) : undefined;
|
||||
|
|
@ -190,10 +193,11 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
let postDataBuffer: Buffer | undefined;
|
||||
if (options.data !== undefined) {
|
||||
if (isString(options.data)) {
|
||||
if (isJsonContentType(headers))
|
||||
if (isJsonContentType(headers)) {
|
||||
jsonData = isJsonParsable(options.data) ? options.data : JSON.stringify(options.data);
|
||||
else
|
||||
} else {
|
||||
postDataBuffer = Buffer.from(options.data, 'utf8');
|
||||
}
|
||||
} else if (Buffer.isBuffer(options.data)) {
|
||||
postDataBuffer = options.data;
|
||||
} else if (typeof options.data === 'object' || typeof options.data === 'number' || typeof options.data === 'boolean') {
|
||||
|
|
@ -205,8 +209,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
if (globalThis.FormData && options.form instanceof FormData) {
|
||||
formData = [];
|
||||
for (const [name, value] of options.form.entries()) {
|
||||
if (typeof value !== 'string')
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Expected string for options.form["${name}"], found File. Please use options.multipart instead.`);
|
||||
}
|
||||
formData.push({ name, value });
|
||||
}
|
||||
} else {
|
||||
|
|
@ -230,12 +235,14 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
}
|
||||
} else {
|
||||
// Convert file-like values to ServerFilePayload structs.
|
||||
for (const [name, value] of Object.entries(options.multipart))
|
||||
for (const [name, value] of Object.entries(options.multipart)) {
|
||||
multipartData.push(await toFormField(name, value));
|
||||
}
|
||||
}
|
||||
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined)
|
||||
}
|
||||
if (postDataBuffer === undefined && jsonData === undefined && formData === undefined && multipartData === undefined) {
|
||||
postDataBuffer = options.request?.postDataBuffer() || undefined;
|
||||
}
|
||||
const fixtures = {
|
||||
__testHookLookup: (options as any).__testHookLookup
|
||||
};
|
||||
|
|
@ -273,8 +280,9 @@ export class APIRequestContext extends ChannelOwner<channels.APIRequestContextCh
|
|||
async function toFormField(name: string, value: string|number|boolean|fs.ReadStream|FilePayload): Promise<channels.FormField> {
|
||||
if (isFilePayload(value)) {
|
||||
const payload = value as FilePayload;
|
||||
if (!Buffer.isBuffer(payload.buffer))
|
||||
if (!Buffer.isBuffer(payload.buffer)) {
|
||||
throw new Error(`Unexpected buffer type of 'data.${name}'`);
|
||||
}
|
||||
return { name, file: filePayloadToJson(payload) };
|
||||
} else if (value instanceof fs.ReadStream) {
|
||||
return { name, file: await readStreamToJson(value as fs.ReadStream) };
|
||||
|
|
@ -284,18 +292,20 @@ async function toFormField(name: string, value: string|number|boolean|fs.ReadStr
|
|||
}
|
||||
|
||||
function isJsonParsable(value: any) {
|
||||
if (typeof value !== 'string')
|
||||
if (typeof value !== 'string') {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JSON.parse(value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError)
|
||||
if (e instanceof SyntaxError) {
|
||||
return false;
|
||||
else
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class APIResponse implements api.APIResponse {
|
||||
private readonly _initializer: channels.APIResponse;
|
||||
|
|
@ -335,12 +345,14 @@ export class APIResponse implements api.APIResponse {
|
|||
async body(): Promise<Buffer> {
|
||||
try {
|
||||
const result = await this._request._channel.fetchResponseBody({ fetchUid: this._fetchUid() });
|
||||
if (result.binary === undefined)
|
||||
if (result.binary === undefined) {
|
||||
throw new Error('Response has been disposed');
|
||||
}
|
||||
return result.binary;
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
if (isTargetClosedError(e)) {
|
||||
throw new Error('Response has been disposed');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -403,21 +415,25 @@ async function readStreamToJson(stream: fs.ReadStream): Promise<ServerFilePayloa
|
|||
}
|
||||
|
||||
function isJsonContentType(headers?: HeadersArray): boolean {
|
||||
if (!headers)
|
||||
if (!headers) {
|
||||
return false;
|
||||
}
|
||||
for (const { name, value } of headers) {
|
||||
if (name.toLocaleLowerCase() === 'content-type')
|
||||
if (name.toLocaleLowerCase() === 'content-type') {
|
||||
return value === 'application/json';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function objectToArray(map?: { [key: string]: any }): NameValue[] | undefined {
|
||||
if (!map)
|
||||
if (!map) {
|
||||
return undefined;
|
||||
}
|
||||
const result = [];
|
||||
for (const [name, value] of Object.entries(map))
|
||||
for (const [name, value] of Object.entries(map)) {
|
||||
result.push({ name, value: String(value) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -66,8 +66,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
this._eventEmitter = new EventEmitter();
|
||||
this._eventEmitter.setMaxListeners(0);
|
||||
this._parentFrame = Frame.fromNullable(initializer.parentFrame);
|
||||
if (this._parentFrame)
|
||||
if (this._parentFrame) {
|
||||
this._parentFrame._childFrames.add(this);
|
||||
}
|
||||
this._name = initializer.name;
|
||||
this._url = initializer.url;
|
||||
this._loadStates = new Set(initializer.loadStates);
|
||||
|
|
@ -76,19 +77,23 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
this._loadStates.add(event.add);
|
||||
this._eventEmitter.emit('loadstate', event.add);
|
||||
}
|
||||
if (event.remove)
|
||||
if (event.remove) {
|
||||
this._loadStates.delete(event.remove);
|
||||
if (!this._parentFrame && event.add === 'load' && this._page)
|
||||
}
|
||||
if (!this._parentFrame && event.add === 'load' && this._page) {
|
||||
this._page.emit(Events.Page.Load, this._page);
|
||||
if (!this._parentFrame && event.add === 'domcontentloaded' && this._page)
|
||||
}
|
||||
if (!this._parentFrame && event.add === 'domcontentloaded' && this._page) {
|
||||
this._page.emit(Events.Page.DOMContentLoaded, this._page);
|
||||
}
|
||||
});
|
||||
this._channel.on('navigated', event => {
|
||||
this._url = event.url;
|
||||
this._name = event.name;
|
||||
this._eventEmitter.emit('navigated', event);
|
||||
if (!event.error && this._page)
|
||||
if (!event.error && this._page) {
|
||||
this._page.emit(Events.Page.FrameNavigated, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -103,8 +108,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
|
||||
private _setupNavigationWaiter(options: { timeout?: number }): Waiter {
|
||||
const waiter = new Waiter(this._page!, '');
|
||||
if (this._page!.isClosed())
|
||||
if (this._page!.isClosed()) {
|
||||
waiter.rejectImmediately(this._page!._closeErrorWithReason());
|
||||
}
|
||||
waiter.rejectOnEvent(this._page!, Events.Page.Close, () => this._page!._closeErrorWithReason());
|
||||
waiter.rejectOnEvent(this._page!, Events.Page.Crash, new Error('Navigation failed because page crashed!'));
|
||||
waiter.rejectOnEvent<Frame>(this._page!, Events.Page.FrameDetached, new Error('Navigating frame was detached!'), frame => frame === this);
|
||||
|
|
@ -123,8 +129,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
|
||||
const navigatedEvent = await waiter.waitForEvent<channels.FrameNavigatedEvent>(this._eventEmitter, 'navigated', event => {
|
||||
// Any failed navigation results in a rejection.
|
||||
if (event.error)
|
||||
if (event.error) {
|
||||
return true;
|
||||
}
|
||||
waiter.log(` navigated to "${event.url}"`);
|
||||
return urlMatches(this._page?.context()._options.baseURL, event.url, options.url);
|
||||
});
|
||||
|
|
@ -165,8 +172,9 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
}
|
||||
|
||||
async waitForURL(url: URLMatch, options: { waitUntil?: LifecycleEvent, timeout?: number } = {}): Promise<void> {
|
||||
if (urlMatches(this._page?.context()._options.baseURL, this.url(), url))
|
||||
if (urlMatches(this._page?.context()._options.baseURL, this.url(), url)) {
|
||||
return await this.waitForLoadState(options.waitUntil, options);
|
||||
}
|
||||
|
||||
await this.waitForNavigation({ url, ...options });
|
||||
}
|
||||
|
|
@ -201,10 +209,12 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions & { state: 'attached' | 'visible' }): Promise<ElementHandle<SVGElement | HTMLElement>>;
|
||||
waitForSelector(selector: string, options?: channels.FrameWaitForSelectorOptions): Promise<ElementHandle<SVGElement | HTMLElement> | null>;
|
||||
async waitForSelector(selector: string, options: channels.FrameWaitForSelectorOptions = {}): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
|
||||
if ((options as any).visibility)
|
||||
if ((options as any).visibility) {
|
||||
throw new Error('options.visibility is not supported, did you mean options.state?');
|
||||
if ((options as any).waitFor && (options as any).waitFor !== 'visible')
|
||||
}
|
||||
if ((options as any).waitFor && (options as any).waitFor !== 'visible') {
|
||||
throw new Error('options.waitFor is not supported, did you mean options.state?');
|
||||
}
|
||||
const result = await this._channel.waitForSelector({ selector, ...options });
|
||||
return ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
|
||||
}
|
||||
|
|
@ -421,19 +431,21 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
}
|
||||
|
||||
async setChecked(selector: string, checked: boolean, options?: channels.FrameCheckOptions) {
|
||||
if (checked)
|
||||
if (checked) {
|
||||
await this.check(selector, options);
|
||||
else
|
||||
} else {
|
||||
await this.uncheck(selector, options);
|
||||
}
|
||||
}
|
||||
|
||||
async waitForTimeout(timeout: number) {
|
||||
await this._channel.waitForTimeout({ timeout });
|
||||
}
|
||||
|
||||
async waitForFunction<R, Arg>(pageFunction: structs.PageFunction<Arg, R>, arg?: Arg, options: WaitForFunctionOptions = {}): Promise<structs.SmartHandle<R>> {
|
||||
if (typeof options.polling === 'string')
|
||||
if (typeof options.polling === 'string') {
|
||||
assert(options.polling === 'raf', 'Unknown polling option: ' + options.polling);
|
||||
}
|
||||
const result = await this._channel.waitForFunction({
|
||||
...options,
|
||||
pollingInterval: options.polling === 'raf' ? undefined : options.polling,
|
||||
|
|
@ -450,9 +462,11 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
|||
}
|
||||
|
||||
export function verifyLoadState(name: string, waitUntil: LifecycleEvent): LifecycleEvent {
|
||||
if (waitUntil as unknown === 'networkidle0')
|
||||
if (waitUntil as unknown === 'networkidle0') {
|
||||
waitUntil = 'networkidle';
|
||||
if (!kLifecycleEvents.has(waitUntil))
|
||||
}
|
||||
if (!kLifecycleEvents.has(waitUntil)) {
|
||||
throw new Error(`${name}: expected one of (load|domcontentloaded|networkidle|commit)`);
|
||||
}
|
||||
return waitUntil;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,9 @@ export class HarRouter {
|
|||
|
||||
static async create(localUtils: LocalUtils, file: string, notFoundAction: HarNotFoundAction, options: { urlMatch?: URLMatch }): Promise<HarRouter> {
|
||||
const { harId, error } = await localUtils._channel.harOpen({ file });
|
||||
if (error)
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
return new HarRouter(localUtils, harId!, notFoundAction, options);
|
||||
}
|
||||
|
||||
|
|
@ -67,8 +68,9 @@ export class HarRouter {
|
|||
// TODO: it'd be better to abort such requests, but then we likely need to respect the timing,
|
||||
// because the request might have been stalled for a long time until the very end of the
|
||||
// test when HAR was recorded but we'd abort it immediately.
|
||||
if (response.status === -1)
|
||||
if (response.status === -1) {
|
||||
return;
|
||||
}
|
||||
await route.fulfill({
|
||||
status: response.status,
|
||||
headers: Object.fromEntries(response.headers!.map(h => [h.name, h.value])),
|
||||
|
|
@ -77,8 +79,9 @@ export class HarRouter {
|
|||
return;
|
||||
}
|
||||
|
||||
if (response.action === 'error')
|
||||
if (response.action === 'error') {
|
||||
debugLogger.log('api', 'HAR: ' + response.message!);
|
||||
}
|
||||
// Report the error, but fall through to the default handler.
|
||||
|
||||
if (this._notFoundAction === 'abort') {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
|
|||
|
||||
async getProperties(): Promise<Map<string, JSHandle>> {
|
||||
const map = new Map<string, JSHandle>();
|
||||
for (const { name, value } of (await this._channel.getPropertyList()).properties)
|
||||
for (const { name, value } of (await this._channel.getPropertyList()).properties) {
|
||||
map.set(name, JSHandle.from(value));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
|
|
@ -72,8 +73,9 @@ export class JSHandle<T = any> extends ChannelOwner<channels.JSHandleChannel> im
|
|||
try {
|
||||
await this._channel.dispose();
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e))
|
||||
if (isTargetClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -92,8 +94,9 @@ export function serializeArgument(arg: any): channels.SerializedArgument {
|
|||
return handles.length - 1;
|
||||
};
|
||||
const value = serializeValue(arg, value => {
|
||||
if (value instanceof JSHandle)
|
||||
if (value instanceof JSHandle) {
|
||||
return { h: pushHandle(value._channel) };
|
||||
}
|
||||
return { fallThrough: value };
|
||||
});
|
||||
return { value, handles };
|
||||
|
|
@ -104,6 +107,7 @@ export function parseResult(value: channels.SerializedValue): any {
|
|||
}
|
||||
|
||||
export function assertMaxArguments(count: number, max: number): asserts count {
|
||||
if (count > max)
|
||||
if (count > max) {
|
||||
throw new Error('Too many arguments. If you need to pass more than 1 argument to the function wrap them in an object.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ export class LocalUtils extends ChannelOwner<channels.LocalUtilsChannel> {
|
|||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
this.devices = {};
|
||||
for (const { name, descriptor } of initializer.deviceDescriptors)
|
||||
for (const { name, descriptor } of initializer.deviceDescriptors) {
|
||||
this.devices[name] = descriptor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,23 +42,27 @@ export class Locator implements api.Locator {
|
|||
this._frame = frame;
|
||||
this._selector = selector;
|
||||
|
||||
if (options?.hasText)
|
||||
if (options?.hasText) {
|
||||
this._selector += ` >> internal:has-text=${escapeForTextSelector(options.hasText, false)}`;
|
||||
}
|
||||
|
||||
if (options?.hasNotText)
|
||||
if (options?.hasNotText) {
|
||||
this._selector += ` >> internal:has-not-text=${escapeForTextSelector(options.hasNotText, false)}`;
|
||||
}
|
||||
|
||||
if (options?.has) {
|
||||
const locator = options.has;
|
||||
if (locator._frame !== frame)
|
||||
if (locator._frame !== frame) {
|
||||
throw new Error(`Inner "has" locator must belong to the same frame.`);
|
||||
}
|
||||
this._selector += ` >> internal:has=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
|
||||
if (options?.hasNot) {
|
||||
const locator = options.hasNot;
|
||||
if (locator._frame !== frame)
|
||||
if (locator._frame !== frame) {
|
||||
throw new Error(`Inner "hasNot" locator must belong to the same frame.`);
|
||||
}
|
||||
this._selector += ` >> internal:has-not=` + JSON.stringify(locator._selector);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,8 +74,9 @@ export class Locator implements api.Locator {
|
|||
return await this._frame._wrapApiCall<R>(async () => {
|
||||
const result = await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout });
|
||||
const handle = ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
|
||||
if (!handle)
|
||||
if (!handle) {
|
||||
throw new Error(`Could not resolve ${this._selector} to DOM Element`);
|
||||
}
|
||||
try {
|
||||
return await task(handle, deadline ? deadline - monotonicTime() : 0);
|
||||
} finally {
|
||||
|
|
@ -145,10 +150,12 @@ export class Locator implements api.Locator {
|
|||
}
|
||||
|
||||
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
|
||||
if (isString(selectorOrLocator))
|
||||
if (isString(selectorOrLocator)) {
|
||||
return new Locator(this._frame, this._selector + ' >> ' + selectorOrLocator, options);
|
||||
if (selectorOrLocator._frame !== this._frame)
|
||||
}
|
||||
if (selectorOrLocator._frame !== this._frame) {
|
||||
throw new Error(`Locators must belong to the same frame.`);
|
||||
}
|
||||
return new Locator(this._frame, this._selector + ' >> internal:chain=' + JSON.stringify(selectorOrLocator._selector), options);
|
||||
}
|
||||
|
||||
|
|
@ -213,14 +220,16 @@ export class Locator implements api.Locator {
|
|||
}
|
||||
|
||||
and(locator: Locator): Locator {
|
||||
if (locator._frame !== this._frame)
|
||||
if (locator._frame !== this._frame) {
|
||||
throw new Error(`Locators must belong to the same frame.`);
|
||||
}
|
||||
return new Locator(this._frame, this._selector + ` >> internal:and=` + JSON.stringify(locator._selector));
|
||||
}
|
||||
|
||||
or(locator: Locator): Locator {
|
||||
if (locator._frame !== this._frame)
|
||||
if (locator._frame !== this._frame) {
|
||||
throw new Error(`Locators must belong to the same frame.`);
|
||||
}
|
||||
return new Locator(this._frame, this._selector + ` >> internal:or=` + JSON.stringify(locator._selector));
|
||||
}
|
||||
|
||||
|
|
@ -306,11 +315,12 @@ export class Locator implements api.Locator {
|
|||
}
|
||||
|
||||
async setChecked(checked: boolean, options?: channels.ElementHandleCheckOptions) {
|
||||
if (checked)
|
||||
if (checked) {
|
||||
await this.check(options);
|
||||
else
|
||||
} else {
|
||||
await this.uncheck(options);
|
||||
}
|
||||
}
|
||||
|
||||
async setInputFiles(files: string | FilePayload | string[] | FilePayload[], options: channels.ElementHandleSetInputFilesOptions = {}) {
|
||||
return await this._frame.setInputFiles(this._selector, files, { strict: true, ...options });
|
||||
|
|
@ -358,8 +368,9 @@ export class Locator implements api.Locator {
|
|||
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
||||
params.expectedValue = serializeArgument(options.expectedValue);
|
||||
const result = (await this._frame._channel.expect(params));
|
||||
if (result.received !== undefined)
|
||||
if (result.received !== undefined) {
|
||||
result.received = parseResult(result.received);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -382,10 +393,12 @@ export class FrameLocator implements api.FrameLocator {
|
|||
}
|
||||
|
||||
locator(selectorOrLocator: string | Locator, options?: LocatorOptions): Locator {
|
||||
if (isString(selectorOrLocator))
|
||||
if (isString(selectorOrLocator)) {
|
||||
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator, options);
|
||||
if (selectorOrLocator._frame !== this._frame)
|
||||
}
|
||||
if (selectorOrLocator._frame !== this._frame) {
|
||||
throw new Error(`Locators must belong to the same frame.`);
|
||||
}
|
||||
return new Locator(this._frame, this._frameSelector + ' >> internal:control=enter-frame >> ' + selectorOrLocator._selector, options);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -99,8 +99,9 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
super(parent, type, guid, initializer);
|
||||
this.markAsInternalType();
|
||||
this._redirectedFrom = Request.fromNullable(initializer.redirectedFrom);
|
||||
if (this._redirectedFrom)
|
||||
if (this._redirectedFrom) {
|
||||
this._redirectedFrom._redirectedTo = this;
|
||||
}
|
||||
this._provisionalHeaders = new RawHeaders(initializer.headers);
|
||||
this._timing = {
|
||||
startTime: 0,
|
||||
|
|
@ -137,15 +138,17 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
|
||||
postDataJSON(): Object | null {
|
||||
const postData = this.postData();
|
||||
if (!postData)
|
||||
if (!postData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const contentType = this.headers()['content-type'];
|
||||
if (contentType?.includes('application/x-www-form-urlencoded')) {
|
||||
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||
const entries: Record<string, string> = {};
|
||||
const parsed = new URLSearchParams(postData);
|
||||
for (const [k, v] of parsed.entries())
|
||||
for (const [k, v] of parsed.entries()) {
|
||||
entries[k] = v;
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
|
@ -160,14 +163,16 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
* @deprecated
|
||||
*/
|
||||
headers(): Headers {
|
||||
if (this._fallbackOverrides.headers)
|
||||
if (this._fallbackOverrides.headers) {
|
||||
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers).headers();
|
||||
}
|
||||
return this._provisionalHeaders.headers();
|
||||
}
|
||||
|
||||
async _actualHeaders(): Promise<RawHeaders> {
|
||||
if (this._fallbackOverrides.headers)
|
||||
if (this._fallbackOverrides.headers) {
|
||||
return RawHeaders._fromHeadersObjectLossy(this._fallbackOverrides.headers);
|
||||
}
|
||||
|
||||
if (!this._actualHeadersPromise) {
|
||||
this._actualHeadersPromise = this._wrapApiCall(async () => {
|
||||
|
|
@ -236,8 +241,9 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
}
|
||||
|
||||
failure(): { errorText: string; } | null {
|
||||
if (this._failureText === null)
|
||||
if (this._failureText === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
errorText: this._failureText
|
||||
};
|
||||
|
|
@ -249,36 +255,42 @@ export class Request extends ChannelOwner<channels.RequestChannel> implements ap
|
|||
|
||||
async sizes(): Promise<RequestSizes> {
|
||||
const response = await this.response();
|
||||
if (!response)
|
||||
if (!response) {
|
||||
throw new Error('Unable to fetch sizes for failed request');
|
||||
}
|
||||
return (await response._channel.sizes()).sizes;
|
||||
}
|
||||
|
||||
_setResponseEndTiming(responseEndTiming: number) {
|
||||
this._timing.responseEnd = responseEndTiming;
|
||||
if (this._timing.responseStart === -1)
|
||||
if (this._timing.responseStart === -1) {
|
||||
this._timing.responseStart = responseEndTiming;
|
||||
}
|
||||
}
|
||||
|
||||
_finalRequest(): Request {
|
||||
return this._redirectedTo ? this._redirectedTo._finalRequest() : this;
|
||||
}
|
||||
|
||||
_applyFallbackOverrides(overrides: FallbackOverrides) {
|
||||
if (overrides.url)
|
||||
if (overrides.url) {
|
||||
this._fallbackOverrides.url = overrides.url;
|
||||
if (overrides.method)
|
||||
}
|
||||
if (overrides.method) {
|
||||
this._fallbackOverrides.method = overrides.method;
|
||||
if (overrides.headers)
|
||||
}
|
||||
if (overrides.headers) {
|
||||
this._fallbackOverrides.headers = overrides.headers;
|
||||
}
|
||||
|
||||
if (isString(overrides.postData))
|
||||
if (isString(overrides.postData)) {
|
||||
this._fallbackOverrides.postDataBuffer = Buffer.from(overrides.postData, 'utf-8');
|
||||
else if (overrides.postData instanceof Buffer)
|
||||
} else if (overrides.postData instanceof Buffer) {
|
||||
this._fallbackOverrides.postDataBuffer = overrides.postData;
|
||||
else if (overrides.postData)
|
||||
} else if (overrides.postData) {
|
||||
this._fallbackOverrides.postDataBuffer = Buffer.from(JSON.stringify(overrides.postData), 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
_fallbackOverridesForContinue() {
|
||||
return this._fallbackOverrides;
|
||||
|
|
@ -375,12 +387,13 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
statusOption ??= options.response.status();
|
||||
headersOption ??= options.response.headers();
|
||||
if (body === undefined && options.path === undefined) {
|
||||
if (options.response._request._connection === this._connection)
|
||||
if (options.response._request._connection === this._connection) {
|
||||
fetchResponseUid = (options.response as APIResponse)._fetchUid();
|
||||
else
|
||||
} else {
|
||||
body = await options.response.body();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let isBase64 = false;
|
||||
let length = 0;
|
||||
|
|
@ -399,16 +412,19 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
}
|
||||
|
||||
const headers: Headers = {};
|
||||
for (const header of Object.keys(headersOption || {}))
|
||||
for (const header of Object.keys(headersOption || {})) {
|
||||
headers[header.toLowerCase()] = String(headersOption![header]);
|
||||
if (options.contentType)
|
||||
}
|
||||
if (options.contentType) {
|
||||
headers['content-type'] = String(options.contentType);
|
||||
else if (options.json)
|
||||
} else if (options.json) {
|
||||
headers['content-type'] = 'application/json';
|
||||
else if (options.path)
|
||||
} else if (options.path) {
|
||||
headers['content-type'] = mime.getType(options.path) || 'application/octet-stream';
|
||||
if (length && !('content-length' in headers))
|
||||
}
|
||||
if (length && !('content-length' in headers)) {
|
||||
headers['content-length'] = String(length);
|
||||
}
|
||||
|
||||
await this._raceWithTargetClose(this._channel.fulfill({
|
||||
status: statusOption || 200,
|
||||
|
|
@ -427,9 +443,10 @@ export class Route extends ChannelOwner<channels.RouteChannel> implements api.Ro
|
|||
}
|
||||
|
||||
_checkNotHandled() {
|
||||
if (!this._handlingPromise)
|
||||
if (!this._handlingPromise) {
|
||||
throw new Error('Route is already handled!');
|
||||
}
|
||||
}
|
||||
|
||||
_reportHandled(done: boolean) {
|
||||
const chain = this._handlingPromise!;
|
||||
|
|
@ -487,10 +504,11 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
|
|||
},
|
||||
|
||||
send: (message: string | Buffer) => {
|
||||
if (isString(message))
|
||||
if (isString(message)) {
|
||||
this._channel.sendToServer({ message, isBase64: false }).catch(() => {});
|
||||
else
|
||||
} else {
|
||||
this._channel.sendToServer({ message: message.toString('base64'), isBase64: true }).catch(() => {});
|
||||
}
|
||||
},
|
||||
|
||||
async [Symbol.asyncDispose]() {
|
||||
|
|
@ -499,31 +517,35 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
|
|||
};
|
||||
|
||||
this._channel.on('messageFromPage', ({ message, isBase64 }) => {
|
||||
if (this._onPageMessage)
|
||||
if (this._onPageMessage) {
|
||||
this._onPageMessage(isBase64 ? Buffer.from(message, 'base64') : message);
|
||||
else if (this._connected)
|
||||
} else if (this._connected) {
|
||||
this._channel.sendToServer({ message, isBase64 }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
this._channel.on('messageFromServer', ({ message, isBase64 }) => {
|
||||
if (this._onServerMessage)
|
||||
if (this._onServerMessage) {
|
||||
this._onServerMessage(isBase64 ? Buffer.from(message, 'base64') : message);
|
||||
else
|
||||
} else {
|
||||
this._channel.sendToPage({ message, isBase64 }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
this._channel.on('closePage', ({ code, reason, wasClean }) => {
|
||||
if (this._onPageClose)
|
||||
if (this._onPageClose) {
|
||||
this._onPageClose(code, reason);
|
||||
else
|
||||
} else {
|
||||
this._channel.closeServer({ code, reason, wasClean }).catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
this._channel.on('closeServer', ({ code, reason, wasClean }) => {
|
||||
if (this._onServerClose)
|
||||
if (this._onServerClose) {
|
||||
this._onServerClose(code, reason);
|
||||
else
|
||||
} else {
|
||||
this._channel.closePage({ code, reason, wasClean }).catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -536,19 +558,21 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
|
|||
}
|
||||
|
||||
connectToServer() {
|
||||
if (this._connected)
|
||||
if (this._connected) {
|
||||
throw new Error('Already connected to the server');
|
||||
}
|
||||
this._connected = true;
|
||||
this._channel.connect().catch(() => {});
|
||||
return this._server;
|
||||
}
|
||||
|
||||
send(message: string | Buffer) {
|
||||
if (isString(message))
|
||||
if (isString(message)) {
|
||||
this._channel.sendToPage({ message, isBase64: false }).catch(() => {});
|
||||
else
|
||||
} else {
|
||||
this._channel.sendToPage({ message: message.toString('base64'), isBase64: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
onMessage(handler: (message: string | Buffer) => any) {
|
||||
this._onPageMessage = handler;
|
||||
|
|
@ -563,8 +587,9 @@ export class WebSocketRoute extends ChannelOwner<channels.WebSocketRouteChannel>
|
|||
}
|
||||
|
||||
async _afterHandle() {
|
||||
if (this._connected)
|
||||
if (this._connected) {
|
||||
return;
|
||||
}
|
||||
// Ensure that websocket is "open" and can send messages without an actual server connection.
|
||||
await this._channel.ensureOpened();
|
||||
}
|
||||
|
|
@ -585,15 +610,17 @@ export class WebSocketRouteHandler {
|
|||
const patterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
if (isString(handler.url))
|
||||
if (isString(handler.url)) {
|
||||
patterns.push({ glob: handler.url });
|
||||
else if (isRegExp(handler.url))
|
||||
} else if (isRegExp(handler.url)) {
|
||||
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||
else
|
||||
} else {
|
||||
all = true;
|
||||
}
|
||||
if (all)
|
||||
}
|
||||
if (all) {
|
||||
return [{ glob: '**/*' }];
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
|
|
@ -753,16 +780,18 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
|
|||
this._isClosed = false;
|
||||
this._page = parent as Page;
|
||||
this._channel.on('frameSent', event => {
|
||||
if (event.opcode === 1)
|
||||
if (event.opcode === 1) {
|
||||
this.emit(Events.WebSocket.FrameSent, { payload: event.data });
|
||||
else if (event.opcode === 2)
|
||||
} else if (event.opcode === 2) {
|
||||
this.emit(Events.WebSocket.FrameSent, { payload: Buffer.from(event.data, 'base64') });
|
||||
}
|
||||
});
|
||||
this._channel.on('frameReceived', event => {
|
||||
if (event.opcode === 1)
|
||||
if (event.opcode === 1) {
|
||||
this.emit(Events.WebSocket.FrameReceived, { payload: event.data });
|
||||
else if (event.opcode === 2)
|
||||
} else if (event.opcode === 2) {
|
||||
this.emit(Events.WebSocket.FrameReceived, { payload: Buffer.from(event.data, 'base64') });
|
||||
}
|
||||
});
|
||||
this._channel.on('socketError', ({ error }) => this.emit(Events.WebSocket.Error, error));
|
||||
this._channel.on('close', () => {
|
||||
|
|
@ -785,10 +814,12 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
|
|||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = Waiter.createForEvent(this, event);
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== Events.WebSocket.Error)
|
||||
if (event !== Events.WebSocket.Error) {
|
||||
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
|
||||
if (event !== Events.WebSocket.Close)
|
||||
}
|
||||
if (event !== Events.WebSocket.Close) {
|
||||
waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed'));
|
||||
}
|
||||
waiter.rejectOnEvent(this._page, Events.Page.Close, () => this._page._closeErrorWithReason());
|
||||
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||
waiter.dispose();
|
||||
|
|
@ -800,10 +831,11 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel> implement
|
|||
export function validateHeaders(headers: Headers) {
|
||||
for (const key of Object.keys(headers)) {
|
||||
const value = headers[key];
|
||||
if (!Object.is(value, undefined) && !isString(value))
|
||||
if (!Object.is(value, undefined) && !isString(value)) {
|
||||
throw new Error(`Expected value of header "${key}" to be String, but "${typeof value}" is found.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteHandler {
|
||||
private handledCount = 0;
|
||||
|
|
@ -827,15 +859,17 @@ export class RouteHandler {
|
|||
const patterns: channels.BrowserContextSetNetworkInterceptionPatternsParams['patterns'] = [];
|
||||
let all = false;
|
||||
for (const handler of handlers) {
|
||||
if (isString(handler.url))
|
||||
if (isString(handler.url)) {
|
||||
patterns.push({ glob: handler.url });
|
||||
else if (isRegExp(handler.url))
|
||||
} else if (isRegExp(handler.url)) {
|
||||
patterns.push({ regexSource: handler.url.source, regexFlags: handler.url.flags });
|
||||
else
|
||||
} else {
|
||||
all = true;
|
||||
}
|
||||
if (all)
|
||||
}
|
||||
if (all) {
|
||||
return [{ glob: '**/*' }];
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
|
|
@ -854,8 +888,9 @@ export class RouteHandler {
|
|||
return await this._handleInternal(route);
|
||||
} catch (e) {
|
||||
// If the handler was stopped (without waiting for completion), we ignore all exceptions.
|
||||
if (this._ignoreException)
|
||||
if (this._ignoreException) {
|
||||
return false;
|
||||
}
|
||||
if (isTargetClosedError(e)) {
|
||||
// We are failing in the handler because the target close closed.
|
||||
// Give user a hint!
|
||||
|
|
@ -878,9 +913,10 @@ export class RouteHandler {
|
|||
} else {
|
||||
const promises = [];
|
||||
for (const activation of this._activeInvocations) {
|
||||
if (!activation.route._didThrow)
|
||||
if (!activation.route._didThrow) {
|
||||
promises.push(activation.complete);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
|
@ -915,14 +951,16 @@ export class RawHeaders {
|
|||
|
||||
constructor(headers: HeadersArray) {
|
||||
this._headersArray = headers;
|
||||
for (const header of headers)
|
||||
for (const header of headers) {
|
||||
this._headersMap.set(header.name.toLowerCase(), header.value);
|
||||
}
|
||||
}
|
||||
|
||||
get(name: string): string | null {
|
||||
const values = this.getAll(name);
|
||||
if (!values || !values.length)
|
||||
if (!values || !values.length) {
|
||||
return null;
|
||||
}
|
||||
return values.join(name.toLowerCase() === 'set-cookie' ? '\n' : ', ');
|
||||
}
|
||||
|
||||
|
|
@ -932,8 +970,9 @@ export class RawHeaders {
|
|||
|
||||
headers(): Headers {
|
||||
const result: Headers = {};
|
||||
for (const name of this._headersMap.keys())
|
||||
for (const name of this._headersMap.keys()) {
|
||||
result[name] = this.get(name)!;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -166,16 +166,18 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
private _onFrameAttached(frame: Frame) {
|
||||
frame._page = this;
|
||||
this._frames.add(frame);
|
||||
if (frame._parentFrame)
|
||||
if (frame._parentFrame) {
|
||||
frame._parentFrame._childFrames.add(frame);
|
||||
}
|
||||
this.emit(Events.Page.FrameAttached, frame);
|
||||
}
|
||||
|
||||
private _onFrameDetached(frame: Frame) {
|
||||
this._frames.delete(frame);
|
||||
frame._detached = true;
|
||||
if (frame._parentFrame)
|
||||
if (frame._parentFrame) {
|
||||
frame._parentFrame._childFrames.delete(frame);
|
||||
}
|
||||
this.emit(Events.Page.FrameDetached, frame);
|
||||
}
|
||||
|
||||
|
|
@ -184,32 +186,39 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
const routeHandlers = this._routes.slice();
|
||||
for (const routeHandler of routeHandlers) {
|
||||
// If the page was closed we stall all requests right away.
|
||||
if (this._closeWasCalled || this._browserContext._closeWasCalled)
|
||||
if (this._closeWasCalled || this._browserContext._closeWasCalled) {
|
||||
return;
|
||||
if (!routeHandler.matches(route.request().url()))
|
||||
}
|
||||
if (!routeHandler.matches(route.request().url())) {
|
||||
continue;
|
||||
}
|
||||
const index = this._routes.indexOf(routeHandler);
|
||||
if (index === -1)
|
||||
if (index === -1) {
|
||||
continue;
|
||||
if (routeHandler.willExpire())
|
||||
}
|
||||
if (routeHandler.willExpire()) {
|
||||
this._routes.splice(index, 1);
|
||||
}
|
||||
const handled = await routeHandler.handle(route);
|
||||
if (!this._routes.length)
|
||||
if (!this._routes.length) {
|
||||
this._wrapApiCall(() => this._updateInterceptionPatterns(), true).catch(() => {});
|
||||
if (handled)
|
||||
}
|
||||
if (handled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this._browserContext._onRoute(route);
|
||||
}
|
||||
|
||||
private async _onWebSocketRoute(webSocketRoute: WebSocketRoute) {
|
||||
const routeHandler = this._webSocketRoutes.find(route => route.matches(webSocketRoute.url()));
|
||||
if (routeHandler)
|
||||
if (routeHandler) {
|
||||
await routeHandler.handle(webSocketRoute);
|
||||
else
|
||||
} else {
|
||||
await this._browserContext._onWebSocketRoute(webSocketRoute);
|
||||
}
|
||||
}
|
||||
|
||||
async _onBinding(bindingCall: BindingCall) {
|
||||
const func = this._bindings.get(bindingCall._initializer.name);
|
||||
|
|
@ -243,8 +252,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async opener(): Promise<Page | null> {
|
||||
if (!this._opener || this._opener.isClosed())
|
||||
if (!this._opener || this._opener.isClosed()) {
|
||||
return null;
|
||||
}
|
||||
return this._opener;
|
||||
}
|
||||
|
||||
|
|
@ -257,8 +267,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
const url = isObject(frameSelector) ? frameSelector.url : undefined;
|
||||
assert(name || url, 'Either name or url matcher should be specified');
|
||||
return this.frames().find(f => {
|
||||
if (name)
|
||||
if (name) {
|
||||
return f.name() === name;
|
||||
}
|
||||
return urlMatches(this._browserContext._options.baseURL, f.url(), url);
|
||||
}) || null;
|
||||
}
|
||||
|
|
@ -282,8 +293,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
private _forceVideo(): Video {
|
||||
if (!this._video)
|
||||
if (!this._video) {
|
||||
this._video = new Video(this, this._connection);
|
||||
}
|
||||
return this._video;
|
||||
}
|
||||
|
||||
|
|
@ -291,8 +303,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
// Note: we are creating Video object lazily, because we do not know
|
||||
// BrowserContextOptions when constructing the page - it is assigned
|
||||
// too late during launchPersistentContext.
|
||||
if (!this._browserContext._options.recordVideo)
|
||||
if (!this._browserContext._options.recordVideo) {
|
||||
return null;
|
||||
}
|
||||
return this._forceVideo();
|
||||
}
|
||||
|
||||
|
|
@ -375,10 +388,12 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async addLocatorHandler(locator: Locator, handler: (locator: Locator) => any, options: { times?: number, noWaitAfter?: boolean } = {}): Promise<void> {
|
||||
if (locator._frame !== this._mainFrame)
|
||||
if (locator._frame !== this._mainFrame) {
|
||||
throw new Error(`Locator must belong to the main frame of this page`);
|
||||
if (options.times === 0)
|
||||
}
|
||||
if (options.times === 0) {
|
||||
return;
|
||||
}
|
||||
const { uid } = await this._channel.registerLocatorHandler({ selector: locator._selector, noWaitAfter: options.noWaitAfter });
|
||||
this._locatorHandlers.set(uid, { locator, handler, times: options.times });
|
||||
}
|
||||
|
|
@ -388,14 +403,16 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
try {
|
||||
const handler = this._locatorHandlers.get(uid);
|
||||
if (handler && handler.times !== 0) {
|
||||
if (handler.times !== undefined)
|
||||
if (handler.times !== undefined) {
|
||||
handler.times--;
|
||||
}
|
||||
await handler.handler(handler.locator);
|
||||
}
|
||||
remove = handler?.times === 0;
|
||||
} finally {
|
||||
if (remove)
|
||||
if (remove) {
|
||||
this._locatorHandlers.delete(uid);
|
||||
}
|
||||
this._wrapApiCall(() => this._channel.resolveLocatorHandlerNoReply({ uid, remove }), true).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
|
@ -423,8 +440,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
|
||||
async waitForRequest(urlOrPredicate: string | RegExp | ((r: Request) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Request> {
|
||||
const predicate = async (request: Request) => {
|
||||
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate))
|
||||
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) {
|
||||
return urlMatches(this._browserContext._options.baseURL, request.url(), urlOrPredicate);
|
||||
}
|
||||
return await urlOrPredicate(request);
|
||||
};
|
||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||
|
|
@ -434,8 +452,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
|
||||
async waitForResponse(urlOrPredicate: string | RegExp | ((r: Response) => boolean | Promise<boolean>), options: { timeout?: number } = {}): Promise<Response> {
|
||||
const predicate = async (response: Response) => {
|
||||
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate))
|
||||
if (isString(urlOrPredicate) || isRegExp(urlOrPredicate)) {
|
||||
return urlMatches(this._browserContext._options.baseURL, response.url(), urlOrPredicate);
|
||||
}
|
||||
return await urlOrPredicate(response);
|
||||
};
|
||||
const trimmedUrl = trimUrl(urlOrPredicate);
|
||||
|
|
@ -456,13 +475,16 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
const timeout = this._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
|
||||
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
|
||||
const waiter = Waiter.createForEvent(this, event);
|
||||
if (logLine)
|
||||
if (logLine) {
|
||||
waiter.log(logLine);
|
||||
}
|
||||
waiter.rejectOnTimeout(timeout, `Timeout ${timeout}ms exceeded while waiting for event "${event}"`);
|
||||
if (event !== Events.Page.Crash)
|
||||
if (event !== Events.Page.Crash) {
|
||||
waiter.rejectOnEvent(this, Events.Page.Crash, new Error('Page crashed'));
|
||||
if (event !== Events.Page.Close)
|
||||
}
|
||||
if (event !== Events.Page.Close) {
|
||||
waiter.rejectOnEvent(this, Events.Page.Close, () => this._closeErrorWithReason());
|
||||
}
|
||||
const result = await waiter.waitForEvent(this, event, predicate as any);
|
||||
waiter.dispose();
|
||||
return result;
|
||||
|
|
@ -545,19 +567,21 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
const removed = [];
|
||||
const remaining = [];
|
||||
for (const route of this._routes) {
|
||||
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler))
|
||||
if (urlMatchesEqual(route.url, url) && (!handler || route.handler === handler)) {
|
||||
removed.push(route);
|
||||
else
|
||||
} else {
|
||||
remaining.push(route);
|
||||
}
|
||||
}
|
||||
await this._unrouteInternal(removed, remaining, 'default');
|
||||
}
|
||||
|
||||
private async _unrouteInternal(removed: RouteHandler[], remaining: RouteHandler[], behavior?: 'wait'|'ignoreErrors'|'default'): Promise<void> {
|
||||
this._routes = remaining;
|
||||
await this._updateInterceptionPatterns();
|
||||
if (!behavior || behavior === 'default')
|
||||
if (!behavior || behavior === 'default') {
|
||||
return;
|
||||
}
|
||||
const promises = removed.map(routeHandler => routeHandler.stop(behavior));
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
|
@ -574,8 +598,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
|
||||
async screenshot(options: Omit<channels.PageScreenshotOptions, 'mask'> & { path?: string, mask?: Locator[] } = {}): Promise<Buffer> {
|
||||
const copy: channels.PageScreenshotOptions = { ...options, mask: undefined };
|
||||
if (!copy.type)
|
||||
if (!copy.type) {
|
||||
copy.type = determineScreenshotType(options);
|
||||
}
|
||||
if (options.mask) {
|
||||
copy.mask = options.mask.map(locator => ({
|
||||
frame: locator._frame._channel,
|
||||
|
|
@ -591,7 +616,7 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> {
|
||||
const mask = options?.mask ? options?.mask.map(locator => ({
|
||||
const mask = options.mask ? options.mask.map(locator => ({
|
||||
frame: (locator as Locator)._frame._channel,
|
||||
selector: (locator as Locator)._selector,
|
||||
})) : undefined;
|
||||
|
|
@ -623,13 +648,15 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
this._closeReason = options.reason;
|
||||
this._closeWasCalled = true;
|
||||
try {
|
||||
if (this._ownedContext)
|
||||
if (this._ownedContext) {
|
||||
await this._ownedContext.close();
|
||||
else
|
||||
} else {
|
||||
await this._channel.close(options);
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTargetClosedError(e) && !options.runBeforeUnload)
|
||||
if (isTargetClosedError(e) && !options.runBeforeUnload) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
@ -787,13 +814,14 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
}
|
||||
|
||||
async pause(_options?: { __testHookKeepTestTimeout: boolean }) {
|
||||
if (require('inspector').url())
|
||||
if (require('inspector').url()) {
|
||||
return;
|
||||
}
|
||||
const defaultNavigationTimeout = this._browserContext._timeoutSettings.defaultNavigationTimeout();
|
||||
const defaultTimeout = this._browserContext._timeoutSettings.defaultTimeout();
|
||||
this._browserContext.setDefaultNavigationTimeout(0);
|
||||
this._browserContext.setDefaultTimeout(0);
|
||||
this._instrumentation?.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
|
||||
this._instrumentation.onWillPause({ keepTestTimeout: !!_options?.__testHookKeepTestTimeout });
|
||||
await this._closedOrCrashedScope.safeRace(this.context()._channel.pause());
|
||||
this._browserContext.setDefaultNavigationTimeout(defaultNavigationTimeout);
|
||||
this._browserContext.setDefaultTimeout(defaultTimeout);
|
||||
|
|
@ -801,17 +829,21 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
|
|||
|
||||
async pdf(options: PDFOptions = {}): Promise<Buffer> {
|
||||
const transportOptions: channels.PagePdfParams = { ...options } as channels.PagePdfParams;
|
||||
if (transportOptions.margin)
|
||||
if (transportOptions.margin) {
|
||||
transportOptions.margin = { ...transportOptions.margin };
|
||||
if (typeof options.width === 'number')
|
||||
}
|
||||
if (typeof options.width === 'number') {
|
||||
transportOptions.width = options.width + 'px';
|
||||
if (typeof options.height === 'number')
|
||||
}
|
||||
if (typeof options.height === 'number') {
|
||||
transportOptions.height = options.height + 'px';
|
||||
}
|
||||
for (const margin of ['top', 'right', 'bottom', 'left']) {
|
||||
const index = margin as 'top' | 'right' | 'bottom' | 'left';
|
||||
if (options.margin && typeof options.margin[index] === 'number')
|
||||
if (options.margin && typeof options.margin[index] === 'number') {
|
||||
transportOptions.margin![index] = transportOptions.margin![index] + 'px';
|
||||
}
|
||||
}
|
||||
const result = await this._channel.pdf(transportOptions);
|
||||
if (options.path) {
|
||||
await fs.promises.mkdir(path.dirname(options.path), { recursive: true });
|
||||
|
|
@ -839,10 +871,11 @@ export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
|
|||
frame
|
||||
};
|
||||
let result: any;
|
||||
if (this._initializer.handle)
|
||||
if (this._initializer.handle) {
|
||||
result = await func(source, JSHandle.from(this._initializer.handle));
|
||||
else
|
||||
} else {
|
||||
result = await func(source, ...this._initializer.args!.map(parseResult));
|
||||
}
|
||||
this._channel.resolve({ result: serializeArgument(result) }).catch(() => {});
|
||||
} catch (e) {
|
||||
this._channel.reject({ error: serializeError(e) }).catch(() => {});
|
||||
|
|
@ -851,8 +884,10 @@ export class BindingCall extends ChannelOwner<channels.BindingCallChannel> {
|
|||
}
|
||||
|
||||
function trimUrl(param: any): string | undefined {
|
||||
if (isRegExp(param))
|
||||
if (isRegExp(param)) {
|
||||
return `/${trimStringWithEllipsis(param.source, 50)}/${param.flags}`;
|
||||
if (isString(param))
|
||||
}
|
||||
if (isString(param)) {
|
||||
return `"${trimStringWithEllipsis(param, 50)}"`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel> {
|
|||
this._bidiChromium._playwright = this;
|
||||
this._bidiFirefox = BrowserType.from(initializer.bidiFirefox);
|
||||
this._bidiFirefox._playwright = this;
|
||||
this.devices = this._connection.localUtils()?.devices ?? {};
|
||||
this.devices = this._connection.localUtils().devices ?? {};
|
||||
this.selectors = new Selectors();
|
||||
this.errors = { TimeoutError };
|
||||
|
||||
|
|
|
|||
|
|
@ -28,16 +28,18 @@ export class Selectors implements api.Selectors {
|
|||
async register(name: string, script: string | (() => SelectorEngine) | { path?: string, content?: string }, options: { contentScript?: boolean } = {}): Promise<void> {
|
||||
const source = await evaluationScript(script, undefined, false);
|
||||
const params = { ...options, name, source };
|
||||
for (const channel of this._channels)
|
||||
for (const channel of this._channels) {
|
||||
await channel._channel.register(params);
|
||||
}
|
||||
this._registrations.push(params);
|
||||
}
|
||||
|
||||
setTestIdAttribute(attributeName: string) {
|
||||
setTestIdAttribute(attributeName);
|
||||
for (const channel of this._channels)
|
||||
for (const channel of this._channels) {
|
||||
channel._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
_addChannel(channel: SelectorsOwner) {
|
||||
this._channels.add(channel);
|
||||
|
|
|
|||
|
|
@ -42,11 +42,12 @@ class StreamImpl extends Readable {
|
|||
|
||||
override async _read() {
|
||||
const result = await this._channel.read({ size: 1024 * 1024 });
|
||||
if (result.binary.byteLength)
|
||||
if (result.binary.byteLength) {
|
||||
this.push(result.binary);
|
||||
else
|
||||
} else {
|
||||
this.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
override _destroy(error: Error | null, callback: (error: Error | null | undefined) => void): void {
|
||||
// Stream might be destroyed after the connection was closed.
|
||||
|
|
|
|||
|
|
@ -87,8 +87,9 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
if (!filePath) {
|
||||
// Not interested in artifacts.
|
||||
await this._channel.tracingStopChunk({ mode: 'discard' });
|
||||
if (this._stacksId)
|
||||
if (this._stacksId) {
|
||||
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -104,8 +105,9 @@ export class Tracing extends ChannelOwner<channels.TracingChannel> implements ap
|
|||
|
||||
// The artifact may be missing if the browser closed while stopping tracing.
|
||||
if (!result.artifact) {
|
||||
if (this._stacksId)
|
||||
if (this._stacksId) {
|
||||
await this._connection.localUtils()._channel.traceDiscarded({ stacksId: this._stacksId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,24 +35,28 @@ export class Video implements api.Video {
|
|||
}
|
||||
|
||||
async path(): Promise<string> {
|
||||
if (this._isRemote)
|
||||
if (this._isRemote) {
|
||||
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
|
||||
}
|
||||
const artifact = await this._artifact;
|
||||
if (!artifact)
|
||||
if (!artifact) {
|
||||
throw new Error('Page did not produce any video frames');
|
||||
}
|
||||
return artifact._initializer.absolutePath;
|
||||
}
|
||||
|
||||
async saveAs(path: string): Promise<void> {
|
||||
const artifact = await this._artifact;
|
||||
if (!artifact)
|
||||
if (!artifact) {
|
||||
throw new Error('Page did not produce any video frames');
|
||||
}
|
||||
return await artifact.saveAs(path);
|
||||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
const artifact = await this._artifact;
|
||||
if (artifact)
|
||||
if (artifact) {
|
||||
await artifact.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,14 +56,19 @@ export class Waiter {
|
|||
|
||||
rejectOnEvent<T = void>(emitter: EventEmitter, event: string, error: Error | (() => Error), predicate?: (arg: T) => boolean | Promise<boolean>) {
|
||||
const { promise, dispose } = waitForEvent(emitter, event, this._savedZone, predicate);
|
||||
this._rejectOn(promise.then(() => { throw (typeof error === 'function' ? error() : error); }), dispose);
|
||||
this._rejectOn(promise.then(() => {
|
||||
throw (typeof error === 'function' ? error() : error);
|
||||
}), dispose);
|
||||
}
|
||||
|
||||
rejectOnTimeout(timeout: number, message: string) {
|
||||
if (!timeout)
|
||||
if (!timeout) {
|
||||
return;
|
||||
}
|
||||
const { promise, dispose } = waitForTimeout(timeout);
|
||||
this._rejectOn(promise.then(() => { throw new TimeoutError(message); }), dispose);
|
||||
this._rejectOn(promise.then(() => {
|
||||
throw new TimeoutError(message);
|
||||
}), dispose);
|
||||
}
|
||||
|
||||
rejectImmediately(error: Error) {
|
||||
|
|
@ -71,21 +76,25 @@ export class Waiter {
|
|||
}
|
||||
|
||||
dispose() {
|
||||
for (const dispose of this._dispose)
|
||||
for (const dispose of this._dispose) {
|
||||
dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async waitForPromise<T>(promise: Promise<T>, dispose?: () => void): Promise<T> {
|
||||
try {
|
||||
if (this._immediateError)
|
||||
if (this._immediateError) {
|
||||
throw this._immediateError;
|
||||
}
|
||||
const result = await Promise.race([promise, ...this._failures]);
|
||||
if (dispose)
|
||||
if (dispose) {
|
||||
dispose();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (dispose)
|
||||
if (dispose) {
|
||||
dispose();
|
||||
}
|
||||
this._error = e.message;
|
||||
this.dispose();
|
||||
rewriteErrorMessage(e, e.message + formatLogRecording(this._logs));
|
||||
|
|
@ -102,10 +111,11 @@ export class Waiter {
|
|||
|
||||
private _rejectOn(promise: Promise<any>, dispose?: () => void) {
|
||||
this._failures.push(promise);
|
||||
if (dispose)
|
||||
if (dispose) {
|
||||
this._dispose.push(dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function waitForEvent<T = void>(emitter: EventEmitter, event: string, savedZone: Zone, predicate?: (arg: T) => boolean | Promise<boolean>): { promise: Promise<T>, dispose: () => void } {
|
||||
let listener: (eventArg: any) => void;
|
||||
|
|
@ -113,8 +123,9 @@ function waitForEvent<T = void>(emitter: EventEmitter, event: string, savedZone:
|
|||
listener = async (eventArg: any) => {
|
||||
await savedZone.run(async () => {
|
||||
try {
|
||||
if (predicate && !(await predicate(eventArg)))
|
||||
if (predicate && !(await predicate(eventArg))) {
|
||||
return;
|
||||
}
|
||||
emitter.removeListener(event, listener);
|
||||
resolve(eventArg);
|
||||
} catch (e) {
|
||||
|
|
@ -137,8 +148,9 @@ function waitForTimeout(timeout: number): { promise: Promise<void>, dispose: ()
|
|||
}
|
||||
|
||||
function formatLogRecording(log: string[]): string {
|
||||
if (!log.length)
|
||||
if (!log.length) {
|
||||
return '';
|
||||
}
|
||||
const header = ` logs `;
|
||||
const headerLength = 60;
|
||||
const leftLength = (headerLength - header.length) / 2;
|
||||
|
|
|
|||
|
|
@ -37,10 +37,12 @@ export class Worker extends ChannelOwner<channels.WorkerChannel> implements api.
|
|||
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WorkerInitializer) {
|
||||
super(parent, type, guid, initializer);
|
||||
this._channel.on('close', () => {
|
||||
if (this._page)
|
||||
if (this._page) {
|
||||
this._page._workers.delete(this);
|
||||
if (this._context)
|
||||
}
|
||||
if (this._context) {
|
||||
this._context._serviceWorkers.delete(this);
|
||||
}
|
||||
this.emit(Events.Worker.Close, this);
|
||||
});
|
||||
this.once(Events.Worker.Close, () => this._closedScope.close(this._page?._closeErrorWithReason() || new TargetClosedError()));
|
||||
|
|
|
|||
|
|
@ -174,8 +174,9 @@ class SocksConnection {
|
|||
case SocksAddressType.IPv6:
|
||||
const bytes = await this._readBytes(16);
|
||||
const tokens: string[] = [];
|
||||
for (let i = 0; i < 8; ++i)
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
tokens.push(bytes.readUInt16BE(i * 2).toString(16));
|
||||
}
|
||||
host = tokens.join(':');
|
||||
break;
|
||||
}
|
||||
|
|
@ -199,16 +200,18 @@ class SocksConnection {
|
|||
|
||||
private async _readBytes(length: number): Promise<Buffer> {
|
||||
this._fence = this._offset + length;
|
||||
if (!this._buffer || this._buffer.length < this._fence)
|
||||
if (!this._buffer || this._buffer.length < this._fence) {
|
||||
await new Promise<void>(f => this._fenceCallback = f);
|
||||
}
|
||||
this._offset += length;
|
||||
return this._buffer.slice(this._offset - length, this._offset);
|
||||
}
|
||||
|
||||
private _writeBytes(buffer: Buffer) {
|
||||
if (this._socket.writable)
|
||||
if (this._socket.writable) {
|
||||
this._socket.write(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private _onClose() {
|
||||
this._client.onSocketClosed({ uid: this._uid });
|
||||
|
|
@ -280,12 +283,18 @@ function hexToNumber(hex: string): number {
|
|||
// Note: parseInt has a few issues including ignoring trailing characters and allowing leading 0x.
|
||||
return [...hex].reduce((value, digit) => {
|
||||
const code = digit.charCodeAt(0);
|
||||
if (code >= 48 && code <= 57) // 0..9
|
||||
if (code >= 48 && code <= 57) {
|
||||
// 0..9
|
||||
return value + code;
|
||||
if (code >= 97 && code <= 102) // a..f
|
||||
}
|
||||
if (code >= 97 && code <= 102) {
|
||||
// a..f
|
||||
return value + (code - 97) + 10;
|
||||
if (code >= 65 && code <= 70) // A..F
|
||||
}
|
||||
if (code >= 65 && code <= 70) {
|
||||
// A..F
|
||||
return value + (code - 65) + 10;
|
||||
}
|
||||
throw new Error('Invalid IPv6 token ' + hex);
|
||||
}, 0);
|
||||
}
|
||||
|
|
@ -300,8 +309,9 @@ function ipToSocksAddress(address: string): number[] {
|
|||
if (net.isIPv6(address)) {
|
||||
const result = [0x04]; // IPv6
|
||||
const tokens = address.split(':', 8);
|
||||
while (tokens.length < 8)
|
||||
while (tokens.length < 8) {
|
||||
tokens.unshift('');
|
||||
}
|
||||
for (const token of tokens) {
|
||||
const value = hexToNumber(token);
|
||||
result.push((value >> 8) & 0xFF, value & 0xFF); // Big-endian
|
||||
|
|
@ -324,21 +334,24 @@ function starMatchToRegex(pattern: string) {
|
|||
// This follows "Proxy bypass rules" syntax without implicit and negative rules.
|
||||
// https://source.chromium.org/chromium/chromium/src/+/main:net/docs/proxy.md;l=331
|
||||
export function parsePattern(pattern: string | undefined): PatternMatcher {
|
||||
if (!pattern)
|
||||
if (!pattern) {
|
||||
return () => false;
|
||||
}
|
||||
|
||||
const matchers: PatternMatcher[] = pattern.split(',').map(token => {
|
||||
const match = token.match(/^(.*?)(?::(\d+))?$/);
|
||||
if (!match)
|
||||
if (!match) {
|
||||
throw new Error(`Unsupported token "${token}" in pattern "${pattern}"`);
|
||||
}
|
||||
const tokenPort = match[2] ? +match[2] : undefined;
|
||||
const portMatches = (port: number) => tokenPort === undefined || tokenPort === port;
|
||||
let tokenHost = match[1];
|
||||
|
||||
if (tokenHost === '<loopback>') {
|
||||
return (host, port) => {
|
||||
if (!portMatches(port))
|
||||
if (!portMatches(port)) {
|
||||
return false;
|
||||
}
|
||||
return host === 'localhost'
|
||||
|| host.endsWith('.localhost')
|
||||
|| host === '127.0.0.1'
|
||||
|
|
@ -346,20 +359,25 @@ export function parsePattern(pattern: string | undefined): PatternMatcher {
|
|||
};
|
||||
}
|
||||
|
||||
if (tokenHost === '*')
|
||||
if (tokenHost === '*') {
|
||||
return (host, port) => portMatches(port);
|
||||
}
|
||||
|
||||
if (net.isIPv4(tokenHost) || net.isIPv6(tokenHost))
|
||||
if (net.isIPv4(tokenHost) || net.isIPv6(tokenHost)) {
|
||||
return (host, port) => host === tokenHost && portMatches(port);
|
||||
}
|
||||
|
||||
if (tokenHost[0] === '.')
|
||||
if (tokenHost[0] === '.') {
|
||||
tokenHost = '*' + tokenHost;
|
||||
}
|
||||
const tokenRegex = starMatchToRegex(tokenHost);
|
||||
return (host, port) => {
|
||||
if (!portMatches(port))
|
||||
if (!portMatches(port)) {
|
||||
return false;
|
||||
if (net.isIPv4(host) || net.isIPv6(host))
|
||||
}
|
||||
if (net.isIPv4(host) || net.isIPv6(host)) {
|
||||
return false;
|
||||
}
|
||||
return !!host.match(tokenRegex);
|
||||
};
|
||||
});
|
||||
|
|
@ -442,11 +460,13 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
|||
}
|
||||
|
||||
async close() {
|
||||
if (this._closed)
|
||||
if (this._closed) {
|
||||
return;
|
||||
}
|
||||
this._closed = true;
|
||||
for (const socket of this._sockets)
|
||||
for (const socket of this._sockets) {
|
||||
socket.destroy();
|
||||
}
|
||||
this._sockets.clear();
|
||||
await new Promise(f => this._server.close(f));
|
||||
}
|
||||
|
|
@ -519,9 +539,10 @@ export class SocksProxyHandler extends EventEmitter {
|
|||
}
|
||||
|
||||
cleanup() {
|
||||
for (const uid of this._sockets.keys())
|
||||
for (const uid of this._sockets.keys()) {
|
||||
this.socketClosed({ uid });
|
||||
}
|
||||
}
|
||||
|
||||
async socketRequested({ uid, host, port }: SocksSocketRequestedPayload): Promise<void> {
|
||||
debugLogger.log('socks', `[${uid}] => request ${host}:${port}`);
|
||||
|
|
@ -532,11 +553,13 @@ export class SocksProxyHandler extends EventEmitter {
|
|||
return;
|
||||
}
|
||||
|
||||
if (host === 'local.playwright')
|
||||
if (host === 'local.playwright') {
|
||||
host = 'localhost';
|
||||
}
|
||||
try {
|
||||
if (this._redirectPortForTest)
|
||||
if (this._redirectPortForTest) {
|
||||
port = this._redirectPortForTest;
|
||||
}
|
||||
const socket = await createSocket(host, port);
|
||||
socket.on('data', data => {
|
||||
const payload: SocksSocketDataPayload = { uid, data };
|
||||
|
|
|
|||
|
|
@ -46,44 +46,57 @@ export class TimeoutSettings {
|
|||
}
|
||||
|
||||
navigationTimeout(options: { timeout?: number }): number {
|
||||
if (typeof options.timeout === 'number')
|
||||
if (typeof options.timeout === 'number') {
|
||||
return options.timeout;
|
||||
if (this._defaultNavigationTimeout !== undefined)
|
||||
}
|
||||
if (this._defaultNavigationTimeout !== undefined) {
|
||||
return this._defaultNavigationTimeout;
|
||||
if (debugMode())
|
||||
}
|
||||
if (debugMode()) {
|
||||
return 0;
|
||||
if (this._defaultTimeout !== undefined)
|
||||
}
|
||||
if (this._defaultTimeout !== undefined) {
|
||||
return this._defaultTimeout;
|
||||
if (this._parent)
|
||||
}
|
||||
if (this._parent) {
|
||||
return this._parent.navigationTimeout(options);
|
||||
}
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
timeout(options: { timeout?: number }): number {
|
||||
if (typeof options.timeout === 'number')
|
||||
if (typeof options.timeout === 'number') {
|
||||
return options.timeout;
|
||||
if (debugMode())
|
||||
}
|
||||
if (debugMode()) {
|
||||
return 0;
|
||||
if (this._defaultTimeout !== undefined)
|
||||
}
|
||||
if (this._defaultTimeout !== undefined) {
|
||||
return this._defaultTimeout;
|
||||
if (this._parent)
|
||||
}
|
||||
if (this._parent) {
|
||||
return this._parent.timeout(options);
|
||||
}
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
static timeout(options: { timeout?: number }): number {
|
||||
if (typeof options.timeout === 'number')
|
||||
if (typeof options.timeout === 'number') {
|
||||
return options.timeout;
|
||||
if (debugMode())
|
||||
}
|
||||
if (debugMode()) {
|
||||
return 0;
|
||||
}
|
||||
return DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
static launchTimeout(options: { timeout?: number }): number {
|
||||
if (typeof options.timeout === 'number')
|
||||
if (typeof options.timeout === 'number') {
|
||||
return options.timeout;
|
||||
if (debugMode())
|
||||
}
|
||||
if (debugMode()) {
|
||||
return 0;
|
||||
}
|
||||
return DEFAULT_LAUNCH_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,12 +67,15 @@ export class FastStats implements Stats {
|
|||
|
||||
const recalc = (mx: number[], idx: number, initial: number, x: number, y: number) => {
|
||||
mx[idx] = initial;
|
||||
if (y > 0)
|
||||
if (y > 0) {
|
||||
mx[idx] += mx[(y - 1) * width + x];
|
||||
if (x > 0)
|
||||
}
|
||||
if (x > 0) {
|
||||
mx[idx] += mx[y * width + x - 1];
|
||||
if (x > 0 && y > 0)
|
||||
}
|
||||
if (x > 0 && y > 0) {
|
||||
mx[idx] -= mx[(y - 1) * width + x - 1];
|
||||
}
|
||||
};
|
||||
|
||||
for (let y = 0; y < height; ++y) {
|
||||
|
|
@ -90,12 +93,15 @@ export class FastStats implements Stats {
|
|||
_sum(partialSum: number[], x1: number, y1: number, x2: number, y2: number): number {
|
||||
const width = this.c1.width;
|
||||
let result = partialSum[y2 * width + x2];
|
||||
if (y1 > 0)
|
||||
if (y1 > 0) {
|
||||
result -= partialSum[(y1 - 1) * width + x2];
|
||||
if (x1 > 0)
|
||||
}
|
||||
if (x1 > 0) {
|
||||
result -= partialSum[y2 * width + x1 - 1];
|
||||
if (x1 > 0 && y1 > 0)
|
||||
}
|
||||
if (x1 > 0 && y1 > 0) {
|
||||
result += partialSum[(y1 - 1) * width + x1 - 1];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,60 +21,77 @@ export function parseSerializedValue(value: SerializedValue, handles: any[] | un
|
|||
}
|
||||
|
||||
function innerParseSerializedValue(value: SerializedValue, handles: any[] | undefined, refs: Map<number, object>): any {
|
||||
if (value.ref !== undefined)
|
||||
if (value.ref !== undefined) {
|
||||
return refs.get(value.ref);
|
||||
if (value.n !== undefined)
|
||||
}
|
||||
if (value.n !== undefined) {
|
||||
return value.n;
|
||||
if (value.s !== undefined)
|
||||
}
|
||||
if (value.s !== undefined) {
|
||||
return value.s;
|
||||
if (value.b !== undefined)
|
||||
}
|
||||
if (value.b !== undefined) {
|
||||
return value.b;
|
||||
}
|
||||
if (value.v !== undefined) {
|
||||
if (value.v === 'undefined')
|
||||
if (value.v === 'undefined') {
|
||||
return undefined;
|
||||
if (value.v === 'null')
|
||||
}
|
||||
if (value.v === 'null') {
|
||||
return null;
|
||||
if (value.v === 'NaN')
|
||||
}
|
||||
if (value.v === 'NaN') {
|
||||
return NaN;
|
||||
if (value.v === 'Infinity')
|
||||
}
|
||||
if (value.v === 'Infinity') {
|
||||
return Infinity;
|
||||
if (value.v === '-Infinity')
|
||||
}
|
||||
if (value.v === '-Infinity') {
|
||||
return -Infinity;
|
||||
if (value.v === '-0')
|
||||
}
|
||||
if (value.v === '-0') {
|
||||
return -0;
|
||||
}
|
||||
if (value.d !== undefined)
|
||||
}
|
||||
if (value.d !== undefined) {
|
||||
return new Date(value.d);
|
||||
if (value.u !== undefined)
|
||||
}
|
||||
if (value.u !== undefined) {
|
||||
return new URL(value.u);
|
||||
if (value.bi !== undefined)
|
||||
}
|
||||
if (value.bi !== undefined) {
|
||||
return BigInt(value.bi);
|
||||
}
|
||||
if (value.e !== undefined) {
|
||||
const error = new Error(value.e.m);
|
||||
error.name = value.e.n;
|
||||
error.stack = value.e.s;
|
||||
return error;
|
||||
}
|
||||
if (value.r !== undefined)
|
||||
if (value.r !== undefined) {
|
||||
return new RegExp(value.r.p, value.r.f);
|
||||
}
|
||||
|
||||
if (value.a !== undefined) {
|
||||
const result: any[] = [];
|
||||
refs.set(value.id!, result);
|
||||
for (const v of value.a)
|
||||
for (const v of value.a) {
|
||||
result.push(innerParseSerializedValue(v, handles, refs));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (value.o !== undefined) {
|
||||
const result: any = {};
|
||||
refs.set(value.id!, result);
|
||||
for (const { k, v } of value.o)
|
||||
for (const { k, v } of value.o) {
|
||||
result[k] = innerParseSerializedValue(v, handles, refs);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (value.h !== undefined) {
|
||||
if (handles === undefined)
|
||||
if (handles === undefined) {
|
||||
throw new Error('Unexpected handle');
|
||||
}
|
||||
return handles[value.h];
|
||||
}
|
||||
throw new Error('Unexpected value');
|
||||
|
|
@ -92,60 +109,79 @@ export function serializeValue(value: any, handleSerializer: (value: any) => Han
|
|||
|
||||
function innerSerializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, visitorInfo: VisitorInfo): SerializedValue {
|
||||
const handle = handleSerializer(value);
|
||||
if ('fallThrough' in handle)
|
||||
if ('fallThrough' in handle) {
|
||||
value = handle.fallThrough;
|
||||
else
|
||||
} else {
|
||||
return handle;
|
||||
}
|
||||
|
||||
if (typeof value === 'symbol')
|
||||
if (typeof value === 'symbol') {
|
||||
return { v: 'undefined' };
|
||||
if (Object.is(value, undefined))
|
||||
}
|
||||
if (Object.is(value, undefined)) {
|
||||
return { v: 'undefined' };
|
||||
if (Object.is(value, null))
|
||||
}
|
||||
if (Object.is(value, null)) {
|
||||
return { v: 'null' };
|
||||
if (Object.is(value, NaN))
|
||||
}
|
||||
if (Object.is(value, NaN)) {
|
||||
return { v: 'NaN' };
|
||||
if (Object.is(value, Infinity))
|
||||
}
|
||||
if (Object.is(value, Infinity)) {
|
||||
return { v: 'Infinity' };
|
||||
if (Object.is(value, -Infinity))
|
||||
}
|
||||
if (Object.is(value, -Infinity)) {
|
||||
return { v: '-Infinity' };
|
||||
if (Object.is(value, -0))
|
||||
}
|
||||
if (Object.is(value, -0)) {
|
||||
return { v: '-0' };
|
||||
if (typeof value === 'boolean')
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return { b: value };
|
||||
if (typeof value === 'number')
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return { n: value };
|
||||
if (typeof value === 'string')
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return { s: value };
|
||||
if (typeof value === 'bigint')
|
||||
}
|
||||
if (typeof value === 'bigint') {
|
||||
return { bi: value.toString() };
|
||||
if (isError(value))
|
||||
}
|
||||
if (isError(value)) {
|
||||
return { e: { n: value.name, m: value.message, s: value.stack || '' } };
|
||||
if (isDate(value))
|
||||
}
|
||||
if (isDate(value)) {
|
||||
return { d: value.toJSON() };
|
||||
if (isURL(value))
|
||||
}
|
||||
if (isURL(value)) {
|
||||
return { u: value.toJSON() };
|
||||
if (isRegExp(value))
|
||||
}
|
||||
if (isRegExp(value)) {
|
||||
return { r: { p: value.source, f: value.flags } };
|
||||
}
|
||||
|
||||
const id = visitorInfo.visited.get(value);
|
||||
if (id)
|
||||
if (id) {
|
||||
return { ref: id };
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const a = [];
|
||||
const id = ++visitorInfo.lastId;
|
||||
visitorInfo.visited.set(value, id);
|
||||
for (let i = 0; i < value.length; ++i)
|
||||
for (let i = 0; i < value.length; ++i) {
|
||||
a.push(innerSerializeValue(value[i], handleSerializer, visitorInfo));
|
||||
}
|
||||
return { a, id };
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const o: { k: string, v: SerializedValue }[] = [];
|
||||
const id = ++visitorInfo.lastId;
|
||||
visitorInfo.visited.set(value, id);
|
||||
for (const name of Object.keys(value))
|
||||
for (const name of Object.keys(value)) {
|
||||
o.push({ k: name, v: innerSerializeValue(value[name], handleSerializer, visitorInfo) });
|
||||
}
|
||||
return { o, id };
|
||||
}
|
||||
throw new Error('Unexpected value');
|
||||
|
|
|
|||
|
|
@ -49,22 +49,25 @@ export class PipeTransport {
|
|||
pipeRead.on('data', buffer => this._dispatch(buffer));
|
||||
pipeRead.on('close', () => {
|
||||
this._closed = true;
|
||||
if (this.onclose)
|
||||
if (this.onclose) {
|
||||
this.onclose();
|
||||
}
|
||||
});
|
||||
this.onmessage = undefined;
|
||||
this.onclose = undefined;
|
||||
}
|
||||
|
||||
send(message: string) {
|
||||
if (this._closed)
|
||||
if (this._closed) {
|
||||
throw new Error('Pipe has been closed');
|
||||
}
|
||||
const data = Buffer.from(message, 'utf-8');
|
||||
const dataLength = Buffer.alloc(4);
|
||||
if (this._endian === 'be')
|
||||
if (this._endian === 'be') {
|
||||
dataLength.writeUInt32BE(data.length, 0);
|
||||
else
|
||||
} else {
|
||||
dataLength.writeUInt32LE(data.length, 0);
|
||||
}
|
||||
this._pipeWrite.write(dataLength);
|
||||
this._pipeWrite.write(data);
|
||||
}
|
||||
|
|
@ -96,8 +99,9 @@ export class PipeTransport {
|
|||
this._data = this._data.slice(this._bytesLeft);
|
||||
this._bytesLeft = 0;
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onmessage)
|
||||
if (this.onmessage) {
|
||||
this.onmessage(message.toString('utf-8'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ export const scheme: { [key: string]: Validator } = {};
|
|||
|
||||
export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator {
|
||||
const validator = maybeFindValidator(type, method, kind);
|
||||
if (!validator)
|
||||
if (!validator) {
|
||||
throw new ValidationError(`Unknown scheme for ${kind}: ${type}.${method}`);
|
||||
}
|
||||
return validator;
|
||||
}
|
||||
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator | undefined {
|
||||
|
|
@ -39,49 +40,60 @@ export function createMetadataValidator(): Validator {
|
|||
}
|
||||
|
||||
export const tNumber: Validator = (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (arg instanceof Number)
|
||||
if (arg instanceof Number) {
|
||||
return arg.valueOf();
|
||||
if (typeof arg === 'number')
|
||||
}
|
||||
if (typeof arg === 'number') {
|
||||
return arg;
|
||||
}
|
||||
throw new ValidationError(`${path}: expected number, got ${typeof arg}`);
|
||||
};
|
||||
export const tBoolean: Validator = (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (arg instanceof Boolean)
|
||||
if (arg instanceof Boolean) {
|
||||
return arg.valueOf();
|
||||
if (typeof arg === 'boolean')
|
||||
}
|
||||
if (typeof arg === 'boolean') {
|
||||
return arg;
|
||||
}
|
||||
throw new ValidationError(`${path}: expected boolean, got ${typeof arg}`);
|
||||
};
|
||||
export const tString: Validator = (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (arg instanceof String)
|
||||
if (arg instanceof String) {
|
||||
return arg.valueOf();
|
||||
if (typeof arg === 'string')
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
return arg;
|
||||
}
|
||||
throw new ValidationError(`${path}: expected string, got ${typeof arg}`);
|
||||
};
|
||||
export const tBinary: Validator = (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (context.binary === 'fromBase64') {
|
||||
if (arg instanceof String)
|
||||
if (arg instanceof String) {
|
||||
return Buffer.from(arg.valueOf(), 'base64');
|
||||
if (typeof arg === 'string')
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
return Buffer.from(arg, 'base64');
|
||||
}
|
||||
throw new ValidationError(`${path}: expected base64-encoded buffer, got ${typeof arg}`);
|
||||
}
|
||||
if (context.binary === 'toBase64') {
|
||||
if (!(arg instanceof Buffer))
|
||||
if (!(arg instanceof Buffer)) {
|
||||
throw new ValidationError(`${path}: expected Buffer, got ${typeof arg}`);
|
||||
}
|
||||
return (arg as Buffer).toString('base64');
|
||||
}
|
||||
if (context.binary === 'buffer') {
|
||||
if (!(arg instanceof Buffer))
|
||||
if (!(arg instanceof Buffer)) {
|
||||
throw new ValidationError(`${path}: expected Buffer, got ${typeof arg}`);
|
||||
}
|
||||
return arg;
|
||||
}
|
||||
throw new ValidationError(`Unsupported binary behavior "${context.binary}"`);
|
||||
};
|
||||
export const tUndefined: Validator = (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (Object.is(arg, undefined))
|
||||
if (Object.is(arg, undefined)) {
|
||||
return arg;
|
||||
}
|
||||
throw new ValidationError(`${path}: expected undefined, got ${typeof arg}`);
|
||||
};
|
||||
export const tAny: Validator = (arg: any, path: string, context: ValidatorContext) => {
|
||||
|
|
@ -89,43 +101,50 @@ export const tAny: Validator = (arg: any, path: string, context: ValidatorContex
|
|||
};
|
||||
export const tOptional = (v: Validator): Validator => {
|
||||
return (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (Object.is(arg, undefined))
|
||||
if (Object.is(arg, undefined)) {
|
||||
return arg;
|
||||
}
|
||||
return v(arg, path, context);
|
||||
};
|
||||
};
|
||||
export const tArray = (v: Validator): Validator => {
|
||||
return (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (!Array.isArray(arg))
|
||||
if (!Array.isArray(arg)) {
|
||||
throw new ValidationError(`${path}: expected array, got ${typeof arg}`);
|
||||
}
|
||||
return arg.map((x, index) => v(x, path + '[' + index + ']', context));
|
||||
};
|
||||
};
|
||||
export const tObject = (s: { [key: string]: Validator }): Validator => {
|
||||
return (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (Object.is(arg, null))
|
||||
if (Object.is(arg, null)) {
|
||||
throw new ValidationError(`${path}: expected object, got null`);
|
||||
if (typeof arg !== 'object')
|
||||
}
|
||||
if (typeof arg !== 'object') {
|
||||
throw new ValidationError(`${path}: expected object, got ${typeof arg}`);
|
||||
}
|
||||
const result: any = {};
|
||||
for (const [key, v] of Object.entries(s)) {
|
||||
const value = v(arg[key], path ? path + '.' + key : key, context);
|
||||
if (!Object.is(value, undefined))
|
||||
if (!Object.is(value, undefined)) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
if (isUnderTest()) {
|
||||
for (const [key, value] of Object.entries(arg)) {
|
||||
if (key.startsWith('__testHook'))
|
||||
if (key.startsWith('__testHook')) {
|
||||
result[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
};
|
||||
export const tEnum = (e: string[]): Validator => {
|
||||
return (arg: any, path: string, context: ValidatorContext) => {
|
||||
if (!e.includes(arg))
|
||||
if (!e.includes(arg)) {
|
||||
throw new ValidationError(`${path}: expected one of (${e.join('|')})`);
|
||||
}
|
||||
return arg;
|
||||
};
|
||||
};
|
||||
|
|
@ -137,8 +156,9 @@ export const tChannel = (names: '*' | string[]): Validator => {
|
|||
export const tType = (name: string): Validator => {
|
||||
return (arg: any, path: string, context: ValidatorContext) => {
|
||||
const v = scheme[name];
|
||||
if (!v)
|
||||
if (!v) {
|
||||
throw new ValidationError(path + ': unknown type "' + name + '"');
|
||||
}
|
||||
return v(arg, path, context);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -61,10 +61,12 @@ export class PlaywrightConnection {
|
|||
this._preLaunched = preLaunched;
|
||||
this._options = options;
|
||||
options.launchOptions = filterLaunchOptions(options.launchOptions);
|
||||
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android')
|
||||
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android') {
|
||||
assert(preLaunched.playwright);
|
||||
if (clientType === 'pre-launched-browser-or-android')
|
||||
}
|
||||
if (clientType === 'pre-launched-browser-or-android') {
|
||||
assert(preLaunched.browser || preLaunched.androidDevice);
|
||||
}
|
||||
this._onClose = onClose;
|
||||
this._id = id;
|
||||
this._profileName = `${new Date().toISOString()}-${clientType}`;
|
||||
|
|
@ -74,10 +76,12 @@ export class PlaywrightConnection {
|
|||
await lock;
|
||||
if (ws.readyState !== ws.CLOSING) {
|
||||
const messageString = JSON.stringify(message);
|
||||
if (debugLogger.isEnabled('server:channel'))
|
||||
if (debugLogger.isEnabled('server:channel')) {
|
||||
debugLogger.log('server:channel', `[${this._id}] ${monotonicTime() * 1000} SEND ► ${messageString}`);
|
||||
if (debugLogger.isEnabled('server:metadata'))
|
||||
}
|
||||
if (debugLogger.isEnabled('server:metadata')) {
|
||||
this.logServerMetadata(message, messageString, 'SEND');
|
||||
}
|
||||
ws.send(messageString);
|
||||
}
|
||||
};
|
||||
|
|
@ -85,10 +89,12 @@ export class PlaywrightConnection {
|
|||
await lock;
|
||||
const messageString = Buffer.from(message).toString();
|
||||
const jsonMessage = JSON.parse(messageString);
|
||||
if (debugLogger.isEnabled('server:channel'))
|
||||
if (debugLogger.isEnabled('server:channel')) {
|
||||
debugLogger.log('server:channel', `[${this._id}] ${monotonicTime() * 1000} ◀ RECV ${messageString}`);
|
||||
if (debugLogger.isEnabled('server:metadata'))
|
||||
}
|
||||
if (debugLogger.isEnabled('server:metadata')) {
|
||||
this.logServerMetadata(jsonMessage, messageString, 'RECV');
|
||||
}
|
||||
this._dispatcherConnection.dispatch(jsonMessage);
|
||||
});
|
||||
|
||||
|
|
@ -102,12 +108,15 @@ export class PlaywrightConnection {
|
|||
|
||||
this._root = new RootDispatcher(this._dispatcherConnection, async (scope, options) => {
|
||||
await startProfiling();
|
||||
if (clientType === 'reuse-browser')
|
||||
if (clientType === 'reuse-browser') {
|
||||
return await this._initReuseBrowsersMode(scope);
|
||||
if (clientType === 'pre-launched-browser-or-android')
|
||||
}
|
||||
if (clientType === 'pre-launched-browser-or-android') {
|
||||
return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
|
||||
if (clientType === 'launch-browser')
|
||||
}
|
||||
if (clientType === 'launch-browser') {
|
||||
return await this._initLaunchBrowserMode(scope, options);
|
||||
}
|
||||
throw new Error('Unsupported client type: ' + clientType);
|
||||
});
|
||||
}
|
||||
|
|
@ -120,8 +129,9 @@ export class PlaywrightConnection {
|
|||
const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
|
||||
|
||||
this._cleanups.push(async () => {
|
||||
for (const browser of playwright.allBrowsers())
|
||||
for (const browser of playwright.allBrowsers()) {
|
||||
await browser.close({ reason: 'Connection terminated' });
|
||||
}
|
||||
});
|
||||
browser.on(Browser.Events.Disconnected, () => {
|
||||
// Underlying browser did close for some reason - force disconnect the client.
|
||||
|
|
@ -147,9 +157,10 @@ export class PlaywrightConnection {
|
|||
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, this._preLaunched.socksProxy, browser);
|
||||
// In pre-launched mode, keep only the pre-launched browser.
|
||||
for (const b of playwright.allBrowsers()) {
|
||||
if (b !== browser)
|
||||
if (b !== browser) {
|
||||
await b.close({ reason: 'Connection terminated' });
|
||||
}
|
||||
}
|
||||
this._cleanups.push(() => playwrightDispatcher.cleanup());
|
||||
return playwrightDispatcher;
|
||||
}
|
||||
|
|
@ -183,19 +194,22 @@ export class PlaywrightConnection {
|
|||
|
||||
const requestedOptions = launchOptionsHash(this._options.launchOptions);
|
||||
let browser = playwright.allBrowsers().find(b => {
|
||||
if (b.options.name !== this._options.browserName)
|
||||
if (b.options.name !== this._options.browserName) {
|
||||
return false;
|
||||
}
|
||||
const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
|
||||
return existingOptions === requestedOptions;
|
||||
});
|
||||
|
||||
// Close remaining browsers of this type+channel. Keep different browser types for the speed.
|
||||
for (const b of playwright.allBrowsers()) {
|
||||
if (b === browser)
|
||||
if (b === browser) {
|
||||
continue;
|
||||
if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel)
|
||||
}
|
||||
if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel) {
|
||||
await b.close({ reason: 'Connection terminated' });
|
||||
}
|
||||
}
|
||||
|
||||
if (!browser) {
|
||||
browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), {
|
||||
|
|
@ -213,14 +227,16 @@ export class PlaywrightConnection {
|
|||
// but close all the empty browsers and contexts to clean up.
|
||||
for (const browser of playwright.allBrowsers()) {
|
||||
for (const context of browser.contexts()) {
|
||||
if (!context.pages().length)
|
||||
if (!context.pages().length) {
|
||||
await context.close({ reason: 'Connection terminated' });
|
||||
else
|
||||
} else {
|
||||
await context.stopPendingOperations('Connection closed');
|
||||
}
|
||||
if (!browser.contexts())
|
||||
}
|
||||
if (!browser.contexts()) {
|
||||
await browser.close({ reason: 'Connection terminated' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, browser);
|
||||
|
|
@ -228,8 +244,9 @@ export class PlaywrightConnection {
|
|||
}
|
||||
|
||||
private async _createOwnedSocksProxy(playwright: Playwright): Promise<SocksProxy | undefined> {
|
||||
if (!this._options.socksProxyPattern)
|
||||
if (!this._options.socksProxyPattern) {
|
||||
return;
|
||||
}
|
||||
const socksProxy = new SocksProxy();
|
||||
socksProxy.setPattern(this._options.socksProxyPattern);
|
||||
playwright.options.socksProxyPort = await socksProxy.listen(0);
|
||||
|
|
@ -243,8 +260,9 @@ export class PlaywrightConnection {
|
|||
debugLogger.log('server', `[${this._id}] disconnected. error: ${error}`);
|
||||
this._root._dispose();
|
||||
debugLogger.log('server', `[${this._id}] starting cleanup`);
|
||||
for (const cleanup of this._cleanups)
|
||||
for (const cleanup of this._cleanups) {
|
||||
await cleanup().catch(() => {});
|
||||
}
|
||||
await stopProfiling(this._profileName);
|
||||
this._onClose();
|
||||
debugLogger.log('server', `[${this._id}] finished cleanup`);
|
||||
|
|
@ -262,8 +280,9 @@ export class PlaywrightConnection {
|
|||
}
|
||||
|
||||
async close(reason?: { code: number, reason: string }) {
|
||||
if (this._disconnected)
|
||||
if (this._disconnected) {
|
||||
return;
|
||||
}
|
||||
debugLogger.log('server', `[${this._id}] force closing connection: ${reason?.reason || ''} (${reason?.code || 0})`);
|
||||
try {
|
||||
this._ws.close(reason?.code, reason?.reason);
|
||||
|
|
@ -276,11 +295,13 @@ function launchOptionsHash(options: LaunchOptions) {
|
|||
const copy = { ...options };
|
||||
for (const k of Object.keys(copy)) {
|
||||
const key = k as keyof LaunchOptions;
|
||||
if (copy[key] === defaultLaunchOptions[key])
|
||||
if (copy[key] === defaultLaunchOptions[key]) {
|
||||
delete copy[key];
|
||||
}
|
||||
for (const key of optionsThatAllowBrowserReuse)
|
||||
}
|
||||
for (const key of optionsThatAllowBrowserReuse) {
|
||||
delete copy[key];
|
||||
}
|
||||
return JSON.stringify(copy);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,10 +43,12 @@ export class PlaywrightServer {
|
|||
|
||||
constructor(options: ServerOptions) {
|
||||
this._options = options;
|
||||
if (options.preLaunchedBrowser)
|
||||
if (options.preLaunchedBrowser) {
|
||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
|
||||
if (options.preLaunchedAndroidDevice)
|
||||
}
|
||||
if (options.preLaunchedAndroidDevice) {
|
||||
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
|
||||
}
|
||||
|
||||
const browserSemaphore = new Semaphore(this._options.maxConnections);
|
||||
const controllerSemaphore = new Semaphore(1);
|
||||
|
|
@ -55,13 +57,15 @@ export class PlaywrightServer {
|
|||
this._wsServer = new WSServer({
|
||||
onUpgrade: (request, socket) => {
|
||||
const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
|
||||
if (uaError)
|
||||
if (uaError) {
|
||||
return { error: `HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}` };
|
||||
}
|
||||
},
|
||||
|
||||
onHeaders: headers => {
|
||||
if (process.env.PWTEST_SERVER_WS_HEADERS)
|
||||
if (process.env.PWTEST_SERVER_WS_HEADERS) {
|
||||
headers.push(process.env.PWTEST_SERVER_WS_HEADERS!);
|
||||
}
|
||||
},
|
||||
|
||||
onConnection: (request, url, ws, id) => {
|
||||
|
|
@ -82,9 +86,10 @@ export class PlaywrightServer {
|
|||
// Instantiate playwright for the extension modes.
|
||||
const isExtension = this._options.mode === 'extension';
|
||||
if (isExtension) {
|
||||
if (!this._preLaunchedPlaywright)
|
||||
if (!this._preLaunchedPlaywright) {
|
||||
this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
|
||||
}
|
||||
}
|
||||
|
||||
let clientType: ClientType = 'launch-browser';
|
||||
let semaphore: Semaphore = browserSemaphore;
|
||||
|
|
@ -114,8 +119,9 @@ export class PlaywrightServer {
|
|||
|
||||
onClose: async () => {
|
||||
debugLogger.log('server', 'closing browsers');
|
||||
if (this._preLaunchedPlaywright)
|
||||
if (this._preLaunchedPlaywright) {
|
||||
await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' })));
|
||||
}
|
||||
debugLogger.log('server', 'closed browsers');
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -42,39 +42,47 @@ export class Accessibility {
|
|||
} = options;
|
||||
const { tree, needle } = await this._getAXTree(root || undefined);
|
||||
if (!interestingOnly) {
|
||||
if (root)
|
||||
if (root) {
|
||||
return needle && serializeTree(needle)[0];
|
||||
}
|
||||
return serializeTree(tree)[0];
|
||||
}
|
||||
|
||||
const interestingNodes: Set<AXNode> = new Set();
|
||||
collectInterestingNodes(interestingNodes, tree, false);
|
||||
if (root && (!needle || !interestingNodes.has(needle)))
|
||||
if (root && (!needle || !interestingNodes.has(needle))) {
|
||||
return null;
|
||||
}
|
||||
return serializeTree(needle || tree, interestingNodes)[0];
|
||||
}
|
||||
}
|
||||
|
||||
function collectInterestingNodes(collection: Set<AXNode>, node: AXNode, insideControl: boolean) {
|
||||
if (node.isInteresting(insideControl))
|
||||
if (node.isInteresting(insideControl)) {
|
||||
collection.add(node);
|
||||
if (node.isLeafNode())
|
||||
}
|
||||
if (node.isLeafNode()) {
|
||||
return;
|
||||
}
|
||||
insideControl = insideControl || node.isControl();
|
||||
for (const child of node.children())
|
||||
for (const child of node.children()) {
|
||||
collectInterestingNodes(collection, child, insideControl);
|
||||
}
|
||||
}
|
||||
|
||||
function serializeTree(node: AXNode, whitelistedNodes?: Set<AXNode>): channels.AXNode[] {
|
||||
const children: channels.AXNode[] = [];
|
||||
for (const child of node.children())
|
||||
for (const child of node.children()) {
|
||||
children.push(...serializeTree(child, whitelistedNodes));
|
||||
}
|
||||
|
||||
if (whitelistedNodes && !whitelistedNodes.has(node))
|
||||
if (whitelistedNodes && !whitelistedNodes.has(node)) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const serializedNode = node.serialize();
|
||||
if (children.length)
|
||||
if (children.length) {
|
||||
serializedNode.children = children;
|
||||
}
|
||||
return [serializedNode];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,15 +80,17 @@ export class Android extends SdkObject {
|
|||
const newSerials = new Set<string>();
|
||||
for (const d of devices) {
|
||||
newSerials.add(d.serial);
|
||||
if (this._devices.has(d.serial))
|
||||
if (this._devices.has(d.serial)) {
|
||||
continue;
|
||||
}
|
||||
const device = await AndroidDevice.create(this, d, options);
|
||||
this._devices.set(d.serial, device);
|
||||
}
|
||||
for (const d of this._devices.keys()) {
|
||||
if (!newSerials.has(d))
|
||||
if (!newSerials.has(d)) {
|
||||
this._devices.delete(d);
|
||||
}
|
||||
}
|
||||
return [...this._devices.values()];
|
||||
}
|
||||
|
||||
|
|
@ -168,10 +170,12 @@ export class AndroidDevice extends SdkObject {
|
|||
}
|
||||
|
||||
private async _driver(): Promise<PipeTransport | undefined> {
|
||||
if (this._isClosed)
|
||||
if (this._isClosed) {
|
||||
return;
|
||||
if (!this._driverPromise)
|
||||
}
|
||||
if (!this._driverPromise) {
|
||||
this._driverPromise = this._installDriver();
|
||||
}
|
||||
return this._driverPromise;
|
||||
}
|
||||
|
||||
|
|
@ -190,8 +194,9 @@ export class AndroidDevice extends SdkObject {
|
|||
const packageManagerCommand = getPackageManagerExecCommand();
|
||||
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
|
||||
const fullName = path.join(executable.directory!, file);
|
||||
if (!fs.existsSync(fullName))
|
||||
if (!fs.existsSync(fullName)) {
|
||||
throw new Error(`Please install Android driver apk using '${packageManagerCommand} playwright install android'`);
|
||||
}
|
||||
await this.installApk(await fs.promises.readFile(fullName));
|
||||
}
|
||||
} else {
|
||||
|
|
@ -206,12 +211,14 @@ export class AndroidDevice extends SdkObject {
|
|||
const response = JSON.parse(message);
|
||||
const { id, result, error } = response;
|
||||
const callback = this._callbacks.get(id);
|
||||
if (!callback)
|
||||
if (!callback) {
|
||||
return;
|
||||
if (error)
|
||||
}
|
||||
if (error) {
|
||||
callback.reject(new Error(error));
|
||||
else
|
||||
} else {
|
||||
callback.fulfill(result);
|
||||
}
|
||||
this._callbacks.delete(id);
|
||||
};
|
||||
return transport;
|
||||
|
|
@ -235,8 +242,9 @@ export class AndroidDevice extends SdkObject {
|
|||
// Patch the timeout in!
|
||||
params.timeout = this._timeoutSettings.timeout(params);
|
||||
const driver = await this._driver();
|
||||
if (!driver)
|
||||
if (!driver) {
|
||||
throw new Error('Device is closed');
|
||||
}
|
||||
const id = ++this._lastId;
|
||||
const result = new Promise((fulfill, reject) => this._callbacks.set(id, { fulfill, reject }));
|
||||
driver.send(JSON.stringify({ id, method, params }));
|
||||
|
|
@ -244,13 +252,16 @@ export class AndroidDevice extends SdkObject {
|
|||
}
|
||||
|
||||
async close() {
|
||||
if (this._isClosed)
|
||||
if (this._isClosed) {
|
||||
return;
|
||||
}
|
||||
this._isClosed = true;
|
||||
if (this._pollingWebViews)
|
||||
if (this._pollingWebViews) {
|
||||
clearTimeout(this._pollingWebViews);
|
||||
for (const connection of this._browserConnections)
|
||||
}
|
||||
for (const connection of this._browserConnections) {
|
||||
await connection.close();
|
||||
}
|
||||
if (this._driverPromise) {
|
||||
const driver = await this._driver();
|
||||
driver?.close();
|
||||
|
|
@ -292,21 +303,25 @@ export class AndroidDevice extends SdkObject {
|
|||
if (proxy) {
|
||||
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||
const proxyBypassRules = [];
|
||||
if (proxy.bypass)
|
||||
if (proxy.bypass) {
|
||||
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
|
||||
}
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) {
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxyBypassRules.length > 0)
|
||||
}
|
||||
if (proxyBypassRules.length > 0) {
|
||||
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
|
||||
}
|
||||
}
|
||||
chromeArguments.push(...args);
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
async connectToWebView(socketName: string): Promise<BrowserContext> {
|
||||
const webView = this._webViews.get(socketName);
|
||||
if (!webView)
|
||||
if (!webView) {
|
||||
throw new Error('WebView has been closed');
|
||||
}
|
||||
return await this._connectToBrowser(socketName);
|
||||
}
|
||||
|
||||
|
|
@ -319,8 +334,9 @@ export class AndroidDevice extends SdkObject {
|
|||
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
||||
const cleanupArtifactsDir = async () => {
|
||||
const errors = await removeFolders([artifactsDir]);
|
||||
for (let i = 0; i < (errors || []).length; ++i)
|
||||
for (let i = 0; i < (errors || []).length; ++i) {
|
||||
debug('pw:android')(`exception while removing ${artifactsDir}: ${errors[i]}`);
|
||||
}
|
||||
};
|
||||
gracefullyCloseSet.add(cleanupArtifactsDir);
|
||||
socket.on('close', async () => {
|
||||
|
|
@ -381,43 +397,50 @@ export class AndroidDevice extends SdkObject {
|
|||
};
|
||||
await send('SEND', Buffer.from(`${path},${mode}`));
|
||||
const maxChunk = 65535;
|
||||
for (let i = 0; i < content.length; i += maxChunk)
|
||||
for (let i = 0; i < content.length; i += maxChunk) {
|
||||
await send('DATA', content.slice(i, i + maxChunk));
|
||||
}
|
||||
await sendHeader('DONE', (Date.now() / 1000) | 0);
|
||||
const result = await new Promise<Buffer>(f => socket.once('data', f));
|
||||
const code = result.slice(0, 4).toString();
|
||||
if (code !== 'OKAY')
|
||||
if (code !== 'OKAY') {
|
||||
throw new Error('Could not push: ' + code);
|
||||
}
|
||||
socket.close();
|
||||
}
|
||||
|
||||
private async _refreshWebViews() {
|
||||
// possible socketName, eg: webview_devtools_remote_32327, webview_devtools_remote_32327_zeus, webview_devtools_remote_zeus
|
||||
const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
|
||||
if (this._isClosed)
|
||||
if (this._isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socketNames = new Set<string>();
|
||||
for (const line of sockets) {
|
||||
const matchSocketName = line.match(/[^@]+@(.*?webview_devtools_remote_?.*)/);
|
||||
if (!matchSocketName)
|
||||
if (!matchSocketName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const socketName = matchSocketName[1];
|
||||
socketNames.add(socketName);
|
||||
if (this._webViews.has(socketName))
|
||||
if (this._webViews.has(socketName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// possible line: 0000000000000000: 00000002 00000000 00010000 0001 01 5841881 @webview_devtools_remote_zeus
|
||||
// the result: match[1] = ''
|
||||
const match = line.match(/[^@]+@.*?webview_devtools_remote_?(\d*)/);
|
||||
let pid = -1;
|
||||
if (match && match[1])
|
||||
if (match && match[1]) {
|
||||
pid = +match[1];
|
||||
}
|
||||
|
||||
const pkg = await this._extractPkg(pid);
|
||||
if (this._isClosed)
|
||||
if (this._isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webView = { pid, pkg, socketName };
|
||||
this._webViews.set(socketName, webView);
|
||||
|
|
@ -433,14 +456,16 @@ export class AndroidDevice extends SdkObject {
|
|||
|
||||
private async _extractPkg(pid: number) {
|
||||
let pkg = '';
|
||||
if (pid === -1)
|
||||
if (pid === -1) {
|
||||
return pkg;
|
||||
}
|
||||
|
||||
const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n');
|
||||
for (const proc of procs) {
|
||||
const match = proc.match(/[^\s]+\s+(\d+).*$/);
|
||||
if (!match)
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
pkg = proc.substring(proc.lastIndexOf(' ') + 1);
|
||||
}
|
||||
return pkg;
|
||||
|
|
@ -462,15 +487,17 @@ class AndroidBrowser extends EventEmitter {
|
|||
this._socket = socket;
|
||||
this._socket.on('close', () => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onclose)
|
||||
if (this.onclose) {
|
||||
this.onclose();
|
||||
}
|
||||
});
|
||||
});
|
||||
this._receiver = new wsReceiver() as stream.Writable;
|
||||
this._receiver.on('message', message => {
|
||||
this._waitForNextTask(() => {
|
||||
if (this.onmessage)
|
||||
if (this.onmessage) {
|
||||
this.onmessage(JSON.parse(message));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,14 +54,16 @@ class AdbDevice implements DeviceBackend {
|
|||
}
|
||||
|
||||
runCommand(command: string): Promise<Buffer> {
|
||||
if (this._closed)
|
||||
if (this._closed) {
|
||||
throw new Error('Device is closed');
|
||||
}
|
||||
return runCommand(command, this.host, this.port, this.serial);
|
||||
}
|
||||
|
||||
async open(command: string): Promise<SocketBackend> {
|
||||
if (this._closed)
|
||||
if (this._closed) {
|
||||
throw new Error('Device is closed');
|
||||
}
|
||||
const result = await open(command, this.host, this.port, this.serial);
|
||||
result.becomeSocket();
|
||||
return result;
|
||||
|
|
@ -134,13 +136,15 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
return;
|
||||
}
|
||||
this._buffer = Buffer.concat([this._buffer, data]);
|
||||
if (this._notifyReader)
|
||||
if (this._notifyReader) {
|
||||
this._notifyReader();
|
||||
}
|
||||
});
|
||||
this._socket.on('close', () => {
|
||||
this._isClosed = true;
|
||||
if (this._notifyReader)
|
||||
if (this._notifyReader) {
|
||||
this._notifyReader();
|
||||
}
|
||||
this.close();
|
||||
this.emit('close');
|
||||
});
|
||||
|
|
@ -154,8 +158,9 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
}
|
||||
|
||||
close() {
|
||||
if (this._isClosed)
|
||||
if (this._isClosed) {
|
||||
return;
|
||||
}
|
||||
debug('pw:adb')('Close ' + this._command);
|
||||
this._socket.destroy();
|
||||
}
|
||||
|
|
@ -163,8 +168,9 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
async read(length: number): Promise<Buffer> {
|
||||
await this._connectPromise;
|
||||
assert(!this._isSocket, 'Can not read by length in socket mode');
|
||||
while (this._buffer.length < length)
|
||||
while (this._buffer.length < length) {
|
||||
await new Promise<void>(f => this._notifyReader = f);
|
||||
}
|
||||
const result = this._buffer.slice(0, length);
|
||||
this._buffer = this._buffer.slice(length);
|
||||
debug('pw:adb:recv')(result.toString().substring(0, 100) + '...');
|
||||
|
|
@ -172,8 +178,9 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
|||
}
|
||||
|
||||
async readAll(): Promise<Buffer> {
|
||||
while (!this._isClosed)
|
||||
while (!this._isClosed) {
|
||||
await new Promise<void>(f => this._notifyReader = f);
|
||||
}
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,8 @@ export function parseAriaSnapshot(text: string): AriaTemplateNode {
|
|||
|
||||
export function parseYamlForAriaSnapshot(text: string): ParsedYaml {
|
||||
const parsed = yaml.parse(text);
|
||||
if (!Array.isArray(parsed))
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('Expected object key starting with "- ":\n\n' + text + '\n');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,21 +49,26 @@ export class Artifact extends SdkObject {
|
|||
}
|
||||
|
||||
async localPathAfterFinished(): Promise<string> {
|
||||
if (this._unaccessibleErrorMessage)
|
||||
if (this._unaccessibleErrorMessage) {
|
||||
throw new Error(this._unaccessibleErrorMessage);
|
||||
}
|
||||
await this._finishedPromise;
|
||||
if (this._failureError)
|
||||
if (this._failureError) {
|
||||
throw this._failureError;
|
||||
}
|
||||
return this._localPath;
|
||||
}
|
||||
|
||||
saveAs(saveCallback: SaveCallback) {
|
||||
if (this._unaccessibleErrorMessage)
|
||||
if (this._unaccessibleErrorMessage) {
|
||||
throw new Error(this._unaccessibleErrorMessage);
|
||||
if (this._deleted)
|
||||
}
|
||||
if (this._deleted) {
|
||||
throw new Error(`File already deleted. Save before deleting.`);
|
||||
if (this._failureError)
|
||||
}
|
||||
if (this._failureError) {
|
||||
throw this._failureError;
|
||||
}
|
||||
|
||||
if (this._finished) {
|
||||
saveCallback(this._localPath).catch(() => {});
|
||||
|
|
@ -73,8 +78,9 @@ export class Artifact extends SdkObject {
|
|||
}
|
||||
|
||||
async failureError(): Promise<string | null> {
|
||||
if (this._unaccessibleErrorMessage)
|
||||
if (this._unaccessibleErrorMessage) {
|
||||
return this._unaccessibleErrorMessage;
|
||||
}
|
||||
await this._finishedPromise;
|
||||
return this._failureError?.message || null;
|
||||
}
|
||||
|
|
@ -85,40 +91,48 @@ export class Artifact extends SdkObject {
|
|||
}
|
||||
|
||||
async delete(): Promise<void> {
|
||||
if (this._unaccessibleErrorMessage)
|
||||
if (this._unaccessibleErrorMessage) {
|
||||
return;
|
||||
}
|
||||
const fileName = await this.localPathAfterFinished();
|
||||
if (this._deleted)
|
||||
if (this._deleted) {
|
||||
return;
|
||||
}
|
||||
this._deleted = true;
|
||||
if (fileName)
|
||||
if (fileName) {
|
||||
await fs.promises.unlink(fileName).catch(e => {});
|
||||
}
|
||||
}
|
||||
|
||||
async deleteOnContextClose(): Promise<void> {
|
||||
// Compared to "delete", this method does not wait for the artifact to finish.
|
||||
// We use it when closing the context to avoid stalling.
|
||||
if (this._deleted)
|
||||
if (this._deleted) {
|
||||
return;
|
||||
}
|
||||
this._deleted = true;
|
||||
if (!this._unaccessibleErrorMessage)
|
||||
if (!this._unaccessibleErrorMessage) {
|
||||
await fs.promises.unlink(this._localPath).catch(e => {});
|
||||
}
|
||||
await this.reportFinished(new TargetClosedError());
|
||||
}
|
||||
|
||||
async reportFinished(error?: Error) {
|
||||
if (this._finished)
|
||||
if (this._finished) {
|
||||
return;
|
||||
}
|
||||
this._finished = true;
|
||||
this._failureError = error;
|
||||
|
||||
if (error) {
|
||||
for (const callback of this._saveCallbacks)
|
||||
for (const callback of this._saveCallbacks) {
|
||||
await callback('', error);
|
||||
}
|
||||
} else {
|
||||
for (const callback of this._saveCallbacks)
|
||||
for (const callback of this._saveCallbacks) {
|
||||
await callback(this._localPath);
|
||||
}
|
||||
}
|
||||
this._saveCallbacks = [];
|
||||
|
||||
this._finishedPromise.resolve();
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ export class BidiBrowser extends Browser {
|
|||
|
||||
static async connect(parent: SdkObject, transport: ConnectionTransport, options: BrowserOptions): Promise<BidiBrowser> {
|
||||
const browser = new BidiBrowser(parent, transport, options);
|
||||
if ((options as any).__testHookOnConnectToBrowser)
|
||||
if ((options as any).__testHookOnConnectToBrowser) {
|
||||
await (options as any).__testHookOnConnectToBrowser();
|
||||
}
|
||||
|
||||
let proxy: bidi.Session.ManualProxyConfiguration | undefined;
|
||||
if (options.proxy) {
|
||||
|
|
@ -68,8 +69,9 @@ export class BidiBrowser extends Browser {
|
|||
default:
|
||||
throw new Error('Invalid proxy server protocol: ' + options.proxy.server);
|
||||
}
|
||||
if (options.proxy.bypass)
|
||||
if (options.proxy.bypass) {
|
||||
proxy.noProxy = options.proxy.bypass.split(',');
|
||||
}
|
||||
// TODO: support authentication.
|
||||
}
|
||||
|
||||
|
|
@ -148,8 +150,9 @@ export class BidiBrowser extends Browser {
|
|||
const parentFrameId = event.parent;
|
||||
for (const page of this._bidiPages.values()) {
|
||||
const parentFrame = page._page._frameManager.frame(parentFrameId);
|
||||
if (!parentFrame)
|
||||
if (!parentFrame) {
|
||||
continue;
|
||||
}
|
||||
page._session.addFrameBrowsingContext(event.context);
|
||||
page._page._frameManager.frameAttached(event.context, parentFrameId);
|
||||
return;
|
||||
|
|
@ -157,10 +160,12 @@ export class BidiBrowser extends Browser {
|
|||
return;
|
||||
}
|
||||
let context = this._contexts.get(event.userContext);
|
||||
if (!context)
|
||||
if (!context) {
|
||||
context = this._defaultContext as BidiBrowserContext;
|
||||
if (!context)
|
||||
}
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
const session = this._connection.createMainFrameBrowsingContextSession(event.context);
|
||||
const opener = event.originalOpener && this._bidiPages.get(event.originalOpener);
|
||||
const page = new BidiPage(context, session, opener || null);
|
||||
|
|
@ -173,27 +178,30 @@ export class BidiBrowser extends Browser {
|
|||
const parentFrameId = event.parent;
|
||||
for (const page of this._bidiPages.values()) {
|
||||
const parentFrame = page._page._frameManager.frame(parentFrameId);
|
||||
if (!parentFrame)
|
||||
if (!parentFrame) {
|
||||
continue;
|
||||
}
|
||||
page._page._frameManager.frameDetached(event.context);
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bidiPage = this._bidiPages.get(event.context);
|
||||
if (!bidiPage)
|
||||
if (!bidiPage) {
|
||||
return;
|
||||
}
|
||||
bidiPage.didClose();
|
||||
this._bidiPages.delete(event.context);
|
||||
}
|
||||
|
||||
private _onScriptRealmDestroyed(event: bidi.Script.RealmDestroyedParameters) {
|
||||
for (const page of this._bidiPages.values()) {
|
||||
if (page._onRealmDestroyed(event))
|
||||
if (page._onRealmDestroyed(event)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BidiBrowserContext extends BrowserContext {
|
||||
declare readonly _browser: BidiBrowser;
|
||||
|
|
@ -282,9 +290,10 @@ export class BidiBrowserContext extends BrowserContext {
|
|||
|
||||
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
|
||||
this._options.httpCredentials = httpCredentials;
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as BidiPage).updateHttpCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
async doAddInitScript(initScript: InitScript) {
|
||||
await Promise.all(this.pages().map(page => (page._delegate as BidiPage).addInitScript(initScript)));
|
||||
|
|
|
|||
|
|
@ -43,14 +43,17 @@ export class BidiChromium extends BrowserType {
|
|||
}
|
||||
|
||||
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||
if (!error.logs)
|
||||
if (!error.logs) {
|
||||
return error;
|
||||
if (error.logs.includes('Missing X server'))
|
||||
}
|
||||
if (error.logs.includes('Missing X server')) {
|
||||
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||
}
|
||||
// These error messages are taken from Chromium source code as of July, 2020:
|
||||
// https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
|
||||
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180'))
|
||||
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180')) {
|
||||
return error;
|
||||
}
|
||||
error.logs = [
|
||||
`Chromium sandboxing failed!`,
|
||||
`================================`,
|
||||
|
|
@ -69,8 +72,9 @@ export class BidiChromium extends BrowserType {
|
|||
|
||||
override attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||
const bidiTransport = (transport as any)[kBidiOverCdpWrapper];
|
||||
if (bidiTransport)
|
||||
if (bidiTransport) {
|
||||
transport = bidiTransport;
|
||||
}
|
||||
transport.send({ method: 'browser.close', params: {}, id: kBrowserCloseMessageId });
|
||||
}
|
||||
|
||||
|
|
@ -78,10 +82,11 @@ export class BidiChromium extends BrowserType {
|
|||
const chromeArguments = this._innerDefaultArgs(options);
|
||||
chromeArguments.push(`--user-data-dir=${userDataDir}`);
|
||||
chromeArguments.push('--remote-debugging-port=0');
|
||||
if (isPersistent)
|
||||
if (isPersistent) {
|
||||
chromeArguments.push('about:blank');
|
||||
else
|
||||
} else {
|
||||
chromeArguments.push('--no-startup-window');
|
||||
}
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
|
|
@ -93,24 +98,29 @@ export class BidiChromium extends BrowserType {
|
|||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||
const { args = [] } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
if (userDataDirArg) {
|
||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
|
||||
}
|
||||
if (args.find(arg => arg.startsWith('--remote-debugging-pipe'))) {
|
||||
throw new Error('Playwright manages remote debugging connection itself.');
|
||||
if (args.find(arg => !arg.startsWith('-')))
|
||||
}
|
||||
if (args.find(arg => !arg.startsWith('-'))) {
|
||||
throw new Error('Arguments can not specify page to be opened');
|
||||
}
|
||||
const chromeArguments = [...chromiumSwitches];
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
// See https://github.com/microsoft/playwright/issues/7362
|
||||
chromeArguments.push('--enable-use-zoom-for-dsf=false');
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
|
||||
if (options.headless)
|
||||
if (options.headless) {
|
||||
chromeArguments.push('--use-angle');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.devtools)
|
||||
if (options.devtools) {
|
||||
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||
}
|
||||
if (options.headless) {
|
||||
chromeArguments.push('--headless');
|
||||
|
||||
|
|
@ -120,8 +130,9 @@ export class BidiChromium extends BrowserType {
|
|||
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
|
||||
);
|
||||
}
|
||||
if (options.chromiumSandbox !== true)
|
||||
if (options.chromiumSandbox !== true) {
|
||||
chromeArguments.push('--no-sandbox');
|
||||
}
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
if (proxy) {
|
||||
const proxyURL = new URL(proxy.server);
|
||||
|
|
@ -134,15 +145,19 @@ export class BidiChromium extends BrowserType {
|
|||
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||
const proxyBypassRules = [];
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
|
||||
if (this.attribution.playwright.options.socksProxyPort)
|
||||
if (this.attribution.playwright.options.socksProxyPort) {
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxy.bypass)
|
||||
}
|
||||
if (proxy.bypass) {
|
||||
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
|
||||
}
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) {
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxyBypassRules.length > 0)
|
||||
}
|
||||
if (proxyBypassRules.length > 0) {
|
||||
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
|
||||
}
|
||||
}
|
||||
chromeArguments.push(...args);
|
||||
return chromeArguments;
|
||||
}
|
||||
|
|
@ -151,9 +166,10 @@ export class BidiChromium extends BrowserType {
|
|||
class ChromiumReadyState extends BrowserReadyState {
|
||||
override onBrowserOutput(message: string): void {
|
||||
const match = message.match(/DevTools listening on (.*)/);
|
||||
if (match)
|
||||
if (match) {
|
||||
this._wsEndpoint.resolve(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const kBidiOverCdpWrapper = Symbol('kBidiConnectionWrapper');
|
||||
|
|
|
|||
|
|
@ -69,10 +69,11 @@ export class BidiConnection {
|
|||
if (object.type === 'event') {
|
||||
// Route page events to the right session.
|
||||
let context;
|
||||
if ('context' in object.params)
|
||||
if ('context' in object.params) {
|
||||
context = object.params.context;
|
||||
else if (object.method === 'log.entryAdded' || object.method === 'script.message')
|
||||
context = object.params.source?.context;
|
||||
} else if (object.method === 'log.entryAdded' || object.method === 'script.message') {
|
||||
context = object.params.source.context;
|
||||
}
|
||||
if (context) {
|
||||
const session = this._browsingContextToSession.get(context);
|
||||
if (session) {
|
||||
|
|
@ -106,9 +107,10 @@ export class BidiConnection {
|
|||
}
|
||||
|
||||
close() {
|
||||
if (!this._closed)
|
||||
if (!this._closed) {
|
||||
this._transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
createMainFrameBrowsingContextSession(bowsingContextId: bidi.BrowsingContext.BrowsingContext): BidiSession {
|
||||
const result = new BidiSession(this, bowsingContextId, message => this.rawSend(message));
|
||||
|
|
@ -165,8 +167,9 @@ export class BidiSession extends EventEmitter {
|
|||
method: T,
|
||||
params?: bidiCommands.Commands[T]['params']
|
||||
): Promise<bidiCommands.Commands[T]['returnType']> {
|
||||
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs)
|
||||
if (this._crashed || this._disposed || this.connection._browserDisconnectedLogs) {
|
||||
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this.connection._browserDisconnectedLogs);
|
||||
}
|
||||
const id = this.connection.nextMessageId();
|
||||
const messageObj = { id, method, params };
|
||||
this._rawSend(messageObj);
|
||||
|
|
@ -190,8 +193,9 @@ export class BidiSession extends EventEmitter {
|
|||
dispose() {
|
||||
this._disposed = true;
|
||||
this.connection._browsingContextToSession.delete(this.sessionId);
|
||||
for (const context of this._browsingContexts)
|
||||
for (const context of this._browsingContexts) {
|
||||
this.connection._browsingContextToSession.delete(context);
|
||||
}
|
||||
this._browsingContexts.clear();
|
||||
for (const callback of this._callbacks.values()) {
|
||||
callback.error.type = this._crashed ? 'crashed' : 'closed';
|
||||
|
|
@ -207,8 +211,9 @@ export class BidiSession extends EventEmitter {
|
|||
|
||||
dispatchMessage(message: any) {
|
||||
const object = message as bidi.Message;
|
||||
if (object.id === kBrowserCloseMessageId)
|
||||
if (object.id === kBrowserCloseMessageId) {
|
||||
return;
|
||||
}
|
||||
if (object.id && this._callbacks.has(object.id)) {
|
||||
const callback = this._callbacks.get(object.id)!;
|
||||
this._callbacks.delete(object.id);
|
||||
|
|
|
|||
|
|
@ -51,10 +51,12 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
|||
awaitPromise: true,
|
||||
userActivation: true,
|
||||
});
|
||||
if (response.type === 'success')
|
||||
if (response.type === 'success') {
|
||||
return BidiDeserializer.deserialize(response.result);
|
||||
if (response.type === 'exception')
|
||||
}
|
||||
if (response.type === 'exception') {
|
||||
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||
}
|
||||
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||
}
|
||||
|
||||
|
|
@ -68,12 +70,14 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
|||
userActivation: true,
|
||||
});
|
||||
if (response.type === 'success') {
|
||||
if ('handle' in response.result)
|
||||
if ('handle' in response.result) {
|
||||
return response.result.handle!;
|
||||
}
|
||||
throw new js.JavaScriptErrorInEvaluate('Cannot get handle: ' + JSON.stringify(response.result));
|
||||
}
|
||||
if (response.type === 'exception')
|
||||
if (response.type === 'exception') {
|
||||
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||
}
|
||||
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||
}
|
||||
|
||||
|
|
@ -91,11 +95,13 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
|||
awaitPromise: true,
|
||||
userActivation: true,
|
||||
});
|
||||
if (response.type === 'exception')
|
||||
if (response.type === 'exception') {
|
||||
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||
}
|
||||
if (response.type === 'success') {
|
||||
if (returnByValue)
|
||||
if (returnByValue) {
|
||||
return parseEvaluationResultValue(BidiDeserializer.deserialize(response.result));
|
||||
}
|
||||
const objectId = 'handle' in response.result ? response.result.handle : undefined ;
|
||||
return utilityScript._context.createHandle({ objectId, ...response.result });
|
||||
}
|
||||
|
|
@ -128,32 +134,41 @@ export class BidiExecutionContext implements js.ExecutionContextDelegate {
|
|||
awaitPromise: true,
|
||||
userActivation: true,
|
||||
});
|
||||
if (response.type === 'exception')
|
||||
if (response.type === 'exception') {
|
||||
throw new js.JavaScriptErrorInEvaluate(response.exceptionDetails.text + '\nFull val: ' + JSON.stringify(response.exceptionDetails));
|
||||
if (response.type === 'success')
|
||||
}
|
||||
if (response.type === 'success') {
|
||||
return response.result;
|
||||
}
|
||||
throw new js.JavaScriptErrorInEvaluate('Unexpected response type: ' + JSON.stringify(response));
|
||||
}
|
||||
}
|
||||
|
||||
function renderPreview(remoteObject: bidi.Script.RemoteValue): string | undefined {
|
||||
if (remoteObject.type === 'undefined')
|
||||
if (remoteObject.type === 'undefined') {
|
||||
return 'undefined';
|
||||
if (remoteObject.type === 'null')
|
||||
}
|
||||
if (remoteObject.type === 'null') {
|
||||
return 'null';
|
||||
if ('value' in remoteObject)
|
||||
}
|
||||
if ('value' in remoteObject) {
|
||||
return String(remoteObject.value);
|
||||
}
|
||||
return `<${remoteObject.type}>`;
|
||||
}
|
||||
|
||||
function remoteObjectValue(remoteObject: bidi.Script.RemoteValue): any {
|
||||
if (remoteObject.type === 'undefined')
|
||||
return undefined;
|
||||
if (remoteObject.type === 'null')
|
||||
return null;
|
||||
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string')
|
||||
return js.parseUnserializableValue(remoteObject.value);
|
||||
if ('value' in remoteObject)
|
||||
return remoteObject.value;
|
||||
if (remoteObject.type === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
if (remoteObject.type === 'null') {
|
||||
return null;
|
||||
}
|
||||
if (remoteObject.type === 'number' && typeof remoteObject.value === 'string') {
|
||||
return js.parseUnserializableValue(remoteObject.value);
|
||||
}
|
||||
if ('value' in remoteObject) {
|
||||
return remoteObject.value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,19 +39,23 @@ export class BidiFirefox extends BrowserType {
|
|||
}
|
||||
|
||||
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||
if (!error.logs)
|
||||
if (!error.logs) {
|
||||
return error;
|
||||
}
|
||||
// https://github.com/microsoft/playwright/issues/6500
|
||||
if (error.logs.includes(`as root in a regular user's session is not supported.`))
|
||||
if (error.logs.includes(`as root in a regular user's session is not supported.`)) {
|
||||
error.logs = '\n' + wrapInASCIIBox(`Firefox is unable to launch if the $HOME folder isn't owned by the current user.\nWorkaround: Set the HOME=/root environment variable${process.env.GITHUB_ACTION ? ' in your GitHub Actions workflow file' : ''} when running Playwright.`, 1);
|
||||
if (error.logs.includes('no DISPLAY environment variable specified'))
|
||||
}
|
||||
if (error.logs.includes('no DISPLAY environment variable specified')) {
|
||||
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
override amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||
if (!path.isAbsolute(os.homedir()))
|
||||
if (!path.isAbsolute(os.homedir())) {
|
||||
throw new Error(`Cannot launch Firefox with relative home directory. Did you set ${os.platform() === 'win32' ? 'USERPROFILE' : 'HOME'} to a relative path?`);
|
||||
}
|
||||
|
||||
env = {
|
||||
...env,
|
||||
|
|
@ -83,13 +87,15 @@ export class BidiFirefox extends BrowserType {
|
|||
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const { args = [], headless } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('-profile') || arg.startsWith('--profile'));
|
||||
if (userDataDirArg)
|
||||
if (userDataDirArg) {
|
||||
throw this._createUserDataDirArgMisuseError('--profile');
|
||||
}
|
||||
const firefoxArguments = ['--remote-debugging-port=0'];
|
||||
if (headless)
|
||||
if (headless) {
|
||||
firefoxArguments.push('--headless');
|
||||
else
|
||||
} else {
|
||||
firefoxArguments.push('--foreground');
|
||||
}
|
||||
firefoxArguments.push(`--profile`, userDataDir);
|
||||
firefoxArguments.push(...args);
|
||||
return firefoxArguments;
|
||||
|
|
@ -105,7 +111,8 @@ class FirefoxReadyState extends BrowserReadyState {
|
|||
override onBrowserOutput(message: string): void {
|
||||
// Bidi WebSocket in Firefox.
|
||||
const match = message.match(/WebDriver BiDi listening on (ws:\/\/.*)$/);
|
||||
if (match)
|
||||
if (match) {
|
||||
this._wsEndpoint.resolve(match[1] + '/session');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,14 +55,17 @@ export class BidiNetworkManager {
|
|||
}
|
||||
|
||||
private _onBeforeRequestSent(param: bidi.Network.BeforeRequestSentParameters) {
|
||||
if (param.request.url.startsWith('data:'))
|
||||
if (param.request.url.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
const redirectedFrom = param.redirectCount ? (this._requests.get(param.request.request) || null) : null;
|
||||
const frame = redirectedFrom ? redirectedFrom.request.frame() : (param.context ? this._page._frameManager.frame(param.context) : null);
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
return;
|
||||
if (redirectedFrom)
|
||||
}
|
||||
if (redirectedFrom) {
|
||||
this._requests.delete(redirectedFrom._id);
|
||||
}
|
||||
let route;
|
||||
if (param.intercepts) {
|
||||
// We do not support intercepting redirects.
|
||||
|
|
@ -82,16 +85,18 @@ export class BidiNetworkManager {
|
|||
|
||||
private _onResponseStarted(params: bidi.Network.ResponseStartedParameters) {
|
||||
const request = this._requests.get(params.request.request);
|
||||
if (!request)
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
const getResponseBody = async () => {
|
||||
throw new Error(`Response body is not available for requests in Bidi`);
|
||||
};
|
||||
const timings = params.request.timings;
|
||||
const startTime = timings.requestTime;
|
||||
function relativeToStart(time: number): number {
|
||||
if (!time)
|
||||
if (!time) {
|
||||
return -1;
|
||||
}
|
||||
return (time - startTime) / 1000;
|
||||
}
|
||||
const timing: network.ResourceTiming = {
|
||||
|
|
@ -111,14 +116,16 @@ export class BidiNetworkManager {
|
|||
response.setRawResponseHeaders(null);
|
||||
response.setResponseHeadersSize(params.response.headersSize);
|
||||
this._page._frameManager.requestReceivedResponse(response);
|
||||
if (params.navigation)
|
||||
if (params.navigation) {
|
||||
this._onNavigationResponseStarted(params);
|
||||
}
|
||||
}
|
||||
|
||||
private _onResponseCompleted(params: bidi.Network.ResponseCompletedParameters) {
|
||||
const request = this._requests.get(params.request.request);
|
||||
if (!request)
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
const response = request.request._existingResponse()!;
|
||||
// TODO: body size is the encoded size
|
||||
response.setTransferSize(params.response.bodySize);
|
||||
|
|
@ -140,8 +147,9 @@ export class BidiNetworkManager {
|
|||
|
||||
private _onFetchError(params: bidi.Network.FetchErrorParameters) {
|
||||
const request = this._requests.get(params.request.request);
|
||||
if (!request)
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
this._requests.delete(request._id);
|
||||
const response = request.request._existingResponse();
|
||||
if (response) {
|
||||
|
|
@ -187,11 +195,13 @@ export class BidiNetworkManager {
|
|||
|
||||
async _updateProtocolRequestInterception(initial?: boolean) {
|
||||
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
|
||||
if (enabled === this._protocolRequestInterceptionEnabled)
|
||||
if (enabled === this._protocolRequestInterceptionEnabled) {
|
||||
return;
|
||||
}
|
||||
this._protocolRequestInterceptionEnabled = enabled;
|
||||
if (initial && !enabled)
|
||||
if (initial && !enabled) {
|
||||
return;
|
||||
}
|
||||
const cachePromise = this._session.send('network.setCacheBehavior', { cacheBehavior: enabled ? 'bypass' : 'default' });
|
||||
let interceptPromise = Promise.resolve<any>(undefined);
|
||||
if (enabled) {
|
||||
|
|
@ -221,8 +231,9 @@ class BidiRequest {
|
|||
|
||||
constructor(frame: frames.Frame, redirectedFrom: BidiRequest | null, payload: bidi.Network.BeforeRequestSentParameters, route: BidiRouteImpl | undefined) {
|
||||
this._id = payload.request.request;
|
||||
if (redirectedFrom)
|
||||
if (redirectedFrom) {
|
||||
redirectedFrom._redirectedTo = this;
|
||||
}
|
||||
// TODO: missing in the spec?
|
||||
const postDataBuffer = null;
|
||||
this.request = new network.Request(frame._page._browserContext, frame, null, redirectedFrom ? redirectedFrom.request : null, payload.navigation ?? undefined,
|
||||
|
|
@ -236,8 +247,9 @@ class BidiRequest {
|
|||
|
||||
_finalRequest(): BidiRequest {
|
||||
let request: BidiRequest = this;
|
||||
while (request._redirectedTo)
|
||||
while (request._redirectedTo) {
|
||||
request = request._redirectedTo;
|
||||
}
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
|
@ -262,8 +274,9 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
let headers = overrides.headers || this._request.headers();
|
||||
if (overrides.postData && headers) {
|
||||
headers = headers.map(header => {
|
||||
if (header.name.toLowerCase() === 'content-length')
|
||||
if (header.name.toLowerCase() === 'content-length') {
|
||||
return { name: header.name, value: overrides.postData!.byteLength.toString() };
|
||||
}
|
||||
return header;
|
||||
});
|
||||
}
|
||||
|
|
@ -297,8 +310,9 @@ class BidiRouteImpl implements network.RouteDelegate {
|
|||
|
||||
function fromBidiHeaders(bidiHeaders: bidi.Network.Header[]): types.HeadersArray {
|
||||
const result: types.HeadersArray = [];
|
||||
for (const { name, value } of bidiHeaders)
|
||||
for (const { name, value } of bidiHeaders) {
|
||||
result.push({ name, value: bidiBytesValueToString(value) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -328,20 +342,25 @@ function toBidiHeaders(headers: types.HeadersArray): bidi.Network.Header[] {
|
|||
}
|
||||
|
||||
export function bidiBytesValueToString(value: bidi.Network.BytesValue): string {
|
||||
if (value.type === 'string')
|
||||
if (value.type === 'string') {
|
||||
return value.value;
|
||||
if (value.type === 'base64')
|
||||
}
|
||||
if (value.type === 'base64') {
|
||||
return Buffer.from(value.type, 'base64').toString('binary');
|
||||
}
|
||||
return 'unknown value type: ' + (value as any).type;
|
||||
|
||||
}
|
||||
|
||||
function toBidiSameSite(sameSite?: 'Strict' | 'Lax' | 'None'): bidi.Network.SameSite | undefined {
|
||||
if (!sameSite)
|
||||
if (!sameSite) {
|
||||
return undefined;
|
||||
if (sameSite === 'Strict')
|
||||
}
|
||||
if (sameSite === 'Strict') {
|
||||
return bidi.Network.SameSite.Strict;
|
||||
if (sameSite === 'Lax')
|
||||
}
|
||||
if (sameSite === 'Lax') {
|
||||
return bidi.Network.SameSite.Lax;
|
||||
}
|
||||
return bidi.Network.SameSite.None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,20 +115,24 @@ export class BidiPage implements PageDelegate {
|
|||
for (const [contextId, context] of this._realmToContext) {
|
||||
if (context.frame === frame) {
|
||||
this._realmToContext.delete(contextId);
|
||||
if (notifyFrame)
|
||||
if (notifyFrame) {
|
||||
frame._contextDestroyed(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onRealmCreated(realmInfo: bidi.Script.RealmInfo) {
|
||||
if (this._realmToContext.has(realmInfo.realm))
|
||||
if (this._realmToContext.has(realmInfo.realm)) {
|
||||
return;
|
||||
if (realmInfo.type !== 'window')
|
||||
}
|
||||
if (realmInfo.type !== 'window') {
|
||||
return;
|
||||
}
|
||||
const frame = this._page._frameManager.frame(realmInfo.context);
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
const delegate = new BidiExecutionContext(this._session, realmInfo);
|
||||
let worldName: types.World;
|
||||
if (!realmInfo.sandbox) {
|
||||
|
|
@ -164,8 +168,9 @@ export class BidiPage implements PageDelegate {
|
|||
|
||||
_onRealmDestroyed(params: bidi.Script.RealmDestroyedParameters): boolean {
|
||||
const context = this._realmToContext.get(params.realm);
|
||||
if (!context)
|
||||
if (!context) {
|
||||
return false;
|
||||
}
|
||||
this._realmToContext.delete(params.realm);
|
||||
context.frame._contextDestroyed(context);
|
||||
return true;
|
||||
|
|
@ -185,10 +190,11 @@ export class BidiPage implements PageDelegate {
|
|||
// Navigation to file urls doesn't emit network events, so we fire 'commit' event right when navigation is started.
|
||||
// Doing it in domcontentload would be too late as we'd clear frame tree.
|
||||
const frame = this._page._frameManager.frame(frameId)!;
|
||||
if (frame)
|
||||
if (frame) {
|
||||
this._page._frameManager.frameCommittedNewDocumentNavigation(frameId, params.url, '', params.navigation!, /* initial */ false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: there is no separate event for committed navigation, so we approximate it with responseStarted.
|
||||
private _onNavigationResponseStarted(params: bidi.Network.ResponseStartedParameters) {
|
||||
|
|
@ -233,12 +239,14 @@ export class BidiPage implements PageDelegate {
|
|||
}
|
||||
|
||||
private _onLogEntryAdded(params: bidi.Log.Entry) {
|
||||
if (params.type !== 'console')
|
||||
if (params.type !== 'console') {
|
||||
return;
|
||||
}
|
||||
const entry: bidi.Log.ConsoleLogEntry = params as bidi.Log.ConsoleLogEntry;
|
||||
const context = this._realmToContext.get(params.source.realm);
|
||||
if (!context)
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
const callFrame = params.stackTrace?.callFrames[0];
|
||||
const location = callFrame ?? { url: '', lineNumber: 1, columnNumber: 1 };
|
||||
this._page._addConsoleMessage(entry.method, entry.args.map(arg => context.createHandle({ objectId: (arg as any).handle, ...arg })), location, params.text || undefined);
|
||||
|
|
@ -274,8 +282,9 @@ export class BidiPage implements PageDelegate {
|
|||
private async _updateViewport(): Promise<void> {
|
||||
const options = this._browserContext._options;
|
||||
const deviceSize = this._page.emulatedSize();
|
||||
if (deviceSize === null)
|
||||
if (deviceSize === null) {
|
||||
return;
|
||||
}
|
||||
const viewportSize = deviceSize.viewport;
|
||||
await this._session.send('browsingContext.setViewport', {
|
||||
context: this._session.sessionId,
|
||||
|
|
@ -353,16 +362,20 @@ export class BidiPage implements PageDelegate {
|
|||
}
|
||||
|
||||
private async _onScriptMessage(event: bidi.Script.MessageParameters) {
|
||||
if (event.channel !== kPlaywrightBindingChannel)
|
||||
if (event.channel !== kPlaywrightBindingChannel) {
|
||||
return;
|
||||
}
|
||||
const pageOrError = await this._page.waitForInitializedOrError();
|
||||
if (pageOrError instanceof Error)
|
||||
if (pageOrError instanceof Error) {
|
||||
return;
|
||||
}
|
||||
const context = this._realmToContext.get(event.source.realm);
|
||||
if (!context)
|
||||
if (!context) {
|
||||
return;
|
||||
if (event.data.type !== 'string')
|
||||
}
|
||||
if (event.data.type !== 'string') {
|
||||
return;
|
||||
}
|
||||
await this._page._onBindingCalled(event.data.value, context);
|
||||
}
|
||||
|
||||
|
|
@ -373,9 +386,10 @@ export class BidiPage implements PageDelegate {
|
|||
// TODO: push to iframes?
|
||||
contexts: [this._session.sessionId],
|
||||
});
|
||||
if (!initScript.internal)
|
||||
if (!initScript.internal) {
|
||||
this._initScriptIds.push(script);
|
||||
}
|
||||
}
|
||||
|
||||
async removeNonInternalInitScripts() {
|
||||
const promises = this._initScriptIds.map(script => this._session.send('script.removePreloadScript', { script }));
|
||||
|
|
@ -431,16 +445,19 @@ export class BidiPage implements PageDelegate {
|
|||
|
||||
async getBoundingBox(handle: dom.ElementHandle): Promise<types.Rect | null> {
|
||||
const box = await handle.evaluate(element => {
|
||||
if (!(element instanceof Element))
|
||||
if (!(element instanceof Element)) {
|
||||
return null;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
||||
});
|
||||
if (!box)
|
||||
if (!box) {
|
||||
return null;
|
||||
}
|
||||
const position = await this._framePosition(handle._frame);
|
||||
if (!position)
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
box.x += position.x;
|
||||
box.y += position.y;
|
||||
return box;
|
||||
|
|
@ -448,15 +465,18 @@ export class BidiPage implements PageDelegate {
|
|||
|
||||
// TODO: move to Frame.
|
||||
private async _framePosition(frame: frames.Frame): Promise<types.Point | null> {
|
||||
if (frame === this._page.mainFrame())
|
||||
if (frame === this._page.mainFrame()) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
const element = await frame.frameElement();
|
||||
const box = await element.boundingBox();
|
||||
if (!box)
|
||||
if (!box) {
|
||||
return null;
|
||||
}
|
||||
const style = await element.evaluateInUtility(([injected, iframe]) => injected.describeIFrameStyle(iframe as Element), {}).catch(e => 'error:notconnected' as const);
|
||||
if (style === 'error:notconnected' || style === 'transformed')
|
||||
if (style === 'error:notconnected' || style === 'transformed') {
|
||||
return null;
|
||||
}
|
||||
// Content box is offset by border and padding widths.
|
||||
box.x += style.left;
|
||||
box.y += style.top;
|
||||
|
|
@ -471,10 +491,12 @@ export class BidiPage implements PageDelegate {
|
|||
behavior: 'instant',
|
||||
});
|
||||
}, null).then(() => 'done' as const).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document')) {
|
||||
return 'error:notconnected';
|
||||
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
|
||||
}
|
||||
if (e instanceof Error && e.message.includes('Node does not have a layout object')) {
|
||||
return 'error:notvisible';
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
|
@ -488,11 +510,13 @@ export class BidiPage implements PageDelegate {
|
|||
|
||||
async getContentQuads(handle: dom.ElementHandle<Element>): Promise<types.Quad[] | null | 'error:notconnected'> {
|
||||
const quads = await handle.evaluateInUtility(([injected, node]) => {
|
||||
if (!node.isConnected)
|
||||
if (!node.isConnected) {
|
||||
return 'error:notconnected';
|
||||
}
|
||||
const rects = node.getClientRects();
|
||||
if (!rects)
|
||||
if (!rects) {
|
||||
return null;
|
||||
}
|
||||
return [...rects].map(rect => [
|
||||
{ x: rect.left, y: rect.top },
|
||||
{ x: rect.right, y: rect.top },
|
||||
|
|
@ -500,12 +524,14 @@ export class BidiPage implements PageDelegate {
|
|||
{ x: rect.left, y: rect.bottom },
|
||||
]);
|
||||
}, null);
|
||||
if (!quads || quads === 'error:notconnected')
|
||||
if (!quads || quads === 'error:notconnected') {
|
||||
return quads;
|
||||
}
|
||||
// TODO: consider transforming quads to support clicks in iframes.
|
||||
const position = await this._framePosition(handle._frame);
|
||||
if (!position)
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
quads.forEach(quad => quad.forEach(point => {
|
||||
point.x += position.x;
|
||||
point.y += position.y;
|
||||
|
|
@ -525,13 +551,15 @@ export class BidiPage implements PageDelegate {
|
|||
const fromContext = toBidiExecutionContext(handle._context);
|
||||
const shared = await fromContext.rawCallFunction('x => x', { handle: handle._objectId });
|
||||
// TODO: store sharedId in the handle.
|
||||
if (!('sharedId' in shared))
|
||||
if (!('sharedId' in shared)) {
|
||||
throw new Error('Element is not a node');
|
||||
}
|
||||
const sharedId = shared.sharedId!;
|
||||
const executionContext = toBidiExecutionContext(to);
|
||||
const result = await executionContext.rawCallFunction('x => x', { sharedId });
|
||||
if ('handle' in result)
|
||||
if ('handle' in result) {
|
||||
return to.createHandle({ objectId: result.handle!, ...result }) as dom.ElementHandle<T>;
|
||||
}
|
||||
throw new Error('Failed to adopt element handle.');
|
||||
}
|
||||
|
||||
|
|
@ -551,10 +579,13 @@ export class BidiPage implements PageDelegate {
|
|||
|
||||
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
|
||||
const parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
if (!parent) {
|
||||
throw new Error('Frame has been detached.');
|
||||
}
|
||||
const parentContext = await parent._mainContext();
|
||||
const list = await parentContext.evaluateHandle(() => { return [...document.querySelectorAll('iframe,frame')]; });
|
||||
const list = await parentContext.evaluateHandle(() => {
|
||||
return [...document.querySelectorAll('iframe,frame')];
|
||||
});
|
||||
const length = await list.evaluate(list => list.length);
|
||||
let foundElement = null;
|
||||
for (let i = 0; i < length; i++) {
|
||||
|
|
@ -568,8 +599,9 @@ export class BidiPage implements PageDelegate {
|
|||
}
|
||||
}
|
||||
list.dispose();
|
||||
if (!foundElement)
|
||||
if (!foundElement) {
|
||||
throw new Error('Frame has been detached.');
|
||||
}
|
||||
return foundElement;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ const unitToPixels: { [key: string]: number } = {
|
|||
};
|
||||
|
||||
function convertPrintParameterToInches(text: string | undefined): number | undefined {
|
||||
if (text === undefined)
|
||||
if (text === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
let unit = text.substring(text.length - 2).toLowerCase();
|
||||
let valueText = '';
|
||||
if (unitToPixels.hasOwnProperty(unit)) {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import type * as Bidi from './bidiProtocol';
|
|||
*/
|
||||
export class BidiDeserializer {
|
||||
static deserialize(result: Bidi.Script.RemoteValue): any {
|
||||
if (!result)
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (result.type) {
|
||||
case 'array':
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/* eslint-disable curly */
|
||||
|
||||
export const getBidiKeyValue = (key: string) => {
|
||||
switch (key) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import type * as Bidi from './bidiProtocol';
|
||||
|
||||
/* eslint-disable curly, indent */
|
||||
/* eslint-disable indent */
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/* eslint-disable curly, indent */
|
||||
/* eslint-disable indent */
|
||||
|
||||
interface ProfileOptions {
|
||||
preferences: Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -98,16 +98,18 @@ export abstract class Browser extends SdkObject {
|
|||
throw error;
|
||||
}
|
||||
context._clientCertificatesProxy = clientCertificatesProxy;
|
||||
if (options.storageState)
|
||||
if (options.storageState) {
|
||||
await context.setStorageState(metadata, options.storageState);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
async newContextForReuse(params: channels.BrowserNewContextForReuseParams, metadata: CallMetadata): Promise<{ context: BrowserContext, needsReset: boolean }> {
|
||||
const hash = BrowserContext.reusableContextHash(params);
|
||||
if (!this._contextForReuse || hash !== this._contextForReuse.hash || !this._contextForReuse.context.canResetForReuse()) {
|
||||
if (this._contextForReuse)
|
||||
if (this._contextForReuse) {
|
||||
await this._contextForReuse.context.close({ reason: 'Context reused' });
|
||||
}
|
||||
this._contextForReuse = { context: await this.newContext(metadata, params), hash };
|
||||
return { context: this._contextForReuse.context, needsReset: false };
|
||||
}
|
||||
|
|
@ -116,7 +118,7 @@ export abstract class Browser extends SdkObject {
|
|||
}
|
||||
|
||||
async stopPendingOperations(reason: string) {
|
||||
await this._contextForReuse?.context?.stopPendingOperations(reason);
|
||||
await this._contextForReuse?.context.stopPendingOperations(reason);
|
||||
}
|
||||
|
||||
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
|
||||
|
|
@ -126,15 +128,17 @@ export abstract class Browser extends SdkObject {
|
|||
|
||||
_downloadFilenameSuggested(uuid: string, suggestedFilename: string) {
|
||||
const download = this._downloads.get(uuid);
|
||||
if (!download)
|
||||
if (!download) {
|
||||
return;
|
||||
}
|
||||
download._filenameSuggested(suggestedFilename);
|
||||
}
|
||||
|
||||
_downloadFinished(uuid: string, error?: string) {
|
||||
const download = this._downloads.get(uuid);
|
||||
if (!download)
|
||||
if (!download) {
|
||||
return;
|
||||
}
|
||||
download.artifact.reportFinished(error ? new Error(error) : undefined);
|
||||
this._downloads.delete(uuid);
|
||||
}
|
||||
|
|
@ -158,24 +162,28 @@ export abstract class Browser extends SdkObject {
|
|||
}
|
||||
|
||||
_didClose() {
|
||||
for (const context of this.contexts())
|
||||
for (const context of this.contexts()) {
|
||||
context._browserClosed();
|
||||
if (this._defaultContext)
|
||||
}
|
||||
if (this._defaultContext) {
|
||||
this._defaultContext._browserClosed();
|
||||
}
|
||||
this.emit(Browser.Events.Disconnected);
|
||||
this.instrumentation.onBrowserClose(this);
|
||||
}
|
||||
|
||||
async close(options: { reason?: string }) {
|
||||
if (!this._startedClosing) {
|
||||
if (options.reason)
|
||||
if (options.reason) {
|
||||
this._closeReason = options.reason;
|
||||
}
|
||||
this._startedClosing = true;
|
||||
await this.options.browserProcess.close();
|
||||
}
|
||||
if (this.isConnected())
|
||||
if (this.isConnected()) {
|
||||
await new Promise(x => this.once(Browser.Events.Disconnected, x));
|
||||
}
|
||||
}
|
||||
|
||||
async killForTests() {
|
||||
await this.options.browserProcess.kill();
|
||||
|
|
|
|||
|
|
@ -103,8 +103,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
this.fetchRequest = new BrowserContextAPIRequestContext(this);
|
||||
|
||||
if (this._options.recordHar)
|
||||
if (this._options.recordHar) {
|
||||
this._harRecorders.set('', new HarRecorder(this, null, this._options.recordHar));
|
||||
}
|
||||
|
||||
this.tracing = new Tracing(this, browser.options.tracesDir);
|
||||
this.clock = new Clock(this);
|
||||
|
|
@ -123,53 +124,63 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
|
||||
async _initialize() {
|
||||
if (this.attribution.playwright.options.isInternalPlaywright)
|
||||
if (this.attribution.playwright.options.isInternalPlaywright) {
|
||||
return;
|
||||
}
|
||||
// Debugger will pause execution upon page.pause in headed mode.
|
||||
this._debugger = new Debugger(this);
|
||||
|
||||
// When PWDEBUG=1, show inspector for each context.
|
||||
if (debugMode() === 'inspector')
|
||||
if (debugMode() === 'inspector') {
|
||||
await Recorder.show('actions', this, RecorderApp.factory(this), { pauseOnNextStatement: true });
|
||||
}
|
||||
|
||||
// When paused, show inspector.
|
||||
if (this._debugger.isPaused())
|
||||
if (this._debugger.isPaused()) {
|
||||
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||
}
|
||||
|
||||
this._debugger.on(Debugger.Events.PausedStateChanged, () => {
|
||||
if (this._debugger.isPaused())
|
||||
if (this._debugger.isPaused()) {
|
||||
Recorder.showInspectorNoReply(this, RecorderApp.factory(this));
|
||||
}
|
||||
});
|
||||
|
||||
if (debugMode() === 'console')
|
||||
if (debugMode() === 'console') {
|
||||
await this.extendInjectedScript(consoleApiSource.source);
|
||||
if (this._options.serviceWorkers === 'block')
|
||||
}
|
||||
if (this._options.serviceWorkers === 'block') {
|
||||
await this.addInitScript(`\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`);
|
||||
}
|
||||
|
||||
if (this._options.permissions)
|
||||
if (this._options.permissions) {
|
||||
await this.grantPermissions(this._options.permissions);
|
||||
}
|
||||
}
|
||||
|
||||
debugger(): Debugger {
|
||||
return this._debugger;
|
||||
}
|
||||
|
||||
async _ensureVideosPath() {
|
||||
if (this._options.recordVideo)
|
||||
if (this._options.recordVideo) {
|
||||
await mkdirIfNeeded(path.join(this._options.recordVideo.dir, 'dummy'));
|
||||
}
|
||||
}
|
||||
|
||||
canResetForReuse(): boolean {
|
||||
if (this._closedStatus !== 'open')
|
||||
if (this._closedStatus !== 'open') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async stopPendingOperations(reason: string) {
|
||||
// When using context reuse, stop pending operations to gracefully terminate all the actions
|
||||
// with a user-friendly error message containing operation log.
|
||||
for (const controller of this._activeProgressControllers)
|
||||
for (const controller of this._activeProgressControllers) {
|
||||
controller.abort(new Error(reason));
|
||||
}
|
||||
// Let rejections in microtask generate events before returning.
|
||||
await new Promise(f => setTimeout(f, 0));
|
||||
}
|
||||
|
|
@ -179,12 +190,14 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
for (const k of Object.keys(paramsCopy)) {
|
||||
const key = k as keyof channels.BrowserNewContextForReuseParams;
|
||||
if (paramsCopy[key] === defaultNewContextParamValues[key])
|
||||
if (paramsCopy[key] === defaultNewContextParamValues[key]) {
|
||||
delete paramsCopy[key];
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of paramsThatAllowContextReuse)
|
||||
for (const key of paramsThatAllowContextReuse) {
|
||||
delete paramsCopy[key];
|
||||
}
|
||||
return JSON.stringify(paramsCopy);
|
||||
}
|
||||
|
||||
|
|
@ -194,17 +207,19 @@ export abstract class BrowserContext extends SdkObject {
|
|||
this.tracing.resetForReuse();
|
||||
|
||||
if (params) {
|
||||
for (const key of paramsThatAllowContextReuse)
|
||||
for (const key of paramsThatAllowContextReuse) {
|
||||
(this._options as any)[key] = params[key];
|
||||
}
|
||||
}
|
||||
|
||||
await this._cancelAllRoutesInFlight();
|
||||
|
||||
// Close extra pages early.
|
||||
let page: Page | undefined = this.pages()[0];
|
||||
const [, ...otherPages] = this.pages();
|
||||
for (const p of otherPages)
|
||||
for (const p of otherPages) {
|
||||
await p.close(metadata);
|
||||
}
|
||||
if (page && page.hasCrashed()) {
|
||||
await page.close(metadata);
|
||||
page = undefined;
|
||||
|
|
@ -222,10 +237,11 @@ export abstract class BrowserContext extends SdkObject {
|
|||
await this._removeInitScripts();
|
||||
this.clock.markAsUninstalled();
|
||||
// TODO: following can be optimized to not perform noops.
|
||||
if (this._options.permissions)
|
||||
if (this._options.permissions) {
|
||||
await this.grantPermissions(this._options.permissions);
|
||||
else
|
||||
} else {
|
||||
await this.clearPermissions();
|
||||
}
|
||||
await this.setExtraHTTPHeaders(this._options.extraHTTPHeaders || []);
|
||||
await this.setGeolocation(this._options.geolocation);
|
||||
await this.setOffline(!!this._options.offline);
|
||||
|
|
@ -237,8 +253,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
|
||||
_browserClosed() {
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
page._didClose();
|
||||
}
|
||||
this._didCloseInternal();
|
||||
}
|
||||
|
||||
|
|
@ -250,8 +267,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
this._clientCertificatesProxy?.close().catch(() => {});
|
||||
this.tracing.abort();
|
||||
if (this._isPersistentContext)
|
||||
if (this._isPersistentContext) {
|
||||
this.onClosePersistent();
|
||||
}
|
||||
this._closePromiseFulfill!(new Error('Context closed'));
|
||||
this.emit(BrowserContext.Events.Close);
|
||||
}
|
||||
|
|
@ -282,8 +300,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
protected abstract onClosePersistent(): void;
|
||||
|
||||
async cookies(urls: string | string[] | undefined = []): Promise<channels.NetworkCookie[]> {
|
||||
if (urls && !Array.isArray(urls))
|
||||
if (urls && !Array.isArray(urls)) {
|
||||
urls = [urls];
|
||||
}
|
||||
return await this.doGetCookies(urls as string[]);
|
||||
}
|
||||
|
||||
|
|
@ -292,8 +311,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
await this.doClearCookies();
|
||||
|
||||
const matches = (cookie: channels.NetworkCookie, prop: 'name' | 'domain' | 'path', value: string | RegExp | undefined) => {
|
||||
if (!value)
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (value instanceof RegExp) {
|
||||
value.lastIndex = 0;
|
||||
return value.test(cookie[prop]);
|
||||
|
|
@ -315,12 +335,14 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
|
||||
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<void> {
|
||||
if (this._pageBindings.has(name))
|
||||
if (this._pageBindings.has(name)) {
|
||||
throw new Error(`Function "${name}" has been already registered`);
|
||||
}
|
||||
for (const page of this.pages()) {
|
||||
if (page.getBinding(name))
|
||||
if (page.getBinding(name)) {
|
||||
throw new Error(`Function "${name}" has been already registered in one of the pages`);
|
||||
}
|
||||
}
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this.doAddInitScript(binding.initScript);
|
||||
|
|
@ -330,10 +352,11 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
async _removeExposedBindings() {
|
||||
for (const [key, binding] of this._pageBindings) {
|
||||
if (!binding.internal)
|
||||
if (!binding.internal) {
|
||||
this._pageBindings.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async grantPermissions(permissions: string[], origin?: string) {
|
||||
let resolvedOrigin = '*';
|
||||
|
|
@ -369,19 +392,22 @@ export abstract class BrowserContext extends SdkObject {
|
|||
await Promise.race([waitForEvent.promise, this._closePromise]);
|
||||
}
|
||||
const page = this.possiblyUninitializedPages()[0];
|
||||
if (!page)
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
const pageOrError = await page.waitForInitializedOrError();
|
||||
if (pageOrError instanceof Error)
|
||||
if (pageOrError instanceof Error) {
|
||||
throw pageOrError;
|
||||
}
|
||||
await page.mainFrame()._waitForLoadState(progress, 'load');
|
||||
return page;
|
||||
}
|
||||
|
||||
async _loadDefaultContext(progress: Progress) {
|
||||
const defaultPage = await this._loadDefaultContextAsIs(progress);
|
||||
if (!defaultPage)
|
||||
if (!defaultPage) {
|
||||
return;
|
||||
}
|
||||
const browserName = this._browser.options.name;
|
||||
if ((this._options.isMobile && browserName === 'chromium') || (this._options.locale && browserName === 'webkit')) {
|
||||
// Workaround for:
|
||||
|
|
@ -407,12 +433,14 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
protected _authenticateProxyViaCredentials() {
|
||||
const proxy = this._options.proxy || this._browser.options.proxy;
|
||||
if (!proxy)
|
||||
if (!proxy) {
|
||||
return;
|
||||
}
|
||||
const { username, password } = proxy;
|
||||
if (username)
|
||||
if (username) {
|
||||
this._options.httpCredentials = { username, password: password || '' };
|
||||
}
|
||||
}
|
||||
|
||||
async addInitScript(source: string) {
|
||||
const initScript = new InitScript(source);
|
||||
|
|
@ -448,22 +476,25 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
async close(options: { reason?: string }) {
|
||||
if (this._closedStatus === 'open') {
|
||||
if (options.reason)
|
||||
if (options.reason) {
|
||||
this._closeReason = options.reason;
|
||||
}
|
||||
this.emit(BrowserContext.Events.BeforeClose);
|
||||
this._closedStatus = 'closing';
|
||||
|
||||
for (const harRecorder of this._harRecorders.values())
|
||||
for (const harRecorder of this._harRecorders.values()) {
|
||||
await harRecorder.flush();
|
||||
}
|
||||
await this.tracing.flush();
|
||||
|
||||
// Cleanup.
|
||||
const promises: Promise<void>[] = [];
|
||||
for (const { context, artifact } of this._browser._idToVideo.values()) {
|
||||
// Wait for the videos to finish.
|
||||
if (context === this)
|
||||
if (context === this) {
|
||||
promises.push(artifact.finishedPromise());
|
||||
}
|
||||
}
|
||||
|
||||
if (this._customCloseHandler) {
|
||||
await this._customCloseHandler();
|
||||
|
|
@ -479,20 +510,23 @@ export abstract class BrowserContext extends SdkObject {
|
|||
await Promise.all(promises);
|
||||
|
||||
// Custom handler should trigger didCloseInternal itself.
|
||||
if (!this._customCloseHandler)
|
||||
if (!this._customCloseHandler) {
|
||||
this._didCloseInternal();
|
||||
}
|
||||
}
|
||||
await this._closePromise;
|
||||
}
|
||||
|
||||
async newPage(metadata: CallMetadata): Promise<Page> {
|
||||
const page = await this.doCreateNewPage();
|
||||
if (metadata.isServerSide)
|
||||
if (metadata.isServerSide) {
|
||||
page.markAsServerSideOnly();
|
||||
}
|
||||
const pageOrError = await page.waitForInitializedOrError();
|
||||
if (pageOrError instanceof Page) {
|
||||
if (pageOrError.isClosed())
|
||||
if (pageOrError.isClosed()) {
|
||||
throw new Error('Page has been closed.');
|
||||
}
|
||||
return pageOrError;
|
||||
}
|
||||
throw pageOrError;
|
||||
|
|
@ -512,14 +546,16 @@ export abstract class BrowserContext extends SdkObject {
|
|||
// First try collecting storage stage from existing pages.
|
||||
for (const page of this.pages()) {
|
||||
const origin = page.mainFrame().origin();
|
||||
if (!origin || !originsToSave.has(origin))
|
||||
if (!origin || !originsToSave.has(origin)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const storage = await page.mainFrame().nonStallingEvaluateInExistingContext(`({
|
||||
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
|
||||
})`, 'utility');
|
||||
if (storage.localStorage.length)
|
||||
if (storage.localStorage.length) {
|
||||
result.origins.push({ origin, localStorage: storage.localStorage } as channels.OriginStorage);
|
||||
}
|
||||
originsToSave.delete(origin);
|
||||
} catch {
|
||||
// When failed on the live page, we'll retry on the blank page below.
|
||||
|
|
@ -542,9 +578,10 @@ export abstract class BrowserContext extends SdkObject {
|
|||
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
|
||||
})`, { world: 'utility' });
|
||||
originStorage.localStorage = storage.localStorage;
|
||||
if (storage.localStorage.length)
|
||||
if (storage.localStorage.length) {
|
||||
result.origins.push(originStorage);
|
||||
}
|
||||
}
|
||||
await page.close(internalMetadata);
|
||||
}
|
||||
return result;
|
||||
|
|
@ -553,8 +590,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
async _resetStorage() {
|
||||
const oldOrigins = this._origins;
|
||||
const newOrigins = new Map(this._options.storageState?.origins?.map(p => [p.origin, p]) || []);
|
||||
if (!oldOrigins.size && !newOrigins.size)
|
||||
if (!oldOrigins.size && !newOrigins.size) {
|
||||
return;
|
||||
}
|
||||
let page = this.pages()[0];
|
||||
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
|
|
@ -583,8 +621,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
async _resetCookies() {
|
||||
await this.doClearCookies();
|
||||
if (this._options.storageState?.cookies)
|
||||
await this.addCookies(this._options.storageState?.cookies);
|
||||
if (this._options.storageState?.cookies) {
|
||||
await this.addCookies(this._options.storageState.cookies);
|
||||
}
|
||||
}
|
||||
|
||||
isSettingStorageState(): boolean {
|
||||
|
|
@ -594,8 +633,9 @@ export abstract class BrowserContext extends SdkObject {
|
|||
async setStorageState(metadata: CallMetadata, state: NonNullable<channels.BrowserNewContextParams['storageState']>) {
|
||||
this._settingStorageState = true;
|
||||
try {
|
||||
if (state.cookies)
|
||||
if (state.cookies) {
|
||||
await this.addCookies(state.cookies);
|
||||
}
|
||||
if (state.origins && state.origins.length) {
|
||||
const internalMetadata = serverSideCallMetadata();
|
||||
const page = await this.newPage(internalMetadata);
|
||||
|
|
@ -660,25 +700,30 @@ export abstract class BrowserContext extends SdkObject {
|
|||
|
||||
export function assertBrowserContextIsNotOwned(context: BrowserContext) {
|
||||
for (const page of context.pages()) {
|
||||
if (page._ownedContext)
|
||||
if (page._ownedContext) {
|
||||
throw new Error('Please use browser.newContext() for multi-page scripts that share the context.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) {
|
||||
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined)
|
||||
if (options.noDefaultViewport && options.deviceScaleFactor !== undefined) {
|
||||
throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`);
|
||||
if (options.noDefaultViewport && !!options.isMobile)
|
||||
}
|
||||
if (options.noDefaultViewport && !!options.isMobile) {
|
||||
throw new Error(`"isMobile" option is not supported with null "viewport"`);
|
||||
if (options.acceptDownloads === undefined && browserOptions.name !== 'electron')
|
||||
}
|
||||
if (options.acceptDownloads === undefined && browserOptions.name !== 'electron') {
|
||||
options.acceptDownloads = 'accept';
|
||||
} else if (options.acceptDownloads === undefined && browserOptions.name === 'electron') {
|
||||
// Electron requires explicit acceptDownloads: true since we wait for
|
||||
// https://github.com/electron/electron/pull/41718 to be widely shipped.
|
||||
// In 6-12 months, we can remove this check.
|
||||
else if (options.acceptDownloads === undefined && browserOptions.name === 'electron')
|
||||
options.acceptDownloads = 'internal-browser-default';
|
||||
if (!options.viewport && !options.noDefaultViewport)
|
||||
}
|
||||
if (!options.viewport && !options.noDefaultViewport) {
|
||||
options.viewport = { width: 1280, height: 720 };
|
||||
}
|
||||
if (options.recordVideo) {
|
||||
if (!options.recordVideo.size) {
|
||||
if (options.noDefaultViewport) {
|
||||
|
|
@ -696,40 +741,51 @@ export function validateBrowserContextOptions(options: types.BrowserContextOptio
|
|||
options.recordVideo.size!.width &= ~1;
|
||||
options.recordVideo.size!.height &= ~1;
|
||||
}
|
||||
if (options.proxy)
|
||||
if (options.proxy) {
|
||||
options.proxy = normalizeProxySettings(options.proxy);
|
||||
}
|
||||
verifyGeolocation(options.geolocation);
|
||||
}
|
||||
|
||||
export function verifyGeolocation(geolocation?: types.Geolocation) {
|
||||
if (!geolocation)
|
||||
if (!geolocation) {
|
||||
return;
|
||||
}
|
||||
geolocation.accuracy = geolocation.accuracy || 0;
|
||||
const { longitude, latitude, accuracy } = geolocation;
|
||||
if (longitude < -180 || longitude > 180)
|
||||
if (longitude < -180 || longitude > 180) {
|
||||
throw new Error(`geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed.`);
|
||||
if (latitude < -90 || latitude > 90)
|
||||
}
|
||||
if (latitude < -90 || latitude > 90) {
|
||||
throw new Error(`geolocation.latitude: precondition -90 <= LATITUDE <= 90 failed.`);
|
||||
if (accuracy < 0)
|
||||
}
|
||||
if (accuracy < 0) {
|
||||
throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) {
|
||||
if (!clientCertificates)
|
||||
if (!clientCertificates) {
|
||||
return;
|
||||
}
|
||||
for (const cert of clientCertificates) {
|
||||
if (!cert.origin)
|
||||
if (!cert.origin) {
|
||||
throw new Error(`clientCertificates.origin is required`);
|
||||
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx)
|
||||
}
|
||||
if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx) {
|
||||
throw new Error('None of cert, key, passphrase or pfx is specified');
|
||||
if (cert.cert && !cert.key)
|
||||
}
|
||||
if (cert.cert && !cert.key) {
|
||||
throw new Error('cert is specified without key');
|
||||
if (!cert.cert && cert.key)
|
||||
}
|
||||
if (!cert.cert && cert.key) {
|
||||
throw new Error('key is specified without cert');
|
||||
if (cert.pfx && (cert.cert || cert.key))
|
||||
}
|
||||
if (cert.pfx && (cert.cert || cert.key)) {
|
||||
throw new Error('pfx is specified together with cert, key or passphrase');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxySettings {
|
||||
let { server, bypass } = proxy;
|
||||
|
|
@ -739,18 +795,22 @@ export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxyS
|
|||
// new URL('localhost:8080') fails to parse host or protocol
|
||||
// In both of these cases, we need to try re-parse URL with `http://` prefix.
|
||||
url = new URL(server);
|
||||
if (!url.host || !url.protocol)
|
||||
if (!url.host || !url.protocol) {
|
||||
url = new URL('http://' + server);
|
||||
}
|
||||
} catch (e) {
|
||||
url = new URL('http://' + server);
|
||||
}
|
||||
if (url.protocol === 'socks4:' && (proxy.username || proxy.password))
|
||||
if (url.protocol === 'socks4:' && (proxy.username || proxy.password)) {
|
||||
throw new Error(`Socks4 proxy protocol does not support authentication`);
|
||||
if (url.protocol === 'socks5:' && (proxy.username || proxy.password))
|
||||
}
|
||||
if (url.protocol === 'socks5:' && (proxy.username || proxy.password)) {
|
||||
throw new Error(`Browser does not support socks5 proxy authentication`);
|
||||
}
|
||||
server = url.protocol + '//' + url.host;
|
||||
if (bypass)
|
||||
if (bypass) {
|
||||
bypass = bypass.split(',').map(t => t.trim()).join(',');
|
||||
}
|
||||
return { ...proxy, server, bypass };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,23 +80,28 @@ export abstract class BrowserType extends SdkObject {
|
|||
|
||||
async launch(metadata: CallMetadata, options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
|
||||
options = this._validateLaunchOptions(options);
|
||||
if (this._useBidi)
|
||||
if (this._useBidi) {
|
||||
options.useWebSocket = true;
|
||||
}
|
||||
const controller = new ProgressController(metadata, this);
|
||||
controller.setLogName('browser');
|
||||
const browser = await controller.run(progress => {
|
||||
const seleniumHubUrl = (options as any).__testHookSeleniumRemoteURL || process.env.SELENIUM_REMOTE_URL;
|
||||
if (seleniumHubUrl)
|
||||
if (seleniumHubUrl) {
|
||||
return this._launchWithSeleniumHub(progress, seleniumHubUrl, options);
|
||||
return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupLog(e); });
|
||||
}
|
||||
return this._innerLaunchWithRetries(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => {
|
||||
throw this._rewriteStartupLog(e);
|
||||
});
|
||||
}, TimeoutSettings.launchTimeout(options));
|
||||
return browser;
|
||||
}
|
||||
|
||||
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean, internalIgnoreHTTPSErrors?: boolean }): Promise<BrowserContext> {
|
||||
const launchOptions = this._validateLaunchOptions(options);
|
||||
if (this._useBidi)
|
||||
if (this._useBidi) {
|
||||
launchOptions.useWebSocket = true;
|
||||
}
|
||||
const controller = new ProgressController(metadata, this);
|
||||
controller.setLogName('browser');
|
||||
const browser = await controller.run(async progress => {
|
||||
|
|
@ -104,12 +109,14 @@ export abstract class BrowserType extends SdkObject {
|
|||
let clientCertificatesProxy: ClientCertificatesProxy | undefined;
|
||||
if (options.clientCertificates?.length) {
|
||||
clientCertificatesProxy = new ClientCertificatesProxy(options);
|
||||
launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
|
||||
launchOptions.proxyOverride = await clientCertificatesProxy.listen();
|
||||
options = { ...options };
|
||||
options.internalIgnoreHTTPSErrors = true;
|
||||
}
|
||||
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close());
|
||||
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); });
|
||||
const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => {
|
||||
throw this._rewriteStartupLog(e);
|
||||
});
|
||||
browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
|
||||
return browser;
|
||||
}, TimeoutSettings.launchTimeout(launchOptions));
|
||||
|
|
@ -134,8 +141,9 @@ export abstract class BrowserType extends SdkObject {
|
|||
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
||||
const browserLogsCollector = new RecentLogsCollector();
|
||||
const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir);
|
||||
if ((options as any).__testHookBeforeCreateBrowser)
|
||||
if ((options as any).__testHookBeforeCreateBrowser) {
|
||||
await (options as any).__testHookBeforeCreateBrowser();
|
||||
}
|
||||
const browserOptions: BrowserOptions = {
|
||||
name: this._name,
|
||||
isChromium: this._name === 'chromium',
|
||||
|
|
@ -154,14 +162,16 @@ export abstract class BrowserType extends SdkObject {
|
|||
wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined,
|
||||
originalLaunchOptions: options,
|
||||
};
|
||||
if (persistent)
|
||||
if (persistent) {
|
||||
validateBrowserContextOptions(persistent, browserOptions);
|
||||
}
|
||||
copyTestHooks(options, browserOptions);
|
||||
const browser = await this.connectToTransport(transport, browserOptions);
|
||||
(browser as any)._userDataDirForTest = userDataDir;
|
||||
// We assume no control when using custom arguments, and do not prepare the default context in that case.
|
||||
if (persistent && !options.ignoreAllDefaultArgs)
|
||||
if (persistent && !options.ignoreAllDefaultArgs) {
|
||||
await browser._defaultContext!._loadDefaultContext(progress);
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
|
|
@ -186,8 +196,9 @@ export abstract class BrowserType extends SdkObject {
|
|||
|
||||
if (userDataDir) {
|
||||
// Firefox bails if the profile directory does not exist, Chrome creates it. We ensure consistent behavior here.
|
||||
if (!await existsAsync(userDataDir))
|
||||
if (!await existsAsync(userDataDir)) {
|
||||
await fs.promises.mkdir(userDataDir, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
} else {
|
||||
userDataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `playwright_${this._name}dev_profile-`));
|
||||
tempDirectories.push(userDataDir);
|
||||
|
|
@ -195,22 +206,25 @@ export abstract class BrowserType extends SdkObject {
|
|||
await this.prepareUserDataDir(options, userDataDir);
|
||||
|
||||
const browserArguments = [];
|
||||
if (ignoreAllDefaultArgs)
|
||||
if (ignoreAllDefaultArgs) {
|
||||
browserArguments.push(...args);
|
||||
else if (ignoreDefaultArgs)
|
||||
} else if (ignoreDefaultArgs) {
|
||||
browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir).filter(arg => ignoreDefaultArgs.indexOf(arg) === -1));
|
||||
else
|
||||
} else {
|
||||
browserArguments.push(...this.defaultArgs(options, isPersistent, userDataDir));
|
||||
}
|
||||
|
||||
let executable: string;
|
||||
if (executablePath) {
|
||||
if (!(await existsAsync(executablePath)))
|
||||
if (!(await existsAsync(executablePath))) {
|
||||
throw new Error(`Failed to launch ${this._name} because executable doesn't exist at ${executablePath}`);
|
||||
}
|
||||
executable = executablePath;
|
||||
} else {
|
||||
const registryExecutable = registry.findExecutable(this.getExecutableName(options));
|
||||
if (!registryExecutable || registryExecutable.browserName !== this._name)
|
||||
if (!registryExecutable || registryExecutable.browserName !== this._name) {
|
||||
throw new Error(`Unsupported ${this._name} channel "${options.channel}"`);
|
||||
}
|
||||
executable = registryExecutable.executablePathOrDie(this.attribution.playwright.options.sdkLanguage);
|
||||
await registry.validateHostRequirementsForExecutablesIfNeeded([registryExecutable], this.attribution.playwright.options.sdkLanguage);
|
||||
}
|
||||
|
|
@ -235,8 +249,9 @@ export abstract class BrowserType extends SdkObject {
|
|||
stdio: 'pipe',
|
||||
tempDirectories,
|
||||
attemptToGracefullyClose: async () => {
|
||||
if ((options as any).__testHookGracefullyClose)
|
||||
if ((options as any).__testHookGracefullyClose) {
|
||||
await (options as any).__testHookGracefullyClose();
|
||||
}
|
||||
// We try to gracefully close to prevent crash reporting and core dumps.
|
||||
// Note that it's fine to reuse the pipe transport, since
|
||||
// our connection ignores kBrowserCloseMessageId.
|
||||
|
|
@ -245,8 +260,9 @@ export abstract class BrowserType extends SdkObject {
|
|||
onExit: (exitCode, signal) => {
|
||||
// Unblock launch when browser prematurely exits.
|
||||
readyState?.onBrowserExit();
|
||||
if (browserProcess && browserProcess.onclose)
|
||||
if (browserProcess && browserProcess.onclose) {
|
||||
browserProcess.onclose(exitCode, signal);
|
||||
}
|
||||
},
|
||||
});
|
||||
async function closeOrKill(timeout: number): Promise<void> {
|
||||
|
|
@ -280,11 +296,13 @@ export abstract class BrowserType extends SdkObject {
|
|||
}
|
||||
|
||||
async _createArtifactDirs(options: types.LaunchOptions): Promise<void> {
|
||||
if (options.downloadsPath)
|
||||
if (options.downloadsPath) {
|
||||
await fs.promises.mkdir(options.downloadsPath, { recursive: true });
|
||||
if (options.tracesDir)
|
||||
}
|
||||
if (options.tracesDir) {
|
||||
await fs.promises.mkdir(options.tracesDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number }, timeout?: number): Promise<Browser> {
|
||||
throw new Error('CDP connections are only supported by Chromium');
|
||||
|
|
@ -297,12 +315,15 @@ export abstract class BrowserType extends SdkObject {
|
|||
private _validateLaunchOptions(options: types.LaunchOptions): types.LaunchOptions {
|
||||
const { devtools = false } = options;
|
||||
let { headless = !devtools, downloadsPath, proxy } = options;
|
||||
if (debugMode())
|
||||
if (debugMode()) {
|
||||
headless = false;
|
||||
if (downloadsPath && !path.isAbsolute(downloadsPath))
|
||||
}
|
||||
if (downloadsPath && !path.isAbsolute(downloadsPath)) {
|
||||
downloadsPath = path.join(process.cwd(), downloadsPath);
|
||||
if (this.attribution.playwright.options.socksProxyPort)
|
||||
}
|
||||
if (this.attribution.playwright.options.socksProxyPort) {
|
||||
proxy = { server: `socks5://127.0.0.1:${this.attribution.playwright.options.socksProxyPort}` };
|
||||
}
|
||||
return { ...options, devtools, headless, downloadsPath, proxy };
|
||||
}
|
||||
|
||||
|
|
@ -320,8 +341,9 @@ export abstract class BrowserType extends SdkObject {
|
|||
}
|
||||
|
||||
_rewriteStartupLog(error: Error): Error {
|
||||
if (!isProtocolError(error))
|
||||
if (!isProtocolError(error)) {
|
||||
return error;
|
||||
}
|
||||
return this.doRewriteStartupLog(error);
|
||||
}
|
||||
|
||||
|
|
@ -345,7 +367,8 @@ export abstract class BrowserType extends SdkObject {
|
|||
|
||||
function copyTestHooks(from: object, to: object) {
|
||||
for (const [key, value] of Object.entries(from)) {
|
||||
if (key.startsWith('__testHook'))
|
||||
if (key.startsWith('__testHook')) {
|
||||
(to as any)[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,9 +58,10 @@ export class Chromium extends BrowserType {
|
|||
constructor(parent: SdkObject) {
|
||||
super(parent, 'chromium');
|
||||
|
||||
if (debugMode())
|
||||
if (debugMode()) {
|
||||
this._devtools = this._createDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
override async connectOverCDP(metadata: CallMetadata, endpointURL: string, options: { slowMo?: number, headers?: types.HeadersArray }, timeout?: number) {
|
||||
const controller = new ProgressController(metadata, this);
|
||||
|
|
@ -72,13 +73,15 @@ export class Chromium extends BrowserType {
|
|||
|
||||
async _connectOverCDPInternal(progress: Progress, endpointURL: string, options: types.LaunchOptions & { headers?: types.HeadersArray }, onClose?: () => Promise<void>) {
|
||||
let headersMap: { [key: string]: string; } | undefined;
|
||||
if (options.headers)
|
||||
if (options.headers) {
|
||||
headersMap = headersArrayToObject(options.headers, false);
|
||||
}
|
||||
|
||||
if (!headersMap)
|
||||
if (!headersMap) {
|
||||
headersMap = { 'User-Agent': getUserAgent() };
|
||||
else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent'))
|
||||
} else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent')) {
|
||||
headersMap['User-Agent'] = getUserAgent();
|
||||
}
|
||||
|
||||
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
||||
|
||||
|
|
@ -135,14 +138,17 @@ export class Chromium extends BrowserType {
|
|||
}
|
||||
|
||||
override doRewriteStartupLog(error: ProtocolError): ProtocolError {
|
||||
if (!error.logs)
|
||||
if (!error.logs) {
|
||||
return error;
|
||||
if (error.logs.includes('Missing X server'))
|
||||
}
|
||||
if (error.logs.includes('Missing X server')) {
|
||||
error.logs = '\n' + wrapInASCIIBox(kNoXServerRunningError, 1);
|
||||
}
|
||||
// These error messages are taken from Chromium source code as of July, 2020:
|
||||
// https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
|
||||
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180'))
|
||||
if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180')) {
|
||||
return error;
|
||||
}
|
||||
error.logs = [
|
||||
`Chromium sandboxing failed!`,
|
||||
`================================`,
|
||||
|
|
@ -167,8 +173,9 @@ export class Chromium extends BrowserType {
|
|||
override async _launchWithSeleniumHub(progress: Progress, hubUrl: string, options: types.LaunchOptions): Promise<CRBrowser> {
|
||||
await this._createArtifactDirs(options);
|
||||
|
||||
if (!hubUrl.endsWith('/'))
|
||||
if (!hubUrl.endsWith('/')) {
|
||||
hubUrl = hubUrl + '/';
|
||||
}
|
||||
|
||||
const args = this._innerDefaultArgs(options);
|
||||
args.push('--remote-debugging-port=0');
|
||||
|
|
@ -180,16 +187,18 @@ export class Chromium extends BrowserType {
|
|||
|
||||
if (process.env.SELENIUM_REMOTE_CAPABILITIES) {
|
||||
const remoteCapabilities = parseSeleniumRemoteParams({ name: 'capabilities', value: process.env.SELENIUM_REMOTE_CAPABILITIES }, progress);
|
||||
if (remoteCapabilities)
|
||||
if (remoteCapabilities) {
|
||||
desiredCapabilities = { ...desiredCapabilities, ...remoteCapabilities };
|
||||
}
|
||||
}
|
||||
|
||||
let headers: { [key: string]: string } = {};
|
||||
if (process.env.SELENIUM_REMOTE_HEADERS) {
|
||||
const remoteHeaders = parseSeleniumRemoteParams({ name: 'headers', value: process.env.SELENIUM_REMOTE_HEADERS }, progress);
|
||||
if (remoteHeaders)
|
||||
if (remoteHeaders) {
|
||||
headers = remoteHeaders;
|
||||
}
|
||||
}
|
||||
|
||||
progress.log(`<selenium> connecting to ${hubUrl}`);
|
||||
const response = await fetchData({
|
||||
|
|
@ -229,8 +238,9 @@ export class Chromium extends BrowserType {
|
|||
progress.log(`<selenium> using selenium v4`);
|
||||
const endpointURLString = addProtocol(capabilities['se:cdp']);
|
||||
endpointURL = new URL(endpointURLString);
|
||||
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1')
|
||||
if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') {
|
||||
endpointURL.hostname = new URL(hubUrl).hostname;
|
||||
}
|
||||
progress.log(`<selenium> retrieved endpoint ${endpointURL.toString()} for sessionId=${sessionId}`);
|
||||
} else {
|
||||
// Selenium 3 - resolve target node IP to use instead of localhost ws url.
|
||||
|
|
@ -274,38 +284,45 @@ export class Chromium extends BrowserType {
|
|||
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||
const chromeArguments = this._innerDefaultArgs(options);
|
||||
chromeArguments.push(`--user-data-dir=${userDataDir}`);
|
||||
if (options.useWebSocket)
|
||||
if (options.useWebSocket) {
|
||||
chromeArguments.push('--remote-debugging-port=0');
|
||||
else
|
||||
} else {
|
||||
chromeArguments.push('--remote-debugging-pipe');
|
||||
if (isPersistent)
|
||||
}
|
||||
if (isPersistent) {
|
||||
chromeArguments.push('about:blank');
|
||||
else
|
||||
} else {
|
||||
chromeArguments.push('--no-startup-window');
|
||||
}
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
private _innerDefaultArgs(options: types.LaunchOptions): string[] {
|
||||
const { args = [] } = options;
|
||||
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
|
||||
if (userDataDirArg)
|
||||
if (userDataDirArg) {
|
||||
throw this._createUserDataDirArgMisuseError('--user-data-dir');
|
||||
if (args.find(arg => arg.startsWith('--remote-debugging-pipe')))
|
||||
}
|
||||
if (args.find(arg => arg.startsWith('--remote-debugging-pipe'))) {
|
||||
throw new Error('Playwright manages remote debugging connection itself.');
|
||||
if (args.find(arg => !arg.startsWith('-')))
|
||||
}
|
||||
if (args.find(arg => !arg.startsWith('-'))) {
|
||||
throw new Error('Arguments can not specify page to be opened');
|
||||
}
|
||||
const chromeArguments = [...chromiumSwitches];
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
// See https://github.com/microsoft/playwright/issues/7362
|
||||
chromeArguments.push('--enable-use-zoom-for-dsf=false');
|
||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
|
||||
if (options.headless && (!options.channel || options.channel === 'chromium-headless-shell'))
|
||||
if (options.headless && (!options.channel || options.channel === 'chromium-headless-shell')) {
|
||||
chromeArguments.push('--use-angle');
|
||||
}
|
||||
}
|
||||
|
||||
if (options.devtools)
|
||||
if (options.devtools) {
|
||||
chromeArguments.push('--auto-open-devtools-for-tabs');
|
||||
}
|
||||
if (options.headless) {
|
||||
chromeArguments.push('--headless');
|
||||
|
||||
|
|
@ -315,8 +332,9 @@ export class Chromium extends BrowserType {
|
|||
'--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4',
|
||||
);
|
||||
}
|
||||
if (options.chromiumSandbox !== true)
|
||||
if (options.chromiumSandbox !== true) {
|
||||
chromeArguments.push('--no-sandbox');
|
||||
}
|
||||
const proxy = options.proxyOverride || options.proxy;
|
||||
if (proxy) {
|
||||
const proxyURL = new URL(proxy.server);
|
||||
|
|
@ -329,28 +347,34 @@ export class Chromium extends BrowserType {
|
|||
chromeArguments.push(`--proxy-server=${proxy.server}`);
|
||||
const proxyBypassRules = [];
|
||||
// https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
|
||||
if (this.attribution.playwright.options.socksProxyPort)
|
||||
if (this.attribution.playwright.options.socksProxyPort) {
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxy.bypass)
|
||||
}
|
||||
if (proxy.bypass) {
|
||||
proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>'))
|
||||
}
|
||||
if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) {
|
||||
proxyBypassRules.push('<-loopback>');
|
||||
if (proxyBypassRules.length > 0)
|
||||
}
|
||||
if (proxyBypassRules.length > 0) {
|
||||
chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
|
||||
}
|
||||
}
|
||||
chromeArguments.push(...args);
|
||||
return chromeArguments;
|
||||
}
|
||||
|
||||
override readyState(options: types.LaunchOptions): BrowserReadyState | undefined {
|
||||
if (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port')))
|
||||
if (options.useWebSocket || options.args?.some(a => a.startsWith('--remote-debugging-port'))) {
|
||||
return new ChromiumReadyState();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
override getExecutableName(options: types.LaunchOptions): string {
|
||||
if (options.channel)
|
||||
if (options.channel) {
|
||||
return options.channel;
|
||||
}
|
||||
return options.headless ? 'chromium-headless-shell' : 'chromium';
|
||||
}
|
||||
}
|
||||
|
|
@ -358,14 +382,16 @@ export class Chromium extends BrowserType {
|
|||
class ChromiumReadyState extends BrowserReadyState {
|
||||
override onBrowserOutput(message: string): void {
|
||||
const match = message.match(/DevTools listening on (.*)/);
|
||||
if (match)
|
||||
if (match) {
|
||||
this._wsEndpoint.resolve(match[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function urlToWSEndpoint(progress: Progress, endpointURL: string, headers: { [key: string]: string; }) {
|
||||
if (endpointURL.startsWith('ws'))
|
||||
if (endpointURL.startsWith('ws')) {
|
||||
return endpointURL;
|
||||
}
|
||||
progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
|
||||
const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
|
||||
const json = await fetchData({
|
||||
|
|
@ -389,8 +415,9 @@ async function seleniumErrorHandler(params: HTTPRequestParams, response: http.In
|
|||
}
|
||||
|
||||
function addProtocol(url: string) {
|
||||
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol)))
|
||||
if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol))) {
|
||||
return 'http://' + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,20 +55,25 @@ class CRAXNode implements accessibility.AXNode {
|
|||
this._richlyEditable = property.value.value === 'richtext';
|
||||
this._editable = true;
|
||||
}
|
||||
if (property.name === 'focusable')
|
||||
if (property.name === 'focusable') {
|
||||
this._focusable = property.value.value;
|
||||
if (property.name === 'expanded')
|
||||
}
|
||||
if (property.name === 'expanded') {
|
||||
this._expanded = property.value.value;
|
||||
if (property.name === 'hidden')
|
||||
}
|
||||
if (property.name === 'hidden') {
|
||||
this._hidden = property.value.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _isPlainTextField(): boolean {
|
||||
if (this._richlyEditable)
|
||||
if (this._richlyEditable) {
|
||||
return false;
|
||||
if (this._editable)
|
||||
}
|
||||
if (this._editable) {
|
||||
return true;
|
||||
}
|
||||
return this._role === 'textbox' || this._role === 'ComboBox' || this._role === 'searchbox';
|
||||
}
|
||||
|
||||
|
|
@ -103,26 +108,30 @@ class CRAXNode implements accessibility.AXNode {
|
|||
}
|
||||
|
||||
find(predicate: (arg0: CRAXNode) => boolean): CRAXNode | null {
|
||||
if (predicate(this))
|
||||
if (predicate(this)) {
|
||||
return this;
|
||||
}
|
||||
for (const child of this._children) {
|
||||
const result = child.find(predicate);
|
||||
if (result)
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
isLeafNode(): boolean {
|
||||
if (!this._children.length)
|
||||
if (!this._children.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// These types of objects may have children that we use as internal
|
||||
// implementation details, but we want to expose them as leaves to platform
|
||||
// accessibility APIs because screen readers might be confused if they find
|
||||
// any children.
|
||||
if (this._isPlainTextField() || this._isTextOnlyObject())
|
||||
if (this._isPlainTextField() || this._isTextOnlyObject()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Roles whose children are only presentational according to the ARIA and
|
||||
// HTML5 Specs should be hidden from screen readers.
|
||||
|
|
@ -143,12 +152,15 @@ class CRAXNode implements accessibility.AXNode {
|
|||
}
|
||||
|
||||
// Here and below: Android heuristics
|
||||
if (this._hasFocusableChild())
|
||||
if (this._hasFocusableChild()) {
|
||||
return false;
|
||||
if (this._focusable && this._role !== 'WebArea' && this._role !== 'RootWebArea' && this._name)
|
||||
}
|
||||
if (this._focusable && this._role !== 'WebArea' && this._role !== 'RootWebArea' && this._name) {
|
||||
return true;
|
||||
if (this._role === 'heading' && this._name)
|
||||
}
|
||||
if (this._role === 'heading' && this._name) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -182,19 +194,23 @@ class CRAXNode implements accessibility.AXNode {
|
|||
|
||||
isInteresting(insideControl: boolean): boolean {
|
||||
const role = this._role;
|
||||
if (role === 'Ignored' || this._hidden)
|
||||
if (role === 'Ignored' || this._hidden) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._focusable || this._richlyEditable)
|
||||
if (this._focusable || this._richlyEditable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If it's not focusable but has a control role, then it's interesting.
|
||||
if (this.isControl())
|
||||
if (this.isControl()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A non focusable child of a control is not interesting
|
||||
if (insideControl)
|
||||
if (insideControl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.isLeafNode() && !!this._name;
|
||||
}
|
||||
|
|
@ -212,10 +228,12 @@ class CRAXNode implements accessibility.AXNode {
|
|||
|
||||
serialize(): channels.AXNode {
|
||||
const properties: Map<string, number | string | boolean> = new Map();
|
||||
for (const property of this._payload.properties || [])
|
||||
for (const property of this._payload.properties || []) {
|
||||
properties.set(property.name.toLowerCase(), property.value.value);
|
||||
if (this._payload.description)
|
||||
}
|
||||
if (this._payload.description) {
|
||||
properties.set('description', this._payload.description.value);
|
||||
}
|
||||
|
||||
const node: {[x in keyof channels.AXNode]: any} = {
|
||||
role: this.normalizedRole(),
|
||||
|
|
@ -229,8 +247,9 @@ class CRAXNode implements accessibility.AXNode {
|
|||
'valuetext',
|
||||
];
|
||||
for (const userStringProperty of userStringProperties) {
|
||||
if (!properties.has(userStringProperty))
|
||||
if (!properties.has(userStringProperty)) {
|
||||
continue;
|
||||
}
|
||||
node[userStringProperty] = properties.get(userStringProperty);
|
||||
}
|
||||
const booleanProperties: Array<keyof channels.AXNode> = [
|
||||
|
|
@ -247,11 +266,13 @@ class CRAXNode implements accessibility.AXNode {
|
|||
for (const booleanProperty of booleanProperties) {
|
||||
// WebArea's treat focus differently than other nodes. They report whether their frame has focus,
|
||||
// not whether focus is specifically on the root node.
|
||||
if (booleanProperty === 'focused' && (this._role === 'WebArea' || this._role === 'RootWebArea'))
|
||||
if (booleanProperty === 'focused' && (this._role === 'WebArea' || this._role === 'RootWebArea')) {
|
||||
continue;
|
||||
}
|
||||
const value = properties.get(booleanProperty);
|
||||
if (!value)
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
node[booleanProperty] = value;
|
||||
}
|
||||
const numericalProperties: Array<keyof channels.AXNode> = [
|
||||
|
|
@ -260,8 +281,9 @@ class CRAXNode implements accessibility.AXNode {
|
|||
'valuemin',
|
||||
];
|
||||
for (const numericalProperty of numericalProperties) {
|
||||
if (!properties.has(numericalProperty))
|
||||
if (!properties.has(numericalProperty)) {
|
||||
continue;
|
||||
}
|
||||
node[numericalProperty] = properties.get(numericalProperty);
|
||||
}
|
||||
const tokenProperties: Array<keyof channels.AXNode> = [
|
||||
|
|
@ -272,33 +294,40 @@ class CRAXNode implements accessibility.AXNode {
|
|||
];
|
||||
for (const tokenProperty of tokenProperties) {
|
||||
const value = properties.get(tokenProperty);
|
||||
if (!value || value === 'false')
|
||||
if (!value || value === 'false') {
|
||||
continue;
|
||||
}
|
||||
node[tokenProperty] = value;
|
||||
}
|
||||
|
||||
const axNode = node as channels.AXNode;
|
||||
if (this._payload.value) {
|
||||
if (typeof this._payload.value.value === 'string')
|
||||
if (typeof this._payload.value.value === 'string') {
|
||||
axNode.valueString = this._payload.value.value;
|
||||
if (typeof this._payload.value.value === 'number')
|
||||
}
|
||||
if (typeof this._payload.value.value === 'number') {
|
||||
axNode.valueNumber = this._payload.value.value;
|
||||
}
|
||||
if (properties.has('checked'))
|
||||
}
|
||||
if (properties.has('checked')) {
|
||||
axNode.checked = properties.get('checked') === 'true' ? 'checked' : properties.get('checked') === 'false' ? 'unchecked' : 'mixed';
|
||||
if (properties.has('pressed'))
|
||||
}
|
||||
if (properties.has('pressed')) {
|
||||
axNode.pressed = properties.get('pressed') === 'true' ? 'pressed' : properties.get('pressed') === 'false' ? 'released' : 'mixed';
|
||||
}
|
||||
return axNode;
|
||||
}
|
||||
|
||||
static createTree(client: CRSession, payloads: Protocol.Accessibility.AXNode[]): CRAXNode {
|
||||
const nodeById: Map<string, CRAXNode> = new Map();
|
||||
for (const payload of payloads)
|
||||
for (const payload of payloads) {
|
||||
nodeById.set(payload.nodeId, new CRAXNode(client, payload));
|
||||
}
|
||||
for (const node of nodeById.values()) {
|
||||
for (const childId of node._payload.childIds || [])
|
||||
for (const childId of node._payload.childIds || []) {
|
||||
node._children.push(nodeById.get(childId)!);
|
||||
}
|
||||
}
|
||||
return nodeById.values().next().value!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,11 +59,13 @@ export class CRBrowser extends Browser {
|
|||
const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
|
||||
const browser = new CRBrowser(parent, connection, options);
|
||||
browser._devtools = devtools;
|
||||
if (browser.isClank())
|
||||
if (browser.isClank()) {
|
||||
browser._isCollocatedWithServer = false;
|
||||
}
|
||||
const session = connection.rootSession;
|
||||
if ((options as any).__testHookOnConnectToBrowser)
|
||||
if ((options as any).__testHookOnConnectToBrowser) {
|
||||
await (options as any).__testHookOnConnectToBrowser();
|
||||
}
|
||||
|
||||
const version = await session.send('Browser.getVersion');
|
||||
browser._version = version.product.substring(version.product.indexOf('/') + 1);
|
||||
|
|
@ -104,11 +106,12 @@ export class CRBrowser extends Browser {
|
|||
const proxy = options.proxyOverride || options.proxy;
|
||||
let proxyBypassList = undefined;
|
||||
if (proxy) {
|
||||
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)
|
||||
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) {
|
||||
proxyBypassList = proxy.bypass;
|
||||
else
|
||||
} else {
|
||||
proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : '');
|
||||
}
|
||||
}
|
||||
|
||||
const { browserContextId } = await this._session.send('Target.createBrowserContext', {
|
||||
disposeOnDetach: true,
|
||||
|
|
@ -134,10 +137,12 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
|
||||
_platform(): 'mac' | 'linux' | 'win' {
|
||||
if (this._userAgent.includes('Windows'))
|
||||
if (this._userAgent.includes('Windows')) {
|
||||
return 'win';
|
||||
if (this._userAgent.includes('Macintosh'))
|
||||
}
|
||||
if (this._userAgent.includes('Macintosh')) {
|
||||
return 'mac';
|
||||
}
|
||||
return 'linux';
|
||||
}
|
||||
|
||||
|
|
@ -150,8 +155,9 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
|
||||
_onAttachedToTarget({ targetInfo, sessionId, waitingForDebugger }: Protocol.Target.attachedToTargetPayload) {
|
||||
if (targetInfo.type === 'browser')
|
||||
if (targetInfo.type === 'browser') {
|
||||
return;
|
||||
}
|
||||
const session = this._session.createChildSession(sessionId);
|
||||
assert(targetInfo.browserContextId, 'targetInfo: ' + JSON.stringify(targetInfo, null, 2));
|
||||
let context = this._contexts.get(targetInfo.browserContextId) || null;
|
||||
|
|
@ -228,14 +234,17 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
|
||||
private _didDisconnect() {
|
||||
for (const crPage of this._crPages.values())
|
||||
for (const crPage of this._crPages.values()) {
|
||||
crPage.didClose();
|
||||
}
|
||||
this._crPages.clear();
|
||||
for (const backgroundPage of this._backgroundPages.values())
|
||||
for (const backgroundPage of this._backgroundPages.values()) {
|
||||
backgroundPage.didClose();
|
||||
}
|
||||
this._backgroundPages.clear();
|
||||
for (const serviceWorker of this._serviceWorkers.values())
|
||||
for (const serviceWorker of this._serviceWorkers.values()) {
|
||||
serviceWorker.didClose();
|
||||
}
|
||||
this._serviceWorkers.clear();
|
||||
this._didClose();
|
||||
}
|
||||
|
|
@ -243,9 +252,10 @@ export class CRBrowser extends Browser {
|
|||
private _findOwningPage(frameId: string) {
|
||||
for (const crPage of this._crPages.values()) {
|
||||
const frame = crPage._page._frameManager.frame(frameId);
|
||||
if (frame)
|
||||
if (frame) {
|
||||
return crPage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -261,19 +271,23 @@ export class CRBrowser extends Browser {
|
|||
|
||||
let originPage = page._page.initializedOrUndefined();
|
||||
// If it's a new window download, report it on the opener page.
|
||||
if (!originPage && page._opener)
|
||||
if (!originPage && page._opener) {
|
||||
originPage = page._opener._page.initializedOrUndefined();
|
||||
if (!originPage)
|
||||
}
|
||||
if (!originPage) {
|
||||
return;
|
||||
}
|
||||
this._downloadCreated(originPage, payload.guid, payload.url, payload.suggestedFilename);
|
||||
}
|
||||
|
||||
_onDownloadProgress(payload: any) {
|
||||
if (payload.state === 'completed')
|
||||
if (payload.state === 'completed') {
|
||||
this._downloadFinished(payload.guid, '');
|
||||
if (payload.state === 'canceled')
|
||||
}
|
||||
if (payload.state === 'canceled') {
|
||||
this._downloadFinished(payload.guid, this._closeReason || 'canceled');
|
||||
}
|
||||
}
|
||||
|
||||
async _closePage(crPage: CRPage) {
|
||||
await this._session.send('Target.closeTarget', { targetId: crPage._targetId });
|
||||
|
|
@ -298,8 +312,9 @@ export class CRBrowser extends Browser {
|
|||
categories = defaultCategories,
|
||||
} = options;
|
||||
|
||||
if (screenshots)
|
||||
if (screenshots) {
|
||||
categories.push('disabled-by-default-devtools.screenshot');
|
||||
}
|
||||
|
||||
this._tracingRecording = true;
|
||||
await this._tracingClient.send('Tracing.start', {
|
||||
|
|
@ -327,8 +342,9 @@ export class CRBrowser extends Browser {
|
|||
}
|
||||
|
||||
async _clientRootSession(): Promise<CDPSession> {
|
||||
if (!this._clientRootSessionPromise)
|
||||
if (!this._clientRootSessionPromise) {
|
||||
this._clientRootSessionPromise = this._connection.createBrowserSession();
|
||||
}
|
||||
return this._clientRootSessionPromise;
|
||||
}
|
||||
}
|
||||
|
|
@ -380,14 +396,16 @@ export class CRBrowserContext extends BrowserContext {
|
|||
// heuristic assuming that there is only one page created at a time.
|
||||
const newKeys = new Set(this._browser._crPages.keys());
|
||||
// Remove old keys.
|
||||
for (const key of oldKeys)
|
||||
for (const key of oldKeys) {
|
||||
newKeys.delete(key);
|
||||
}
|
||||
// Remove potential concurrent popups.
|
||||
for (const key of newKeys) {
|
||||
const page = this._browser._crPages.get(key)!;
|
||||
if (page._opener)
|
||||
if (page._opener) {
|
||||
newKeys.delete(key);
|
||||
}
|
||||
}
|
||||
assert(newKeys.size === 1);
|
||||
[targetId] = [...newKeys];
|
||||
}
|
||||
|
|
@ -437,8 +455,9 @@ export class CRBrowserContext extends BrowserContext {
|
|||
]);
|
||||
const filtered = permissions.map(permission => {
|
||||
const protocolPermission = webPermissionToProtocol.get(permission);
|
||||
if (!protocolPermission)
|
||||
if (!protocolPermission) {
|
||||
throw new Error('Unknown permission: ' + permission);
|
||||
}
|
||||
return protocolPermission;
|
||||
});
|
||||
await this._browser._session.send('Browser.grantPermissions', { origin: origin === '*' ? undefined : origin, browserContextId: this._browserContextId, permissions: filtered });
|
||||
|
|
@ -451,57 +470,69 @@ export class CRBrowserContext extends BrowserContext {
|
|||
async setGeolocation(geolocation?: types.Geolocation): Promise<void> {
|
||||
verifyGeolocation(geolocation);
|
||||
this._options.geolocation = geolocation;
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).updateGeolocation();
|
||||
}
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(headers: types.HeadersArray): Promise<void> {
|
||||
this._options.extraHTTPHeaders = headers;
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).updateExtraHTTPHeaders();
|
||||
for (const sw of this.serviceWorkers())
|
||||
}
|
||||
for (const sw of this.serviceWorkers()) {
|
||||
await (sw as CRServiceWorker).updateExtraHTTPHeaders();
|
||||
}
|
||||
}
|
||||
|
||||
async setUserAgent(userAgent: string | undefined): Promise<void> {
|
||||
this._options.userAgent = userAgent;
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).updateUserAgent();
|
||||
}
|
||||
// TODO: service workers don't have Emulation domain?
|
||||
}
|
||||
|
||||
async setOffline(offline: boolean): Promise<void> {
|
||||
this._options.offline = offline;
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).updateOffline();
|
||||
for (const sw of this.serviceWorkers())
|
||||
}
|
||||
for (const sw of this.serviceWorkers()) {
|
||||
await (sw as CRServiceWorker).updateOffline();
|
||||
}
|
||||
}
|
||||
|
||||
async doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void> {
|
||||
this._options.httpCredentials = httpCredentials;
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).updateHttpCredentials();
|
||||
for (const sw of this.serviceWorkers())
|
||||
}
|
||||
for (const sw of this.serviceWorkers()) {
|
||||
await (sw as CRServiceWorker).updateHttpCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
async doAddInitScript(initScript: InitScript) {
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).addInitScript(initScript);
|
||||
}
|
||||
}
|
||||
|
||||
async doRemoveNonInternalInitScripts() {
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).removeNonInternalInitScripts();
|
||||
}
|
||||
}
|
||||
|
||||
async doUpdateRequestInterception(): Promise<void> {
|
||||
for (const page of this.pages())
|
||||
for (const page of this.pages()) {
|
||||
await (page._delegate as CRPage).updateRequestInterception();
|
||||
for (const sw of this.serviceWorkers())
|
||||
}
|
||||
for (const sw of this.serviceWorkers()) {
|
||||
await (sw as CRServiceWorker).updateRequestInterception();
|
||||
}
|
||||
}
|
||||
|
||||
async doClose(reason: string | undefined) {
|
||||
// Headful chrome cannot dispose browser context with opened 'beforeunload'
|
||||
|
|
@ -525,8 +556,9 @@ export class CRBrowserContext extends BrowserContext {
|
|||
await this._browser._session.send('Target.disposeBrowserContext', { browserContextId: this._browserContextId });
|
||||
this._browser._contexts.delete(this._browserContextId);
|
||||
for (const [targetId, serviceWorker] of this._browser._serviceWorkers) {
|
||||
if (serviceWorker._browserContext !== this)
|
||||
if (serviceWorker._browserContext !== this) {
|
||||
continue;
|
||||
}
|
||||
// When closing a browser context, service workers are shutdown
|
||||
// asynchronously and we get detached from them later.
|
||||
// To avoid the wrong order of notifications, we manually fire
|
||||
|
|
@ -552,9 +584,10 @@ export class CRBrowserContext extends BrowserContext {
|
|||
}
|
||||
|
||||
override async clearCache(): Promise<void> {
|
||||
for (const page of this._crPages())
|
||||
for (const page of this._crPages()) {
|
||||
await page._networkManager.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
async cancelDownload(guid: string) {
|
||||
// The upstream CDP method is implemented in a way that no explicit error would be given
|
||||
|
|
@ -569,9 +602,10 @@ export class CRBrowserContext extends BrowserContext {
|
|||
backgroundPages(): Page[] {
|
||||
const result: Page[] = [];
|
||||
for (const backgroundPage of this._browser._backgroundPages.values()) {
|
||||
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined())
|
||||
if (backgroundPage._browserContext === this && backgroundPage._page.initializedOrUndefined()) {
|
||||
result.push(backgroundPage._page);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -585,8 +619,9 @@ export class CRBrowserContext extends BrowserContext {
|
|||
targetId = (page._delegate as CRPage)._targetId;
|
||||
} else if (page instanceof Frame) {
|
||||
const session = (page._page._delegate as CRPage)._sessions.get(page._id);
|
||||
if (!session)
|
||||
if (!session) {
|
||||
throw new Error(`This frame does not have a separate CDP session, it is a part of the parent frame's session`);
|
||||
}
|
||||
targetId = session._targetId;
|
||||
} else {
|
||||
throw new Error('page: expected Page or Frame');
|
||||
|
|
|
|||
|
|
@ -59,8 +59,9 @@ export class CRConnection extends EventEmitter {
|
|||
_rawSend(sessionId: string, method: string, params: any): number {
|
||||
const id = ++this._lastId;
|
||||
const message: ProtocolRequest = { id, method, params };
|
||||
if (sessionId)
|
||||
if (sessionId) {
|
||||
message.sessionId = sessionId;
|
||||
}
|
||||
this._protocolLogger('send', message);
|
||||
this._transport.send(message);
|
||||
return id;
|
||||
|
|
@ -68,12 +69,14 @@ export class CRConnection extends EventEmitter {
|
|||
|
||||
async _onMessage(message: ProtocolResponse) {
|
||||
this._protocolLogger('receive', message);
|
||||
if (message.id === kBrowserCloseMessageId)
|
||||
if (message.id === kBrowserCloseMessageId) {
|
||||
return;
|
||||
}
|
||||
const session = this._sessions.get(message.sessionId || '');
|
||||
if (session)
|
||||
if (session) {
|
||||
session._onMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
_onClose(reason?: string) {
|
||||
this._closed = true;
|
||||
|
|
@ -85,9 +88,10 @@ export class CRConnection extends EventEmitter {
|
|||
}
|
||||
|
||||
close() {
|
||||
if (!this._closed)
|
||||
if (!this._closed) {
|
||||
this._transport.close();
|
||||
}
|
||||
}
|
||||
|
||||
async createBrowserSession(): Promise<CDPSession> {
|
||||
const { sessionId } = await this.rootSession.send('Target.attachToBrowserTarget');
|
||||
|
|
@ -140,8 +144,9 @@ export class CRSession extends EventEmitter {
|
|||
method: T,
|
||||
params?: Protocol.CommandParameters[T]
|
||||
): Promise<Protocol.CommandReturnValues[T]> {
|
||||
if (this._crashed || this._closed || this._connection._closed || this._connection._browserDisconnectedLogs)
|
||||
if (this._crashed || this._closed || this._connection._closed || this._connection._browserDisconnectedLogs) {
|
||||
throw new ProtocolError(this._crashed ? 'crashed' : 'closed', undefined, this._connection._browserDisconnectedLogs);
|
||||
}
|
||||
const id = this._connection._rawSend(this._sessionId, method, params);
|
||||
return new Promise((resolve, reject) => {
|
||||
this._callbacks.set(id, { resolve, reject, error: new ProtocolError('error', method) });
|
||||
|
|
@ -165,20 +170,23 @@ export class CRSession extends EventEmitter {
|
|||
} else if (object.id && object.error?.code === -32001) {
|
||||
// Message to a closed session, just ignore it.
|
||||
} else {
|
||||
assert(!object.id, object?.error?.message || undefined);
|
||||
assert(!object.id, object.error?.message || undefined);
|
||||
Promise.resolve().then(() => {
|
||||
if (this._eventListener)
|
||||
if (this._eventListener) {
|
||||
this._eventListener(object.method!, object.params);
|
||||
}
|
||||
this.emit(object.method!, object.params);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async detach() {
|
||||
if (this._closed)
|
||||
if (this._closed) {
|
||||
throw new Error(`Session already detached. Most likely the page has been closed.`);
|
||||
if (!this._parentSession)
|
||||
}
|
||||
if (!this._parentSession) {
|
||||
throw new Error('Root session cannot be closed');
|
||||
}
|
||||
// Ideally, detaching should resume any target, but there is a bug in the backend,
|
||||
// so we must Runtime.runIfWaitingForDebugger first.
|
||||
await this._sendMayFail('Runtime.runIfWaitingForDebugger');
|
||||
|
|
@ -214,8 +222,9 @@ export class CDPSession extends EventEmitter {
|
|||
this.guid = `cdp-session@${sessionId}`;
|
||||
this._session = parentSession.createChildSession(sessionId, (method, params) => this.emit(CDPSession.Events.Event, { method, params }));
|
||||
this._listeners = [eventsHelper.addEventListener(parentSession, 'Target.detachedFromTarget', (event: Protocol.Target.detachedFromTargetPayload) => {
|
||||
if (event.sessionId === sessionId)
|
||||
if (event.sessionId === sessionId) {
|
||||
this._onClose();
|
||||
}
|
||||
})];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,8 +95,9 @@ class JSCoverage {
|
|||
}
|
||||
|
||||
_onExecutionContextsCleared() {
|
||||
if (!this._resetOnNavigation)
|
||||
if (!this._resetOnNavigation) {
|
||||
return;
|
||||
}
|
||||
this._scriptIds.clear();
|
||||
this._scriptSources.clear();
|
||||
}
|
||||
|
|
@ -104,13 +105,15 @@ class JSCoverage {
|
|||
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) {
|
||||
this._scriptIds.add(event.scriptId);
|
||||
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
|
||||
if (!event.url && !this._reportAnonymousScripts)
|
||||
if (!event.url && !this._reportAnonymousScripts) {
|
||||
return;
|
||||
}
|
||||
// This might fail if the page has already navigated away.
|
||||
const response = await this._client._sendMayFail('Debugger.getScriptSource', { scriptId: event.scriptId });
|
||||
if (response)
|
||||
if (response) {
|
||||
this._scriptSources.set(event.scriptId, response.scriptSource);
|
||||
}
|
||||
}
|
||||
|
||||
async stop(): Promise<channels.PageStopJSCoverageResult> {
|
||||
assert(this._enabled, 'JSCoverage is not enabled');
|
||||
|
|
@ -125,16 +128,19 @@ class JSCoverage {
|
|||
|
||||
const coverage: channels.PageStopJSCoverageResult = { entries: [] };
|
||||
for (const entry of profileResponse.result) {
|
||||
if (!this._scriptIds.has(entry.scriptId))
|
||||
if (!this._scriptIds.has(entry.scriptId)) {
|
||||
continue;
|
||||
if (!entry.url && !this._reportAnonymousScripts)
|
||||
}
|
||||
if (!entry.url && !this._reportAnonymousScripts) {
|
||||
continue;
|
||||
}
|
||||
const source = this._scriptSources.get(entry.scriptId);
|
||||
if (source)
|
||||
if (source) {
|
||||
coverage.entries.push({ ...entry, source });
|
||||
else
|
||||
} else {
|
||||
coverage.entries.push(entry);
|
||||
}
|
||||
}
|
||||
return coverage;
|
||||
}
|
||||
}
|
||||
|
|
@ -175,8 +181,9 @@ class CSSCoverage {
|
|||
}
|
||||
|
||||
_onExecutionContextsCleared() {
|
||||
if (!this._resetOnNavigation)
|
||||
if (!this._resetOnNavigation) {
|
||||
return;
|
||||
}
|
||||
this._stylesheetURLs.clear();
|
||||
this._stylesheetSources.clear();
|
||||
}
|
||||
|
|
@ -184,8 +191,9 @@ class CSSCoverage {
|
|||
async _onStyleSheet(event: Protocol.CSS.styleSheetAddedPayload) {
|
||||
const header = event.header;
|
||||
// Ignore anonymous scripts
|
||||
if (!header.sourceURL)
|
||||
if (!header.sourceURL) {
|
||||
return;
|
||||
}
|
||||
// This might fail if the page has already navigated away.
|
||||
const response = await this._client._sendMayFail('CSS.getStyleSheetText', { styleSheetId: header.styleSheetId });
|
||||
if (response) {
|
||||
|
|
@ -243,16 +251,19 @@ function convertToDisjointRanges(nestedRanges: {
|
|||
// Sort points to form a valid parenthesis sequence.
|
||||
points.sort((a, b) => {
|
||||
// Sort with increasing offsets.
|
||||
if (a.offset !== b.offset)
|
||||
if (a.offset !== b.offset) {
|
||||
return a.offset - b.offset;
|
||||
}
|
||||
// All "end" points should go before "start" points.
|
||||
if (a.type !== b.type)
|
||||
if (a.type !== b.type) {
|
||||
return b.type - a.type;
|
||||
}
|
||||
const aLength = a.range.endOffset - a.range.startOffset;
|
||||
const bLength = b.range.endOffset - b.range.startOffset;
|
||||
// For two "start" points, the one with longer range goes first.
|
||||
if (a.type === 0)
|
||||
if (a.type === 0) {
|
||||
return bLength - aLength;
|
||||
}
|
||||
// For two "end" points, the one with shorter range goes first.
|
||||
return aLength - bLength;
|
||||
});
|
||||
|
|
@ -264,17 +275,19 @@ function convertToDisjointRanges(nestedRanges: {
|
|||
for (const point of points) {
|
||||
if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) {
|
||||
const lastResult = results.length ? results[results.length - 1] : null;
|
||||
if (lastResult && lastResult.end === lastOffset)
|
||||
if (lastResult && lastResult.end === lastOffset) {
|
||||
lastResult.end = point.offset;
|
||||
else
|
||||
} else {
|
||||
results.push({ start: lastOffset, end: point.offset });
|
||||
}
|
||||
}
|
||||
lastOffset = point.offset;
|
||||
if (point.type === 0)
|
||||
if (point.type === 0) {
|
||||
hitCountStack.push(point.range.count);
|
||||
else
|
||||
} else {
|
||||
hitCountStack.pop();
|
||||
}
|
||||
}
|
||||
// Filter out empty ranges.
|
||||
return results.filter(range => range.end - range.start > 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,14 @@ export class CRDevTools {
|
|||
|
||||
install(session: CRSession) {
|
||||
session.on('Runtime.bindingCalled', async event => {
|
||||
if (event.name !== kBindingName)
|
||||
if (event.name !== kBindingName) {
|
||||
return;
|
||||
}
|
||||
const parsed = JSON.parse(event.payload);
|
||||
let result = undefined;
|
||||
if (this.__testHookOnBinding)
|
||||
if (this.__testHookOnBinding) {
|
||||
this.__testHookOnBinding(parsed);
|
||||
}
|
||||
if (parsed.method === 'getPreferences') {
|
||||
if (this._prefs === undefined) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -34,8 +34,9 @@ export class DragManager {
|
|||
}
|
||||
|
||||
async cancelDrag() {
|
||||
if (!this._dragState)
|
||||
if (!this._dragState) {
|
||||
return false;
|
||||
}
|
||||
await this._crPage._mainFrameSession._client.send('Input.dispatchDragEvent', {
|
||||
type: 'dragCancel',
|
||||
x: this._lastPosition.x,
|
||||
|
|
@ -61,8 +62,9 @@ export class DragManager {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (button !== 'left')
|
||||
if (button !== 'left') {
|
||||
return moveCallback();
|
||||
}
|
||||
|
||||
const client = this._crPage._mainFrameSession._client;
|
||||
let onDragIntercepted: (payload: Protocol.Input.dragInterceptedPayload) => void;
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
contextId: this._contextId,
|
||||
returnByValue: true,
|
||||
}).catch(rewriteError);
|
||||
if (exceptionDetails)
|
||||
if (exceptionDetails) {
|
||||
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
|
||||
}
|
||||
return remoteObject.value;
|
||||
}
|
||||
|
||||
|
|
@ -48,8 +49,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
expression,
|
||||
contextId: this._contextId,
|
||||
}).catch(rewriteError);
|
||||
if (exceptionDetails)
|
||||
if (exceptionDetails) {
|
||||
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
|
||||
}
|
||||
return remoteObject.objectId!;
|
||||
}
|
||||
|
||||
|
|
@ -66,8 +68,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
awaitPromise: true,
|
||||
userGesture: true
|
||||
}).catch(rewriteError);
|
||||
if (exceptionDetails)
|
||||
if (exceptionDetails) {
|
||||
throw new js.JavaScriptErrorInEvaluate(getExceptionMessage(exceptionDetails));
|
||||
}
|
||||
return returnByValue ? parseEvaluationResultValue(remoteObject.value) : utilityScript._context.createHandle(remoteObject);
|
||||
}
|
||||
|
||||
|
|
@ -78,8 +81,9 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
});
|
||||
const result = new Map();
|
||||
for (const property of response.result) {
|
||||
if (!property.enumerable || !property.value)
|
||||
if (!property.enumerable || !property.value) {
|
||||
continue;
|
||||
}
|
||||
result.set(property.name, context.createHandle(property.value));
|
||||
}
|
||||
return result;
|
||||
|
|
@ -95,15 +99,19 @@ export class CRExecutionContext implements js.ExecutionContextDelegate {
|
|||
}
|
||||
|
||||
function rewriteError(error: Error): Protocol.Runtime.evaluateReturnValue {
|
||||
if (error.message.includes('Object reference chain is too long'))
|
||||
if (error.message.includes('Object reference chain is too long')) {
|
||||
throw new Error('Cannot serialize result: object reference chain is too long.');
|
||||
if (error.message.includes('Object couldn\'t be returned by value'))
|
||||
}
|
||||
if (error.message.includes('Object couldn\'t be returned by value')) {
|
||||
return { result: { type: 'undefined' } };
|
||||
}
|
||||
|
||||
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON'))
|
||||
if (error instanceof TypeError && error.message.startsWith('Converting circular structure to JSON')) {
|
||||
rewriteErrorMessage(error, error.message + ' Are you passing a nested JSHandle?');
|
||||
if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error))
|
||||
}
|
||||
if (!js.isJavaScriptErrorInEvaluate(error) && !isSessionClosedError(error)) {
|
||||
throw new Error('Execution context was destroyed, most likely because of a navigation.');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
|
@ -114,20 +122,25 @@ function potentiallyUnserializableValue(remoteObject: Protocol.Runtime.RemoteObj
|
|||
}
|
||||
|
||||
function renderPreview(object: Protocol.Runtime.RemoteObject): string | undefined {
|
||||
if (object.type === 'undefined')
|
||||
if (object.type === 'undefined') {
|
||||
return 'undefined';
|
||||
if ('value' in object)
|
||||
}
|
||||
if ('value' in object) {
|
||||
return String(object.value);
|
||||
if (object.unserializableValue)
|
||||
}
|
||||
if (object.unserializableValue) {
|
||||
return String(object.unserializableValue);
|
||||
}
|
||||
|
||||
if (object.description === 'Object' && object.preview) {
|
||||
const tokens = [];
|
||||
for (const { name, value } of object.preview.properties)
|
||||
for (const { name, value } of object.preview.properties) {
|
||||
tokens.push(`${name}: ${value}`);
|
||||
}
|
||||
return `{${tokens.join(', ')}}`;
|
||||
}
|
||||
if (object.subtype === 'array' && object.preview)
|
||||
if (object.subtype === 'array' && object.preview) {
|
||||
return js.sparseArrayToString(object.preview.properties);
|
||||
}
|
||||
return object.description;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,18 +32,21 @@ export class RawKeyboardImpl implements input.RawKeyboard {
|
|||
) { }
|
||||
|
||||
_commandsForCode(code: string, modifiers: Set<types.KeyboardModifier>) {
|
||||
if (!this._isMac)
|
||||
if (!this._isMac) {
|
||||
return [];
|
||||
}
|
||||
const parts = [];
|
||||
for (const modifier of (['Shift', 'Control', 'Alt', 'Meta']) as types.KeyboardModifier[]) {
|
||||
if (modifiers.has(modifier))
|
||||
if (modifiers.has(modifier)) {
|
||||
parts.push(modifier);
|
||||
}
|
||||
}
|
||||
parts.push(code);
|
||||
const shortcut = parts.join('+');
|
||||
let commands = macEditingCommands[shortcut] || [];
|
||||
if (isString(commands))
|
||||
if (isString(commands)) {
|
||||
commands = [commands];
|
||||
}
|
||||
// Commands that insert text are not supported
|
||||
commands = commands.filter(x => !x.startsWith('insert'));
|
||||
// remove the trailing : to match the Chromium command names.
|
||||
|
|
@ -51,8 +54,9 @@ export class RawKeyboardImpl implements input.RawKeyboard {
|
|||
}
|
||||
|
||||
async keydown(modifiers: Set<types.KeyboardModifier>, code: string, keyCode: number, keyCodeWithoutLocation: number, key: string, location: number, autoRepeat: boolean, text: string | undefined): Promise<void> {
|
||||
if (code === 'Escape' && await this._dragManger.cancelDrag())
|
||||
if (code === 'Escape' && await this._dragManger.cancelDrag()) {
|
||||
return;
|
||||
}
|
||||
const commands = this._commandsForCode(code, modifiers);
|
||||
await this._client.send('Input.dispatchKeyEvent', {
|
||||
type: text ? 'keyDown' : 'rawKeyDown',
|
||||
|
|
@ -116,8 +120,9 @@ export class RawMouseImpl implements input.RawMouse {
|
|||
}
|
||||
|
||||
async down(x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {
|
||||
if (this._dragManager.isDragging())
|
||||
if (this._dragManager.isDragging()) {
|
||||
return;
|
||||
}
|
||||
await this._client.send('Input.dispatchMouseEvent', {
|
||||
type: 'mousePressed',
|
||||
button,
|
||||
|
|
|
|||
|
|
@ -92,19 +92,22 @@ export class CRNetworkManager {
|
|||
|
||||
removeSession(session: CRSession) {
|
||||
const info = this._sessions.get(session);
|
||||
if (info)
|
||||
if (info) {
|
||||
eventsHelper.removeEventListeners(info.eventListeners);
|
||||
}
|
||||
this._sessions.delete(session);
|
||||
}
|
||||
|
||||
private async _forEachSession(cb: (sessionInfo: SessionInfo) => Promise<any>) {
|
||||
await Promise.all([...this._sessions.values()].map(info => {
|
||||
if (info.isMain)
|
||||
if (info.isMain) {
|
||||
return cb(info);
|
||||
}
|
||||
return cb(info).catch(e => {
|
||||
// Broadcasting a message to the closed target should be a noop.
|
||||
if (isSessionClosedError(e))
|
||||
if (isSessionClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}));
|
||||
|
|
@ -116,18 +119,21 @@ export class CRNetworkManager {
|
|||
}
|
||||
|
||||
async setOffline(offline: boolean) {
|
||||
if (offline === this._offline)
|
||||
if (offline === this._offline) {
|
||||
return;
|
||||
}
|
||||
this._offline = offline;
|
||||
await this._forEachSession(info => this._setOfflineForSession(info));
|
||||
}
|
||||
|
||||
private async _setOfflineForSession(info: SessionInfo, initial?: boolean) {
|
||||
if (initial && !this._offline)
|
||||
if (initial && !this._offline) {
|
||||
return;
|
||||
}
|
||||
// Workers are affected by the owner frame's Network.emulateNetworkConditions.
|
||||
if (info.workerFrame)
|
||||
if (info.workerFrame) {
|
||||
return;
|
||||
}
|
||||
await info.session.send('Network.emulateNetworkConditions', {
|
||||
offline: this._offline,
|
||||
// values of 0 remove any active throttling. crbug.com/456324#c9
|
||||
|
|
@ -144,37 +150,42 @@ export class CRNetworkManager {
|
|||
|
||||
async _updateProtocolRequestInterception() {
|
||||
const enabled = this._userRequestInterceptionEnabled || !!this._credentials;
|
||||
if (enabled === this._protocolRequestInterceptionEnabled)
|
||||
if (enabled === this._protocolRequestInterceptionEnabled) {
|
||||
return;
|
||||
}
|
||||
this._protocolRequestInterceptionEnabled = enabled;
|
||||
await this._forEachSession(info => this._updateProtocolRequestInterceptionForSession(info));
|
||||
}
|
||||
|
||||
private async _updateProtocolRequestInterceptionForSession(info: SessionInfo, initial?: boolean) {
|
||||
const enabled = this._protocolRequestInterceptionEnabled;
|
||||
if (initial && !enabled)
|
||||
if (initial && !enabled) {
|
||||
return;
|
||||
}
|
||||
const cachePromise = info.session.send('Network.setCacheDisabled', { cacheDisabled: enabled });
|
||||
let fetchPromise = Promise.resolve<any>(undefined);
|
||||
if (!info.workerFrame) {
|
||||
if (enabled)
|
||||
if (enabled) {
|
||||
fetchPromise = info.session.send('Fetch.enable', { handleAuthRequests: true, patterns: [{ urlPattern: '*', requestStage: 'Request' }] });
|
||||
else
|
||||
} else {
|
||||
fetchPromise = info.session.send('Fetch.disable');
|
||||
}
|
||||
}
|
||||
await Promise.all([cachePromise, fetchPromise]);
|
||||
}
|
||||
|
||||
async setExtraHTTPHeaders(extraHTTPHeaders: types.HeadersArray) {
|
||||
if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length)
|
||||
if (!this._extraHTTPHeaders.length && !extraHTTPHeaders.length) {
|
||||
return;
|
||||
}
|
||||
this._extraHTTPHeaders = extraHTTPHeaders;
|
||||
await this._forEachSession(info => this._setExtraHTTPHeadersForSession(info));
|
||||
}
|
||||
|
||||
private async _setExtraHTTPHeadersForSession(info: SessionInfo, initial?: boolean) {
|
||||
if (initial && !this._extraHTTPHeaders.length)
|
||||
if (initial && !this._extraHTTPHeaders.length) {
|
||||
return;
|
||||
}
|
||||
await info.session.send('Network.setExtraHTTPHeaders', { headers: headersArrayToObject(this._extraHTTPHeaders, false /* lowerCase */) });
|
||||
}
|
||||
|
||||
|
|
@ -182,10 +193,12 @@ export class CRNetworkManager {
|
|||
await this._forEachSession(async info => {
|
||||
// Sending 'Network.setCacheDisabled' with 'cacheDisabled = true' will clear the MemoryCache.
|
||||
await info.session.send('Network.setCacheDisabled', { cacheDisabled: true });
|
||||
if (!this._protocolRequestInterceptionEnabled)
|
||||
if (!this._protocolRequestInterceptionEnabled) {
|
||||
await info.session.send('Network.setCacheDisabled', { cacheDisabled: false });
|
||||
if (!info.workerFrame)
|
||||
}
|
||||
if (!info.workerFrame) {
|
||||
await info.session.send('Network.clearBrowserCache');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -230,8 +243,9 @@ export class CRNetworkManager {
|
|||
}
|
||||
|
||||
_shouldProvideCredentials(url: string): boolean {
|
||||
if (!this._credentials)
|
||||
if (!this._credentials) {
|
||||
return false;
|
||||
}
|
||||
return !this._credentials.origin || new URL(url).origin.toLowerCase() === this._credentials.origin.toLowerCase();
|
||||
}
|
||||
|
||||
|
|
@ -242,8 +256,9 @@ export class CRNetworkManager {
|
|||
sessionInfo.session._sendMayFail('Fetch.continueRequest', { requestId: event.requestId });
|
||||
return;
|
||||
}
|
||||
if (event.request.url.startsWith('data:'))
|
||||
if (event.request.url.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = event.networkId;
|
||||
const requestWillBeSentEvent = this._requestIdToRequestWillBeSentEvent.get(requestId);
|
||||
|
|
@ -274,8 +289,9 @@ export class CRNetworkManager {
|
|||
}
|
||||
|
||||
_onRequest(requestWillBeSentSessionInfo: SessionInfo, requestWillBeSentEvent: Protocol.Network.requestWillBeSentPayload, requestPausedSessionInfo: SessionInfo | undefined, requestPausedEvent: Protocol.Fetch.requestPausedPayload | undefined) {
|
||||
if (requestWillBeSentEvent.request.url.startsWith('data:'))
|
||||
if (requestWillBeSentEvent.request.url.startsWith('data:')) {
|
||||
return;
|
||||
}
|
||||
let redirectedFrom: InterceptableRequest | null = null;
|
||||
if (requestWillBeSentEvent.redirectResponse) {
|
||||
const request = this._requestIdToRequest.get(requestWillBeSentEvent.requestId);
|
||||
|
|
@ -289,11 +305,12 @@ export class CRNetworkManager {
|
|||
// Requests from workers lack frameId, because we receive Network.requestWillBeSent
|
||||
// on the worker target. However, we receive Fetch.requestPaused on the page target,
|
||||
// and lack workerFrame there. Luckily, Fetch.requestPaused provides a frameId.
|
||||
if (!frame && this._page && requestPausedEvent && requestPausedEvent.frameId)
|
||||
if (!frame && this._page && requestPausedEvent && requestPausedEvent.frameId) {
|
||||
frame = this._page._frameManager.frame(requestPausedEvent.frameId);
|
||||
}
|
||||
|
||||
// Check if it's main resource request interception (targetId === main frame id).
|
||||
if (!frame && this._page && requestWillBeSentEvent.frameId === (this._page?._delegate as CRPage)._targetId) {
|
||||
if (!frame && this._page && requestWillBeSentEvent.frameId === (this._page._delegate as CRPage)._targetId) {
|
||||
// Main resource request for the page is being intercepted so the Frame is not created
|
||||
// yet. Precreate it here for the purposes of request interception. It will be updated
|
||||
// later as soon as the request continues and we receive frame tree from the page.
|
||||
|
|
@ -312,8 +329,9 @@ export class CRNetworkManager {
|
|||
{ name: 'Access-Control-Allow-Methods', value: requestHeaders['Access-Control-Request-Method'] || 'GET, POST, OPTIONS, DELETE' },
|
||||
{ name: 'Access-Control-Allow-Credentials', value: 'true' }
|
||||
];
|
||||
if (requestHeaders['Access-Control-Request-Headers'])
|
||||
if (requestHeaders['Access-Control-Request-Headers']) {
|
||||
responseHeaders.push({ name: 'Access-Control-Allow-Headers', value: requestHeaders['Access-Control-Request-Headers'] });
|
||||
}
|
||||
requestPausedSessionInfo!.session._sendMayFail('Fetch.fulfillRequest', {
|
||||
requestId: requestPausedEvent.requestId,
|
||||
responseCode: 204,
|
||||
|
|
@ -326,8 +344,9 @@ export class CRNetworkManager {
|
|||
|
||||
// Non-service-worker requests MUST have a frame—if they don't, we pretend there was no request
|
||||
if (!frame && !this._serviceWorker) {
|
||||
if (requestPausedEvent)
|
||||
if (requestPausedEvent) {
|
||||
requestPausedSessionInfo!.session._sendMayFail('Fetch.continueRequest', { requestId: requestPausedEvent.requestId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -375,12 +394,14 @@ export class CRNetworkManager {
|
|||
|
||||
const session = request.session;
|
||||
const response = await session.send('Network.getResponseBody', { requestId: request._requestId });
|
||||
if (response.body || !expectedLength)
|
||||
if (response.body || !expectedLength) {
|
||||
return Buffer.from(response.body, response.base64Encoded ? 'base64' : 'utf8');
|
||||
}
|
||||
|
||||
// Make sure no network requests sent while reading the body for fulfilled requests.
|
||||
if (request._route?._fulfilled)
|
||||
if (request._route?._fulfilled) {
|
||||
return Buffer.from('');
|
||||
}
|
||||
|
||||
// For <link prefetch we are going to receive empty body with non-empty content-length expectation. Reach out for the actual content.
|
||||
const resource = await session.send('Network.loadNetworkResource', { url: request.request.url(), frameId: this._serviceWorker ? undefined : request.request.frame()!._id, options: { disableCache: false, includeCredentials: true } });
|
||||
|
|
@ -421,7 +442,7 @@ export class CRNetworkManager {
|
|||
};
|
||||
}
|
||||
const response = new network.Response(request.request, responsePayload.status, responsePayload.statusText, headersObjectToArray(responsePayload.headers), timing, getResponseBody, !!responsePayload.fromServiceWorker, responsePayload.protocol);
|
||||
if (responsePayload?.remoteIPAddress && typeof responsePayload?.remotePort === 'number') {
|
||||
if (responsePayload.remoteIPAddress && typeof responsePayload.remotePort === 'number') {
|
||||
response._serverAddrFinished({
|
||||
ipAddress: responsePayload.remoteIPAddress,
|
||||
port: responsePayload.remotePort,
|
||||
|
|
@ -430,11 +451,11 @@ export class CRNetworkManager {
|
|||
response._serverAddrFinished();
|
||||
}
|
||||
response._securityDetailsFinished({
|
||||
protocol: responsePayload?.securityDetails?.protocol,
|
||||
subjectName: responsePayload?.securityDetails?.subjectName,
|
||||
issuer: responsePayload?.securityDetails?.issuer,
|
||||
validFrom: responsePayload?.securityDetails?.validFrom,
|
||||
validTo: responsePayload?.securityDetails?.validTo,
|
||||
protocol: responsePayload.securityDetails?.protocol,
|
||||
subjectName: responsePayload.securityDetails?.subjectName,
|
||||
issuer: responsePayload.securityDetails?.issuer,
|
||||
validFrom: responsePayload.securityDetails?.validFrom,
|
||||
validTo: responsePayload.securityDetails?.validTo,
|
||||
});
|
||||
this._responseExtraInfoTracker.processResponse(request._requestId, response, hasExtraInfo);
|
||||
return response;
|
||||
|
|
@ -442,9 +463,10 @@ export class CRNetworkManager {
|
|||
|
||||
_deleteRequest(request: InterceptableRequest) {
|
||||
this._requestIdToRequest.delete(request._requestId);
|
||||
if (request._interceptionId)
|
||||
if (request._interceptionId) {
|
||||
this._attemptedAuthentications.delete(request._interceptionId);
|
||||
}
|
||||
}
|
||||
|
||||
_handleRequestRedirect(request: InterceptableRequest, responsePayload: Protocol.Network.Response, timestamp: number, hasExtraInfo: boolean) {
|
||||
const response = this._createResponse(request, responsePayload, hasExtraInfo);
|
||||
|
|
@ -474,8 +496,9 @@ export class CRNetworkManager {
|
|||
}
|
||||
}
|
||||
// FileUpload sends a response without a matching request.
|
||||
if (!request)
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
const response = this._createResponse(request, event.response, event.hasExtraInfo);
|
||||
(this._page?._frameManager || this._serviceWorker)!.requestReceivedResponse(response);
|
||||
}
|
||||
|
|
@ -486,8 +509,9 @@ export class CRNetworkManager {
|
|||
const request = this._requestIdToRequest.get(event.requestId);
|
||||
// For certain requestIds we never receive requestWillBeSent event.
|
||||
// @see https://crbug.com/750469
|
||||
if (!request)
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
|
||||
|
||||
// Under certain conditions we never get the Network.responseReceived
|
||||
|
|
@ -521,8 +545,9 @@ export class CRNetworkManager {
|
|||
|
||||
// For certain requestIds we never receive requestWillBeSent event.
|
||||
// @see https://crbug.com/750469
|
||||
if (!request)
|
||||
if (!request) {
|
||||
return;
|
||||
}
|
||||
this._maybeUpdateOOPIFMainRequest(sessionInfo, request);
|
||||
const response = request.request._existingResponse();
|
||||
if (response) {
|
||||
|
|
@ -542,10 +567,11 @@ export class CRNetworkManager {
|
|||
// OOPIF has a main request that starts in the parent session but finishes in the child session.
|
||||
// We check for the main request by matching loaderId and requestId, and if it now belongs to
|
||||
// a child session, migrate it there.
|
||||
if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId)
|
||||
if (request.session !== sessionInfo.session && !sessionInfo.isMain && request._documentId === request._requestId) {
|
||||
request.session = sessionInfo.session;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InterceptableRequest {
|
||||
readonly request: network.Request;
|
||||
|
|
@ -591,8 +617,9 @@ class InterceptableRequest {
|
|||
const type = (requestWillBeSentEvent.type || '').toLowerCase();
|
||||
let postDataBuffer = null;
|
||||
const entries = postDataEntries?.filter(entry => entry.bytes);
|
||||
if (entries && entries.length)
|
||||
if (entries && entries.length) {
|
||||
postDataBuffer = Buffer.concat(entries.map(entry => Buffer.from(entry.bytes!, 'base64')));
|
||||
}
|
||||
|
||||
this.request = new network.Request(context, frame, serviceWorker, redirectedFrom?.request || null, documentId, url, type, method, postDataBuffer, headersOverride || headersObjectToArray(headers));
|
||||
}
|
||||
|
|
@ -656,21 +683,24 @@ async function catchDisallowedErrors(callback: () => Promise<void>) {
|
|||
try {
|
||||
return await callback();
|
||||
} catch (e) {
|
||||
if (isProtocolError(e) && e.message.includes('Invalid http status code or phrase'))
|
||||
if (isProtocolError(e) && e.message.includes('Invalid http status code or phrase')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function splitSetCookieHeader(headers: types.HeadersArray): types.HeadersArray {
|
||||
const index = headers.findIndex(({ name }) => name.toLowerCase() === 'set-cookie');
|
||||
if (index === -1)
|
||||
if (index === -1) {
|
||||
return headers;
|
||||
}
|
||||
|
||||
const header = headers[index];
|
||||
const values = header.value.split('\n');
|
||||
if (values.length === 1)
|
||||
if (values.length === 1) {
|
||||
return headers;
|
||||
}
|
||||
const result = headers.slice();
|
||||
result.splice(index, 1, ...values.map(value => ({ name: header.name, value })));
|
||||
return result;
|
||||
|
|
@ -767,16 +797,18 @@ class ResponseExtraInfoTracker {
|
|||
|
||||
loadingFinished(event: Protocol.Network.loadingFinishedPayload) {
|
||||
const info = this._requests.get(event.requestId);
|
||||
if (!info)
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
info.loadingFinished = event;
|
||||
this._checkFinished(info);
|
||||
}
|
||||
|
||||
loadingFailed(event: Protocol.Network.loadingFailedPayload) {
|
||||
const info = this._requests.get(event.requestId);
|
||||
if (!info)
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
info.loadingFailed = event;
|
||||
this._checkFinished(info);
|
||||
}
|
||||
|
|
@ -811,8 +843,9 @@ class ResponseExtraInfoTracker {
|
|||
}
|
||||
|
||||
private _checkFinished(info: RequestInfo) {
|
||||
if (!info.loadingFinished && !info.loadingFailed)
|
||||
if (!info.loadingFinished && !info.loadingFailed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.responses.length <= info.responseReceivedExtraInfo.length) {
|
||||
// We have extra info for each response.
|
||||
|
|
|
|||
|
|
@ -103,9 +103,10 @@ export class CRPage implements PageDelegate {
|
|||
if (opener && !browserContext._options.noDefaultViewport) {
|
||||
const features = opener._nextWindowOpenPopupFeatures.shift() || [];
|
||||
const viewportSize = helper.getViewportSizeFromWindowFeatures(features);
|
||||
if (viewportSize)
|
||||
if (viewportSize) {
|
||||
this._page._emulatedSize = { viewport: viewportSize, screen: viewportSize };
|
||||
}
|
||||
}
|
||||
|
||||
const createdEvent = this._isBackgroundPage ? CRBrowserContext.CREvents.BackgroundPage : BrowserContext.Events.Page;
|
||||
this._mainFrameSession._initialize(bits.hasUIWindow).then(
|
||||
|
|
@ -116,12 +117,14 @@ export class CRPage implements PageDelegate {
|
|||
private async _forAllFrameSessions(cb: (frame: FrameSession) => Promise<any>) {
|
||||
const frameSessions = Array.from(this._sessions.values());
|
||||
await Promise.all(frameSessions.map(frameSession => {
|
||||
if (frameSession._isMainFrame())
|
||||
if (frameSession._isMainFrame()) {
|
||||
return cb(frameSession);
|
||||
}
|
||||
return cb(frameSession).catch(e => {
|
||||
// Broadcasting a message to the closed iframe should be a noop.
|
||||
if (isSessionClosedError(e))
|
||||
if (isSessionClosedError(e)) {
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}));
|
||||
|
|
@ -131,8 +134,9 @@ export class CRPage implements PageDelegate {
|
|||
// Frame id equals target id.
|
||||
while (!this._sessions.has(frame._id)) {
|
||||
const parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
if (!parent) {
|
||||
throw new Error(`Frame has been detached.`);
|
||||
}
|
||||
frame = parent;
|
||||
}
|
||||
return this._sessions.get(frame._id)!;
|
||||
|
|
@ -148,8 +152,9 @@ export class CRPage implements PageDelegate {
|
|||
}
|
||||
|
||||
didClose() {
|
||||
for (const session of this._sessions.values())
|
||||
for (const session of this._sessions.values()) {
|
||||
session.dispose();
|
||||
}
|
||||
this._page._didClose();
|
||||
}
|
||||
|
||||
|
|
@ -208,8 +213,9 @@ export class CRPage implements PageDelegate {
|
|||
private async _go(delta: number): Promise<boolean> {
|
||||
const history = await this._mainFrameSession._client.send('Page.getNavigationHistory');
|
||||
const entry = history.entries[history.currentIndex + delta];
|
||||
if (!entry)
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
await this._mainFrameSession._client.send('Page.navigateToHistoryEntry', { entryId: entry.id });
|
||||
return true;
|
||||
}
|
||||
|
|
@ -235,11 +241,12 @@ export class CRPage implements PageDelegate {
|
|||
}
|
||||
|
||||
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||
if (runBeforeUnload)
|
||||
if (runBeforeUnload) {
|
||||
await this._mainFrameSession._client.send('Page.close');
|
||||
else
|
||||
} else {
|
||||
await this._browserContext._browser._closePage(this);
|
||||
}
|
||||
}
|
||||
|
||||
async setBackgroundColor(color?: { r: number; g: number; b: number; a: number; }): Promise<void> {
|
||||
await this._mainFrameSession._client.send('Emulation.setDefaultBackgroundColorOverride', { color });
|
||||
|
|
@ -317,8 +324,9 @@ export class CRPage implements PageDelegate {
|
|||
|
||||
async setInputFilePaths(handle: dom.ElementHandle<HTMLInputElement>, files: string[]): Promise<void> {
|
||||
const frame = await handle.ownerFrame();
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
throw new Error('Cannot set input files to detached input element');
|
||||
}
|
||||
const parentSession = this._sessionForFrame(frame);
|
||||
await parentSession._client.send('DOM.setFileInputFiles', {
|
||||
objectId: handle._objectId,
|
||||
|
|
@ -353,17 +361,20 @@ export class CRPage implements PageDelegate {
|
|||
|
||||
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
|
||||
let parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
if (!parent) {
|
||||
throw new Error('Frame has been detached.');
|
||||
}
|
||||
const parentSession = this._sessionForFrame(parent);
|
||||
const { backendNodeId } = await parentSession._client.send('DOM.getFrameOwner', { frameId: frame._id }).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('Frame with the given id was not found.'))
|
||||
if (e instanceof Error && e.message.includes('Frame with the given id was not found.')) {
|
||||
rewriteErrorMessage(e, 'Frame has been detached.');
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
parent = frame.parentFrame();
|
||||
if (!parent)
|
||||
if (!parent) {
|
||||
throw new Error('Frame has been detached.');
|
||||
}
|
||||
return parentSession._adoptBackendNodeId(backendNodeId, await parent._mainContext());
|
||||
}
|
||||
|
||||
|
|
@ -401,8 +412,9 @@ class FrameSession {
|
|||
this._page = crPage._page;
|
||||
this._targetId = targetId;
|
||||
this._parentSession = parentSession;
|
||||
if (parentSession)
|
||||
if (parentSession) {
|
||||
parentSession._childSessions.add(this);
|
||||
}
|
||||
this._firstNonInitialNavigationCommittedPromise = new Promise((f, r) => {
|
||||
this._firstNonInitialNavigationCommittedFulfill = f;
|
||||
this._firstNonInitialNavigationCommittedReject = r;
|
||||
|
|
@ -468,14 +480,16 @@ class FrameSession {
|
|||
// and it is equally important to send Page.startScreencast before sending Runtime.runIfWaitingForDebugger.
|
||||
await this._createVideoRecorder(screencastId, screencastOptions);
|
||||
this._crPage._page.waitForInitializedOrError().then(p => {
|
||||
if (p instanceof Error)
|
||||
if (p instanceof Error) {
|
||||
this._stopVideoRecording().catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let lifecycleEventsEnabled: Promise<any>;
|
||||
if (!this._isMainFrame())
|
||||
if (!this._isMainFrame()) {
|
||||
this._addRendererListeners();
|
||||
}
|
||||
this._addBrowserListeners();
|
||||
const promises: Promise<any>[] = [
|
||||
this._client.send('Page.enable'),
|
||||
|
|
@ -493,9 +507,10 @@ class FrameSession {
|
|||
grantUniveralAccess: true,
|
||||
worldName: UTILITY_WORLD_NAME,
|
||||
});
|
||||
for (const initScript of this._crPage._page.allInitScripts())
|
||||
for (const initScript of this._crPage._page.allInitScripts()) {
|
||||
frame.evaluateExpression(initScript.source).catch(e => {});
|
||||
}
|
||||
}
|
||||
|
||||
const isInitialEmptyPage = this._isMainFrame() && this._page.mainFrame().url() === ':';
|
||||
if (isInitialEmptyPage) {
|
||||
|
|
@ -522,35 +537,47 @@ class FrameSession {
|
|||
this._client.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }),
|
||||
];
|
||||
if (!isSettingStorageState) {
|
||||
if (this._isMainFrame())
|
||||
if (this._isMainFrame()) {
|
||||
promises.push(this._client.send('Emulation.setFocusEmulationEnabled', { enabled: true }));
|
||||
}
|
||||
const options = this._crPage._browserContext._options;
|
||||
if (options.bypassCSP)
|
||||
if (options.bypassCSP) {
|
||||
promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
|
||||
if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors)
|
||||
}
|
||||
if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors) {
|
||||
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true }));
|
||||
if (this._isMainFrame())
|
||||
}
|
||||
if (this._isMainFrame()) {
|
||||
promises.push(this._updateViewport());
|
||||
if (options.hasTouch)
|
||||
}
|
||||
if (options.hasTouch) {
|
||||
promises.push(this._client.send('Emulation.setTouchEmulationEnabled', { enabled: true }));
|
||||
if (options.javaScriptEnabled === false)
|
||||
}
|
||||
if (options.javaScriptEnabled === false) {
|
||||
promises.push(this._client.send('Emulation.setScriptExecutionDisabled', { value: true }));
|
||||
if (options.userAgent || options.locale)
|
||||
}
|
||||
if (options.userAgent || options.locale) {
|
||||
promises.push(this._updateUserAgent());
|
||||
if (options.locale)
|
||||
}
|
||||
if (options.locale) {
|
||||
promises.push(emulateLocale(this._client, options.locale));
|
||||
if (options.timezoneId)
|
||||
}
|
||||
if (options.timezoneId) {
|
||||
promises.push(emulateTimezone(this._client, options.timezoneId));
|
||||
if (!this._crPage._browserContext._browser.options.headful)
|
||||
}
|
||||
if (!this._crPage._browserContext._browser.options.headful) {
|
||||
promises.push(this._setDefaultFontFamilies(this._client));
|
||||
}
|
||||
promises.push(this._updateGeolocation(true));
|
||||
promises.push(this._updateEmulateMedia());
|
||||
promises.push(this._updateFileChooserInterception(true));
|
||||
for (const initScript of this._crPage._page.allInitScripts())
|
||||
for (const initScript of this._crPage._page.allInitScripts()) {
|
||||
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
|
||||
if (screencastOptions)
|
||||
}
|
||||
if (screencastOptions) {
|
||||
promises.push(this._startVideoRecording(screencastOptions));
|
||||
}
|
||||
}
|
||||
promises.push(this._client.send('Runtime.runIfWaitingForDebugger'));
|
||||
promises.push(this._firstNonInitialNavigationCommittedPromise);
|
||||
await Promise.all(promises);
|
||||
|
|
@ -558,10 +585,12 @@ class FrameSession {
|
|||
|
||||
dispose() {
|
||||
this._firstNonInitialNavigationCommittedReject(new TargetClosedError());
|
||||
for (const childSession of this._childSessions)
|
||||
for (const childSession of this._childSessions) {
|
||||
childSession.dispose();
|
||||
if (this._parentSession)
|
||||
}
|
||||
if (this._parentSession) {
|
||||
this._parentSession._childSessions.delete(this);
|
||||
}
|
||||
eventsHelper.removeEventListeners(this._eventListeners);
|
||||
this._crPage._networkManager.removeSession(this._client);
|
||||
this._crPage._sessions.delete(this._targetId);
|
||||
|
|
@ -570,35 +599,41 @@ class FrameSession {
|
|||
|
||||
async _navigate(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult> {
|
||||
const response = await this._client.send('Page.navigate', { url, referrer, frameId: frame._id, referrerPolicy: 'unsafeUrl' });
|
||||
if (response.errorText)
|
||||
if (response.errorText) {
|
||||
throw new frames.NavigationAbortedError(response.loaderId, `${response.errorText} at ${url}`);
|
||||
}
|
||||
return { newDocumentId: response.loaderId };
|
||||
}
|
||||
|
||||
_onLifecycleEvent(event: Protocol.Page.lifecycleEventPayload) {
|
||||
if (this._eventBelongsToStaleFrame(event.frameId))
|
||||
if (this._eventBelongsToStaleFrame(event.frameId)) {
|
||||
return;
|
||||
if (event.name === 'load')
|
||||
}
|
||||
if (event.name === 'load') {
|
||||
this._page._frameManager.frameLifecycleEvent(event.frameId, 'load');
|
||||
else if (event.name === 'DOMContentLoaded')
|
||||
} else if (event.name === 'DOMContentLoaded') {
|
||||
this._page._frameManager.frameLifecycleEvent(event.frameId, 'domcontentloaded');
|
||||
}
|
||||
}
|
||||
|
||||
_handleFrameTree(frameTree: Protocol.Page.FrameTree) {
|
||||
this._onFrameAttached(frameTree.frame.id, frameTree.frame.parentId || null);
|
||||
this._onFrameNavigated(frameTree.frame, true);
|
||||
if (!frameTree.childFrames)
|
||||
if (!frameTree.childFrames) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const child of frameTree.childFrames)
|
||||
for (const child of frameTree.childFrames) {
|
||||
this._handleFrameTree(child);
|
||||
}
|
||||
}
|
||||
|
||||
private _eventBelongsToStaleFrame(frameId: string) {
|
||||
const frame = this._page._frameManager.frame(frameId);
|
||||
// Subtree may be already gone because some ancestor navigation destroyed the oopif.
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
return true;
|
||||
}
|
||||
// When frame goes remote, parent process may still send some events
|
||||
// related to the local frame before it sends frameDetached.
|
||||
// In this case, we already have a new session for this frame, so events
|
||||
|
|
@ -614,8 +649,9 @@ class FrameSession {
|
|||
frameSession._swappedIn = true;
|
||||
const frame = this._page._frameManager.frame(frameId);
|
||||
// Frame or even a whole subtree may be already gone, because some ancestor did navigate.
|
||||
if (frame)
|
||||
if (frame) {
|
||||
this._page._frameManager.removeChildFramesRecursively(frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parentFrameId && !this._page._frameManager.frame(parentFrameId)) {
|
||||
|
|
@ -629,23 +665,28 @@ class FrameSession {
|
|||
}
|
||||
|
||||
_onFrameNavigated(framePayload: Protocol.Page.Frame, initial: boolean) {
|
||||
if (this._eventBelongsToStaleFrame(framePayload.id))
|
||||
if (this._eventBelongsToStaleFrame(framePayload.id)) {
|
||||
return;
|
||||
}
|
||||
this._page._frameManager.frameCommittedNewDocumentNavigation(framePayload.id, framePayload.url + (framePayload.urlFragment || ''), framePayload.name || '', framePayload.loaderId, initial);
|
||||
if (!initial)
|
||||
if (!initial) {
|
||||
this._firstNonInitialNavigationCommittedFulfill();
|
||||
}
|
||||
}
|
||||
|
||||
_onFrameRequestedNavigation(payload: Protocol.Page.frameRequestedNavigationPayload) {
|
||||
if (this._eventBelongsToStaleFrame(payload.frameId))
|
||||
if (this._eventBelongsToStaleFrame(payload.frameId)) {
|
||||
return;
|
||||
if (payload.disposition === 'currentTab')
|
||||
}
|
||||
if (payload.disposition === 'currentTab') {
|
||||
this._page._frameManager.frameRequestedNavigation(payload.frameId);
|
||||
}
|
||||
}
|
||||
|
||||
_onFrameNavigatedWithinDocument(frameId: string, url: string) {
|
||||
if (this._eventBelongsToStaleFrame(frameId))
|
||||
if (this._eventBelongsToStaleFrame(frameId)) {
|
||||
return;
|
||||
}
|
||||
this._page._frameManager.frameCommittedSameDocumentNavigation(frameId, url);
|
||||
}
|
||||
|
||||
|
|
@ -661,8 +702,9 @@ class FrameSession {
|
|||
// Page.frameDetached arrives before Target.attachedToTarget.
|
||||
// We should keep the frame in the tree, and it will be used for the new target.
|
||||
const frame = this._page._frameManager.frame(frameId);
|
||||
if (frame)
|
||||
if (frame) {
|
||||
this._page._frameManager.removeChildFramesRecursively(frame);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Just a regular frame detach.
|
||||
|
|
@ -671,33 +713,38 @@ class FrameSession {
|
|||
|
||||
_onExecutionContextCreated(contextPayload: Protocol.Runtime.ExecutionContextDescription) {
|
||||
const frame = contextPayload.auxData ? this._page._frameManager.frame(contextPayload.auxData.frameId) : null;
|
||||
if (!frame || this._eventBelongsToStaleFrame(frame._id))
|
||||
if (!frame || this._eventBelongsToStaleFrame(frame._id)) {
|
||||
return;
|
||||
}
|
||||
const delegate = new CRExecutionContext(this._client, contextPayload);
|
||||
let worldName: types.World|null = null;
|
||||
if (contextPayload.auxData && !!contextPayload.auxData.isDefault)
|
||||
if (contextPayload.auxData && !!contextPayload.auxData.isDefault) {
|
||||
worldName = 'main';
|
||||
else if (contextPayload.name === UTILITY_WORLD_NAME)
|
||||
} else if (contextPayload.name === UTILITY_WORLD_NAME) {
|
||||
worldName = 'utility';
|
||||
}
|
||||
const context = new dom.FrameExecutionContext(delegate, frame, worldName);
|
||||
(context as any)[contextDelegateSymbol] = delegate;
|
||||
if (worldName)
|
||||
if (worldName) {
|
||||
frame._contextCreated(worldName, context);
|
||||
}
|
||||
this._contextIdToContext.set(contextPayload.id, context);
|
||||
}
|
||||
|
||||
_onExecutionContextDestroyed(executionContextId: number) {
|
||||
const context = this._contextIdToContext.get(executionContextId);
|
||||
if (!context)
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
this._contextIdToContext.delete(executionContextId);
|
||||
context.frame._contextDestroyed(context);
|
||||
}
|
||||
|
||||
_onExecutionContextsCleared() {
|
||||
for (const contextId of Array.from(this._contextIdToContext.keys()))
|
||||
for (const contextId of Array.from(this._contextIdToContext.keys())) {
|
||||
this._onExecutionContextDestroyed(contextId);
|
||||
}
|
||||
}
|
||||
|
||||
_onAttachedToTarget(event: Protocol.Target.attachedToTargetPayload) {
|
||||
const session = this._client.createChildSession(event.sessionId);
|
||||
|
|
@ -706,13 +753,15 @@ class FrameSession {
|
|||
// Frame id equals target id.
|
||||
const targetId = event.targetInfo.targetId;
|
||||
const frame = this._page._frameManager.frame(targetId);
|
||||
if (!frame)
|
||||
return; // Subtree may be already gone due to renderer/browser race.
|
||||
if (!frame) {
|
||||
return;
|
||||
} // Subtree may be already gone due to renderer/browser race.
|
||||
this._page._frameManager.removeChildFramesRecursively(frame);
|
||||
for (const [contextId, context] of this._contextIdToContext) {
|
||||
if (context.frame === frame)
|
||||
if (context.frame === frame) {
|
||||
this._onExecutionContextDestroyed(contextId);
|
||||
}
|
||||
}
|
||||
const frameSession = new FrameSession(this._crPage, session, targetId, this);
|
||||
this._crPage._sessions.set(targetId, frameSession);
|
||||
frameSession._initialize(false).catch(e => e);
|
||||
|
|
@ -757,8 +806,9 @@ class FrameSession {
|
|||
|
||||
// ... or an oopif.
|
||||
const childFrameSession = this._crPage._sessions.get(event.targetId!);
|
||||
if (!childFrameSession)
|
||||
if (!childFrameSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Usually, we get frameAttached in this session first and mark child as swappedIn.
|
||||
if (childFrameSession._swappedIn) {
|
||||
|
|
@ -773,8 +823,9 @@ class FrameSession {
|
|||
this._client.send('Page.enable').catch(e => null).then(() => {
|
||||
// Child was not swapped in - that means frameAttached did not happen and
|
||||
// this is remote detach rather than remote -> local swap.
|
||||
if (!childFrameSession._swappedIn)
|
||||
if (!childFrameSession._swappedIn) {
|
||||
this._page._frameManager.frameDetached(event.targetId!);
|
||||
}
|
||||
childFrameSession.dispose();
|
||||
});
|
||||
}
|
||||
|
|
@ -801,8 +852,9 @@ class FrameSession {
|
|||
return;
|
||||
}
|
||||
const context = this._contextIdToContext.get(event.executionContextId);
|
||||
if (!context)
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
const values = event.args.map(arg => context.createHandle(arg));
|
||||
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
|
||||
}
|
||||
|
|
@ -811,22 +863,25 @@ class FrameSession {
|
|||
const pageOrError = await this._crPage._page.waitForInitializedOrError();
|
||||
if (!(pageOrError instanceof Error)) {
|
||||
const context = this._contextIdToContext.get(event.executionContextId);
|
||||
if (context)
|
||||
if (context) {
|
||||
await this._page._onBindingCalled(event.payload, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_onDialog(event: Protocol.Page.javascriptDialogOpeningPayload) {
|
||||
if (!this._page._frameManager.frame(this._targetId))
|
||||
return; // Our frame/subtree may be gone already.
|
||||
if (!this._page._frameManager.frame(this._targetId)) {
|
||||
return;
|
||||
} // Our frame/subtree may be gone already.
|
||||
this._page.emitOnContext(BrowserContext.Events.Dialog, new dialog.Dialog(
|
||||
this._page,
|
||||
event.type,
|
||||
event.message,
|
||||
async (accept: boolean, promptText?: string) => {
|
||||
// TODO: this should actually be a CDP event that notifies about a cancelled navigation attempt.
|
||||
if (this._isMainFrame() && event.type === 'beforeunload' && !accept)
|
||||
if (this._isMainFrame() && event.type === 'beforeunload' && !accept) {
|
||||
this._page._frameManager.frameAbortedNavigation(this._page.mainFrame()._id, 'navigation cancelled by beforeunload dialog');
|
||||
}
|
||||
await this._client.send('Page.handleJavaScriptDialog', { accept, promptText });
|
||||
},
|
||||
event.defaultPrompt));
|
||||
|
|
@ -843,8 +898,9 @@ class FrameSession {
|
|||
|
||||
_onLogEntryAdded(event: Protocol.Log.entryAddedPayload) {
|
||||
const { level, text, args, source, url, lineNumber } = event.entry;
|
||||
if (args)
|
||||
if (args) {
|
||||
args.map(arg => releaseObject(this._client, arg.objectId!));
|
||||
}
|
||||
if (source !== 'worker') {
|
||||
const location: types.ConsoleMessageLocation = {
|
||||
url: url || '',
|
||||
|
|
@ -856,11 +912,13 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _onFileChooserOpened(event: Protocol.Page.fileChooserOpenedPayload) {
|
||||
if (!event.backendNodeId)
|
||||
if (!event.backendNodeId) {
|
||||
return;
|
||||
}
|
||||
const frame = this._page._frameManager.frame(event.frameId);
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
let handle;
|
||||
try {
|
||||
const utilityContext = await frame._utilityContext();
|
||||
|
|
@ -918,8 +976,9 @@ class FrameSession {
|
|||
}
|
||||
|
||||
async _stopVideoRecording(): Promise<void> {
|
||||
if (!this._screencastId)
|
||||
if (!this._screencastId) {
|
||||
return;
|
||||
}
|
||||
const screencastId = this._screencastId;
|
||||
this._screencastId = null;
|
||||
const recorder = this._videoRecorder!;
|
||||
|
|
@ -934,30 +993,35 @@ class FrameSession {
|
|||
|
||||
async _startScreencast(client: any, options: Protocol.Page.startScreencastParameters = {}) {
|
||||
this._screencastClients.add(client);
|
||||
if (this._screencastClients.size === 1)
|
||||
if (this._screencastClients.size === 1) {
|
||||
await this._client.send('Page.startScreencast', options);
|
||||
}
|
||||
}
|
||||
|
||||
async _stopScreencast(client: any) {
|
||||
this._screencastClients.delete(client);
|
||||
if (!this._screencastClients.size)
|
||||
if (!this._screencastClients.size) {
|
||||
await this._client._sendMayFail('Page.stopScreencast');
|
||||
}
|
||||
}
|
||||
|
||||
async _updateGeolocation(initial: boolean): Promise<void> {
|
||||
const geolocation = this._crPage._browserContext._options.geolocation;
|
||||
if (!initial || geolocation)
|
||||
if (!initial || geolocation) {
|
||||
await this._client.send('Emulation.setGeolocationOverride', geolocation || {});
|
||||
}
|
||||
}
|
||||
|
||||
async _updateViewport(preserveWindowBoundaries?: boolean): Promise<void> {
|
||||
if (this._crPage._browserContext._browser.isClank())
|
||||
if (this._crPage._browserContext._browser.isClank()) {
|
||||
return;
|
||||
}
|
||||
assert(this._isMainFrame());
|
||||
const options = this._crPage._browserContext._options;
|
||||
const emulatedSize = this._page.emulatedSize();
|
||||
if (emulatedSize === null)
|
||||
if (emulatedSize === null) {
|
||||
return;
|
||||
}
|
||||
const viewportSize = emulatedSize.viewport;
|
||||
const screenSize = emulatedSize.screen;
|
||||
const isLandscape = screenSize.width > screenSize.height;
|
||||
|
|
@ -973,8 +1037,9 @@ class FrameSession {
|
|||
) : { angle: 0, type: 'landscapePrimary' },
|
||||
dontSetVisibleSize: preserveWindowBoundaries
|
||||
};
|
||||
if (JSON.stringify(this._metricsOverride) === JSON.stringify(metricsOverride))
|
||||
if (JSON.stringify(this._metricsOverride) === JSON.stringify(metricsOverride)) {
|
||||
return;
|
||||
}
|
||||
const promises = [
|
||||
this._client.send('Emulation.setDeviceMetricsOverride', metricsOverride),
|
||||
];
|
||||
|
|
@ -983,12 +1048,13 @@ class FrameSession {
|
|||
if (this._crPage._browserContext._browser.options.headful) {
|
||||
// TODO: popup windows have their own insets.
|
||||
insets = { width: 24, height: 88 };
|
||||
if (process.platform === 'win32')
|
||||
if (process.platform === 'win32') {
|
||||
insets = { width: 16, height: 88 };
|
||||
else if (process.platform === 'linux')
|
||||
} else if (process.platform === 'linux') {
|
||||
insets = { width: 8, height: 85 };
|
||||
else if (process.platform === 'darwin')
|
||||
} else if (process.platform === 'darwin') {
|
||||
insets = { width: 2, height: 80 };
|
||||
}
|
||||
if (this._crPage._browserContext.isPersistentContext()) {
|
||||
// FIXME: Chrome bug: OOPIF router is confused when hit target is
|
||||
// outside browser window.
|
||||
|
|
@ -1050,17 +1116,19 @@ class FrameSession {
|
|||
|
||||
async _updateFileChooserInterception(initial: boolean) {
|
||||
const enabled = this._page.fileChooserIntercepted();
|
||||
if (initial && !enabled)
|
||||
if (initial && !enabled) {
|
||||
return;
|
||||
}
|
||||
await this._client.send('Page.setInterceptFileChooserDialog', { enabled }).catch(() => {}); // target can be closed.
|
||||
}
|
||||
|
||||
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
|
||||
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
|
||||
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
|
||||
if (!initScript.internal)
|
||||
if (!initScript.internal) {
|
||||
this._evaluateOnNewDocumentIdentifiers.push(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
async _removeEvaluatesOnNewDocument(): Promise<void> {
|
||||
const identifiers = this._evaluateOnNewDocumentIdentifiers;
|
||||
|
|
@ -1072,8 +1140,9 @@ class FrameSession {
|
|||
const nodeInfo = await this._client.send('DOM.describeNode', {
|
||||
objectId: handle._objectId
|
||||
});
|
||||
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string')
|
||||
if (!nodeInfo || typeof nodeInfo.node.frameId !== 'string') {
|
||||
return null;
|
||||
}
|
||||
return this._page._frameManager.frame(nodeInfo.node.frameId);
|
||||
}
|
||||
|
||||
|
|
@ -1081,14 +1150,17 @@ class FrameSession {
|
|||
// document.documentElement has frameId of the owner frame.
|
||||
const documentElement = await handle.evaluateHandle(node => {
|
||||
const doc = node as Document;
|
||||
if (doc.documentElement && doc.documentElement.ownerDocument === doc)
|
||||
if (doc.documentElement && doc.documentElement.ownerDocument === doc) {
|
||||
return doc.documentElement;
|
||||
}
|
||||
return node.ownerDocument ? node.ownerDocument.documentElement : null;
|
||||
});
|
||||
if (!documentElement)
|
||||
if (!documentElement) {
|
||||
return null;
|
||||
if (!documentElement._objectId)
|
||||
}
|
||||
if (!documentElement._objectId) {
|
||||
return null;
|
||||
}
|
||||
const nodeInfo = await this._client.send('DOM.describeNode', {
|
||||
objectId: documentElement._objectId
|
||||
});
|
||||
|
|
@ -1102,25 +1174,29 @@ class FrameSession {
|
|||
const result = await this._client._sendMayFail('DOM.getBoxModel', {
|
||||
objectId: handle._objectId
|
||||
});
|
||||
if (!result)
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const quad = result.model.border;
|
||||
const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
||||
const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
||||
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
|
||||
const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y;
|
||||
const position = await this._framePosition();
|
||||
if (!position)
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
return { x: x + position.x, y: y + position.y, width, height };
|
||||
}
|
||||
|
||||
private async _framePosition(): Promise<types.Point | null> {
|
||||
const frame = this._page._frameManager.frame(this._targetId);
|
||||
if (!frame)
|
||||
if (!frame) {
|
||||
return null;
|
||||
if (frame === this._page.mainFrame())
|
||||
}
|
||||
if (frame === this._page.mainFrame()) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
const element = await frame.frameElement();
|
||||
const box = await element.boundingBox();
|
||||
return box;
|
||||
|
|
@ -1131,10 +1207,12 @@ class FrameSession {
|
|||
objectId: handle._objectId,
|
||||
rect,
|
||||
}).then(() => 'done' as const).catch(e => {
|
||||
if (e instanceof Error && e.message.includes('Node does not have a layout object'))
|
||||
if (e instanceof Error && e.message.includes('Node does not have a layout object')) {
|
||||
return 'error:notvisible';
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document'))
|
||||
}
|
||||
if (e instanceof Error && e.message.includes('Node is detached from document')) {
|
||||
return 'error:notconnected';
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
|
@ -1143,11 +1221,13 @@ class FrameSession {
|
|||
const result = await this._client._sendMayFail('DOM.getContentQuads', {
|
||||
objectId: handle._objectId
|
||||
});
|
||||
if (!result)
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const position = await this._framePosition();
|
||||
if (!position)
|
||||
if (!position) {
|
||||
return null;
|
||||
}
|
||||
return result.quads.map(quad => [
|
||||
{ x: quad[0] + position.x, y: quad[1] + position.y },
|
||||
{ x: quad[2] + position.x, y: quad[3] + position.y },
|
||||
|
|
@ -1168,8 +1248,9 @@ class FrameSession {
|
|||
backendNodeId,
|
||||
executionContextId: ((to as any)[contextDelegateSymbol] as CRExecutionContext)._contextId,
|
||||
});
|
||||
if (!result || result.object.subtype === 'null')
|
||||
if (!result || result.object.subtype === 'null') {
|
||||
throw new Error(dom.kUnableToAdoptErrorMessage);
|
||||
}
|
||||
return to.createHandle(result.object).asElement()!;
|
||||
}
|
||||
}
|
||||
|
|
@ -1181,8 +1262,9 @@ async function emulateLocale(session: CRSession, locale: string) {
|
|||
// All pages in the same renderer share locale. All such pages belong to the same
|
||||
// context and if locale is overridden for one of them its value is the same as
|
||||
// we are trying to set so it's not a problem.
|
||||
if (exception.message.includes('Another locale override is already in effect'))
|
||||
if (exception.message.includes('Another locale override is already in effect')) {
|
||||
return;
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
|
@ -1191,10 +1273,12 @@ async function emulateTimezone(session: CRSession, timezoneId: string) {
|
|||
try {
|
||||
await session.send('Emulation.setTimezoneOverride', { timezoneId: timezoneId });
|
||||
} catch (exception) {
|
||||
if (exception.message.includes('Timezone override is already in effect'))
|
||||
if (exception.message.includes('Timezone override is already in effect')) {
|
||||
return;
|
||||
if (exception.message.includes('Invalid timezone'))
|
||||
}
|
||||
if (exception.message.includes('Invalid timezone')) {
|
||||
throw new Error(`Invalid timezone ID: ${timezoneId}`);
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
|
@ -1204,8 +1288,9 @@ const contextDelegateSymbol = Symbol('delegate');
|
|||
// Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2
|
||||
function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
|
||||
const ua = options.userAgent;
|
||||
if (!ua)
|
||||
if (!ua) {
|
||||
return undefined;
|
||||
}
|
||||
const metadata: Protocol.Emulation.UserAgentMetadata = {
|
||||
mobile: !!options.isMobile,
|
||||
model: '',
|
||||
|
|
@ -1233,15 +1318,17 @@ function calculateUserAgentMetadata(options: types.BrowserContextOptions) {
|
|||
} else if (macOSMatch) {
|
||||
metadata.platform = 'macOS';
|
||||
metadata.platformVersion = macOSMatch[1];
|
||||
if (!ua.includes('Intel'))
|
||||
if (!ua.includes('Intel')) {
|
||||
metadata.architecture = 'arm';
|
||||
}
|
||||
} else if (windowsMatch) {
|
||||
metadata.platform = 'Windows';
|
||||
metadata.platformVersion = windowsMatch[1];
|
||||
} else if (ua.toLowerCase().includes('linux')) {
|
||||
metadata.platform = 'Linux';
|
||||
}
|
||||
if (ua.includes('ARM'))
|
||||
if (ua.includes('ARM')) {
|
||||
metadata.architecture = 'arm';
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ const unitToPixels: { [key: string]: number } = {
|
|||
};
|
||||
|
||||
function convertPrintParameterToInches(text: string | undefined): number | undefined {
|
||||
if (text === undefined)
|
||||
if (text === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
let unit = text.substring(text.length - 2).toLowerCase();
|
||||
let valueText = '';
|
||||
if (unitToPixels.hasOwnProperty(unit)) {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ import { mkdirIfNeeded } from '../../utils/fileUtils';
|
|||
import { splitErrorMessage } from '../../utils/stackTrace';
|
||||
|
||||
export function getExceptionMessage(exceptionDetails: Protocol.Runtime.ExceptionDetails): string {
|
||||
if (exceptionDetails.exception)
|
||||
if (exceptionDetails.exception) {
|
||||
return exceptionDetails.exception.description || String(exceptionDetails.exception.value);
|
||||
}
|
||||
let message = exceptionDetails.text;
|
||||
if (exceptionDetails.stackTrace) {
|
||||
for (const callframe of exceptionDetails.stackTrace.callFrames) {
|
||||
|
|
@ -98,24 +99,31 @@ export function exceptionToError(exceptionDetails: Protocol.Runtime.ExceptionDet
|
|||
|
||||
export function toModifiersMask(modifiers: Set<types.KeyboardModifier>): number {
|
||||
let mask = 0;
|
||||
if (modifiers.has('Alt'))
|
||||
if (modifiers.has('Alt')) {
|
||||
mask |= 1;
|
||||
if (modifiers.has('Control'))
|
||||
}
|
||||
if (modifiers.has('Control')) {
|
||||
mask |= 2;
|
||||
if (modifiers.has('Meta'))
|
||||
}
|
||||
if (modifiers.has('Meta')) {
|
||||
mask |= 4;
|
||||
if (modifiers.has('Shift'))
|
||||
}
|
||||
if (modifiers.has('Shift')) {
|
||||
mask |= 8;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
export function toButtonsMask(buttons: Set<types.MouseButton>): number {
|
||||
let mask = 0;
|
||||
if (buttons.has('left'))
|
||||
if (buttons.has('left')) {
|
||||
mask |= 1;
|
||||
if (buttons.has('right'))
|
||||
}
|
||||
if (buttons.has('right')) {
|
||||
mask |= 2;
|
||||
if (buttons.has('middle'))
|
||||
}
|
||||
if (buttons.has('middle')) {
|
||||
mask |= 4;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,9 @@ export class CRServiceWorker extends Worker {
|
|||
super(browserContext, url);
|
||||
this._session = session;
|
||||
this._browserContext = browserContext;
|
||||
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS)
|
||||
if (!!process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS) {
|
||||
this._networkManager = new CRNetworkManager(null, this);
|
||||
}
|
||||
session.once('Runtime.executionContextCreated', event => {
|
||||
this._createExecutionContext(new CRExecutionContext(session, event.context));
|
||||
});
|
||||
|
|
@ -59,26 +60,30 @@ export class CRServiceWorker extends Worker {
|
|||
}
|
||||
|
||||
async updateOffline(): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
if (!this._isNetworkInspectionEnabled()) {
|
||||
return;
|
||||
}
|
||||
await this._networkManager?.setOffline(!!this._browserContext._options.offline).catch(() => {});
|
||||
}
|
||||
|
||||
async updateHttpCredentials(): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
if (!this._isNetworkInspectionEnabled()) {
|
||||
return;
|
||||
}
|
||||
await this._networkManager?.authenticate(this._browserContext._options.httpCredentials || null).catch(() => {});
|
||||
}
|
||||
|
||||
async updateExtraHTTPHeaders(): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
if (!this._isNetworkInspectionEnabled()) {
|
||||
return;
|
||||
}
|
||||
await this._networkManager?.setExtraHTTPHeaders(this._browserContext._options.extraHTTPHeaders || []).catch(() => {});
|
||||
}
|
||||
|
||||
async updateRequestInterception(): Promise<void> {
|
||||
if (!this._isNetworkInspectionEnabled())
|
||||
if (!this._isNetworkInspectionEnabled()) {
|
||||
return;
|
||||
}
|
||||
await this._networkManager?.setRequestInterception(this.needsRequestInterception()).catch(() => {});
|
||||
}
|
||||
|
||||
|
|
@ -102,8 +107,9 @@ export class CRServiceWorker extends Worker {
|
|||
this._browserContext.emit(BrowserContext.Events.Request, request);
|
||||
if (route) {
|
||||
const r = new network.Route(request, route);
|
||||
if (this._browserContext._requestInterceptor?.(r, request))
|
||||
if (this._browserContext._requestInterceptor?.(r, request)) {
|
||||
return;
|
||||
}
|
||||
r.continue({ isFallback: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,9 @@ export class VideoRecorder {
|
|||
private _ffmpegPath: string;
|
||||
|
||||
static async launch(page: Page, ffmpegPath: string, options: types.PageScreencastOptions): Promise<VideoRecorder> {
|
||||
if (!options.outputFile.endsWith('.webm'))
|
||||
if (!options.outputFile.endsWith('.webm')) {
|
||||
throw new Error('File must have .webm extension');
|
||||
}
|
||||
|
||||
const controller = new ProgressController(serverSideCallMetadata(), page);
|
||||
controller.setLogName('browser');
|
||||
|
|
@ -128,14 +129,16 @@ export class VideoRecorder {
|
|||
|
||||
writeFrame(frame: Buffer, timestamp: number) {
|
||||
assert(this._process);
|
||||
if (this._isStopped)
|
||||
if (this._isStopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._lastFrameBuffer) {
|
||||
const durationSec = timestamp - this._lastFrameTimestamp;
|
||||
const repeatCount = Math.max(1, Math.round(fps * durationSec));
|
||||
for (let i = 0; i < repeatCount; ++i)
|
||||
for (let i = 0; i < repeatCount; ++i) {
|
||||
this._frameQueue.push(this._lastFrameBuffer);
|
||||
}
|
||||
this._lastWritePromise = this._lastWritePromise.then(() => this._sendFrames());
|
||||
}
|
||||
|
||||
|
|
@ -145,20 +148,23 @@ export class VideoRecorder {
|
|||
}
|
||||
|
||||
private async _sendFrames() {
|
||||
while (this._frameQueue.length)
|
||||
while (this._frameQueue.length) {
|
||||
await this._sendFrame(this._frameQueue.shift()!);
|
||||
}
|
||||
}
|
||||
|
||||
private async _sendFrame(frame: Buffer) {
|
||||
return new Promise(f => this._process!.stdin!.write(frame, f)).then(error => {
|
||||
if (error)
|
||||
if (error) {
|
||||
this._progress.log(`ffmpeg failed to write: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async stop() {
|
||||
if (this._isStopped)
|
||||
if (this._isStopped) {
|
||||
return;
|
||||
}
|
||||
this.writeFrame(Buffer.from([]), this._lastFrameTimestamp + (monotonicTime() - this._lastWriteTimestamp) / 1000);
|
||||
this._isStopped = true;
|
||||
await this._lastWritePromise;
|
||||
|
|
|
|||
|
|
@ -78,8 +78,9 @@ export class Clock {
|
|||
}
|
||||
|
||||
private async _installIfNeeded() {
|
||||
if (this._scriptInstalled)
|
||||
if (this._scriptInstalled) {
|
||||
return;
|
||||
}
|
||||
this._scriptInstalled = true;
|
||||
const script = `(() => {
|
||||
const module = {};
|
||||
|
|
@ -101,10 +102,12 @@ export class Clock {
|
|||
* to clock.tick()
|
||||
*/
|
||||
function parseTicks(value: number | string): number {
|
||||
if (typeof value === 'number')
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
if (!value)
|
||||
}
|
||||
if (!value) {
|
||||
return 0;
|
||||
}
|
||||
const str = value;
|
||||
|
||||
const strings = str.split(':');
|
||||
|
|
@ -121,8 +124,9 @@ function parseTicks(value: number | string): number {
|
|||
|
||||
while (i--) {
|
||||
parsed = parseInt(strings[i], 10);
|
||||
if (parsed >= 60)
|
||||
if (parsed >= 60) {
|
||||
throw new Error(`Invalid time ${str}`);
|
||||
}
|
||||
ms += parsed * Math.pow(60, l - i - 1);
|
||||
}
|
||||
|
||||
|
|
@ -130,12 +134,15 @@ function parseTicks(value: number | string): number {
|
|||
}
|
||||
|
||||
function parseTime(epoch: string | number | undefined): number {
|
||||
if (!epoch)
|
||||
if (!epoch) {
|
||||
return 0;
|
||||
if (typeof epoch === 'number')
|
||||
}
|
||||
if (typeof epoch === 'number') {
|
||||
return epoch;
|
||||
}
|
||||
const parsed = new Date(epoch);
|
||||
if (!isFinite(parsed.getTime()))
|
||||
if (!isFinite(parsed.getTime())) {
|
||||
throw new Error(`Invalid date: ${epoch}`);
|
||||
}
|
||||
return parsed.getTime();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,24 +48,28 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
|
||||
generateAction(actionInContext: actions.ActionInContext): string {
|
||||
const action = this._generateActionInner(actionInContext);
|
||||
if (action)
|
||||
if (action) {
|
||||
return action;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
_generateActionInner(actionInContext: actions.ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage'))
|
||||
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage')) {
|
||||
return '';
|
||||
}
|
||||
let pageAlias = actionInContext.frame.pageAlias;
|
||||
if (this._mode !== 'library')
|
||||
if (this._mode !== 'library') {
|
||||
pageAlias = pageAlias.replace('page', 'Page');
|
||||
}
|
||||
const formatter = new CSharpFormatter(this._mode === 'library' ? 0 : 8);
|
||||
|
||||
if (action.name === 'openPage') {
|
||||
formatter.add(`var ${pageAlias} = await context.NewPageAsync();`);
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
|
||||
formatter.add(`await ${pageAlias}.GotoAsync(${quote(action.url)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -96,8 +100,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
lines.push(`});`);
|
||||
}
|
||||
|
||||
for (const line of lines)
|
||||
for (const line of lines) {
|
||||
formatter.add(line);
|
||||
}
|
||||
|
||||
return formatter.format();
|
||||
}
|
||||
|
|
@ -111,11 +116,13 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
return `await ${subject}.CloseAsync();`;
|
||||
case 'click': {
|
||||
let method = 'Click';
|
||||
if (action.clickCount === 2)
|
||||
if (action.clickCount === 2) {
|
||||
method = 'DblClick';
|
||||
}
|
||||
const options = toClickOptionsForSourceCode(action);
|
||||
if (!Object.entries(options).length)
|
||||
if (!Object.entries(options).length) {
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async();`;
|
||||
}
|
||||
const optionsString = formatObject(options, ' ', 'Locator' + method + 'Options');
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}Async(${optionsString});`;
|
||||
}
|
||||
|
|
@ -156,8 +163,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
}
|
||||
|
||||
generateHeader(options: LanguageGeneratorOptions): string {
|
||||
if (this._mode === 'library')
|
||||
if (this._mode === 'library') {
|
||||
return this.generateStandaloneHeader(options);
|
||||
}
|
||||
return this.generateTestRunnerHeader(options);
|
||||
}
|
||||
|
||||
|
|
@ -171,8 +179,9 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
using var playwright = await Playwright.CreateAsync();
|
||||
await using var browser = await playwright.${toPascal(options.browserName)}.LaunchAsync(${formatObject(options.launchOptions, ' ', 'BrowserTypeLaunchOptions')});
|
||||
var context = await browser.NewContextAsync(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
|
||||
}
|
||||
formatter.newLine();
|
||||
return formatter.format();
|
||||
}
|
||||
|
|
@ -198,43 +207,50 @@ export class CSharpLanguageGenerator implements LanguageGenerator {
|
|||
formatter.add(` [${this._mode === 'nunit' ? 'Test' : 'TestMethod'}]
|
||||
public async Task MyTest()
|
||||
{`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` await context.RouteFromHARAsync(${quote(options.contextOptions.recordHar.path)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
generateFooter(saveStorage: string | undefined): string {
|
||||
const offset = this._mode === 'library' ? '' : ' ';
|
||||
let storageStateLine = saveStorage ? `\n${offset}await context.StorageStateAsync(new BrowserContextStorageStateOptions\n${offset}{\n${offset} Path = ${quote(saveStorage)}\n${offset}});\n` : '';
|
||||
if (this._mode !== 'library')
|
||||
if (this._mode !== 'library') {
|
||||
storageStateLine += ` }\n}\n`;
|
||||
}
|
||||
return storageStateLine;
|
||||
}
|
||||
}
|
||||
|
||||
function formatObject(value: any, indent = ' ', name = ''): string {
|
||||
if (typeof value === 'string') {
|
||||
if (['permissions', 'colorScheme', 'modifiers', 'button', 'recordHarContent', 'recordHarMode', 'serviceWorkers'].includes(name))
|
||||
if (['permissions', 'colorScheme', 'modifiers', 'button', 'recordHarContent', 'recordHarMode', 'serviceWorkers'].includes(name)) {
|
||||
return `${getClassName(name)}.${toPascal(value)}`;
|
||||
}
|
||||
return quote(value);
|
||||
}
|
||||
if (Array.isArray(value))
|
||||
if (Array.isArray(value)) {
|
||||
return `new[] { ${value.map(o => formatObject(o, indent, name)).join(', ')} }`;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
|
||||
if (!keys.length)
|
||||
if (!keys.length) {
|
||||
return name ? `new ${getClassName(name)}` : '';
|
||||
}
|
||||
const tokens: string[] = [];
|
||||
for (const key of keys) {
|
||||
const property = getPropertyName(key);
|
||||
tokens.push(`${property} = ${formatObject(value[key], indent, key)},`);
|
||||
}
|
||||
if (name)
|
||||
if (name) {
|
||||
return `new ${getClassName(name)}\n{\n${indent}${tokens.join(`\n${indent}`)}\n${indent}}`;
|
||||
}
|
||||
return `{\n${indent}${tokens.join(`\n${indent}`)}\n${indent}}`;
|
||||
}
|
||||
if (name === 'latitude' || name === 'longitude')
|
||||
if (name === 'latitude' || name === 'longitude') {
|
||||
return String(value) + 'm';
|
||||
}
|
||||
|
||||
return String(value);
|
||||
}
|
||||
|
|
@ -271,14 +287,16 @@ function formatContextOptions(contextOptions: BrowserContextOptions, deviceName:
|
|||
delete options.recordHar;
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device) {
|
||||
if (!Object.entries(options).length)
|
||||
if (!Object.entries(options).length) {
|
||||
return '';
|
||||
}
|
||||
return formatObject(options, ' ', 'BrowserNewContextOptions');
|
||||
}
|
||||
|
||||
options = sanitizeDeviceOptions(device, options);
|
||||
if (!Object.entries(options).length)
|
||||
if (!Object.entries(options).length) {
|
||||
return `playwright.Devices[${quote(deviceName!)}]`;
|
||||
}
|
||||
|
||||
return formatObject(options, ' ', `BrowserNewContextOptions(playwright.Devices[${quote(deviceName!)}])`);
|
||||
}
|
||||
|
|
@ -309,19 +327,23 @@ class CSharpFormatter {
|
|||
let spaces = '';
|
||||
let previousLine = '';
|
||||
return this._lines.map((line: string) => {
|
||||
if (line === '')
|
||||
if (line === '') {
|
||||
return line;
|
||||
if (line.startsWith('}') || line.startsWith(']') || line.includes('});') || line === ');')
|
||||
}
|
||||
if (line.startsWith('}') || line.startsWith(']') || line.includes('});') || line === ');') {
|
||||
spaces = spaces.substring(this._baseIndent.length);
|
||||
}
|
||||
|
||||
const extraSpaces = /^(for|while|if).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
|
||||
previousLine = line;
|
||||
|
||||
line = spaces + extraSpaces + line;
|
||||
if (line.endsWith('{') || line.endsWith('[') || line.endsWith('('))
|
||||
if (line.endsWith('{') || line.endsWith('[') || line.endsWith('(')) {
|
||||
spaces += this._baseIndent;
|
||||
if (line.endsWith('));'))
|
||||
}
|
||||
if (line.endsWith('));')) {
|
||||
spaces = spaces.substring(this._baseIndent.length);
|
||||
}
|
||||
|
||||
return this._baseOffset + line;
|
||||
}).join('\n');
|
||||
|
|
|
|||
|
|
@ -51,13 +51,15 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
const offset = this._mode === 'junit' ? 4 : 6;
|
||||
const formatter = new JavaScriptFormatter(offset);
|
||||
|
||||
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage'))
|
||||
if (this._mode !== 'library' && (action.name === 'openPage' || action.name === 'closePage')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (action.name === 'openPage') {
|
||||
formatter.add(`Page ${pageAlias} = context.newPage();`);
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
|
||||
formatter.add(`${pageAlias}.navigate(${quote(action.url)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -100,8 +102,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
return `${subject}.close();`;
|
||||
case 'click': {
|
||||
let method = 'click';
|
||||
if (action.clickCount === 2)
|
||||
if (action.clickCount === 2) {
|
||||
method = 'dblclick';
|
||||
}
|
||||
const options = toClickOptionsForSourceCode(action);
|
||||
const optionsText = formatClickOptions(options);
|
||||
return `${subject}.${this._asLocator(action.selector, inFrameLocator)}.${method}(${optionsText});`;
|
||||
|
|
@ -170,8 +173,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
try (Playwright playwright = Playwright.create()) {
|
||||
Browser browser = playwright.${options.browserName}().launch(${formatLaunchOptions(options.launchOptions)});
|
||||
BrowserContext context = browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName)});`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -189,8 +193,9 @@ export class JavaLanguageGenerator implements LanguageGenerator {
|
|||
|
||||
function formatPath(files: string | string[]): string {
|
||||
if (Array.isArray(files)) {
|
||||
if (files.length === 0)
|
||||
if (files.length === 0) {
|
||||
return 'new Path[0]';
|
||||
}
|
||||
return `new Path[] {${files.map(s => 'Paths.get(' + quote(s) + ')').join(', ')}}`;
|
||||
}
|
||||
return `Paths.get(${quote(files)})`;
|
||||
|
|
@ -198,8 +203,9 @@ function formatPath(files: string | string[]): string {
|
|||
|
||||
function formatSelectOption(options: string | string[]): string {
|
||||
if (Array.isArray(options)) {
|
||||
if (options.length === 0)
|
||||
if (options.length === 0) {
|
||||
return 'new String[0]';
|
||||
}
|
||||
return `new String[] {${options.map(s => quote(s)).join(', ')}}`;
|
||||
}
|
||||
return quote(options);
|
||||
|
|
@ -207,66 +213,89 @@ function formatSelectOption(options: string | string[]): string {
|
|||
|
||||
function formatLaunchOptions(options: any): string {
|
||||
const lines = [];
|
||||
if (!Object.keys(options).filter(key => options[key] !== undefined).length)
|
||||
if (!Object.keys(options).filter(key => options[key] !== undefined).length) {
|
||||
return '';
|
||||
}
|
||||
lines.push('new BrowserType.LaunchOptions()');
|
||||
if (options.channel)
|
||||
if (options.channel) {
|
||||
lines.push(` .setChannel(${quote(options.channel)})`);
|
||||
if (typeof options.headless === 'boolean')
|
||||
}
|
||||
if (typeof options.headless === 'boolean') {
|
||||
lines.push(` .setHeadless(false)`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatContextOptions(contextOptions: BrowserContextOptions, deviceName: string | undefined): string {
|
||||
const lines = [];
|
||||
if (!Object.keys(contextOptions).length && !deviceName)
|
||||
if (!Object.keys(contextOptions).length && !deviceName) {
|
||||
return '';
|
||||
}
|
||||
const device = deviceName ? deviceDescriptors[deviceName] : {};
|
||||
const options: BrowserContextOptions = { ...device, ...contextOptions };
|
||||
lines.push('new Browser.NewContextOptions()');
|
||||
if (options.acceptDownloads)
|
||||
if (options.acceptDownloads) {
|
||||
lines.push(` .setAcceptDownloads(true)`);
|
||||
if (options.bypassCSP)
|
||||
}
|
||||
if (options.bypassCSP) {
|
||||
lines.push(` .setBypassCSP(true)`);
|
||||
if (options.colorScheme)
|
||||
}
|
||||
if (options.colorScheme) {
|
||||
lines.push(` .setColorScheme(ColorScheme.${options.colorScheme.toUpperCase()})`);
|
||||
if (options.deviceScaleFactor)
|
||||
}
|
||||
if (options.deviceScaleFactor) {
|
||||
lines.push(` .setDeviceScaleFactor(${options.deviceScaleFactor})`);
|
||||
if (options.geolocation)
|
||||
}
|
||||
if (options.geolocation) {
|
||||
lines.push(` .setGeolocation(${options.geolocation.latitude}, ${options.geolocation.longitude})`);
|
||||
if (options.hasTouch)
|
||||
}
|
||||
if (options.hasTouch) {
|
||||
lines.push(` .setHasTouch(${options.hasTouch})`);
|
||||
if (options.isMobile)
|
||||
}
|
||||
if (options.isMobile) {
|
||||
lines.push(` .setIsMobile(${options.isMobile})`);
|
||||
if (options.locale)
|
||||
}
|
||||
if (options.locale) {
|
||||
lines.push(` .setLocale(${quote(options.locale)})`);
|
||||
if (options.proxy)
|
||||
}
|
||||
if (options.proxy) {
|
||||
lines.push(` .setProxy(new Proxy(${quote(options.proxy.server)}))`);
|
||||
if (options.serviceWorkers)
|
||||
}
|
||||
if (options.serviceWorkers) {
|
||||
lines.push(` .setServiceWorkers(ServiceWorkerPolicy.${options.serviceWorkers.toUpperCase()})`);
|
||||
if (options.storageState)
|
||||
}
|
||||
if (options.storageState) {
|
||||
lines.push(` .setStorageStatePath(Paths.get(${quote(options.storageState as string)}))`);
|
||||
if (options.timezoneId)
|
||||
}
|
||||
if (options.timezoneId) {
|
||||
lines.push(` .setTimezoneId(${quote(options.timezoneId)})`);
|
||||
if (options.userAgent)
|
||||
}
|
||||
if (options.userAgent) {
|
||||
lines.push(` .setUserAgent(${quote(options.userAgent)})`);
|
||||
if (options.viewport)
|
||||
}
|
||||
if (options.viewport) {
|
||||
lines.push(` .setViewportSize(${options.viewport.width}, ${options.viewport.height})`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatClickOptions(options: types.MouseClickOptions) {
|
||||
const lines = [];
|
||||
if (options.button)
|
||||
if (options.button) {
|
||||
lines.push(` .setButton(MouseButton.${options.button.toUpperCase()})`);
|
||||
if (options.modifiers)
|
||||
}
|
||||
if (options.modifiers) {
|
||||
lines.push(` .setModifiers(Arrays.asList(${options.modifiers.map(m => `KeyboardModifier.${m.toUpperCase()}`).join(', ')}))`);
|
||||
if (options.clickCount)
|
||||
}
|
||||
if (options.clickCount) {
|
||||
lines.push(` .setClickCount(${options.clickCount})`);
|
||||
if (options.position)
|
||||
}
|
||||
if (options.position) {
|
||||
lines.push(` .setPosition(${options.position.x}, ${options.position.y})`);
|
||||
if (!lines.length)
|
||||
}
|
||||
if (!lines.length) {
|
||||
return '';
|
||||
}
|
||||
lines.unshift(`new Locator.ClickOptions()`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,16 +36,18 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
|
||||
generateAction(actionInContext: actions.ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage'))
|
||||
if (this._isTest && (action.name === 'openPage' || action.name === 'closePage')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const pageAlias = actionInContext.frame.pageAlias;
|
||||
const formatter = new JavaScriptFormatter(2);
|
||||
|
||||
if (action.name === 'openPage') {
|
||||
formatter.add(`const ${pageAlias} = await context.newPage();`);
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
|
||||
formatter.add(`await ${pageAlias}.goto(${quote(action.url)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -60,17 +62,21 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
});`);
|
||||
}
|
||||
|
||||
if (signals.popup)
|
||||
if (signals.popup) {
|
||||
formatter.add(`const ${signals.popup.popupAlias}Promise = ${pageAlias}.waitForEvent('popup');`);
|
||||
if (signals.download)
|
||||
}
|
||||
if (signals.download) {
|
||||
formatter.add(`const download${signals.download.downloadAlias}Promise = ${pageAlias}.waitForEvent('download');`);
|
||||
}
|
||||
|
||||
formatter.add(wrapWithStep(actionInContext.description, this._generateActionCall(subject, actionInContext)));
|
||||
|
||||
if (signals.popup)
|
||||
if (signals.popup) {
|
||||
formatter.add(`const ${signals.popup.popupAlias} = await ${signals.popup.popupAlias}Promise;`);
|
||||
if (signals.download)
|
||||
}
|
||||
if (signals.download) {
|
||||
formatter.add(`const download${signals.download.downloadAlias} = await download${signals.download.downloadAlias}Promise;`);
|
||||
}
|
||||
|
||||
return formatter.format();
|
||||
}
|
||||
|
|
@ -84,8 +90,9 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
return `await ${subject}.close();`;
|
||||
case 'click': {
|
||||
let method = 'click';
|
||||
if (action.clickCount === 2)
|
||||
if (action.clickCount === 2) {
|
||||
method = 'dblclick';
|
||||
}
|
||||
const options = toClickOptionsForSourceCode(action);
|
||||
const optionsString = formatOptions(options, false);
|
||||
return `await ${subject}.${this._asLocator(action.selector)}.${method}(${optionsString});`;
|
||||
|
|
@ -129,14 +136,16 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
}
|
||||
|
||||
generateHeader(options: LanguageGeneratorOptions): string {
|
||||
if (this._isTest)
|
||||
if (this._isTest) {
|
||||
return this.generateTestHeader(options);
|
||||
}
|
||||
return this.generateStandaloneHeader(options);
|
||||
}
|
||||
|
||||
generateFooter(saveStorage: string | undefined): string {
|
||||
if (this._isTest)
|
||||
if (this._isTest) {
|
||||
return this.generateTestFooter(saveStorage);
|
||||
}
|
||||
return this.generateStandaloneFooter(saveStorage);
|
||||
}
|
||||
|
||||
|
|
@ -147,8 +156,9 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
|||
import { test, expect${options.deviceName ? ', devices' : ''} } from '@playwright/test';
|
||||
${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
||||
test('test', async ({ page }) => {`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` await page.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -164,8 +174,9 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
|||
(async () => {
|
||||
const browser = await ${options.browserName}.launch(${formatObjectOrVoid(options.launchOptions)});
|
||||
const context = await browser.newContext(${formatContextOptions(options.contextOptions, options.deviceName, false)});`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` await context.routeFromHAR(${quote(options.contextOptions.recordHar.path)});`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -180,23 +191,28 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
|
|||
|
||||
function formatOptions(value: any, hasArguments: boolean): string {
|
||||
const keys = Object.keys(value);
|
||||
if (!keys.length)
|
||||
if (!keys.length) {
|
||||
return '';
|
||||
}
|
||||
return (hasArguments ? ', ' : '') + formatObject(value);
|
||||
}
|
||||
|
||||
function formatObject(value: any, indent = ' '): string {
|
||||
if (typeof value === 'string')
|
||||
if (typeof value === 'string') {
|
||||
return quote(value);
|
||||
if (Array.isArray(value))
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(o => formatObject(o)).join(', ')}]`;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
|
||||
if (!keys.length)
|
||||
if (!keys.length) {
|
||||
return '{}';
|
||||
}
|
||||
const tokens: string[] = [];
|
||||
for (const key of keys)
|
||||
for (const key of keys) {
|
||||
tokens.push(`${key}: ${formatObject(value[key])}`);
|
||||
}
|
||||
return `{\n${indent}${tokens.join(`,\n${indent}`)}\n}`;
|
||||
}
|
||||
return String(value);
|
||||
|
|
@ -211,13 +227,15 @@ function formatContextOptions(options: BrowserContextOptions, deviceName: string
|
|||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
// recordHAR is replaced with routeFromHAR in the generated code.
|
||||
options = { ...options, recordHar: undefined };
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return formatObjectOrVoid(options);
|
||||
}
|
||||
// Filter out all the properties from the device descriptor.
|
||||
let serializedObject = formatObjectOrVoid(sanitizeDeviceOptions(device, options));
|
||||
// When there are no additional context options, we still want to spread the device inside.
|
||||
if (!serializedObject)
|
||||
if (!serializedObject) {
|
||||
serializedObject = '{\n}';
|
||||
}
|
||||
const lines = serializedObject.split('\n');
|
||||
lines.splice(1, 0, `...devices[${quote(deviceName!)}],`);
|
||||
return lines.join('\n');
|
||||
|
|
@ -251,18 +269,21 @@ export class JavaScriptFormatter {
|
|||
let spaces = '';
|
||||
let previousLine = '';
|
||||
return this._lines.map((line: string) => {
|
||||
if (line === '')
|
||||
if (line === '') {
|
||||
return line;
|
||||
if (line.startsWith('}') || line.startsWith(']'))
|
||||
}
|
||||
if (line.startsWith('}') || line.startsWith(']')) {
|
||||
spaces = spaces.substring(this._baseIndent.length);
|
||||
}
|
||||
|
||||
const extraSpaces = /^(for|while|if|try).*\(.*\)$/.test(previousLine) ? this._baseIndent : '';
|
||||
previousLine = line;
|
||||
|
||||
const callCarryOver = line.startsWith('.set');
|
||||
line = spaces + extraSpaces + (callCarryOver ? this._baseIndent : '') + line;
|
||||
if (line.endsWith('{') || line.endsWith('['))
|
||||
if (line.endsWith('{') || line.endsWith('[')) {
|
||||
spaces += this._baseIndent;
|
||||
}
|
||||
return this._baseOffset + line;
|
||||
}).join('\n');
|
||||
}
|
||||
|
|
@ -283,8 +304,9 @@ export function quoteMultiline(text: string, indent = ' ') {
|
|||
.replace(/`/g, '\\`')
|
||||
.replace(/\$\{/g, '\\${');
|
||||
const lines = text.split('\n');
|
||||
if (lines.length === 1)
|
||||
if (lines.length === 1) {
|
||||
return '`' + escape(text) + '`';
|
||||
}
|
||||
return '`\n' + lines.map(line => indent + escape(line).replace(/\${/g, '\\${')).join('\n') + `\n${indent}\``;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,9 +31,10 @@ export function sanitizeDeviceOptions(device: any, options: BrowserContextOption
|
|||
// Filter out all the properties from the device descriptor.
|
||||
const cleanedOptions: Record<string, any> = {};
|
||||
for (const property in options) {
|
||||
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
|
||||
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property])) {
|
||||
cleanedOptions[property] = (options as any)[property];
|
||||
}
|
||||
}
|
||||
return cleanedOptions;
|
||||
}
|
||||
|
||||
|
|
@ -42,13 +43,14 @@ export function toSignalMap(action: actions.Action) {
|
|||
let download: actions.DownloadSignal | undefined;
|
||||
let dialog: actions.DialogSignal | undefined;
|
||||
for (const signal of action.signals) {
|
||||
if (signal.name === 'popup')
|
||||
if (signal.name === 'popup') {
|
||||
popup = signal;
|
||||
else if (signal.name === 'download')
|
||||
} else if (signal.name === 'download') {
|
||||
download = signal;
|
||||
else if (signal.name === 'dialog')
|
||||
} else if (signal.name === 'dialog') {
|
||||
dialog = signal;
|
||||
}
|
||||
}
|
||||
return {
|
||||
popup,
|
||||
download,
|
||||
|
|
@ -58,45 +60,59 @@ export function toSignalMap(action: actions.Action) {
|
|||
|
||||
export function toKeyboardModifiers(modifiers: number): types.SmartKeyboardModifier[] {
|
||||
const result: types.SmartKeyboardModifier[] = [];
|
||||
if (modifiers & 1)
|
||||
if (modifiers & 1) {
|
||||
result.push('Alt');
|
||||
if (modifiers & 2)
|
||||
}
|
||||
if (modifiers & 2) {
|
||||
result.push('ControlOrMeta');
|
||||
if (modifiers & 4)
|
||||
}
|
||||
if (modifiers & 4) {
|
||||
result.push('ControlOrMeta');
|
||||
if (modifiers & 8)
|
||||
}
|
||||
if (modifiers & 8) {
|
||||
result.push('Shift');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function fromKeyboardModifiers(modifiers?: types.SmartKeyboardModifier[]): number {
|
||||
let result = 0;
|
||||
if (!modifiers)
|
||||
if (!modifiers) {
|
||||
return result;
|
||||
if (modifiers.includes('Alt'))
|
||||
}
|
||||
if (modifiers.includes('Alt')) {
|
||||
result |= 1;
|
||||
if (modifiers.includes('Control'))
|
||||
}
|
||||
if (modifiers.includes('Control')) {
|
||||
result |= 2;
|
||||
if (modifiers.includes('ControlOrMeta'))
|
||||
}
|
||||
if (modifiers.includes('ControlOrMeta')) {
|
||||
result |= 2;
|
||||
if (modifiers.includes('Meta'))
|
||||
}
|
||||
if (modifiers.includes('Meta')) {
|
||||
result |= 4;
|
||||
if (modifiers.includes('Shift'))
|
||||
}
|
||||
if (modifiers.includes('Shift')) {
|
||||
result |= 8;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function toClickOptionsForSourceCode(action: actions.ClickAction): types.MouseClickOptions {
|
||||
const modifiers = toKeyboardModifiers(action.modifiers);
|
||||
const options: types.MouseClickOptions = {};
|
||||
if (action.button !== 'left')
|
||||
if (action.button !== 'left') {
|
||||
options.button = action.button;
|
||||
if (modifiers.length)
|
||||
}
|
||||
if (modifiers.length) {
|
||||
options.modifiers = modifiers;
|
||||
}
|
||||
// Do not render clickCount === 2 for dblclick.
|
||||
if (action.clickCount > 2)
|
||||
if (action.clickCount > 2) {
|
||||
options.clickCount = action.clickCount;
|
||||
if (action.position)
|
||||
}
|
||||
if (action.position) {
|
||||
options.position = action.position;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,16 +43,18 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
|
||||
generateAction(actionInContext: actions.ActionInContext): string {
|
||||
const action = actionInContext.action;
|
||||
if (this._isPyTest && (action.name === 'openPage' || action.name === 'closePage'))
|
||||
if (this._isPyTest && (action.name === 'openPage' || action.name === 'closePage')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const pageAlias = actionInContext.frame.pageAlias;
|
||||
const formatter = new PythonFormatter(4);
|
||||
|
||||
if (action.name === 'openPage') {
|
||||
formatter.add(`${pageAlias} = ${this._awaitPrefix}context.new_page()`);
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/')
|
||||
if (action.url && action.url !== 'about:blank' && action.url !== 'chrome://newtab/') {
|
||||
formatter.add(`${this._awaitPrefix}${pageAlias}.goto(${quote(action.url)})`);
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +62,9 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
const subject = `${pageAlias}${locators.join('')}`;
|
||||
const signals = toSignalMap(action);
|
||||
|
||||
if (signals.dialog)
|
||||
if (signals.dialog) {
|
||||
formatter.add(` ${pageAlias}.once("dialog", lambda dialog: dialog.dismiss())`);
|
||||
}
|
||||
|
||||
let code = `${this._awaitPrefix}${this._generateActionCall(subject, actionInContext)}`;
|
||||
|
||||
|
|
@ -93,8 +96,9 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
|||
return `${subject}.close()`;
|
||||
case 'click': {
|
||||
let method = 'click';
|
||||
if (action.clickCount === 2)
|
||||
if (action.clickCount === 2) {
|
||||
method = 'dblclick';
|
||||
}
|
||||
const options = toClickOptionsForSourceCode(action);
|
||||
const optionsString = formatOptions(options, false);
|
||||
return `${subject}.${this._asLocator(action.selector)}.${method}(${optionsString})`;
|
||||
|
|
@ -151,8 +155,9 @@ from playwright.sync_api import Page, expect
|
|||
${fixture}
|
||||
|
||||
def test_example(page: Page) -> None {`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
||||
}
|
||||
} else if (this._isAsync) {
|
||||
formatter.add(`
|
||||
import asyncio
|
||||
|
|
@ -163,8 +168,9 @@ from playwright.async_api import Playwright, async_playwright, expect
|
|||
async def run(playwright: Playwright) -> None {
|
||||
browser = await playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||
context = await browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` await page.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
||||
}
|
||||
} else {
|
||||
formatter.add(`
|
||||
import re
|
||||
|
|
@ -174,9 +180,10 @@ from playwright.sync_api import Playwright, sync_playwright, expect
|
|||
def run(playwright: Playwright) -> None {
|
||||
browser = playwright.${options.browserName}.launch(${formatOptions(options.launchOptions, false)})
|
||||
context = browser.new_context(${formatContextOptions(options.contextOptions, options.deviceName)})`);
|
||||
if (options.contextOptions.recordHar)
|
||||
if (options.contextOptions.recordHar) {
|
||||
formatter.add(` context.route_from_har(${quote(options.contextOptions.recordHar.path)})`);
|
||||
}
|
||||
}
|
||||
return formatter.format();
|
||||
}
|
||||
|
||||
|
|
@ -212,28 +219,36 @@ with sync_playwright() as playwright:
|
|||
}
|
||||
|
||||
function formatValue(value: any): string {
|
||||
if (value === false)
|
||||
if (value === false) {
|
||||
return 'False';
|
||||
if (value === true)
|
||||
}
|
||||
if (value === true) {
|
||||
return 'True';
|
||||
if (value === undefined)
|
||||
}
|
||||
if (value === undefined) {
|
||||
return 'None';
|
||||
if (Array.isArray(value))
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(formatValue).join(', ')}]`;
|
||||
if (typeof value === 'string')
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return quote(value);
|
||||
if (typeof value === 'object')
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function formatOptions(value: any, hasArguments: boolean, asDict?: boolean): string {
|
||||
const keys = Object.keys(value).filter(key => value[key] !== undefined).sort();
|
||||
if (!keys.length)
|
||||
if (!keys.length) {
|
||||
return '';
|
||||
}
|
||||
return (hasArguments ? ', ' : '') + keys.map(key => {
|
||||
if (asDict)
|
||||
if (asDict) {
|
||||
return `"${toSnakeCase(key)}": ${formatValue(value[key])}`;
|
||||
}
|
||||
return `${toSnakeCase(key)}=${formatValue(value[key])}`;
|
||||
}).join(', ');
|
||||
}
|
||||
|
|
@ -242,8 +257,9 @@ function formatContextOptions(options: BrowserContextOptions, deviceName: string
|
|||
// recordHAR is replaced with routeFromHAR in the generated code.
|
||||
options = { ...options, recordHar: undefined };
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device)
|
||||
if (!device) {
|
||||
return formatOptions(options, false, asDict);
|
||||
}
|
||||
return `**playwright.devices[${quote(deviceName!)}]` + formatOptions(sanitizeDeviceOptions(device, options), true, asDict);
|
||||
}
|
||||
|
||||
|
|
@ -273,8 +289,9 @@ class PythonFormatter {
|
|||
let spaces = '';
|
||||
const lines: string[] = [];
|
||||
this._lines.forEach((line: string) => {
|
||||
if (line === '')
|
||||
if (line === '') {
|
||||
return lines.push(line);
|
||||
}
|
||||
if (line === '}') {
|
||||
spaces = spaces.substring(this._baseIndent.length);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -42,8 +42,9 @@ export class ConsoleMessage {
|
|||
}
|
||||
|
||||
text(): string {
|
||||
if (this._text === undefined)
|
||||
if (this._text === undefined) {
|
||||
this._text = this._args.map(arg => arg.preview()).join(' ');
|
||||
}
|
||||
return this._text;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,12 +29,15 @@ class Cookie {
|
|||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.4
|
||||
matches(url: URL): boolean {
|
||||
if (this._raw.secure && (url.protocol !== 'https:' && url.hostname !== 'localhost'))
|
||||
if (this._raw.secure && (url.protocol !== 'https:' && url.hostname !== 'localhost')) {
|
||||
return false;
|
||||
if (!domainMatches(url.hostname, this._raw.domain))
|
||||
}
|
||||
if (!domainMatches(url.hostname, this._raw.domain)) {
|
||||
return false;
|
||||
if (!pathMatches(url.pathname, this._raw.path))
|
||||
}
|
||||
if (!pathMatches(url.pathname, this._raw.path)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -53,8 +56,9 @@ class Cookie {
|
|||
}
|
||||
|
||||
expired() {
|
||||
if (this._raw.expires === -1)
|
||||
if (this._raw.expires === -1) {
|
||||
return false;
|
||||
}
|
||||
return this._raw.expires * 1000 < Date.now();
|
||||
}
|
||||
}
|
||||
|
|
@ -63,23 +67,26 @@ export class CookieStore {
|
|||
private readonly _nameToCookies: Map<string, Set<Cookie>> = new Map();
|
||||
|
||||
addCookies(cookies: channels.NetworkCookie[]) {
|
||||
for (const cookie of cookies)
|
||||
for (const cookie of cookies) {
|
||||
this._addCookie(new Cookie(cookie));
|
||||
}
|
||||
}
|
||||
|
||||
cookies(url: URL): channels.NetworkCookie[] {
|
||||
const result = [];
|
||||
for (const cookie of this._cookiesIterator()) {
|
||||
if (cookie.matches(url))
|
||||
if (cookie.matches(url)) {
|
||||
result.push(cookie.networkCookie());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
allCookies(): channels.NetworkCookie[] {
|
||||
const result = [];
|
||||
for (const cookie of this._cookiesIterator())
|
||||
for (const cookie of this._cookiesIterator()) {
|
||||
result.push(cookie.networkCookie());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -91,9 +98,10 @@ export class CookieStore {
|
|||
}
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.3
|
||||
for (const other of set) {
|
||||
if (other.equals(cookie))
|
||||
if (other.equals(cookie)) {
|
||||
set.delete(other);
|
||||
}
|
||||
}
|
||||
set.add(cookie);
|
||||
CookieStore.pruneExpired(set);
|
||||
}
|
||||
|
|
@ -101,20 +109,23 @@ export class CookieStore {
|
|||
private *_cookiesIterator(): IterableIterator<Cookie> {
|
||||
for (const [name, cookies] of this._nameToCookies) {
|
||||
CookieStore.pruneExpired(cookies);
|
||||
for (const cookie of cookies)
|
||||
for (const cookie of cookies) {
|
||||
yield cookie;
|
||||
if (cookies.size === 0)
|
||||
}
|
||||
if (cookies.size === 0) {
|
||||
this._nameToCookies.delete(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static pruneExpired(cookies: Set<Cookie>) {
|
||||
for (const cookie of cookies) {
|
||||
if (cookie.expired())
|
||||
if (cookie.expired()) {
|
||||
cookies.delete(cookie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RawCookie = {
|
||||
name: string,
|
||||
|
|
@ -143,8 +154,9 @@ export function parseRawCookie(header: string): RawCookie | null {
|
|||
}
|
||||
return [key, value];
|
||||
});
|
||||
if (!pairs.length)
|
||||
if (!pairs.length) {
|
||||
return null;
|
||||
}
|
||||
const [name, value] = pairs[0];
|
||||
const cookie: RawCookie = {
|
||||
name,
|
||||
|
|
@ -157,11 +169,12 @@ export function parseRawCookie(header: string): RawCookie | null {
|
|||
const expiresMs = (+new Date(value));
|
||||
// https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.1
|
||||
if (isFinite(expiresMs)) {
|
||||
if (expiresMs <= 0)
|
||||
if (expiresMs <= 0) {
|
||||
cookie.expires = 0;
|
||||
else
|
||||
} else {
|
||||
cookie.expires = Math.min(expiresMs / 1000, kMaxCookieExpiresDateInSeconds);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'max-age':
|
||||
const maxAgeSec = parseInt(value, 10);
|
||||
|
|
@ -169,16 +182,18 @@ export function parseRawCookie(header: string): RawCookie | null {
|
|||
// From https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.2
|
||||
// If delta-seconds is less than or equal to zero (0), let expiry-time
|
||||
// be the earliest representable date and time.
|
||||
if (maxAgeSec <= 0)
|
||||
if (maxAgeSec <= 0) {
|
||||
cookie.expires = 0;
|
||||
else
|
||||
} else {
|
||||
cookie.expires = Math.min(Date.now() / 1000 + maxAgeSec, kMaxCookieExpiresDateInSeconds);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'domain':
|
||||
cookie.domain = value.toLocaleLowerCase() || '';
|
||||
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.'))
|
||||
if (cookie.domain && !cookie.domain.startsWith('.') && cookie.domain.includes('.')) {
|
||||
cookie.domain = '.' + cookie.domain;
|
||||
}
|
||||
break;
|
||||
case 'path':
|
||||
cookie.path = value || '';
|
||||
|
|
@ -208,21 +223,26 @@ export function parseRawCookie(header: string): RawCookie | null {
|
|||
}
|
||||
|
||||
export function domainMatches(value: string, domain: string): boolean {
|
||||
if (value === domain)
|
||||
if (value === domain) {
|
||||
return true;
|
||||
}
|
||||
// Only strict match is allowed if domain doesn't start with '.' (host-only-flag is true in the spec)
|
||||
if (!domain.startsWith('.'))
|
||||
if (!domain.startsWith('.')) {
|
||||
return false;
|
||||
}
|
||||
value = '.' + value;
|
||||
return value.endsWith(domain);
|
||||
}
|
||||
|
||||
function pathMatches(value: string, path: string): boolean {
|
||||
if (value === path)
|
||||
if (value === path) {
|
||||
return true;
|
||||
if (!value.endsWith('/'))
|
||||
}
|
||||
if (!value.endsWith('/')) {
|
||||
value = value + '/';
|
||||
if (!path.endsWith('/'))
|
||||
}
|
||||
if (!path.endsWith('/')) {
|
||||
path = path + '/';
|
||||
}
|
||||
return value.startsWith(path);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,16 +82,19 @@ export class DebugController extends SdkObject {
|
|||
|
||||
async resetForReuse() {
|
||||
const contexts = new Set<BrowserContext>();
|
||||
for (const page of this._playwright.allPages())
|
||||
for (const page of this._playwright.allPages()) {
|
||||
contexts.add(page.context());
|
||||
for (const context of contexts)
|
||||
}
|
||||
for (const context of contexts) {
|
||||
await context.resetForReuse(internalMetadata, null);
|
||||
}
|
||||
}
|
||||
|
||||
async navigate(url: string) {
|
||||
for (const p of this._playwright.allPages())
|
||||
for (const p of this._playwright.allPages()) {
|
||||
await p.mainFrame().goto(internalMetadata, url);
|
||||
}
|
||||
}
|
||||
|
||||
async setRecorderMode(params: { mode: Mode, file?: string, testIdAttributeName?: string }) {
|
||||
// TODO: |file| is only used in the legacy mode.
|
||||
|
|
@ -106,8 +109,9 @@ export class DebugController extends SdkObject {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!this._playwright.allBrowsers().length)
|
||||
if (!this._playwright.allBrowsers().length) {
|
||||
await this._playwright.chromium.launch(internalMetadata, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS });
|
||||
}
|
||||
// Create page if none.
|
||||
const pages = this._playwright.allPages();
|
||||
if (!pages.length) {
|
||||
|
|
@ -117,56 +121,65 @@ export class DebugController extends SdkObject {
|
|||
}
|
||||
// Update test id attribute.
|
||||
if (params.testIdAttributeName) {
|
||||
for (const page of this._playwright.allPages())
|
||||
for (const page of this._playwright.allPages()) {
|
||||
page.context().selectors().setTestIdAttributeName(params.testIdAttributeName);
|
||||
}
|
||||
}
|
||||
// Toggle the mode.
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
recorder.hideHighlightedSelector();
|
||||
if (params.mode !== 'inspecting')
|
||||
if (params.mode !== 'inspecting') {
|
||||
recorder.setOutput(this._codegenId, params.file);
|
||||
}
|
||||
recorder.setMode(params.mode);
|
||||
}
|
||||
this.setAutoCloseEnabled(true);
|
||||
}
|
||||
|
||||
async setAutoCloseEnabled(enabled: boolean) {
|
||||
if (!this._autoCloseAllowed)
|
||||
if (!this._autoCloseAllowed) {
|
||||
return;
|
||||
if (this._autoCloseTimer)
|
||||
}
|
||||
if (this._autoCloseTimer) {
|
||||
clearTimeout(this._autoCloseTimer);
|
||||
if (!enabled)
|
||||
}
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
const heartBeat = () => {
|
||||
if (!this._playwright.allPages().length)
|
||||
if (!this._playwright.allPages().length) {
|
||||
gracefullyProcessExitDoNotHang(0);
|
||||
else
|
||||
} else {
|
||||
this._autoCloseTimer = setTimeout(heartBeat, 5000);
|
||||
}
|
||||
};
|
||||
this._autoCloseTimer = setTimeout(heartBeat, 30000);
|
||||
}
|
||||
|
||||
async highlight(params: { selector?: string, ariaTemplate?: string }) {
|
||||
// Assert parameters validity.
|
||||
if (params.selector)
|
||||
if (params.selector) {
|
||||
unsafeLocatorOrSelectorAsSelector(this._sdkLanguage, params.selector, 'data-testid');
|
||||
}
|
||||
let parsedYaml: ParsedYaml | undefined;
|
||||
if (params.ariaTemplate) {
|
||||
parsedYaml = parseYamlForAriaSnapshot(params.ariaTemplate);
|
||||
parseYamlTemplate(parsedYaml);
|
||||
}
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
if (parsedYaml)
|
||||
if (parsedYaml) {
|
||||
recorder.setHighlightedAriaTemplate(parsedYaml);
|
||||
else if (params.selector)
|
||||
} else if (params.selector) {
|
||||
recorder.setHighlightedSelector(this._sdkLanguage, params.selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async hideHighlight() {
|
||||
// Hide all active recorder highlights.
|
||||
for (const recorder of await this._allRecorders())
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
recorder.hideHighlightedSelector();
|
||||
}
|
||||
// Hide all locator.highlight highlights.
|
||||
await this._playwright.hideHighlight();
|
||||
}
|
||||
|
|
@ -176,9 +189,10 @@ export class DebugController extends SdkObject {
|
|||
}
|
||||
|
||||
async resume() {
|
||||
for (const recorder of await this._allRecorders())
|
||||
for (const recorder of await this._allRecorders()) {
|
||||
recorder.resume();
|
||||
}
|
||||
}
|
||||
|
||||
async kill() {
|
||||
gracefullyProcessExitDoNotHang(0);
|
||||
|
|
@ -201,8 +215,9 @@ export class DebugController extends SdkObject {
|
|||
pages: [] as any[]
|
||||
};
|
||||
b.contexts.push(c);
|
||||
for (const page of context.pages())
|
||||
for (const page of context.pages()) {
|
||||
c.pages.push(page.mainFrame().url());
|
||||
}
|
||||
pageCount += context.pages().length;
|
||||
}
|
||||
}
|
||||
|
|
@ -211,8 +226,9 @@ export class DebugController extends SdkObject {
|
|||
|
||||
private async _allRecorders(): Promise<Recorder[]> {
|
||||
const contexts = new Set<BrowserContext>();
|
||||
for (const page of this._playwright.allPages())
|
||||
for (const page of this._playwright.allPages()) {
|
||||
contexts.add(page.context());
|
||||
}
|
||||
const result = await Promise.all([...contexts].map(c => Recorder.showInspector(c, { omitCallTracking: true }, () => Promise.resolve(new InspectingRecorderApp(this)))));
|
||||
return result.filter(Boolean) as Recorder[];
|
||||
}
|
||||
|
|
@ -220,14 +236,16 @@ export class DebugController extends SdkObject {
|
|||
private async _closeBrowsersWithoutPages() {
|
||||
for (const browser of this._playwright.allBrowsers()) {
|
||||
for (const context of browser.contexts()) {
|
||||
if (!context.pages().length)
|
||||
if (!context.pages().length) {
|
||||
await context.close({ reason: 'Browser collected' });
|
||||
}
|
||||
if (!browser.contexts())
|
||||
}
|
||||
if (!browser.contexts()) {
|
||||
await browser.close({ reason: 'Browser collected' });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InspectingRecorderApp extends EmptyRecorderApp {
|
||||
private _debugController: DebugController;
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
|
|||
this._context = context;
|
||||
(this._context as any)[symbol] = this;
|
||||
this._enabled = debugMode() === 'inspector';
|
||||
if (this._enabled)
|
||||
if (this._enabled) {
|
||||
this.pauseOnNextStatement();
|
||||
}
|
||||
context.instrumentation.addListener(this, context);
|
||||
this._context.once(BrowserContext.Events.Close, () => {
|
||||
this._context.instrumentation.removeListener(this);
|
||||
|
|
@ -53,31 +54,37 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
|
|||
}
|
||||
|
||||
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (this._muted)
|
||||
if (this._muted) {
|
||||
return;
|
||||
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseBeforeStep(metadata)))
|
||||
}
|
||||
if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseBeforeStep(metadata))) {
|
||||
await this.pause(sdkObject, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
async _doSlowMo() {
|
||||
await new Promise(f => setTimeout(f, this._slowMo));
|
||||
}
|
||||
|
||||
async onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (this._slowMo && shouldSlowMo(metadata))
|
||||
if (this._slowMo && shouldSlowMo(metadata)) {
|
||||
await this._doSlowMo();
|
||||
}
|
||||
}
|
||||
|
||||
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
|
||||
if (this._muted)
|
||||
if (this._muted) {
|
||||
return;
|
||||
if (this._enabled && this._pauseOnNextStatement)
|
||||
}
|
||||
if (this._enabled && this._pauseOnNextStatement) {
|
||||
await this.pause(sdkObject, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
async pause(sdkObject: SdkObject, metadata: CallMetadata) {
|
||||
if (this._muted)
|
||||
if (this._muted) {
|
||||
return;
|
||||
}
|
||||
this._enabled = true;
|
||||
metadata.pauseStartTime = monotonicTime();
|
||||
const result = new Promise<void>(resolve => {
|
||||
|
|
@ -88,8 +95,9 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
|
|||
}
|
||||
|
||||
resume(step: boolean) {
|
||||
if (!this.isPaused())
|
||||
if (!this.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pauseOnNextStatement = step;
|
||||
const endTime = monotonicTime();
|
||||
|
|
@ -106,36 +114,43 @@ export class Debugger extends EventEmitter implements InstrumentationListener {
|
|||
}
|
||||
|
||||
isPaused(metadata?: CallMetadata): boolean {
|
||||
if (metadata)
|
||||
if (metadata) {
|
||||
return this._pausedCallsMetadata.has(metadata);
|
||||
}
|
||||
return !!this._pausedCallsMetadata.size;
|
||||
}
|
||||
|
||||
pausedDetails(): { metadata: CallMetadata, sdkObject: SdkObject }[] {
|
||||
const result: { metadata: CallMetadata, sdkObject: SdkObject }[] = [];
|
||||
for (const [metadata, { sdkObject }] of this._pausedCallsMetadata)
|
||||
for (const [metadata, { sdkObject }] of this._pausedCallsMetadata) {
|
||||
result.push({ metadata, sdkObject });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
|
||||
if (sdkObject.attribution.playwright.options.isServer)
|
||||
if (sdkObject.attribution.playwright.options.isServer) {
|
||||
return false;
|
||||
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
|
||||
}
|
||||
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest()) {
|
||||
return false;
|
||||
}
|
||||
return metadata.method === 'pause';
|
||||
}
|
||||
|
||||
function shouldPauseBeforeStep(metadata: CallMetadata): boolean {
|
||||
// Don't stop on internal.
|
||||
if (!metadata.apiName)
|
||||
if (!metadata.apiName) {
|
||||
return false;
|
||||
}
|
||||
// Always stop on 'close'
|
||||
if (metadata.method === 'close')
|
||||
if (metadata.method === 'close') {
|
||||
return true;
|
||||
if (metadata.method === 'waitForSelector' || metadata.method === 'waitForEventInfo')
|
||||
return false; // Never stop on those, primarily for the test harness.
|
||||
}
|
||||
if (metadata.method === 'waitForSelector' || metadata.method === 'waitForEventInfo') {
|
||||
return false;
|
||||
} // Never stop on those, primarily for the test harness.
|
||||
const step = metadata.type + '.' + metadata.method;
|
||||
// Stop before everything that generates snapshot. But don't stop before those marked as pausesBeforeInputActions
|
||||
// since we stop in them on a separate instrumentation signal.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue