diff --git a/trace-viewer/.opencode/prompts/playwright-test-generator.md b/trace-viewer/.opencode/prompts/playwright-test-generator.md
new file mode 100644
index 0000000..08f1991
--- /dev/null
+++ b/trace-viewer/.opencode/prompts/playwright-test-generator.md
@@ -0,0 +1,53 @@
+You are a Playwright Test Generator, an expert in browser automation and end-to-end testing.
+Your specialty is creating robust, reliable Playwright tests that accurately simulate user interactions and validate
+application behavior.
+
+# For each test you generate
+- Obtain the test plan with all the steps and verification specification
+- Run the `generator_setup_page` tool to set up page for the scenario
+- For each step and verification in the scenario, do the following:
+ - Use Playwright tool to manually execute it in real-time.
+ - Use the step description as the intent for each Playwright tool call.
+- Retrieve generator log via `generator_read_log`
+- Immediately after reading the test log, invoke `generator_write_test` with the generated source code
+ - File should contain single test
+ - File name must be fs-friendly scenario name
+ - Test must be placed in a describe matching the top-level test plan item
+ - Test title must match the scenario name
+ - Includes a comment with the step text before each step execution. Do not duplicate comments if step requires
+ multiple actions.
+ - Always use best practices from the log when generating tests.
+
+
+ For following plan:
+
+ ```markdown file=specs/plan.md
+ ### 1. Adding New Todos
+ **Seed:** `tests/seed.spec.ts`
+
+ #### 1.1 Add Valid Todo
+ **Steps:**
+ 1. Click in the "What needs to be done?" input field
+
+ #### 1.2 Add Multiple Todos
+ ...
+ ```
+
+ Following file is generated:
+
+ ```ts file=add-valid-todo.spec.ts
+ // spec: specs/plan.md
+ // seed: tests/seed.spec.ts
+
+ test.describe('Adding New Todos', () => {
+ test('Add Valid Todo', async { page } => {
+ // 1. Click in the "What needs to be done?" input field
+ await page.click(...);
+
+ ...
+ });
+ });
+ ```
+
+
+Context: User wants to generate a test for the test plan item.
\ No newline at end of file
diff --git a/trace-viewer/.opencode/prompts/playwright-test-healer.md b/trace-viewer/.opencode/prompts/playwright-test-healer.md
new file mode 100644
index 0000000..70173c4
--- /dev/null
+++ b/trace-viewer/.opencode/prompts/playwright-test-healer.md
@@ -0,0 +1,37 @@
+You are the Playwright Test Healer, an expert test automation engineer specializing in debugging and
+resolving Playwright test failures. Your mission is to systematically identify, diagnose, and fix
+broken Playwright tests using a methodical approach.
+
+Your workflow:
+1. **Initial Execution**: Run all tests using `test_run` tool to identify failing tests
+2. **Debug failed tests**: For each failing test run `test_debug`.
+3. **Error Investigation**: When the test pauses on errors, use available Playwright MCP tools to:
+ - Examine the error details
+ - Capture page snapshot to understand the context
+ - Analyze selectors, timing issues, or assertion failures
+4. **Root Cause Analysis**: Determine the underlying cause of the failure by examining:
+ - Element selectors that may have changed
+ - Timing and synchronization issues
+ - Data dependencies or test environment problems
+ - Application changes that broke test assumptions
+5. **Code Remediation**: Edit the test code to address identified issues, focusing on:
+ - Updating selectors to match current application state
+ - Fixing assertions and expected values
+ - Improving test reliability and maintainability
+ - For inherently dynamic data, utilize regular expressions to produce resilient locators
+6. **Verification**: Restart the test after each fix to validate the changes
+7. **Iteration**: Repeat the investigation and fixing process until the test passes cleanly
+
+Key principles:
+- Be systematic and thorough in your debugging approach
+- Document your findings and reasoning for each fix
+- Prefer robust, maintainable solutions over quick hacks
+- Use Playwright best practices for reliable test automation
+- If multiple errors exist, fix them one at a time and retest
+- Provide clear explanations of what was broken and how you fixed it
+- You will continue this process until the test runs successfully without any failures or errors.
+- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme()
+ so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead
+ of the expected behavior.
+- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.
+- Never wait for networkidle or use other discouraged or deprecated apis
diff --git a/trace-viewer/.opencode/prompts/playwright-test-planner.md b/trace-viewer/.opencode/prompts/playwright-test-planner.md
new file mode 100644
index 0000000..59c50f1
--- /dev/null
+++ b/trace-viewer/.opencode/prompts/playwright-test-planner.md
@@ -0,0 +1,44 @@
+You are an expert web test planner with extensive experience in quality assurance, user experience testing, and test
+scenario design. Your expertise includes functional testing, edge case identification, and comprehensive test coverage
+planning.
+
+You will:
+
+1. **Navigate and Explore**
+ - Invoke the `planner_setup_page` tool once to set up page before using any other tools
+ - Explore the browser snapshot
+ - Do not take screenshots unless absolutely necessary
+ - Use `browser_*` tools to navigate and discover interface
+ - Thoroughly explore the interface, identifying all interactive elements, forms, navigation paths, and functionality
+
+2. **Analyze User Flows**
+ - Map out the primary user journeys and identify critical paths through the application
+ - Consider different user types and their typical behaviors
+
+3. **Design Comprehensive Scenarios**
+
+ Create detailed test scenarios that cover:
+ - Happy path scenarios (normal user behavior)
+ - Edge cases and boundary conditions
+ - Error handling and validation
+
+4. **Structure Test Plans**
+
+ Each scenario must include:
+ - Clear, descriptive title
+ - Detailed step-by-step instructions
+ - Expected outcomes where appropriate
+ - Assumptions about starting state (always assume blank/fresh state)
+ - Success criteria and failure conditions
+
+5. **Create Documentation**
+
+ Submit your test plan using `planner_save_plan` tool.
+
+**Quality Standards**:
+- Write steps that are specific enough for any tester to follow
+- Include negative testing scenarios
+- Ensure scenarios are independent and can be run in any order
+
+**Output Format**: Always save the complete test plan as a markdown file with clear headings, numbered steps, and
+professional formatting suitable for sharing with development and QA teams.
diff --git a/trace-viewer/opencode.json b/trace-viewer/opencode.json
new file mode 100644
index 0000000..a31a71c
--- /dev/null
+++ b/trace-viewer/opencode.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "https://opencode.ai/config.json",
+ "mcp": {
+ "playwright-test": {
+ "type": "local",
+ "command": [
+ "npx",
+ "playwright",
+ "run-test-mcp-server"
+ ],
+ "enabled": true
+ }
+ },
+ "tools": {
+ "playwright*": false
+ },
+ "agent": {
+ "playwright-test-generator": {
+ "description": "Use this agent when you need to create automated browser tests using Playwright",
+ "mode": "subagent",
+ "prompt": "{file:.opencode/prompts/playwright-test-generator.md}",
+ "tools": {
+ "ls": true,
+ "glob": true,
+ "grep": true,
+ "read": true,
+ "playwright-test*browser_click": true,
+ "playwright-test*browser_drag": true,
+ "playwright-test*browser_evaluate": true,
+ "playwright-test*browser_file_upload": true,
+ "playwright-test*browser_handle_dialog": true,
+ "playwright-test*browser_hover": true,
+ "playwright-test*browser_navigate": true,
+ "playwright-test*browser_press_key": true,
+ "playwright-test*browser_select_option": true,
+ "playwright-test*browser_snapshot": true,
+ "playwright-test*browser_type": true,
+ "playwright-test*browser_verify_element_visible": true,
+ "playwright-test*browser_verify_list_visible": true,
+ "playwright-test*browser_verify_text_visible": true,
+ "playwright-test*browser_verify_value": true,
+ "playwright-test*browser_wait_for": true,
+ "playwright-test*generator_read_log": true,
+ "playwright-test*generator_setup_page": true,
+ "playwright-test*generator_write_test": true
+ }
+ },
+ "playwright-test-healer": {
+ "description": "Use this agent when you need to debug and fix failing Playwright tests",
+ "mode": "subagent",
+ "prompt": "{file:.opencode/prompts/playwright-test-healer.md}",
+ "tools": {
+ "ls": true,
+ "glob": true,
+ "grep": true,
+ "read": true,
+ "edit": true,
+ "write": true,
+ "playwright-test*browser_console_messages": true,
+ "playwright-test*browser_evaluate": true,
+ "playwright-test*browser_generate_locator": true,
+ "playwright-test*browser_network_requests": true,
+ "playwright-test*browser_snapshot": true,
+ "playwright-test*test_debug": true,
+ "playwright-test*test_list": true,
+ "playwright-test*test_run": true
+ }
+ },
+ "playwright-test-planner": {
+ "description": "Use this agent when you need to create comprehensive test plan for a web application or website",
+ "mode": "subagent",
+ "prompt": "{file:.opencode/prompts/playwright-test-planner.md}",
+ "tools": {
+ "ls": true,
+ "glob": true,
+ "grep": true,
+ "read": true,
+ "playwright-test*browser_click": true,
+ "playwright-test*browser_close": true,
+ "playwright-test*browser_console_messages": true,
+ "playwright-test*browser_drag": true,
+ "playwright-test*browser_evaluate": true,
+ "playwright-test*browser_file_upload": true,
+ "playwright-test*browser_handle_dialog": true,
+ "playwright-test*browser_hover": true,
+ "playwright-test*browser_navigate": true,
+ "playwright-test*browser_navigate_back": true,
+ "playwright-test*browser_network_requests": true,
+ "playwright-test*browser_press_key": true,
+ "playwright-test*browser_select_option": true,
+ "playwright-test*browser_snapshot": true,
+ "playwright-test*browser_take_screenshot": true,
+ "playwright-test*browser_type": true,
+ "playwright-test*browser_wait_for": true,
+ "playwright-test*planner_setup_page": true,
+ "playwright-test*planner_save_plan": true
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/trace-viewer/specs/README.md b/trace-viewer/specs/README.md
new file mode 100644
index 0000000..48a788b
--- /dev/null
+++ b/trace-viewer/specs/README.md
@@ -0,0 +1,3 @@
+# Specs
+
+This is a directory for test plans.
diff --git a/trace-viewer/src/trace-viewer/components/SearchCombobox.tsx b/trace-viewer/src/trace-viewer/components/SearchCombobox.tsx
index c7c7617..96b7196 100644
--- a/trace-viewer/src/trace-viewer/components/SearchCombobox.tsx
+++ b/trace-viewer/src/trace-viewer/components/SearchCombobox.tsx
@@ -90,9 +90,9 @@ export function SearchCombobox(props: SearchComboboxProps) {
// Sync external query changes (e.g., when cleared externally)
createEffect(() => setLocalValue(props.query));
- // Auto-open when there's text and results, but allow manual close
+ // Auto-open when there's text (to show results or "No results found"), but allow manual close
createEffect(() => {
- if (localValue().length > 0 && props.results.length > 0) {
+ if (localValue().length > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
@@ -216,13 +216,15 @@ export function SearchCombobox(props: SearchComboboxProps) {
-
-
+
diff --git a/trace-viewer/test-e2e/search/basic-search.spec.ts b/trace-viewer/test-e2e/search/basic-search.spec.ts
new file mode 100644
index 0000000..527024b
--- /dev/null
+++ b/trace-viewer/test-e2e/search/basic-search.spec.ts
@@ -0,0 +1,548 @@
+// spec: tests/search/search-functionality.plan.md
+// seed: tests/search/seed.spec.ts
+
+import { expect, test, type APIRequestContext } from "@playwright/test";
+
+const TRACE_API_URL = "http://localhost:9295";
+
+// Helper to generate unique trace IDs
+function generateTraceId(suffix: string): string {
+ return `search${suffix}`.padEnd(32, "0").slice(0, 32);
+}
+
+// Shared trace data setup function
+async function setupTraceData(
+ request: APIRequestContext,
+ traceIdHex: string,
+ testStartTime: number,
+) {
+ const testEndTime = testStartTime + 3000; // 3 second test
+
+ // Send OTLP traces via POST /v1/traces
+ await request.post(`${TRACE_API_URL}/v1/traces`, {
+ data: {
+ resourceSpans: [
+ // Playwright test spans
+ {
+ resource: {
+ attributes: [
+ {
+ key: "service.name",
+ value: { stringValue: "playwright-tests" },
+ },
+ ],
+ },
+ scopeSpans: [
+ {
+ scope: { name: "playwright", version: "1.50.0" },
+ spans: [
+ // Test span (root)
+ {
+ traceId: traceIdHex,
+ spanId: "span00000001",
+ name: "playwright.test",
+ kind: 1, // INTERNAL
+ startTimeUnixNano: `${testStartTime}000000`,
+ endTimeUnixNano: `${testEndTime}000000`,
+ attributes: [
+ {
+ key: "test.case.title",
+ value: { stringValue: "Search functionality test" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ // Step: Navigate to login page
+ {
+ traceId: traceIdHex,
+ spanId: "span00000002",
+ parentSpanId: "span00000001",
+ name: "playwright.test.step",
+ kind: 1, // INTERNAL
+ startTimeUnixNano: `${testStartTime + 100}000000`,
+ endTimeUnixNano: `${testStartTime + 500}000000`,
+ attributes: [
+ {
+ key: "test.step.title",
+ value: { stringValue: "Navigate to login page" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ // Step: Fill login form
+ {
+ traceId: traceIdHex,
+ spanId: "span00000003",
+ parentSpanId: "span00000001",
+ name: "playwright.test.step",
+ kind: 1, // INTERNAL
+ startTimeUnixNano: `${testStartTime + 600}000000`,
+ endTimeUnixNano: `${testStartTime + 1200}000000`,
+ attributes: [
+ {
+ key: "test.step.title",
+ value: { stringValue: "Fill login form" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ // Step: Submit credentials
+ {
+ traceId: traceIdHex,
+ spanId: "span00000004",
+ parentSpanId: "span00000001",
+ name: "playwright.test.step",
+ kind: 1, // INTERNAL
+ startTimeUnixNano: `${testStartTime + 1300}000000`,
+ endTimeUnixNano: `${testStartTime + 1800}000000`,
+ attributes: [
+ {
+ key: "test.step.title",
+ value: { stringValue: "Submit credentials" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ ],
+ },
+ ],
+ },
+ // Browser spans with HTTP requests
+ {
+ resource: {
+ attributes: [
+ {
+ key: "service.name",
+ value: { stringValue: "playwright-browser" },
+ },
+ ],
+ },
+ scopeSpans: [
+ {
+ scope: { name: "playwright-browser", version: "1.0" },
+ spans: [
+ // HTTP GET to playwright.dev
+ {
+ traceId: traceIdHex,
+ spanId: "span00000010",
+ name: "HTTP GET",
+ kind: 3, // CLIENT
+ startTimeUnixNano: `${testStartTime + 150}000000`,
+ endTimeUnixNano: `${testStartTime + 400}000000`,
+ attributes: [
+ {
+ key: "http.request.method",
+ value: { stringValue: "GET" },
+ },
+ {
+ key: "server.address",
+ value: { stringValue: "playwright.dev" },
+ },
+ {
+ key: "url.full",
+ value: { stringValue: "https://playwright.dev/docs" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ // Another HTTP GET to playwright.dev
+ {
+ traceId: traceIdHex,
+ spanId: "span00000011",
+ name: "HTTP GET",
+ kind: 3, // CLIENT
+ startTimeUnixNano: `${testStartTime + 450}000000`,
+ endTimeUnixNano: `${testStartTime + 600}000000`,
+ attributes: [
+ {
+ key: "http.request.method",
+ value: { stringValue: "GET" },
+ },
+ {
+ key: "server.address",
+ value: { stringValue: "playwright.dev" },
+ },
+ {
+ key: "url.full",
+ value: { stringValue: "https://playwright.dev/api" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ // HTTP POST request
+ {
+ traceId: traceIdHex,
+ spanId: "span00000012",
+ name: "HTTP POST",
+ kind: 3, // CLIENT
+ startTimeUnixNano: `${testStartTime + 700}000000`,
+ endTimeUnixNano: `${testStartTime + 1100}000000`,
+ attributes: [
+ {
+ key: "http.request.method",
+ value: { stringValue: "POST" },
+ },
+ {
+ key: "server.address",
+ value: { stringValue: "api.example.com" },
+ },
+ {
+ key: "url.full",
+ value: { stringValue: "https://api.example.com/auth" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ // Another HTTP GET
+ {
+ traceId: traceIdHex,
+ spanId: "span00000013",
+ name: "HTTP GET",
+ kind: 3, // CLIENT
+ startTimeUnixNano: `${testStartTime + 1200}000000`,
+ endTimeUnixNano: `${testStartTime + 1500}000000`,
+ attributes: [
+ {
+ key: "http.request.method",
+ value: { stringValue: "GET" },
+ },
+ {
+ key: "server.address",
+ value: { stringValue: "cdn.example.com" },
+ },
+ {
+ key: "url.full",
+ value: { stringValue: "https://cdn.example.com/assets" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ ],
+ },
+ ],
+ },
+ // External API spans
+ {
+ resource: {
+ attributes: [
+ {
+ key: "service.name",
+ value: { stringValue: "api-service" },
+ },
+ ],
+ },
+ scopeSpans: [
+ {
+ scope: { name: "api", version: "1.0" },
+ spans: [
+ {
+ traceId: traceIdHex,
+ spanId: "span00000020",
+ name: "POST /api/auth",
+ kind: 2, // SERVER
+ startTimeUnixNano: `${testStartTime + 750}000000`,
+ endTimeUnixNano: `${testStartTime + 1050}000000`,
+ attributes: [
+ {
+ key: "http.route",
+ value: { stringValue: "/api/auth" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ {
+ traceId: traceIdHex,
+ spanId: "span00000021",
+ name: "DB query users",
+ kind: 1, // INTERNAL
+ startTimeUnixNano: `${testStartTime + 800}000000`,
+ endTimeUnixNano: `${testStartTime + 950}000000`,
+ attributes: [
+ {
+ key: "db.system",
+ value: { stringValue: "postgresql" },
+ },
+ ],
+ status: { code: 1 },
+ events: [],
+ links: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ });
+
+ // Send test.json via PUT
+ await request.put(`${TRACE_API_URL}/otel-playwright-reporter/test.json`, {
+ headers: {
+ "X-Trace-Id": traceIdHex,
+ },
+ data: {
+ name: "Search functionality test",
+ describes: ["Search", "Basic Search"],
+ file: "search/basic-search.spec.ts",
+ line: 10,
+ status: "passed",
+ traceId: traceIdHex,
+ startTimeUnixNano: `${testStartTime}000000`,
+ endTimeUnixNano: `${testEndTime}000000`,
+ },
+ });
+}
+
+// Helper to load trace in the viewer
+async function loadTrace(
+ page: import("@playwright/test").Page,
+ traceIdHex: string,
+) {
+ await page.goto("/");
+ await page
+ .getByTestId("api-url-input")
+ .fill(`${TRACE_API_URL}/otel-trace-viewer/${traceIdHex}`);
+ await page.getByTestId("load-api-button").click();
+
+ // Wait for trace to load
+ await expect(page.getByTestId("test-name")).toBeVisible();
+}
+
+test.describe("Basic Search Functionality", () => {
+ test.describe.configure({ mode: "serial" });
+
+ // Use a shared trace ID for all tests in this suite to avoid setup overhead
+ const traceIdHex = generateTraceId("basicsearch1");
+ let testStartTime: number;
+
+ test.beforeAll(async ({ request }) => {
+ testStartTime = Date.now();
+ await setupTraceData(request, traceIdHex, testStartTime);
+ });
+
+ test("1.1. should focus search input when clicking on search box", async ({
+ page,
+ }) => {
+ // 1. Load a trace with spans and external requests
+ await loadTrace(page, traceIdHex);
+
+ // Verify search combobox is visible in the header
+ const searchInput = page.getByPlaceholder("Search spans...");
+ await expect(searchInput).toBeVisible();
+
+ // Verify keyboard shortcut hint (/) is visible when not focused
+ const keyboardHint = page.locator("kbd").filter({ hasText: "/" });
+ await expect(keyboardHint).toBeVisible();
+
+ // 2. Click on the search combobox
+ await searchInput.click();
+
+ // 3. Verify the search input is focused
+ await expect(searchInput).toBeFocused();
+
+ // Verify keyboard shortcut hint (/) is hidden when focused
+ await expect(keyboardHint).not.toBeVisible();
+ });
+
+ test("1.2. should focus search input when pressing / key", async ({
+ page,
+ }) => {
+ // 1. Load a trace with spans
+ await loadTrace(page, traceIdHex);
+
+ const searchInput = page.getByPlaceholder("Search spans...");
+
+ // Verify search combobox shows / keyboard hint when not focused
+ const keyboardHint = page.locator("kbd").filter({ hasText: "/" });
+ await expect(keyboardHint).toBeVisible();
+
+ // 2. Click somewhere on the page to ensure search is not focused
+ await page.getByTestId("test-name").click();
+ await expect(searchInput).not.toBeFocused();
+
+ // 3. Press the / key
+ await page.keyboard.press("/");
+
+ // 4. Verify the search input is focused
+ await expect(searchInput).toBeFocused();
+
+ // The / character should NOT be typed into the input
+ await expect(searchInput).toHaveValue("");
+ });
+
+ test("1.3. should display search results when typing a query", async ({
+ page,
+ }) => {
+ // 1. Load a trace with HTTP GET spans
+ await loadTrace(page, traceIdHex);
+
+ const searchInput = page.getByPlaceholder("Search spans...");
+
+ // 2. Click on the search combobox
+ await searchInput.click();
+
+ // 3. Type 'GET' into the search input
+ await searchInput.fill("GET");
+
+ // 4. Wait for search results to appear (200ms debounce + rendering)
+ await page.waitForTimeout(300);
+
+ // Verify search results dropdown opens after typing
+ const dropdown = page.locator(
+ '[data-scope="combobox"][data-part="content"]',
+ );
+ await expect(dropdown).toBeVisible();
+
+ // Results show spans matching 'GET'
+ // Each result displays the matched text and parent span title
+ const resultItems = page.locator(
+ '[data-scope="combobox"][data-part="item"]',
+ );
+ await expect(resultItems.first()).toBeVisible();
+
+ // Verify results contain GET-related content
+ await expect(dropdown).toContainText("GET");
+ });
+
+ test("1.4. should show 'No results found' for queries with no matches", async ({
+ page,
+ }) => {
+ // 1. Load a trace with spans
+ await loadTrace(page, traceIdHex);
+
+ const searchInput = page.getByPlaceholder("Search spans...");
+
+ // 2. Click on the search combobox
+ await searchInput.click();
+
+ // 3. Type a non-existent query
+ await searchInput.fill("zzzznonexistent");
+
+ // 4. Wait for search to execute (200ms debounce)
+ await page.waitForTimeout(300);
+
+ // Verify dropdown shows 'No results found' message
+ const dropdown = page.locator(
+ '[data-scope="combobox"][data-part="content"]',
+ );
+ await expect(dropdown).toBeVisible();
+ await expect(dropdown).toContainText("No results found");
+
+ // No span results are displayed
+ const resultItems = page.locator(
+ '[data-scope="combobox"][data-part="item"]',
+ );
+ await expect(resultItems).toHaveCount(0);
+ });
+
+ test("1.5. should clear search when clicking the clear button", async ({
+ page,
+ }) => {
+ // 1. Load a trace with spans
+ await loadTrace(page, traceIdHex);
+
+ const searchInput = page.getByPlaceholder("Search spans...");
+
+ // 2. Type a search query
+ await searchInput.click();
+ await searchInput.fill("GET");
+
+ // Wait for results to appear
+ await page.waitForTimeout(300);
+ const dropdown = page.locator(
+ '[data-scope="combobox"][data-part="content"]',
+ );
+ await expect(dropdown).toBeVisible();
+
+ // Verify clear button appears when there is search text
+ const clearButton = page.getByTestId("search-clear-button");
+ await expect(clearButton).toBeVisible();
+
+ // 3. Click the clear (X) button
+ await clearButton.click();
+
+ // 4. Verify search is cleared
+ // Clicking clear button empties the search input
+ await expect(searchInput).toHaveValue("");
+
+ // Search results dropdown closes
+ await expect(dropdown).not.toBeVisible();
+ });
+
+ test("1.6. should close search dropdown when pressing Escape", async ({
+ page,
+ }) => {
+ // 1. Load a trace with spans
+ await loadTrace(page, traceIdHex);
+
+ const searchInput = page.getByPlaceholder("Search spans...");
+
+ // 2. Type a search query to open results dropdown
+ await searchInput.click();
+ await searchInput.fill("GET");
+
+ // Wait for results dropdown to appear
+ await page.waitForTimeout(300);
+ const dropdown = page.locator(
+ '[data-scope="combobox"][data-part="content"]',
+ );
+ await expect(dropdown).toBeVisible();
+
+ // 3. Press the Escape key
+ await page.keyboard.press("Escape");
+
+ // 4. Verify dropdown closes
+ await expect(dropdown).not.toBeVisible();
+
+ // Search text remains in the input
+ await expect(searchInput).toHaveValue("GET");
+ });
+
+ test("1.7. should highlight matched text in search results", async ({
+ page,
+ }) => {
+ // 1. Load a trace with spans
+ await loadTrace(page, traceIdHex);
+
+ const searchInput = page.getByPlaceholder("Search spans...");
+
+ // 2. Type a specific search query
+ await searchInput.click();
+ await searchInput.fill("playwright.dev");
+
+ // 3. Wait for search results
+ await page.waitForTimeout(300);
+
+ const dropdown = page.locator(
+ '[data-scope="combobox"][data-part="content"]',
+ );
+ await expect(dropdown).toBeVisible();
+
+ // Verify matched portions of text are highlighted with yellow background
+ // The highlighting uses class "bg-yellow-200 font-semibold"
+ const highlightedText = dropdown.locator(".bg-yellow-200.font-semibold");
+ await expect(highlightedText.first()).toBeVisible();
+
+ // Verify the highlighted text contains part of the search query
+ const highlightedContent = await highlightedText.first().textContent();
+ expect(highlightedContent?.toLowerCase()).toContain("playwright");
+ });
+});
diff --git a/trace-viewer/test-e2e/seed.spec.ts b/trace-viewer/test-e2e/seed.spec.ts
new file mode 100644
index 0000000..ef5ce4c
--- /dev/null
+++ b/trace-viewer/test-e2e/seed.spec.ts
@@ -0,0 +1,7 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Test group', () => {
+ test('seed', async ({ page }) => {
+ // generate code here.
+ });
+});
diff --git a/trace-viewer/tests/search/search-functionality.plan.md b/trace-viewer/tests/search/search-functionality.plan.md
new file mode 100644
index 0000000..c2f5c84
--- /dev/null
+++ b/trace-viewer/tests/search/search-functionality.plan.md
@@ -0,0 +1,573 @@
+# Trace Viewer Search Functionality Test Plan
+
+## Application Overview
+
+This test plan covers the search functionality within the Trace Viewer application. The search allows users to find spans by their names, attributes (key-value pairs), and other indexed fields like kind, title, and serviceName. The search uses fuzzy matching (uFuzzy) and displays results in a dropdown combobox. Tests cover basic search, attribute search, zoomed viewport states, result selection, and keyboard interactions.
+
+## Test Scenarios
+
+### 1. Basic Search Functionality
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 1.1. should focus search input when clicking on search box
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans and external requests
+ 2. Click on the search combobox
+ 3. Verify the search input is focused
+
+**Expected Results:**
+ - Search combobox is visible in the header
+ - Search input becomes active/focused after clicking
+ - Keyboard shortcut hint (/) is hidden when focused
+
+#### 1.2. should focus search input when pressing / key
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Click somewhere on the timeline to ensure search is not focused
+ 3. Press the / key
+ 4. Verify the search input is focused
+
+**Expected Results:**
+ - Search combobox shows / keyboard hint when not focused
+ - Pressing / focuses the search input
+ - The / character is NOT typed into the input
+
+#### 1.3. should display search results when typing a query
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with HTTP GET spans
+ 2. Click on the search combobox
+ 3. Type 'GET' into the search input
+ 4. Wait for search results to appear
+
+**Expected Results:**
+ - Search results dropdown opens after typing
+ - Results show spans matching 'GET'
+ - Each result displays the matched text and parent span title
+ - Results count is shown if results exceed 50 items
+
+#### 1.4. should show 'No results found' for queries with no matches
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Click on the search combobox
+ 3. Type a non-existent query like 'zzzznonexistent'
+ 4. Observe the dropdown
+
+**Expected Results:**
+ - Dropdown shows 'No results found' message
+ - No span results are displayed
+
+#### 1.5. should clear search when clicking the clear button
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Type a search query
+ 3. Click the clear (X) button that appears
+ 4. Verify search is cleared
+
+**Expected Results:**
+ - Clear button appears when there is search text
+ - Clicking clear button empties the search input
+ - Search results dropdown closes
+ - Details panel returns to default state
+
+#### 1.6. should close search dropdown when pressing Escape
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Type a search query to open results dropdown
+ 3. Press the Escape key
+ 4. Verify dropdown closes
+
+**Expected Results:**
+ - Search results dropdown closes on Escape
+ - Search text remains in the input
+ - Search input may remain focused or blur depending on behavior
+
+#### 1.7. should highlight matched text in search results
+
+**File:** `tests/search/basic-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Type a specific search query like 'playwright.dev'
+ 3. Observe the search results
+
+**Expected Results:**
+ - Matched portions of text are highlighted with yellow background
+ - Highlighting uses the fuzzy match ranges from uFuzzy
+
+### 2. Search for Attributes in Spans
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 2.1. should find spans by attribute value
+
+**File:** `tests/search/attribute-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with HTTP spans containing 'playwright.dev' in server.address
+ 2. Type 'playwright.dev' in the search
+ 3. Observe the search results
+
+**Expected Results:**
+ - Results show 'server.address: playwright.dev' format
+ - Each result shows the parent span title (HTTP GET)
+ - Multiple matches are returned for all spans with this attribute
+
+#### 2.2. should find spans by attribute key
+
+**File:** `tests/search/attribute-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans containing http.request.method attribute
+ 2. Type 'request method' in the search (fuzzy match)
+ 3. Observe the search results
+
+**Expected Results:**
+ - Results show spans with http.request.method attribute
+ - Fuzzy matching works for attribute keys with dots and underscores
+
+#### 2.3. should find spans by special fields (kind, name, title)
+
+**File:** `tests/search/attribute-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans of different kinds
+ 2. Type 'internal' in the search
+ 3. Observe the search results
+
+**Expected Results:**
+ - Results show spans where kind is 'internal'
+ - Results are displayed with just the value (not key: value format for special fields)
+
+#### 2.4. should find test step spans by step name
+
+**File:** `tests/search/attribute-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with test steps
+ 2. Type 'Navigate' in the search
+ 3. Observe the search results
+
+**Expected Results:**
+ - Results include test steps with 'Navigate' in their title
+ - Results show the full step title as matched text
+
+#### 2.5. should perform fuzzy matching for typos
+
+**File:** `tests/search/attribute-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Type a query with minor typos like 'playwrght' (missing 'i')
+ 3. Observe the search results
+
+**Expected Results:**
+ - Fuzzy matching finds results despite typo
+ - Results show spans containing 'playwright'
+
+#### 2.6. should limit results to maximum 50 and show count
+
+**File:** `tests/search/attribute-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with many spans (50+)
+ 2. Type a broad search query like 'GET'
+ 3. Scroll through results
+
+**Expected Results:**
+ - Maximum 50 results are displayed in the dropdown
+ - Footer shows 'Showing 50 of N results' when there are more than 50 matches
+
+### 3. Search in Zoomed In State
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 3.1. should search and find spans while zoomed into timeline
+
+**File:** `tests/search/zoomed-in-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Zoom into the timeline (click and drag to select a range)
+ 3. Perform a search for spans
+ 4. Verify search works regardless of zoom level
+
+**Expected Results:**
+ - Search returns all matching spans, not just visible ones
+ - Results include spans outside the current viewport
+ - Hovering results can show position indicator even if span is off-screen
+
+#### 3.2. should show hover indicator when result span is visible
+
+**File:** `tests/search/zoomed-in-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace and zoom into a specific time range
+ 2. Search for a span that exists within the zoomed range
+ 3. Hover over the search result
+
+**Expected Results:**
+ - Position indicator line appears at the span's start time
+ - Details panel shows the hovered span's details
+ - Span is highlighted in the timeline if visible
+
+#### 3.3. should handle search result hover when span is outside viewport
+
+**File:** `tests/search/zoomed-in-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace and zoom into a narrow time range at the beginning
+ 2. Search for a span that exists later in the trace (outside viewport)
+ 3. Hover over the search result
+
+**Expected Results:**
+ - Details panel shows the span information
+ - Position indicator may not be visible if span start is off-screen
+ - Timeline does not automatically scroll or pan to the span
+
+#### 3.4. should maintain search results after zooming
+
+**File:** `tests/search/zoomed-in-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace and perform a search
+ 2. While results are open, zoom the timeline
+ 3. Verify search results remain stable
+
+**Expected Results:**
+ - Search results remain displayed after zoom
+ - Results are not re-filtered based on viewport
+ - Selecting a result still works after zooming
+
+### 4. Search in Zoomed Out State
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 4.1. should search and find spans in zoomed out overview
+
+**File:** `tests/search/zoomed-out-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Double-click to reset zoom to full trace view
+ 3. Perform a search for spans
+
+**Expected Results:**
+ - Search returns all matching spans
+ - Full trace is visible in timeline
+ - Search works identically to zoomed in state
+
+#### 4.2. should show position indicator in full timeline view
+
+**File:** `tests/search/zoomed-out-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace and reset to full view (double-click)
+ 2. Search for a span
+ 3. Hover over a search result
+
+**Expected Results:**
+ - Position indicator line appears at span's start time
+ - Indicator is visible since full trace is in view
+ - Details panel shows span information
+
+#### 4.3. should find spans across entire trace duration
+
+**File:** `tests/search/zoomed-out-search.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans at beginning, middle, and end
+ 2. Reset to full view
+ 3. Search for a term that matches spans across the trace
+
+**Expected Results:**
+ - Results include spans from all parts of the trace
+ - Results are ordered by search relevance, not by time
+
+### 5. Selection of Search Results
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 5.1. should select span when clicking on search result
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Perform a search
+ 3. Click on a search result
+
+**Expected Results:**
+ - Search dropdown closes after selection
+ - Timeline position locks to the selected span's start time
+ - Position indicator changes from thin hover line to thick locked line
+ - Details panel shows the selected span's information
+ - Clear button appears in search input
+
+#### 5.2. should select span when pressing Enter on highlighted result
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Perform a search
+ 3. Use arrow keys to navigate to a result
+ 4. Press Enter to select
+
+**Expected Results:**
+ - Dropdown closes after Enter
+ - Selected span is locked in details panel
+ - Timeline shows locked position indicator
+
+#### 5.3. should navigate through results with arrow keys
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace with multiple matching spans
+ 2. Perform a search that returns multiple results
+ 3. Press ArrowDown to move through results
+ 4. Press ArrowUp to move backwards
+
+**Expected Results:**
+ - ArrowDown highlights next result
+ - ArrowUp highlights previous result
+ - Highlighted result shows hover state in details panel
+ - Position indicator moves as navigation changes
+
+#### 5.4. should update details panel when hovering search results
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Perform a search
+ 3. Hover over different search results
+
+**Expected Results:**
+ - Details panel updates to show hovered span
+ - Time indicator in header shows span's timestamp
+ - Screenshot (if visible) corresponds to hover time
+
+#### 5.5. should show span details with attributes on selection
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace with HTTP spans
+ 2. Search for a specific HTTP request
+ 3. Click to select the result
+
+**Expected Results:**
+ - Details panel shows span title and duration
+ - All attributes are displayed (http.request.method, url.full, etc.)
+ - Kind is displayed (client, internal, etc.)
+ - Time range is shown
+
+#### 5.6. should unlock position when clicking on timeline after selection
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace and select a search result (locking position)
+ 2. Click anywhere on the timeline
+ 3. Observe the position indicator behavior
+
+**Expected Results:**
+ - Locked position unlocks on timeline click
+ - Position indicator returns to thin hover line
+ - Details panel switches to hover mode
+
+#### 5.7. should unlock position when pressing Escape after selection
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace and select a search result (locking position)
+ 2. Press Escape key
+ 3. Observe the position indicator behavior
+
+**Expected Results:**
+ - Locked position unlocks on Escape
+ - Details panel returns to hover mode
+
+#### 5.8. should maintain locked state while hovering new search results
+
+**File:** `tests/search/result-selection.spec.ts`
+
+**Steps:**
+ 1. Load a trace and select a search result (locking position)
+ 2. Perform a new search without clearing
+ 3. Hover over new search results
+
+**Expected Results:**
+ - Locked position indicator remains visible (thick line)
+ - A secondary hover indicator appears for new hover
+ - Mode switches to 'search-override' showing both indicators
+ - Details panel shows hovered span while maintaining lock
+
+### 6. Search Edge Cases and Error Handling
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 6.1. should handle empty search gracefully
+
+**File:** `tests/search/edge-cases.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Focus the search input
+ 3. Submit empty search (just press Enter)
+
+**Expected Results:**
+ - No results are shown
+ - Application does not crash
+ - Dropdown remains closed
+
+#### 6.2. should handle special characters in search
+
+**File:** `tests/search/edge-cases.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Search for terms with special characters like '/' or ':' or '.'
+ 3. Observe search behavior
+
+**Expected Results:**
+ - Search handles special characters gracefully
+ - Results include spans with matching special characters
+ - No JavaScript errors occur
+
+#### 6.3. should debounce search input
+
+**File:** `tests/search/edge-cases.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Type rapidly into the search input
+ 3. Observe network/performance behavior
+
+**Expected Results:**
+ - Search is debounced (200ms delay before search executes)
+ - Typing quickly does not cause excessive re-renders
+ - Final search query is processed correctly
+
+#### 6.4. should handle very long search queries
+
+**File:** `tests/search/edge-cases.spec.ts`
+
+**Steps:**
+ 1. Load a trace with spans
+ 2. Type a very long search query (100+ characters)
+ 3. Observe application behavior
+
+**Expected Results:**
+ - Application handles long queries without crashing
+ - Search either finds results or shows 'no results'
+ - UI remains responsive
+
+#### 6.5. should handle search when no trace is loaded
+
+**File:** `tests/search/edge-cases.spec.ts`
+
+**Steps:**
+ 1. Navigate to application without loading a trace
+ 2. Attempt to use search if accessible
+
+**Expected Results:**
+ - Search is either disabled or handles gracefully
+ - No errors when trace data is unavailable
+
+#### 6.6. should preserve search text when switching between hover and locked modes
+
+**File:** `tests/search/edge-cases.spec.ts`
+
+**Steps:**
+ 1. Load a trace and perform a search
+ 2. Select a result (enters locked mode)
+ 3. Click on timeline (unlocks)
+ 4. Observe search input state
+
+**Expected Results:**
+ - Search text remains in input after mode changes
+ - Clear button remains visible if text is present
+ - Can immediately search again without re-typing
+
+### 7. Search Integration with Timeline Panels
+
+**Seed:** `tests/search/seed.spec.ts`
+
+#### 7.1. should highlight matching spans in Steps Timeline
+
+**File:** `tests/search/timeline-integration.spec.ts`
+
+**Steps:**
+ 1. Load a trace with test steps
+ 2. Search for a step name
+ 3. Observe the Steps Timeline panel
+
+**Expected Results:**
+ - Matching spans in Steps Timeline are visually highlighted
+ - Non-matching spans have reduced visibility
+ - Highlighting updates as search query changes
+
+#### 7.2. should highlight matching spans in External Spans panel
+
+**File:** `tests/search/timeline-integration.spec.ts`
+
+**Steps:**
+ 1. Load a trace with external HTTP spans
+ 2. Search for 'HTTP GET'
+ 3. Observe the External Spans panel
+
+**Expected Results:**
+ - Matching HTTP spans are highlighted
+ - Search highlighting works across all timeline panels
+ - Scrolling to see more spans maintains highlighting
+
+#### 7.3. should scroll details panel to focused span on search result hover
+
+**File:** `tests/search/timeline-integration.spec.ts`
+
+**Steps:**
+ 1. Load a trace with many spans
+ 2. Perform a search
+ 3. Hover over a result that corresponds to a span not currently in view
+
+**Expected Results:**
+ - Details panel scrolls to show the hovered span
+ - Span details card is visible after scroll
+ - Focused span has visual indicator (ring or border)
+
+#### 7.4. should navigate to parent span from search result
+
+**File:** `tests/search/timeline-integration.spec.ts`
+
+**Steps:**
+ 1. Load a trace with nested spans
+ 2. Search for a child span
+ 3. Select the result and observe details panel
+ 4. Click the 'Parent' navigation button
+
+**Expected Results:**
+ - Parent span details are shown after clicking parent button
+ - Position remains locked
+ - Navigation works within the locked state