Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 67 additions & 53 deletions skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,79 @@
import { chromium } from "playwright";
import type { Browser, BrowserContext, Page } from "playwright";
import { beforeAll, afterAll, beforeEach, afterEach, describe, test, expect } from "vitest";
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
import { getSnapshotScript, clearSnapshotScriptCache } from "../browser-script";

let browser: Browser;
let context: BrowserContext;
let page: Page;
let chromium: any = null;
let playwrightAvailable = true;

beforeAll(async () => {
browser = await chromium.launch();
});
try {
// @ts-ignore - optional dependency that may not be installed in all environments
const playwright = await import("playwright");
chromium = playwright.chromium;
} catch (error) {
playwrightAvailable = false;
}

afterAll(async () => {
await browser.close();
});
const runSnapshotSuite = (suiteName: string, cb: () => void) => {
if (playwrightAvailable) {
describe(suiteName, cb);
} else {
describe.skip(suiteName, cb);
}
};

runSnapshotSuite("ARIA Snapshot", () => {
if (!playwrightAvailable) {
return;
}

let browser: any;
let context: any;
let page: any;

beforeAll(async () => {
browser = await chromium.launch();
});

beforeEach(async () => {
context = await browser.newContext();
page = await context.newPage();
clearSnapshotScriptCache(); // Start fresh for each test
});
afterAll(async () => {
await browser.close();
});

afterEach(async () => {
await context.close();
});
beforeEach(async () => {
context = await browser.newContext();
page = await context.newPage();
clearSnapshotScriptCache();
});

async function setContent(html: string): Promise<void> {
await page.setContent(html, { waitUntil: "domcontentloaded" });
}
afterEach(async () => {
await context.close();
});

async function getSnapshot(): Promise<string> {
const script = getSnapshotScript();
return await page.evaluate((s: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const w = globalThis as any;
if (!w.__devBrowser_getAISnapshot) {
// eslint-disable-next-line no-eval
eval(s);
}
return w.__devBrowser_getAISnapshot();
}, script);
}
async function setContent(html: string): Promise<void> {
await page.setContent(html, { waitUntil: "domcontentloaded" });
}

async function selectRef(ref: string): Promise<unknown> {
return await page.evaluate((refId: string) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const w = globalThis as any;
const element = w.__devBrowser_selectSnapshotRef(refId);
return {
tagName: element.tagName,
textContent: element.textContent?.trim(),
};
}, ref);
}
async function getSnapshot(): Promise<string> {
const script = getSnapshotScript();
return await page.evaluate((s: string) => {
const w = globalThis as any;
if (!w.__devBrowser_getAISnapshot) {
// eslint-disable-next-line no-eval
eval(s);
}
return w.__devBrowser_getAISnapshot();
}, script);
}

async function selectRef(ref: string): Promise<unknown> {
return await page.evaluate((refId: string) => {
const w = globalThis as any;
const element = w.__devBrowser_selectSnapshotRef(refId);
return {
tagName: element.tagName,
textContent: element.textContent?.trim(),
};
}, ref);
}

describe("ARIA Snapshot", () => {
test("generates snapshot for simple page", async () => {
await setContent(`
<html>
Expand Down Expand Up @@ -85,7 +104,6 @@ describe("ARIA Snapshot", () => {

const snapshot = await getSnapshot();

// Should have refs
expect(snapshot).toMatch(/\[ref=e\d+\]/);
});

Expand All @@ -100,9 +118,7 @@ describe("ARIA Snapshot", () => {

await getSnapshot();

// Check that refs are stored
const hasRefs = await page.evaluate(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const w = globalThis as any;
return typeof w.__devBrowserRefs === "object" && Object.keys(w.__devBrowserRefs).length > 0;
});
Expand All @@ -121,13 +137,11 @@ describe("ARIA Snapshot", () => {

const snapshot = await getSnapshot();

// Extract a ref from the snapshot
const refMatch = snapshot.match(/\[ref=(e\d+)\]/);
expect(refMatch).toBeTruthy();
expect(refMatch![1]).toBeDefined();
const ref = refMatch![1] as string;

// Select the element by ref
const result = (await selectRef(ref)) as { tagName: string; textContent: string };
expect(result.tagName).toBe("BUTTON");
expect(result.textContent).toBe("My Button");
Expand All @@ -146,7 +160,6 @@ describe("ARIA Snapshot", () => {

expect(snapshot).toContain("link");
expect(snapshot).toContain("Example Link");
// URL should be included as a prop
expect(snapshot).toContain("/url:");
});

Expand Down Expand Up @@ -221,3 +234,4 @@ describe("ARIA Snapshot", () => {
expect(snapshot).toContain("[checked]");
});
});

86 changes: 86 additions & 0 deletions src/utils/__tests__/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect } from "bun:test";
import {
categorizeError,
createTypedError,
isRecoverable,
ErrorCategory,
ErrorCode,
} from "../errors";

describe("errors utility", () => {
describe("categorizeError", () => {
it("detects validation errors", () => {
const error = new Error("Validation failed due to invalid input");
expect(categorizeError(error)).toBe(ErrorCategory.VALIDATION);
});

it("detects permission errors", () => {
const error = new Error("Permission denied: EACCES");
expect(categorizeError(error)).toBe(ErrorCategory.PERMISSION);
});

it("detects filesystem errors", () => {
const error = new Error("ENOENT: file not found");
expect(categorizeError(error)).toBe(ErrorCategory.FILESYSTEM);
});

it("falls back to unknown", () => {
expect(categorizeError(new Error("unexpected"))).toBe(ErrorCategory.UNKNOWN);
});
});

describe("createTypedError", () => {
it("builds typed error with metadata", () => {
const error = createTypedError(
"network failure",
ErrorCategory.NETWORK,
ErrorCode.NETWORK_ERROR,
{
recoverable: true,
suggestions: ["check connection"],
}
);

expect(error.message).toBe("network failure");
expect(error.category).toBe(ErrorCategory.NETWORK);
expect(error.code).toBe(ErrorCode.NETWORK_ERROR);
expect(error.recoverable).toBe(true);
expect(error.suggestions).toEqual(["check connection"]);
});

it("defaults recoverable to false", () => {
const error = createTypedError(
"invalid input",
ErrorCategory.VALIDATION,
ErrorCode.INVALID_INPUT
);
expect(error.recoverable).toBe(false);
});
});

describe("isRecoverable", () => {
it("returns true for typed recoverable errors", () => {
const error = createTypedError(
"temporary network glitch",
ErrorCategory.NETWORK,
ErrorCode.NETWORK_ERROR,
{ recoverable: true }
);
expect(isRecoverable(error)).toBe(true);
});

it("returns true for inferred transient categories", () => {
const error = new Error("ETIMEDOUT waiting for response");
expect(isRecoverable(error)).toBe(true);
});

it("returns false for non-recoverable errors", () => {
const error = createTypedError(
"bad input",
ErrorCategory.VALIDATION,
ErrorCode.INVALID_INPUT
);
expect(isRecoverable(error)).toBe(false);
});
});
});
79 changes: 79 additions & 0 deletions src/utils/__tests__/logger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it, expect, beforeEach, beforeAll, afterAll } from "bun:test";
import { logger } from "../logger";

const noop = () => {};
let originalConsoleLog: typeof console.log;
let originalConsoleWarn: typeof console.warn;
let originalConsoleError: typeof console.error;
let originalConsoleDebug: typeof console.debug;

describe("logger", () => {
beforeAll(() => {
originalConsoleLog = console.log;
originalConsoleWarn = console.warn;
originalConsoleError = console.error;
originalConsoleDebug = console.debug;
console.log = noop;
console.warn = noop;
console.error = noop;
console.debug = noop;
});

afterAll(() => {
console.log = originalConsoleLog;
console.warn = originalConsoleWarn;
console.error = originalConsoleError;
console.debug = originalConsoleDebug;
});

beforeEach(() => {
logger.clearLogs();
logger.setLogLevel("info");
logger.setModule("test-module");
});

it("respects log level thresholds", () => {
logger.debug("debug message");
logger.info("info message");
logger.warn("warn message");
logger.error("error message");

const logs = logger.getLogs();
expect(logs).toHaveLength(3);
expect(logs[0]!.level).toBe("info");
expect(logs[1]!.level).toBe("warn");
expect(logs[2]!.level).toBe("error");
});

it("captures metadata and errors", () => {
const testError = new Error("boom");
logger.info("info", { key: "value" });
logger.error("fail", undefined, testError);

const logs = logger.getLogs();
expect(logs).toHaveLength(2);
expect(logs[0]!.metadata).toEqual({ key: "value" });
expect(logs[1]!.error).toBeDefined();
expect(logs[1]!.error?.message).toBe("boom");
});

it("honors max log retention", () => {
for (let i = 0; i < 1100; i++) {
logger.info(`entry-${i}`);
}

expect(logger.getLogs()).toHaveLength(1000);
});

it("exports and clears logs", () => {
logger.warn("warning");
const exportPayload = logger.exportLogs();
const parsed = JSON.parse(exportPayload);

expect(Array.isArray(parsed)).toBe(true);
expect(parsed).toHaveLength(1);

logger.clearLogs();
expect(logger.getLogs()).toHaveLength(0);
});
});
Loading
Loading