diff --git a/src/main/frontend/package-lock.json b/src/main/frontend/package-lock.json index 45e9c009..a4bc3e6e 100644 --- a/src/main/frontend/package-lock.json +++ b/src/main/frontend/package-lock.json @@ -5139,9 +5139,9 @@ "license": "MIT" }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "license": "MIT" }, "node_modules/lodash.debounce": { diff --git a/src/main/frontend/package.json b/src/main/frontend/package.json index 5438d088..857c8aaf 100644 --- a/src/main/frontend/package.json +++ b/src/main/frontend/package.json @@ -33,7 +33,8 @@ "tailwindcss-animate": "^1.0.7" }, "overrides": { - "dompurify": "^3.3.3" + "dompurify": "^3.3.3", + "lodash": ">=4.17.24" }, "devDependencies": { "@axe-core/playwright": "^4.10.1", diff --git a/src/main/frontend/playwright-report/data/52d6e968bcc3e3f9fb8eaecdf7ea2f3d2ab41deb.png b/src/main/frontend/playwright-report/data/52d6e968bcc3e3f9fb8eaecdf7ea2f3d2ab41deb.png new file mode 100644 index 00000000..dba53727 Binary files /dev/null and b/src/main/frontend/playwright-report/data/52d6e968bcc3e3f9fb8eaecdf7ea2f3d2ab41deb.png differ diff --git a/src/main/frontend/playwright-report/data/813e22f62153becae158a32a0ef8aa8037da1508.webm b/src/main/frontend/playwright-report/data/813e22f62153becae158a32a0ef8aa8037da1508.webm new file mode 100644 index 00000000..65ff4263 Binary files /dev/null and b/src/main/frontend/playwright-report/data/813e22f62153becae158a32a0ef8aa8037da1508.webm differ diff --git a/src/main/frontend/playwright-report/data/8c72b585fa3d6ad6b85b44878d2427a335bdf9de.md b/src/main/frontend/playwright-report/data/8c72b585fa3d6ad6b85b44878d2427a335bdf9de.md new file mode 100644 index 00000000..e3e09aae --- /dev/null +++ b/src/main/frontend/playwright-report/data/8c72b585fa3d6ad6b85b44878d2427a335bdf9de.md @@ -0,0 +1,432 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: search.spec.ts >> Global search >> keyboard navigation in search results (ArrowDown / Enter) +- Location: tests/e2e/search.spec.ts:106:3 + +# Error details + +``` +Error: expect(page).toHaveURL(expected) failed + +Expected pattern: /\/explorer|\/graph/ +Received string: "http://localhost:8080/" +Timeout: 5000ms + +Call log: + - Expect "toHaveURL" with timeout 5000ms + 8 × unexpected value "http://localhost:8080/" + +``` + +# Page snapshot + +```yaml +- generic [ref=e3]: + - complementary "Main navigation" [ref=e4]: + - generic [ref=e5]: + - generic [ref=e6]: + - img [ref=e7] + - generic [ref=e10]: IQ + - generic [ref=e11]: + - heading "Code IQ" [level=1] [ref=e12] + - paragraph [ref=e13]: Knowledge Graph + - button "Collapse sidebar" [ref=e14] [cursor=pointer]: + - img + - navigation [ref=e15]: + - link "Dashboard" [ref=e16] [cursor=pointer]: + - /url: / + - img [ref=e17] + - generic [ref=e22]: Dashboard + - link "Code Graph" [ref=e23] [cursor=pointer]: + - /url: /graph + - img [ref=e24] + - generic [ref=e29]: Code Graph + - link "Explorer" [ref=e30] [cursor=pointer]: + - /url: /explorer + - img [ref=e31] + - generic [ref=e35]: Explorer + - link "Console" [ref=e36] [cursor=pointer]: + - /url: /console + - img [ref=e37] + - generic [ref=e39]: Console + - link "API Docs" [ref=e40] [cursor=pointer]: + - /url: /api-docs + - img [ref=e41] + - generic [ref=e43]: API Docs + - generic [ref=e45]: + - generic [ref=e47]: + - paragraph [ref=e48]: Project Files + - generic [ref=e49]: 401 files + - generic [ref=e53]: + - img [ref=e54] + - textbox "Filter files" [ref=e57]: + - /placeholder: Filter files… + - tree "Project file tree" [ref=e58]: + - treeitem "Project 401" [expanded] [ref=e59] [cursor=pointer]: + - img [ref=e61] + - img [ref=e64] + - generic [ref=e66]: Project + - generic "401 graph nodes" [ref=e67]: "401" + - treeitem ".claude 3" [ref=e68] [cursor=pointer]: + - img [ref=e70] + - img [ref=e73] + - generic [ref=e75]: .claude + - generic "3 graph nodes" [ref=e76]: "3" + - treeitem ".github 31" [ref=e77] [cursor=pointer]: + - img [ref=e79] + - img [ref=e82] + - generic [ref=e84]: .github + - generic "31 graph nodes" [ref=e85]: "31" + - treeitem "pytest-of-dev 1" [ref=e86] [cursor=pointer]: + - img [ref=e88] + - img [ref=e91] + - generic [ref=e93]: pytest-of-dev + - generic "1 graph node" [ref=e94]: "1" + - treeitem "src 1.4k" [ref=e95] [cursor=pointer]: + - img [ref=e97] + - img [ref=e100] + - generic [ref=e102]: src + - generic "1439 graph nodes" [ref=e103]: 1.4k + - treeitem ". 1" [ref=e104] [cursor=pointer]: + - img [ref=e107] + - generic [ref=e110]: . + - generic "1 graph node" [ref=e111]: "1" + - treeitem "CLAUDE.md 43" [ref=e112] [cursor=pointer]: + - img [ref=e115] + - generic [ref=e118]: CLAUDE.md + - generic "43 graph nodes" [ref=e119]: "43" + - treeitem "README.md 53" [ref=e120] [cursor=pointer]: + - img [ref=e123] + - generic [ref=e126]: README.md + - generic "53 graph nodes" [ref=e127]: "53" + - treeitem "pom.xml 1" [ref=e128] [cursor=pointer]: + - img [ref=e131] + - generic [ref=e134]: pom.xml + - generic "1 graph node" [ref=e135]: "1" + - treeitem "sonar-project.properties 9" [ref=e136] [cursor=pointer]: + - img [ref=e139] + - generic [ref=e142]: sonar-project.properties + - generic "9 graph nodes" [ref=e143]: "9" + - generic [ref=e144]: + - banner [ref=e145]: + - generic [ref=e147]: + - generic [ref=e148]: + - img [ref=e149] + - searchbox "Search nodes, kinds, files..." [active] [ref=e152]: User + - button "Clear search" [ref=e153] [cursor=pointer]: + - img [ref=e154] + - listbox [ref=e157]: + - option "class UserService" [ref=e158] [cursor=pointer]: + - generic [ref=e159]: class + - generic [ref=e160]: UserService + - option "method findById" [ref=e161] [cursor=pointer]: + - generic [ref=e162]: method + - generic [ref=e163]: findById + - 'option "endpoint GET /users/{id}" [ref=e164] [cursor=pointer]': + - generic [ref=e165]: endpoint + - generic [ref=e166]: "GET /users/{id}" + - generic [ref=e167]: + - button "Toggle theme" [ref=e168] [cursor=pointer]: + - img + - generic [ref=e169]: Toggle theme + - button "User profile" [ref=e170] [cursor=pointer]: + - img + - main [ref=e174]: + - generic [ref=e175]: + - generic [ref=e176]: + - generic [ref=e177]: + - heading "Dashboard" [level=1] [ref=e178] + - paragraph [ref=e179]: Code knowledge graph overview + - button "Refresh stats" [ref=e180] [cursor=pointer]: + - img + - generic [ref=e181]: + - button "View Nodes in Explorer" [ref=e182] [cursor=pointer]: + - generic [ref=e185]: + - generic [ref=e186]: + - paragraph [ref=e187]: Nodes + - paragraph [ref=e188]: "0" + - paragraph [ref=e189]: Total graph nodes + - img [ref=e191] + - generic [ref=e197]: + - generic [ref=e198]: + - paragraph [ref=e199]: Edges + - paragraph [ref=e200]: "0" + - paragraph [ref=e201]: Relationships + - img [ref=e203] + - button "View Files in Explorer" [ref=e207] [cursor=pointer]: + - generic [ref=e210]: + - generic [ref=e211]: + - paragraph [ref=e212]: Files + - paragraph [ref=e213]: "0" + - paragraph [ref=e214]: Source files scanned + - img [ref=e216] + - generic [ref=e224]: + - generic [ref=e225]: + - paragraph [ref=e226]: Languages + - paragraph [ref=e227]: "0" + - paragraph [ref=e228]: Detected languages + - img [ref=e230] + - generic [ref=e235]: + - heading "Node Kinds" [level=3] [ref=e237]: + - img [ref=e238] + - text: Node Kinds + - list [ref=e241]: + - button "method 671 0" [ref=e242] [cursor=pointer]: + - generic [ref=e243]: + - generic [ref=e244]: method + - generic [ref=e245]: "671" + - progressbar [ref=e246] + - button "class 421 0" [ref=e247] [cursor=pointer]: + - generic [ref=e248]: + - generic [ref=e249]: class + - generic [ref=e250]: "421" + - progressbar [ref=e251] + - button "config_key 166 0" [ref=e252] [cursor=pointer]: + - generic [ref=e253]: + - generic [ref=e254]: config_key + - generic [ref=e255]: "166" + - progressbar [ref=e256] + - button "endpoint 74 0" [ref=e257] [cursor=pointer]: + - generic [ref=e258]: + - generic [ref=e259]: endpoint + - generic [ref=e260]: "74" + - progressbar [ref=e261] + - button "module 56 0" [ref=e262] [cursor=pointer]: + - generic [ref=e263]: + - generic [ref=e264]: module + - generic [ref=e265]: "56" + - progressbar [ref=e266] + - button "interface 54 0" [ref=e267] [cursor=pointer]: + - generic [ref=e268]: + - generic [ref=e269]: interface + - generic [ref=e270]: "54" + - progressbar [ref=e271] + - button "middleware 32 0" [ref=e272] [cursor=pointer]: + - generic [ref=e273]: + - generic [ref=e274]: middleware + - generic [ref=e275]: "32" + - progressbar [ref=e276] + - button "component 26 0" [ref=e277] [cursor=pointer]: + - generic [ref=e278]: + - generic [ref=e279]: component + - generic [ref=e280]: "26" + - progressbar [ref=e281] + - button "query 23 0" [ref=e282] [cursor=pointer]: + - generic [ref=e283]: + - generic [ref=e284]: query + - generic [ref=e285]: "23" + - progressbar [ref=e286] + - button "guard 19 0" [ref=e287] [cursor=pointer]: + - generic [ref=e288]: + - generic [ref=e289]: guard + - generic [ref=e290]: "19" + - progressbar [ref=e291] + - button "abstract_class 17 0" [ref=e292] [cursor=pointer]: + - generic [ref=e293]: + - generic [ref=e294]: abstract_class + - generic [ref=e295]: "17" + - progressbar [ref=e296] + - button "config_file 15 0" [ref=e297] [cursor=pointer]: + - generic [ref=e298]: + - generic [ref=e299]: config_file + - generic [ref=e300]: "15" + - progressbar [ref=e301] + - button "event 12 0" [ref=e302] [cursor=pointer]: + - generic [ref=e303]: + - generic [ref=e304]: event + - generic [ref=e305]: "12" + - progressbar [ref=e306] + - button "queue 10 0" [ref=e307] [cursor=pointer]: + - generic [ref=e308]: + - generic [ref=e309]: queue + - generic [ref=e310]: "10" + - progressbar [ref=e311] +``` + +# Test source + +```ts + 19 | await mockStats(page); + 20 | + 21 | // Mock search API + 22 | await page.route('**/api/search**', route => + 23 | route.fulfill({ + 24 | status: 200, + 25 | contentType: 'application/json', + 26 | body: JSON.stringify(MOCK_SEARCH_RESULTS), + 27 | }) + 28 | ); + 29 | }); + 30 | + 31 | test('search box is visible in header on all views', async ({ page }) => { + 32 | for (const route of Object.values(ROUTES)) { + 33 | await gotoRoute(page, route); + 34 | await expect(page.getByRole('searchbox')).toBeVisible(); + 35 | } + 36 | }); + 37 | + 38 | test('typing fewer than 2 characters does not trigger search', async ({ page }) => { + 39 | await gotoRoute(page, ROUTES.dashboard); + 40 | let searchCalled = false; + 41 | await page.route('**/api/search**', () => { searchCalled = true; }); + 42 | + 43 | await page.getByRole('searchbox').fill('U'); + 44 | await page.waitForTimeout(400); // debounce window + 45 | + 46 | expect(searchCalled).toBe(false); + 47 | }); + 48 | + 49 | test('typing 2+ characters triggers search with debounce', async ({ page }) => { + 50 | await gotoRoute(page, ROUTES.dashboard); + 51 | const searchBox = page.getByRole('searchbox'); + 52 | await searchBox.fill('User'); + 53 | + 54 | // Wait for debounce (300ms) + render + 55 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 56 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 57 | }); + 58 | + 59 | test('search results show correct names and kinds', async ({ page }) => { + 60 | await gotoRoute(page, ROUTES.dashboard); + 61 | await page.getByRole('searchbox').fill('User'); + 62 | + 63 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 64 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 65 | + 66 | await expect(dropdown.getByText('UserService')).toBeVisible(); + 67 | await expect(dropdown.getByText('findById')).toBeVisible(); + 68 | await expect(dropdown.getByText('GET /users/{id}')).toBeVisible(); + 69 | }); + 70 | + 71 | test('clicking a result navigates to the Explorer view', async ({ page }) => { + 72 | await gotoRoute(page, ROUTES.dashboard); + 73 | await page.getByRole('searchbox').fill('User'); + 74 | + 75 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 76 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 77 | await dropdown.getByText('UserService').click(); + 78 | + 79 | // Should navigate to explorer with the selected node + 80 | await expect(page).toHaveURL(/\/explorer/); + 81 | }); + 82 | + 83 | test('pressing Escape clears search dropdown', async ({ page }) => { + 84 | await gotoRoute(page, ROUTES.dashboard); + 85 | await page.getByRole('searchbox').fill('User'); + 86 | + 87 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 88 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 89 | + 90 | await page.keyboard.press('Escape'); + 91 | await expect(dropdown).not.toBeVisible(); + 92 | }); + 93 | + 94 | test('clicking outside search closes the dropdown', async ({ page }) => { + 95 | await gotoRoute(page, ROUTES.dashboard); + 96 | await page.getByRole('searchbox').fill('User'); + 97 | + 98 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 99 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 100 | + 101 | // Click somewhere outside the search bar + 102 | await page.locator('main').click({ position: { x: 10, y: 10 }, force: true }); + 103 | await expect(dropdown).not.toBeVisible(); + 104 | }); + 105 | + 106 | test('keyboard navigation in search results (ArrowDown / Enter)', async ({ page }) => { + 107 | await gotoRoute(page, ROUTES.dashboard); + 108 | const searchBox = page.getByRole('searchbox'); + 109 | await searchBox.fill('User'); + 110 | + 111 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 112 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 113 | + 114 | // Navigate with ArrowDown and select with Enter + 115 | await page.keyboard.press('ArrowDown'); + 116 | await page.keyboard.press('Enter'); + 117 | + 118 | // Should have navigated +> 119 | await expect(page).toHaveURL(/\/explorer|\/graph/); + | ^ Error: expect(page).toHaveURL(expected) failed + 120 | }); + 121 | + 122 | test('loading indicator shows while search is in progress', async ({ page }) => { + 123 | // Slow down the search response to see the loading state + 124 | await page.route('**/api/search**', async route => { + 125 | await new Promise(resolve => setTimeout(resolve, 300)); + 126 | await route.fulfill({ + 127 | status: 200, + 128 | contentType: 'application/json', + 129 | body: JSON.stringify(MOCK_SEARCH_RESULTS), + 130 | }); + 131 | }); + 132 | + 133 | await gotoRoute(page, ROUTES.dashboard); + 134 | await page.getByRole('searchbox').fill('User'); + 135 | + 136 | // Loading indicator should appear briefly + 137 | const spinner = page.locator('[data-testid="search-spinner"]'); + 138 | await expect(spinner).toBeVisible({ timeout: 500 }); + 139 | }); + 140 | + 141 | test('empty search results shows "no results" message', async ({ page }) => { + 142 | await page.route('**/api/search**', route => + 143 | route.fulfill({ status: 200, contentType: 'application/json', body: '[]' }) + 144 | ); + 145 | + 146 | await gotoRoute(page, ROUTES.dashboard); + 147 | await page.getByRole('searchbox').fill('xyznonexistent'); + 148 | + 149 | const dropdown = page.locator('[data-testid="search-dropdown"]'); + 150 | await expect(dropdown).toBeVisible({ timeout: 1000 }); + 151 | await expect(dropdown).toContainText(/no results/i); + 152 | }); + 153 | }); + 154 | + 155 | // ── File tree filtering (Phase 2 Frontend) ──────────────────────────────────── + 156 | + 157 | test.describe('File tree search integration', () => { + 158 | test.beforeEach(async ({ page }) => { + 159 | await mockStats(page); + 160 | await page.route('**/api/file-tree**', route => + 161 | route.fulfill({ + 162 | status: 200, + 163 | contentType: 'application/json', + 164 | body: JSON.stringify({ + 165 | name: 'root', + 166 | children: [ + 167 | { name: 'src', children: [ + 168 | { name: 'main', children: [ + 169 | { name: 'java', children: [ + 170 | { name: 'UserService.java', nodeCount: 5 }, + 171 | { name: 'UserController.java', nodeCount: 3 }, + 172 | ]}, + 173 | ]}, + 174 | ]}, + 175 | ], + 176 | }), + 177 | }) + 178 | ); + 179 | }); + 180 | + 181 | test('typing in search filters the file tree', async ({ page }) => { + 182 | await gotoRoute(page, ROUTES.explorer); + 183 | await page.getByRole('searchbox').fill('UserService'); + 184 | + 185 | // File tree should filter to show only matching files + 186 | const tree = page.locator('[data-testid="file-tree"]'); + 187 | if (await tree.isVisible()) { + 188 | await expect(tree.getByText('UserService.java')).toBeVisible(); + 189 | // Non-matching file should be hidden + 190 | await expect(tree.getByText('UserController.java')).not.toBeVisible(); + 191 | } + 192 | }); + 193 | }); + 194 | +``` \ No newline at end of file diff --git a/src/main/frontend/playwright-report/data/b2072c8aa5cd97f91093ec8cad3e5f7b48b55c66.md b/src/main/frontend/playwright-report/data/b2072c8aa5cd97f91093ec8cad3e5f7b48b55c66.md new file mode 100644 index 00000000..eac5b650 --- /dev/null +++ b/src/main/frontend/playwright-report/data/b2072c8aa5cd97f91093ec8cad3e5f7b48b55c66.md @@ -0,0 +1,222 @@ +# Instructions + +- Following Playwright test failed. +- Explain why, be concise, respect Playwright best practices. +- Provide a snippet of code with the fix, if possible. + +# Test info + +- Name: search.spec.ts >> File tree search integration >> typing in search filters the file tree +- Location: tests/e2e/search.spec.ts:181:3 + +# Error details + +``` +Test timeout of 30000ms exceeded. +``` + +``` +TimeoutError: page.waitForSelector: Timeout 30000ms exceeded. +Call log: + - waiting for locator('main') to be visible + +``` + +# Page snapshot + +```yaml +- generic [ref=e3]: + - heading "Something went wrong" [level=1] [ref=e4] + - paragraph [ref=e5]: Cannot read properties of undefined (reading 'toLocaleString') + - button "Reload page" [ref=e6] [cursor=pointer] +``` + +# Test source + +```ts + 1 | /// + 2 | import { type Page, expect } from '@playwright/test'; + 3 | import { readFileSync, existsSync } from 'node:fs'; + 4 | import { resolve } from 'node:path'; + 5 | + 6 | // ── Route helpers ──────────────────────────────────────────────────────────── + 7 | + 8 | export const ROUTES = { + 9 | dashboard: '/', + 10 | graph: '/graph', + 11 | explorer: '/explorer', + 12 | console: '/console', + 13 | apiDocs: '/api-docs', + 14 | } as const; + 15 | + 16 | export type AppRoute = (typeof ROUTES)[keyof typeof ROUTES]; + 17 | + 18 | /** + 19 | * Intercept the HTML shell served by Spring Boot and replace it with the + 20 | * current on-disk version. The running JAR may contain a stale index.html + 21 | * (built before the last frontend rebuild), causing it to load an old + 22 | * JS bundle that crashes before React mounts. + 23 | * + 24 | * Bug: STALE_BUNDLE — tracked in RAN-80 (filed separately). + 25 | */ + 26 | export async function patchIndexHtml(page: Page) { + 27 | // process.cwd() is the frontend dir when running `npx playwright test` + 28 | const staticDir = resolve(process.cwd(), '../resources/static'); + 29 | const diskHtml = readFileSync(resolve(staticDir, 'index.html'), 'utf-8'); + 30 | + 31 | const CONTENT_TYPES: Record = { + 32 | '.js': 'application/javascript', + 33 | '.mjs': 'application/javascript', + 34 | '.css': 'text/css', + 35 | '.svg': 'image/svg+xml', + 36 | '.png': 'image/png', + 37 | '.ico': 'image/x-icon', + 38 | '.woff2': 'font/woff2', + 39 | '.woff': 'font/woff', + 40 | }; + 41 | + 42 | // Intercept the SPA shell route (all navigation routes return the same HTML) + 43 | await page.route('**/*', async (route) => { + 44 | const req = route.request(); + 45 | const url = req.url(); + 46 | + 47 | // Serve HTML shell from disk + 48 | if ( + 49 | req.resourceType() === 'document' && + 50 | !url.includes('/api/') && + 51 | !url.includes('/swagger') && + 52 | !url.includes('/v3/') + 53 | ) { + 54 | await route.fulfill({ status: 200, contentType: 'text/html', body: diskHtml }); + 55 | return; + 56 | } + 57 | + 58 | // Serve static assets from disk if available (fixes stale-JAR bundle mismatch) + 59 | const assetMatch = url.match(/\/assets\/([^?#]+)/); + 60 | if (assetMatch) { + 61 | const assetName = assetMatch[1]; + 62 | const diskPath = resolve(staticDir, 'assets', assetName); + 63 | const ext = assetName.includes('.') ? '.' + assetName.split('.').pop()! : ''; + 64 | if (existsSync(diskPath)) { + 65 | const body = readFileSync(diskPath); + 66 | await route.fulfill({ + 67 | status: 200, + 68 | contentType: CONTENT_TYPES[ext] ?? 'application/octet-stream', + 69 | body, + 70 | }); + 71 | return; + 72 | } + 73 | } + 74 | + 75 | await route.fallback(); + 76 | }); + 77 | } + 78 | + 79 | /** Navigate to a route and wait for the main content area to be visible. */ + 80 | export async function gotoRoute(page: Page, route: AppRoute) { + 81 | await patchIndexHtml(page); + 82 | await page.goto(route); + 83 | // Wait for React to hydrate (main rendered by Layout component) +> 84 | await page.waitForSelector('main', { state: 'visible', timeout: 30000 }); + | ^ TimeoutError: page.waitForSelector: Timeout 30000ms exceeded. + 85 | } + 86 | + 87 | // ── Theme helpers ──────────────────────────────────────────────────────────── + 88 | + 89 | /** Returns the current theme: 'dark' | 'light'. */ + 90 | export async function getTheme(page: Page): Promise<'dark' | 'light'> { + 91 | const cls = await page.locator('html').getAttribute('class') ?? ''; + 92 | return cls.includes('dark') ? 'dark' : 'light'; + 93 | } + 94 | + 95 | /** Click the theme toggle and wait for the class to flip. */ + 96 | export async function toggleTheme(page: Page) { + 97 | const before = await getTheme(page); + 98 | // Theme toggle button — uses aria-label or data-testid set by the component + 99 | await page.getByRole('button', { name: /toggle theme|switch theme|dark mode|light mode/i }).click(); + 100 | await expect(page.locator('html')).toHaveClass(before === 'dark' ? /light/ : /dark/, { timeout: 2000 }); + 101 | } + 102 | + 103 | // ── API mock helpers ───────────────────────────────────────────────────────── + 104 | + 105 | /** Seed the `/api/stats` mock for deterministic dashboard tests. */ + 106 | export async function mockStats(page: Page, nodeCount = 1234, edgeCount = 5678) { + 107 | await page.route('**/api/stats', route => + 108 | route.fulfill({ + 109 | status: 200, + 110 | contentType: 'application/json', + 111 | body: JSON.stringify({ + 112 | totalNodes: nodeCount, + 113 | totalEdges: edgeCount, + 114 | nodesByKind: { endpoint: 10, class: 20, method: 30 }, + 115 | edgesByKind: { calls: 100, depends_on: 50 }, + 116 | languages: { java: 500, typescript: 200 }, + 117 | frameworks: { spring_boot: 300 }, + 118 | layers: { backend: 600, frontend: 200, infra: 100, shared: 50, unknown: 284 }, + 119 | }), + 120 | }) + 121 | ); + 122 | } + 123 | + 124 | /** + 125 | * Generate a synthetic node list for performance/stress tests. + 126 | * Returns a NodesListResponse-shaped object. + 127 | */ + 128 | export function generateNodeList(count: number) { + 129 | const nodes = Array.from({ length: count }, (_, i) => ({ + 130 | id: `node:file${i % 100}.ts:class:Class${i}`, + 131 | kind: ['class', 'method', 'endpoint', 'entity', 'function'][i % 5], + 132 | name: `Symbol${i}`, + 133 | qualifiedName: `com.example.Symbol${i}`, + 134 | filePath: `src/file${i % 100}.ts`, + 135 | layer: 'backend', + 136 | framework: null, + 137 | properties: {}, + 138 | })); + 139 | return { nodes, total: count, offset: 0, limit: count }; + 140 | } + 141 | + 142 | /** Seed the `/api/kinds` + `/api/nodes` endpoints with synthetic data. */ + 143 | export async function mockGraphData(page: Page, nodeCount: number) { + 144 | const data = generateNodeList(nodeCount); + 145 | + 146 | await page.route('**/api/kinds', route => + 147 | route.fulfill({ + 148 | status: 200, + 149 | contentType: 'application/json', + 150 | body: JSON.stringify({ + 151 | kinds: [ + 152 | { kind: 'class', count: Math.floor(nodeCount * 0.3) }, + 153 | { kind: 'method', count: Math.floor(nodeCount * 0.3) }, + 154 | { kind: 'endpoint', count: Math.floor(nodeCount * 0.15) }, + 155 | { kind: 'entity', count: Math.floor(nodeCount * 0.15) }, + 156 | { kind: 'function', count: Math.floor(nodeCount * 0.1) }, + 157 | ], + 158 | }), + 159 | }) + 160 | ); + 161 | + 162 | await page.route('**/api/nodes**', route => + 163 | route.fulfill({ + 164 | status: 200, + 165 | contentType: 'application/json', + 166 | body: JSON.stringify(data), + 167 | }) + 168 | ); + 169 | + 170 | await page.route('**/api/topology', route => + 171 | route.fulfill({ + 172 | status: 200, + 173 | contentType: 'application/json', + 174 | body: JSON.stringify({ + 175 | services: [ + 176 | { name: 'api-service', nodeCount: Math.floor(nodeCount / 3), dependencies: ['db-service'] }, + 177 | { name: 'db-service', nodeCount: Math.floor(nodeCount / 3), dependencies: [] }, + 178 | { name: 'frontend-service', nodeCount: Math.floor(nodeCount / 3), dependencies: ['api-service'] }, + 179 | ], + 180 | }), + 181 | }) + 182 | ); + 183 | } + 184 | +``` \ No newline at end of file diff --git a/src/main/frontend/playwright-report/data/f1d00512fbfc05f51c50987f6e805c0156f31b2d.png b/src/main/frontend/playwright-report/data/f1d00512fbfc05f51c50987f6e805c0156f31b2d.png new file mode 100644 index 00000000..2e81505d Binary files /dev/null and b/src/main/frontend/playwright-report/data/f1d00512fbfc05f51c50987f6e805c0156f31b2d.png differ diff --git a/src/main/frontend/playwright-report/data/f6eb10ff67df8fe6b39c93a7519699a0d4e67b55.webm b/src/main/frontend/playwright-report/data/f6eb10ff67df8fe6b39c93a7519699a0d4e67b55.webm new file mode 100644 index 00000000..a092c5ee Binary files /dev/null and b/src/main/frontend/playwright-report/data/f6eb10ff67df8fe6b39c93a7519699a0d4e67b55.webm differ diff --git a/src/main/frontend/playwright-report/index.html b/src/main/frontend/playwright-report/index.html index 44aea3e0..d3c4cd5f 100644 --- a/src/main/frontend/playwright-report/index.html +++ b/src/main/frontend/playwright-report/index.html @@ -87,4 +87,4 @@
- \ No newline at end of file + \ No newline at end of file diff --git a/src/main/frontend/test-results/.last-run.json b/src/main/frontend/test-results/.last-run.json new file mode 100644 index 00000000..cbcc1fba --- /dev/null +++ b/src/main/frontend/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "passed", + "failedTests": [] +} \ No newline at end of file diff --git a/src/main/frontend/test-results/.playwright-artifacts-16/page@333818398abd7970554a5643c2e3eeb2.webm b/src/main/frontend/test-results/.playwright-artifacts-16/page@333818398abd7970554a5643c2e3eeb2.webm deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/frontend/test-results/.playwright-artifacts-17/page@1643517e96d910eac709a898d06616e6.webm b/src/main/frontend/test-results/.playwright-artifacts-17/page@1643517e96d910eac709a898d06616e6.webm deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/error-context.md deleted file mode 100644 index d7cc8b9a..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/error-context.md +++ /dev/null @@ -1,134 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> accessibility: tree has correct ARIA roles -- Location: tests/e2e/file-tree.spec.ts:252:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { -> 253 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/video.webm deleted file mode 100644 index 78cda285..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-0093f-tree-has-correct-ARIA-roles-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/error-context.md deleted file mode 100644 index e494b664..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> clearing file filter on graph view removes badge -- Location: tests/e2e/file-tree.spec.ts:236:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { -> 237 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/video.webm deleted file mode 100644 index 77657540..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-0398c-on-graph-view-removes-badge-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/error-context.md deleted file mode 100644 index f1c3bf43..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/error-context.md +++ /dev/null @@ -1,185 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> keyboard navigation: ArrowDown moves focus -- Location: tests/e2e/file-tree.spec.ts:201:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { -> 202 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/video.webm deleted file mode 100644 index b1064e04..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-2bb63-ation-ArrowDown-moves-focus-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/error-context.md deleted file mode 100644 index 8f5f130a..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/error-context.md +++ /dev/null @@ -1,173 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> keyboard navigation: Enter selects and navigates -- Location: tests/e2e/file-tree.spec.ts:213:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { -> 214 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/video.webm deleted file mode 100644 index 75016b4b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-36508-Enter-selects-and-navigates-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/error-context.md deleted file mode 100644 index 1a22a3b9..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/error-context.md +++ /dev/null @@ -1,160 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> hides file tree when sidebar is collapsed -- Location: tests/e2e/file-tree.spec.ts:226:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { -> 227 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/video.webm deleted file mode 100644 index b5268646..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-50ba7-e-when-sidebar-is-collapsed-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/error-context.md deleted file mode 100644 index f2e736ec..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/error-context.md +++ /dev/null @@ -1,226 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> collapses expanded directory on second click -- Location: tests/e2e/file-tree.spec.ts:132:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 33 | type: 'file', - 34 | nodeCount: 3, - 35 | }, - 36 | ], - 37 | }, - 38 | { - 39 | name: 'components', - 40 | path: 'src/components', - 41 | type: 'directory', - 42 | nodeCount: 20, - 43 | children: [ - 44 | { - 45 | name: 'Button.tsx', - 46 | path: 'src/components/Button.tsx', - 47 | type: 'file', - 48 | nodeCount: 5, - 49 | }, - 50 | ], - 51 | }, - 52 | ], - 53 | }, - 54 | { - 55 | name: 'pom.xml', - 56 | path: 'pom.xml', - 57 | type: 'file', - 58 | nodeCount: 2, - 59 | }, - 60 | ], - 61 | }; - 62 | - 63 | async function mockFileTree(page: Page) { - 64 | await page.route('**/api/file-tree**', route => - 65 | route.fulfill({ - 66 | status: 200, - 67 | contentType: 'application/json', - 68 | body: JSON.stringify(MOCK_FILE_TREE), - 69 | }), - 70 | ); - 71 | } - 72 | - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { -> 133 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/video.webm deleted file mode 100644 index 58323b59..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-58a5d-d-directory-on-second-click-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/error-context.md deleted file mode 100644 index 68d29979..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/error-context.md +++ /dev/null @@ -1,226 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> shows root directory expanded by default -- Location: tests/e2e/file-tree.spec.ts:112:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 13 | name: 'src', - 14 | path: 'src', - 15 | type: 'directory', - 16 | nodeCount: 80, - 17 | children: [ - 18 | { - 19 | name: 'main', - 20 | path: 'src/main', - 21 | type: 'directory', - 22 | nodeCount: 60, - 23 | children: [ - 24 | { - 25 | name: 'App.tsx', - 26 | path: 'src/main/App.tsx', - 27 | type: 'file', - 28 | nodeCount: 12, - 29 | }, - 30 | { - 31 | name: 'index.ts', - 32 | path: 'src/main/index.ts', - 33 | type: 'file', - 34 | nodeCount: 3, - 35 | }, - 36 | ], - 37 | }, - 38 | { - 39 | name: 'components', - 40 | path: 'src/components', - 41 | type: 'directory', - 42 | nodeCount: 20, - 43 | children: [ - 44 | { - 45 | name: 'Button.tsx', - 46 | path: 'src/components/Button.tsx', - 47 | type: 'file', - 48 | nodeCount: 5, - 49 | }, - 50 | ], - 51 | }, - 52 | ], - 53 | }, - 54 | { - 55 | name: 'pom.xml', - 56 | path: 'pom.xml', - 57 | type: 'file', - 58 | nodeCount: 2, - 59 | }, - 60 | ], - 61 | }; - 62 | - 63 | async function mockFileTree(page: Page) { - 64 | await page.route('**/api/file-tree**', route => - 65 | route.fulfill({ - 66 | status: 200, - 67 | contentType: 'application/json', - 68 | body: JSON.stringify(MOCK_FILE_TREE), - 69 | }), - 70 | ); - 71 | } - 72 | - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { -> 113 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/video.webm deleted file mode 100644 index 137d74f2..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-9e7a2-rectory-expanded-by-default-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/error-context.md deleted file mode 100644 index d08c3d34..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/error-context.md +++ /dev/null @@ -1,214 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> shows "no match" message for unmatched query -- Location: tests/e2e/file-tree.spec.ts:172:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { -> 173 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/video.webm deleted file mode 100644 index 32cb7323..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-a07f8-message-for-unmatched-query-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/error-context.md deleted file mode 100644 index a1c7cf94..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/error-context.md +++ /dev/null @@ -1,197 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> navigates to graph view on file click -- Location: tests/e2e/file-tree.spec.ts:189:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { -> 190 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/video.webm deleted file mode 100644 index 25687c76..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tre-b3d13-to-graph-view-on-file-click-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/error-context.md deleted file mode 100644 index bd7f5f96..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/error-context.md +++ /dev/null @@ -1,225 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> clears search with X button -- Location: tests/e2e/file-tree.spec.ts:161:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 62 | - 63 | async function mockFileTree(page: Page) { - 64 | await page.route('**/api/file-tree**', route => - 65 | route.fulfill({ - 66 | status: 200, - 67 | contentType: 'application/json', - 68 | body: JSON.stringify(MOCK_FILE_TREE), - 69 | }), - 70 | ); - 71 | } - 72 | - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { -> 162 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/video.webm deleted file mode 100644 index 40b06050..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-clears-search-with-X-button-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/error-context.md deleted file mode 100644 index 7ec52912..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/error-context.md +++ /dev/null @@ -1,226 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> expands directory on click -- Location: tests/e2e/file-tree.spec.ts:121:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 22 | nodeCount: 60, - 23 | children: [ - 24 | { - 25 | name: 'App.tsx', - 26 | path: 'src/main/App.tsx', - 27 | type: 'file', - 28 | nodeCount: 12, - 29 | }, - 30 | { - 31 | name: 'index.ts', - 32 | path: 'src/main/index.ts', - 33 | type: 'file', - 34 | nodeCount: 3, - 35 | }, - 36 | ], - 37 | }, - 38 | { - 39 | name: 'components', - 40 | path: 'src/components', - 41 | type: 'directory', - 42 | nodeCount: 20, - 43 | children: [ - 44 | { - 45 | name: 'Button.tsx', - 46 | path: 'src/components/Button.tsx', - 47 | type: 'file', - 48 | nodeCount: 5, - 49 | }, - 50 | ], - 51 | }, - 52 | ], - 53 | }, - 54 | { - 55 | name: 'pom.xml', - 56 | path: 'pom.xml', - 57 | type: 'file', - 58 | nodeCount: 2, - 59 | }, - 60 | ], - 61 | }; - 62 | - 63 | async function mockFileTree(page: Page) { - 64 | await page.route('**/api/file-tree**', route => - 65 | route.fulfill({ - 66 | status: 200, - 67 | contentType: 'application/json', - 68 | body: JSON.stringify(MOCK_FILE_TREE), - 69 | }), - 70 | ); - 71 | } - 72 | - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { -> 122 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/video.webm deleted file mode 100644 index 9be5193c..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-expands-directory-on-click-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/error-context.md deleted file mode 100644 index 19b95889..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/error-context.md +++ /dev/null @@ -1,226 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> filters tree with search input -- Location: tests/e2e/file-tree.spec.ts:145:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 46 | path: 'src/components/Button.tsx', - 47 | type: 'file', - 48 | nodeCount: 5, - 49 | }, - 50 | ], - 51 | }, - 52 | ], - 53 | }, - 54 | { - 55 | name: 'pom.xml', - 56 | path: 'pom.xml', - 57 | type: 'file', - 58 | nodeCount: 2, - 59 | }, - 60 | ], - 61 | }; - 62 | - 63 | async function mockFileTree(page: Page) { - 64 | await page.route('**/api/file-tree**', route => - 65 | route.fulfill({ - 66 | status: 200, - 67 | contentType: 'application/json', - 68 | body: JSON.stringify(MOCK_FILE_TREE), - 69 | }), - 70 | ); - 71 | } - 72 | - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { -> 146 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/video.webm deleted file mode 100644 index 0b489a3b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-filters-tree-with-search-input-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/error-context.md deleted file mode 100644 index 70569476..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/error-context.md +++ /dev/null @@ -1,226 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> renders file tree in sidebar -- Location: tests/e2e/file-tree.spec.ts:105:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 6 | const MOCK_FILE_TREE = { - 7 | name: 'root', - 8 | path: '', - 9 | type: 'directory', - 10 | nodeCount: 100, - 11 | children: [ - 12 | { - 13 | name: 'src', - 14 | path: 'src', - 15 | type: 'directory', - 16 | nodeCount: 80, - 17 | children: [ - 18 | { - 19 | name: 'main', - 20 | path: 'src/main', - 21 | type: 'directory', - 22 | nodeCount: 60, - 23 | children: [ - 24 | { - 25 | name: 'App.tsx', - 26 | path: 'src/main/App.tsx', - 27 | type: 'file', - 28 | nodeCount: 12, - 29 | }, - 30 | { - 31 | name: 'index.ts', - 32 | path: 'src/main/index.ts', - 33 | type: 'file', - 34 | nodeCount: 3, - 35 | }, - 36 | ], - 37 | }, - 38 | { - 39 | name: 'components', - 40 | path: 'src/components', - 41 | type: 'directory', - 42 | nodeCount: 20, - 43 | children: [ - 44 | { - 45 | name: 'Button.tsx', - 46 | path: 'src/components/Button.tsx', - 47 | type: 'file', - 48 | nodeCount: 5, - 49 | }, - 50 | ], - 51 | }, - 52 | ], - 53 | }, - 54 | { - 55 | name: 'pom.xml', - 56 | path: 'pom.xml', - 57 | type: 'file', - 58 | nodeCount: 2, - 59 | }, - 60 | ], - 61 | }; - 62 | - 63 | async function mockFileTree(page: Page) { - 64 | await page.route('**/api/file-tree**', route => - 65 | route.fulfill({ - 66 | status: 200, - 67 | contentType: 'application/json', - 68 | body: JSON.stringify(MOCK_FILE_TREE), - 69 | }), - 70 | ); - 71 | } - 72 | - 73 | async function mockMinimalApis(page: Page) { - 74 | await page.route('**/api/stats**', route => - 75 | route.fulfill({ - 76 | status: 200, - 77 | contentType: 'application/json', - 78 | body: JSON.stringify({ node_count: 100, edge_count: 200, nodes_by_kind: {}, nodes_by_layer: {} }), - 79 | }), - 80 | ); - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { -> 106 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { - 181 | await page.goto('/'); - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/video.webm deleted file mode 100644 index 189bb8f8..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-renders-file-tree-in-sidebar-chromium/video.webm and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/error-context.md b/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/error-context.md deleted file mode 100644 index ca62cb23..00000000 --- a/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/error-context.md +++ /dev/null @@ -1,206 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: file-tree.spec.ts >> Project File Tree >> shows node count badges -- Location: tests/e2e/file-tree.spec.ts:180:3 - -# Error details - -``` -Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ -Call log: - - navigating to "http://localhost:8080/", waiting until "load" - -``` - -# Test source - -```ts - 81 | await page.route('**/api/kinds**', route => - 82 | route.fulfill({ - 83 | status: 200, - 84 | contentType: 'application/json', - 85 | body: JSON.stringify({ kinds: [{ kind: 'class', count: 50 }, { kind: 'method', count: 50 }], total: 100 }), - 86 | }), - 87 | ); - 88 | await page.route('**/api/nodes**', route => - 89 | route.fulfill({ - 90 | status: 200, - 91 | contentType: 'application/json', - 92 | body: JSON.stringify({ nodes: [], total: 0, offset: 0, limit: 50 }), - 93 | }), - 94 | ); - 95 | } - 96 | - 97 | /* ── Tests ────────────────────────────────────────────────────────── */ - 98 | - 99 | test.describe('Project File Tree', () => { - 100 | test.beforeEach(async ({ page }) => { - 101 | await mockFileTree(page); - 102 | await mockMinimalApis(page); - 103 | }); - 104 | - 105 | test('renders file tree in sidebar', async ({ page }) => { - 106 | await page.goto('/'); - 107 | await page.waitForSelector('[aria-label="Project file tree"]'); - 108 | await expect(page.getByText('Project Files')).toBeVisible(); - 109 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 110 | }); - 111 | - 112 | test('shows root directory expanded by default', async ({ page }) => { - 113 | await page.goto('/'); - 114 | await page.waitForSelector('[aria-label="Project file tree"]'); - 115 | // src directory should be visible (root auto-expanded) - 116 | await expect(page.getByText('src')).toBeVisible(); - 117 | // pom.xml should be visible (top-level file) - 118 | await expect(page.getByText('pom.xml')).toBeVisible(); - 119 | }); - 120 | - 121 | test('expands directory on click', async ({ page }) => { - 122 | await page.goto('/'); - 123 | await page.waitForSelector('[aria-label="Project file tree"]'); - 124 | - 125 | // 'main' subdirectory is inside 'src' which is inside root - 126 | // Click 'src' to expand it - 127 | await page.getByText('src').click(); - 128 | await expect(page.getByText('main')).toBeVisible(); - 129 | await expect(page.getByText('components')).toBeVisible(); - 130 | }); - 131 | - 132 | test('collapses expanded directory on second click', async ({ page }) => { - 133 | await page.goto('/'); - 134 | await page.waitForSelector('[aria-label="Project file tree"]'); - 135 | - 136 | // Click src to expand - 137 | await page.getByText('src').click(); - 138 | await expect(page.getByText('main')).toBeVisible(); - 139 | - 140 | // Click src again to collapse - 141 | await page.getByText('src').click(); - 142 | await expect(page.getByText('main')).not.toBeVisible(); - 143 | }); - 144 | - 145 | test('filters tree with search input', async ({ page }) => { - 146 | await page.goto('/'); - 147 | await page.waitForSelector('[aria-label="Project file tree"]'); - 148 | - 149 | // Expand src to make files visible - 150 | await page.getByText('src').click(); - 151 | await page.getByText('main').click(); - 152 | - 153 | const searchInput = page.getByPlaceholder('Filter files…'); - 154 | await searchInput.fill('App'); - 155 | - 156 | // Only App.tsx should be visible, not index.ts - 157 | await expect(page.getByText('App.tsx')).toBeVisible(); - 158 | await expect(page.getByText('index.ts')).not.toBeVisible(); - 159 | }); - 160 | - 161 | test('clears search with X button', async ({ page }) => { - 162 | await page.goto('/'); - 163 | await page.waitForSelector('[aria-label="Project file tree"]'); - 164 | - 165 | const searchInput = page.getByPlaceholder('Filter files…'); - 166 | await searchInput.fill('App'); - 167 | - 168 | await page.getByRole('button', { name: 'Clear filter' }).click(); - 169 | await expect(searchInput).toHaveValue(''); - 170 | }); - 171 | - 172 | test('shows "no match" message for unmatched query', async ({ page }) => { - 173 | await page.goto('/'); - 174 | await page.waitForSelector('[aria-label="Project file tree"]'); - 175 | - 176 | await page.getByPlaceholder('Filter files…').fill('xyznotfound'); - 177 | await expect(page.getByText(/No files match/)).toBeVisible(); - 178 | }); - 179 | - 180 | test('shows node count badges', async ({ page }) => { -> 181 | await page.goto('/'); - | ^ Error: page.goto: net::ERR_CONNECTION_REFUSED at http://localhost:8080/ - 182 | await page.waitForSelector('[aria-label="Project file tree"]'); - 183 | - 184 | // pom.xml has nodeCount: 2, should show badge - 185 | const pomRow = page.getByTestId('tree-node-pom.xml'); - 186 | await expect(pomRow).toContainText('2'); - 187 | }); - 188 | - 189 | test('navigates to graph view on file click', async ({ page }) => { - 190 | await page.goto('/'); - 191 | await page.waitForSelector('[aria-label="Project file tree"]'); - 192 | - 193 | // Click pom.xml (visible at root level) - 194 | await page.getByTestId('tree-node-pom.xml').click(); - 195 | - 196 | await expect(page).toHaveURL(/\/graph/); - 197 | await expect(page.getByTestId('file-filter-badge')).toBeVisible(); - 198 | await expect(page.getByTestId('file-filter-badge')).toContainText('pom.xml'); - 199 | }); - 200 | - 201 | test('keyboard navigation: ArrowDown moves focus', async ({ page }) => { - 202 | await page.goto('/'); - 203 | await page.waitForSelector('[aria-label="Project file tree"]'); - 204 | - 205 | // Focus the tree - 206 | const tree = page.getByRole('tree', { name: 'Project file tree' }); - 207 | await tree.press('ArrowDown'); - 208 | // Check that a treeitem receives focus - 209 | const focused = page.locator('[role="treeitem"]:focus'); - 210 | await expect(focused).toHaveCount(1); - 211 | }); - 212 | - 213 | test('keyboard navigation: Enter selects and navigates', async ({ page }) => { - 214 | await page.goto('/'); - 215 | await page.waitForSelector('[aria-label="Project file tree"]'); - 216 | - 217 | // Focus first treeitem and press Enter - 218 | const firstItem = page.locator('[role="treeitem"]').first(); - 219 | await firstItem.focus(); - 220 | await firstItem.press('Enter'); - 221 | - 222 | // Should navigate to /graph - 223 | await expect(page).toHaveURL(/\/graph/); - 224 | }); - 225 | - 226 | test('hides file tree when sidebar is collapsed', async ({ page }) => { - 227 | await page.goto('/'); - 228 | await page.waitForSelector('[aria-label="Project file tree"]'); - 229 | - 230 | // Collapse the sidebar - 231 | await page.getByRole('button', { name: /collapse sidebar/i }).click(); - 232 | - 233 | await expect(page.getByRole('tree', { name: 'Project file tree' })).not.toBeVisible(); - 234 | }); - 235 | - 236 | test('clearing file filter on graph view removes badge', async ({ page }) => { - 237 | await page.goto('/'); - 238 | await page.waitForSelector('[aria-label="Project file tree"]'); - 239 | - 240 | // Navigate via file click - 241 | await page.getByTestId('tree-node-pom.xml').click(); - 242 | await expect(page).toHaveURL(/\/graph/); - 243 | - 244 | const badge = page.getByTestId('file-filter-badge'); - 245 | await expect(badge).toBeVisible(); - 246 | - 247 | // Click X on the badge - 248 | await page.getByRole('button', { name: 'Clear file filter' }).click(); - 249 | await expect(badge).not.toBeVisible(); - 250 | }); - 251 | - 252 | test('accessibility: tree has correct ARIA roles', async ({ page }) => { - 253 | await page.goto('/'); - 254 | await page.waitForSelector('[aria-label="Project file tree"]'); - 255 | - 256 | await expect(page.getByRole('tree', { name: 'Project file tree' })).toBeVisible(); - 257 | const items = page.locator('[role="treeitem"]'); - 258 | await expect(items).not.toHaveCount(0); - 259 | }); - 260 | }); - 261 | -``` \ No newline at end of file diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/test-failed-1.png b/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/test-failed-1.png deleted file mode 100644 index 3ddab50b..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/test-failed-1.png and /dev/null differ diff --git a/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/video.webm b/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/video.webm deleted file mode 100644 index 6a7273b9..00000000 Binary files a/src/main/frontend/test-results/file-tree-Project-File-Tree-shows-node-count-badges-chromium/video.webm and /dev/null differ diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/error-context.md" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/error-context.md" deleted file mode 100644 index 7008b94d..00000000 --- "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/error-context.md" +++ /dev/null @@ -1,189 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: graph.spec.ts >> Graph view — initial render >> controls toolbar is visible -- Location: tests/e2e/graph.spec.ts:30:3 - -# Error details - -``` -Test timeout of 30000ms exceeded while running "beforeEach" hook. -``` - -``` -TimeoutError: page.waitForSelector: Timeout 30000ms exceeded. -Call log: - - waiting for locator('main') to be visible - -``` - -# Test source - -```ts - 1 | /// - 2 | import { type Page, expect } from '@playwright/test'; - 3 | import { readFileSync } from 'node:fs'; - 4 | import { resolve } from 'node:path'; - 5 | - 6 | // ── Route helpers ──────────────────────────────────────────────────────────── - 7 | - 8 | export const ROUTES = { - 9 | dashboard: '/', - 10 | graph: '/graph', - 11 | explorer: '/explorer', - 12 | console: '/console', - 13 | apiDocs: '/api-docs', - 14 | } as const; - 15 | - 16 | export type AppRoute = (typeof ROUTES)[keyof typeof ROUTES]; - 17 | - 18 | /** - 19 | * Intercept the HTML shell served by Spring Boot and replace it with the - 20 | * current on-disk version. The running JAR may contain a stale index.html - 21 | * (built before the last frontend rebuild), causing it to load an old - 22 | * JS bundle that crashes before React mounts. - 23 | * - 24 | * Bug: STALE_BUNDLE — tracked in RAN-80 (filed separately). - 25 | */ - 26 | export async function patchIndexHtml(page: Page) { - 27 | // process.cwd() is the frontend dir when running `npx playwright test` - 28 | const diskHtml = readFileSync( - 29 | resolve(process.cwd(), '../resources/static/index.html'), - 30 | 'utf-8', - 31 | ); - 32 | // Intercept the SPA shell route (all navigation routes return the same HTML) - 33 | await page.route('**/*', async (route) => { - 34 | const req = route.request(); - 35 | const url = req.url(); - 36 | // Only intercept HTML document requests (the SPA shell), not API/asset calls - 37 | if ( - 38 | req.resourceType() === 'document' && - 39 | !url.includes('/api/') && - 40 | !url.includes('/assets/') && - 41 | !url.includes('/swagger') && - 42 | !url.includes('/v3/') - 43 | ) { - 44 | await route.fulfill({ - 45 | status: 200, - 46 | contentType: 'text/html', - 47 | body: diskHtml, - 48 | }); - 49 | } else { - 50 | await route.continue(); - 51 | } - 52 | }); - 53 | } - 54 | - 55 | /** Navigate to a route and wait for the main content area to be visible. */ - 56 | export async function gotoRoute(page: Page, route: AppRoute) { - 57 | await patchIndexHtml(page); - 58 | await page.goto(route); - 59 | // Wait for React to hydrate (main rendered by Layout component) -> 60 | await page.waitForSelector('main', { state: 'visible', timeout: 30000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 30000ms exceeded. - 61 | } - 62 | - 63 | // ── Theme helpers ──────────────────────────────────────────────────────────── - 64 | - 65 | /** Returns the current theme: 'dark' | 'light'. */ - 66 | export async function getTheme(page: Page): Promise<'dark' | 'light'> { - 67 | const cls = await page.locator('html').getAttribute('class') ?? ''; - 68 | return cls.includes('dark') ? 'dark' : 'light'; - 69 | } - 70 | - 71 | /** Click the theme toggle and wait for the class to flip. */ - 72 | export async function toggleTheme(page: Page) { - 73 | const before = await getTheme(page); - 74 | // Theme toggle button — uses aria-label or data-testid set by the component - 75 | await page.getByRole('button', { name: /toggle theme|switch theme|dark mode|light mode/i }).click(); - 76 | await expect(page.locator('html')).toHaveClass(before === 'dark' ? /light/ : /dark/, { timeout: 2000 }); - 77 | } - 78 | - 79 | // ── API mock helpers ───────────────────────────────────────────────────────── - 80 | - 81 | /** Seed the `/api/stats` mock for deterministic dashboard tests. */ - 82 | export async function mockStats(page: Page, nodeCount = 1234, edgeCount = 5678) { - 83 | await page.route('**/api/stats', route => - 84 | route.fulfill({ - 85 | status: 200, - 86 | contentType: 'application/json', - 87 | body: JSON.stringify({ - 88 | totalNodes: nodeCount, - 89 | totalEdges: edgeCount, - 90 | nodesByKind: { endpoint: 10, class: 20, method: 30 }, - 91 | edgesByKind: { calls: 100, depends_on: 50 }, - 92 | languages: { java: 500, typescript: 200 }, - 93 | frameworks: { spring_boot: 300 }, - 94 | layers: { backend: 600, frontend: 200, infra: 100, shared: 50, unknown: 284 }, - 95 | }), - 96 | }) - 97 | ); - 98 | } - 99 | - 100 | /** - 101 | * Generate a synthetic node list for performance/stress tests. - 102 | * Returns a NodesListResponse-shaped object. - 103 | */ - 104 | export function generateNodeList(count: number) { - 105 | const nodes = Array.from({ length: count }, (_, i) => ({ - 106 | id: `node:file${i % 100}.ts:class:Class${i}`, - 107 | kind: ['class', 'method', 'endpoint', 'entity', 'function'][i % 5], - 108 | name: `Symbol${i}`, - 109 | qualifiedName: `com.example.Symbol${i}`, - 110 | filePath: `src/file${i % 100}.ts`, - 111 | layer: 'backend', - 112 | framework: null, - 113 | properties: {}, - 114 | })); - 115 | return { nodes, total: count, offset: 0, limit: count }; - 116 | } - 117 | - 118 | /** Seed the `/api/kinds` + `/api/nodes` endpoints with synthetic data. */ - 119 | export async function mockGraphData(page: Page, nodeCount: number) { - 120 | const data = generateNodeList(nodeCount); - 121 | - 122 | await page.route('**/api/kinds', route => - 123 | route.fulfill({ - 124 | status: 200, - 125 | contentType: 'application/json', - 126 | body: JSON.stringify({ - 127 | kinds: [ - 128 | { kind: 'class', count: Math.floor(nodeCount * 0.3) }, - 129 | { kind: 'method', count: Math.floor(nodeCount * 0.3) }, - 130 | { kind: 'endpoint', count: Math.floor(nodeCount * 0.15) }, - 131 | { kind: 'entity', count: Math.floor(nodeCount * 0.15) }, - 132 | { kind: 'function', count: Math.floor(nodeCount * 0.1) }, - 133 | ], - 134 | }), - 135 | }) - 136 | ); - 137 | - 138 | await page.route('**/api/nodes**', route => - 139 | route.fulfill({ - 140 | status: 200, - 141 | contentType: 'application/json', - 142 | body: JSON.stringify(data), - 143 | }) - 144 | ); - 145 | - 146 | await page.route('**/api/topology', route => - 147 | route.fulfill({ - 148 | status: 200, - 149 | contentType: 'application/json', - 150 | body: JSON.stringify({ - 151 | services: [ - 152 | { name: 'api-service', nodeCount: Math.floor(nodeCount / 3), dependencies: ['db-service'] }, - 153 | { name: 'db-service', nodeCount: Math.floor(nodeCount / 3), dependencies: [] }, - 154 | { name: 'frontend-service', nodeCount: Math.floor(nodeCount / 3), dependencies: ['api-service'] }, - 155 | ], - 156 | }), - 157 | }) - 158 | ); - 159 | } - 160 | -``` \ No newline at end of file diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/test-failed-1.png" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/test-failed-1.png" deleted file mode 100644 index 3ddab50b..00000000 Binary files "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/test-failed-1.png" and /dev/null differ diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/video.webm" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/video.webm" deleted file mode 100644 index dac5f70b..00000000 Binary files "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-4a1c2-controls-toolbar-is-visible-chromium/video.webm" and /dev/null differ diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-9271c-mb-shows-Level-0-landscape--chromium/test-failed-1.png" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-9271c-mb-shows-Level-0-landscape--chromium/test-failed-1.png" deleted file mode 100644 index 3ddab50b..00000000 Binary files "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-9271c-mb-shows-Level-0-landscape--chromium/test-failed-1.png" and /dev/null differ diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/error-context.md" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/error-context.md" deleted file mode 100644 index 524e2b3e..00000000 --- "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/error-context.md" +++ /dev/null @@ -1,189 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: graph.spec.ts >> Graph view — initial render >> graph container is visible -- Location: tests/e2e/graph.spec.ts:26:3 - -# Error details - -``` -Test timeout of 30000ms exceeded while running "beforeEach" hook. -``` - -``` -TimeoutError: page.waitForSelector: Timeout 30000ms exceeded. -Call log: - - waiting for locator('main') to be visible - -``` - -# Test source - -```ts - 1 | /// - 2 | import { type Page, expect } from '@playwright/test'; - 3 | import { readFileSync } from 'node:fs'; - 4 | import { resolve } from 'node:path'; - 5 | - 6 | // ── Route helpers ──────────────────────────────────────────────────────────── - 7 | - 8 | export const ROUTES = { - 9 | dashboard: '/', - 10 | graph: '/graph', - 11 | explorer: '/explorer', - 12 | console: '/console', - 13 | apiDocs: '/api-docs', - 14 | } as const; - 15 | - 16 | export type AppRoute = (typeof ROUTES)[keyof typeof ROUTES]; - 17 | - 18 | /** - 19 | * Intercept the HTML shell served by Spring Boot and replace it with the - 20 | * current on-disk version. The running JAR may contain a stale index.html - 21 | * (built before the last frontend rebuild), causing it to load an old - 22 | * JS bundle that crashes before React mounts. - 23 | * - 24 | * Bug: STALE_BUNDLE — tracked in RAN-80 (filed separately). - 25 | */ - 26 | export async function patchIndexHtml(page: Page) { - 27 | // process.cwd() is the frontend dir when running `npx playwright test` - 28 | const diskHtml = readFileSync( - 29 | resolve(process.cwd(), '../resources/static/index.html'), - 30 | 'utf-8', - 31 | ); - 32 | // Intercept the SPA shell route (all navigation routes return the same HTML) - 33 | await page.route('**/*', async (route) => { - 34 | const req = route.request(); - 35 | const url = req.url(); - 36 | // Only intercept HTML document requests (the SPA shell), not API/asset calls - 37 | if ( - 38 | req.resourceType() === 'document' && - 39 | !url.includes('/api/') && - 40 | !url.includes('/assets/') && - 41 | !url.includes('/swagger') && - 42 | !url.includes('/v3/') - 43 | ) { - 44 | await route.fulfill({ - 45 | status: 200, - 46 | contentType: 'text/html', - 47 | body: diskHtml, - 48 | }); - 49 | } else { - 50 | await route.continue(); - 51 | } - 52 | }); - 53 | } - 54 | - 55 | /** Navigate to a route and wait for the main content area to be visible. */ - 56 | export async function gotoRoute(page: Page, route: AppRoute) { - 57 | await patchIndexHtml(page); - 58 | await page.goto(route); - 59 | // Wait for React to hydrate (main rendered by Layout component) -> 60 | await page.waitForSelector('main', { state: 'visible', timeout: 30000 }); - | ^ TimeoutError: page.waitForSelector: Timeout 30000ms exceeded. - 61 | } - 62 | - 63 | // ── Theme helpers ──────────────────────────────────────────────────────────── - 64 | - 65 | /** Returns the current theme: 'dark' | 'light'. */ - 66 | export async function getTheme(page: Page): Promise<'dark' | 'light'> { - 67 | const cls = await page.locator('html').getAttribute('class') ?? ''; - 68 | return cls.includes('dark') ? 'dark' : 'light'; - 69 | } - 70 | - 71 | /** Click the theme toggle and wait for the class to flip. */ - 72 | export async function toggleTheme(page: Page) { - 73 | const before = await getTheme(page); - 74 | // Theme toggle button — uses aria-label or data-testid set by the component - 75 | await page.getByRole('button', { name: /toggle theme|switch theme|dark mode|light mode/i }).click(); - 76 | await expect(page.locator('html')).toHaveClass(before === 'dark' ? /light/ : /dark/, { timeout: 2000 }); - 77 | } - 78 | - 79 | // ── API mock helpers ───────────────────────────────────────────────────────── - 80 | - 81 | /** Seed the `/api/stats` mock for deterministic dashboard tests. */ - 82 | export async function mockStats(page: Page, nodeCount = 1234, edgeCount = 5678) { - 83 | await page.route('**/api/stats', route => - 84 | route.fulfill({ - 85 | status: 200, - 86 | contentType: 'application/json', - 87 | body: JSON.stringify({ - 88 | totalNodes: nodeCount, - 89 | totalEdges: edgeCount, - 90 | nodesByKind: { endpoint: 10, class: 20, method: 30 }, - 91 | edgesByKind: { calls: 100, depends_on: 50 }, - 92 | languages: { java: 500, typescript: 200 }, - 93 | frameworks: { spring_boot: 300 }, - 94 | layers: { backend: 600, frontend: 200, infra: 100, shared: 50, unknown: 284 }, - 95 | }), - 96 | }) - 97 | ); - 98 | } - 99 | - 100 | /** - 101 | * Generate a synthetic node list for performance/stress tests. - 102 | * Returns a NodesListResponse-shaped object. - 103 | */ - 104 | export function generateNodeList(count: number) { - 105 | const nodes = Array.from({ length: count }, (_, i) => ({ - 106 | id: `node:file${i % 100}.ts:class:Class${i}`, - 107 | kind: ['class', 'method', 'endpoint', 'entity', 'function'][i % 5], - 108 | name: `Symbol${i}`, - 109 | qualifiedName: `com.example.Symbol${i}`, - 110 | filePath: `src/file${i % 100}.ts`, - 111 | layer: 'backend', - 112 | framework: null, - 113 | properties: {}, - 114 | })); - 115 | return { nodes, total: count, offset: 0, limit: count }; - 116 | } - 117 | - 118 | /** Seed the `/api/kinds` + `/api/nodes` endpoints with synthetic data. */ - 119 | export async function mockGraphData(page: Page, nodeCount: number) { - 120 | const data = generateNodeList(nodeCount); - 121 | - 122 | await page.route('**/api/kinds', route => - 123 | route.fulfill({ - 124 | status: 200, - 125 | contentType: 'application/json', - 126 | body: JSON.stringify({ - 127 | kinds: [ - 128 | { kind: 'class', count: Math.floor(nodeCount * 0.3) }, - 129 | { kind: 'method', count: Math.floor(nodeCount * 0.3) }, - 130 | { kind: 'endpoint', count: Math.floor(nodeCount * 0.15) }, - 131 | { kind: 'entity', count: Math.floor(nodeCount * 0.15) }, - 132 | { kind: 'function', count: Math.floor(nodeCount * 0.1) }, - 133 | ], - 134 | }), - 135 | }) - 136 | ); - 137 | - 138 | await page.route('**/api/nodes**', route => - 139 | route.fulfill({ - 140 | status: 200, - 141 | contentType: 'application/json', - 142 | body: JSON.stringify(data), - 143 | }) - 144 | ); - 145 | - 146 | await page.route('**/api/topology', route => - 147 | route.fulfill({ - 148 | status: 200, - 149 | contentType: 'application/json', - 150 | body: JSON.stringify({ - 151 | services: [ - 152 | { name: 'api-service', nodeCount: Math.floor(nodeCount / 3), dependencies: ['db-service'] }, - 153 | { name: 'db-service', nodeCount: Math.floor(nodeCount / 3), dependencies: [] }, - 154 | { name: 'frontend-service', nodeCount: Math.floor(nodeCount / 3), dependencies: ['api-service'] }, - 155 | ], - 156 | }), - 157 | }) - 158 | ); - 159 | } - 160 | -``` \ No newline at end of file diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/test-failed-1.png" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/test-failed-1.png" deleted file mode 100644 index 3ddab50b..00000000 Binary files "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/test-failed-1.png" and /dev/null differ diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/video.webm" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/video.webm" deleted file mode 100644 index 0a6f5b06..00000000 Binary files "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-graph-container-is-visible-chromium/video.webm" and /dev/null differ diff --git "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-minimap-is-visible-chromium/test-failed-1.png" "b/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-minimap-is-visible-chromium/test-failed-1.png" deleted file mode 100644 index 3ddab50b..00000000 Binary files "a/src/main/frontend/test-results/graph-Graph-view-\342\200\224-initial-render-minimap-is-visible-chromium/test-failed-1.png" and /dev/null differ