Generate MCP resources at build time and host on playwriter.dev

- Add build-resources.ts script to generate markdown API docs
- Resources now use https://playwriter.dev/resources/*.md URLs
- Output to both playwriter/dist/ and website/public/resources/
- Simplify mcp.ts resource handlers to read pre-built files
This commit is contained in:
Tommy D. Rossi
2025-12-30 14:30:30 +01:00
parent aca26896b4
commit 237f24d3f8
8 changed files with 1082 additions and 77 deletions
+457
View File
@@ -0,0 +1,457 @@
# Debugger API Reference
## Types
```ts
import type { CDPSession } from './cdp-session.js';
export interface BreakpointInfo {
id: string;
file: string;
line: number;
}
export interface LocationInfo {
url: string;
lineNumber: number;
columnNumber: number;
callstack: Array<{
functionName: string;
url: string;
lineNumber: number;
columnNumber: number;
}>;
sourceContext: string;
}
export interface EvaluateResult {
value: unknown;
}
export interface ScriptInfo {
scriptId: string;
url: string;
}
/**
* A class for debugging JavaScript code via Chrome DevTools Protocol.
* Works with both Node.js (--inspect) and browser debugging.
*
* @example
* ```ts
* const cdp = await getCDPSessionForPage({ page, wsUrl })
* const dbg = new Debugger({ cdp })
*
* await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 })
* // trigger the code path, then:
* const location = await dbg.getLocation()
* const vars = await dbg.inspectLocalVariables()
* await dbg.resume()
* ```
*/
export declare class Debugger {
private cdp;
private debuggerEnabled;
private paused;
private currentCallFrames;
private breakpoints;
private scripts;
private xhrBreakpoints;
private blackboxPatterns;
/**
* Creates a new Debugger instance.
*
* @param options - Configuration options
* @param options.cdp - A CDPSession instance for sending CDP commands
*
* @example
* ```ts
* const cdp = await getCDPSessionForPage({ page, wsUrl })
* const dbg = new Debugger({ cdp })
* ```
*/
constructor({ cdp }: {
cdp: CDPSession;
});
private setupEventListeners;
/**
* Enables the debugger and runtime domains. Called automatically by other methods.
* Also resumes execution if the target was started with --inspect-brk.
*
* @example
* ```ts
* await dbg.enable()
* ```
*/
enable(): Promise<void>;
/**
* Sets a breakpoint at a specified URL and line number.
* Use the URL from listScripts() to find available scripts.
*
* @param options - Breakpoint options
* @param options.file - Script URL (e.g. https://example.com/app.js)
* @param options.line - Line number (1-based)
* @param options.condition - Optional JS expression; only pause when it evaluates to true
* @returns The breakpoint ID for later removal
*
* @example
* ```ts
* const id = await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 })
* // later:
* await dbg.deleteBreakpoint({ breakpointId: id })
*
* // Conditional breakpoint - only pause when userId is 123
* await dbg.setBreakpoint({
* file: 'https://example.com/app.js',
* line: 42,
* condition: 'userId === 123'
* })
* ```
*/
setBreakpoint({ file, line, condition }: {
file: string;
line: number;
condition?: string;
}): Promise<string>;
/**
* Removes a breakpoint by its ID.
*
* @param options - Options
* @param options.breakpointId - The breakpoint ID returned by setBreakpoint
*
* @example
* ```ts
* await dbg.deleteBreakpoint({ breakpointId: 'bp-123' })
* ```
*/
deleteBreakpoint({ breakpointId }: {
breakpointId: string;
}): Promise<void>;
/**
* Returns a list of all active breakpoints set by this debugger instance.
*
* @returns Array of breakpoint info objects
*
* @example
* ```ts
* const breakpoints = dbg.listBreakpoints()
* // [{ id: 'bp-123', file: 'https://example.com/index.js', line: 42 }]
* ```
*/
listBreakpoints(): BreakpointInfo[];
/**
* Inspects local variables in the current call frame.
* Must be paused at a breakpoint. String values over 1000 chars are truncated.
* Use evaluate() for full control over reading specific values.
*
* @returns Record of variable names to values
* @throws Error if not paused or no active call frames
*
* @example
* ```ts
* const vars = await dbg.inspectLocalVariables()
* // { myVar: 'hello', count: 42 }
* ```
*/
inspectLocalVariables(): Promise<Record<string, unknown>>;
/**
* Returns global lexical scope variable names.
*
* @returns Array of global variable names
*
* @example
* ```ts
* const globals = await dbg.inspectGlobalVariables()
* // ['myGlobal', 'CONFIG']
* ```
*/
inspectGlobalVariables(): Promise<string[]>;
/**
* Evaluates a JavaScript expression and returns the result.
* When paused at a breakpoint, evaluates in the current stack frame scope,
* allowing access to local variables. Otherwise evaluates in global scope.
* Values are not truncated, use this for full control over reading specific variables.
*
* @param options - Options
* @param options.expression - JavaScript expression to evaluate
* @returns The result value
*
* @example
* ```ts
* // When paused, can access local variables:
* const result = await dbg.evaluate({ expression: 'localVar + 1' })
*
* // Read a large string that would be truncated in inspectLocalVariables:
* const full = await dbg.evaluate({ expression: 'largeStringVar' })
* ```
*/
evaluate({ expression }: {
expression: string;
}): Promise<EvaluateResult>;
/**
* Gets the current execution location when paused at a breakpoint.
* Includes the call stack and surrounding source code for context.
*
* @returns Location info with URL, line number, call stack, and source context
* @throws Error if debugger is not paused
*
* @example
* ```ts
* const location = await dbg.getLocation()
* console.log(location.url) // 'https://example.com/src/index.js'
* console.log(location.lineNumber) // 42
* console.log(location.callstack) // [{ functionName: 'handleRequest', ... }]
* console.log(location.sourceContext)
* // ' 40: function handleRequest(req) {
* // 41: const data = req.body
* // > 42: processData(data)
* // 43: }'
* ```
*/
getLocation(): Promise<LocationInfo>;
/**
* Steps over to the next line of code, not entering function calls.
*
* @throws Error if debugger is not paused
*
* @example
* ```ts
* await dbg.stepOver()
* const newLocation = await dbg.getLocation()
* ```
*/
stepOver(): Promise<void>;
/**
* Steps into a function call on the current line.
*
* @throws Error if debugger is not paused
*
* @example
* ```ts
* await dbg.stepInto()
* const location = await dbg.getLocation()
* // now inside the called function
* ```
*/
stepInto(): Promise<void>;
/**
* Steps out of the current function, returning to the caller.
*
* @throws Error if debugger is not paused
*
* @example
* ```ts
* await dbg.stepOut()
* const location = await dbg.getLocation()
* // back in the calling function
* ```
*/
stepOut(): Promise<void>;
/**
* Resumes code execution until the next breakpoint or completion.
*
* @throws Error if debugger is not paused
*
* @example
* ```ts
* await dbg.resume()
* // execution continues
* ```
*/
resume(): Promise<void>;
/**
* Returns whether the debugger is currently paused at a breakpoint.
*
* @returns true if paused, false otherwise
*
* @example
* ```ts
* if (dbg.isPaused()) {
* const vars = await dbg.inspectLocalVariables()
* }
* ```
*/
isPaused(): boolean;
/**
* Configures the debugger to pause on exceptions.
*
* @param options - Options
* @param options.state - When to pause: 'none' (never), 'uncaught' (only uncaught), or 'all' (all exceptions)
*
* @example
* ```ts
* // Pause only on uncaught exceptions
* await dbg.setPauseOnExceptions({ state: 'uncaught' })
*
* // Pause on all exceptions (caught and uncaught)
* await dbg.setPauseOnExceptions({ state: 'all' })
*
* // Disable pausing on exceptions
* await dbg.setPauseOnExceptions({ state: 'none' })
* ```
*/
setPauseOnExceptions({ state }: {
state: 'none' | 'uncaught' | 'all';
}): Promise<void>;
/**
* Lists available scripts where breakpoints can be set.
* Automatically enables the debugger if not already enabled.
*
* @param options - Options
* @param options.search - Optional string to filter scripts by URL (case-insensitive)
* @returns Array of up to 20 matching scripts with scriptId and url
*
* @example
* ```ts
* // List all scripts
* const scripts = await dbg.listScripts()
* // [{ scriptId: '1', url: 'https://example.com/app.js' }, ...]
*
* // Search for specific files
* const handlers = await dbg.listScripts({ search: 'handler' })
* // [{ scriptId: '5', url: 'https://example.com/handlers.js' }]
* ```
*/
listScripts({ search }?: {
search?: string;
}): Promise<ScriptInfo[]>;
setXHRBreakpoint({ url }: {
url: string;
}): Promise<void>;
removeXHRBreakpoint({ url }: {
url: string;
}): Promise<void>;
listXHRBreakpoints(): string[];
/**
* Sets regex patterns for scripts to blackbox (skip when stepping).
* Blackboxed scripts are hidden from the call stack and stepped over automatically.
* Useful for ignoring framework/library code during debugging.
*
* @param options - Options
* @param options.patterns - Array of regex patterns to match script URLs
*
* @example
* ```ts
* // Skip all node_modules
* await dbg.setBlackboxPatterns({ patterns: ['node_modules'] })
*
* // Skip React and other frameworks
* await dbg.setBlackboxPatterns({
* patterns: [
* 'node_modules/react',
* 'node_modules/react-dom',
* 'node_modules/next',
* 'webpack://',
* ]
* })
*
* // Skip all third-party scripts
* await dbg.setBlackboxPatterns({ patterns: ['^https://cdn\\.'] })
*
* // Clear all blackbox patterns
* await dbg.setBlackboxPatterns({ patterns: [] })
* ```
*/
setBlackboxPatterns({ patterns }: {
patterns: string[];
}): Promise<void>;
/**
* Adds a single regex pattern to the blackbox list.
*
* @param options - Options
* @param options.pattern - Regex pattern to match script URLs
*
* @example
* ```ts
* await dbg.addBlackboxPattern({ pattern: 'node_modules/lodash' })
* await dbg.addBlackboxPattern({ pattern: 'node_modules/axios' })
* ```
*/
addBlackboxPattern({ pattern }: {
pattern: string;
}): Promise<void>;
/**
* Removes a pattern from the blackbox list.
*
* @param options - Options
* @param options.pattern - The exact pattern string to remove
*/
removeBlackboxPattern({ pattern }: {
pattern: string;
}): Promise<void>;
/**
* Returns the current list of blackbox patterns.
*/
listBlackboxPatterns(): string[];
private truncateValue;
private formatPropertyValue;
private processRemoteObject;
}
```
## Examples
```ts
import { page, getCDPSession, createDebugger, console } from './debugger-examples-types.js'
// Example: List available scripts and set a breakpoint
async function listScriptsAndSetBreakpoint() {
const cdp = await getCDPSession({ page })
const dbg = createDebugger({ cdp })
await dbg.enable()
const scripts = await dbg.listScripts({ search: 'app' })
console.log(scripts)
if (scripts.length > 0) {
const bpId = await dbg.setBreakpoint({ file: scripts[0].url, line: 100 })
console.log('Breakpoint set:', bpId)
}
}
// Example: Inspect state when paused at a breakpoint
async function inspectWhenPaused() {
const cdp = await getCDPSession({ page })
const dbg = createDebugger({ cdp })
await dbg.enable()
if (dbg.isPaused()) {
const loc = await dbg.getLocation()
console.log('Paused at:', loc.url, 'line', loc.lineNumber)
console.log('Source:', loc.sourceContext)
const vars = await dbg.inspectLocalVariables()
console.log('Variables:', vars)
const result = await dbg.evaluate({ expression: 'myVar.length' })
console.log('myVar.length =', result.value)
await dbg.stepOver()
}
}
// Example: Step through code
async function stepThroughCode() {
const cdp = await getCDPSession({ page })
const dbg = createDebugger({ cdp })
await dbg.enable()
await dbg.setBreakpoint({ file: 'https://example.com/app.js', line: 42 })
if (dbg.isPaused()) {
await dbg.stepOver()
await dbg.stepInto()
await dbg.stepOut()
await dbg.resume()
}
}
// Example: Cleanup all breakpoints
async function cleanupBreakpoints() {
const cdp = await getCDPSession({ page })
const dbg = createDebugger({ cdp })
const breakpoints = dbg.listBreakpoints()
for (const bp of breakpoints) {
await dbg.deleteBreakpoint({ breakpointId: bp.id })
}
}
export { listScriptsAndSetBreakpoint, inspectWhenPaused, stepThroughCode, cleanupBreakpoints }
```
+364
View File
@@ -0,0 +1,364 @@
# Editor API Reference
The Editor class provides a Claude Code-like interface for viewing and editing web page scripts at runtime.
## Types
```ts
import type { CDPSession } from './cdp-session.js';
export interface ReadResult {
content: string;
totalLines: number;
startLine: number;
endLine: number;
}
export interface SearchMatch {
url: string;
lineNumber: number;
lineContent: string;
}
export interface EditResult {
success: boolean;
stackChanged?: boolean;
}
/**
* A class for viewing and editing web page scripts via Chrome DevTools Protocol.
* Provides a Claude Code-like interface: list, read, edit, grep.
*
* Edits are in-memory only and persist until page reload. They modify the running
* V8 instance but are not saved to disk or server.
*
* @example
* ```ts
* const cdp = await getCDPSession({ page })
* const editor = new Editor({ cdp })
* await editor.enable()
*
* // List available scripts
* const scripts = editor.list({ search: 'app' })
*
* // Read a script
* const { content } = await editor.read({ url: 'https://example.com/app.js' })
*
* // Edit a script
* await editor.edit({
* url: 'https://example.com/app.js',
* oldString: 'console.log("old")',
* newString: 'console.log("new")'
* })
* ```
*/
export declare class Editor {
private cdp;
private enabled;
private scripts;
private stylesheets;
private sourceCache;
constructor({ cdp }: {
cdp: CDPSession;
});
private setupEventListeners;
/**
* Enables the editor. Must be called before other methods.
* Scripts are collected from Debugger.scriptParsed events.
* Reload the page after enabling to capture all scripts.
*/
enable(): Promise<void>;
private getIdByUrl;
/**
* Lists available script and stylesheet URLs. Use pattern to filter by regex.
* Automatically enables the editor if not already enabled.
*
* @param options - Options
* @param options.pattern - Optional regex to filter URLs
* @returns Array of URLs
*
* @example
* ```ts
* // List all scripts and stylesheets
* const urls = await editor.list()
*
* // List only JS files
* const jsFiles = await editor.list({ pattern: /\.js/ })
*
* // List only CSS files
* const cssFiles = await editor.list({ pattern: /\.css/ })
*
* // Search for specific scripts
* const appScripts = await editor.list({ pattern: /app/ })
* ```
*/
list({ pattern }?: {
pattern?: RegExp;
}): Promise<string[]>;
/**
* Reads a script or stylesheet's source code by URL.
* Returns line-numbered content like Claude Code's Read tool.
* For inline scripts, use the `inline://` URL from list() or grep().
*
* @param options - Options
* @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs)
* @param options.offset - Line number to start from (0-based, default 0)
* @param options.limit - Number of lines to return (default 2000)
* @returns Content with line numbers, total lines, and range info
*
* @example
* ```ts
* // Read by URL
* const { content, totalLines } = await editor.read({
* url: 'https://example.com/app.js'
* })
*
* // Read a CSS file
* const { content } = await editor.read({ url: 'https://example.com/styles.css' })
*
* // Read lines 100-200
* const { content } = await editor.read({
* url: 'https://example.com/app.js',
* offset: 100,
* limit: 100
* })
* ```
*/
read({ url, offset, limit }: {
url: string;
offset?: number;
limit?: number;
}): Promise<ReadResult>;
private getSource;
/**
* Edits a script or stylesheet by replacing oldString with newString.
* Like Claude Code's Edit tool - performs exact string replacement.
*
* @param options - Options
* @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs)
* @param options.oldString - Exact string to find and replace
* @param options.newString - Replacement string
* @param options.dryRun - If true, validate without applying (default false)
* @returns Result with success status
*
* @example
* ```ts
* // Replace a string in JS
* await editor.edit({
* url: 'https://example.com/app.js',
* oldString: 'const DEBUG = false',
* newString: 'const DEBUG = true'
* })
*
* // Edit CSS
* await editor.edit({
* url: 'https://example.com/styles.css',
* oldString: 'color: red',
* newString: 'color: blue'
* })
* ```
*/
edit({ url, oldString, newString, dryRun, }: {
url: string;
oldString: string;
newString: string;
dryRun?: boolean;
}): Promise<EditResult>;
private setSource;
/**
* Searches for a regex across all scripts and stylesheets.
* Like Claude Code's Grep tool - returns matching lines with context.
*
* @param options - Options
* @param options.regex - Regular expression to search for in file contents
* @param options.pattern - Optional regex to filter which URLs to search
* @returns Array of matches with url, line number, and line content
*
* @example
* ```ts
* // Search all scripts and stylesheets for "color"
* const matches = await editor.grep({ regex: /color/ })
*
* // Search only CSS files
* const matches = await editor.grep({
* regex: /background-color/,
* pattern: /\.css/
* })
*
* // Regex search for console methods in JS
* const matches = await editor.grep({
* regex: /console\.(log|error|warn)/,
* pattern: /\.js/
* })
* ```
*/
grep({ regex, pattern }: {
regex: RegExp;
pattern?: RegExp;
}): Promise<SearchMatch[]>;
/**
* Writes entire content to a script or stylesheet, replacing all existing code.
* Use with caution - prefer edit() for targeted changes.
*
* @param options - Options
* @param options.url - Script or stylesheet URL (inline scripts have `inline://{id}` URLs)
* @param options.content - New content
* @param options.dryRun - If true, validate without applying (default false, only works for JS)
*/
write({ url, content, dryRun }: {
url: string;
content: string;
dryRun?: boolean;
}): Promise<EditResult>;
}
```
## Examples
```ts
import { page, getCDPSession, createEditor, console } from './debugger-examples-types.js'
// Example: List available scripts
async function listScripts() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const scripts = editor.list({ pattern: /app/ })
console.log(scripts)
}
// Example: Read a script with line numbers
async function readScript() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const { content, totalLines } = await editor.read({
url: 'https://example.com/app.js',
})
console.log('Total lines:', totalLines)
console.log(content)
const { content: partial } = await editor.read({
url: 'https://example.com/app.js',
offset: 100,
limit: 50,
})
console.log(partial)
}
// Example: Edit a script (exact string replacement)
async function editScript() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
await editor.edit({
url: 'https://example.com/app.js',
oldString: 'const DEBUG = false',
newString: 'const DEBUG = true',
})
const dryRunResult = await editor.edit({
url: 'https://example.com/app.js',
oldString: 'old code',
newString: 'new code',
dryRun: true,
})
console.log('Dry run result:', dryRunResult)
}
// Example: Search across all scripts
async function searchScripts() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const matches = await editor.grep({ regex: /console\.log/ })
console.log(matches)
const todoMatches = await editor.grep({
regex: /TODO|FIXME/i,
pattern: /app/,
})
console.log(todoMatches)
}
// Example: Write entire script content
async function writeScript() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const { content } = await editor.read({ url: 'https://example.com/app.js' })
const newContent = content.replace(/console\.log/g, 'console.debug')
await editor.write({
url: 'https://example.com/app.js',
content: newContent,
})
}
// Example: Edit an inline script (scripts without URL get inline://{id} URLs)
async function editInlineScript() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const matches = await editor.grep({ regex: /myFunction/ })
if (matches.length > 0) {
const { url } = matches[0]
console.log('Found in:', url)
await editor.edit({
url,
oldString: 'return false',
newString: 'return true',
})
}
}
// Example: List and read CSS stylesheets
async function readStylesheet() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const stylesheets = await editor.list({ pattern: /\.css/ })
console.log('Stylesheets:', stylesheets)
if (stylesheets.length > 0) {
const { content, totalLines } = await editor.read({
url: stylesheets[0],
})
console.log('Total lines:', totalLines)
console.log(content)
}
}
// Example: Edit a CSS stylesheet
async function editStylesheet() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
await editor.edit({
url: 'https://example.com/styles.css',
oldString: 'color: red',
newString: 'color: blue',
})
}
// Example: Search CSS for specific properties
async function searchStyles() {
const cdp = await getCDPSession({ page })
const editor = createEditor({ cdp })
await editor.enable()
const matches = await editor.grep({
regex: /background-color/,
pattern: /\.css/,
})
console.log(matches)
}
export { listScripts, readScript, editScript, searchScripts, writeScript, editInlineScript, readStylesheet, editStylesheet, searchStyles }
```
+117
View File
@@ -0,0 +1,117 @@
# Styles API Reference
The getStylesForLocator function inspects CSS styles applied to an element, similar to browser DevTools "Styles" panel.
## Types
```ts
import type { CDPSession } from './cdp-session.js';
import type { Locator } from 'playwright-core';
export interface StyleSource {
url: string;
line: number;
column: number;
}
export type StyleDeclarations = Record<string, string>;
export interface StyleRule {
selector: string;
source: StyleSource | null;
origin: 'regular' | 'user-agent' | 'injected' | 'inspector';
declarations: StyleDeclarations;
inheritedFrom: string | null;
}
export interface StylesResult {
element: string;
inlineStyle: StyleDeclarations | null;
rules: StyleRule[];
}
export declare function getStylesForLocator({ locator, cdp, includeUserAgentStyles, }: {
locator: Locator;
cdp: CDPSession;
includeUserAgentStyles?: boolean;
}): Promise<StylesResult>;
export declare function formatStylesAsText(styles: StylesResult): string;
```
## Examples
```ts
import { page, getStylesForLocator, formatStylesAsText, console } from './debugger-examples-types.js'
// Example: Get styles for an element and display them
async function getElementStyles() {
const loc = page.locator('.my-button')
const styles = await getStylesForLocator({ locator: loc })
console.log(formatStylesAsText(styles))
}
// Example: Inspect computed styles for a specific element
async function inspectButtonStyles() {
const button = page.getByRole('button', { name: 'Submit' })
const styles = await getStylesForLocator({ locator: button })
console.log('Element:', styles.element)
if (styles.inlineStyle) {
console.log('Inline styles:', styles.inlineStyle)
}
for (const rule of styles.rules) {
console.log(`${rule.selector}: ${JSON.stringify(rule.declarations)}`)
if (rule.source) {
console.log(` Source: ${rule.source.url}:${rule.source.line}`)
}
}
}
// Example: Include browser default (user-agent) styles
async function getStylesWithUserAgent() {
const loc = page.locator('input[type="text"]')
const styles = await getStylesForLocator({
locator: loc,
includeUserAgentStyles: true,
})
console.log(formatStylesAsText(styles))
}
// Example: Find where a CSS property is defined
async function findPropertySource() {
const loc = page.locator('.card')
const styles = await getStylesForLocator({ locator: loc })
const backgroundRule = styles.rules.find((r) => 'background-color' in r.declarations)
if (backgroundRule) {
console.log('background-color defined by:', backgroundRule.selector)
if (backgroundRule.source) {
console.log(` at ${backgroundRule.source.url}:${backgroundRule.source.line}`)
}
}
}
// Example: Check inherited styles
async function checkInheritedStyles() {
const loc = page.locator('.nested-text')
const styles = await getStylesForLocator({ locator: loc })
const inheritedRules = styles.rules.filter((r) => r.inheritedFrom)
for (const rule of inheritedRules) {
console.log(`Inherited from ${rule.inheritedFrom}: ${rule.selector}`)
console.log(' Properties:', rule.declarations)
}
}
// Example: Compare styles between two elements
async function compareStyles() {
const primary = await getStylesForLocator({ locator: page.locator('.btn-primary') })
const secondary = await getStylesForLocator({ locator: page.locator('.btn-secondary') })
console.log('Primary button:')
console.log(formatStylesAsText(primary))
console.log('Secondary button:')
console.log(formatStylesAsText(secondary))
}
export { getElementStyles, inspectButtonStyles, getStylesWithUserAgent, findPropertySource, checkInheritedStyles, compareStyles }
```