Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e12a4a52e | ||
|
|
9edf0ed711 | ||
|
|
627bed416d | ||
|
|
d9d9417cfb | ||
|
|
aa7b216383 | ||
|
|
9c1f3bc46a | ||
|
|
455683b29d | ||
|
|
a0321f44ec | ||
|
|
945bbf2828 | ||
|
|
cafe5f3fd1 | ||
|
|
895585ddce | ||
|
|
dc9d4ceffe | ||
|
|
99c5df9c61 | ||
|
|
2b62811fd6 |
1661
package-lock.json
generated
1661
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "playwright-internal",
|
||||
"private": true,
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
6
packages/create-playwright/package-lock.json
generated
6
packages/create-playwright/package-lock.json
generated
|
|
@ -23,7 +23,7 @@
|
|||
},
|
||||
"../playwright-test": {
|
||||
"name": "@playwright/test",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
"open": "^8.3.0",
|
||||
"pirates": "^4.0.1",
|
||||
"pixelmatch": "^5.2.1",
|
||||
"playwright-core": "=1.16.0-next",
|
||||
"playwright-core": "=1.16.1",
|
||||
"source-map-support": "^0.4.18"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
"open": "^8.3.0",
|
||||
"pirates": "^4.0.1",
|
||||
"pixelmatch": "^5.2.1",
|
||||
"playwright-core": "=1.16.0-next",
|
||||
"playwright-core": "=1.16.1",
|
||||
"source-map-support": "^0.4.18"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-chromium",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate Chromium",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -25,6 +25,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "=1.16.0-next"
|
||||
"playwright-core": "=1.16.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-core",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
|
|||
|
|
@ -36,7 +36,10 @@ export class Locator implements api.Locator {
|
|||
private async _withElement<R>(task: (handle: ElementHandle<SVGElement | HTMLElement>, timeout?: number) => Promise<R>, timeout?: number): Promise<R> {
|
||||
timeout = this._frame.page()._timeoutSettings.timeout({ timeout });
|
||||
const deadline = timeout ? monotonicTime() + timeout : 0;
|
||||
const handle = await this.elementHandle({ timeout });
|
||||
|
||||
return this._frame._wrapApiCall<R>(async (channel: channels.FrameChannel) => {
|
||||
const result = await channel.waitForSelector({ selector: this._selector, strict: true, state: 'attached', timeout });
|
||||
const handle = ElementHandle.fromNullable(result.element) as ElementHandle<SVGElement | HTMLElement> | null;
|
||||
if (!handle)
|
||||
throw new Error(`Could not resolve ${this._selector} to DOM Element`);
|
||||
try {
|
||||
|
|
@ -44,6 +47,7 @@ export class Locator implements api.Locator {
|
|||
} finally {
|
||||
await handle.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async boundingBox(options?: TimeoutOptions): Promise<Rect | null> {
|
||||
|
|
|
|||
|
|
@ -505,7 +505,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (poll === 'error:notconnected')
|
||||
return poll;
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
const result = await pollHandler.finish();
|
||||
const result = await pollHandler.finishMaybeNotConnected();
|
||||
await this._page._doSlowMo();
|
||||
return result;
|
||||
});
|
||||
|
|
@ -530,7 +530,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (poll === 'error:notconnected')
|
||||
return 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.
|
||||
if (filled === 'error:notconnected')
|
||||
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));
|
||||
}, options.force);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll));
|
||||
const result = await pollHandler.finish();
|
||||
const result = await pollHandler.finishMaybeNotConnected();
|
||||
assertDone(throwRetargetableDOMError(result));
|
||||
}, 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);
|
||||
}, state);
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, throwRetargetableDOMError(poll));
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finish()));
|
||||
assertDone(throwRetargetableDOMError(await pollHandler.finishMaybeNotConnected()));
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
|
|
@ -808,7 +808,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
|||
if (poll === 'error:notconnected')
|
||||
return poll;
|
||||
const pollHandler = new InjectedScriptPollHandler(progress, poll);
|
||||
const result = await pollHandler.finish();
|
||||
const result = await pollHandler.finishMaybeNotConnected();
|
||||
if (waitForEnabled)
|
||||
progress.log(' element is visible, enabled and stable');
|
||||
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 {
|
||||
const result = await this._poll!.evaluate(poll => poll.run());
|
||||
await this._finishInternal();
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type ContextData = {
|
|||
contextPromise: Promise<dom.FrameExecutionContext>;
|
||||
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
|
||||
context: dom.FrameExecutionContext | null;
|
||||
rerunnableTasks: Set<RerunnableTask>;
|
||||
rerunnableTasks: Set<RerunnableTask<any>>;
|
||||
};
|
||||
|
||||
type DocumentInfo = {
|
||||
|
|
@ -1181,6 +1181,11 @@ export class Frame extends SdkObject {
|
|||
if (options.expression.endsWith('.array') && expectsEmptyList !== options.isNot)
|
||||
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.
|
||||
return continuePolling;
|
||||
}
|
||||
|
|
@ -1201,6 +1206,8 @@ export class Frame extends SdkObject {
|
|||
}, options, { strict: true, querySelectorAll, mainWorld, omitAttached: true, logScale: true, ...options }).catch(e => {
|
||||
if (js.isJavaScriptErrorInEvaluate(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 };
|
||||
});
|
||||
}
|
||||
|
|
@ -1280,7 +1287,7 @@ export class Frame extends SdkObject {
|
|||
|
||||
return controller.run(async progress => {
|
||||
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 }) => {
|
||||
const callback = injected.eval(callbackText) as DomTaskBody<T, R, Element | undefined>;
|
||||
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.'));
|
||||
if (data.context)
|
||||
rerunnableTask.rerun(data.context);
|
||||
return await rerunnableTask.promise;
|
||||
return await rerunnableTask.promise!;
|
||||
}, this._page._timeoutSettings.timeout(options));
|
||||
}
|
||||
|
||||
private _scheduleRerunnableHandleTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<js.SmartHandle<T>> {
|
||||
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)
|
||||
rerunnableTask.terminate(new Error('waitForFunction failed: frame got detached.'));
|
||||
if (data.context)
|
||||
rerunnableTask.rerun(data.context);
|
||||
return rerunnableTask.promise;
|
||||
return rerunnableTask.handlePromise!;
|
||||
}
|
||||
|
||||
private _setContext(world: types.World, context: dom.FrameExecutionContext | null) {
|
||||
|
|
@ -1394,31 +1401,42 @@ export class Frame extends SdkObject {
|
|||
}
|
||||
}
|
||||
|
||||
class RerunnableTask {
|
||||
readonly promise: Promise<any>;
|
||||
private _task: dom.SchedulableTask<any>;
|
||||
private _resolve: (result: any) => void = () => {};
|
||||
private _reject: (reason: Error) => void = () => {};
|
||||
class RerunnableTask<T> {
|
||||
readonly promise: ManualPromise<T> | undefined;
|
||||
readonly handlePromise: ManualPromise<js.SmartHandle<T>> | undefined;
|
||||
private _task: dom.SchedulableTask<T>;
|
||||
private _progress: Progress;
|
||||
private _returnByValue: boolean;
|
||||
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._progress = progress;
|
||||
this._returnByValue = returnByValue;
|
||||
if (returnByValue)
|
||||
this.promise = new ManualPromise<T>();
|
||||
else
|
||||
this.handlePromise = new ManualPromise<js.SmartHandle<T>>();
|
||||
this._contextData = data;
|
||||
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) {
|
||||
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) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,6 @@ export async function showTraceViewer(traceUrl: string, browserName: string, hea
|
|||
else
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ const TestResultView: React.FC<{
|
|||
|
||||
{!!traces.length && <Chip header='Traces'>
|
||||
{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 }} />
|
||||
</a>
|
||||
</div>)}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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="16x16" href="/trace/icon-16x16.png">
|
||||
<link rel="manifest" href="/trace/manifest.webmanifest">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="icon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="icon-16x16.png">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<title>Playwright Trace Viewer</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ import '../common.css';
|
|||
|
||||
(async () => {
|
||||
applyTheme();
|
||||
navigator.serviceWorker.register('/trace/sw.bundle.js', {
|
||||
scope: '/trace/'
|
||||
});
|
||||
navigator.serviceWorker.register('sw.bundle.js');
|
||||
if (!navigator.serviceWorker.controller) {
|
||||
await new Promise<void>(f => {
|
||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||
});
|
||||
}
|
||||
// Keep SW running.
|
||||
setInterval(function() { fetch('ping'); }, 10000);
|
||||
ReactDOM.render(<Workbench/>, document.querySelector('#root'));
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -2,28 +2,27 @@
|
|||
"theme_color": "#000",
|
||||
"background_color": "#fff",
|
||||
"display": "browser",
|
||||
"scope": "/trace",
|
||||
"start_url": "/trace/index.html",
|
||||
"start_url": "index.html",
|
||||
"name": "Playwright Trace Viewer",
|
||||
"short_name": "Trace Viewer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/trace/icon-192x192.png",
|
||||
"src": "icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/trace/icon-256x256.png",
|
||||
"src": "icon-256x256.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/trace/icon-384x384.png",
|
||||
"src": "icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/trace/icon-512x512.png",
|
||||
"src": "icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,8 +56,12 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
const url = new URL(request.url);
|
||||
|
||||
const relativePath = url.pathname.substring(scopePath.length - 1);
|
||||
if (relativePath === '/context') {
|
||||
if (relativePath === '/ping') {
|
||||
await gc();
|
||||
return new Response(null, { status: 200 });
|
||||
}
|
||||
|
||||
if (relativePath === '/context') {
|
||||
const traceModel = await loadTrace(traceUrl, event.clientId);
|
||||
return new Response(JSON.stringify(traceModel!.contextEntry), {
|
||||
status: 200,
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@
|
|||
}
|
||||
|
||||
.action-icons {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ export const ActionList: React.FC<ActionListProps> = ({
|
|||
<span>{metadata.apiName}</span>
|
||||
{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>}
|
||||
<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 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>}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@
|
|||
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 {
|
||||
contain: size;
|
||||
user-select: none;
|
||||
|
|
|
|||
|
|
@ -37,10 +37,22 @@ export const Workbench: React.FunctionComponent<{
|
|||
const [highlightedAction, setHighlightedAction] = React.useState<ActionTraceEvent | undefined>();
|
||||
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(() => {
|
||||
(async () => {
|
||||
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);
|
||||
setContextEntry(contextEntry);
|
||||
} else {
|
||||
|
|
@ -52,6 +64,21 @@ export const Workbench: React.FunctionComponent<{
|
|||
const defaultSnapshotSize = contextEntry.options.viewport || { width: 1280, height: 720 };
|
||||
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.
|
||||
boundaries.maximum += (boundaries.maximum - boundaries.minimum) / 20;
|
||||
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'
|
||||
onDragOver={event => { event.preventDefault(); }}
|
||||
onDrop={event => {
|
||||
event.preventDefault();
|
||||
const url = URL.createObjectURL(event.dataTransfer.files[0]);
|
||||
setTraceURL(url.toString());
|
||||
}}>
|
||||
onDrop={event => handleDropEvent(event)}>
|
||||
<div className='hbox header'>
|
||||
<div className='logo'>🎭</div>
|
||||
<div className='product'>Playwright</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-firefox",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate Firefox",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -25,6 +25,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "=1.16.0-next"
|
||||
"playwright-core": "=1.16.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
packages/playwright-test/.npmignore
Normal file
1
packages/playwright-test/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
src
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@playwright/test",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -43,15 +43,15 @@
|
|||
"@babel/preset-typescript": "^7.14.5",
|
||||
"colors": "^1.4.0",
|
||||
"commander": "^8.2.0",
|
||||
"expect": "^27.2.5",
|
||||
"jest-matcher-utils": "^27.2.5",
|
||||
"expect": "=27.2.5",
|
||||
"jest-matcher-utils": "=27.2.5",
|
||||
"jpeg-js": "^0.4.2",
|
||||
"minimatch": "^3.0.3",
|
||||
"ms": "^2.1.2",
|
||||
"open": "^8.3.0",
|
||||
"pirates": "^4.0.1",
|
||||
"pixelmatch": "^5.2.1",
|
||||
"playwright-core": "=1.16.0-next",
|
||||
"playwright-core": "=1.16.1",
|
||||
"pngjs": "^5.0.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"source-map-support": "^0.4.18",
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export class BaseReporter implements Reporter {
|
|||
}
|
||||
|
||||
onError(error: TestError) {
|
||||
console.log(formatError(error, colors.enabled));
|
||||
console.log(formatError(error, colors.enabled).message);
|
||||
}
|
||||
|
||||
async onEnd(result: FullResult) {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,11 @@ import colors from 'colors/safe';
|
|||
import fs from 'fs';
|
||||
import open from 'open';
|
||||
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 { calculateSha1, removeFolders } from 'playwright-core/src/utils/utils';
|
||||
import RawReporter, { JsonReport, JsonSuite, JsonTestCase, JsonTestResult, JsonTestStep, JsonAttachment } from './raw';
|
||||
import assert from 'assert';
|
||||
import { formatError } from './base';
|
||||
|
||||
export type Stats = {
|
||||
total: number;
|
||||
|
|
@ -141,10 +140,6 @@ class HtmlReporter {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
onError(error: TestError) {
|
||||
console.log(formatError(error, colors.enabled));
|
||||
}
|
||||
}
|
||||
|
||||
export function htmlReportFolder(outputFolder?: string): string {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright-webkit",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate WebKit",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -25,6 +25,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "=1.16.0-next"
|
||||
"playwright-core": "=1.16.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "playwright",
|
||||
"version": "1.16.0-next",
|
||||
"version": "1.16.1",
|
||||
"description": "A high-level API to automate web browsers",
|
||||
"repository": "github:Microsoft/playwright",
|
||||
"homepage": "https://playwright.dev",
|
||||
|
|
@ -24,6 +24,6 @@
|
|||
"install": "node install.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"playwright-core": "=1.16.0-next"
|
||||
"playwright-core": "=1.16.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
tests/page/matchers.misc.spec.ts
Normal file
25
tests/page/matchers.misc.spec.ts
Normal 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();
|
||||
});
|
||||
|
|
@ -33,10 +33,41 @@ test('should support toHaveCount', async ({ runInlineTest }) => {
|
|||
await promise;
|
||||
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 });
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
const output = stripAscii(result.output);
|
||||
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 }) => {
|
||||
|
|
|
|||
|
|
@ -368,6 +368,43 @@ test('should not have internal error when steps are finished after timeout', asy
|
|||
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) {
|
||||
return str.replace(/\\u00[a-z0-9][a-z0-9]\[[^m]+m/g, '');
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue