// INVARIANT: no duplicate attributes with the same name
// COMPLEXITY: O(n)/O(1)
export const componentTagger = (options?: ComponentTaggerOptions): PluginOption => {
const attributeName = options?.attributeName ?? componentPathAttributeName
+ const jsxOptions: JsxTaggerOptions | undefined = options
+ ? { tagComponents: options.tagComponents }
+ : undefined
let resolvedRoot = process.cwd()
return {
@@ -162,7 +186,9 @@ export const componentTagger = (options?: ComponentTaggerOptions): PluginOption
return null
}
- return Effect.runPromise(pipe(runTransform(code, id, resolvedRoot, attributeName), Effect.provide(NodePathLayer)))
+ return Effect.runPromise(
+ pipe(runTransform(code, id, resolvedRoot, attributeName, jsxOptions), Effect.provide(NodePathLayer))
+ )
}
}
}
diff --git a/packages/app/tests/core/component-path.test.ts b/packages/app/tests/core/component-path.test.ts
index 62b39d0..e4916d5 100644
--- a/packages/app/tests/core/component-path.test.ts
+++ b/packages/app/tests/core/component-path.test.ts
@@ -4,6 +4,7 @@ import { Effect } from "effect"
import {
componentPathAttributeName,
formatComponentPathValue,
+ isHtmlTag,
isJsxFile,
normalizeModuleId
} from "../../src/core/component-path.js"
@@ -43,4 +44,55 @@ describe("component-path", () => {
expect(normalizeModuleId("?")).toBe("")
expect(normalizeModuleId("file?")).toBe("file")
}))
+
+ describe("isHtmlTag", () => {
+ // CHANGE: add unit tests for isHtmlTag predicate.
+ // WHY: ensure correct classification of HTML vs React Component elements.
+ // QUOTE(TZ): "Есть тесты на и под разными настройками."
+ // REF: issue-23
+ // SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
+ // FORMAT THEOREM: ∀ name: isHtmlTag(name) ↔ name[0] ∈ [a-z]
+ // PURITY: CORE
+ // EFFECT: n/a
+ // INVARIANT: predicate correctly classifies HTML vs Component elements
+ // COMPLEXITY: O(1)/O(1)
+
+ it.effect("returns true for lowercase HTML tags", () =>
+ Effect.sync(() => {
+ expect(isHtmlTag("div")).toBe(true)
+ expect(isHtmlTag("h1")).toBe(true)
+ expect(isHtmlTag("span")).toBe(true)
+ expect(isHtmlTag("p")).toBe(true)
+ expect(isHtmlTag("main")).toBe(true)
+ expect(isHtmlTag("article")).toBe(true)
+ }))
+
+ it.effect("returns false for PascalCase React Components", () =>
+ Effect.sync(() => {
+ expect(isHtmlTag("MyComponent")).toBe(false)
+ expect(isHtmlTag("Route")).toBe(false)
+ expect(isHtmlTag("App")).toBe(false)
+ expect(isHtmlTag("Button")).toBe(false)
+ }))
+
+ it.effect("returns false for empty string", () =>
+ Effect.sync(() => {
+ expect(isHtmlTag("")).toBe(false)
+ }))
+
+ it.effect("returns false for strings starting with non-letter characters", () =>
+ Effect.sync(() => {
+ expect(isHtmlTag("123div")).toBe(false)
+ expect(isHtmlTag("_component")).toBe(false)
+ expect(isHtmlTag("$button")).toBe(false)
+ }))
+
+ it.effect("handles single-character element names", () =>
+ Effect.sync(() => {
+ expect(isHtmlTag("a")).toBe(true)
+ expect(isHtmlTag("A")).toBe(false)
+ expect(isHtmlTag("b")).toBe(true)
+ expect(isHtmlTag("B")).toBe(false)
+ }))
+ })
})
diff --git a/packages/app/tests/core/jsx-tagger-scoping.test.ts b/packages/app/tests/core/jsx-tagger-scoping.test.ts
new file mode 100644
index 0000000..bf91388
--- /dev/null
+++ b/packages/app/tests/core/jsx-tagger-scoping.test.ts
@@ -0,0 +1,135 @@
+import { types as t } from "@babel/core"
+import { describe, expect, it } from "@effect/vitest"
+import { Effect } from "effect"
+
+import { processJsxElement, shouldTagElement } from "../../src/core/jsx-tagger.js"
+import {
+ createJsxElement,
+ createJsxElementWithLocation,
+ createNamespacedElement,
+ createTestContext
+} from "./jsx-test-fixtures.js"
+
+// CHANGE: add comprehensive tests for tagComponents configuration in separate file.
+// WHY: reduce file length to stay under 300-line limit while maintaining full test coverage.
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// REF: issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
+// FORMAT THEOREM: ∀ elem, opts: shouldTagElement(elem, opts) satisfies configurable tagging invariants
+// PURITY: CORE (tests pure functions)
+// EFFECT: n/a
+// INVARIANT: tests verify mathematical properties of tagging predicates
+// COMPLEXITY: O(1) per test
+
+// Helper function for testing component tagging with different options
+const testComponentTagging = (
+ description: string,
+ options: { tagComponents?: boolean } | undefined,
+ expected: boolean
+) => {
+ it.effect(description, () =>
+ Effect.sync(() => {
+ const elements = [createJsxElement(t, "MyComponent"), createJsxElement(t, "Route")]
+ for (const element of elements) {
+ expect(shouldTagElement(element, options, t)).toBe(expected)
+ }
+ }))
+}
+
+// Helper function for testing processJsxElement integration
+const testProcessing = (
+ description: string,
+ elementName: string,
+ options: { tagComponents?: boolean } | undefined,
+ expectedProcessed: boolean,
+ expectedAttrCount: number
+) => {
+ it.effect(description, () =>
+ Effect.sync(() => {
+ const element = createJsxElementWithLocation(t, elementName, 5, 2)
+ const context = createTestContext("src/App.tsx", "data-path", options)
+ const result = processJsxElement(element, context, t)
+ expect(result).toBe(expectedProcessed)
+ expect(element.attributes).toHaveLength(expectedAttrCount)
+ }))
+}
+
+describe("jsx-tagger: shouldTagElement scoping", () => {
+ describe("HTML tags (lowercase)", () => {
+ it.effect("always tags HTML elements regardless of options", () =>
+ Effect.sync(() => {
+ const divElement = createJsxElement(t, "div")
+ const h1Element = createJsxElement(t, "h1")
+
+ // With tagComponents: true
+ expect(shouldTagElement(divElement, { tagComponents: true }, t)).toBe(true)
+ expect(shouldTagElement(h1Element, { tagComponents: true }, t)).toBe(true)
+
+ // With tagComponents: false
+ expect(shouldTagElement(divElement, { tagComponents: false }, t)).toBe(true)
+ expect(shouldTagElement(h1Element, { tagComponents: false }, t)).toBe(true)
+
+ // With undefined options (default behavior)
+ expect(shouldTagElement(divElement, undefined, t)).toBe(true)
+ expect(shouldTagElement(h1Element, undefined, t)).toBe(true)
+
+ // With empty options object
+ expect(shouldTagElement(divElement, {}, t)).toBe(true)
+ expect(shouldTagElement(h1Element, {}, t)).toBe(true)
+ }))
+ })
+
+ describe("React Components (PascalCase)", () => {
+ testComponentTagging("tags Components when tagComponents is true", { tagComponents: true }, true)
+ testComponentTagging("skips Components when tagComponents is false", { tagComponents: false }, false)
+ testComponentTagging("tags Components by default (undefined options)", undefined, true)
+ testComponentTagging("tags Components by default (empty options object)", {}, true)
+ })
+
+ describe("JSXMemberExpression (e.g., Foo.Bar)", () => {
+ it.effect("skips JSXMemberExpression elements", () =>
+ Effect.sync(() => {
+ const memberExpr = t.jsxOpeningElement(
+ t.jsxMemberExpression(t.jsxIdentifier("Foo"), t.jsxIdentifier("Bar")),
+ [],
+ false
+ )
+
+ expect(shouldTagElement(memberExpr, { tagComponents: true }, t)).toBe(false)
+ expect(shouldTagElement(memberExpr, { tagComponents: false }, t)).toBe(false)
+ }))
+ })
+
+ describe("JSXNamespacedName (e.g., svg:path)", () => {
+ it.effect("tags namespaced elements based on namespace name", () =>
+ Effect.sync(() => {
+ // svg:path - namespace is lowercase "svg"
+ const namespacedElement = createNamespacedElement(t, "svg", "path")
+
+ expect(shouldTagElement(namespacedElement, { tagComponents: true }, t)).toBe(true)
+ expect(shouldTagElement(namespacedElement, { tagComponents: false }, t)).toBe(true)
+ }))
+
+ it.effect("skips namespaced elements with uppercase namespace", () =>
+ Effect.sync(() => {
+ // Custom:Element - namespace is uppercase "Custom"
+ const namespacedElement = createNamespacedElement(t, "Custom", "Element")
+
+ expect(shouldTagElement(namespacedElement, { tagComponents: true }, t)).toBe(true)
+ expect(shouldTagElement(namespacedElement, { tagComponents: false }, t)).toBe(false)
+ }))
+ })
+
+ describe("processJsxElement integration with tagComponents", () => {
+ testProcessing("tags HTML elements with default options", "div", undefined, true, 1)
+ testProcessing("tags React Components with tagComponents: true", "MyComponent", { tagComponents: true }, true, 1)
+ testProcessing(
+ "skips React Components with tagComponents: false",
+ "MyComponent",
+ { tagComponents: false },
+ false,
+ 0
+ )
+ testProcessing("tags React Components by default (no options)", "Route", undefined, true, 1)
+ })
+})
diff --git a/packages/app/tests/core/jsx-tagger.test.ts b/packages/app/tests/core/jsx-tagger.test.ts
index 5325922..ac6877d 100644
--- a/packages/app/tests/core/jsx-tagger.test.ts
+++ b/packages/app/tests/core/jsx-tagger.test.ts
@@ -2,11 +2,12 @@ import { types as t } from "@babel/core"
import { describe, expect, it } from "@effect/vitest"
import { Effect } from "effect"
-import { attrExists, createPathAttribute, type JsxTaggerContext, processJsxElement } from "../../src/core/jsx-tagger.js"
+import { attrExists, createPathAttribute, processJsxElement } from "../../src/core/jsx-tagger.js"
import {
createEmptyNodeWithLocation,
createNodeWithClassName,
- createNodeWithClassNameAndLocation
+ createNodeWithClassNameAndLocation,
+ createTestContext
} from "./jsx-test-fixtures.js"
// CHANGE: add comprehensive unit tests for jsx-tagger core functions
@@ -18,14 +19,6 @@ import {
// INVARIANT: tests catch regressions in attribute handling and format
// COMPLEXITY: O(1) per test case
-// CHANGE: extract context factory to module scope per linter requirement
-// WHY: unicorn/consistent-function-scoping rule enforces scope consistency
-// REF: ESLint unicorn plugin rules
-const createTestContext = (filename = "src/App.tsx", attributeName = "data-path"): JsxTaggerContext => ({
- relativeFilename: filename,
- attributeName
-})
-
describe("jsx-tagger", () => {
describe("attrExists", () => {
// FORMAT THEOREM: ∀ node, name: attrExists(node, name) ↔ ∃ attr ∈ node.attributes: attr.name = name
diff --git a/packages/app/tests/core/jsx-test-fixtures.ts b/packages/app/tests/core/jsx-test-fixtures.ts
index a0089c5..c786b83 100644
--- a/packages/app/tests/core/jsx-test-fixtures.ts
+++ b/packages/app/tests/core/jsx-test-fixtures.ts
@@ -1,5 +1,7 @@
import type { types as t } from "@babel/core"
+import type { JsxTaggerContext } from "../../src/core/jsx-tagger.js"
+
// CHANGE: extract common test fixtures to reduce code duplication
// WHY: vibecode-linter detects duplicates in test setup code
// REF: issue-25 test implementation
@@ -7,6 +9,22 @@ import type { types as t } from "@babel/core"
// INVARIANT: factories produce deterministic test nodes
// COMPLEXITY: O(1) per factory call
+/**
+ * Creates a test context for JSX tagger tests.
+ *
+ * @pure true
+ * @complexity O(1)
+ */
+export const createTestContext = (
+ filename = "src/App.tsx",
+ attributeName = "data-path",
+ options?: { tagComponents?: boolean }
+): JsxTaggerContext => ({
+ relativeFilename: filename,
+ attributeName,
+ ...(options !== undefined && { options })
+})
+
/**
* Creates a mock SourceLocation for testing.
*
@@ -82,3 +100,53 @@ export const createNodeWithClassNameAndLocation = (
node.loc = createMockLocation(line, column)
return node
}
+
+/**
+ * Creates a simple JSX opening element without attributes for testing.
+ *
+ * @pure true
+ * @complexity O(1)
+ */
+export const createJsxElement = (types: typeof t, name: string): t.JSXOpeningElement => {
+ return types.jsxOpeningElement(types.jsxIdentifier(name), [], false)
+}
+
+/**
+ * Creates a JSX element with location info for testing.
+ *
+ * @pure true
+ * @complexity O(1)
+ */
+export const createJsxElementWithLocation = (
+ types: typeof t,
+ name: string,
+ line: number,
+ column: number
+): t.JSXOpeningElement => {
+ const element = types.jsxOpeningElement(types.jsxIdentifier(name), [], false)
+ element.loc = {
+ start: { line, column, index: 0 },
+ end: { line, column: column + name.length, index: 0 },
+ filename: "test.tsx",
+ identifierName: name
+ }
+ return element
+}
+
+/**
+ * Creates a JSX namespaced element for testing (e.g., svg:path).
+ *
+ * @pure true
+ * @complexity O(1)
+ */
+export const createNamespacedElement = (
+ types: typeof t,
+ namespace: string,
+ name: string
+): t.JSXOpeningElement => {
+ return types.jsxOpeningElement(
+ types.jsxNamespacedName(types.jsxIdentifier(namespace), types.jsxIdentifier(name)),
+ [],
+ false
+ )
+}
diff --git a/packages/frontend-nextjs/app/page.tsx b/packages/frontend-nextjs/app/page.tsx
index c6ce1b0..0b223a7 100644
--- a/packages/frontend-nextjs/app/page.tsx
+++ b/packages/frontend-nextjs/app/page.tsx
@@ -1,29 +1,54 @@
import type { ReactElement } from "react"
+/**
+ * Custom React Component for testing Component tagging.
+ *
+ * @returns ReactElement
+ *
+ * @pure true
+ * @invariant component wrapper test id is present
+ * @complexity O(1)
+ */
+// CHANGE: add custom React Component for integration tests.
+// WHY: verify that Components are tagged according to tagComponents option in Next.js.
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// REF: issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
+// FORMAT THEOREM: forall render: render(CustomComponent) -> has(testId)
+// PURITY: CORE
+// EFFECT: n/a
+// INVARIANT: wrapper div has test id for assertions
+// COMPLEXITY: O(1)/O(1)
+const CustomComponent = (): ReactElement => (
+ Custom Component Content
+)
+
/**
* Renders a minimal UI for verifying component tagger output.
*
* @returns ReactElement
*
* @pure true
- * @invariant title and description test ids are present
+ * @invariant title, description, and custom component test ids are present
* @complexity O(1)
*/
-// CHANGE: add a tiny React tree for Playwright assertions.
-// WHY: ensure the component tagger can be verified in a real Next.js runtime.
+// CHANGE: add a tiny React tree for Playwright assertions with both HTML and Component elements.
+// WHY: ensure the component tagger can be verified in a real Next.js runtime for both element types.
// QUOTE(TZ): "Создай новый проект типо packages/frontend только создай его для nextjs и проверь будет ли работать всё тоже самое на нём"
-// REF: issue-12
-// SOURCE: n/a
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// REF: issue-12, issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
// FORMAT THEOREM: forall render: render(Page) -> has(testIds)
// PURITY: CORE
// EFFECT: n/a
-// INVARIANT: title/description are stable for e2e checks
+// INVARIANT: title/description/custom-component are stable for e2e checks
// COMPLEXITY: O(1)/O(1)
export default function Home(): ReactElement {
return (
Component Tagger Next.js Demo
Every JSX element is tagged with path.
+
)
}
diff --git a/packages/frontend-nextjs/tests/component-path.spec.ts b/packages/frontend-nextjs/tests/component-path.spec.ts
index d7fa63f..0479a12 100644
--- a/packages/frontend-nextjs/tests/component-path.spec.ts
+++ b/packages/frontend-nextjs/tests/component-path.spec.ts
@@ -1,23 +1,56 @@
import { expect, test } from "@playwright/test"
-test("tags JSX with data-path", async ({ page }) => {
+// CHANGE: add integration tests for both HTML and Component tagging in Next.js with data-path attribute.
+// WHY: verify correct tagging behavior for both element types with Babel plugin and new attribute name.
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// QUOTE(issue-14): "Rename attribute path → data-path (breaking change)"
+// REF: issue-14, issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
+// FORMAT THEOREM: ∀ elem ∈ {HTML, Component}: tagged(elem) ∧ has_data_path_attr(elem)
+// PURITY: SHELL (E2E test with side effects)
+// EFFECT: Browser automation
+// INVARIANT: all JSX elements have data-path attribute (default behavior)
+// COMPLEXITY: O(1) per test
+
+test("tags HTML elements (lowercase tags) with data-path attribute", async ({ page }) => {
await page.goto("/")
+ // Test element (HTML tag)
const title = page.getByTestId("title")
- const value = await title.getAttribute("data-path")
+ const titlePath = await title.getAttribute("data-path")
// Log the tagged element HTML for CI visibility
const taggedHtml = await title.evaluate((el) => el.outerHTML)
console.log("\n=== Component Tagger Result ===")
console.log("Tagged element HTML:", taggedHtml)
- console.log("data-path attribute value:", value)
+ console.log("data-path attribute value:", titlePath)
console.log("===============================\n")
- expect(value).not.toBeNull()
- expect(value ?? "").toMatch(/(app|packages\/frontend-nextjs\/app)\/page\.tsx:\d+:\d+$/u)
+ expect(titlePath).not.toBeNull()
+ expect(titlePath ?? "").toMatch(/(app|packages\/frontend-nextjs\/app)\/page\.tsx:\d+:\d+$/u)
+
+ // Test
element (HTML tag)
+ const description = page.getByTestId("description")
+ const descPath = await description.getAttribute("data-path")
+
+ expect(descPath).not.toBeNull()
+ expect(descPath ?? "").toMatch(/(app|packages\/frontend-nextjs\/app)\/page\.tsx:\d+:\d+$/u)
+})
+
+test("tags React Components (PascalCase) with data-path attribute by default", async ({ page }) => {
+ await page.goto("/")
+
+ // Test (React Component)
+ // The component renders a div wrapper, but the CustomComponent invocation should be tagged
+ const customComponent = page.getByTestId("custom-component")
+
+ // Verify the wrapper div inside CustomComponent has data-path attribute
+ const componentPath = await customComponent.getAttribute("data-path")
+ expect(componentPath).not.toBeNull()
+ expect(componentPath ?? "").toMatch(/(app|packages\/frontend-nextjs\/app)\/page\.tsx:\d+:\d+$/u)
})
-test("shows tagged HTML in page source", async ({ page }) => {
+test("shows tagged HTML in page source with comprehensive verification", async ({ page }) => {
await page.goto("/")
// Get all elements with data-path attribute for comprehensive verification
@@ -34,6 +67,6 @@ test("shows tagged HTML in page source", async ({ page }) => {
}
console.log("===================================\n")
- // Verify we have tagged elements
+ // Verify we have tagged elements (at least 4: main, h1, p, CustomComponent's div)
expect(taggedElements.length).toBeGreaterThan(0)
})
diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 103e6e4..aa64c8e 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -1,25 +1,50 @@
+/**
+ * Custom React Component for testing Component tagging.
+ *
+ * @returns JSX.Element
+ *
+ * @pure true
+ * @invariant component wrapper test id is present
+ * @complexity O(1)
+ */
+// CHANGE: add custom React Component for integration tests.
+// WHY: verify that Components are tagged according to tagComponents option.
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// REF: issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
+// FORMAT THEOREM: forall render: render(CustomComponent) -> has(testId)
+// PURITY: CORE
+// EFFECT: n/a
+// INVARIANT: wrapper div has test id for assertions
+// COMPLEXITY: O(1)/O(1)
+const CustomComponent = (): JSX.Element => (
+ Custom Component Content
+)
+
/**
* Renders a minimal UI for verifying component tagger output.
*
* @returns JSX.Element
*
* @pure true
- * @invariant title and description test ids are present
+ * @invariant title, description, and custom component test ids are present
* @complexity O(1)
*/
-// CHANGE: add a tiny React tree for Playwright assertions.
-// WHY: ensure the component tagger can be verified in a real frontend runtime.
-// QUOTE(TZ): "\u0421\u0430\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c app \u043d\u043e \u0432\u043e\u0442 \u0447\u0442\u043e \u0431\u044b \u0435\u0433\u043e \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u0430\u0434\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u043f\u0440\u043e\u0435\u043a\u0442 \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u0448 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0430\u043f\u043f \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c"
-// REF: user-2026-01-14-frontend-consumer
-// SOURCE: n/a
+// CHANGE: add a tiny React tree for Playwright assertions with both HTML and Component elements.
+// WHY: ensure the component tagger can be verified in a real frontend runtime for both element types.
+// QUOTE(TZ): "Сам компонент должен быть в текущем app но вот что бы его протестировать надо создать ещё один проект который наш текущий апп будет подключать"
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// REF: user-2026-01-14-frontend-consumer, issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
// FORMAT THEOREM: forall render: render(App) -> has(testIds)
// PURITY: CORE
// EFFECT: n/a
-// INVARIANT: title/description are stable for e2e checks
+// INVARIANT: title/description/custom-component are stable for e2e checks
// COMPLEXITY: O(1)/O(1)
export const App = (): JSX.Element => (
Component Tagger Demo
Every JSX element is tagged with path.
+
)
diff --git a/packages/frontend/tests/component-path.spec.ts b/packages/frontend/tests/component-path.spec.ts
index 6f8e4dc..a4c5453 100644
--- a/packages/frontend/tests/component-path.spec.ts
+++ b/packages/frontend/tests/component-path.spec.ts
@@ -1,13 +1,44 @@
import { expect, test } from "@playwright/test"
-test("tags JSX with data-path", async ({ page }) => {
+// CHANGE: add integration tests for both HTML and Component tagging with data-path attribute.
+// WHY: verify correct tagging behavior for both element types with new attribute name.
+// QUOTE(TZ): "Есть тесты на и под разными настройками."
+// QUOTE(issue-14): "Rename attribute path → data-path (breaking change)"
+// REF: issue-14, issue-23
+// SOURCE: https://github.com/ProverCoderAI/component-tagger/issues/23
+// FORMAT THEOREM: ∀ elem ∈ {HTML, Component}: tagged(elem) ∧ has_data_path_attr(elem)
+// PURITY: SHELL (E2E test with side effects)
+// EFFECT: Browser automation
+// INVARIANT: all JSX elements have data-path attribute (default behavior)
+// COMPLEXITY: O(1) per test
+
+test("tags HTML elements (lowercase tags) with data-path attribute", async ({ page }) => {
await page.goto("/")
+ // Test element (HTML tag)
const title = page.getByTestId("title")
- const value = await title.getAttribute("data-path")
+ const titlePath = await title.getAttribute("data-path")
+
+ expect(titlePath).not.toBeNull()
+ expect(titlePath ?? "").toMatch(/(src|packages\/frontend\/src)\/App\.tsx:\d+:\d+$/u)
+
+ // Test
element (HTML tag)
+ const description = page.getByTestId("description")
+ const descPath = await description.getAttribute("data-path")
+
+ expect(descPath).not.toBeNull()
+ expect(descPath ?? "").toMatch(/(src|packages\/frontend\/src)\/App\.tsx:\d+:\d+$/u)
+})
+
+test("tags React Components (PascalCase) with data-path attribute by default", async ({ page }) => {
+ await page.goto("/")
- expect(value).not.toBeNull()
- expect(value ?? "").toMatch(/(src|packages\/frontend\/src)\/App\.tsx:\d+:\d+$/u)
+ // Test (React Component)
+ // The component renders a div wrapper, but the CustomComponent invocation should be tagged
+ const customComponent = page.getByTestId("custom-component")
- // Only assert the presence of the single data-path attribute.
+ // Verify the wrapper div inside CustomComponent has data-path attribute
+ const componentPath = await customComponent.getAttribute("data-path")
+ expect(componentPath).not.toBeNull()
+ expect(componentPath ?? "").toMatch(/(src|packages\/frontend\/src)\/App\.tsx:\d+:\d+$/u)
})