@@ -21,30 +21,65 @@ import { Locator, Page } from '@playwright/test';
2121
2222/**
2323 * Tabs component for Ant Design tab navigation.
24+ *
25+ * Expects the locator to point to the `.ant-tabs` wrapper element
26+ * (not the inner tablist) so that `nav` can scope to the outer tab bar
27+ * and exclude nested/inner tabs (e.g. Results / Query history in SQL Lab).
2428 */
2529export class Tabs {
2630 readonly page : Page ;
2731 private readonly locator : Locator ;
2832
2933 constructor ( page : Page , locator ?: Locator ) {
3034 this . page = page ;
31- // Default to the tablist role if no specific locator provided
32- this . locator = locator ?? page . getByRole ( 'tablist' ) ;
35+ this . locator = locator ?? page . locator ( '.ant-tabs' ) . first ( ) ;
3336 }
3437
3538 /**
36- * Gets the tablist element locator
39+ * The root element locator for this tabs component.
3740 */
3841 get element ( ) : Locator {
3942 return this . locator ;
4043 }
4144
4245 /**
43- * Gets a tab by name, scoped to this tablist's container
46+ * The tab navigation bar for this component.
47+ * Scoped to the first `.ant-tabs-nav` descendant so that queries
48+ * only hit this component's tabs, never nested/inner tab bars.
49+ */
50+ protected get nav ( ) : Locator {
51+ return this . locator . locator ( '.ant-tabs-nav' ) . first ( ) ;
52+ }
53+
54+ /**
55+ * Returns the number of tabs.
56+ * Counts `.ant-tabs-tab` wrappers in the nav bar — one per physical tab,
57+ * regardless of inner role="tab" elements (btn, remove button, etc.).
58+ */
59+ async getTabCount ( ) : Promise < number > {
60+ return this . nav . locator ( '.ant-tabs-tab' ) . count ( ) ;
61+ }
62+
63+ /**
64+ * Returns the text content of all tabs.
65+ */
66+ async getTabNames ( ) : Promise < string [ ] > {
67+ return this . nav . locator ( '.ant-tabs-tab-btn' ) . allTextContents ( ) ;
68+ }
69+
70+ /**
71+ * Gets a tab button by name, scoped to this component's nav bar.
72+ * Anchored at start (^) with negative lookahead (?!\d) to prevent
73+ * partial matches: "Query" won't match "Query 1", and "Query 1"
74+ * won't match "Query 10". Trailing icon text (e.g. " circle-solid")
75+ * is allowed since (?!\d) permits non-digit suffixes.
4476 * @param tabName - The name/label of the tab
4577 */
4678 getTab ( tabName : string ) : Locator {
47- return this . locator . getByRole ( 'tab' , { name : tabName } ) ;
79+ const escaped = tabName . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
80+ return this . nav
81+ . locator ( '.ant-tabs-tab-btn' )
82+ . filter ( { hasText : new RegExp ( `^${ escaped } (?!\\d)` ) } ) ;
4883 }
4984
5085 /**
@@ -63,6 +98,16 @@ export class Tabs {
6398 return this . page . getByRole ( 'tabpanel' , { name : tabName } ) ;
6499 }
65100
101+ /**
102+ * Returns the name of the currently active tab.
103+ */
104+ async getActiveTabName ( ) : Promise < string > {
105+ const text = await this . nav
106+ . locator ( '.ant-tabs-tab-active .ant-tabs-tab-btn' )
107+ . textContent ( ) ;
108+ return text ?. trim ( ) ?? '' ;
109+ }
110+
66111 /**
67112 * Checks if a tab is selected
68113 * @param tabName - The name/label of the tab
0 commit comments