-
Notifications
You must be signed in to change notification settings - Fork 389
feat(scan): ranked list search with stable bar widths #429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| ); | ||
| }); | ||
| }); |
| 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); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -178,10 +178,21 @@ export const Widget = () => { | |
|
|
||
| const handleDrag = useCallback( | ||
| (e: JSX.TargetedPointerEvent<HTMLDivElement>) => { | ||
| e.preventDefault(); | ||
| if (!refWidget.current) return; | ||
|
|
||
| const eventTarget = e.target; | ||
| if (!(eventTarget instanceof HTMLElement)) return; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drag breaks when pointer target is SVG elementMedium Severity The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| if (!refWidget.current || (e.target as HTMLElement).closest("button")) | ||
| if ( | ||
| eventTarget.closest("button") || | ||
| eventTarget.closest( | ||
| 'input, textarea, select, option, label, [contenteditable="true"]', | ||
| ) | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| e.preventDefault(); | ||
|
|
||
| const container = refWidget.current; | ||
| const containerStyle = container.style; | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.