@@ -315,6 +315,35 @@ describe("DOM snapshot collector", () => {
315315 expect ( labelledDiv ?. name ) . toBe ( "Description Label" ) ;
316316 } ) ;
317317
318+ it ( "captures span with aria-label inside nested structure (real-world icon button)" , ( ) => {
319+ setHtml ( `
320+ <div class="ant-space-item">
321+ <span aria-describedby="rh">
322+ <span class="anticon zcp-icon" aria-label="Show Deploy Detail" data-testid="action-detail" style="font-size: 16px;">
323+ <svg class="icon" viewBox="0 0 1024 1024" width="200" height="200">
324+ <path d="M833.013155 249.550056L468.049052"></path>
325+ </svg>
326+ </span>
327+ </span>
328+ </div>
329+ ` ) ;
330+
331+ const snapshot = collectDomSnapshot ( document ) ;
332+ const nodes = Object . values ( snapshot . idToNode ) ;
333+
334+ // The span with aria-label="Show Deploy Detail" should be captured
335+ const iconSpan = nodes . find ( ( n ) => n . name === "Show Deploy Detail" ) ;
336+ expect ( iconSpan ) . toBeTruthy ( ) ;
337+ expect ( iconSpan ?. tagName ) . toBe ( "span" ) ;
338+ expect ( iconSpan ?. name ) . toBe ( "Show Deploy Detail" ) ;
339+
340+ // Search should find it
341+ const searchResult = searchAndFormat ( snapshot , "Show Deploy Detail" ) ;
342+ expect ( searchResult ) . not . toBeNull ( ) ;
343+ expect ( searchResult ) . toContain ( "Show Deploy Detail" ) ;
344+ expect ( searchResult ) . not . toContain ( "No matches found" ) ;
345+ } ) ;
346+
318347 it ( "captures nested text content through multiple skipped generic elements" , ( ) => {
319348 setHtml ( `
320349 <div>
@@ -1944,3 +1973,197 @@ describe("searchAndFormat", () => {
19441973 expect ( result ) . toContain ( "Cancel" ) ;
19451974 } ) ;
19461975} ) ;
1976+
1977+ describe ( "cursor: pointer detection" , ( ) => {
1978+ beforeEach ( ( ) => {
1979+ document . body . innerHTML = "" ;
1980+ } ) ;
1981+
1982+ it ( "includes elements with cursor: pointer style as interactive" , ( ) => {
1983+ // Add a style element with cursor: pointer
1984+ document . body . innerHTML = `
1985+ <style>
1986+ .clickable-card { cursor: pointer; }
1987+ </style>
1988+ <div class="clickable-card">
1989+ <span>Card Title</span>
1990+ <span>Card Description</span>
1991+ </div>
1992+ ` ;
1993+
1994+ const snapshot = collectDomSnapshot ( document ) ;
1995+ const nodes = Object . values ( snapshot . idToNode ) ;
1996+
1997+ // The clickable-card div should be captured as a node (not just its text children)
1998+ const cardNode = nodes . find (
1999+ ( n ) =>
2000+ n . tagName === "div" &&
2001+ n . children &&
2002+ n . children . some (
2003+ ( c ) => c . role === "StaticText" && c . name === "Card Title" ,
2004+ ) ,
2005+ ) ;
2006+
2007+ expect ( cardNode ) . toBeDefined ( ) ;
2008+ expect ( cardNode ?. id ) . toBeTruthy ( ) ;
2009+ } ) ;
2010+
2011+ it ( "includes inline cursor: pointer elements" , ( ) => {
2012+ document . body . innerHTML = `
2013+ <div style="cursor: pointer;">Clickable Inline</div>
2014+ ` ;
2015+
2016+ const snapshot = collectDomSnapshot ( document ) ;
2017+ const nodes = Object . values ( snapshot . idToNode ) ;
2018+
2019+ // Should capture the div with cursor: pointer
2020+ const clickableDiv = nodes . find (
2021+ ( n ) =>
2022+ n . tagName === "div" &&
2023+ n . children ?. some (
2024+ ( c ) => c . role === "StaticText" && c . name === "Clickable Inline" ,
2025+ ) ,
2026+ ) ;
2027+
2028+ expect ( clickableDiv ) . toBeDefined ( ) ;
2029+ } ) ;
2030+
2031+ it ( "assigns node IDs to cursor: pointer elements for automation" , ( ) => {
2032+ document . body . innerHTML = `
2033+ <style>.clickable { cursor: pointer; }</style>
2034+ <div class="clickable">Click Me</div>
2035+ ` ;
2036+
2037+ const snapshot = collectDomSnapshot ( document ) ;
2038+ const clickableEl = document . querySelector ( ".clickable" ) ;
2039+ const nodeId = clickableEl ?. getAttribute ( "data-aipex-nodeid" ) ;
2040+
2041+ expect ( nodeId ) . toBeTruthy ( ) ;
2042+ expect ( snapshot . idToNode [ nodeId ! ] ) . toBeDefined ( ) ;
2043+ } ) ;
2044+
2045+ it ( "captures card component with cursor-pointer (simulating shadcn/ui card)" , ( ) => {
2046+ document . body . innerHTML = `
2047+ <style>.cursor-pointer { cursor: pointer; }</style>
2048+ <div data-slot="card" class="cursor-pointer bg-card rounded-xl border shadow-sm">
2049+ <div data-slot="card-header" class="flex flex-row items-center">
2050+ <div class="p-2 rounded-md bg-gray-100">
2051+ <svg class="size-4"></svg>
2052+ </div>
2053+ <div>
2054+ <div data-slot="card-title" class="text-base font-semibold">
2055+ deploy-k8s-workloads
2056+ </div>
2057+ <div class="text-sm text-gray-600 mt-1">
2058+ Usage zam and kapp deploy k8s workloads
2059+ </div>
2060+ </div>
2061+ </div>
2062+ </div>
2063+ ` ;
2064+
2065+ const snapshot = collectDomSnapshot ( document ) ;
2066+ const cardEl = document . querySelector ( '[data-slot="card"]' ) ;
2067+ const nodeId = cardEl ?. getAttribute ( "data-aipex-nodeid" ) ;
2068+
2069+ expect ( nodeId ) . toBeTruthy ( ) ;
2070+ expect ( snapshot . idToNode [ nodeId ! ] ) . toBeDefined ( ) ;
2071+ expect ( snapshot . idToNode [ nodeId ! ] ?. tagName ) . toBe ( "div" ) ;
2072+ } ) ;
2073+
2074+ it ( "searchAndFormat finds text within cursor-pointer card" , ( ) => {
2075+ document . body . innerHTML = `
2076+ <style>.cursor-pointer { cursor: pointer; }</style>
2077+ <div class="cursor-pointer">
2078+ <span>deploy-k8s-workloads</span>
2079+ <span>Usage zam and kapp deploy</span>
2080+ </div>
2081+ ` ;
2082+
2083+ const snapshot = collectDomSnapshot ( document ) ;
2084+ const result = searchAndFormat ( snapshot , "deploy-k8s-workloads" ) ;
2085+
2086+ expect ( result ) . not . toBeNull ( ) ;
2087+ expect ( result ) . toContain ( "deploy-k8s-workloads" ) ;
2088+ expect ( result ) . not . toContain ( "No matches found" ) ;
2089+ } ) ;
2090+
2091+ it ( "captures nested cursor-pointer elements with separate IDs" , ( ) => {
2092+ document . body . innerHTML = `
2093+ <style>
2094+ .outer-card { cursor: pointer; }
2095+ .inner-tag { cursor: pointer; }
2096+ </style>
2097+ <div class="outer-card">
2098+ <h3>Card Title</h3>
2099+ <span class="inner-tag">Clickable Tag</span>
2100+ </div>
2101+ ` ;
2102+
2103+ collectDomSnapshot ( document ) ;
2104+ const outerEl = document . querySelector ( ".outer-card" ) ;
2105+ const innerEl = document . querySelector ( ".inner-tag" ) ;
2106+
2107+ expect ( outerEl ?. getAttribute ( "data-aipex-nodeid" ) ) . toBeTruthy ( ) ;
2108+ expect ( innerEl ?. getAttribute ( "data-aipex-nodeid" ) ) . toBeTruthy ( ) ;
2109+ expect ( outerEl ?. getAttribute ( "data-aipex-nodeid" ) ) . not . toBe (
2110+ innerEl ?. getAttribute ( "data-aipex-nodeid" ) ,
2111+ ) ;
2112+ } ) ;
2113+
2114+ it ( "captures ant-tag with cursor-pointer as clickable element" , ( ) => {
2115+ document . body . innerHTML = `
2116+ <style>.cursor-pointer { cursor: pointer; }</style>
2117+ <span class="ant-tag cursor-pointer text-blue-500">
2118+ dev/main/va1/meta
2119+ </span>
2120+ ` ;
2121+
2122+ const snapshot = collectDomSnapshot ( document ) ;
2123+ const tagEl = document . querySelector ( ".ant-tag" ) ;
2124+ const nodeId = tagEl ?. getAttribute ( "data-aipex-nodeid" ) ;
2125+
2126+ expect ( nodeId ) . toBeTruthy ( ) ;
2127+ expect ( snapshot . idToNode [ nodeId ! ] ) . toBeDefined ( ) ;
2128+ expect ( snapshot . idToNode [ nodeId ! ] ?. tagName ) . toBe ( "span" ) ;
2129+ } ) ;
2130+
2131+ it ( "does not treat cursor: default elements as interactive" , ( ) => {
2132+ document . body . innerHTML = `
2133+ <style>.not-clickable { cursor: default; }</style>
2134+ <div class="not-clickable">Not Clickable</div>
2135+ ` ;
2136+
2137+ const snapshot = collectDomSnapshot ( document ) ;
2138+ const nodes = Object . values ( snapshot . idToNode ) ;
2139+
2140+ // Should not include the div as a separate node since it has cursor: default
2141+ // The text should still be captured as StaticText
2142+ const staticTextNode = nodes . find (
2143+ ( n ) => n . role === "StaticText" && n . name === "Not Clickable" ,
2144+ ) ;
2145+ expect ( staticTextNode ) . toBeDefined ( ) ;
2146+ } ) ;
2147+
2148+ it ( "captures table row with cursor-pointer for row click actions" , ( ) => {
2149+ document . body . innerHTML = `
2150+ <style>.clickable-row { cursor: pointer; }</style>
2151+ <table>
2152+ <tbody>
2153+ <tr class="clickable-row">
2154+ <td>Row Data 1</td>
2155+ <td>Row Data 2</td>
2156+ </tr>
2157+ </tbody>
2158+ </table>
2159+ ` ;
2160+
2161+ const snapshot = collectDomSnapshot ( document ) ;
2162+ const rowEl = document . querySelector ( ".clickable-row" ) ;
2163+ const nodeId = rowEl ?. getAttribute ( "data-aipex-nodeid" ) ;
2164+
2165+ expect ( nodeId ) . toBeTruthy ( ) ;
2166+ expect ( snapshot . idToNode [ nodeId ! ] ) . toBeDefined ( ) ;
2167+ expect ( snapshot . idToNode [ nodeId ! ] ?. tagName ) . toBe ( "tr" ) ;
2168+ } ) ;
2169+ } ) ;
0 commit comments