Compare commits

...

14 commits

Author SHA1 Message Date
Pavel Feldman 5e12a4a52e cherry-pick(release-1.16): keep sw alive
PR #9725
SHA e2710451f3
2021-10-22 12:15:33 -10:00
Joel Einbinder 9edf0ed711
cherry-pick(release-1.16): don't ship src in @playwright/test (#9722)
PR: #9717 
Commit: 940466f830
2021-10-22 13:25:41 -07:00
Andrey Lushnikov 627bed416d
chore: mark v1.16.1 (#9721) 2021-10-22 11:11:43 -07:00
Max Schmitt d9d9417cfb cherrty-pick(#9713): chore: pin expect package 2021-10-22 08:16:18 -07:00
Pavel Feldman aa7b216383 charry-pick(#9698): feat(trace): add drop trace here label 2021-10-22 08:05:13 -07:00
Pavel Feldman 9c1f3bc46a charry-pick(#9697): fix(trace-viewer): restore dragndrop 2021-10-22 08:04:11 -07:00
Pavel Feldman 455683b29d cherry-pick(release-1.16.1): fix locator stacks to hide internal detail
PR #9693 SHA 6a3e08d1ac
2021-10-21 14:07:33 -10:00
Pavel Feldman a0321f44ec cherry-pick(release-1.16.1): fix toHaveCount(0)
This cherry-picks PR #9690 SHA 299dffbdb3
2021-10-21 11:47:51 -10:00
Andrey Lushnikov 945bbf2828
chore: mark v1.16.0 (#9663) 2021-10-20 18:53:47 -07:00
Pavel Feldman cafe5f3fd1 cherry-pick(release-1.16): allow serving html report w/ traces off relative path
Cherry-picks PR #9668 SHA f853176df3

Fixes #9652
2021-10-20 15:31:26 -10:00
Andrey Lushnikov 895585ddce cherry-pick(release-1.16): do not show negative time in action duration (#9669)
This cherry-picks 962525b592
2021-10-20 14:30:02 -10:00
Andrey Lushnikov dc9d4ceffe cherry-pick(release-1.16): fix action icons to be always visible (#9666)
This cherry-picks a9bf96e1bd
2021-10-20 14:29:38 -10:00
Andrey Lushnikov 99c5df9c61
cherry-pick(release-1.16): Revert "chore: print global errors when running html report" (#9662)
This reverts commit 68c9fce507.

Co-authored-by: Pavel Feldman <pavel.feldman@gmail.com>
2021-10-20 13:10:07 -07:00
Pavel Feldman 2b62811fd6 cherry-pick(#9659): fix(expect): do not fail on navigated frames while polling 2021-10-20 13:03:51 -07:00
28 changed files with 266 additions and 1710 deletions

1661
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{ {
"name": "playwright-internal", "name": "playwright-internal",
"private": true, "private": true,
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",

View file

@ -23,7 +23,7 @@
}, },
"../playwright-test": { "../playwright-test": {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.16.0-next", "version": "1.16.1",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@ -52,7 +52,7 @@
"open": "^8.3.0", "open": "^8.3.0",
"pirates": "^4.0.1", "pirates": "^4.0.1",
"pixelmatch": "^5.2.1", "pixelmatch": "^5.2.1",
"playwright-core": "=1.16.0-next", "playwright-core": "=1.16.1",
"source-map-support": "^0.4.18" "source-map-support": "^0.4.18"
}, },
"engines": { "engines": {
@ -142,7 +142,7 @@
"open": "^8.3.0", "open": "^8.3.0",
"pirates": "^4.0.1", "pirates": "^4.0.1",
"pixelmatch": "^5.2.1", "pixelmatch": "^5.2.1",
"playwright-core": "=1.16.0-next", "playwright-core": "=1.16.1",
"source-map-support": "^0.4.18" "source-map-support": "^0.4.18"
} }
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-chromium", "name": "playwright-chromium",
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate Chromium", "description": "A high-level API to automate Chromium",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -25,6 +25,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "=1.16.0-next" "playwright-core": "=1.16.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-core", "name": "playwright-core",
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",

View file

@ -36,14 +36,18 @@ export class Locator implements api.Locator {
private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> { private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
timeout = this._frame.page()._timeoutSettings.timeout({ timeout }); timeout = this._frame.page()._timeoutSettings.timeout({ timeout });
const deadline = timeout ? monotonicTime() + timeout : 0; const deadline = timeout ? monotonicTime() + timeout : 0;
const handle = await this.elementHandle({ timeout });
if (!handle) return this._frame._wrapApiCall<R>(async (channel: channels.FrameChannel) => {
throw new Error(`Could not resolve ${this._selector} to DOM Element`); const result = await channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout });
try { const handle = ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
return await task(handle, deadline ? deadline - monotonicTime() : 0); if (!handle)
} finally { throw new Error(`Could not resolve ${this._selector} to DOM Element`);
await handle.dispose(); try {
} return await task(handle, deadline ? deadline - monotonicTime() : 0);
} finally {
await handle.dispose();
}
});
} }
async boundingBox(options?: TimeoutOptions): Promise<Rect | null> { async boundingBox(options?: TimeoutOptions): Promise<Rect | null> {

View file

@ -505,7 +505,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (poll === 'error:notconnected') if (poll === 'error:notconnected')
return poll; return poll;
const pollHandler = new InjectedScriptPollHandler(progress, poll); const pollHandler = new InjectedScriptPollHandler(progress, poll);
const result = await pollHandler.finish(); const result = await pollHandler.finishMaybeNotConnected();
await this._page._doSlowMo(); await this._page._doSlowMo();
return result; return result;
}); });
@ -530,7 +530,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (poll === 'error:notconnected') if (poll === 'error:notconnected')
return poll; return poll;
const pollHandler = new InjectedScriptPollHandler(progress, poll); const pollHandler = new InjectedScriptPollHandler(progress, poll);
const filled = await pollHandler.finish(); const filled = await pollHandler.finishMaybeNotConnected();
progress.throwIfAborted(); // Avoid action that has side-effects. progress.throwIfAborted(); // Avoid action that has side-effects.
if (filled === 'error:notconnected') if (filled === 'error:notconnected')
return filled; return filled;
@ -556,7 +556,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return injected.waitForElementStatesAndPerformAction(node, ['visible'], force, injected.selectText.bind(injected)); return injected.waitForElementStatesAndPerformAction(node, ['visible'], force, injected.selectText.bind(injected));
}, options.force); }, options.force);
const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll)); const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll));
const result = await pollHandler.finish(); const result = await pollHandler.finishMaybeNotConnected();
assertDone(throwRetargetableDOMError(result)); assertDone(throwRetargetableDOMError(result));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
@ -761,7 +761,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
return injected.waitForElementStatesAndPerformAction(node, [state], false, () => 'done' as const); return injected.waitForElementStatesAndPerformAction(node, [state], false, () => 'done' as const);
}, state); }, state);
const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll)); const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll));
assertDone(throwRetargetableDOMError(await pollHandler.finish())); assertDone(throwRetargetableDOMError(await pollHandler.finishMaybeNotConnected()));
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
@ -808,7 +808,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
if (poll === 'error:notconnected') if (poll === 'error:notconnected')
return poll; return poll;
const pollHandler = new InjectedScriptPollHandler(progress, poll); const pollHandler = new InjectedScriptPollHandler(progress, poll);
const result = await pollHandler.finish(); const result = await pollHandler.finishMaybeNotConnected();
if (waitForEnabled) if (waitForEnabled)
progress.log(' element is visible, enabled and stable'); progress.log(' element is visible, enabled and stable');
else else
@ -867,7 +867,17 @@ export class InjectedScriptPollHandler<T> {
} }
} }
async finish(): Promise<T | 'error:notconnected'> { async finish(): Promise<T> {
try {
const result = await this._poll!.evaluate(poll => poll.run());
await this._finishInternal();
return result;
} finally {
await this.cancel();
}
}
async finishMaybeNotConnected(): Promise<T | 'error:notconnected'> {
try { try {
const result = await this._poll!.evaluate(poll => poll.run()); const result = await this._poll!.evaluate(poll => poll.run());
await this._finishInternal(); await this._finishInternal();

View file

@ -38,7 +38,7 @@ type ContextData = {
contextPromise: Promise<dom.FrameExecutionContext>; contextPromise: Promise<dom.FrameExecutionContext>;
contextResolveCallback: (c: dom.FrameExecutionContext) => void; contextResolveCallback: (c: dom.FrameExecutionContext) => void;
context: dom.FrameExecutionContext | null; context: dom.FrameExecutionContext | null;
rerunnableTasks: Set<RerunnableTask>; rerunnableTasks: Set<RerunnableTask<any>>;
}; };
type DocumentInfo = { type DocumentInfo = {
@ -1181,6 +1181,11 @@ export class Frame extends SdkObject {
if (options.expression.endsWith('.array') && expectsEmptyList !== options.isNot) if (options.expression.endsWith('.array') && expectsEmptyList !== options.isNot)
return { matches: expectsEmptyList }; return { matches: expectsEmptyList };
// expect(listLocator).toHaveCount(0) passes when there are no elements matching.
// expect(listLocator).not.toHaveCount(1) passes when there are no elements matching.
if (options.expression === 'to.have.count')
return { matches: options.expectedNumber === 0, received: options.expectedNumber };
// When none of the above applies, keep waiting for the element. // When none of the above applies, keep waiting for the element.
return continuePolling; return continuePolling;
} }
@ -1201,6 +1206,8 @@ export class Frame extends SdkObject {
}, options, { strict: true, querySelectorAll, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => { }, options, { strict: true, querySelectorAll, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
if (js.isJavaScriptErrorInEvaluate(e)) if (js.isJavaScriptErrorInEvaluate(e))
throw e; throw e;
// Q: Why not throw upon isSessionClosedError(e) as in other places?
// A: We want user to receive a friendly message containing the last intermediate result.
return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log }; return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log };
}); });
} }
@ -1280,7 +1287,7 @@ export class Frame extends SdkObject {
return controller.run(async progress => { return controller.run(async progress => {
progress.log(`waiting for selector "${selector}"`); progress.log(`waiting for selector "${selector}"`);
const rerunnableTask = new RerunnableTask(data, progress, injectedScript => { const rerunnableTask = new RerunnableTask<R>(data, progress, injectedScript => {
return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached, snapshotName }) => { return injectedScript.evaluateHandle((injected, { info, taskData, callbackText, querySelectorAll, logScale, omitAttached, snapshotName }) => {
const callback = injected.eval(callbackText) as DomTaskBody<T, R, Element | undefined>; const callback = injected.eval(callbackText) as DomTaskBody<T, R, Element | undefined>;
const poller = logScale ? injected.pollLogScale.bind(injected) : injected.pollRaf.bind(injected); const poller = logScale ? injected.pollLogScale.bind(injected) : injected.pollRaf.bind(injected);
@ -1324,18 +1331,18 @@ export class Frame extends SdkObject {
rerunnableTask.terminate(new Error('Frame got detached.')); rerunnableTask.terminate(new Error('Frame got detached.'));
if (data.context) if (data.context)
rerunnableTask.rerun(data.context); rerunnableTask.rerun(data.context);
return await rerunnableTask.promise; return await rerunnableTask.promise!;
}, this._page._timeoutSettings.timeout(options)); }, this._page._timeoutSettings.timeout(options));
} }
private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> { private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
const data = this._contextData.get(world)!; const data = this._contextData.get(world)!;
const rerunnableTask = new RerunnableTask(data, progress, task, false /* returnByValue */); const rerunnableTask = new RerunnableTask<T>(data, progress, task, false /* returnByValue */);
if (this._detached) if (this._detached)
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.')); rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
if (data.context) if (data.context)
rerunnableTask.rerun(data.context); rerunnableTask.rerun(data.context);
return rerunnableTask.promise; return rerunnableTask.handlePromise!;
} }
private _setContext(world: types.World, context: dom.FrameExecutionContext | null) { private _setContext(world: types.World, context: dom.FrameExecutionContext | null) {
@ -1394,31 +1401,42 @@ export class Frame extends SdkObject {
} }
} }
class RerunnableTask { class RerunnableTask<T> {
readonly promise: Promise<any>; readonly promise: ManualPromise<T> | undefined;
private _task: dom.SchedulableTask<any>; readonly handlePromise: ManualPromise<js.SmartHandle<T>> | undefined;
private _resolve: (result: any) => void = () => {}; private _task: dom.SchedulableTask<T>;
private _reject: (reason: Error) => void = () => {};
private _progress: Progress; private _progress: Progress;
private _returnByValue: boolean; private _returnByValue: boolean;
private _contextData: ContextData; private _contextData: ContextData;
constructor(data: ContextData, progress: Progress, task: dom.SchedulableTask<any>, returnByValue: boolean) { constructor(data: ContextData, progress: Progress, task: dom.SchedulableTask<T>, returnByValue: boolean) {
this._task = task; this._task = task;
this._progress = progress; this._progress = progress;
this._returnByValue = returnByValue; this._returnByValue = returnByValue;
if (returnByValue)
this.promise = new ManualPromise<T>();
else
this.handlePromise = new ManualPromise<js.SmartHandle<T>>();
this._contextData = data; this._contextData = data;
this._contextData.rerunnableTasks.add(this); this._contextData.rerunnableTasks.add(this);
this.promise = new Promise<any>((resolve, reject) => {
// The task is either resolved with a value, or rejected with a meaningful evaluation error.
this._resolve = resolve;
this._reject = reject;
});
} }
terminate(error: Error) { terminate(error: Error) {
this._reject(error); this._reject(error);
} }
private _resolve(value: T | js.SmartHandle<T>) {
if (this.promise)
this.promise.resolve(value as T);
if (this.handlePromise)
this.handlePromise.resolve(value as js.SmartHandle<T>);
}
private _reject(error: Error) {
if (this.promise)
this.promise.reject(error);
if (this.handlePromise)
this.handlePromise.reject(error);
}
async rerun(context: dom.FrameExecutionContext) { async rerun(context: dom.FrameExecutionContext) {
try { try {

View file

@ -77,6 +77,6 @@ export async function showTraceViewer(traceUrl: string, browserName: string, hea
else else
page.on('close', () => process.exit()); page.on('close', () => process.exit());
await page.mainFrame().goto(internalCallMetadata(), urlPrefix + `/trace/index.html?trace=${traceUrl}`); await page.mainFrame().goto(internalCallMetadata(), urlPrefix + `/trace/index.html${traceUrl ? '?trace=' + traceUrl : ''}`);
return context; return context;
} }

View file

@ -188,7 +188,7 @@ const TestResultView: React.FC<{
{!!traces.length && <Chip header='Traces'> {!!traces.length && <Chip header='Traces'>
{traces.map((a, i) => <div key={`trace-${i}`}> {traces.map((a, i) => <div key={`trace-${i}`}>
<a href={`trace/index.html?trace=${window.location.origin}/` + a.path}> <a href={`trace/index.html?trace=${new URL(a.path!, window.location.href)}`}>
<img src='trace.png' style={{ width: 192, height: 117, marginLeft: 20 }} /> <img src='trace.png' style={{ width: 192, height: 117, marginLeft: 20 }} />
</a> </a>
</div>)} </div>)}

View file

@ -19,9 +19,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" sizes="32x32" href="/trace/icon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="icon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/trace/icon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="icon-16x16.png">
<link rel="manifest" href="/trace/manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest">
<title>Playwright Trace Viewer</title> <title>Playwright Trace Viewer</title>
</head> </head>
<body> <body>

View file

@ -23,13 +23,13 @@ import '../common.css';
(async () => { (async () => {
applyTheme(); applyTheme();
navigator.serviceWorker.register('/trace/sw.bundle.js', { navigator.serviceWorker.register('sw.bundle.js');
scope: '/trace/'
});
if (!navigator.serviceWorker.controller) { if (!navigator.serviceWorker.controller) {
await new Promise<void>(f => { await new Promise<void>(f => {
navigator.serviceWorker.oncontrollerchange = () => f(); navigator.serviceWorker.oncontrollerchange = () => f();
}); });
} }
// Keep SW running.
setInterval(function() { fetch('ping'); }, 10000);
ReactDOM.render(<Workbench/>, document.querySelector('#root')); ReactDOM.render(<Workbench/>, document.querySelector('#root'));
})(); })();

View file

@ -2,28 +2,27 @@
"theme_color": "#000", "theme_color": "#000",
"background_color": "#fff", "background_color": "#fff",
"display": "browser", "display": "browser",
"scope": "/trace", "start_url": "index.html",
"start_url": "/trace/index.html",
"name": "Playwright Trace Viewer", "name": "Playwright Trace Viewer",
"short_name": "Trace Viewer", "short_name": "Trace Viewer",
"icons": [ "icons": [
{ {
"src": "/trace/icon-192x192.png", "src": "icon-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/trace/icon-256x256.png", "src": "icon-256x256.png",
"sizes": "256x256", "sizes": "256x256",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/trace/icon-384x384.png", "src": "icon-384x384.png",
"sizes": "384x384", "sizes": "384x384",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "/trace/icon-512x512.png", "src": "icon-512x512.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
} }

View file

@ -56,8 +56,12 @@ async function doFetch(event: FetchEvent): Promise<Response> {
const url = new URL(request.url); const url = new URL(request.url);
const relativePath = url.pathname.substring(scopePath.length - 1); const relativePath = url.pathname.substring(scopePath.length - 1);
if (relativePath === '/context') { if (relativePath === '/ping') {
await gc(); await gc();
return new Response(null, { status: 200 });
}
if (relativePath === '/context') {
const traceModel = await loadTrace(traceUrl, event.clientId); const traceModel = await loadTrace(traceUrl, event.clientId);
return new Response(JSON.stringify(traceModel!.contextEntry), { return new Response(JSON.stringify(traceModel!.contextEntry), {
status: 200, status: 200,

View file

@ -77,6 +77,7 @@
} }
.action-icons { .action-icons {
flex: none;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
cursor: pointer; cursor: pointer;

View file

@ -90,7 +90,7 @@ export const ActionList: React.FC<ActionListProps> = ({
<span>{metadata.apiName}</span> <span>{metadata.apiName}</span>
{metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>} {metadata.params.selector && <div className='action-selector' title={metadata.params.selector}>{metadata.params.selector}</div>}
{metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>} {metadata.method === 'goto' && metadata.params.url && <div className='action-url' title={metadata.params.url}>{metadata.params.url}</div>}
<span className='action-duration'> {msToString(metadata.endTime - metadata.startTime)}</span> <span className='action-duration'> {metadata.endTime ? msToString(metadata.endTime - metadata.startTime) : 'Timed Out'}</span>
</div> </div>
<div className='action-icons' onClick={() => setSelectedTab('console')}> <div className='action-icons' onClick={() => setSelectedTab('console')}>
{!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>} {!!errors && <div className='action-icon'><span className={'codicon codicon-error'}></span><span className="action-icon-value">{errors}</span></div>}

View file

@ -14,6 +14,17 @@
limitations under the License. limitations under the License.
*/ */
.drop-target {
display: flex;
align-items: center;
justify-content: center;
box-shadow: var(--box-shadow);
flex: auto;
font-size: 24px;
color: #666;
font-weight: bold;
}
.workbench { .workbench {
contain: size; contain: size;
user-select: none; user-select: none;

View file

@ -37,10 +37,22 @@ export const Workbench: React.FunctionComponent<{
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>(); const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
const [selectedTab, setSelectedTab] = React.useState<string>('logs'); const [selectedTab, setSelectedTab] = React.useState<string>('logs');
const handleDropEvent = (event: any) => {
event.preventDefault();
const blobTraceURL = URL.createObjectURL(event.dataTransfer.files[0]);
const url = new URL(window.location.href);
url.searchParams.set('trace', blobTraceURL);
const href = url.toString();
// Snapshot loaders will inherit the trace url from the query parameters,
// so set it here.
window.history.pushState({}, '', href);
setTraceURL(blobTraceURL);
};
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
if (traceURL) { if (traceURL) {
const contextEntry = (await fetch(`/trace/context?trace=${traceURL}`).then(response => response.json())) as ContextEntry; const contextEntry = (await fetch(`context?trace=${traceURL}`).then(response => response.json())) as ContextEntry;
modelUtil.indexModel(contextEntry); modelUtil.indexModel(contextEntry);
setContextEntry(contextEntry); setContextEntry(contextEntry);
} else { } else {
@ -52,6 +64,21 @@ export const Workbench: React.FunctionComponent<{
const defaultSnapshotSize = contextEntry.options.viewport || { width: 1280, height: 720 }; const defaultSnapshotSize = contextEntry.options.viewport || { width: 1280, height: 720 };
const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime }; const boundaries = { minimum: contextEntry.startTime, maximum: contextEntry.endTime };
if (!traceURL) {
return <div className='vbox workbench'>
<div className='hbox header'>
<div className='logo'>🎭</div>
<div className='product'>Playwright</div>
<div className='spacer'></div>
</div>
<div className='drop-target'
onDragOver={event => { event.preventDefault(); }}
onDrop={event => handleDropEvent(event)}>
Drop Playwright Trace here
</div>
</div>;
}
// Leave some nice free space on the right hand side. // Leave some nice free space on the right hand side.
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20; boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
const { errors, warnings } = selectedAction ? modelUtil.stats(selectedAction) : { errors: 0, warnings: 0 }; const { errors, warnings } = selectedAction ? modelUtil.stats(selectedAction) : { errors: 0, warnings: 0 };
@ -60,11 +87,7 @@ export const Workbench: React.FunctionComponent<{
return <div className='vbox workbench' return <div className='vbox workbench'
onDragOver={event => { event.preventDefault(); }} onDragOver={event => { event.preventDefault(); }}
onDrop={event => { onDrop={event => handleDropEvent(event)}>
event.preventDefault();
const url = URL.createObjectURL(event.dataTransfer.files[0]);
setTraceURL(url.toString());
}}>
<div className='hbox header'> <div className='hbox header'>
<div className='logo'>🎭</div> <div className='logo'>🎭</div>
<div className='product'>Playwright</div> <div className='product'>Playwright</div>

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-firefox", "name": "playwright-firefox",
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate Firefox", "description": "A high-level API to automate Firefox",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -25,6 +25,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "=1.16.0-next" "playwright-core": "=1.16.1"
} }
} }

View file

@ -0,0 +1 @@
src

View file

@ -1,6 +1,6 @@
{ {
"name": "@playwright/test", "name": "@playwright/test",
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -43,15 +43,15 @@
"@babel/preset-typescript": "^7.14.5", "@babel/preset-typescript": "^7.14.5",
"colors": "^1.4.0", "colors": "^1.4.0",
"commander": "^8.2.0", "commander": "^8.2.0",
"expect": "^27.2.5", "expect": "=27.2.5",
"jest-matcher-utils": "^27.2.5", "jest-matcher-utils": "=27.2.5",
"jpeg-js": "^0.4.2", "jpeg-js": "^0.4.2",
"minimatch": "^3.0.3", "minimatch": "^3.0.3",
"ms": "^2.1.2", "ms": "^2.1.2",
"open": "^8.3.0", "open": "^8.3.0",
"pirates": "^4.0.1", "pirates": "^4.0.1",
"pixelmatch": "^5.2.1", "pixelmatch": "^5.2.1",
"playwright-core": "=1.16.0-next", "playwright-core": "=1.16.1",
"pngjs": "^5.0.0", "pngjs": "^5.0.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"source-map-support": "^0.4.18", "source-map-support": "^0.4.18",

View file

@ -98,7 +98,7 @@ export class BaseReporter implements Reporter {
} }
onError(error: TestError) { onError(error: TestError) {
console.log(formatError(error, colors.enabled)); console.log(formatError(error, colors.enabled).message);
} }
async onEnd(result: FullResult) { async onEnd(result: FullResult) {

View file

@ -18,12 +18,11 @@ import colors from 'colors/safe';
import fs from 'fs'; import fs from 'fs';
import open from 'open'; import open from 'open';
import path from 'path'; import path from 'path';
import { FullConfig, Suite, TestError } from '../../types/testReporter'; import { FullConfig, Suite } from '../../types/testReporter';
import { HttpServer } from 'playwright-core/src/utils/httpServer'; import { HttpServer } from 'playwright-core/src/utils/httpServer';
import { calculateSha1, removeFolders } from 'playwright-core/src/utils/utils'; import { calculateSha1, removeFolders } from 'playwright-core/src/utils/utils';
import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw'; import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw';
import assert from 'assert'; import assert from 'assert';
import { formatError } from './base';
export type Stats = { export type Stats = {
total: number; total: number;
@ -141,10 +140,6 @@ class HtmlReporter {
} }
} }
} }
onError(error: TestError) {
console.log(formatError(error, colors.enabled));
}
} }
export function htmlReportFolder(outputFolder?: string): string { export function htmlReportFolder(outputFolder?: string): string {

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright-webkit", "name": "playwright-webkit",
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate WebKit", "description": "A high-level API to automate WebKit",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -25,6 +25,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "=1.16.0-next" "playwright-core": "=1.16.1"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "playwright", "name": "playwright",
"version": "1.16.0-next", "version": "1.16.1",
"description": "A high-level API to automate web browsers", "description": "A high-level API to automate web browsers",
"repository": "github:Microsoft/playwright", "repository": "github:Microsoft/playwright",
"homepage": "https://playwright.dev", "homepage": "https://playwright.dev",
@ -24,6 +24,6 @@
"install": "node install.js" "install": "node install.js"
}, },
"dependencies": { "dependencies": {
"playwright-core": "=1.16.0-next" "playwright-core": "=1.16.1"
} }
} }

View file

@ -0,0 +1,25 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test as it, expect } from './pageTest';
it('should outlive frame navigation', async ({ page, server }) => {
await page.goto(server.EMPTY_PAGE);
setTimeout(async () => {
await page.goto(server.PREFIX + '/grid.html').catch(() => {});
}, 1000);
await expect(page.locator('.box').first()).toBeEmpty();
});

View file

@ -33,10 +33,41 @@ test('should support toHaveCount', async ({ runInlineTest }) => {
await promise; await promise;
expect(done).toBe(true); expect(done).toBe(true);
}); });
test('pass zero', async ({ page }) => {
await page.setContent('<div></div>');
const locator = page.locator('span');
await expect(locator).toHaveCount(0);
await expect(locator).not.toHaveCount(1);
});
test('eventually pass zero', async ({ page }) => {
await page.setContent('<div></div>');
const locator = page.locator('span');
setTimeout(() => page.evaluate(() => div.textContent = '').catch(() => {}), 200);
await expect(locator).toHaveCount(0);
await expect(locator).not.toHaveCount(1);
});
test('fail zero', async ({ page }) => {
await page.setContent('<div><span></span></div>');
const locator = page.locator('span');
await expect(locator).toHaveCount(0, { timeout: 500 });
});
test('fail zero 2', async ({ page }) => {
await page.setContent('<div><span></span></div>');
const locator = page.locator('span');
await expect(locator).not.toHaveCount(1, { timeout: 500 });
});
`, `,
}, { workers: 1 }); }, { workers: 1 });
expect(result.passed).toBe(1); const output = stripAscii(result.output);
expect(result.exitCode).toBe(0); expect(result.passed).toBe(3);
expect(result.failed).toBe(2);
expect(result.exitCode).toBe(1);
expect(output).toContain('Expected: 0');
expect(output).toContain('Received: 1');
}); });
test('should support toHaveJSProperty', async ({ runInlineTest }) => { test('should support toHaveJSProperty', async ({ runInlineTest }) => {

View file

@ -368,6 +368,43 @@ test('should not have internal error when steps are finished after timeout', asy
expect(result.output).not.toContain('Internal error'); expect(result.output).not.toContain('Internal error');
}); });
test('should show nice stacks for locators', async ({ runInlineTest }) => {
const result = await runInlineTest({
'reporter.ts': stepsReporterJS,
'playwright.config.ts': `
module.exports = {
reporter: './reporter',
};
`,
'a.test.ts': `
const { test } = pwt;
test('pass', async ({ page }) => {
await page.setContent('<button></button>');
const locator = page.locator('button');
await locator.evaluate(e => e.innerText);
});
`
}, { reporter: '', workers: 1 });
expect(result.exitCode).toBe(0);
expect(result.passed).toBe(0);
expect(result.output).not.toContain('Internal error');
expect(result.output.split('\n').filter(line => line.startsWith('%%')).map(stripEscapedAscii)).toEqual([
`%% begin {"title":"Before Hooks","category":"hook"}`,
`%% begin {"title":"browserContext.newPage","category":"pw:api"}`,
`%% end {"title":"browserContext.newPage","category":"pw:api"}`,
`%% end {"title":"Before Hooks","category":"hook","steps":[{"title":"browserContext.newPage","category":"pw:api"}]}`,
`%% begin {"title":"page.setContent","category":"pw:api"}`,
`%% end {"title":"page.setContent","category":"pw:api"}`,
`%% begin {"title":"locator.evaluate(button)","category":"pw:api"}`,
`%% end {"title":"locator.evaluate(button)","category":"pw:api"}`,
`%% begin {"title":"After Hooks","category":"hook"}`,
`%% begin {"title":"browserContext.close","category":"pw:api"}`,
`%% end {"title":"browserContext.close","category":"pw:api"}`,
`%% end {"title":"After Hooks","category":"hook","steps":[{"title":"browserContext.close","category":"pw:api"}]}`,
]);
});
function stripEscapedAscii(str: string) { function stripEscapedAscii(str: string) {
return str.replace(/\\u00[a-z0-9][a-z0-9]\[[^m]+m/g, ''); return str.replace(/\\u00[a-z0-9][a-z0-9]\[[^m]+m/g, '');
} }