From 5ae9d53aadd82c0609d4e4c6e7cc1a014ac21df4 Mon Sep 17 00:00:00 2001 From: tkattkat Date: Thu, 16 Oct 2025 13:36:40 -0700 Subject: [PATCH 01/12] test: add comprehensive CDP method tests for v3 module --- .../frame-get-location-and-click.spec.ts | 68 ++++ .../v3/tests/locator-content-methods.spec.ts | 255 +++++++++++++++ .../v3/tests/locator-input-methods.spec.ts | 190 +++++++++++ .../v3/tests/locator-select-option.spec.ts | 301 ++++++++++++++++++ 4 files changed, 814 insertions(+) create mode 100644 packages/core/lib/v3/tests/frame-get-location-and-click.spec.ts create mode 100644 packages/core/lib/v3/tests/locator-content-methods.spec.ts create mode 100644 packages/core/lib/v3/tests/locator-input-methods.spec.ts create mode 100644 packages/core/lib/v3/tests/locator-select-option.spec.ts diff --git a/packages/core/lib/v3/tests/frame-get-location-and-click.spec.ts b/packages/core/lib/v3/tests/frame-get-location-and-click.spec.ts new file mode 100644 index 000000000..32d890a76 --- /dev/null +++ b/packages/core/lib/v3/tests/frame-get-location-and-click.spec.ts @@ -0,0 +1,68 @@ +import { expect, test } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Coordinate-based clicking", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch(() => {}); + }); + + test("clicking by coordinates toggles a button state", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + +
+ + `, + ), + ); + + // Initial state should be idle + let state = await page.mainFrame().evaluate(() => { + const out = document.getElementById("out"); + return out?.textContent || ""; + }); + expect(state).toBe("idle"); + + // Compute button location via Frame.getLocationForSelector + const { x, y, width, height } = await page + .mainFrame() + .getLocationForSelector("#btn"); + + // Click near the center of the button using Page.click coordinates + const cx = Math.round(x + width / 2); + const cy = Math.round(y + height / 2); + await page.click(cx, cy); + + state = await page.mainFrame().evaluate(() => { + const out = document.getElementById("out"); + return out?.textContent || ""; + }); + expect(state).toBe("clicked"); + + // Click again to toggle back to idle + await page.click(cx, cy); + state = await page.mainFrame().evaluate(() => { + const out = document.getElementById("out"); + return out?.textContent || ""; + }); + expect(state).toBe("idle"); + }); +}); diff --git a/packages/core/lib/v3/tests/locator-content-methods.spec.ts b/packages/core/lib/v3/tests/locator-content-methods.spec.ts new file mode 100644 index 000000000..40f0a0dfe --- /dev/null +++ b/packages/core/lib/v3/tests/locator-content-methods.spec.ts @@ -0,0 +1,255 @@ +import { expect, test } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Locator content methods (textContent, innerHtml, innerText, inputValue)", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch((e) => { + void e; + }); + }); + + test("Locator.textContent() returns raw text including hidden content", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ Hello + Hidden + World +
+ `, + ), + ); + + const content = await page.mainFrame().locator("#content").textContent(); + // textContent includes all text nodes, even hidden ones + expect(content).toContain("Hello"); + expect(content).toContain("Hidden"); + expect(content).toContain("World"); + }); + + test("Locator.innerText() returns visible text only", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ Visible + Hidden + Text +
+ `, + ), + ); + + const text = await page.mainFrame().locator("#content").innerText(); + // innerText is layout-aware and excludes hidden elements + expect(text).toContain("Visible"); + expect(text).toContain("Text"); + expect(text).not.toContain("Hidden"); + }); + + test("Locator.innerHtml() returns HTML markup", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+

Hello

+ World +
+ `, + ), + ); + + const html = await page.mainFrame().locator("#container").innerHtml(); + expect(html).toContain('

Hello

'); + expect(html).toContain("World"); + }); + + test("Locator.inputValue() reads value from input elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + + + `, + ), + ); + + const textValue = await page + .mainFrame() + .locator("#text-input") + .inputValue(); + expect(textValue).toBe("hello world"); + + const taValue = await page.mainFrame().locator("#textarea").inputValue(); + expect(taValue).toBe("multi\nline\ntext"); + + const numValue = await page + .mainFrame() + .locator("#number-input") + .inputValue(); + expect(numValue).toBe("42"); + }); + + test("Locator.textContent() on empty elements returns empty string", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ + `, + ), + ); + + const empty = await page.mainFrame().locator("#empty").textContent(); + expect(empty).toBe(""); + + const whitespace = await page + .mainFrame() + .locator("#whitespace") + .textContent(); + expect(whitespace.trim()).toBe(""); + }); + + test("Locator.innerText() with nested elements and formatting", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+

Line 1

+

Line 2

+ +
+ `, + ), + ); + + const text = await page.mainFrame().locator("#formatted").innerText(); + expect(text).toContain("Line 1"); + expect(text).toContain("Line 2"); + expect(text).toContain("Item 1"); + expect(text).toContain("Item 2"); + }); + + test("Locator.inputValue() on contenteditable elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Editable content
+ `, + ), + ); + + const value = await page.mainFrame().locator("#editable").inputValue(); + expect(value).toBe("Editable content"); + }); + + test("Locator.innerHtml() preserves attributes and structure", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ Link + test +
+ `, + ), + ); + + const html = await page.mainFrame().locator("#complex").innerHtml(); + expect(html).toContain('href="/link"'); + expect(html).toContain('class="link-class"'); + expect(html).toContain('src="image.png"'); + expect(html).toContain('alt="test"'); + }); + + test("Locator.textContent() vs innerText() with script/style tags", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ Visible text + + + More visible +
+ `, + ), + ); + + const textContent = await page.mainFrame().locator("#mixed").textContent(); + // textContent includes script content + expect(textContent).toContain("Visible text"); + expect(textContent).toContain("More visible"); + + const innerText = await page.mainFrame().locator("#mixed").innerText(); + // innerText excludes script/style + expect(innerText).toContain("Visible text"); + expect(innerText).toContain("More visible"); + expect(innerText).not.toContain("console.log"); + }); + + test("Locator.inputValue() returns empty string for non-input elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Not an input
+ + `, + ), + ); + + const divValue = await page.mainFrame().locator("#div").inputValue(); + expect(divValue).toBe(""); + + const emptyInput = await page + .mainFrame() + .locator("#empty-input") + .inputValue(); + expect(emptyInput).toBe(""); + }); +}); diff --git a/packages/core/lib/v3/tests/locator-input-methods.spec.ts b/packages/core/lib/v3/tests/locator-input-methods.spec.ts new file mode 100644 index 000000000..79b4ccbec --- /dev/null +++ b/packages/core/lib/v3/tests/locator-input-methods.spec.ts @@ -0,0 +1,190 @@ +import { expect, test } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Locator input methods (fill, type, hover, isVisible, isChecked)", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch((e) => { + void e; + }); + }); + + test("Locator.fill() sets input value directly", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + +
+ `, + ), + ); + + const input = page.mainFrame().locator("#name"); + await input.fill("Hello World"); + + const value = await input.inputValue(); + expect(value).toBe("Hello World"); + }); + + test("Locator.type() types text character by character", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const input = page.mainFrame().locator("#search"); + await input.type("test123", { delay: 10 }); + + const value = await input.inputValue(); + expect(value).toBe("test123"); + }); + + test("Locator.hover() moves mouse to element center", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const btn = page.mainFrame().locator("#btn"); + await btn.hover(); + + const hovered = await page.mainFrame().evaluate(() => { + const b = document.getElementById("btn") as HTMLButtonElement | null; + return b?.dataset.hovered === "true"; + }); + + expect(hovered).toBe(true); + }); + + test("Locator.isVisible() returns true for visible elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
I am visible
+ + +
I am transparent
+
Zero size
+ `, + ), + ); + + const visible = await page.mainFrame().locator("#visible").isVisible(); + expect(visible).toBe(true); + + const hidden = await page.mainFrame().locator("#hidden").isVisible(); + expect(hidden).toBe(false); + + const invisible = await page.mainFrame().locator("#invisible").isVisible(); + expect(invisible).toBe(false); + + const transparent = await page + .mainFrame() + .locator("#transparent") + .isVisible(); + expect(transparent).toBe(false); + + const zeroSize = await page.mainFrame().locator("#zero-size").isVisible(); + expect(zeroSize).toBe(false); + }); + + test("Locator.isChecked() detects checkbox state", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + + + + `, + ), + ); + + const checked = await page.mainFrame().locator("#checked").isChecked(); + expect(checked).toBe(true); + + const unchecked = await page.mainFrame().locator("#unchecked").isChecked(); + expect(unchecked).toBe(false); + + const radioSelected = await page + .mainFrame() + .locator("#radio-selected") + .isChecked(); + expect(radioSelected).toBe(true); + + const radioUnselected = await page + .mainFrame() + .locator("#radio-unselected") + .isChecked(); + expect(radioUnselected).toBe(false); + }); + + test("Locator.fill() on textarea", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const ta = page.mainFrame().locator("#ta"); + await ta.fill("Multi\nline\ntext"); + + const value = await ta.inputValue(); + expect(value).toBe("Multi\nline\ntext"); + }); + + test("Locator.fill() clears and sets new value", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const inp = page.mainFrame().locator("#inp"); + + let value = await inp.inputValue(); + expect(value).toBe("initial"); + + await inp.fill("replaced"); + value = await inp.inputValue(); + expect(value).toBe("replaced"); + }); +}); diff --git a/packages/core/lib/v3/tests/locator-select-option.spec.ts b/packages/core/lib/v3/tests/locator-select-option.spec.ts new file mode 100644 index 000000000..850efd221 --- /dev/null +++ b/packages/core/lib/v3/tests/locator-select-option.spec.ts @@ -0,0 +1,301 @@ +import { expect, test } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Locator.selectOption() method", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch((e) => { + void e; // ignore cleanup errors + }); + }); + + test("selectOption() selects single option by value", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#fruit"); + const selected = await select.selectOption("banana"); + + expect(selected).toEqual(["banana"]); + + const value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("fruit") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe("banana"); + }); + + test("selectOption() selects option by label/text", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#country"); + const selected = await select.selectOption("United Kingdom"); + + expect(selected).toEqual(["uk"]); + }); + + test("selectOption() selects multiple options in multiple select", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#colors"); + const selected = await select.selectOption(["red", "blue"]); + + expect(selected.sort()).toEqual(["blue", "red"]); + + const values = await page.mainFrame().evaluate(() => { + const s = document.getElementById("colors") as HTMLSelectElement | null; + return Array.from(s?.selectedOptions ?? []).map((o) => o.value); + }); + expect(values.sort()).toEqual(["blue", "red"]); + }); + + test("selectOption() deselects previous option on single select", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#size"); + + let value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("size") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe("m"); + + await select.selectOption("l"); + + value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("size") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe("l"); + }); + + test("selectOption() triggers change event", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + +
+ + `, + ), + ); + + const select = page.mainFrame().locator("#opt"); + await select.selectOption("b"); + + const output = await page.mainFrame().evaluate(() => { + const out = document.getElementById("out"); + return out?.textContent; + }); + expect(output).toBe("changed-b"); + }); + + test("selectOption() with optgroup structure", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#grouped"); + await select.selectOption("celery"); + + const value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("grouped") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe("celery"); + }); + + test("selectOption() returns array of selected values", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#multi"); + const selected = await select.selectOption(["1", "3"]); + + expect(selected).toContain("1"); + expect(selected).toContain("3"); + expect(selected.length).toBe(2); + }); + + test("selectOption() with empty string value", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#opt"); + const selected = await select.selectOption(""); + + expect(selected).toEqual([""]); + + const value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("opt") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe(""); + }); + + test("selectOption() with numeric values", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#nums"); + await select.selectOption("10"); + + const value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("nums") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe("10"); + }); + + test("selectOption() with disabled option", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const select = page.mainFrame().locator("#mixed"); + // Should still select disabled option if explicitly requested + await select.selectOption("b"); + + const value = await page.mainFrame().evaluate(() => { + const s = document.getElementById("mixed") as HTMLSelectElement | null; + return s?.value; + }); + expect(value).toBe("b"); + }); +}); From bbdfbb38628f4c0adda306b328f7773bc83ba595 Mon Sep 17 00:00:00 2001 From: tkattkat Date: Thu, 16 Oct 2025 13:44:35 -0700 Subject: [PATCH 02/12] chore: add c8 code coverage setup for v3 module tests --- .gitignore | 5 ++++- packages/core/.c8rc.json | 30 ++++++++++++++++++++++++++++++ packages/core/package.json | 19 +++++++++++-------- 3 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 packages/core/.c8rc.json diff --git a/.gitignore b/.gitignore index 4707ded13..2b9fa1da6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ packages/core/lib/version.ts packages/core/test-results/ /examples/inference_summary /inference_summary -.turbo \ No newline at end of file +.turbo +# Code coverage +coverage/ +.nyc_output/ diff --git a/packages/core/.c8rc.json b/packages/core/.c8rc.json new file mode 100644 index 000000000..b0c6d24b7 --- /dev/null +++ b/packages/core/.c8rc.json @@ -0,0 +1,30 @@ +{ + "all": true, + "include": [ + "lib/v3/**/*.ts", + "!lib/v3/**/*.spec.ts", + "!lib/v3/**/*.d.ts" + ], + "exclude": [ + "**/*.spec.ts", + "**/tests/**", + "**/node_modules/**", + "**/dist/**" + ], + "extension": [".ts"], + "reporter": [ + "html", + "text", + "text-summary", + "lcov" + ], + "report-dir": "./coverage", + "temp-dir": "./.nyc_output", + "instrument": true, + "sourceMap": true, + "produce-source-map": true, + "lines": 50, + "functions": 50, + "branches": 50, + "statements": 50 +} diff --git a/packages/core/package.json b/packages/core/package.json index 631ab674d..03b23b9cd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,6 +11,8 @@ "build-js": "cd ../.. && tsup --entry.index packages/core/lib/v3/index.ts --dts --outDir packages/core/dist --external chrome-launcher --external playwright-core --external puppeteer-core --external patchright-core --external @playwright/test --external @langchain/core --external @langchain/openai", "typecheck": "tsc --noEmit", "build": "pnpm run gen-version && pnpm run build-dom-scripts && pnpm run build-js && pnpm run typecheck", + "test": "playwright test --config lib/v3/tests/v3.playwright.config.ts", + "test:coverage": "c8 --reporter=html --reporter=text-summary pnpm run test", "example": "node --import tsx -e \"const args=process.argv.slice(1).filter(a=>a!=='--'); const [p]=args; const n=(p||'example').replace(/^\\.\\//,'').replace(/\\.ts$/i,''); import(new URL(require('node:path').resolve('examples', n + '.ts'), 'file:'));\" --", "lint": "cd ../.. && prettier --check packages/core && cd packages/core && eslint .", "format": "prettier --write ." @@ -66,18 +68,19 @@ "ollama-ai-provider": "^1.2.0" }, "devDependencies": { - "playwright-core": "^1.54.1", - "puppeteer-core": "^22.8.0", - "chrome-launcher": "^1.2.0", - "patchright-core": "^1.55.2", - "@playwright/test": "^1.42.1", "@langchain/core": "^0.3.40", "@langchain/openai": "^0.4.4", - "typescript": "^5.2.2", + "@playwright/test": "^1.42.1", + "c8": "^10.1.3", + "chrome-launcher": "^1.2.0", + "eslint": "^9.16.0", + "patchright-core": "^1.55.2", + "playwright-core": "^1.54.1", + "prettier": "^3.2.5", + "puppeteer-core": "^22.8.0", "tsup": "^8.2.1", "tsx": "^4.10.5", - "prettier": "^3.2.5", - "eslint": "^9.16.0" + "typescript": "^5.2.2" }, "repository": { "type": "git", From ed79c9335776541b101137609c6a8f951942785c Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:50:41 -0700 Subject: [PATCH 03/12] Update .gitignore to remove coverage entries Remove code coverage directories from .gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 2b9fa1da6..a24e4186a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,4 @@ packages/core/test-results/ /examples/inference_summary /inference_summary .turbo -# Code coverage -coverage/ -.nyc_output/ + From 42c52e49bdd12453d8c726ded4c3a2db11dd9c23 Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:51:03 -0700 Subject: [PATCH 04/12] remove empty space --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a24e4186a..d7c90ee87 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,3 @@ packages/core/test-results/ /examples/inference_summary /inference_summary .turbo - From b0726b15371afaf96d7a78555899cdcf7386bd71 Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:51:18 -0700 Subject: [PATCH 05/12] Delete packages/core/.c8rc.json --- packages/core/.c8rc.json | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 packages/core/.c8rc.json diff --git a/packages/core/.c8rc.json b/packages/core/.c8rc.json deleted file mode 100644 index b0c6d24b7..000000000 --- a/packages/core/.c8rc.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "all": true, - "include": [ - "lib/v3/**/*.ts", - "!lib/v3/**/*.spec.ts", - "!lib/v3/**/*.d.ts" - ], - "exclude": [ - "**/*.spec.ts", - "**/tests/**", - "**/node_modules/**", - "**/dist/**" - ], - "extension": [".ts"], - "reporter": [ - "html", - "text", - "text-summary", - "lcov" - ], - "report-dir": "./coverage", - "temp-dir": "./.nyc_output", - "instrument": true, - "sourceMap": true, - "produce-source-map": true, - "lines": 50, - "functions": 50, - "branches": 50, - "statements": 50 -} From 0fe4ab7f4b6d295b522eb1f2dc5c96585d7a619c Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:51:55 -0700 Subject: [PATCH 06/12] Remove 'c8' from devDependencies Removed 'c8' from devDependencies. --- packages/core/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 03b23b9cd..3870604a6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -71,7 +71,6 @@ "@langchain/core": "^0.3.40", "@langchain/openai": "^0.4.4", "@playwright/test": "^1.42.1", - "c8": "^10.1.3", "chrome-launcher": "^1.2.0", "eslint": "^9.16.0", "patchright-core": "^1.55.2", From dd94e70c4a30fe096958311881b96521c20a2828 Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:52:18 -0700 Subject: [PATCH 07/12] Remove test scripts from package.json Removed test and test:coverage scripts from package.json --- packages/core/package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 3870604a6..2c1f57c70 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,8 +11,6 @@ "build-js": "cd ../.. && tsup --entry.index packages/core/lib/v3/index.ts --dts --outDir packages/core/dist --external chrome-launcher --external playwright-core --external puppeteer-core --external patchright-core --external @playwright/test --external @langchain/core --external @langchain/openai", "typecheck": "tsc --noEmit", "build": "pnpm run gen-version && pnpm run build-dom-scripts && pnpm run build-js && pnpm run typecheck", - "test": "playwright test --config lib/v3/tests/v3.playwright.config.ts", - "test:coverage": "c8 --reporter=html --reporter=text-summary pnpm run test", "example": "node --import tsx -e \"const args=process.argv.slice(1).filter(a=>a!=='--'); const [p]=args; const n=(p||'example').replace(/^\\.\\//,'').replace(/\\.ts$/i,''); import(new URL(require('node:path').resolve('examples', n + '.ts'), 'file:'));\" --", "lint": "cd ../.. && prettier --check packages/core && cd packages/core && eslint .", "format": "prettier --write ." From c95cd51e2945bd524f9b340d63ab4e3993722e0b Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:52:49 -0700 Subject: [PATCH 08/12] diff --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d7c90ee87..a24e4186a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ packages/core/test-results/ /examples/inference_summary /inference_summary .turbo + From f8750ea5f6ac6ff073c37ed8879abc232bcf5b79 Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:53:39 -0700 Subject: [PATCH 09/12] Remove empty line from .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index a24e4186a..d7c90ee87 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,3 @@ packages/core/test-results/ /examples/inference_summary /inference_summary .turbo - From a3e27361e7bdd4f5826266c064a389126f5cd4b8 Mon Sep 17 00:00:00 2001 From: tkattkat Date: Thu, 16 Oct 2025 15:51:33 -0700 Subject: [PATCH 10/12] add scroll + backendnodeid tests --- .../v3/tests/locator-backend-node-id.spec.ts | 216 ++++++++++++++ .../core/lib/v3/tests/page-scroll.spec.ts | 268 ++++++++++++++++++ 2 files changed, 484 insertions(+) create mode 100644 packages/core/lib/v3/tests/locator-backend-node-id.spec.ts create mode 100644 packages/core/lib/v3/tests/page-scroll.spec.ts diff --git a/packages/core/lib/v3/tests/locator-backend-node-id.spec.ts b/packages/core/lib/v3/tests/locator-backend-node-id.spec.ts new file mode 100644 index 000000000..66c580765 --- /dev/null +++ b/packages/core/lib/v3/tests/locator-backend-node-id.spec.ts @@ -0,0 +1,216 @@ +import { test, expect } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Locator.backendNodeId() - CDP DOM node ID", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch(() => {}); + }); + + test("returns a valid backend node ID for an element", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const locator = page.locator("button#btn"); + const nodeId = await locator.backendNodeId(); + + // Backend node ID should be a valid number + expect(typeof nodeId).toBe("number"); + expect(nodeId).toBeGreaterThan(0); + }); + + test("returns different node IDs for different elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
First
+
Second
+

Third

+ `, + ), + ); + + const nodeId1 = await page.locator("div#div1").backendNodeId(); + const nodeId2 = await page.locator("div#div2").backendNodeId(); + const nodeId3 = await page.locator("p#p1").backendNodeId(); + + // All node IDs should be unique + expect(nodeId1).not.toBe(nodeId2); + expect(nodeId2).not.toBe(nodeId3); + expect(nodeId1).not.toBe(nodeId3); + }); + + test("returns consistent node ID for the same element", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const locator = page.locator("input#input"); + + // Call multiple times on the same element + const nodeId1 = await locator.backendNodeId(); + const nodeId2 = await locator.backendNodeId(); + + // Should return the same ID (same element) + expect(nodeId1).toBe(nodeId2); + }); + + test("returns node ID for nested elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+
+ Deep +
+
+ `, + ), + ); + + const outerNodeId = await page.locator("div#outer").backendNodeId(); + const middleNodeId = await page.locator("div#middle").backendNodeId(); + const innerNodeId = await page.locator("span#inner").backendNodeId(); + + // All should be valid and unique + expect(outerNodeId).toBeGreaterThan(0); + expect(middleNodeId).toBeGreaterThan(0); + expect(innerNodeId).toBeGreaterThan(0); + expect(new Set([outerNodeId, middleNodeId, innerNodeId]).size).toBe(3); + }); + + test("returns node ID for elements with various attributes", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const locator = page.locator("button"); + const nodeId = await locator.backendNodeId(); + + // Should work with complex elements + expect(typeof nodeId).toBe("number"); + expect(nodeId).toBeGreaterThan(0); + }); + + test("returns node ID for form elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ + + + +
+ `, + ), + ); + + const emailNodeId = await page.locator("input#email").backendNodeId(); + const textareaNodeId = await page + .locator("textarea#message") + .backendNodeId(); + const selectNodeId = await page.locator("select#country").backendNodeId(); + const submitNodeId = await page + .locator("button[type='submit']") + .backendNodeId(); + + // All form elements should have valid node IDs + expect(emailNodeId).toBeGreaterThan(0); + expect(textareaNodeId).toBeGreaterThan(0); + expect(selectNodeId).toBeGreaterThan(0); + expect(submitNodeId).toBeGreaterThan(0); + + // All should be unique + const nodeIds = [emailNodeId, textareaNodeId, selectNodeId, submitNodeId]; + expect(new Set(nodeIds).size).toBe(4); + }); + + test("returns node ID for dynamically created elements", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ + `, + ), + ); + + const locator = page.locator("button#dynamic-btn"); + const nodeId = await locator.backendNodeId(); + + // Should work with dynamically created elements + expect(typeof nodeId).toBe("number"); + expect(nodeId).toBeGreaterThan(0); + }); + + test("returns node ID for elements with text selectors", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` + + `, + ), + ); + + const locator = page.locator("text=Submit Form"); + const nodeId = await locator.backendNodeId(); + + // Should work with text-based selectors + expect(typeof nodeId).toBe("number"); + expect(nodeId).toBeGreaterThan(0); + }); +}); diff --git a/packages/core/lib/v3/tests/page-scroll.spec.ts b/packages/core/lib/v3/tests/page-scroll.spec.ts new file mode 100644 index 000000000..850312b4e --- /dev/null +++ b/packages/core/lib/v3/tests/page-scroll.spec.ts @@ -0,0 +1,268 @@ +import { test, expect } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Page.scroll() - mouse wheel scrolling", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch(() => {}); + }); + + test("scrolls page vertically with positive deltaY", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Section 1
+
Section 2
+
Section 3
+
Section 4
+
Section 5
+ `, + ), + ); + + // Get initial scroll position + let scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBe(0); + + // Scroll down (positive deltaY) + await page.scroll(640, 400, 0, 300); + + // Wait for scroll to complete + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + // Check that we've scrolled down + scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBeGreaterThan(0); + }); + + test("scrolls page horizontally with positive deltaX", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Section 1
+
Section 2
+
Section 3
+
Section 4
+
Section 5
+ `, + ), + ); + + let scrollX = await page.evaluate(() => window.scrollX); + expect(scrollX).toBe(0); + + // Scroll right (positive deltaX) + await page.scroll(640, 400, 300, 0); + + // Wait for scroll to complete + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + // Check that we've scrolled right + scrollX = await page.evaluate(() => window.scrollX); + expect(scrollX).toBeGreaterThan(0); + }); + + test("scrolls in both directions simultaneously", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ Diagonal content +
+ `, + ), + ); + + // Scroll both horizontally and vertically + await page.scroll(640, 400, 200, 200); + + // Wait for scroll to complete + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + // Check both directions changed + const scrollPos = await page.evaluate(() => ({ + x: window.scrollX, + y: window.scrollY, + })); + + expect(scrollPos.x).toBeGreaterThan(0); + expect(scrollPos.y).toBeGreaterThan(0); + }); + + test("scrolls at specific coordinate on page", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+
Top
+
Middle
+
Bottom
+ `, + ), + ); + + // Scroll from specific coordinates + await page.scroll(640, 400, 0, 400); + + // Wait for scroll to complete + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + // Verify scroll happened + const scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBeGreaterThan(0); + }); + + test("scrolls with large deltaY values", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Section 1
+
Section 2
+
Section 3
+
Section 4
+
Section 5
+ `, + ), + ); + + // Scroll with large delta + await page.scroll(640, 400, 0, 1000); + + // Wait for scroll to complete + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + // Should scroll significantly + const scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBeGreaterThan(500); + }); + + test("negative deltaY scrolls up", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Top
+
Middle 1
+
Middle 2
+
Bottom
+ `, + ), + ); + + // First scroll down + await page.scroll(640, 400, 0, 500); + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + let scrollY = await page.evaluate(() => window.scrollY); + const scrolledDown = scrollY; + expect(scrolledDown).toBeGreaterThan(0); + + // Now scroll up (negative delta) + await page.scroll(640, 400, 0, -300); + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + scrollY = await page.evaluate(() => window.scrollY); + expect(scrollY).toBeLessThan(scrolledDown); + }); + + test("scroll returns xpath when requested", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
+ Target element +
+

