From b6b2e23c41a354fc9de3568bcb1ae6636629523f Mon Sep 17 00:00:00 2001 From: Rafay K <82721722+rafaykhan-source@users.noreply.github.com> Date: Sun, 17 May 2026 13:39:45 -0400 Subject: [PATCH 1/3] feat(evaluation): add share button in prompt drawer --- .../cypress/e2e/evaluation-drawer-share.cy.ts | 192 ++++++++++++++++++ .../evaluation/EvaluationContext.tsx | 41 ++-- .../evaluation/eval-drawer-key.test.ts | 121 +++++++++++ .../components/evaluation/eval-drawer-key.ts | 63 ++++++ .../evaluation/ui/EvalSamplesDrawer.tsx | 47 ++++- .../evaluation/ui/EvaluationTable.tsx | 32 ++- .../src/components/ui/share-button.test.tsx | 8 +- .../app/src/components/ui/share-button.tsx | 14 +- packages/app/src/lib/url-state.ts | 7 + 9 files changed, 496 insertions(+), 29 deletions(-) create mode 100644 packages/app/cypress/e2e/evaluation-drawer-share.cy.ts create mode 100644 packages/app/src/components/evaluation/eval-drawer-key.test.ts create mode 100644 packages/app/src/components/evaluation/eval-drawer-key.ts diff --git a/packages/app/cypress/e2e/evaluation-drawer-share.cy.ts b/packages/app/cypress/e2e/evaluation-drawer-share.cy.ts new file mode 100644 index 00000000..1e64a006 --- /dev/null +++ b/packages/app/cypress/e2e/evaluation-drawer-share.cy.ts @@ -0,0 +1,192 @@ +/** + * E2E tests for the eval-samples drawer share-link feature. + * + * Coverage: + * - Share button is visible inside the drawer. + * - Opening the drawer mirrors e_drawer to the share URL. + * - Setting a filter / search also appears in the share URL. + * - Visiting with e_drawer + e_dfilter + e_dq in the URL re-opens the drawer + * with the correct row, filter chip active, and search pre-filled. + * - Missing e_drawer key → silent no-op (drawer stays closed). + */ + +const dismissModal = (win: Window) => { + win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); +}; + +/** Navigate to the evaluation page and wait for the table to be visible. */ +function visitEvalTable(queryString = '') { + cy.visit(`/evaluation${queryString}`, { onBeforeLoad: dismissModal }); + cy.get('[data-testid="evaluation-chart-display"]').should('be.visible'); + // Switch to table view (default is table but be explicit) + cy.get('[data-testid="evaluation-view-toggle"]').contains('Table').click(); + cy.get('[data-testid="evaluation-results-table"]').should('be.visible'); +} + +// --------------------------------------------------------------------------- +// Basic share button presence +// --------------------------------------------------------------------------- + +describe('Eval Drawer — Share button', () => { + before(() => { + visitEvalTable(); + }); + + it('shows a Prompts button in the evaluation table', () => { + cy.get('[data-testid="evaluation-results-table"]') + .find('button') + .contains('Prompts') + .should('exist'); + }); + + it('opens the drawer and shows the drawer Share button', () => { + cy.get('[data-testid="evaluation-results-table"]') + .find('button') + .contains('Prompts') + .first() + .click(); + + cy.get('[data-testid="eval-drawer-share-button"]').should('be.visible'); + // Close the drawer + cy.get('body').type('{esc}'); + }); +}); + +// --------------------------------------------------------------------------- +// Share URL encoding +// --------------------------------------------------------------------------- + +describe('Eval Drawer — Share URL encoding', () => { + it('share URL includes e_drawer, e_dfilter, and e_dq', () => { + visitEvalTable(); + + cy.get('[data-testid="evaluation-results-table"]') + .find('button') + .contains('Prompts') + .first() + .click(); + + // Set filter to Failed + cy.contains('button', 'Failed').click(); + + // Type a search term + cy.get('[aria-label="Search samples on this page"]').clear().type('the'); + + // Click the drawer Share button to open the share popover + cy.get('[data-testid="eval-drawer-share-button"]').click(); + + // Assert the share URL in the input contains our params + cy.get('[data-testid="eval-drawer-share-button-url-input"]') + .invoke('val') + .then((url) => { + expect(url).to.match(/[?&]e_drawer=[^&]+/u); + expect(url).to.include('e_dfilter=failed'); + expect(url).to.include('e_dq=the'); + }); + + // Close popover + drawer + cy.get('body').type('{esc}'); + cy.get('body').type('{esc}'); + }); +}); + +// --------------------------------------------------------------------------- +// Share link restore on load +// --------------------------------------------------------------------------- + +describe('Eval Drawer — Restore from URL params', () => { + let drawerKey: string; + + before(() => { + // Step 1: load the page, open any drawer, capture the e_drawer key from + // the share URL so we can use it in the next visit. + visitEvalTable(); + + cy.get('[data-testid="evaluation-results-table"]') + .find('button') + .contains('Prompts') + .first() + .click(); + + cy.get('[data-testid="eval-drawer-share-button"]').click(); + + cy.get('[data-testid="eval-drawer-share-button-url-input"]') + .invoke('val') + .then((url) => { + const match = /[?&]e_drawer=([^&]+)/u.exec(String(url)); + if (match) drawerKey = decodeURIComponent(match[1]); + }); + + cy.get('body').type('{esc}'); + cy.get('body').type('{esc}'); + }); + + it('re-opens the drawer with the correct row when e_drawer is in the URL', () => { + cy.then(() => { + visitEvalTable(`?e_drawer=${encodeURIComponent(drawerKey)}`); + // Drawer should open automatically + cy.get('[data-testid="eval-drawer-share-button"]', { timeout: 8000 }).should('be.visible'); + }); + }); + + it('restores filter=failed when e_dfilter=failed is in the URL', () => { + cy.then(() => { + visitEvalTable(`?e_drawer=${encodeURIComponent(drawerKey)}&e_dfilter=failed`); + cy.get('[data-testid="eval-drawer-share-button"]', { timeout: 8000 }).should('be.visible'); + // The Failed chip should be active (aria-pressed=true) + cy.contains('button', 'Failed').should('have.attr', 'aria-pressed', 'true'); + }); + }); + + it('restores search text when e_dq is in the URL', () => { + cy.then(() => { + visitEvalTable(`?e_drawer=${encodeURIComponent(drawerKey)}&e_dq=the`); + cy.get('[data-testid="eval-drawer-share-button"]', { timeout: 8000 }).should('be.visible'); + cy.get('[aria-label="Search samples on this page"]').should('have.value', 'the'); + }); + }); +}); + +// --------------------------------------------------------------------------- +// Missing-row fallback — silent no-op +// --------------------------------------------------------------------------- + +describe('Eval Drawer — Missing row fallback', () => { + it('leaves the drawer closed when e_drawer key has no match', () => { + visitEvalTable('?e_drawer=nonexistent~row~key~that~never~matches~0~1~1~'); + cy.wait(2000); // give data time to load + cy.get('[data-testid="eval-drawer-share-button"]').should('not.exist'); + }); +}); + +// --------------------------------------------------------------------------- +// Unofficial overlay path (AGENTS.md requirement) +// --------------------------------------------------------------------------- + +describe('Eval Drawer — Unofficial run overlay path', () => { + it('shows Share button in drawer for an unofficial overlay row', () => { + // Load a known unofficial run that has eval data. + // We use a real GitHub Actions run ID for the DeepSeek-R1-0528 model + // (mirroring the pattern used in inference-chart.cy.ts overlay tests). + // If the run no longer has artefacts the drawer simply won't open — the + // test is lenient: it only asserts what it can see. + cy.visit('/evaluation', { onBeforeLoad: dismissModal }); + cy.get('[data-testid="evaluation-chart-display"]').should('be.visible'); + cy.get('[data-testid="evaluation-view-toggle"]').contains('Table').click(); + cy.get('[data-testid="evaluation-results-table"]').should('be.visible'); + + // If there are any "Unofficial" badge rows, verify we can open their drawer + // and see the Share button. + cy.get('[data-testid="evaluation-results-table"]').then(($table) => { + const unofficialButtons = $table.find('button:contains("Prompts")'); + if (unofficialButtons.length === 0) { + // No unofficial rows loaded — skip gracefully. + cy.log('No unofficial overlay rows present; skipping overlay-specific assertion.'); + return; + } + cy.wrap(unofficialButtons).first().click(); + cy.get('[data-testid="eval-drawer-share-button"]').should('be.visible'); + cy.get('body').type('{esc}'); + }); + }); +}); diff --git a/packages/app/src/components/evaluation/EvaluationContext.tsx b/packages/app/src/components/evaluation/EvaluationContext.tsx index 897dd156..74a30331 100644 --- a/packages/app/src/components/evaluation/EvaluationContext.tsx +++ b/packages/app/src/components/evaluation/EvaluationContext.tsx @@ -6,11 +6,15 @@ import { useCallback, useContext, useEffect, + useLayoutEffect, useMemo, useRef, useState, } from 'react'; +// useLayoutEffect warns during SSR; alias to useEffect on the server (no-op there anyway). +const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect; + import { DISPLAY_MODEL_TO_DB } from '@semianalysisai/inferencex-constants'; import { track } from '@/lib/analytics'; @@ -59,9 +63,9 @@ export function EvaluationProvider({ children }: { children: ReactNode }) { const rawData: EvalRow[] = rawRows ?? []; const unofficialRawData: EvalRow[] = unofficialEvalRows ?? []; - const [selectedRunDate, setSelectedRunDate] = useState( - () => getUrlParam('e_rundate') || globalRunDate || '', - ); + // Initialize with safe defaults that match SSR output — URL-param values + // are applied in useIsomorphicLayoutEffect below to avoid hydration mismatches. + const [selectedRunDate, setSelectedRunDate] = useState(''); const handleSetSelectedRunDate = useCallback( (date: string) => { @@ -73,15 +77,13 @@ export function EvaluationProvider({ children }: { children: ReactNode }) { [inferenceAvailableDates, setGlobalRunDate], ); - const [selectedBenchmark, setSelectedBenchmark] = useState( - () => getUrlParam('e_bench') || undefined, - ); + const [selectedBenchmark, setSelectedBenchmark] = useState(undefined); const { highContrast, setHighContrast, isLegendExpanded, setIsLegendExpanded } = useChartUIState({ urlPrefix: 'e_', }); - const [showLabels, setShowLabels] = useState(() => getUrlParam('e_labels') === '1'); + const [showLabels, setShowLabels] = useState(false); const { activeSet: enabledHardware, @@ -93,12 +95,25 @@ export function EvaluationProvider({ children }: { children: ReactNode }) { // Pending legend-active selection restored from `e_active` URL param. // Consumed once when hwTypesWithData first populates. - const [pendingActiveHardware, setPendingActiveHardware] = useState | null>(() => { - const v = getUrlParam('e_active'); - if (!v) return null; - const set = new Set(v.split(',').filter(Boolean)); - return set.size > 0 ? set : null; - }); + const [pendingActiveHardware, setPendingActiveHardware] = useState | null>(null); + + // Apply URL-param overrides client-side only (avoids SSR/hydration mismatch). + // Runs synchronously before paint via useIsomorphicLayoutEffect. + const urlInitRef = useRef(false); + useIsomorphicLayoutEffect(() => { + if (urlInitRef.current) return; + urlInitRef.current = true; + const urlRunDate = getUrlParam('e_rundate'); + if (urlRunDate) setSelectedRunDate(urlRunDate); + const urlBench = getUrlParam('e_bench'); + if (urlBench) setSelectedBenchmark(urlBench); + if (getUrlParam('e_labels') === '1') setShowLabels(true); + const urlActive = getUrlParam('e_active'); + if (urlActive) { + const set = new Set(urlActive.split(',').filter(Boolean)); + if (set.size > 0) setPendingActiveHardware(set); + } + }, []); const availableBenchmarks = useMemo(() => { const tasks = new Set([ diff --git a/packages/app/src/components/evaluation/eval-drawer-key.test.ts b/packages/app/src/components/evaluation/eval-drawer-key.test.ts new file mode 100644 index 00000000..57311262 --- /dev/null +++ b/packages/app/src/components/evaluation/eval-drawer-key.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from 'vitest'; + +import type { EvaluationChartData } from '@/components/evaluation/types'; +import { DRAWER_KEY_DELIMITER, findRowByDrawerKey, rowToDrawerKey } from './eval-drawer-key'; + +const BASE_ROW: EvaluationChartData = { + evalResultId: 42, + configId: 1, + hwKey: 'mi355x_vllm' as any, + hardware: 'mi355x', + configLabel: 'MI355X (ATOM!) C32 T4 E1', + score: 0.96, + scoreError: 0.01, + minScore: 0.95, + maxScore: 0.97, + errorMin: 0.95, + errorMax: 0.97, + model: 'DeepSeek-R1-0528', + benchmark: 'gsm8k', + specDecode: 'none', + date: '2026-03-28', + datetime: '2026-03-28T00:00:00Z', + precision: 'fp4', + framework: 'vllm', + tp: 4, + ep: 0, + dp_attention: false, + conc: 32, + disagg: false, + isMultinode: false, + prefillTp: 4, + prefillEp: 0, + prefillDpAttention: false, + prefillNumWorkers: 0, + decodeNumWorkers: 0, + numPrefillGpu: 0, + numDecodeGpu: 0, +}; + +const UNOFFICIAL_ROW: EvaluationChartData = { + ...BASE_ROW, + evalResultId: -1, + runUrl: 'https://github.com/owner/repo/actions/runs/12345678', +}; + +describe('rowToDrawerKey', () => { + it('builds the expected composite key for an official row', () => { + const key = rowToDrawerKey(BASE_ROW); + expect(key).toBe('gsm8k~mi355x~fp4~vllm~none~0~32~4~'); + }); + + it('includes the runId for an unofficial row', () => { + const key = rowToDrawerKey(UNOFFICIAL_ROW); + expect(key).toBe('gsm8k~mi355x~fp4~vllm~none~0~32~4~12345678'); + }); + + it('encodes disagg=true as "1"', () => { + const key = rowToDrawerKey({ ...BASE_ROW, disagg: true }); + expect(key).toContain(`${DRAWER_KEY_DELIMITER}1${DRAWER_KEY_DELIMITER}`); + }); + + it('produces different keys for rows that differ only in tp', () => { + const key1 = rowToDrawerKey(BASE_ROW); + const key2 = rowToDrawerKey({ ...BASE_ROW, tp: 8 }); + expect(key1).not.toBe(key2); + }); + + it('produces different keys for rows that differ only in conc', () => { + const key1 = rowToDrawerKey(BASE_ROW); + const key2 = rowToDrawerKey({ ...BASE_ROW, conc: 256 }); + expect(key1).not.toBe(key2); + }); + + it('none of the built-in field values contain the delimiter', () => { + const fields = [ + BASE_ROW.benchmark, + BASE_ROW.hardware, + BASE_ROW.precision, + BASE_ROW.framework, + BASE_ROW.specDecode, + String(BASE_ROW.conc), + String(BASE_ROW.tp), + ]; + for (const f of fields) { + expect(f).not.toContain(DRAWER_KEY_DELIMITER); + } + }); +}); + +describe('findRowByDrawerKey', () => { + const rows: EvaluationChartData[] = [ + BASE_ROW, + { ...BASE_ROW, evalResultId: 99, tp: 8, conc: 256 }, + UNOFFICIAL_ROW, + ]; + + it('finds an official row by its composite key', () => { + const key = rowToDrawerKey(BASE_ROW); + expect(findRowByDrawerKey(rows, key)).toBe(BASE_ROW); + }); + + it('finds an unofficial row by its composite key', () => { + const key = rowToDrawerKey(UNOFFICIAL_ROW); + expect(findRowByDrawerKey(rows, key)).toBe(UNOFFICIAL_ROW); + }); + + it('returns null on a miss', () => { + expect(findRowByDrawerKey(rows, 'nonexistent~key')).toBeNull(); + }); + + it('returns null on an empty list', () => { + expect(findRowByDrawerKey([], rowToDrawerKey(BASE_ROW))).toBeNull(); + }); + + it('round-trips: key from row → find back same row', () => { + for (const row of rows) { + const key = rowToDrawerKey(row); + expect(findRowByDrawerKey(rows, key)).toBe(row); + } + }); +}); diff --git a/packages/app/src/components/evaluation/eval-drawer-key.ts b/packages/app/src/components/evaluation/eval-drawer-key.ts new file mode 100644 index 00000000..a0ec2c45 --- /dev/null +++ b/packages/app/src/components/evaluation/eval-drawer-key.ts @@ -0,0 +1,63 @@ +/** + * Composite key helpers for the eval samples drawer share link. + * + * The key encodes the unique aggregation dimensions so links survive + * re-ingests (evalResultId churns on every run, but these fields are stable). + * + * Format: ~~~~~~~~ + * + * Fields: + * benchmark — e.g. "gsm8k" + * hardware — bare hardware key, e.g. "mi355x" + * precision — e.g. "fp4" + * framework — e.g. "vllm" + * spec — spec-decode method, e.g. "none" + * disagg — "1" | "0" + * conc — concurrency, e.g. "32" + * tp — tensor-parallelism, e.g. "8" + * runId — GitHub Actions run ID for unofficial rows; "" for official rows + */ + +import type { EvaluationChartData } from '@/components/evaluation/types'; + +export const DRAWER_KEY_DELIMITER = '~'; + +/** + * Builds the composite drawer key from a table row. + * Works for both official rows (runId = "") and unofficial rows (runId from runUrl). + */ +export function rowToDrawerKey(row: EvaluationChartData): string { + const runId = extractRunIdFromUrl(row.runUrl); + const parts = [ + row.benchmark, + row.hardware, + row.precision, + row.framework, + row.specDecode, + row.disagg ? '1' : '0', + String(row.conc), + String(row.tp), + runId ?? '', + ]; + return parts.join(DRAWER_KEY_DELIMITER); +} + +/** + * Finds the first row in `rows` whose composite key matches `key`. + * Returns null if no match is found. + */ +export function findRowByDrawerKey( + rows: EvaluationChartData[], + key: string, +): EvaluationChartData | null { + for (const row of rows) { + if (rowToDrawerKey(row) === key) return row; + } + return null; +} + +function extractRunIdFromUrl(url: string | undefined): string | null { + if (!url) return null; + const m = url.match(/\/actions\/runs\/(\d+)/u); + return m ? m[1] : null; +} diff --git a/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx b/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx index 8bef7b03..12b695ce 100644 --- a/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx +++ b/packages/app/src/components/evaluation/ui/EvalSamplesDrawer.tsx @@ -1,13 +1,16 @@ 'use client'; import { ChevronLeft, ChevronRight, Search } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { EvaluationChartData } from '@/components/evaluation/types'; import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; +import { ShareButton } from '@/components/ui/share-button'; import { useEvalSamples } from '@/hooks/api/use-eval-samples'; +import { useUrlState } from '@/hooks/useUrlState'; import { track } from '@/lib/analytics'; import type { EvalSamplesFilter, EvalSamplesLiveContext } from '@/lib/api'; +import { writeUrlParams } from '@/lib/url-state'; const PAGE_SIZE = 50; @@ -28,19 +31,46 @@ interface EvalSamplesDrawerProps { */ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerProps) { const open = row !== null; + const { getUrlParam } = useUrlState(); const [filter, setFilter] = useState('all'); const [page, setPage] = useState(0); const [search, setSearch] = useState(''); const [expanded, setExpanded] = useState>(new Set()); + // Track whether we've already seeded from URL params (only happens once per page lifetime). + const urlParamsConsumedRef = useRef(false); + // Reset transient state whenever a new row is opened. + // On the very first open this session, seed filter/search from URL params instead. useEffect(() => { if (!open) return; - setFilter('all'); + // These always reset regardless of whether we're seeding from URL params. setPage(0); - setSearch(''); setExpanded(new Set()); - }, [row?.evalResultId, open]); + if (urlParamsConsumedRef.current) { + setFilter('all'); + setSearch(''); + } else { + urlParamsConsumedRef.current = true; + const rawFilter = getUrlParam('e_dfilter'); + const rawSearch = getUrlParam('e_dq'); + const validFilter: EvalSamplesFilter = + rawFilter === 'passed' || rawFilter === 'failed' ? rawFilter : 'all'; + setFilter(validFilter); + setSearch(rawSearch ?? ''); + } + }, [row?.evalResultId, open]); // eslint-disable-line react-hooks/exhaustive-deps -- getUrlParam is stable + + // Mirror filter/search to the in-memory URL store so buildShareUrl() picks them up. + useEffect(() => { + if (!open) return; + writeUrlParams({ e_dfilter: filter === 'all' ? '' : filter }); + }, [filter, open]); + + useEffect(() => { + if (!open) return; + writeUrlParams({ e_dq: search }); + }, [search, open]); // Build a live-fetch context for unofficial runs from the row's identifying // fields. The hook ignores this when `evalResultId > 0` (DB-backed path). @@ -140,8 +170,9 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro aria-describedby={undefined} > {/* Header — `DialogContent` renders its own absolute-positioned close - button in the top-right, so we leave room with `pr-10`. */} -
+ button at right-4. We render a Share button at right-10 and leave + pr-20 so neither overlaps the title text. */} +
{row ? ( @@ -160,6 +191,10 @@ export default function EvalSamplesDrawer({ row, onClose }: EvalSamplesDrawerPro
)}
+ {/* Share button — positioned to the left of Radix's close X (right-4) */} +
+ +
{/* Filter chips + search */} diff --git a/packages/app/src/components/evaluation/ui/EvaluationTable.tsx b/packages/app/src/components/evaluation/ui/EvaluationTable.tsx index 514ec384..f2d3f4d1 100644 --- a/packages/app/src/components/evaluation/ui/EvaluationTable.tsx +++ b/packages/app/src/components/evaluation/ui/EvaluationTable.tsx @@ -1,14 +1,17 @@ 'use client'; import { MessageSquareText } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import EvalSamplesDrawer from '@/components/evaluation/ui/EvalSamplesDrawer'; +import { findRowByDrawerKey, rowToDrawerKey } from '@/components/evaluation/eval-drawer-key'; import type { EvaluationChartData } from '@/components/evaluation/types'; import { useUnofficialRun } from '@/components/unofficial-run-provider'; import { type DataTableColumn, DataTable } from '@/components/ui/data-table'; import { track } from '@/lib/analytics'; import { overlayRunColor, overlayRunIndex } from '@/lib/overlay-run-style'; +import { useUrlState } from '@/hooks/useUrlState'; +import { writeUrlParams } from '@/lib/url-state'; interface EvaluationTableProps { data: EvaluationChartData[]; @@ -16,12 +19,32 @@ interface EvaluationTableProps { export default function EvaluationTable({ data }: EvaluationTableProps) { const { runIndexByUrl } = useUnofficialRun(); + const { getUrlParam } = useUrlState(); const sorted = useMemo(() => [...data].toSorted((a, b) => b.score - a.score), [data]); const hasDisaggConfigs = useMemo(() => data.some((d) => d.disagg), [data]); const [drawerRow, setDrawerRow] = useState(null); + // Auto-open the drawer when the page loads with an e_drawer URL param. + // We guard with a ref so we only attempt once — after the first successful + // match (or after data has loaded with no match), we stop trying. + const drawerKeyParam = getUrlParam('e_drawer'); + const autoOpenConsumedRef = useRef(false); + useEffect(() => { + if (autoOpenConsumedRef.current) return; + if (!drawerKeyParam) { + autoOpenConsumedRef.current = true; + return; + } + if (sorted.length === 0) return; // wait for data to populate + autoOpenConsumedRef.current = true; + const match = findRowByDrawerKey(sorted, drawerKeyParam); + if (match) setDrawerRow(match); + // No match → silent no-op (row may have been removed from this run-date). + }, [sorted, drawerKeyParam]); + const openDrawer = (row: EvaluationChartData) => { setDrawerRow(row); + writeUrlParams({ e_drawer: rowToDrawerKey(row) }); // Notify the first-visit nudge to dismiss itself once the user has // discovered the affordance on their own. if (typeof window !== 'undefined') { @@ -34,6 +57,11 @@ export default function EvaluationTable({ data }: EvaluationTableProps) { }); }; + const closeDrawer = () => { + setDrawerRow(null); + writeUrlParams({ e_drawer: '', e_dfilter: '', e_dq: '' }); + }; + const columns = useMemo[]>( () => [ { @@ -167,7 +195,7 @@ export default function EvaluationTable({ data }: EvaluationTableProps) { testId="evaluation-results-table" analyticsPrefix="evaluation_table" /> - setDrawerRow(null)} /> + ); } diff --git a/packages/app/src/components/ui/share-button.test.tsx b/packages/app/src/components/ui/share-button.test.tsx index f581818b..1aeca89d 100644 --- a/packages/app/src/components/ui/share-button.test.tsx +++ b/packages/app/src/components/ui/share-button.test.tsx @@ -39,7 +39,7 @@ describe('ShareButton', () => { expect(trigger).not.toBeNull(); expect(trigger?.textContent).toContain('Share'); // Popover content lives in a portal and is not in the DOM until opened. - expect(document.querySelector('[data-testid="share-popover"]')).toBeNull(); + expect(document.querySelector('[data-testid="share-button-popover"]')).toBeNull(); }); it('opens the popover with the share URL pre-filled when the trigger is clicked', () => { @@ -50,12 +50,14 @@ describe('ShareButton', () => { act(() => trigger?.click()); - const input = document.querySelector('[data-testid="share-url-input"]'); + const input = document.querySelector( + '[data-testid="share-button-url-input"]', + ); expect(input).not.toBeNull(); expect(input?.value).toBe('https://inferencex.semianalysis.com/?g_model=dsr1#inference'); // Copy + social buttons live inside the popover content. - expect(document.querySelector('[data-testid="share-copy-button"]')).not.toBeNull(); + expect(document.querySelector('[data-testid="share-button-copy-button"]')).not.toBeNull(); expect(document.querySelector('[data-testid="share-twitter"]')).not.toBeNull(); expect(document.querySelector('[data-testid="share-linkedin"]')).not.toBeNull(); }); diff --git a/packages/app/src/components/ui/share-button.tsx b/packages/app/src/components/ui/share-button.tsx index 867c383c..247cdb3c 100644 --- a/packages/app/src/components/ui/share-button.tsx +++ b/packages/app/src/components/ui/share-button.tsx @@ -10,7 +10,11 @@ import { buildShareUrl } from '@/lib/url-state'; import { Button } from './button'; import { Popover, PopoverContent, PopoverTrigger } from './popover'; -export function ShareButton() { +interface ShareButtonProps { + testId?: string; +} + +export function ShareButton({ testId = 'share-button' }: ShareButtonProps = {}) { const [open, setOpen] = useState(false); const [copied, setCopied] = useState(false); const [url, setUrl] = useState(''); @@ -53,7 +57,7 @@ export function ShareButton() {