Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/scan/src/web/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export const MIN_CONTAINER_WIDTH = 240;
export const LOCALSTORAGE_KEY = "react-scan-widget-settings-v2";
export const LOCALSTORAGE_COLLAPSED_KEY = "react-scan-widget-collapsed-v1";
export const LOCALSTORAGE_LAST_VIEW_KEY = "react-scan-widget-last-view-v1";

export const RANKED_BAR_CHART_WIDTH_DENOMINATOR_MIN = 1e-6;
97 changes: 97 additions & 0 deletions packages/scan/src/web/utils/ranked-bar-chart-search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, expect, it } from 'vitest';
import { RANKED_BAR_CHART_WIDTH_DENOMINATOR_MIN } from '~web/constants';
import {
filterSortedRankedBarsBySearch,
getRankedBarDisplayLabel,
getSafeRankedWidthDenominator,
rankedBarMatchesSearch,
type RankedBarChartBar,
} from '~web/utils/ranked-bar-chart-search';

const renderBar = (name: string, totalTime: number): RankedBarChartBar => ({
kind: 'render',
event: { name },
totalTime,
});

const otherJavascriptBar = (totalTime: number): RankedBarChartBar => ({
kind: 'other-javascript',
totalTime,
});

describe('getRankedBarDisplayLabel', () => {
it('returns component name for render bars', () => {
expect(getRankedBarDisplayLabel(renderBar('TodoList', 10))).toBe('TodoList');
});

it('returns fixed labels for non-render bars', () => {
expect(getRankedBarDisplayLabel(otherJavascriptBar(5))).toBe(
'JavaScript/React Hooks',
);
expect(
getRankedBarDisplayLabel({ kind: 'other-frame-drop', totalTime: 1 }),
).toBe('JavaScript, DOM updates, Draw Frame');
expect(
getRankedBarDisplayLabel({ kind: 'other-not-javascript', totalTime: 2 }),
).toBe('Update DOM and Draw New Frame');
});
});

describe('rankedBarMatchesSearch', () => {
it('matches every bar when query is empty', () => {
expect(rankedBarMatchesSearch(renderBar('Any', 1), '')).toBe(true);
});

it('matches case-insensitive substrings on component names', () => {
expect(rankedBarMatchesSearch(renderBar('TodoListForm', 1), 'todo')).toBe(
true,
);
expect(rankedBarMatchesSearch(renderBar('TodoListForm', 1), 'FORM')).toBe(
true,
);
expect(rankedBarMatchesSearch(renderBar('TodoListForm', 1), 'missing')).toBe(
false,
);
});

it('matches non-render bar labels', () => {
expect(rankedBarMatchesSearch(otherJavascriptBar(1), 'hooks')).toBe(true);
expect(rankedBarMatchesSearch(otherJavascriptBar(1), 'java')).toBe(true);
});
});

describe('filterSortedRankedBarsBySearch', () => {
const sortedBars: RankedBarChartBar[] = [
renderBar('TodoListForm', 24),
renderBar('TodoListItem', 6),
otherJavascriptBar(10),
];

it('returns all bars for empty or whitespace query', () => {
expect(filterSortedRankedBarsBySearch(sortedBars, '')).toEqual(sortedBars);
expect(filterSortedRankedBarsBySearch(sortedBars, ' ')).toEqual(sortedBars);
});

it('returns only matching bars in original order', () => {
expect(filterSortedRankedBarsBySearch(sortedBars, 'TodoList')).toEqual([
renderBar('TodoListForm', 24),
renderBar('TodoListItem', 6),
]);
});

it('returns empty array when nothing matches', () => {
expect(filterSortedRankedBarsBySearch(sortedBars, 'xyz')).toEqual([]);
});
});

describe('getSafeRankedWidthDenominator', () => {
it('returns the value when above the floor', () => {
expect(getSafeRankedWidthDenominator(240)).toBe(240);
});

it('returns the minimum constant when sum is zero', () => {
expect(getSafeRankedWidthDenominator(0)).toBe(
RANKED_BAR_CHART_WIDTH_DENOMINATOR_MIN,
);
});
});
54 changes: 54 additions & 0 deletions packages/scan/src/web/utils/ranked-bar-chart-search.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { RANKED_BAR_CHART_WIDTH_DENOMINATOR_MIN } from '~web/constants';

interface RankedBarChartRenderEvent {
name: string;
}

export type RankedBarChartBar =
| { kind: 'other-frame-drop'; totalTime: number }
| { kind: 'other-not-javascript'; totalTime: number }
| { kind: 'other-javascript'; totalTime: number }
| { kind: 'render'; event: RankedBarChartRenderEvent; totalTime: number };

export const getRankedBarDisplayLabel = (bar: RankedBarChartBar): string => {
switch (bar.kind) {
case 'other-frame-drop': {
return 'JavaScript, DOM updates, Draw Frame';
}
case 'other-javascript': {
return 'JavaScript/React Hooks';
}
case 'other-not-javascript': {
return 'Update DOM and Draw New Frame';
}
case 'render': {
return bar.event.name;
}
}
};

export const rankedBarMatchesSearch = (
bar: RankedBarChartBar,
trimmedQuery: string,
): boolean => {
if (trimmedQuery === '') {
return true;
}
return getRankedBarDisplayLabel(bar)
.toLowerCase()
.includes(trimmedQuery.toLowerCase());
};

export const filterSortedRankedBarsBySearch = <T extends RankedBarChartBar>(
sortedBars: T[],
query: string,
): T[] => {
const trimmedQuery = query.trim();
if (trimmedQuery === '') {
return sortedBars;
}
return sortedBars.filter((bar) => rankedBarMatchesSearch(bar, trimmedQuery));
};

export const getSafeRankedWidthDenominator = (widthDenominator: number): number =>
Math.max(widthDenominator, RANKED_BAR_CHART_WIDTH_DENOMINATOR_MIN);
13 changes: 9 additions & 4 deletions packages/scan/src/web/views/notifications/details-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ export const DetailsRoutes = () => {
case 'render-visualization': {
return (
<TabLayout>
<RenderBarChart selectedEvent={notificationState.selectedEvent} />
<RenderBarChart
key={notificationState.selectedEvent.id}
selectedEvent={notificationState.selectedEvent}
/>
</TabLayout>
);
}
Expand Down Expand Up @@ -194,12 +197,14 @@ const TabLayout = ({ children }: { children: ReactNode }) => {
);
}
return (
<div className={cn([`w-full h-full flex flex-col gap-y-2`])}>
<div className={cn(['h-[50px] w-full'])}>
<div className={cn(['flex h-full min-h-0 w-full flex-col'])}>
<div className={cn(['w-full shrink-0'])}>
<NotificationTabs selectedEvent={notificationState.selectedEvent} />
</div>
<div
className={cn(['h-calc(100%-50px) flex flex-col overflow-y-auto px-3'])}
className={cn([
'flex min-h-0 flex-1 flex-col overflow-y-auto px-3 pt-2',
])}
>
{children}
</div>
Expand Down
Loading