add right context menu for pinning
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
"name": "Playwriter MCP",
|
"name": "Playwriter MCP",
|
||||||
"version": "0.0.55",
|
"version": "0.0.55",
|
||||||
"description": "Automate your Browser using Cursor, Claude, VS Code. More capable and context efficient than Playwright MCP.",
|
"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>"],
|
"host_permissions": ["<all_urls>"],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "lib/background.mjs",
|
"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, '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(
|
const result = (await chrome.debugger.sendCommand(
|
||||||
debuggee,
|
debuggee,
|
||||||
'Target.getTargetInfo',
|
'Target.getTargetInfo',
|
||||||
@@ -935,6 +946,19 @@ async function onActionClicked(tab: chrome.tabs.Tab): Promise<void> {
|
|||||||
|
|
||||||
resetDebugger()
|
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) => {
|
chrome.runtime.onInstalled.addListener((details) => {
|
||||||
if (import.meta.env.TESTING) return
|
if (import.meta.env.TESTING) return
|
||||||
if (details.reason === 'install') {
|
if (details.reason === 'install') {
|
||||||
@@ -949,6 +973,7 @@ function serializeTabs(tabs: Map<number, TabInfo>): string {
|
|||||||
store.subscribe((state, prevState) => {
|
store.subscribe((state, prevState) => {
|
||||||
logger.log(state)
|
logger.log(state)
|
||||||
void updateIcons()
|
void updateIcons()
|
||||||
|
updateContextMenuVisibility()
|
||||||
const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)
|
const tabsChanged = serializeTabs(state.tabs) !== serializeTabs(prevState.tabs)
|
||||||
if (tabsChanged) {
|
if (tabsChanged) {
|
||||||
syncTabGroupQueue = syncTabGroupQueue.then(syncTabGroup).catch((e) => {
|
syncTabGroupQueue = syncTabGroupQueue.then(syncTabGroup).catch((e) => {
|
||||||
@@ -965,5 +990,66 @@ chrome.tabs.onUpdated.addListener(() => {
|
|||||||
void updateIcons()
|
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
|
// Sync icons on first load
|
||||||
void updateIcons()
|
void updateIcons()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface TabInfo {
|
|||||||
targetId?: string
|
targetId?: string
|
||||||
state: TabState
|
state: TabState
|
||||||
errorText?: string
|
errorText?: string
|
||||||
|
pinnedCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtensionState {
|
export interface ExtensionState {
|
||||||
|
|||||||
@@ -139,6 +139,25 @@ you have access to some functions in addition to playwright methods:
|
|||||||
// search only in CSS files
|
// search only in CSS files
|
||||||
const cssMatches = await editor.grep({ regex: /background-color/, pattern: /\.css/ });
|
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:
|
example:
|
||||||
|
|
||||||
@@ -184,6 +203,16 @@ const stableLocator = page.getByRole('button', { name: 'Save' })
|
|||||||
await stableLocator.click();
|
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
|
## 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.
|
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