本文へ移動
cccskills
無料GitHub で公開

code-server-playwright-automation

Fix Playwright automation failures against code-server (VS Code in browser). Use when: (1) trust dialog blocks all clicks — "monaco-dialog-modal-block intercepts pointer events", (2) button:has-text() finds wrong buttons behind modal dialog, (3) keyboard shortcuts don't work — Meta vs Control inconsistency, (4) VS Code extension activity bar icon not found by aria-label, (5) panels show placeholder text despite extension being "Connected". Covers trust dialog dismissal, keyboard shortcut hybrid mode, extension panel selectors, and database connection verification for Grafema extension.

インストール方法を見る

含まれるファイル(1)

  • SKILL.md8.8 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

Code-Server Playwright Automation

Problem

Automating code-server (VS Code Web) with Playwright fails in non-obvious ways:

  • Trust dialog uses custom Monaco dialog component that blocks ALL mouse events
  • Keyboard shortcuts are a hybrid of Mac and Linux depending on context
  • Extension activity bar icons have dynamic class names, not stable aria-labels
  • Standard Playwright selectors find elements behind modal overlays

Context / Trigger Conditions

  • Automating code-server at http://localhost:8080 via Playwright
  • Error: <div class="monaco-dialog-modal-block dimmed"> intercepts pointer events
  • button:has-text("trust") finds 0 or wrong elements
  • page.keyboard.press('Control+p') does nothing (Quick Open doesn't open)
  • page.$('[aria-label="ExtensionName"]') returns null for activity bar icons
  • Extension panels show placeholder text after clicking entities

Solution

1. Trust Dialog: Use page.evaluate(), NOT standard selectors

The trust dialog is a custom monaco-dialog-box with a monaco-dialog-modal-block backdrop. The backdrop intercepts ALL pointer events, so page.click() and button:has-text() fail. Standard page.$$('button') finds Getting Started page buttons behind the dialog, not dialog buttons.

Only reliable method — with retry loop and explicit "Yes" check:

// Trust dialog can appear 6-12s after page load — MUST retry
for (let attempt = 0; attempt < 15; attempt++) {
  await page.waitForTimeout(2000);
  const result = await page.evaluate(() => {
    const allButtons = document.querySelectorAll('button, a.monaco-button');
    for (const btn of allButtons) {
      const text = btn.textContent || '';
      // CRITICAL: Must match "Yes" — the "No" button also contains "trust"!
      if (text.includes('Yes') && text.includes('trust')) {
        btn.click();
        return 'YES-clicked';
      }
    }
    const modal = document.querySelector('.monaco-dialog-modal-block');
    if (modal) return 'modal-waiting';
    const sb = document.querySelector('.statusbar');
    if (sb && sb.textContent.includes('Restricted')) return 'restricted';
    return 'no-dialog';
  });
  if (result === 'YES-clicked') { await page.waitForTimeout(3000); break; }
  if (result === 'no-dialog' && attempt >= 5) break;
}

CRITICAL BUG: Searching for text.includes('trust') without text.includes('Yes') will match "No, I don't trust the authors" FIRST (it appears earlier in DOM). This puts code-server in Restricted Mode where extensions are disabled.

Why standard selectors fail:

  • Dialog buttons are <a> tags with class="monaco-button", role="button" — not <button>
  • page.waitForSelector('button:has-text("trust")') finds Getting Started buttons
  • page.click() with force:true still fails because backdrop intercepts events
  • Only element.click() via evaluate bypasses the backdrop

2. Keyboard Shortcuts: Hybrid Mac/Linux Mode

Code-server detects the CLIENT browser's OS and adapts shortcuts. When accessed from macOS browser:

ShortcutKeyNotes
Quick OpenMeta+pWorks (Mac)
Command PaletteMeta+Shift+pWorks (Mac)
Close TabMeta+wWorks (Mac)
Go to LineControl+gException — NOT Meta+g
ExplorerMeta+Shift+eWorks (Mac)

Key insight: Most shortcuts use Meta (Mac), but Go to Line uses Control+g. Always try both if one doesn't work.

3. Extension Activity Bar: Use Class Selector

Extensions register activity bar icons with dynamically generated classes:

action-label activity-workbench-view-extension-{extensionId}-{hash}

The aria-label="ExtensionName" exists but on a parent <div>, not the clickable <a>.

Reliable selector:

const icon = await page.$('a[class*="view-extension-grafema"]');
if (icon) await icon.click();

Unreliable: page.$('[aria-label="Grafema"]') — matches wrong elements (tree headers, etc.)

4. Panel Selectors: aria-label on .pane-header

Extension panels use .pane-header with aria-label="{Name} Section":

const header = await page.$('[aria-label="Value Trace Section"]');
const expanded = await header.getAttribute('aria-expanded');
if (expanded === 'false') await header.click(); // Expand

// Read panel content
const content = await header.evaluate(h => {
  const body = h.closest('.pane')?.querySelector('.pane-body');
  return body ? body.textContent.substring(0, 400).trim() : null;
});

5. File Extension: Use .cjs in ESM Projects

If the project has "type": "module" in package.json, Playwright scripts with require() must use .cjs extension. Otherwise: ReferenceError: require is not defined in ES module scope.

6. Verify File Actually Opened

Quick Open (Meta+p) can silently fail (e.g., trust dialog still present, Restricted Mode). Always verify:

const fileInfo = await page.evaluate(() => {
  const items = document.querySelectorAll('.statusbar-item');
  const statusText = Array.from(items).map(el => el.textContent.trim()).filter(Boolean).join(' | ');
  const tabs = Array.from(document.querySelectorAll('.tab')).map(t => t.textContent.trim()).filter(Boolean);
  return { hasLn: statusText.includes('Ln'), tabs };
});
if (!fileInfo.hasLn) {
  console.log('ERROR: File not opened. Tabs:', fileInfo.tabs);
  // Abort or retry
}

7. Docker Extension Hot-Reload (Without Image Rebuild)

To test extension changes without rebuilding the Docker image:

# 1. Build locally
pnpm build

# 2. Copy bundle to container
docker cp packages/vscode/dist/extension.js \
  grafema-demo:/home/coder/.local/share/code-server/extensions/grafema.grafema-explore-0.2.0/dist/extension.js

# 3. Restart code-server (extension host reloads from disk)
docker exec grafema-demo pkill -HUP -f code-server

CRITICAL: docker restart reverts to the original image files! You must re-copy extension.js after every container restart.

8. URI Scheme: vscode-remote Not file

Code-server uses vscode-remote URI scheme, not file. Extensions that check document.uri.scheme !== 'file' will silently skip all code files in code-server.

Fix pattern:

function isCodeDocument(uri: vscode.Uri): boolean {
  return uri.scheme === 'file' || uri.scheme === 'vscode-remote';
}

Replace all scheme === 'file' / scheme !== 'file' checks with isCodeDocument().

9. Cursor Tracking: Verify via Debug Log, Not Panels

Playwright mouse clicks and arrow keys may NOT trigger onDidChangeTextEditorSelection in code-server's extension host. To verify cursor tracking works:

  • Check the Debug Log panel for findNodeAtCursor entries
  • "No queries yet" = cursor tracking not firing
  • "found: FUNCTION ..." = working correctly

If cursor tracking doesn't fire, focus the editor with Meta+1 before clicking.

10. Hover Tooltip: Unreliable in Headless

page.mouse.move() does NOT reliably trigger Monaco hover providers in headless Playwright. Hover tooltip testing requires non-headless mode or real browser. All other extension features (panels, CodeLens, cursor tracking) work fine in headless.

Verification

After applying these patterns:

  1. Trust dialog dismissed → no "Restricted Mode" in status bar
  2. Quick Open works → file tab appears (verify with status bar check!)
  3. Activity bar icon clicks → panels change in sidebar
  4. Panels expand → content visible (may show placeholder if no DB connected)
  5. Debug Log shows findNodeAtCursor found: entries after cursor movement

Notes

  • The trust dialog appears on EVERY new browser launch (headless: true = new profile)
  • Trust dialog can appear 6-12s after page load — MUST retry, not just check once
  • Clicking wrong trust button ("No") puts code-server in Restricted Mode = extensions disabled
  • After dismissing trust, close the Welcome tab with Meta+w before opening files
  • Panel placeholder text means the feature works but has no data — check Debug Log for errors
  • Screenshots taken with page.screenshot() can be read by Claude (multimodal)
  • Wait at least 8000ms after page.goto() for full code-server initialization
  • docker restart reverts file changes — always re-copy extension.js after restart

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Fix Elixir/Erlang AST processing bugs in Grafema beam-analyzer. Use when: (1) Elixir parser returns MODULE node but 0 functions/calls — body nesting issue, (2) Erlang parser crashes with "cannot convert list to string" on OTP 26+ — location format changed from integer to keyword list, (3) pipe operator |> creates spurious CALL nodes instead of desugared function calls — clause ordering bug, (4) multi-module .ex files return only the first module — missing __block__ handler, (5) installing Erlang/Elixir on macOS with outdated Xcode/Clang.

日本語の概要は準備中です。原文の説明を表示しています。

Disentinel/grafema362026年8月24日 更新

Fires after fixing any non-trivial bug or regression. Asks: could the graph have caught this as a guarantee? Pairs with reflection-in-and-on-action — picks up after "earliest catchable signal" and asks the next question: was that signal expressible in graph? Triggers: (1) after any non-trivial bug fix is verified working, (2) after a regression report (something used to work, broke), (3) during step 6 (knowledge extraction) of the workflow, (4) when reflection-on-action surfaces a "would have been catchable" signal. Outcome is a triage decision (graph-reachable? rule expressible? rule sound?) and either a draft Linear issue + guarantee proposal, or a recorded "graph capability gap" note. Never auto-creates guarantees.

日本語の概要は準備中です。原文の説明を表示しています。

Disentinel/grafema362026年8月24日 更新

Systematic methodology for achieving 100% backward dataflow reachability in a new language. Create gauntlet fixture, write trace, diagnose gaps, fix analyzer/algorithm, iterate to 100%. Language-agnostic process. Use when: (1) adding a new language to Grafema, (2) auditing dataflow coverage for existing language, (3) user says "/dataflow-gauntlet".

日本語の概要は準備中です。原文の説明を表示しています。

Disentinel/grafema362026年8月24日 更新

Fix docker exec hanging when starting background processes (servers, daemons) inside containers. Use when: (1) docker exec never returns despite using & or nohup, (2) background server started in container causes docker exec to hang indefinitely, (3) env_startup_command in SWE-bench or similar frameworks times out, (4) setsid/disown needed for proper process detachment in Docker. Root cause: docker exec tracks ALL processes in the exec session, not just the top-level PID.

日本語の概要は準備中です。原文の説明を表示しています。

Disentinel/grafema362026年8月24日 更新

Fix Node.js CLI tools crashing inside Docker containers when host-installed node_modules require a newer Node version than the container provides. Use when: (1) "SyntaxError: Invalid regular expression flags" with /v flag in string-width or similar packages, (2) node_modules installed on host with Node 20+ but container has Node 18, (3) `npm install` with file: protocol creates symlinks that break inside Docker, (4) pnpm workspace packages become broken symlinks in containers. Covers version detection, Node binary mounting, and npm install strategies for cross-version compatibility.

日本語の概要は準備中です。原文の説明を表示しています。

Disentinel/grafema362026年8月24日 更新

Fix intermittent `{:no_translation, :unicode, :latin1}` crashes in Elixir escript daemons that use length-prefixed framed IPC on stdin/stdout. Use when: (1) daemon worker crashes only on some input files, usually ones with non-ASCII bytes (kanji, cyrillic, emoji); (2) error surfaces as `Protocol error` from daemon's error branch or as garbled frame-length bytes seen by the orchestrator/client side; (3) standalone one-shot mode works fine on the same input but multi-request daemon mode fails; (4) `IO.binread(:stdio, N)` returns `{:error, {:no_translation, :unicode, :latin1}}` despite the "bin" prefix suggesting it should be encoding-agnostic. Root cause is the escript default `:standard_io` encoding — it's `:unicode`, and `IO.binread` still routes through the io_server, which translates bytes to codepoints and errors when raw binary frames contain invalid UTF-8 sequences.

日本語の概要は準備中です。原文の説明を表示しています。

Disentinel/grafema362026年8月24日 更新

Disentinel のスキルをすべて見る

このスキルの問題を報告する