Content below

+ `, + ), + ); + + // Scroll at coordinate (550, 50) which should be directly over the target div + // div spans: left 400-700px, top 0-100px + // coordinate 550,50 is within that range + const xpath = await page.scroll(550, 50, 0, 200, { returnXpath: true }); + + // Should return a non-empty xpath string for the element at that coordinate + expect(typeof xpath).toBe("string"); + expect(xpath.length).toBeGreaterThan(0); + // Xpath should reference the div or contain "target" + expect(xpath.toLowerCase()).toMatch(/div|target/); + }); + + test("scroll without returnXpath returns void", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Content
+ `, + ), + ); + + // Scroll without returnXpath + const result = await page.scroll(640, 400, 0, 200); + + // Should return undefined (void) + expect(result).toBeUndefined(); + }); + + test("multiple sequential scrolls accumulate", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent( + ` +
Section 1
+
Section 2
+
Section 3
+
Section 4
+ `, + ), + ); + + // First scroll + await page.scroll(640, 400, 0, 200); + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + const after1 = await page.evaluate(() => window.scrollY); + expect(after1).toBeGreaterThan(0); + + // Second scroll + await page.scroll(640, 400, 0, 200); + await page.evaluate(() => new Promise((r) => setTimeout(r, 200))); + + const after2 = await page.evaluate(() => window.scrollY); + + expect(after2).toBeGreaterThan(after1); + }); +}); From 72492fe2476de976d9cbcff7ec8ad3d57ca6a5c6 Mon Sep 17 00:00:00 2001 From: tkattkat Date: Thu, 16 Oct 2025 16:03:47 -0700 Subject: [PATCH 11/12] drag and drop --- .../lib/v3/tests/page-drag-and-drop.spec.ts | 519 ++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 packages/core/lib/v3/tests/page-drag-and-drop.spec.ts diff --git a/packages/core/lib/v3/tests/page-drag-and-drop.spec.ts b/packages/core/lib/v3/tests/page-drag-and-drop.spec.ts new file mode 100644 index 000000000..8784f5741 --- /dev/null +++ b/packages/core/lib/v3/tests/page-drag-and-drop.spec.ts @@ -0,0 +1,519 @@ +import { test, expect } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Page.dragAndDrop() - dragging elements", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch(() => {}); + }); + + test("drags and drops element to target zone", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
+
Drag Me
+
Drop Here
+
+
Status: Waiting
+ + + + `), + ); + + // Get coordinates for drag and drop + const sourceLocation = await page + .frames()[0] + .getLocationForSelector("#source"); + const dropZoneLocation = await page + .frames()[0] + .getLocationForSelector("#dropZone"); + + const fromX = sourceLocation.x + sourceLocation.width / 2; + const fromY = sourceLocation.y + sourceLocation.height / 2; + const toX = dropZoneLocation.x + dropZoneLocation.width / 2; + const toY = dropZoneLocation.y + dropZoneLocation.height / 2; + + // Perform drag and drop + await page.dragAndDrop(fromX, fromY, toX, toY); + + // Wait for events to be processed + await page.evaluate(() => new Promise((r) => setTimeout(r, 100))); + + // Verify visual result + const resultText = await page.evaluate( + () => document.getElementById("result").textContent, + ); + expect(resultText).toContain("DROP SUCCESSFUL"); + }); + + test("drag and drop with steps parameter", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
+
+
Not dropped
+ + + + `), + ); + + const boxLocation = await page.frames()[0].getLocationForSelector("#box"); + const targetLocation = await page + .frames()[0] + .getLocationForSelector("#target"); + + const fromX = boxLocation.x + boxLocation.width / 2; + const fromY = boxLocation.y + boxLocation.height / 2; + const toX = targetLocation.x + targetLocation.width / 2; + const toY = targetLocation.y + targetLocation.height / 2; + + // Drag with multiple steps for smoother motion + await page.dragAndDrop(fromX, fromY, toX, toY, { steps: 5 }); + + // Wait for events to be processed + await page.evaluate(() => new Promise((r) => setTimeout(r, 100))); + + const status = await page.evaluate( + () => document.getElementById("status").textContent, + ); + expect(status).toContain("Dropped"); + }); + + test("drag and drop with delay between steps", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
+
+
false
+ + + + `), + ); + + const itemLocation = await page + .frames()[0] + .getLocationForSelector("#dragItem"); + const areaLocation = await page + .frames()[0] + .getLocationForSelector("#dropArea"); + + const fromX = itemLocation.x + itemLocation.width / 2; + const fromY = itemLocation.y + itemLocation.height / 2; + const toX = areaLocation.x + areaLocation.width / 2; + const toY = areaLocation.y + areaLocation.height / 2; + + // Drag with delay between steps + await page.dragAndDrop(fromX, fromY, toX, toY, { steps: 3, delay: 50 }); + + // Wait for events to be processed + await page.evaluate(() => new Promise((r) => setTimeout(r, 100))); + + const isComplete = await page.evaluate( + () => document.getElementById("complete").textContent === "true", + ); + expect(isComplete).toBe(true); + }); + + test("drag and drop returns xpath when requested", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
+
+ + + + `), + ); + + const sourceLocation = await page + .frames()[0] + .getLocationForSelector("#source"); + const targetLocation = await page + .frames()[0] + .getLocationForSelector("#target"); + + const fromX = sourceLocation.x + sourceLocation.width / 2; + const fromY = sourceLocation.y + sourceLocation.height / 2; + const toX = targetLocation.x + targetLocation.width / 2; + const toY = targetLocation.y + targetLocation.height / 2; + + const [fromXpath, toXpath] = await page.dragAndDrop( + fromX, + fromY, + toX, + toY, + { + returnXpath: true, + }, + ); + + // Should return xpaths for both start and end positions + expect(typeof fromXpath).toBe("string"); + expect(typeof toXpath).toBe("string"); + expect(fromXpath.length).toBeGreaterThan(0); + expect(toXpath.length).toBeGreaterThan(0); + }); + + test("drag and drop without returnXpath returns void", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
+
+ + + + `), + ); + + const item1Location = await page + .frames()[0] + .getLocationForSelector("#item1"); + const item2Location = await page + .frames()[0] + .getLocationForSelector("#item2"); + + const fromX = item1Location.x + item1Location.width / 2; + const fromY = item1Location.y + item1Location.height / 2; + const toX = item2Location.x + item2Location.width / 2; + const toY = item2Location.y + item2Location.height / 2; + + const result = await page.dragAndDrop(fromX, fromY, toX, toY); + + expect(result).toBeUndefined(); + }); + + test("drag and drop with different mouse buttons", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
+
+
none
+ + + + `), + ); + + const sourceLocation = await page + .frames()[0] + .getLocationForSelector("#source"); + const targetLocation = await page + .frames()[0] + .getLocationForSelector("#target"); + + const fromX = sourceLocation.x + sourceLocation.width / 2; + const fromY = sourceLocation.y + sourceLocation.height / 2; + const toX = targetLocation.x + targetLocation.width / 2; + const toY = targetLocation.y + targetLocation.height / 2; + + // Test with left button (default) + await page.dragAndDrop(fromX, fromY, toX, toY, { button: "left" }); + + // Wait for events to be processed + await page.evaluate(() => new Promise((r) => setTimeout(r, 100))); + + const buttonUsed = await page.evaluate( + () => document.getElementById("buttonUsed").textContent, + ); + expect(buttonUsed).toBe("left"); + }); + + test("multiple sequential drag and drops", async () => { + const page = v3.context.pages()[0]; + + await page.goto( + "data:text/html," + + encodeURIComponent(` + + + + + + +
Item 1
+
+
Item 2
+
+
Drops: 0
+ + + + `), + ); + + const item1Location = await page + .frames()[0] + .getLocationForSelector("#item1"); + const zone1Location = await page + .frames()[0] + .getLocationForSelector("#zone1"); + + const from1X = item1Location.x + item1Location.width / 2; + const from1Y = item1Location.y + item1Location.height / 2; + const to1X = zone1Location.x + zone1Location.width / 2; + const to1Y = zone1Location.y + zone1Location.height / 2; + + await page.dragAndDrop(from1X, from1Y, to1X, to1Y); + + await page.evaluate(() => new Promise((r) => setTimeout(r, 100))); + + let dropCountText = await page.evaluate( + () => document.getElementById("log").textContent, + ); + expect(dropCountText).toContain("Drops: 1"); + + const item2Location = await page + .frames()[0] + .getLocationForSelector("#item2"); + const zone2Location = await page + .frames()[0] + .getLocationForSelector("#zone2"); + + const from2X = item2Location.x + item2Location.width / 2; + const from2Y = item2Location.y + item2Location.height / 2; + const to2X = zone2Location.x + zone2Location.width / 2; + const to2Y = zone2Location.y + zone2Location.height / 2; + + await page.dragAndDrop(from2X, from2Y, to2X, to2Y); + + // Wait for events to be processed + await page.evaluate(() => new Promise((r) => setTimeout(r, 100))); + + dropCountText = await page.evaluate( + () => document.getElementById("log").textContent, + ); + expect(dropCountText).toContain("Drops: 2"); + }); +}); From c73fe81239e9bd159b4e3595528b97dc6fa83b42 Mon Sep 17 00:00:00 2001 From: tkattkat <48974763+tkattkat@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:18:55 -0700 Subject: [PATCH 12/12] update package json Added prettier and eslint as dependencies. --- packages/core/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 2ba6fcef2..d2759a13e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -76,7 +76,8 @@ "typescript": "^5.2.2", "tsup": "^8.2.1", "tsx": "^4.10.5", - "typescript": "^5.2.2" + "prettier": "^3.2.5", + "eslint": "^9.16.0" }, "repository": { "type": "git",