diff --git a/playwriter/CHANGELOG.md b/playwriter/CHANGELOG.md new file mode 100644 index 0000000..191d781 --- /dev/null +++ b/playwriter/CHANGELOG.md @@ -0,0 +1,74 @@ +# Changelog + +## 0.0.3 + +### Patch Changes + +- Replace CommonJS `require` with ESM `import` for user-agents module + +## 2025-07-24 22:15 + +- Changed Chrome process stdio from 'ignore' to 'inherit' to print Chrome logs +- Helps with debugging CDP connection issues + +## 2025-07-24 22:00 + +- Simplified email validation by checking profiles directly in MCP connect tool +- Connect tool validates email against available profiles before starting Chrome +- Returns helpful message with available profiles when email doesn't match +- startPlaywriter now simply throws an error for invalid emails + +## 2025-07-24 21:45 + +- Added test infrastructure with vitest for MCP server testing +- Created mcp-client.ts with MCP client setup using vite-node +- Added comprehensive tests for Chrome CDP connection and console log capture +- Fixed callTool signatures to match MCP SDK API +- Added proper TypeScript types for CallToolResult + +## 2025-07-24 21:30 + +- Moved profile listing functionality into connect tool when emailProfile is not provided +- Updated parameter description with agent-appropriate phrasing ("ask your user/owner") +- Removed separate get_profiles tool for cleaner API +- Connect tool now handles both profile listing and connection in one place + +## 2025-07-24 21:15 + +- Modified startPlaywriter to accept optional emailProfile parameter +- Removed prompts dependency and interactive profile selection +- Connect tool now accepts emailProfile parameter or returns available profiles +- Added security guidance for profile selection in MCP response +- Suggests storing selected email in AGENTS.md or CLAUDE.md to avoid repeated selection + +## 2025-07-24 21:00 + +- Integrated Chrome launch via startPlaywriter from playwriter.ts +- Connect tool now starts Chrome with CDP port and connects via playwright.chromium.connectOverCDP +- Added proper cleanup handlers for browser and Chrome process on server shutdown +- Removed placeholder getActivePage function in favor of direct browser connection + +## 2025-07-24 20:50 + +- Moved console object definition outside of the Function constructor template string +- Improved code readability and maintainability + +## 2025-07-24 20:45 + +- Refactored console capture to use a custom console object instead of overriding global console +- Cleaner implementation that avoids modifying global state + +## 2025-07-24 20:40 + +- Enhanced execute tool to capture console.log, console.info, console.warn, console.error, and console.debug output +- Console methods are temporarily overridden during code execution to collect logs +- Output now includes both console logs and return values in a formatted response + +## 2025-07-24 20:35 + +- Added execute tool to run arbitrary JavaScript code with page and context in scope +- The tool uses the Playwright automation guide from prompt.md as its description + +## 2025-07-24 20:30 + +- Fixed MCP server tool registration API usage to match the correct method signature (name, description, schema, handler) \ No newline at end of file diff --git a/playwriter/README.md b/playwriter/README.md new file mode 100644 index 0000000..a6d7ba3 --- /dev/null +++ b/playwriter/README.md @@ -0,0 +1,60 @@ +# PlayWriter MCP + +A powerful browser automation MCP (Model Context Protocol) server that provides persistent browser sessions with advanced automation capabilities through Playwright. + +## Key Differences from playwright-mcp + +### ๐Ÿ”’ Persistent Data Directory +PlayWriter MCP maintains a persistent user data directory between runs. This enables: +- Login sessions with Google and other OAuth providers +- Website authentication that persists across sessions +- Test automation without repeated logins +- Preserved cookies, local storage, and browser state + +### ๐Ÿ›ก๏ธ Anti-Detection Features +Built-in detection prevention mechanisms allow automation on websites with bot protection: +- Works seamlessly with Google and other major platforms +- User-agent rotation +- Automation flag removal +- Realistic browser fingerprinting + +### ๐ŸŒ Shared Chrome Instance +- Single Chrome instance shared between all agents +- Each agent operates in its own tab +- Can reuse existing pages/tabs via `context.pages()` in the execute tool +- Find and switch between pages by URL or other criteria +- Efficient resource usage +- Better performance for multi-agent workflows + +### ๐Ÿš€ Single Powerful Execute Tool +Instead of many granular tools, PlayWriter provides one flexible `execute` tool: +- Direct access to Playwright `page` and `context` objects +- Write complex automation logic with loops and conditions +- Full JavaScript execution capabilities +- Custom wait conditions and complex interactions + +Example: +```javascript +// Complex wait logic with custom conditions +while (!(await page.isVisible('.success-message'))) { + await page.click('.retry-button'); + await page.waitForTimeout(1000); +} +``` + +### ๐Ÿ”ง Additional Tools +- `new_page` - Create a new browser tab/page +- `accessibility_snapshot` - Get page structure as JSON +- `console_logs` - Retrieve browser console messages +- `network_history` - Monitor network requests + +## Roadmap + +- โ˜๏ธ Cloud service integration for shared persistent state +- ๐Ÿงช Quality assertion tests in CI/CD pipelines +- ๐Ÿค– Multi-agent collaboration features +- ๐Ÿ“Š Advanced debugging and monitoring capabilities + +## Installation + +See the main project documentation for installation and setup instructions. \ No newline at end of file diff --git a/playwriter/bin.js b/playwriter/bin.js new file mode 100755 index 0000000..d5d0852 --- /dev/null +++ b/playwriter/bin.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import './dist/mcp.js' diff --git a/playwriter/package.json b/playwriter/package.json new file mode 100644 index 0000000..f753dae --- /dev/null +++ b/playwriter/package.json @@ -0,0 +1,38 @@ +{ + "name": "playwriter", + "description": "", + "version": "0.0.3", + "type": "module", + "repository": "https://github.com/remorses/", + "scripts": { + "build": "rm -rf dist *.tsbuildinfo && mkdir dist && cp src/prompt.md dist/ && tsc", + "prepublishOnly": "pnpm build", + "watch": "tsc -w", + "typecheck": "tsc --noEmit", + "mcp": "vite-node src/mcp.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "bin": "./bin.js", + "files": [ + "dist", + "src", + "esm", + "bin.js" + ], + "keywords": [], + "author": "Tommaso De Rossi, morse ", + "license": "", + "devDependencies": { + "@vitest/ui": "^3.2.4", + "vite-node": "^3.2.4", + "vitest": "^3.2.4" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.18.0", + "@playwright/mcp": "latest", + "patchright-core": "^1.52.5", + "user-agents": "^1.1.610", + "zod": "^4.1.12" + } +} diff --git a/playwriter/src/browser-config.ts b/playwriter/src/browser-config.ts new file mode 100644 index 0000000..6ed763f --- /dev/null +++ b/playwriter/src/browser-config.ts @@ -0,0 +1,64 @@ +import fs from 'node:fs' +import os from 'node:os' + +// Function to get the browser executable path +// Can be overridden by environment variable PLAYWRITER_BROWSER_PATH +export function getBrowserExecutablePath(): string { + // Check environment variable first + const envPath = process.env.PLAYWRITER_BROWSER_PATH + if (envPath && fs.existsSync(envPath)) { + return envPath + } + + // Check for Ghost Browser on macOS + // const ghostBrowserPath = '/Applications/Ghost Browser.app/Contents/MacOS/Ghost Browser' + // if (fs.existsSync(ghostBrowserPath)) { + // return ghostBrowserPath + // } + + // Fall back to finding Chrome + return findChromeExecutablePath() +} + +// Original Chrome finding logic +function findChromeExecutablePath(): string { + const osPlatform = os.platform() + const paths = (() => { + if (osPlatform === 'darwin') { + return [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + ] + } + if (osPlatform === 'win32') { + return [ + 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', + 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', + `${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`, + `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`, + `${process.env['PROGRAMFILES(X86)']}\\Google\\Chrome\\Application\\chrome.exe`, + ].filter(Boolean) + } + // Linux + return [ + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + '/snap/bin/chromium', + ] + })() + + for (const path of paths) { + const resolvedPath = path.startsWith('~') ? path.replace('~', process.env.HOME || '') : path + if (fs.existsSync(resolvedPath)) { + return resolvedPath + } + } + + throw new Error( + 'Could not find Chrome executable. Please install Google Chrome or set PLAYWRITER_BROWSER_PATH environment variable.', + ) +} diff --git a/playwriter/src/mcp-client.ts b/playwriter/src/mcp-client.ts new file mode 100644 index 0000000..8adaace --- /dev/null +++ b/playwriter/src/mcp-client.ts @@ -0,0 +1,73 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +import { Stream } from 'node:stream' +import path from 'node:path' +import url from 'node:url' + +const __filename = url.fileURLToPath(import.meta.url) + +export interface CreateTransportOptions { + clientName?: string +} + +export async function createTransport(args: string[]): Promise<{ + transport: Transport + stderr: Stream | null +}> { + const transport = new StdioClientTransport({ + command: 'pnpm', + args: ['vite-node', path.join(path.dirname(__filename), 'mcp.ts'), ...args], + cwd: path.join(path.dirname(__filename), '..'), + stderr: 'pipe', + env: { + ...process.env, + DEBUG: 'playwriter:mcp:test', + DEBUG_COLORS: '0', + DEBUG_HIDE_DATE: '1', + }, + }) + + return { + transport, + stderr: transport.stderr!, + } +} + +export async function createMCPClient(options?: CreateTransportOptions): Promise<{ + client: Client + stderr: string + cleanup: () => Promise +}> { + const client = new Client({ + name: options?.clientName ?? 'test', + version: '1.0.0', + }) + + const { transport, stderr } = await createTransport([]) + + let stderrBuffer = '' + stderr?.on('data', (data) => { + process.stderr.write(data) + + stderrBuffer += data.toString() + }) + + await client.connect(transport) + await client.ping() + + const cleanup = async () => { + try { + await client.close() + } catch (e) { + console.error('Error during MCP client cleanup:', e) + // Ignore errors during cleanup + } + } + + return { + client, + stderr: stderrBuffer, + cleanup, + } +} diff --git a/playwriter/src/mcp.test.ts b/playwriter/src/mcp.test.ts new file mode 100644 index 0000000..721ddb1 --- /dev/null +++ b/playwriter/src/mcp.test.ts @@ -0,0 +1,229 @@ +import { createMCPClient } from './mcp-client.js' +import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest' +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' + +describe('MCP Server Tests', () => { + let client: Awaited>['client'] + let cleanup: (() => Promise) | null = null + + beforeAll(async () => { + const result = await createMCPClient() + client = result.client + cleanup = result.cleanup + }) + + afterAll(async () => { + if (cleanup) { + await cleanup() + cleanup = null + } + }) + + it('should capture console logs', async () => { + // Connect first (open a new page) + const connectResult = await client.callTool({ + name: 'new_page', + arguments: {}, + }) + expect(connectResult.content).toBeDefined() + expect(connectResult.content).toMatchInlineSnapshot(` + [ + { + "text": "Created new page. URL: about:blank. Total pages: 20", + "type": "text", + }, + ] + `) + + // Navigate to a page and log something + const result = await client.callTool({ + name: 'execute', + arguments: { + code: ` + await page.goto('https://news.ycombinator.com'); + await page.evaluate(() => { + console.log('Test log message'); + console.error('Test error message'); + }); + `, + }, + }) + expect(result.content).toBeDefined() + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "Code executed successfully (no output)", + "type": "text", + }, + ] + `) + + // Get console logs + const logsResult = (await client.callTool({ + name: 'console_logs', + arguments: { + limit: 10, + }, + })) as CallToolResult + + expect(logsResult.content).toBeDefined() + expect(logsResult.content).toMatchInlineSnapshot(` + [ + { + "text": "[log]: Test log message :1:32 + [error]: Test error message :2:32", + "type": "text", + }, + ] + `) + + // Close the page opened + await client.callTool({ + name: 'close_page', + arguments: {}, + }) + }, 30000) + + it('should capture accessibility snapshot of hacker news', async () => { + // Create new page + await client.callTool({ + name: 'new_page', + arguments: {}, + }) + + // Navigate to a specific old Hacker News story that won't change + await client.callTool({ + name: 'execute', + arguments: { + code: `await page.goto('https://news.ycombinator.com/item?id=1', { waitUntil: 'networkidle' })`, + }, + }) + + // Get initial accessibility snapshot + const initialSnapshot = await client.callTool({ + name: 'accessibility_snapshot', + arguments: {}, + }) + expect(initialSnapshot.content).toBeDefined() + + // Save initial snapshot + const initialData = + typeof initialSnapshot === 'object' && initialSnapshot.content?.[0]?.text + ? tryJsonParse(initialSnapshot.content[0].text) + : initialSnapshot + expect(initialData).toMatchFileSnapshot('snapshots/hacker-news-initial-accessibility.md') + expect(initialData).toContain('table') + expect(initialData).toContain('Hacker News') + + // Focus on first link on the page + await client.callTool({ + name: 'execute', + arguments: { + code: ` + // Find and focus the first link + const firstLink = await page.$('a') + if (firstLink) { + await firstLink.focus() + const linkText = await firstLink.textContent() + console.log('Focused on first link:', linkText) + } + `, + }, + }) + + // Get snapshot after focusing + const focusedSnapshot = await client.callTool({ + name: 'accessibility_snapshot', + arguments: {}, + }) + expect(focusedSnapshot.content).toBeDefined() + + // Save focused snapshot + const focusedData = + typeof focusedSnapshot === 'object' && focusedSnapshot.content?.[0]?.text + ? tryJsonParse(focusedSnapshot.content[0].text) + : focusedSnapshot + expect(focusedData).toMatchFileSnapshot('snapshots/hacker-news-focused-accessibility.md') + + // Verify the snapshot contains expected content + expect(focusedData).toBeDefined() + expect(focusedData).toContain('link') + + // Press Tab to go to next item + await client.callTool({ + name: 'execute', + arguments: { + code: ` + await page.keyboard.press('Tab') + console.log('Pressed Tab key') + `, + }, + }) + + // Get snapshot after tab navigation + const tabbedSnapshot = await client.callTool({ + name: 'accessibility_snapshot', + arguments: {}, + }) + expect(tabbedSnapshot.content).toBeDefined() + + // Save tabbed snapshot + const tabbedData = + typeof tabbedSnapshot === 'object' && tabbedSnapshot.content?.[0]?.text + ? tryJsonParse(tabbedSnapshot.content[0].text) + : tabbedSnapshot + expect(tabbedData).toMatchFileSnapshot('snapshots/hacker-news-tabbed-accessibility.md') + + // Verify the snapshot is different + expect(tabbedData).toBeDefined() + expect(tabbedData).toContain('Hacker News') + + // Close the page opened + await client.callTool({ + name: 'close_page', + arguments: {}, + }) + }, 30000) + + it('should capture accessibility snapshot of shadcn UI', async () => { + // Create new page + await client.callTool({ + name: 'new_page', + arguments: {}, + }) + + // Navigate to shadcn UI + await client.callTool({ + name: 'execute', + arguments: { + code: `await page.goto('https://ui.shadcn.com/', { waitUntil: 'networkidle' })`, + }, + }) + + // Get accessibility snapshot + const snapshot = await client.callTool({ + name: 'accessibility_snapshot', + arguments: {}, + }) + expect(snapshot.content).toBeDefined() + + // Save snapshot + const data = + typeof snapshot === 'object' && snapshot.content?.[0]?.text ? tryJsonParse(snapshot.content[0].text) : snapshot + expect(data).toMatchFileSnapshot('snapshots/shadcn-ui-accessibility.md') + expect(data).toContain('shadcn') + + // Close the page opened + await client.callTool({ + name: 'close_page', + arguments: {}, + }) + }, 30000) +}) +function tryJsonParse(str: string) { + try { + return JSON.parse(str) + } catch { + return str + } +} diff --git a/playwriter/src/mcp.ts b/playwriter/src/mcp.ts new file mode 100644 index 0000000..18fc960 --- /dev/null +++ b/playwriter/src/mcp.ts @@ -0,0 +1,704 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { z } from 'zod' +import { Page, Browser, BrowserContext, chromium } from 'patchright-core' +import fs from 'node:fs' +import path from 'node:path' +import os from 'node:os' +import { spawn } from 'child_process' +import type { ChildProcess } from 'child_process' +import { getBrowserExecutablePath } from './browser-config.js' + +// Chrome executable finding logic moved to browser-config.ts + +// Store for maintaining state across tool calls +interface ToolState { + isConnected: boolean + page: Page | null + browser: Browser | null + chromeProcess: ChildProcess | null + consoleLogs: Map + networkRequests: Map +} + +const state: ToolState = { + isConnected: false, + page: null, + browser: null, + chromeProcess: null, + consoleLogs: new Map(), + networkRequests: new Map(), +} + +interface ConsoleMessage { + type: string + text: string + timestamp: number + location?: { + url: string + lineNumber: number + columnNumber: number + } +} + +interface NetworkRequest { + url: string + method: string + status: number + headers: Record + timestamp: number + duration: number + size: number + requestBody?: any + responseBody?: any +} + +const CDP_PORT = 9922 + +// Check if CDP is available on the specified port +async function isCDPAvailable(): Promise { + try { + const response = await fetch(`http://127.0.0.1:${CDP_PORT}/json/version`) + return response.ok + } catch { + return false + } +} + +// Launch Chrome with CDP enabled +async function launchChromeWithCDP(): Promise { + const userDataDir = path.join(os.homedir(), '.playwriter') + if (!fs.existsSync(userDataDir)) { + fs.mkdirSync(userDataDir, { recursive: true }) + } + + const executablePath = getBrowserExecutablePath() + + const chromeArgs = [ + `--remote-debugging-port=${CDP_PORT}`, + `--user-data-dir=${userDataDir}`, + '--no-first-run', + '--no-default-browser-check', + '--disable-session-crashed-bubble', + '--disable-features=DevToolsDebuggingRestrictions', + '--disable-blink-features=AutomationControlled', + '--no-sandbox', + '--disable-web-security', + '--disable-infobars', + '--disable-translate', + '--disable-features=AutomationControlled', // disables --enable-automation + '--disable-background-timer-throttling', + '--disable-popup-blocking', + '--disable-backgrounding-occluded-windows', + '--disable-renderer-backgrounding', + '--disable-window-activation', + '--disable-focus-on-load', + '--no-startup-window', + '--window-position=0,0', + '--disable-site-isolation-trials', + '--disable-features=IsolateOrigins,site-per-process', + ] + + const chromeProcess = spawn(executablePath, chromeArgs, { + detached: true, + stdio: 'ignore', + }) + + // Unref the process so it doesn't keep the parent process alive + chromeProcess.unref() + + // Give Chrome time to start up + await new Promise((resolve) => setTimeout(resolve, 2000)) + + return chromeProcess +} + +// Ensure connection to Chrome via CDP +async function ensureConnection(): Promise<{ browser: Browser; page: Page }> { + if (state.isConnected && state.browser && state.page) { + return { browser: state.browser, page: state.page } + } + + // Check if CDP is already available + const cdpAvailable = await isCDPAvailable() + + if (!cdpAvailable) { + // Launch Chrome with CDP + const chromeProcess = await launchChromeWithCDP() + state.chromeProcess = chromeProcess + } + + // Connect to Chrome via CDP + const browser = await chromium.connectOverCDP(`http://127.0.0.1:${CDP_PORT}`) + + // Get the default context + const contexts = browser.contexts() + let context: BrowserContext + + if (contexts.length > 0) { + context = contexts[0] + } else { + context = await browser.newContext() + } + + // Generate user agent and set it on context + const { default: UserAgent } = await import('user-agents') + const userAgent = new UserAgent({ + platform: 'MacIntel', + deviceCategory: 'desktop', + }) + + // Get or create page + const pages = context.pages() + let page: Page + + if (pages.length > 0) { + page = pages[0] + // Set user agent on existing page + await page.setExtraHTTPHeaders({ + 'User-Agent': userAgent.toString(), + }) + } else { + page = await context.newPage() + // Set user agent on new page + await page.setExtraHTTPHeaders({ + 'User-Agent': userAgent.toString(), + }) + } + + // Set up event listeners if not already set + if (!state.isConnected) { + page.on('console', (msg) => { + // Get or create logs array for this page + let pageLogs = state.consoleLogs.get(page) + if (!pageLogs) { + pageLogs = [] + state.consoleLogs.set(page, pageLogs) + } + + // Add new log + pageLogs.push({ + type: msg.type(), + text: msg.text(), + timestamp: Date.now(), + location: msg.location(), + }) + + // Keep only last 1000 logs + if (pageLogs.length > 1000) { + pageLogs.shift() + } + }) + + // Clean up logs and network requests when page is closed + page.on('close', () => { + state.consoleLogs.delete(page) + state.networkRequests.delete(page) + }) + + page.on('request', (request) => { + const startTime = Date.now() + const entry: Partial = { + url: request.url(), + method: request.method(), + headers: request.headers(), + timestamp: startTime, + } + + request + .response() + .then((response) => { + if (response) { + entry.status = response.status() + entry.duration = Date.now() - startTime + entry.size = response.headers()['content-length'] ? parseInt(response.headers()['content-length']) : 0 + + // Get or create requests array for this page + let pageRequests = state.networkRequests.get(page) + if (!pageRequests) { + pageRequests = [] + state.networkRequests.set(page, pageRequests) + } + + // Add new request + pageRequests.push(entry as NetworkRequest) + + // Keep only last 1000 requests + if (pageRequests.length > 1000) { + pageRequests.shift() + } + } + }) + .catch(() => { + // Handle response errors silently + }) + }) + } + + state.browser = browser + state.page = page + state.isConnected = true + + return { browser, page } +} + +// Initialize MCP server +const server = new McpServer({ + name: 'playwriter', + title: 'Playwright MCP Server', + version: '1.0.0', +}) + +// Tool 1: New Page - Creates a new browser page +server.tool('new_page', 'Create a new browser page in the shared Chrome instance', {}, async () => { + try { + const { browser, page } = await ensureConnection() + + // Always create a new page + const context = browser.contexts()[0] || (await browser.newContext()) + const newPage = await context.newPage() + + // Set user agent on new page + const { default: UserAgent } = await import('user-agents') + const userAgent = new UserAgent({ + platform: 'MacIntel', + deviceCategory: 'desktop', + }) + await newPage.setExtraHTTPHeaders({ + 'User-Agent': userAgent.toString(), + }) + + // Update state to use the new page + state.page = newPage + + // Set up event listeners on the new page + newPage.on('console', (msg) => { + // Get or create logs array for this page + let pageLogs = state.consoleLogs.get(newPage) + if (!pageLogs) { + pageLogs = [] + state.consoleLogs.set(newPage, pageLogs) + } + + // Add new log + pageLogs.push({ + type: msg.type(), + text: msg.text(), + timestamp: Date.now(), + location: msg.location(), + }) + + // Keep only last 1000 logs + if (pageLogs.length > 1000) { + pageLogs.shift() + } + }) + + // Clean up logs and network requests when page is closed + newPage.on('close', () => { + state.consoleLogs.delete(newPage) + state.networkRequests.delete(newPage) + }) + + newPage.on('request', (request) => { + const startTime = Date.now() + const entry: Partial = { + url: request.url(), + method: request.method(), + headers: request.headers(), + timestamp: startTime, + } + + request + .response() + .then((response) => { + if (response) { + entry.status = response.status() + entry.duration = Date.now() - startTime + entry.size = response.headers()['content-length'] ? parseInt(response.headers()['content-length']) : 0 + + // Get or create requests array for this page + let pageRequests = state.networkRequests.get(newPage) + if (!pageRequests) { + pageRequests = [] + state.networkRequests.set(newPage, pageRequests) + } + + // Add new request + pageRequests.push(entry as NetworkRequest) + + // Keep only last 1000 requests + if (pageRequests.length > 1000) { + pageRequests.shift() + } + } + }) + .catch(() => { + // Handle response errors silently + }) + }) + + return { + content: [ + { + type: 'text', + text: `Created new page. URL: ${newPage.url()}. Total pages: ${context.pages().length}`, + }, + ], + } + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Failed to create new page: ${error.message}`, + }, + ], + isError: true, + } + } +}) + +// Tool 2: Console Logs +server.tool( + 'console_logs', + 'Retrieve console messages from the page', + { + limit: z.number().default(50).describe('Maximum number of messages to return'), + type: z.enum(['log', 'info', 'warning', 'error', 'debug']).optional().describe('Filter by message type'), + offset: z.number().default(0).describe('Start from this index'), + }, + async ({ limit, type, offset }) => { + try { + const { page } = await ensureConnection() // Ensure we're connected first + + // Get logs for current page + const pageLogs = state.consoleLogs.get(page) || [] + + // Filter and paginate logs + let logs = [...pageLogs] + if (type) { + logs = logs.filter((log) => log.type === type) + } + + const paginatedLogs = logs.slice(offset, offset + limit) + + // Format logs to look like Chrome console output + let consoleOutput = '' + + if (paginatedLogs.length === 0) { + consoleOutput = 'No console messages' + } else { + consoleOutput = paginatedLogs + .map((log) => { + const timestamp = new Date(log.timestamp).toLocaleTimeString() + const location = log.location + ? ` ${log.location.url}:${log.location.lineNumber}:${log.location.columnNumber}` + : '' + return `[${log.type}]: ${log.text}${location}` + }) + .join('\n') + + if (logs.length > paginatedLogs.length) { + consoleOutput += `\n\n(Showing ${paginatedLogs.length} of ${logs.length} total messages)` + } + } + + return { + content: [ + { + type: 'text', + text: consoleOutput, + }, + ], + } + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Failed to get console logs: ${error.message}`, + }, + ], + isError: true, + } + } + }, +) + +// Tool 3: Network History +server.tool( + 'network_history', + 'Get history of network requests', + { + limit: z.number().default(50).describe('Maximum number of requests to return'), + urlPattern: z.string().optional().describe('Filter by URL pattern (supports wildcards)'), + method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().describe('Filter by HTTP method'), + statusCode: z + .object({ + min: z.number().optional(), + max: z.number().optional(), + }) + .optional() + .describe('Filter by status code range'), + includeBody: z.boolean().default(false).describe('Include request/response bodies'), + }, + async ({ limit, urlPattern, method, statusCode, includeBody }) => { + try { + const { page } = await ensureConnection() + + // Get requests for current page + const pageRequests = state.networkRequests.get(page) || [] + + // If includeBody is requested, we need to fetch bodies for existing requests + if (includeBody && pageRequests.length > 0) { + // Note: In a real implementation, you'd store bodies during capture + console.warn('Body capture not implemented in this example') + } + + // Filter requests + let requests = [...pageRequests] + + if (urlPattern) { + const pattern = new RegExp(urlPattern.replace(/\*/g, '.*')) + requests = requests.filter((req) => pattern.test(req.url)) + } + + if (method) { + requests = requests.filter((req) => req.method === method) + } + + if (statusCode) { + requests = requests.filter((req) => { + if (statusCode.min && req.status < statusCode.min) return false + if (statusCode.max && req.status > statusCode.max) return false + return true + }) + } + + const limitedRequests = requests.slice(-limit) + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total: requests.length, + requests: limitedRequests, + }, + null, + 2, + ), + }, + ], + } + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Failed to get network history: ${error.message}`, + }, + ], + isError: true, + } + } + }, +) + +// Tool 4: Accessibility Snapshot - Get page accessibility tree as JSON +server.tool('accessibility_snapshot', 'Get the accessibility snapshot of the current page as JSON', {}, async ({}) => { + try { + const { page } = await ensureConnection() + + // Check if the method exists + if (typeof (page as any)._snapshotForAI !== 'function') { + // Fall back to regular accessibility snapshot + const snapshot = await page.accessibility.snapshot({ + interestingOnly: true, + root: undefined, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify(snapshot, null, 2), + }, + ], + } + } + + const snapshot = await (page as any)._snapshotForAI() + + return { + content: [ + { + type: 'text', + text: snapshot, + }, + ], + } + } catch (error: any) { + console.error('Accessibility snapshot error:', error) + return { + content: [ + { + type: 'text', + text: `Failed to get accessibility snapshot: ${error.message}`, + }, + ], + isError: true, + } + } +}) + +// Tool 5: Execute - Run arbitrary JavaScript code with page and context in scope +const promptContent = fs.readFileSync(path.join(path.dirname(new URL(import.meta.url).pathname), 'prompt.md'), 'utf-8') + +server.tool( + 'execute', + promptContent, + { + code: z + .string() + .describe( + 'JavaScript code to execute with page and context in scope. Should be one line, using ; to execute multiple statements. To execute complex actions call execute multiple times. ', + ), + timeout: z.number().default(3000).describe('Timeout in milliseconds for code execution (default: 3000ms)'), + }, + async ({ code, timeout }) => { + const { page } = await ensureConnection() + const context = page.context() + console.error('Executing code:', code) + try { + // Collect console logs during execution + const consoleLogs: Array<{ method: string; args: any[] }> = [] + + // Create a custom console object that collects logs + const customConsole = { + log: (...args: any[]) => { + consoleLogs.push({ method: 'log', args }) + }, + info: (...args: any[]) => { + consoleLogs.push({ method: 'info', args }) + }, + warn: (...args: any[]) => { + consoleLogs.push({ method: 'warn', args }) + }, + error: (...args: any[]) => { + consoleLogs.push({ method: 'error', args }) + }, + debug: (...args: any[]) => { + consoleLogs.push({ method: 'debug', args }) + }, + } + + // Create a function that has page, context, and console in scope + const executeCode = new Function( + 'page', + 'context', + 'console', + ` + return (async () => { + ${code} + })(); + `, + ) + + // Execute the code with page, context, and custom console with timeout + const result = await Promise.race([ + executeCode(page, context, customConsole), + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Code execution timed out after ${timeout}ms`)), timeout), + ), + ]) + + // Format the response with both console output and return value + let responseText = '' + + // Add console logs if any + if (consoleLogs.length > 0) { + responseText += 'Console output:\n' + consoleLogs.forEach(({ method, args }) => { + const formattedArgs = args + .map((arg) => { + if (typeof arg === 'object') { + return JSON.stringify(arg, null, 2) + } + return String(arg) + }) + .join(' ') + responseText += `[${method}] ${formattedArgs}\n` + }) + responseText += '\n' + } + + // Add return value if any + if (result !== undefined) { + responseText += 'Return value:\n' + responseText += JSON.stringify(result, null, 2) + } else if (consoleLogs.length === 0) { + responseText += 'Code executed successfully (no output)' + } + + return { + content: [ + { + type: 'text', + text: responseText.trim(), + }, + ], + } + } catch (error: any) { + return { + content: [ + { + type: 'text', + text: `Error executing code: ${error.message}\n${error.stack}`, + }, + ], + isError: true, + } + } + }, +) + +// Start the server +async function main() { + const transport = new StdioServerTransport() + await server.connect(transport) + console.error('Playwright MCP server running on stdio') +} + +// Cleanup function +async function cleanup() { + console.error('Shutting down MCP server...') + + if (state.browser) { + try { + // Close the browser connection but not the Chrome process + // Since we're using CDP, closing the browser object just closes + // the connection, not the actual Chrome instance + await state.browser.close() + } catch (e) { + // Ignore errors during browser close + } + } + + // Don't kill the Chrome process - let it continue running + // The process was started with detached: true and unref() + // so it will persist after this process exits + + process.exit(0) +} + +// Handle process termination +process.on('SIGINT', cleanup) +process.on('SIGTERM', cleanup) +process.on('exit', () => { + // Browser cleanup is handled by the async cleanup function +}) + +main().catch(console.error) diff --git a/playwriter/src/prompt.md b/playwriter/src/prompt.md new file mode 100644 index 0000000..ab45007 --- /dev/null +++ b/playwriter/src/prompt.md @@ -0,0 +1,539 @@ +Executes code in the server to control playwright. + +You have access to a `page` object where you can call playwright methods on it to accomplish actions on the page. + +You can also use `console.log` to examine the results of your actions. + +You only have access to `page`, `context` and node.js globals. Do not try to import anything or setup handlers. + +Your code should be stateless and do not depend on any state. + +If you really want to attach listeners you should also detach them using a try finally block, to prevent memory leaks. + +You can also create a new page via `context.newPage()` if you need to start fresh. You can then find that page by iteration over `context.pages()`: + +```javascript +const page = context.pages().find((p) => p.url().includes('/some/path')) +``` + +## important rules + +- NEVER call `page.waitForTimeout`, instead use `page.waitForSelector` or use a while loop that waits for a condition to be true. +- when a timeout error happen for example during navigation don't worry too much. try to get the snapshot of the page to see the current state, then continue without retrying if the state is what you expect. If the state is not what you expect, then you can retry the action. +- only call `page.close()` if the user asks you so or if you are in a test feedback loop and you know the user is not dependently interacting with the page (for example for debugging). +- always call `new_page` at the start of a conversation. later this page will be passed to the `execute` tool. +- In some rare cases you can also skip `new_page` tool, if the user asks you to instead use an existing page in the browser. You can set a page as default using `state.page = page`, `execute` calls will be passed this page in the scope later on. +- if running in localhost and some elements are difficult to target with locators you can update the source code to add `data-testid` attributes to elements you want to target. This will make running tests much easier later on. Also update the source markdown documents your are following if you do so. +- after every action call the tool `accessibility_snapshot` to get the page structure and understand what elements are available on the page +- after form submissions use `page.waitForLoadState('networkidle')` to ensure the page is fully loaded before proceeding +- sometimes when in localhost and using Vite you can encounter issues in the first page load, where a module is not found, because of updated optimization of the node_modules. In these cases you can try reloading the page 2 times and see if the issue resolves itself. +- for Google and GitHub login always use the Google account you have access to, already signed in +- if you are following a markdown document describing the steps to follow to test the website, update this document if you encounter unexpected behavior or if you can add information that would make the test faster, for example telling how to wait for actions that trigger loading states or to use a different timeout for specific actions. + +## getting outputs of code execution + +You can use `console.log` to print values you want to see in the tool call result + +## using page.evaluate + +you can execute client side JavaScript code using `page.evaluate()` + +When executing code with `page.evaluate()`, return values directly from the evaluate function. Use `console.log()` outside of evaluate to display results: + +```javascript +// Get data from the page by returning it +const title = await page.evaluate(() => document.title) +console.log('Page title:', title) + +// Return multiple values as an object +const pageInfo = await page.evaluate(() => ({ + url: window.location.href, + buttonCount: document.querySelectorAll('button').length, + readyState: document.readyState, +})) +console.log('Page URL:', pageInfo.url) +console.log('Number of buttons:', pageInfo.buttonCount) +console.log('Page ready state:', pageInfo.readyState) +``` + +## Finding Elements on the Page + +you can use the tool accessibility_snapshot to get the page accessibility snapshot tree, which provides a structured view of the page's elements, including their roles and names. This is useful for understanding the page structure and finding elements to interact with. + +Example accessibility snapshot result: + +```md +- generic [active] [ref=e1]: + - generic [ref=e2]: + - banner [ref=e3]: + - generic [ref=e5]: + - link "shadcn/ui" [ref=e6] [cursor=pointer]: + - /url: / + - img + - generic [ref=e11] [cursor=pointer]: shadcn/ui + - navigation [ref=e12]: + - link "Docs" [ref=e13] [cursor=pointer]: + - /url: /docs/installation + - link "Components" [ref=e14] [cursor=pointer]: + - /url: /docs/components + - link "Blocks" [ref=e15] [cursor=pointer]: + - /url: /blocks + - link "Charts" [ref=e16] [cursor=pointer]: + - /url: /charts/area + - link "Themes" [ref=e17] [cursor=pointer]: + - /url: /themes + - link "Colors" [ref=e18] [cursor=pointer]: + - /url: /colors +``` + +Then you can use `page.locator(`aria-ref=${ref}`).describe(element);` to get an element with a specific `ref` and interact with it. + +For example: + +```javascript +const componentsLink = page + // Exact target element reference from the page snapshot + .locator('aria-ref=e14') + // Human-readable element description used to obtain permission to interact with the element + .describe('Components link') + +componentsLink.click() +console.log('Clicked on Components link') +``` + +This approach is the preferred way to find elements on the page, as it allows you to use the structured information from the accessibility snapshot to interact with elements reliably. + +You can also find `getByRole` to get elements on the page. + +```javascript +// Then use the information from the snapshot to click elements +// For example, if snapshot shows: { "role": "button", "name": "Sign In" } +await page.getByRole('button', { name: 'Sign In' }).click() + +// For a link with { "role": "link", "name": "About" } +await page.getByRole('link', { name: 'About' }).click() + +// For a textbox with { "role": "textbox", "name": "Email" } +await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com') + +// For a heading with { "role": "heading", "name": "Welcome to Example.com" } +const headingText = await page + .getByRole('heading', { name: 'Welcome to Example.com' }) + .textContent() +console.log('Heading text:', headingText) +``` + +### Complete Example: Find and Click Elements + +```javascript +await page.getByRole('button', { name: 'Submit Form' }).click() +console.log('Clicked submit button') + +await page.waitForLoadState('networkidle') +console.log('Form submitted successfully') +``` + +## Core Concepts + +### Page and Context + +In Playwright, automation happens through a `page` object (representing a browser tab) and `context` (representing a browser session with cookies, storage, etc.). + +```javascript +// Assuming you have page and context already available +const page = await context.newPage() +``` + +### Element Selection + +Playwright uses locators to find elements. The examples below show various selection methods: + +```javascript +// By role (recommended) +await page.getByRole('button', { name: 'Submit' }) + +// By text +await page.getByText('Welcome') + +// By placeholder +await page.getByPlaceholder('Enter email') + +// By label +await page.getByLabel('Username') + +// By test id +await page.getByTestId('submit-button') + +// By CSS selector +await page.locator('.my-class') + +// By XPath +await page.locator('//div[@class="content"]') +``` + +## Navigation + +### Navigate to URL + +```javascript +await page.goto('https://example.com') +// Wait for network idle (no requests for 500ms) +await page.goto('https://example.com', { waitUntil: 'networkidle' }) +``` + +### Navigate Back/Forward + +```javascript +// Go back to previous page +await page.goBack() + +// Go forward to next page +await page.goForward() +``` + +## Screenshots + +### Take Screenshot + +```javascript +// Screenshot of viewport +await page.screenshot({ path: 'screenshot.png' }) + +// Full page screenshot +await page.screenshot({ path: 'fullpage.png', fullPage: true }) + +// Screenshot of specific element +const element = await page.getByRole('button', { name: 'Submit' }) +await element.screenshot({ path: 'button.png' }) + +// Screenshot with custom dimensions +await page.setViewportSize({ width: 1280, height: 720 }) +await page.screenshot({ path: 'custom-size.png' }) +``` + +## Mouse Interactions + +### Click Elements + +```javascript +// Click by role +await page.getByRole('button', { name: 'Submit' }).click() + +// Click at coordinates +await page.mouse.click(100, 200) + +// Double click +await page.getByText('Double click me').dblclick() + +// Right click +await page.getByText('Right click me').click({ button: 'right' }) + +// Click with modifiers +await page.getByText('Ctrl click me').click({ modifiers: ['Control'] }) +``` + +### Hover + +```javascript +// Hover over element +await page.getByText('Hover me').hover() + +// Hover at coordinates +await page.mouse.move(100, 200) +``` + +## Keyboard Input + +### Type Text + +```javascript +// Type into input field +await page.getByLabel('Email').fill('user@example.com') + +// Type character by character (simulates real typing) +await page.getByLabel('Email').type('user@example.com', { delay: 100 }) + +// Clear and type +await page.getByLabel('Email').clear() +await page.getByLabel('Email').fill('new@example.com') +``` + +### Press Keys + +```javascript +// Press single key +await page.keyboard.press('Enter') + +// Press key combination +await page.keyboard.press('Control+A') + +// Press sequence of keys +await page.keyboard.press('Tab') +await page.keyboard.press('Tab') +await page.keyboard.press('Space') + +// Common key shortcuts +await page.keyboard.press('Control+C') // Copy +await page.keyboard.press('Control+V') // Paste +await page.keyboard.press('Control+Z') // Undo +``` + +## Form Interactions + +### Select Dropdown Options + +```javascript +// Select by value +await page.selectOption('select#country', 'us') + +// Select by label +await page.selectOption('select#country', { label: 'United States' }) + +// Select multiple options +await page.selectOption('select#colors', ['red', 'blue', 'green']) + +// Get selected option +const selectedValue = await page.$eval('select#country', (el) => el.value) +``` + +### Checkboxes and Radio Buttons + +```javascript +// Check checkbox +await page.getByLabel('I agree').check() + +// Uncheck checkbox +await page.getByLabel('Subscribe').uncheck() + +// Check if checked +const isChecked = await page.getByLabel('I agree').isChecked() + +// Select radio button +await page.getByLabel('Option A').check() +``` + +## JavaScript Evaluation + +### Execute JavaScript in Page Context + +```javascript +// Evaluate simple expression +const result = await page.evaluate(() => 2 + 2) + +// Access page variables +const pageTitle = await page.evaluate(() => document.title) + +// Modify page +await page.evaluate(() => { + document.body.style.backgroundColor = 'red' +}) + +// Pass arguments to page context +const sum = await page.evaluate(([a, b]) => a + b, [5, 3]) + +// Work with elements +const elementText = await page.evaluate( + (el) => el.textContent, + await page.getByRole('heading'), +) +``` + +### Execute JavaScript on Element + +```javascript +// Get element property +const href = await page.getByRole('link').evaluate((el) => el.href) + +// Modify element +await page.getByRole('button').evaluate((el) => { + el.style.backgroundColor = 'green' + el.disabled = true +}) + +// Scroll element into view +await page.getByText('Section').evaluate((el) => el.scrollIntoView()) +``` + +## File Handling + +### File Upload + +```javascript +// Upload single file +await page.getByLabel('Upload file').setInputFiles('/path/to/file.pdf') + +// Upload multiple files +await page + .getByLabel('Upload files') + .setInputFiles(['/path/to/file1.pdf', '/path/to/file2.pdf']) + +// Clear file input +await page.getByLabel('Upload file').setInputFiles([]) + +// For file inputs, use setInputFiles directly on the input element +// Find the file input element (often hidden) +await page.locator('input[type="file"]').setInputFiles('/path/to/file.pdf') +``` + +## Network Monitoring + +### Check Network Activity + +```javascript +// Wait for a specific request to complete and get its response +const response = await page.waitForResponse( + (response) => + response.url().includes('/api/user') && response.status() === 200, +) + +// Get response data +const responseBody = await response.json() +console.log('API response:', responseBody) + +// Wait for specific request +const request = await page.waitForRequest('**/api/data') +console.log('Request URL:', request.url()) +console.log('Request method:', request.method()) + +// Get all resources loaded by the page +const resources = await page.evaluate(() => + performance.getEntriesByType('resource').map((r) => ({ + name: r.name, + duration: r.duration, + size: r.transferSize, + })), +) +console.log('Page resources:', resources) +``` + +## Console Messages + +### Capture Console Output + +```javascript +// Console messages are automatically captured by the MCP implementation +// Use the console_logs tool to retrieve them + +// To trigger console messages from the page: +await page.evaluate(() => { + console.log('This message will be captured') + console.error('This error will be captured') + console.warn('This warning will be captured') +}) + +// Then use the console_logs MCP tool to retrieve all captured messages +// The tool provides filtering by type and pagination +``` + +## Waiting + +### Wait for Conditions + +```javascript +// Wait for element to appear +await page.waitForSelector('.success-message') + +// Wait for element to disappear +await page.waitForSelector('.loading', { state: 'hidden' }) + +await page.waitForURL(/github\.com.*\/pull/) +await page.waitForURL(/\/new-org/) + +// Wait for text to appear +await page.waitForFunction( + (text) => document.body.textContent.includes(text), + 'Success!', +) + +// Wait for navigation +await page.waitForURL('**/success') + +// Wait for page load +await page.waitForLoadState('networkidle') + +// Wait for specific condition +await page.waitForFunction( + (text) => document.querySelector('.status')?.textContent === text, + 'Ready', +) +``` + +### Wait for Text to Appear or Disappear + +```javascript +// Wait for specific text to appear on the page +await page.getByText('Loading complete').first().waitFor({ state: 'visible' }) +console.log('Loading complete text is now visible') + +// Wait for text to disappear from the page +await page.getByText('Loading...').first().waitFor({ state: 'hidden' }) +console.log('Loading text has disappeared') + +// Wait for multiple conditions sequentially +// First wait for loading to disappear, then wait for success message +await page.getByText('Processing...').first().waitFor({ state: 'hidden' }) +await page.getByText('Success!').first().waitFor({ state: 'visible' }) +console.log('Processing finished and success message appeared') + +// Example: Wait for error message to disappear before proceeding +await page + .getByText('Error: Please try again') + .first() + .waitFor({ state: 'hidden' }) +await page.getByRole('button', { name: 'Submit' }).click() + +// Example: Wait for confirmation text after form submission +await page.getByRole('button', { name: 'Save' }).click() +await page + .getByText('Your changes have been saved') + .first() + .waitFor({ state: 'visible' }) +console.log('Save confirmed') + +// Example: Wait for dynamic content to load +await page.getByRole('button', { name: 'Load More' }).click() +await page + .getByText('Loading more items...') + .first() + .waitFor({ state: 'visible' }) +await page + .getByText('Loading more items...') + .first() + .waitFor({ state: 'hidden' }) +console.log('Additional items loaded') +``` + +### Work with Frames + +```javascript +// Get frame by name +const frame = page.frame('frameName') + +// Get frame by URL +const frame = page.frame({ url: /frame\.html/ }) + +// Interact with frame content +await frame.getByText('In Frame').click() + +// Get all frames +const frames = page.frames() +``` + +## Best Practices + +### Reliable Selectors + +```javascript +// Prefer user-facing attributes +await page.getByRole('button', { name: 'Submit' }) +await page.getByLabel('Email') +await page.getByPlaceholder('Search...') +await page.getByText('Welcome') + +// Use test IDs for complex cases +await page.getByTestId('complex-component') + +// Avoid brittle selectors +// Bad: await page.locator('.btn-3842'); +// Good: await page.getByRole('button', { name: 'Submit' }); +``` diff --git a/playwriter/src/snapshots/hacker-news-focused-accessibility.md b/playwriter/src/snapshots/hacker-news-focused-accessibility.md new file mode 100644 index 0000000..1e5c937 --- /dev/null +++ b/playwriter/src/snapshots/hacker-news-focused-accessibility.md @@ -0,0 +1,202 @@ +- table [ref=e3]: + - rowgroup [ref=e4]: + - row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e5]: + - cell "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e6]: + - table [ref=e7]: + - rowgroup [ref=e8]: + - row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e9]: + - cell [ref=e10]: + - link [active] [ref=e11] [cursor=pointer]: + - /url: https://news.ycombinator.com + - img [ref=e12] [cursor=pointer] + - cell "Hacker Newsnew | past | comments | ask | show | jobs | submit" [ref=e13]: + - generic [ref=e14]: + - link "Hacker News" [ref=e16] [cursor=pointer]: + - /url: news + - link "new" [ref=e17] [cursor=pointer]: + - /url: newest + - text: "|" + - link "past" [ref=e18] [cursor=pointer]: + - /url: front + - text: "|" + - link "comments" [ref=e19] [cursor=pointer]: + - /url: newcomments + - text: "|" + - link "ask" [ref=e20] [cursor=pointer]: + - /url: ask + - text: "|" + - link "show" [ref=e21] [cursor=pointer]: + - /url: show + - text: "|" + - link "jobs" [ref=e22] [cursor=pointer]: + - /url: jobs + - text: "|" + - link "submit" [ref=e23] [cursor=pointer]: + - /url: submit + - cell "login" [ref=e24]: + - link "login" [ref=e26] [cursor=pointer]: + - /url: login?goto=item%3Fid%3D1 + - row [ref=e27] + - row "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e28]: + - cell "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e29]: + - table [ref=e30]: + - rowgroup [ref=e31]: + - row "upvote Y Combinator (ycombinator.com)" [ref=e32]: + - cell [ref=e33] + - cell "upvote" [ref=e35]: + - link "upvote" [ref=e37] [cursor=pointer]: + - /url: vote?id=1&how=up&goto=item%3Fid%3D1 + - cell "Y Combinator (ycombinator.com)" [ref=e39]: + - generic [ref=e40]: + - link "Y Combinator" [ref=e41] [cursor=pointer]: + - /url: http://ycombinator.com + - generic [ref=e42]: + - text: ( + - link "ycombinator.com" [ref=e43] [cursor=pointer]: + - /url: from?site=ycombinator.com + - generic [ref=e44] [cursor=pointer]: ycombinator.com + - text: ) + - row "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e45]: + - cell [ref=e46] + - cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]: + - generic [ref=e48]: + - generic [ref=e49]: 57 points + - text: by + - link "pg" [ref=e50] [cursor=pointer]: + - /url: user?id=pg + - link "on Oct 9, 2006" [ref=e52] [cursor=pointer]: + - /url: item?id=1 + - text: "|" + - link "hide" [ref=e54] [cursor=pointer]: + - /url: hide?id=1&goto=item%3Fid%3D1 + - text: "|" + - link "past" [ref=e55] [cursor=pointer]: + - /url: https://hn.algolia.com/?query=Y%20Combinator&type=story&dateRange=all&sort=byDate&storyText=false&prefix&page=0 + - text: "|" + - link "favorite" [ref=e56] [cursor=pointer]: + - /url: fave?id=1&auth=5328fcfde7333d68e67a8a2334e25acee5599932 + - text: "|" + - link "3 comments" [ref=e57] [cursor=pointer]: + - /url: item?id=1 + - row [ref=e58]: + - cell [ref=e59] + - cell [ref=e60] + - table [ref=e63]: + - rowgroup [ref=e64]: + - row "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]: + - cell "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e66]: + - table [ref=e67]: + - rowgroup [ref=e68]: + - row "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e69]: + - cell [ref=e70]: + - img + - cell "upvote" [ref=e72]: + - link "upvote" [ref=e74] [cursor=pointer]: + - /url: vote?id=15&how=up&goto=item%3Fid%3D1 + - cell "sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e76]: + - generic [ref=e78]: + - link "sama" [ref=e79] [cursor=pointer]: + - /url: user?id=sama + - link "on Oct 9, 2006" [ref=e81] [cursor=pointer]: + - /url: item?id=15 + - link "[โ€“]" [ref=e84] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e87]: + - generic [ref=e88]: "\"the rising star of venture capital\" -unknown VC eating lunch on SHR" + - generic: + - paragraph + - row "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e92]: + - cell "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e93]: + - table [ref=e94]: + - rowgroup [ref=e95]: + - row "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e96]: + - cell [ref=e97]: + - img [ref=e98] + - cell "upvote" [ref=e99]: + - link "upvote" [ref=e101] [cursor=pointer]: + - /url: vote?id=17&how=up&goto=item%3Fid%3D1 + - cell "pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e103]: + - generic [ref=e105]: + - link "pg" [ref=e106] [cursor=pointer]: + - /url: user?id=pg + - link "on Oct 9, 2006" [ref=e108] [cursor=pointer]: + - /url: item?id=17 + - generic [ref=e110]: + - text: "|" + - link [ref=e111] [cursor=pointer]: + - /url: "#15" + - text: parent + - link "[โ€“]" [ref=e112] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e115]: + - generic [ref=e116]: Is there anywhere to eat on Sandhill Road? + - generic: + - paragraph + - row "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e120]: + - cell "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e121]: + - table [ref=e122]: + - rowgroup [ref=e123]: + - row "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e124]: + - cell [ref=e125]: + - img [ref=e126] + - cell "upvote" [ref=e127]: + - link "upvote" [ref=e129] [cursor=pointer]: + - /url: vote?id=1079&how=up&goto=item%3Fid%3D1 + - cell "dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e131]: + - generic [ref=e133]: + - link "dmon" [ref=e134] [cursor=pointer]: + - /url: user?id=dmon + - link "on Feb 25, 2007" [ref=e136] [cursor=pointer]: + - /url: item?id=1079 + - generic [ref=e138]: + - text: "|" + - link [ref=e139] [cursor=pointer]: + - /url: "#15" + - text: root + - text: "|" + - link [ref=e140] [cursor=pointer]: + - /url: "#17" + - text: parent + - link "[โ€“]" [ref=e141] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e144]: + - generic [ref=e145]: sure + - generic: + - paragraph + - row "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e151]: + - cell "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e152]: + - img + - table [ref=e154]: + - rowgroup [ref=e155]: + - row [ref=e156]: + - cell [ref=e157] + - link "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4" [ref=e160] [cursor=pointer]: + - /url: https://www.ycombinator.com/apply/ + - generic [ref=e162]: + - generic [ref=e163]: + - link "Guidelines" [ref=e164] [cursor=pointer]: + - /url: newsguidelines.html + - text: "|" + - link "FAQ" [ref=e165] [cursor=pointer]: + - /url: newsfaq.html + - text: "|" + - link "Lists" [ref=e166] [cursor=pointer]: + - /url: lists + - text: "|" + - link "API" [ref=e167] [cursor=pointer]: + - /url: https://github.com/HackerNews/API + - text: "|" + - link "Security" [ref=e168] [cursor=pointer]: + - /url: security.html + - text: "|" + - link "Legal" [ref=e169] [cursor=pointer]: + - /url: https://www.ycombinator.com/legal/ + - text: "|" + - link "Apply to YC" [ref=e170] [cursor=pointer]: + - /url: https://www.ycombinator.com/apply/ + - text: "|" + - link "Contact" [ref=e171] [cursor=pointer]: + - /url: mailto:hn@ycombinator.com + - generic [ref=e174]: + - text: "Search:" + - textbox [ref=e175] \ No newline at end of file diff --git a/playwriter/src/snapshots/hacker-news-initial-accessibility.md b/playwriter/src/snapshots/hacker-news-initial-accessibility.md new file mode 100644 index 0000000..5d9fa46 --- /dev/null +++ b/playwriter/src/snapshots/hacker-news-initial-accessibility.md @@ -0,0 +1,202 @@ +- table [ref=e3]: + - rowgroup [ref=e4]: + - row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e5]: + - cell "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e6]: + - table [ref=e7]: + - rowgroup [ref=e8]: + - row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e9]: + - cell [ref=e10]: + - link [ref=e11] [cursor=pointer]: + - /url: https://news.ycombinator.com + - img [ref=e12] [cursor=pointer] + - cell "Hacker Newsnew | past | comments | ask | show | jobs | submit" [ref=e13]: + - generic [ref=e14]: + - link "Hacker News" [ref=e16] [cursor=pointer]: + - /url: news + - link "new" [ref=e17] [cursor=pointer]: + - /url: newest + - text: "|" + - link "past" [ref=e18] [cursor=pointer]: + - /url: front + - text: "|" + - link "comments" [ref=e19] [cursor=pointer]: + - /url: newcomments + - text: "|" + - link "ask" [ref=e20] [cursor=pointer]: + - /url: ask + - text: "|" + - link "show" [ref=e21] [cursor=pointer]: + - /url: show + - text: "|" + - link "jobs" [ref=e22] [cursor=pointer]: + - /url: jobs + - text: "|" + - link "submit" [ref=e23] [cursor=pointer]: + - /url: submit + - cell "login" [ref=e24]: + - link "login" [ref=e26] [cursor=pointer]: + - /url: login?goto=item%3Fid%3D1 + - row [ref=e27] + - row "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e28]: + - cell "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e29]: + - table [ref=e30]: + - rowgroup [ref=e31]: + - row "upvote Y Combinator (ycombinator.com)" [ref=e32]: + - cell [ref=e33] + - cell "upvote" [ref=e35]: + - link "upvote" [ref=e37] [cursor=pointer]: + - /url: vote?id=1&how=up&goto=item%3Fid%3D1 + - cell "Y Combinator (ycombinator.com)" [ref=e39]: + - generic [ref=e40]: + - link "Y Combinator" [ref=e41] [cursor=pointer]: + - /url: http://ycombinator.com + - generic [ref=e42]: + - text: ( + - link "ycombinator.com" [ref=e43] [cursor=pointer]: + - /url: from?site=ycombinator.com + - generic [ref=e44] [cursor=pointer]: ycombinator.com + - text: ) + - row "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e45]: + - cell [ref=e46] + - cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]: + - generic [ref=e48]: + - generic [ref=e49]: 57 points + - text: by + - link "pg" [ref=e50] [cursor=pointer]: + - /url: user?id=pg + - link "on Oct 9, 2006" [ref=e52] [cursor=pointer]: + - /url: item?id=1 + - text: "|" + - link "hide" [ref=e54] [cursor=pointer]: + - /url: hide?id=1&goto=item%3Fid%3D1 + - text: "|" + - link "past" [ref=e55] [cursor=pointer]: + - /url: https://hn.algolia.com/?query=Y%20Combinator&type=story&dateRange=all&sort=byDate&storyText=false&prefix&page=0 + - text: "|" + - link "favorite" [ref=e56] [cursor=pointer]: + - /url: fave?id=1&auth=5328fcfde7333d68e67a8a2334e25acee5599932 + - text: "|" + - link "3 comments" [ref=e57] [cursor=pointer]: + - /url: item?id=1 + - row [ref=e58]: + - cell [ref=e59] + - cell [ref=e60] + - table [ref=e63]: + - rowgroup [ref=e64]: + - row "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]: + - cell "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e66]: + - table [ref=e67]: + - rowgroup [ref=e68]: + - row "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e69]: + - cell [ref=e70]: + - img + - cell "upvote" [ref=e72]: + - link "upvote" [ref=e74] [cursor=pointer]: + - /url: vote?id=15&how=up&goto=item%3Fid%3D1 + - cell "sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e76]: + - generic [ref=e78]: + - link "sama" [ref=e79] [cursor=pointer]: + - /url: user?id=sama + - link "on Oct 9, 2006" [ref=e81] [cursor=pointer]: + - /url: item?id=15 + - link "[โ€“]" [ref=e84] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e87]: + - generic [ref=e88]: "\"the rising star of venture capital\" -unknown VC eating lunch on SHR" + - generic: + - paragraph + - row "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e92]: + - cell "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e93]: + - table [ref=e94]: + - rowgroup [ref=e95]: + - row "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e96]: + - cell [ref=e97]: + - img [ref=e98] + - cell "upvote" [ref=e99]: + - link "upvote" [ref=e101] [cursor=pointer]: + - /url: vote?id=17&how=up&goto=item%3Fid%3D1 + - cell "pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e103]: + - generic [ref=e105]: + - link "pg" [ref=e106] [cursor=pointer]: + - /url: user?id=pg + - link "on Oct 9, 2006" [ref=e108] [cursor=pointer]: + - /url: item?id=17 + - generic [ref=e110]: + - text: "|" + - link [ref=e111] [cursor=pointer]: + - /url: "#15" + - text: parent + - link "[โ€“]" [ref=e112] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e115]: + - generic [ref=e116]: Is there anywhere to eat on Sandhill Road? + - generic: + - paragraph + - row "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e120]: + - cell "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e121]: + - table [ref=e122]: + - rowgroup [ref=e123]: + - row "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e124]: + - cell [ref=e125]: + - img [ref=e126] + - cell "upvote" [ref=e127]: + - link "upvote" [ref=e129] [cursor=pointer]: + - /url: vote?id=1079&how=up&goto=item%3Fid%3D1 + - cell "dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e131]: + - generic [ref=e133]: + - link "dmon" [ref=e134] [cursor=pointer]: + - /url: user?id=dmon + - link "on Feb 25, 2007" [ref=e136] [cursor=pointer]: + - /url: item?id=1079 + - generic [ref=e138]: + - text: "|" + - link [ref=e139] [cursor=pointer]: + - /url: "#15" + - text: root + - text: "|" + - link [ref=e140] [cursor=pointer]: + - /url: "#17" + - text: parent + - link "[โ€“]" [ref=e141] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e144]: + - generic [ref=e145]: sure + - generic: + - paragraph + - row "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e151]: + - cell "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e152]: + - img + - table [ref=e154]: + - rowgroup [ref=e155]: + - row [ref=e156]: + - cell [ref=e157] + - link "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4" [ref=e160] [cursor=pointer]: + - /url: https://www.ycombinator.com/apply/ + - generic [ref=e162]: + - generic [ref=e163]: + - link "Guidelines" [ref=e164] [cursor=pointer]: + - /url: newsguidelines.html + - text: "|" + - link "FAQ" [ref=e165] [cursor=pointer]: + - /url: newsfaq.html + - text: "|" + - link "Lists" [ref=e166] [cursor=pointer]: + - /url: lists + - text: "|" + - link "API" [ref=e167] [cursor=pointer]: + - /url: https://github.com/HackerNews/API + - text: "|" + - link "Security" [ref=e168] [cursor=pointer]: + - /url: security.html + - text: "|" + - link "Legal" [ref=e169] [cursor=pointer]: + - /url: https://www.ycombinator.com/legal/ + - text: "|" + - link "Apply to YC" [ref=e170] [cursor=pointer]: + - /url: https://www.ycombinator.com/apply/ + - text: "|" + - link "Contact" [ref=e171] [cursor=pointer]: + - /url: mailto:hn@ycombinator.com + - generic [ref=e174]: + - text: "Search:" + - textbox [ref=e175] \ No newline at end of file diff --git a/playwriter/src/snapshots/hacker-news-tabbed-accessibility.md b/playwriter/src/snapshots/hacker-news-tabbed-accessibility.md new file mode 100644 index 0000000..27e3228 --- /dev/null +++ b/playwriter/src/snapshots/hacker-news-tabbed-accessibility.md @@ -0,0 +1,202 @@ +- table [ref=e3]: + - rowgroup [ref=e4]: + - row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e5]: + - cell "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e6]: + - table [ref=e7]: + - rowgroup [ref=e8]: + - row "Hacker Newsnew | past | comments | ask | show | jobs | submit login" [ref=e9]: + - cell [ref=e10]: + - link [ref=e11] [cursor=pointer]: + - /url: https://news.ycombinator.com + - img [ref=e12] [cursor=pointer] + - cell "Hacker Newsnew | past | comments | ask | show | jobs | submit" [ref=e13]: + - generic [ref=e14]: + - link "Hacker News" [active] [ref=e16] [cursor=pointer]: + - /url: news + - link "new" [ref=e17] [cursor=pointer]: + - /url: newest + - text: "|" + - link "past" [ref=e18] [cursor=pointer]: + - /url: front + - text: "|" + - link "comments" [ref=e19] [cursor=pointer]: + - /url: newcomments + - text: "|" + - link "ask" [ref=e20] [cursor=pointer]: + - /url: ask + - text: "|" + - link "show" [ref=e21] [cursor=pointer]: + - /url: show + - text: "|" + - link "jobs" [ref=e22] [cursor=pointer]: + - /url: jobs + - text: "|" + - link "submit" [ref=e23] [cursor=pointer]: + - /url: submit + - cell "login" [ref=e24]: + - link "login" [ref=e26] [cursor=pointer]: + - /url: login?goto=item%3Fid%3D1 + - row [ref=e27] + - row "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e28]: + - cell "upvote Y Combinator (ycombinator.com) 57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road? upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e29]: + - table [ref=e30]: + - rowgroup [ref=e31]: + - row "upvote Y Combinator (ycombinator.com)" [ref=e32]: + - cell [ref=e33] + - cell "upvote" [ref=e35]: + - link "upvote" [ref=e37] [cursor=pointer]: + - /url: vote?id=1&how=up&goto=item%3Fid%3D1 + - cell "Y Combinator (ycombinator.com)" [ref=e39]: + - generic [ref=e40]: + - link "Y Combinator" [ref=e41] [cursor=pointer]: + - /url: http://ycombinator.com + - generic [ref=e42]: + - text: ( + - link "ycombinator.com" [ref=e43] [cursor=pointer]: + - /url: from?site=ycombinator.com + - generic [ref=e44] [cursor=pointer]: ycombinator.com + - text: ) + - row "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e45]: + - cell [ref=e46] + - cell "57 points by pg on Oct 9, 2006 | hide | past | favorite | 3 comments" [ref=e47]: + - generic [ref=e48]: + - generic [ref=e49]: 57 points + - text: by + - link "pg" [ref=e50] [cursor=pointer]: + - /url: user?id=pg + - link "on Oct 9, 2006" [ref=e52] [cursor=pointer]: + - /url: item?id=1 + - text: "|" + - link "hide" [ref=e54] [cursor=pointer]: + - /url: hide?id=1&goto=item%3Fid%3D1 + - text: "|" + - link "past" [ref=e55] [cursor=pointer]: + - /url: https://hn.algolia.com/?query=Y%20Combinator&type=story&dateRange=all&sort=byDate&storyText=false&prefix&page=0 + - text: "|" + - link "favorite" [ref=e56] [cursor=pointer]: + - /url: fave?id=1&auth=5328fcfde7333d68e67a8a2334e25acee5599932 + - text: "|" + - link "3 comments" [ref=e57] [cursor=pointer]: + - /url: item?id=1 + - row [ref=e58]: + - cell [ref=e59] + - cell [ref=e60] + - table [ref=e63]: + - rowgroup [ref=e64]: + - row "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e65]: + - cell "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e66]: + - table [ref=e67]: + - rowgroup [ref=e68]: + - row "upvote sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e69]: + - cell [ref=e70]: + - img + - cell "upvote" [ref=e72]: + - link "upvote" [ref=e74] [cursor=pointer]: + - /url: vote?id=15&how=up&goto=item%3Fid%3D1 + - cell "sama on Oct 9, 2006 [โ€“] \"the rising star of venture capital\" -unknown VC eating lunch on SHR" [ref=e76]: + - generic [ref=e78]: + - link "sama" [ref=e79] [cursor=pointer]: + - /url: user?id=sama + - link "on Oct 9, 2006" [ref=e81] [cursor=pointer]: + - /url: item?id=15 + - link "[โ€“]" [ref=e84] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e87]: + - generic [ref=e88]: "\"the rising star of venture capital\" -unknown VC eating lunch on SHR" + - generic: + - paragraph + - row "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e92]: + - cell "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e93]: + - table [ref=e94]: + - rowgroup [ref=e95]: + - row "upvote pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e96]: + - cell [ref=e97]: + - img [ref=e98] + - cell "upvote" [ref=e99]: + - link "upvote" [ref=e101] [cursor=pointer]: + - /url: vote?id=17&how=up&goto=item%3Fid%3D1 + - cell "pg on Oct 9, 2006 | [โ€“] Is there anywhere to eat on Sandhill Road?" [ref=e103]: + - generic [ref=e105]: + - link "pg" [ref=e106] [cursor=pointer]: + - /url: user?id=pg + - link "on Oct 9, 2006" [ref=e108] [cursor=pointer]: + - /url: item?id=17 + - generic [ref=e110]: + - text: "|" + - link [ref=e111] [cursor=pointer]: + - /url: "#15" + - text: parent + - link "[โ€“]" [ref=e112] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e115]: + - generic [ref=e116]: Is there anywhere to eat on Sandhill Road? + - generic: + - paragraph + - row "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e120]: + - cell "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e121]: + - table [ref=e122]: + - rowgroup [ref=e123]: + - row "upvote dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e124]: + - cell [ref=e125]: + - img [ref=e126] + - cell "upvote" [ref=e127]: + - link "upvote" [ref=e129] [cursor=pointer]: + - /url: vote?id=1079&how=up&goto=item%3Fid%3D1 + - cell "dmon on Feb 25, 2007 | | [โ€“] sure" [ref=e131]: + - generic [ref=e133]: + - link "dmon" [ref=e134] [cursor=pointer]: + - /url: user?id=dmon + - link "on Feb 25, 2007" [ref=e136] [cursor=pointer]: + - /url: item?id=1079 + - generic [ref=e138]: + - text: "|" + - link [ref=e139] [cursor=pointer]: + - /url: "#15" + - text: root + - text: "|" + - link [ref=e140] [cursor=pointer]: + - /url: "#17" + - text: parent + - link "[โ€“]" [ref=e141] [cursor=pointer]: + - /url: javascript:void(0) + - generic [ref=e144]: + - generic [ref=e145]: sure + - generic: + - paragraph + - row "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e151]: + - cell "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4 Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact Search:" [ref=e152]: + - img + - table [ref=e154]: + - rowgroup [ref=e155]: + - row [ref=e156]: + - cell [ref=e157] + - link "Consider applying for YC's Fall 2025 batch! Applications are open till Aug 4" [ref=e160] [cursor=pointer]: + - /url: https://www.ycombinator.com/apply/ + - generic [ref=e162]: + - generic [ref=e163]: + - link "Guidelines" [ref=e164] [cursor=pointer]: + - /url: newsguidelines.html + - text: "|" + - link "FAQ" [ref=e165] [cursor=pointer]: + - /url: newsfaq.html + - text: "|" + - link "Lists" [ref=e166] [cursor=pointer]: + - /url: lists + - text: "|" + - link "API" [ref=e167] [cursor=pointer]: + - /url: https://github.com/HackerNews/API + - text: "|" + - link "Security" [ref=e168] [cursor=pointer]: + - /url: security.html + - text: "|" + - link "Legal" [ref=e169] [cursor=pointer]: + - /url: https://www.ycombinator.com/legal/ + - text: "|" + - link "Apply to YC" [ref=e170] [cursor=pointer]: + - /url: https://www.ycombinator.com/apply/ + - text: "|" + - link "Contact" [ref=e171] [cursor=pointer]: + - /url: mailto:hn@ycombinator.com + - generic [ref=e174]: + - text: "Search:" + - textbox [ref=e175] \ No newline at end of file diff --git a/playwriter/src/snapshots/shadcn-ui-accessibility.md b/playwriter/src/snapshots/shadcn-ui-accessibility.md new file mode 100644 index 0000000..cfde863 --- /dev/null +++ b/playwriter/src/snapshots/shadcn-ui-accessibility.md @@ -0,0 +1,553 @@ +- generic [active] [ref=e1]: + - generic [ref=e2]: + - banner [ref=e3]: + - generic [ref=e5]: + - link "shadcn/ui" [ref=e6] [cursor=pointer]: + - /url: / + - img + - generic [ref=e11] [cursor=pointer]: shadcn/ui + - navigation [ref=e12]: + - link "Docs" [ref=e13] [cursor=pointer]: + - /url: /docs/installation + - link "Components" [ref=e14] [cursor=pointer]: + - /url: /docs/components + - link "Blocks" [ref=e15] [cursor=pointer]: + - /url: /blocks + - link "Charts" [ref=e16] [cursor=pointer]: + - /url: /charts/area + - link "Themes" [ref=e17] [cursor=pointer]: + - /url: /themes + - link "Colors" [ref=e18] [cursor=pointer]: + - /url: /colors + - generic [ref=e19]: + - button "Search documentation... โŒ˜ K" [ref=e21]: + - generic [ref=e22]: Search documentation... + - generic [ref=e23]: + - generic: โŒ˜ + - generic: K + - link "91.7k" [ref=e26] [cursor=pointer]: + - /url: https://github.com/shadcn-ui/ui + - img + - generic [ref=e29] [cursor=pointer]: 91.7k + - button "Toggle layout" [ref=e30]: + - generic [ref=e31]: Toggle layout + - img + - button "Toggle theme" [ref=e36]: + - img + - generic [ref=e44]: Toggle theme + - main [ref=e45]: + - generic [ref=e46]: + - generic [ref=e49]: + - link "New Calendar Component" [ref=e50] [cursor=pointer]: + - /url: /docs/components/calendar + - text: New Calendar Component + - img + - heading "The Foundation for your Design System" [level=1] [ref=e54] + - paragraph [ref=e55]: A set of beautifully designed components that you can customize, extend, and build on. Start here then make it your own. Open Source. Open Code. + - generic [ref=e56]: + - link "Get Started" [ref=e57] [cursor=pointer]: + - /url: /docs/installation + - link "View Components" [ref=e58] [cursor=pointer]: + - /url: /docs/components + - generic [ref=e60]: + - generic [ref=e65]: + - link "Examples" [ref=e66] [cursor=pointer]: + - /url: / + - link "Dashboard" [ref=e67] [cursor=pointer]: + - /url: /examples/dashboard + - link "Tasks" [ref=e68] [cursor=pointer]: + - /url: /examples/tasks + - link "Playground" [ref=e69] [cursor=pointer]: + - /url: /examples/playground + - link "Authentication" [ref=e70] [cursor=pointer]: + - /url: /examples/authentication + - generic [ref=e71]: + - generic [ref=e72]: Theme + - combobox "Theme" [ref=e73]: + - generic [ref=e74]: "Theme:" + - generic: Default + - img + - generic [ref=e81]: + - generic [ref=e82]: + - generic [ref=e83]: + - generic [ref=e84]: + - generic [ref=e85]: + - generic [ref=e86]: Total Revenue + - generic [ref=e87]: $15,231.89 + - generic [ref=e88]: +20.1% from last month + - img [ref=e93] + - generic [ref=e99]: + - generic [ref=e100]: + - generic [ref=e101]: Subscriptions + - generic [ref=e102]: +2,350 + - generic [ref=e103]: +180.1% from last month + - button "View More" [ref=e105] + - img [ref=e110] + - generic [ref=e123]: + - generic [ref=e124]: + - generic [ref=e125]: + - generic [ref=e126]: + - generic [ref=e127]: Upgrade your subscription + - generic [ref=e128]: You are currently on the free plan. Upgrade to the pro plan to get access to all features. + - generic [ref=e130]: + - generic [ref=e131]: + - generic [ref=e132]: + - generic [ref=e133]: Name + - textbox "Name" [ref=e134] + - generic [ref=e135]: + - generic [ref=e136]: Email + - textbox "Email" [ref=e137] + - generic [ref=e138]: + - generic [ref=e139]: Card Number + - generic [ref=e140]: + - textbox "Card Number" [ref=e141] + - textbox "MM/YY" [ref=e142] + - textbox "CVC" [ref=e143] + - group "Plan" [ref=e144]: + - generic [ref=e145]: Plan + - paragraph [ref=e146]: Select the plan that best fits your needs. + - radiogroup [ref=e147]: + - generic [ref=e148]: + - radio "Starter Plan Perfect for small businesses." [checked] [ref=e149]: + - img [ref=e151] + - generic [ref=e153]: + - generic [ref=e154]: Starter Plan + - generic [ref=e155]: Perfect for small businesses. + - generic [ref=e156]: + - radio "Pro Plan More features and storage." [ref=e157] + - generic [ref=e158]: + - generic [ref=e159]: Pro Plan + - generic [ref=e160]: More features and storage. + - generic [ref=e161]: + - generic [ref=e162]: Notes + - textbox "Notes" [ref=e163] + - generic [ref=e164]: + - generic [ref=e165]: + - checkbox "I agree to the terms and conditions" [ref=e166] + - generic [ref=e167]: I agree to the terms and conditions + - generic [ref=e168]: + - checkbox "Allow us to send you emails" [checked] [ref=e169]: + - generic: + - img + - generic [ref=e173]: Allow us to send you emails + - generic [ref=e174]: + - button "Cancel" [ref=e175] + - button "Upgrade Plan" [ref=e176] + - generic [ref=e177]: + - generic [ref=e178]: + - generic [ref=e179]: Team Members + - generic [ref=e180]: Invite your team members to collaborate. + - generic [ref=e181]: + - generic [ref=e182]: + - generic [ref=e183]: + - img "Image" [ref=e185] + - generic [ref=e186]: + - paragraph [ref=e187]: Sofia Davis + - paragraph [ref=e188]: m@example.com + - button "Owner" [ref=e189]: + - text: Owner + - img + - generic [ref=e192]: + - generic [ref=e193]: + - img "Image" [ref=e195] + - generic [ref=e196]: + - paragraph [ref=e197]: Jackson Lee + - paragraph [ref=e198]: p@example.com + - button "Developer" [ref=e199]: + - text: Developer + - img + - generic [ref=e202]: + - generic [ref=e203]: + - img "Image" [ref=e205] + - generic [ref=e206]: + - paragraph [ref=e207]: Isabella Nguyen + - paragraph [ref=e208]: i@example.com + - button "Billing" [ref=e209]: + - text: Billing + - img + - generic [ref=e212]: + - generic [ref=e213]: + - generic [ref=e214]: Cookie Settings + - generic [ref=e215]: Manage your cookie settings here. + - generic [ref=e216]: + - generic [ref=e217]: + - generic [ref=e218]: + - generic [ref=e219]: Strictly Necessary + - generic [ref=e220]: These cookies are essential in order to use the website and use its features. + - switch "Necessary" [checked] [ref=e221] + - generic [ref=e223]: + - generic [ref=e224]: + - generic [ref=e225]: Functional Cookies + - generic [ref=e226]: These cookies allow the website to provide personalized functionality. + - switch "Functional" [ref=e227] + - button "Save preferences" [ref=e230] + - generic [ref=e231]: + - generic [ref=e232]: + - generic [ref=e233]: + - generic [ref=e234]: Create an account + - generic [ref=e235]: Enter your email below to create your account + - generic [ref=e236]: + - generic [ref=e237]: + - button "GitHub" [ref=e238]: + - img + - text: GitHub + - button "Google" [ref=e241]: + - img + - text: Google + - generic [ref=e248]: Or continue with + - generic [ref=e249]: + - generic [ref=e250]: Email + - textbox "Email" [ref=e251] + - generic [ref=e252]: + - generic [ref=e253]: Password + - textbox "Password" [ref=e254] + - button "Create account" [ref=e256] + - generic [ref=e257]: + - generic [ref=e258]: + - generic [ref=e259]: + - img "Image" [ref=e261] + - generic [ref=e262]: + - paragraph [ref=e263]: Sofia Davis + - paragraph [ref=e264]: m@example.com + - button "New message" [ref=e265]: + - img + - generic [ref=e269]: New message + - generic [ref=e271]: + - generic [ref=e272]: Hi, how can I help you today? + - generic [ref=e273]: Hey, I'm having trouble with my account. + - generic [ref=e274]: What seems to be the problem? + - generic [ref=e275]: I can't log in. + - generic [ref=e277]: + - textbox "Type your message..." [ref=e278] + - button "Send" [disabled]: + - img + - generic: Send + - generic [ref=e285]: + - generic [ref=e286]: + - generic [ref=e287]: Report an issue + - generic [ref=e288]: What area are you having problems with? + - generic [ref=e289]: + - generic [ref=e290]: + - generic [ref=e291]: + - generic [ref=e292]: Area + - combobox "Area" [ref=e293]: + - generic: Billing + - img + - generic [ref=e297]: + - generic [ref=e298]: Security Level + - combobox "Security Level" [ref=e299]: + - generic: Severity 2 + - img + - generic [ref=e303]: + - generic [ref=e304]: Subject + - textbox "Subject" [ref=e305] + - generic [ref=e306]: + - generic [ref=e307]: Description + - textbox "Description" [ref=e308] + - generic [ref=e309]: + - button "Cancel" [ref=e310] + - button "Submit" [ref=e311] + - generic [ref=e312]: + - generic [ref=e313]: + - generic [ref=e317]: + - navigation [ref=e318]: + - button "Go to the Previous Month" [ref=e319]: + - img + - button "Go to the Next Month" [ref=e322]: + - img + - generic [ref=e325]: + - status [ref=e327]: June 2025 + - grid "June 2025" [ref=e328]: + - rowgroup [ref=e329]: + - row [ref=e330]: + - columnheader [ref=e331]: Su + - columnheader [ref=e332]: Mo + - columnheader [ref=e333]: Tu + - columnheader [ref=e334]: We + - columnheader [ref=e335]: Th + - columnheader [ref=e336]: Fr + - columnheader [ref=e337]: Sa + - rowgroup [ref=e338]: + - row "Sunday, June 1st, 2025 Monday, June 2nd, 2025 Tuesday, June 3rd, 2025 Wednesday, June 4th, 2025 Thursday, June 5th, 2025, selected Friday, June 6th, 2025, selected Saturday, June 7th, 2025, selected" [ref=e339]: + - gridcell "Sunday, June 1st, 2025" [ref=e340]: + - button "Sunday, June 1st, 2025" [ref=e341]: "1" + - gridcell "Monday, June 2nd, 2025" [ref=e342]: + - button "Monday, June 2nd, 2025" [ref=e343]: "2" + - gridcell "Tuesday, June 3rd, 2025" [ref=e344]: + - button "Tuesday, June 3rd, 2025" [ref=e345]: "3" + - gridcell "Wednesday, June 4th, 2025" [ref=e346]: + - button "Wednesday, June 4th, 2025" [ref=e347]: "4" + - gridcell "Thursday, June 5th, 2025, selected" [selected] [ref=e348]: + - button "Thursday, June 5th, 2025, selected" [ref=e349]: "5" + - gridcell "Friday, June 6th, 2025, selected" [selected] [ref=e350]: + - button "Friday, June 6th, 2025, selected" [ref=e351]: "6" + - gridcell "Saturday, June 7th, 2025, selected" [selected] [ref=e352]: + - button "Saturday, June 7th, 2025, selected" [ref=e353]: "7" + - row "Sunday, June 8th, 2025, selected Monday, June 9th, 2025, selected Tuesday, June 10th, 2025, selected Wednesday, June 11th, 2025, selected Thursday, June 12th, 2025, selected Friday, June 13th, 2025, selected Saturday, June 14th, 2025" [ref=e354]: + - gridcell "Sunday, June 8th, 2025, selected" [selected] [ref=e355]: + - button "Sunday, June 8th, 2025, selected" [ref=e356]: "8" + - gridcell "Monday, June 9th, 2025, selected" [selected] [ref=e357]: + - button "Monday, June 9th, 2025, selected" [ref=e358]: "9" + - gridcell "Tuesday, June 10th, 2025, selected" [selected] [ref=e359]: + - button "Tuesday, June 10th, 2025, selected" [ref=e360]: "10" + - gridcell "Wednesday, June 11th, 2025, selected" [selected] [ref=e361]: + - button "Wednesday, June 11th, 2025, selected" [ref=e362]: "11" + - gridcell "Thursday, June 12th, 2025, selected" [selected] [ref=e363]: + - button "Thursday, June 12th, 2025, selected" [ref=e364]: "12" + - gridcell "Friday, June 13th, 2025, selected" [selected] [ref=e365]: + - button "Friday, June 13th, 2025, selected" [ref=e366]: "13" + - gridcell "Saturday, June 14th, 2025" [ref=e367]: + - button "Saturday, June 14th, 2025" [ref=e368]: "14" + - row "Sunday, June 15th, 2025 Monday, June 16th, 2025 Tuesday, June 17th, 2025 Wednesday, June 18th, 2025 Thursday, June 19th, 2025 Friday, June 20th, 2025 Saturday, June 21st, 2025" [ref=e369]: + - gridcell "Sunday, June 15th, 2025" [ref=e370]: + - button "Sunday, June 15th, 2025" [ref=e371]: "15" + - gridcell "Monday, June 16th, 2025" [ref=e372]: + - button "Monday, June 16th, 2025" [ref=e373]: "16" + - gridcell "Tuesday, June 17th, 2025" [ref=e374]: + - button "Tuesday, June 17th, 2025" [ref=e375]: "17" + - gridcell "Wednesday, June 18th, 2025" [ref=e376]: + - button "Wednesday, June 18th, 2025" [ref=e377]: "18" + - gridcell "Thursday, June 19th, 2025" [ref=e378]: + - button "Thursday, June 19th, 2025" [ref=e379]: "19" + - gridcell "Friday, June 20th, 2025" [ref=e380]: + - button "Friday, June 20th, 2025" [ref=e381]: "20" + - gridcell "Saturday, June 21st, 2025" [ref=e382]: + - button "Saturday, June 21st, 2025" [ref=e383]: "21" + - row "Sunday, June 22nd, 2025 Monday, June 23rd, 2025 Tuesday, June 24th, 2025 Wednesday, June 25th, 2025 Thursday, June 26th, 2025 Friday, June 27th, 2025 Saturday, June 28th, 2025" [ref=e384]: + - gridcell "Sunday, June 22nd, 2025" [ref=e385]: + - button "Sunday, June 22nd, 2025" [ref=e386]: "22" + - gridcell "Monday, June 23rd, 2025" [ref=e387]: + - button "Monday, June 23rd, 2025" [ref=e388]: "23" + - gridcell "Tuesday, June 24th, 2025" [ref=e389]: + - button "Tuesday, June 24th, 2025" [ref=e390]: "24" + - gridcell "Wednesday, June 25th, 2025" [ref=e391]: + - button "Wednesday, June 25th, 2025" [ref=e392]: "25" + - gridcell "Thursday, June 26th, 2025" [ref=e393]: + - button "Thursday, June 26th, 2025" [ref=e394]: "26" + - gridcell "Friday, June 27th, 2025" [ref=e395]: + - button "Friday, June 27th, 2025" [ref=e396]: "27" + - gridcell "Saturday, June 28th, 2025" [ref=e397]: + - button "Saturday, June 28th, 2025" [ref=e398]: "28" + - row "Sunday, June 29th, 2025 Monday, June 30th, 2025 Tuesday, July 1st, 2025 Wednesday, July 2nd, 2025 Thursday, July 3rd, 2025 Friday, July 4th, 2025 Saturday, July 5th, 2025" [ref=e399]: + - gridcell "Sunday, June 29th, 2025" [ref=e400]: + - button "Sunday, June 29th, 2025" [ref=e401]: "29" + - gridcell "Monday, June 30th, 2025" [ref=e402]: + - button "Monday, June 30th, 2025" [ref=e403]: "30" + - gridcell "Tuesday, July 1st, 2025" [ref=e404]: + - button "Tuesday, July 1st, 2025" [ref=e405]: "1" + - gridcell "Wednesday, July 2nd, 2025" [ref=e406]: + - button "Wednesday, July 2nd, 2025" [ref=e407]: "2" + - gridcell "Thursday, July 3rd, 2025" [ref=e408]: + - button "Thursday, July 3rd, 2025" [ref=e409]: "3" + - gridcell "Friday, July 4th, 2025" [ref=e410]: + - button "Friday, July 4th, 2025" [ref=e411]: "4" + - gridcell "Saturday, July 5th, 2025" [ref=e412]: + - button "Saturday, July 5th, 2025" [ref=e413]: "5" + - generic [ref=e415]: + - generic [ref=e416]: + - generic [ref=e417]: Move Goal + - generic [ref=e418]: Set your daily activity goal. + - generic [ref=e419]: + - generic [ref=e420]: + - button "Decrease" [ref=e421]: + - img + - generic [ref=e424]: Decrease + - generic [ref=e425]: + - generic [ref=e426]: "350" + - generic [ref=e427]: Calories/day + - button "Increase" [ref=e428]: + - img + - generic [ref=e432]: Increase + - img [ref=e437] + - button "Set Goal" [ref=e472] + - generic [ref=e474]: + - generic [ref=e475]: + - generic [ref=e476]: Exercise Minutes + - generic [ref=e477]: Your exercise minutes are ahead of where you normally are. + - generic [ref=e481]: + - application [ref=e482]: + - generic [ref=e494]: + - generic [ref=e497]: Mon + - generic [ref=e500]: Tue + - generic [ref=e503]: Wed + - generic [ref=e506]: Thu + - generic [ref=e509]: Fri + - generic [ref=e512]: Sat + - generic [ref=e515]: Sun + - generic: + - generic: + - generic: Thursday + - generic: + - generic: + - generic: + - generic: + - generic: Today + - generic: "390" + - generic: + - generic: + - generic: + - generic: Average + - generic: "278" + - generic [ref=e542]: + - generic [ref=e543]: + - generic [ref=e544]: Payments + - generic [ref=e545]: Manage your payments. + - button "Add Payment" [ref=e547] + - generic [ref=e548]: + - table [ref=e551]: + - rowgroup [ref=e552]: + - row "Select all Status Email Amount" [ref=e553]: + - cell "Select all" [ref=e554]: + - checkbox "Select all" [ref=e555] + - cell "Status" [ref=e556] + - cell "Email" [ref=e557] + - cell "Amount" [ref=e558]: + - generic [ref=e559]: Amount + - cell [ref=e560] + - rowgroup [ref=e561]: + - row "Select row success ken99@example.com $316.00 Open menu" [ref=e562]: + - cell "Select row" [ref=e563]: + - checkbox "Select row" [ref=e564] + - cell "success" [ref=e565]: + - generic [ref=e566]: success + - cell "ken99@example.com" [ref=e567]: + - generic [ref=e568]: ken99@example.com + - cell "$316.00" [ref=e569]: + - generic [ref=e570]: $316.00 + - cell "Open menu" [ref=e571]: + - button "Open menu" [ref=e572]: + - generic [ref=e573]: Open menu + - img + - row "Select row success Abe45@example.com $242.00 Open menu" [ref=e578]: + - cell "Select row" [ref=e579]: + - checkbox "Select row" [ref=e580] + - cell "success" [ref=e581]: + - generic [ref=e582]: success + - cell "Abe45@example.com" [ref=e583]: + - generic [ref=e584]: Abe45@example.com + - cell "$242.00" [ref=e585]: + - generic [ref=e586]: $242.00 + - cell "Open menu" [ref=e587]: + - button "Open menu" [ref=e588]: + - generic [ref=e589]: Open menu + - img + - row "Select row processing Monserrat44@example.com $837.00 Open menu" [ref=e594]: + - cell "Select row" [ref=e595]: + - checkbox "Select row" [ref=e596] + - cell "processing" [ref=e597]: + - generic [ref=e598]: processing + - cell "Monserrat44@example.com" [ref=e599]: + - generic [ref=e600]: Monserrat44@example.com + - cell "$837.00" [ref=e601]: + - generic [ref=e602]: $837.00 + - cell "Open menu" [ref=e603]: + - button "Open menu" [ref=e604]: + - generic [ref=e605]: Open menu + - img + - row "Select row failed carmella@example.com $721.00 Open menu" [ref=e610]: + - cell "Select row" [ref=e611]: + - checkbox "Select row" [ref=e612] + - cell "failed" [ref=e613]: + - generic [ref=e614]: failed + - cell "carmella@example.com" [ref=e615]: + - generic [ref=e616]: carmella@example.com + - cell "$721.00" [ref=e617]: + - generic [ref=e618]: $721.00 + - cell "Open menu" [ref=e619]: + - button "Open menu" [ref=e620]: + - generic [ref=e621]: Open menu + - img + - row "Select row pending jason78@example.com $450.00 Open menu" [ref=e626]: + - cell "Select row" [ref=e627]: + - checkbox "Select row" [ref=e628] + - cell "pending" [ref=e629]: + - generic [ref=e630]: pending + - cell "jason78@example.com" [ref=e631]: + - generic [ref=e632]: jason78@example.com + - cell "$450.00" [ref=e633]: + - generic [ref=e634]: $450.00 + - cell "Open menu" [ref=e635]: + - button "Open menu" [ref=e636]: + - generic [ref=e637]: Open menu + - img + - row "Select row success sarah23@example.com $1,280.00 Open menu" [ref=e642]: + - cell "Select row" [ref=e643]: + - checkbox "Select row" [ref=e644] + - cell "success" [ref=e645]: + - generic [ref=e646]: success + - cell "sarah23@example.com" [ref=e647]: + - generic [ref=e648]: sarah23@example.com + - cell "$1,280.00" [ref=e649]: + - generic [ref=e650]: $1,280.00 + - cell "Open menu" [ref=e651]: + - button "Open menu" [ref=e652]: + - generic [ref=e653]: Open menu + - img + - generic [ref=e658]: + - generic [ref=e659]: 0 of 6 row(s) selected. + - generic [ref=e660]: + - button "Previous" [disabled] + - button "Next" [disabled] + - generic [ref=e663]: + - generic [ref=e664]: + - generic [ref=e665]: Share this document + - generic [ref=e666]: Anyone with the link can view this document. + - generic [ref=e667]: + - generic [ref=e668]: + - generic [ref=e669]: Link + - textbox "Link" [ref=e670]: http://example.com/link/to/document + - button "Copy Link" [ref=e671] + - generic [ref=e672]: + - generic [ref=e673]: People with access + - generic [ref=e674]: + - generic [ref=e675]: + - generic [ref=e676]: + - img "Image" [ref=e678] + - generic [ref=e679]: + - paragraph [ref=e680]: Olivia Martin + - paragraph [ref=e681]: m@example.com + - combobox "Edit" [ref=e682]: + - generic: Can edit + - img + - generic [ref=e686]: + - generic [ref=e687]: + - img "Image" [ref=e689] + - generic [ref=e690]: + - paragraph [ref=e691]: Isabella Nguyen + - paragraph [ref=e692]: b@example.com + - combobox "Edit" [ref=e693]: + - generic: Can edit + - img + - generic [ref=e697]: + - generic [ref=e698]: + - img "Image" [ref=e700] + - generic [ref=e701]: + - paragraph [ref=e702]: Sofia Davis + - paragraph [ref=e703]: p@example.com + - combobox "Edit" [ref=e704]: + - generic: Can edit + - img + - generic [ref=e708]: + - generic [ref=e709]: + - img "Image" [ref=e711] + - generic [ref=e712]: + - paragraph [ref=e713]: Ethan Thompson + - paragraph [ref=e714]: e@example.com + - combobox "Edit" [ref=e715]: + - generic: Can edit + - img + - contentinfo [ref=e719]: + - generic [ref=e722]: + - text: Built by + - link "shadcn" [ref=e723] [cursor=pointer]: + - /url: https://twitter.com/shadcn + - text: at + - link "Vercel" [ref=e724] [cursor=pointer]: + - /url: https://vercel.com/new?utm_source=shadcn_site&utm_medium=web&utm_campaign=docs_cta_deploy_now_callout + - text: . The source code is available on + - link "GitHub" [ref=e725] [cursor=pointer]: + - /url: https://github.com/shadcn-ui/ui + - text: . + - region "Notifications alt+T" + - alert [ref=e728] + - generic [ref=e729]: Mon \ No newline at end of file diff --git a/playwriter/tsconfig.json b/playwriter/tsconfig.json new file mode 100644 index 0000000..c17b043 --- /dev/null +++ b/playwriter/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/playwriter/vitest.config.ts b/playwriter/vitest.config.ts new file mode 100644 index 0000000..d5721bc --- /dev/null +++ b/playwriter/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + testTimeout: 60000, // 60 seconds for Chrome startup + hookTimeout: 30000, + }, +})