@@ -111,6 +111,54 @@ def empty_bf_df(
111111 return session .read_pandas (empty_pandas_df )
112112
113113
114+ @pytest .fixture (scope = "module" )
115+ def custom_index_pandas_df () -> pd .DataFrame :
116+ """Create a DataFrame with a custom named index for testing."""
117+ test_data = pd .DataFrame (
118+ {
119+ "value_a" : [10 , 20 , 30 , 40 , 50 , 60 ],
120+ "value_b" : ["a" , "b" , "c" , "d" , "e" , "f" ],
121+ }
122+ )
123+ test_data .index = pd .Index (
124+ ["row_1" , "row_2" , "row_3" , "row_4" , "row_5" , "row_6" ], name = "custom_idx"
125+ )
126+ return test_data
127+
128+
129+ @pytest .fixture (scope = "module" )
130+ def custom_index_bf_df (
131+ session : bf .Session , custom_index_pandas_df : pd .DataFrame
132+ ) -> bf .dataframe .DataFrame :
133+ return session .read_pandas (custom_index_pandas_df )
134+
135+
136+ @pytest .fixture (scope = "module" )
137+ def multiindex_pandas_df () -> pd .DataFrame :
138+ """Create a DataFrame with MultiIndex for testing."""
139+ test_data = pd .DataFrame (
140+ {
141+ "value" : [100 , 200 , 300 , 400 , 500 , 600 ],
142+ "category" : ["X" , "Y" , "Z" , "X" , "Y" , "Z" ],
143+ }
144+ )
145+ test_data .index = pd .MultiIndex .from_arrays (
146+ [
147+ ["group_A" , "group_A" , "group_A" , "group_B" , "group_B" , "group_B" ],
148+ [1 , 2 , 3 , 1 , 2 , 3 ],
149+ ],
150+ names = ["group" , "item" ],
151+ )
152+ return test_data
153+
154+
155+ @pytest .fixture (scope = "module" )
156+ def multiindex_bf_df (
157+ session : bf .Session , multiindex_pandas_df : pd .DataFrame
158+ ) -> bf .dataframe .DataFrame :
159+ return session .read_pandas (multiindex_pandas_df )
160+
161+
114162def mock_execute_result_with_params (
115163 self , schema , total_rows_val , arrow_batches_val , * args , ** kwargs
116164):
@@ -549,6 +597,157 @@ def test_widget_row_count_reflects_actual_data_available(
549597 assert widget .page_size == 2 # Respects the display option
550598
551599
552- # TODO(shuowei): Add tests for custom index and multiindex
553- # This may not be necessary for the SQL Cell use case but should be
554- # considered for completeness.
600+ def test_widget_with_custom_index_should_display_index_column (
601+ custom_index_bf_df : bf .dataframe .DataFrame ,
602+ custom_index_pandas_df : pd .DataFrame ,
603+ ):
604+ """
605+ Given a DataFrame with a custom named index, when rendered in anywidget mode,
606+ then the index column should be visible in the HTML output.
607+ """
608+ from bigframes .display import TableWidget
609+
610+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 2 ):
611+ widget = TableWidget (custom_index_bf_df )
612+ html = widget .table_html
613+
614+ assert "custom_idx" in html
615+ assert "row_1" in html
616+ assert "row_2" in html
617+ assert "row_3" not in html
618+ assert "row_4" not in html
619+
620+
621+ def test_widget_with_custom_index_pagination_preserves_index (
622+ custom_index_bf_df : bf .dataframe .DataFrame ,
623+ custom_index_pandas_df : pd .DataFrame ,
624+ ):
625+ """
626+ Given a DataFrame with a custom index, when navigating between pages,
627+ then each page should display the correct index values.
628+ """
629+ from bigframes .display import TableWidget
630+
631+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 2 ):
632+ widget = TableWidget (custom_index_bf_df )
633+
634+ widget .page = 1
635+ html = widget .table_html
636+
637+ assert "row_3" in html
638+ assert "row_4" in html
639+ assert "row_1" not in html
640+ assert "row_2" not in html
641+
642+
643+ def test_widget_with_custom_index_matches_pandas_output (
644+ custom_index_bf_df : bf .dataframe .DataFrame ,
645+ custom_index_pandas_df : pd .DataFrame ,
646+ ):
647+ """
648+ Given a DataFrame with a custom index, the widget's HTML output should
649+ match what pandas would render for the same slice.
650+ """
651+ from bigframes .display import TableWidget
652+
653+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 3 ):
654+ widget = TableWidget (custom_index_bf_df )
655+ html = widget .table_html
656+
657+ expected_slice = custom_index_pandas_df .iloc [0 :3 ]
658+
659+ for idx_value in expected_slice .index :
660+ assert str (idx_value ) in html
661+
662+
663+ def test_widget_with_multiindex_should_display_all_index_levels (
664+ multiindex_bf_df : bf .dataframe .DataFrame ,
665+ multiindex_pandas_df : pd .DataFrame ,
666+ ):
667+ """
668+ Given a DataFrame with MultiIndex, when rendered in anywidget mode,
669+ then all index levels should be visible in the HTML output.
670+ """
671+ from bigframes .display import TableWidget
672+
673+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 2 ):
674+ widget = TableWidget (multiindex_bf_df )
675+ html = widget .table_html
676+
677+ assert "group" in html
678+ assert "item" in html
679+ assert "group_A" in html
680+
681+
682+ def test_widget_with_multiindex_pagination_preserves_structure (
683+ multiindex_bf_df : bf .dataframe .DataFrame ,
684+ multiindex_pandas_df : pd .DataFrame ,
685+ ):
686+ """
687+ Given a DataFrame with MultiIndex, when navigating between pages,
688+ then the multiindex structure should be preserved on each page.
689+ """
690+ from bigframes .display import TableWidget
691+
692+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 2 ):
693+ widget = TableWidget (multiindex_bf_df )
694+
695+ widget .page = 1
696+ html = widget .table_html
697+
698+ assert "group_A" in html or "3" in html
699+ assert "group_B" in html
700+
701+
702+ def test_widget_with_multiindex_all_pages_have_correct_indices (
703+ multiindex_bf_df : bf .dataframe .DataFrame ,
704+ multiindex_pandas_df : pd .DataFrame ,
705+ ):
706+ """
707+ Given a DataFrame with MultiIndex, verify that each page displays
708+ the correct multiindex values across all pages.
709+ """
710+ from bigframes .display import TableWidget
711+
712+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 2 ):
713+ widget = TableWidget (multiindex_bf_df )
714+
715+ for page_num in range (3 ):
716+ widget .page = page_num
717+ html = widget .table_html
718+
719+ start_row = page_num * 2
720+ end_row = start_row + 2
721+ expected_slice = multiindex_pandas_df .iloc [start_row :end_row ]
722+
723+ found_index_value = False
724+ for idx_tuple in expected_slice .index :
725+ if str (idx_tuple [0 ]) in html or str (idx_tuple [1 ]) in html :
726+ found_index_value = True
727+ break
728+
729+ assert found_index_value , f"Page { page_num } missing expected index values"
730+
731+
732+ def test_widget_with_multiindex_page_size_change_preserves_structure (
733+ multiindex_bf_df : bf .dataframe .DataFrame ,
734+ multiindex_pandas_df : pd .DataFrame ,
735+ ):
736+ """
737+ Given a DataFrame with MultiIndex, when the page size is changed,
738+ then the multiindex structure should still be correctly displayed.
739+ """
740+ from bigframes .display import TableWidget
741+
742+ with bf .option_context ("display.repr_mode" , "anywidget" , "display.max_rows" , 2 ):
743+ widget = TableWidget (multiindex_bf_df )
744+
745+ widget .page_size = 3
746+ html = widget .table_html
747+
748+ assert "group" in html
749+ assert "item" in html
750+
751+ expected_slice = multiindex_pandas_df .iloc [0 :3 ]
752+ for idx_tuple in expected_slice .index :
753+ assert str (idx_tuple [0 ]) in html or str (idx_tuple [1 ]) in html
0 commit comments