add right context menu for pinning
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"name": "Playwriter MCP",
|
||||
"version": "0.0.55",
|
||||
"description": "Automate your Browser using Cursor, Claude, VS Code. More capable and context efficient than Playwright MCP.",
|
||||
"permissions": ["debugger", "tabGroups"],
|
||||
"permissions": ["debugger", "tabGroups", "contextMenus"],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
"service_worker": "lib/background.mjs",
|
||||
|
||||
@@ -368,6 +368,17 @@ async function attachTab(tabId: number): Promise<Protocol.Target.TargetInfo> {
|
||||
|
||||
await chrome.debugger.sendCommand(debuggee, 'Page.enable')
|
||||
|
||||
await chrome.debugger.sendCommand(debuggee, 'Runtime.evaluate', {
|
||||
expression: `
|
||||
if (!window.__playwriter_contextmenu_listener) {
|
||||
window.__playwriter_contextmenu_listener = true;
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
window.__playwriter_lastRightClicked = e.target;
|
||||
}, true);
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
const result = (await chrome.debugger.sendCommand(
|
||||
debuggee,
|
||||
'Target.getTargetInfo',
|
||||
@@ -935,6 +946,19 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
|
||||
|
||||
resetDebugger()
|
||||
|
||||
chrome.contextMenus.create({
|
||||
id: 'playwriter-pin-element',
|
||||
title: 'Pin to Playwriter',
|
||||
contexts: ['all'],
|
||||
visible: false,
|
||||
})
|
||||
|
||||
function updateContextMenuVisibility(): void {
|
||||
const { currentTabId, tabs } = store.getState()
|
||||
const isConnected = currentTabId !== undefined && tabs.get(currentTabId)?.state === 'connected'
|
||||
chrome.contextMenus.update('playwriter-pin-element', { visible: isConnected })
|
||||
}
|
||||
|
||||
chrome.runtime.onInstalled.addListener((details) => {
|
||||
if (import.meta.env.TESTING) return
|
||||
if (details.reason === 'install') {
|
||||
@@ -949,6 +973,7 @@ function serializeTabs(tabs: Map<number, TabInfo>): string {
|
||||
store.subscribe((state, prevState) => {
|
||||
logger.log(state)
|
||||
void updateIcons()
|
||||
updateContextMenuVisibility()
|
||||
const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)
|
||||
if (tabsChanged) {
|
||||
syncTabGroupQueue = syncTabGroupQueue.then(syncTabGroup).catch((e) => {
|
||||
@@ -965,5 +990,66 @@ chrome.tabs.onUpdated.addListener(() => {
|
||||
void updateIcons()
|
||||
})
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.menuItemId !== 'playwriter-pin-element' || !tab?.id) return
|
||||
|
||||
const tabInfo = store.getState().tabs.get(tab.id)
|
||||
if (!tabInfo || tabInfo.state !== 'connected') {
|
||||
logger.debug('Tab not connected, ignoring')
|
||||
return
|
||||
}
|
||||
|
||||
const debuggee = { tabId: tab.id }
|
||||
const count = (tabInfo.pinnedCount || 0) + 1
|
||||
|
||||
store.setState((state) => {
|
||||
const newTabs = new Map(state.tabs)
|
||||
const existing = newTabs.get(tab.id!)
|
||||
if (existing) {
|
||||
newTabs.set(tab.id!, { ...existing, pinnedCount: count })
|
||||
}
|
||||
return { tabs: newTabs }
|
||||
})
|
||||
|
||||
const name = `playwriterPinnedElem${count}`
|
||||
|
||||
try {
|
||||
const result = (await chrome.debugger.sendCommand(debuggee, 'Runtime.evaluate', {
|
||||
expression: `
|
||||
if (window.__playwriter_lastRightClicked) {
|
||||
window.${name} = window.__playwriter_lastRightClicked;
|
||||
'${name}';
|
||||
} else {
|
||||
throw new Error('No element was right-clicked');
|
||||
}
|
||||
`,
|
||||
returnByValue: true,
|
||||
})) as { result?: { value?: string }; exceptionDetails?: { text: string } }
|
||||
|
||||
if (result.exceptionDetails) {
|
||||
logger.error('Failed to pin element:', result.exceptionDetails.text)
|
||||
return
|
||||
}
|
||||
|
||||
await chrome.debugger.sendCommand(debuggee, 'Runtime.evaluate', {
|
||||
expression: `
|
||||
(() => {
|
||||
const el = window.${name};
|
||||
if (!el) return;
|
||||
const orig = el.getAttribute('style') || '';
|
||||
el.setAttribute('style', orig + '; outline: 3px solid #22c55e !important; outline-offset: 2px !important; box-shadow: 0 0 0 3px #22c55e !important;');
|
||||
setTimeout(() => el.setAttribute('style', orig), 300);
|
||||
return navigator.clipboard.writeText('globalThis.${name}');
|
||||
})()
|
||||
`,
|
||||
awaitPromise: true,
|
||||
})
|
||||
|
||||
logger.debug('Pinned element as:', name)
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to pin element:', error.message)
|
||||
}
|
||||
})
|
||||
|
||||
// Sync icons on first load
|
||||
void updateIcons()
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface TabInfo {
|
||||
targetId?: string
|
||||
state: TabState
|
||||
errorText?: string
|
||||
pinnedCount?: number
|
||||
}
|
||||
|
||||
export interface ExtensionState {
|
||||
|
||||
@@ -139,6 +139,25 @@ you have access to some functions in addition to playwright methods:
|
||||
// search only in CSS files
|
||||
const cssMatches = await editor.grep({ regex: /background-color/, pattern: /\.css/ });
|
||||
```
|
||||
- `getStylesForLocator({ locator, includeUserAgentStyles? })`: gets the CSS styles applied to an element, similar to browser DevTools "Styles" panel.
|
||||
- `locator`: a Playwright Locator for the element to inspect
|
||||
- `includeUserAgentStyles`: (optional, default: false) include browser default styles
|
||||
- Returns: `StylesResult` object with:
|
||||
- `element`: string description of the element (e.g. `div#main.container`)
|
||||
- `inlineStyle`: object of `{ property: value }` inline styles, or null
|
||||
- `rules`: array of CSS rules that apply to this element, each with:
|
||||
- `selector`: the CSS selector that matched
|
||||
- `source`: `{ url, line, column }` location in the stylesheet, or null
|
||||
- `origin`: `"regular"` | `"user-agent"` | `"injected"` | `"inspector"`
|
||||
- `declarations`: object of `{ property: value }` (values include `!important` if applicable)
|
||||
- `inheritedFrom`: element description if inherited (e.g. `body`), or null for direct matches
|
||||
- Example:
|
||||
```js
|
||||
const loc = page.locator('.my-button');
|
||||
const styles = await getStylesForLocator({ locator: loc });
|
||||
console.log(formatStylesAsText(styles));
|
||||
```
|
||||
- `formatStylesAsText(styles)`: formats a `StylesResult` object as human-readable text. Use this to display styles in a readable format.
|
||||
|
||||
example:
|
||||
|
||||
@@ -184,6 +203,16 @@ const stableLocator = page.getByRole('button', { name: 'Save' })
|
||||
await stableLocator.click();
|
||||
```
|
||||
|
||||
## pinned elements (user right-click to pin)
|
||||
|
||||
Users can right-click an element and select "Pin to Playwriter" to store it in `globalThis.playwriterPinnedElem1` (increments for each pin). The variable name is copied to clipboard.
|
||||
|
||||
```js
|
||||
const el = await page.evaluateHandle(() => globalThis.playwriterPinnedElem1);
|
||||
await el.click();
|
||||
const selector = await getLocatorStringForElement(el);
|
||||
```
|
||||
|
||||
## finding specific elements with snapshot
|
||||
|
||||
You can use `search` to find specific elements in the snapshot without reading the whole page structure. This is useful for finding forms, textareas, or specific text.
|
||||
|
||||
Reference in New Issue
Block a user