Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 18 additions & 27 deletions packages/devtools/src/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { navigate } from './index';
import { Screencast } from './screencast';
import { SettingsButton } from './settingsView';

import type { SessionFile } from '../../playwright-core/src/cli/client/registry';
import type { BrowserDescriptor } from '../../playwright-core/src/serverRegistry';
import type { Tab } from './devtoolsChannel';
import type { SessionModel, SessionStatus } from './sessionModel';

Expand All @@ -44,7 +44,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
const workspaceGroups = React.useMemo(() => {
const groups = new Map<string, SessionStatus[]>();
for (const session of sessions) {
const key = session.file.config.workspaceDir || 'Global';
const key = session.browserDescriptor.workspaceDir || 'Global';
let list = groups.get(key);
if (!list) {
list = [];
Expand All @@ -53,7 +53,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
list.push(session);
}
for (const list of groups.values())
list.sort((a, b) => a.file.config.name.localeCompare(b.file.config.name));
list.sort((a, b) => a.browserDescriptor.title.localeCompare(b.browserDescriptor.title));

// Current workspace first, then alphabetical.
const entries = [...groups.entries()];
Expand Down Expand Up @@ -91,7 +91,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
</div>
{isExpanded && (
<div className='session-chips'>
{entries.map(({ file, canConnect }) => <SessionChip key={file.config.socketPath} sessionFile={file} canConnect={canConnect} visible={isExpanded} model={model} />)}
{entries.map(session => <SessionChip key={session.browserDescriptor.pipeName} descriptor={session.browserDescriptor} wsUrl={session.wsUrl} visible={isExpanded} model={model} />)}
</div>
)}
</div>
Expand All @@ -102,16 +102,14 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
</div>);
};

const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; visible: boolean; model: SessionModel }> = ({ sessionFile, canConnect, visible, model }) => {
const { config } = sessionFile;
const href = '#session=' + encodeURIComponent(config.socketPath);
const wsUrl = model.wsUrls.get(config.socketPath);
const SessionChip: React.FC<{ descriptor: BrowserDescriptor; wsUrl: string | undefined; visible: boolean; model: SessionModel }> = ({ descriptor, wsUrl, visible, model }) => {
const href = '#session=' + encodeURIComponent(descriptor.pipeName!);

const channel = React.useMemo(() => {
if (!canConnect || !visible || !wsUrl)
if (!wsUrl || !visible)
return undefined;
return DevToolsClient.create(wsUrl);
}, [canConnect, visible, wsUrl]);
}, [wsUrl, visible]);

const [selectedTab, setSelectedTab] = React.useState<Tab | undefined>();

Expand All @@ -129,28 +127,27 @@ const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; vis
};
}, [channel]);

const chipTitle = selectedTab ? `[${config.name}] ${selectedTab.url} \u2014 ${selectedTab.title}` : config.name;
const clickable = canConnect && wsUrl !== null;
const chipTitle = selectedTab ? `[${descriptor.title}] ${selectedTab.url} \u2014 ${selectedTab.title}` : descriptor.title;

