22
33import { render , screen , within } from "@testing-library/react" ;
44import userEvent from "@testing-library/user-event" ;
5- import { describe , expect , it } from "vitest" ;
5+ import { beforeAll , describe , expect , it , vi } from "vitest" ;
66import { ModelUsageCard } from "@/components/model-usage-card" ;
77import type { OverviewResponse } from "@/lib/api" ;
8- import { buildDonutData , priceTones , sortModels , tokenBreakdown } from "@/lib/model-analytics" ;
8+ import { buildDonutData , modelPageColors , OTHER_MODEL_COLOR , priceTones , sortModels , tokenBreakdown } from "@/lib/model-analytics" ;
99import i18n from "@/i18n" ;
1010
1111type Model = OverviewResponse [ "models" ] [ number ] ;
@@ -24,6 +24,10 @@ const model = (name: string, totalTokens: number, overrides: Partial<Model> = {}
2424 ...overrides ,
2525} ) ;
2626
27+ beforeAll ( ( ) => {
28+ window . HTMLElement . prototype . scrollIntoView = vi . fn ( ) ;
29+ } ) ;
30+
2731describe ( "model analytics" , ( ) => {
2832 it ( "does not double count cached input in token composition" , ( ) => {
2933 const parts = tokenBreakdown ( model ( "one" , 1_200 , { inputTokens : 1_000 , cachedInputTokens : 400 , outputTokens : 200 } ) ) ;
@@ -35,7 +39,26 @@ describe("model analytics", () => {
3539 const data = buildDonutData ( Array . from ( { length : 8 } , ( _ , index ) => model ( `m${ index + 1 } ` , 800 - index * 100 ) ) , "Other" ) ;
3640 expect ( data ) . toHaveLength ( 7 ) ;
3741 expect ( data . slice ( 0 , 6 ) . map ( ( item ) => item . value ) ) . toEqual ( [ 800 , 700 , 600 , 500 , 400 , 300 ] ) ;
38- expect ( data [ 6 ] ) . toMatchObject ( { name : "Other" , value : 300 } ) ;
42+ expect ( data [ 6 ] ) . toMatchObject ( { name : "Other" , value : 300 , color : OTHER_MODEL_COLOR } ) ;
43+ } ) ;
44+
45+ it ( "assigns the first six models unique colors in deterministic token order" , ( ) => {
46+ const rows = [
47+ model ( "zeta" , 100 ) ,
48+ model ( "beta" , 300 ) ,
49+ model ( "alpha" , 300 ) ,
50+ model ( "gamma" , 200 ) ,
51+ model ( "delta" , 150 ) ,
52+ model ( "epsilon" , 125 ) ,
53+ ] ;
54+ const colors = modelPageColors ( rows ) ;
55+ const rankedNames = [ "alpha" , "beta" , "gamma" , "delta" , "epsilon" , "zeta" ] ;
56+
57+ expect ( [ ...colors . keys ( ) ] ) . toEqual ( rankedNames ) ;
58+ expect ( new Set ( rankedNames . map ( ( name ) => colors . get ( name ) ) ) . size ) . toBe ( 6 ) ;
59+ expect ( buildDonutData ( rows , "Other" , colors ) . map ( ( entry ) => entry . color ) ) . toEqual (
60+ rankedNames . map ( ( name ) => colors . get ( name ) ) ,
61+ ) ;
3962 } ) ;
4063
4164 it ( "sorts descending by tokens, cost, and effective price with unknown prices last" , ( ) => {
@@ -64,8 +87,29 @@ describe("model analytics", () => {
6487 expect ( within ( row ) . getAllByText ( "Pricing unavailable" ) ) . toHaveLength ( 4 ) ;
6588 expect ( screen . getByText ( "Token composition" ) ) . toBeInTheDocument ( ) ;
6689
67- await userEvent . selectOptions ( screen . getByRole ( "combobox" , { name : "Sort descending" } ) , "effective" ) ;
68- expect ( screen . getByRole ( "combobox" ) ) . toHaveValue ( "effective" ) ;
90+ const user = userEvent . setup ( ) ;
91+ screen . getByRole ( "combobox" , { name : "Sort descending" } ) . focus ( ) ;
92+ await user . keyboard ( "[Enter][End][Enter]" ) ;
93+ expect ( screen . getByRole ( "combobox" , { name : "Sort descending" } ) ) . toHaveTextContent ( "Effective price" ) ;
94+ } ) ;
95+
96+ it ( "uses one model color mapping across the chart and table while sorting" , async ( ) => {
97+ const rows = [ model ( "alpha" , 100 , { costUSD : 9 } ) , model ( "beta" , 300 , { costUSD : 1 } ) ] ;
98+ const colors = modelPageColors ( rows ) ;
99+ render ( < ModelUsageCard models = { rows } /> ) ;
100+
101+ for ( const row of rows ) {
102+ const tableRow = document . querySelector ( `[data-model-row='${ row . model } ']` ) as HTMLElement ;
103+ expect ( tableRow ) . toHaveAttribute ( "data-model-color" , colors . get ( row . model ) ) ;
104+ expect ( document . querySelector ( `[data-model-legend='${ row . model } ']` ) ) . toHaveAttribute ( "data-model-color" , colors . get ( row . model ) ) ;
105+ expect ( tableRow . querySelector ( "[data-model-badge]" ) ) . toHaveClass ( "text-foreground" ) ;
106+ }
107+
108+ const user = userEvent . setup ( ) ;
109+ screen . getByRole ( "combobox" , { name : "Sort descending" } ) . focus ( ) ;
110+ await user . keyboard ( "[Enter][ArrowDown][Enter]" ) ;
111+ expect ( document . querySelector ( "tbody tr" ) ?. getAttribute ( "data-model-row" ) ) . toBe ( "alpha" ) ;
112+ expect ( document . querySelector ( "[data-model-row='beta']" ) ) . toHaveAttribute ( "data-model-color" , colors . get ( "beta" ) ) ;
69113 } ) ;
70114
71115 it ( "renders the model page labels in Chinese" , async ( ) => {
0 commit comments