return (
<a className={'session-chip' + (canConnect ? '' : ' disconnected') + (wsUrl === null ? ' not-supported' : '')} href={clickable ? href : undefined} title={chipTitle} onClick={e => {
<a className={'session-chip' + (wsUrl ? '' : ' disconnected')} href={wsUrl ? href : undefined} title={chipTitle} onClick={e => {
e.preventDefault();
if (clickable)
if (wsUrl)
navigate(href);
}}>
<div className='session-chip-header'>
<div className={'session-status-dot ' + (canConnect ? 'open' : 'closed')} />
<div className={'session-status-dot ' + (wsUrl ? 'open' : 'closed')} />
<span className='session-chip-name'>
{selectedTab ? <>[{config.name}] {selectedTab.url} <span className='session-chip-title'>&mdash; {selectedTab.title}</span></> : config.name}
{selectedTab ? <>[{descriptor.title}] {selectedTab.url} <span className='session-chip-title'>&mdash; {selectedTab.title}</span></> : descriptor.title}
</span>
{canConnect && (
{wsUrl && (
<button
className='session-chip-action'
title='Close session'
onClick={e => {
e.preventDefault();
e.stopPropagation();
void model.closeSession(sessionFile);
void model.closeSession(descriptor);
}}
>
<svg viewBox='0 0 12 12' fill='none' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round'>
Expand All @@ -159,14 +156,14 @@ const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; vis
</svg>
</button>
)}
{!canConnect && (
{!wsUrl && (
<button
className='session-chip-action'
title='Delete session data'
onClick={e => {
e.preventDefault();
e.stopPropagation();
void model.deleteSessionData(sessionFile);
void model.deleteSessionData(descriptor);
}}
>
<svg viewBox='0 0 16 16' fill='none' stroke='currentColor' strokeWidth='1.2' strokeLinecap='round' strokeLinejoin='round'>
Expand All @@ -179,13 +176,7 @@ const SessionChip: React.FC<{ sessionFile: SessionFile; canConnect: boolean; vis
</div>
<div className='screencast-container'>
{channel && <Screencast channel={channel} />}
{!canConnect && <div className='screencast-placeholder'>Session closed</div>}
{canConnect && !channel && wsUrl === null && <div className='screencast-placeholder'>
Session v{sessionFile.config.version} is not compatible with this viewer{model.clientInfo ? ` v${model.clientInfo.version}` : ''}.
<br />
Please update playwright-cli and restart this with "playwright-cli show".
</div>}
{canConnect && !channel && wsUrl === undefined && <div className='screencast-placeholder'>Connecting</div>}
{!wsUrl && <div className='screencast-placeholder'>Session closed</div>}
</div>
</a>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ const App: React.FC = () => {
}, []);

if (socketPath) {
const wsUrl = model.wsUrls.get(socketPath);
const wsUrl = model.sessionBySocketPath(socketPath)?.wsUrl;
return <DevTools wsUrl={wsUrl || undefined} />;
}
return <Grid model={model} />;
Expand Down
47 changes: 11 additions & 36 deletions packages/devtools/src/sessionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,23 @@
* limitations under the License.
*/

import type { ClientInfo, SessionFile } from '../../playwright-core/src/cli/client/registry';
import type { ClientInfo } from '../../playwright-core/src/cli/client/registry';
import type { BrowserDescriptor } from '../../playwright-core/src/serverRegistry';

export type SessionStatus = {
file: SessionFile;
canConnect: boolean;
browserDescriptor: BrowserDescriptor;
wsUrl?: string;
};


type Listener = () => void;

export class SessionModel {
sessions: SessionStatus[] = [];
readonly wsUrls: Map<string, string | null> = new Map();
clientInfo: ClientInfo | undefined;
error: string | undefined;
loading = true;

private _knownTimestamps = new Map<string, number>();
private _pollActive = false;
private _pollTimeout: ReturnType<typeof setTimeout> | undefined;
private _lastJson = '';
Expand Down Expand Up @@ -67,7 +67,7 @@ export class SessionModel {
}

sessionBySocketPath(socketPath: string): SessionStatus | undefined {
return this.sessions.find(s => s.file.config.socketPath === socketPath);
return this.sessions.find(s => s.browserDescriptor.pipeName === socketPath);
}

private async _fetchSessions() {
Expand All @@ -84,10 +84,7 @@ export class SessionModel {
this.clientInfo = data.clientInfo;
this._notify();

for (const session of this.sessions) {
if (session.canConnect)
this._obtainDevtoolsUrl(session.file);
}

}
this.error = undefined;
} catch (e: any) {
Expand All @@ -102,46 +99,24 @@ export class SessionModel {
await this._fetchSessions();
}

async closeSession(sessionFile: SessionFile) {
async closeSession(descriptor: BrowserDescriptor) {
await fetch('/api/sessions/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionFile }),
body: JSON.stringify({ browserDescriptor: descriptor }),
});
await this._fetchSessions();
}

async deleteSessionData(sessionFile: SessionFile) {
async deleteSessionData(descriptor: BrowserDescriptor) {
await fetch('/api/sessions/delete-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionFile }),
body: JSON.stringify({ browserDescriptor: descriptor }),
});
await this._fetchSessions();
}

private _obtainDevtoolsUrl(sessionFile: SessionFile) {
const { config } = sessionFile;
if (this._knownTimestamps.get(config.socketPath) === config.timestamp)
return;
this._knownTimestamps.set(config.socketPath, config.timestamp);
fetch('/api/sessions/devtools-start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionFile }),
}).then(async resp => {
if (resp.ok) {
const { url } = await resp.json();
this.wsUrls.set(config.socketPath, url);
} else {
this.wsUrls.set(config.socketPath, null);
}
this._notify();
}).catch(() => {
this._knownTimestamps.delete(config.socketPath);
});
}

dispose() {
this.stopPolling();
this._listeners.clear();
Expand Down
7 changes: 7 additions & 0 deletions packages/playwright-core/src/cli/daemon/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ export function decorateCLICommand(command: Command, version: string) {
const socketPath = await startCliDaemonServer(sessionName, browserContext, mcpConfig, clientInfo, { ...options, exitOnClose: true });
console.log(`### Success\nDaemon listening on ${socketPath}`);
console.log('<EOF>');
try {
await (browser as any)._startServer(sessionName, { workspaceDir: clientInfo.workspaceDir });
browserContext.on('close', () => (browser as any)._stopServer().catch(() => {}));
} catch (error) {
if (!error.message.includes('Server is already running'))
throw error;
}
} catch (error) {
const message = process.env.PWDEBUGIMPL ? (error as Error).stack || (error as Error).message : (error as Error).message;
console.log(`### Error\n${message}`);
Expand Down
4 changes: 0 additions & 4 deletions packages/playwright-core/src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,10 +589,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
return;
throw new Error(`File access denied: ${filePath} is outside allowed roots. Allowed roots: ${this._allowedDirectories.length ? this._allowedDirectories.join(', ') : 'none'}`);
}

async _devtoolsStart(): Promise<{ url: string }> {
return await this._channel.devtoolsStart();
}
}

async function prepareStorageState(platform: Platform, storageState: string | SetStorageState): Promise<NonNullable<channels.BrowserNewContextParams['storageState']>> {
Expand Down
9 changes: 7 additions & 2 deletions packages/playwright-core/src/client/eventEmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import type { EventEmitter as EventEmitterType } from 'events';
import type { Platform } from './platform';
import type { Disposable } from './disposable';

type EventEmitterLike = {
on(eventName: string | symbol, handler: (...args: any[]) => unknown): unknown;
removeListener(eventName: string | symbol, handler: (...args: any[]) => unknown): unknown;
};

type EventType = string | symbol;
type Listener = (...args: any[]) => any;
type EventMap = Record<EventType, Listener | Listener[]>;
Expand Down Expand Up @@ -400,9 +405,9 @@ function wrappedListener(l: Listener): Listener {

class EventsHelper {
static addEventListener(
emitter: EventEmitterType,
emitter: EventEmitterLike,
eventName: (string | symbol),
handler: (...args: any[]) => void): Disposable {
handler: (...args: any[]) => any): Disposable {
emitter.on(eventName, handler);
return {
dispose: async () => { emitter.removeListener(eventName, handler); }
Expand Down
5 changes: 3 additions & 2 deletions packages/playwright-core/src/devtools/DEPS.list
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
../../
../server/registry/index.ts
../server/utils/
../serverRegistry.ts
../utils/
../cli/client/registry.ts
../cli/client/session.ts
../client/connect.ts
../client/eventEmitter.ts
Loading
Loading