Skip to content

Commit ae950f7

Browse files
cdervclaude
andcommitted
Add Playwright tests for search tab activation
Five tests covering tab activation when navigating to a page with ?q=: - Match in inactive tab activates that tab - Match in already-active tab keeps it (no unnecessary switch) - Match outside tabs leaves tab state unchanged - Match in nested tabset activates both outer and inner tabs - Search activation overrides localStorage tab preference TDD verified: tests fail against stock v1.9.20 (3 of 5 fail where tab activation is needed), pass with our changes. Co-Authored-By: Claude Opus 4.6 <[email protected]>
1 parent 1b8f682 commit ae950f7

1 file changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
const BASE = './html/search-tabsets/_site/index.html';
4+
5+
// Helper: wait for search tab activation (deferred to pageshow)
6+
// and return the active pane ID for a given tab-content index.
7+
async function getActiveTabId(page, tabContentIndex: number): Promise<string> {
8+
return page.evaluate((idx) => {
9+
const tabContents = document.querySelectorAll('.tab-content');
10+
const tc = tabContents[idx];
11+
if (!tc) return 'not-found';
12+
const active = tc.querySelector('.tab-pane.active');
13+
return active?.id ?? 'none';
14+
}, tabContentIndex);
15+
}
16+
17+
// Helper: count marks visible (not inside an inactive tab pane)
18+
async function visibleMarkCount(page): Promise<number> {
19+
return page.evaluate(() => {
20+
return Array.from(document.querySelectorAll('mark')).filter(m => {
21+
let el: Element | null = m;
22+
while (el) {
23+
if (el.classList?.contains('tab-pane') && !el.classList.contains('active')) {
24+
return false;
25+
}
26+
el = el.parentElement;
27+
}
28+
return true;
29+
}).length;
30+
});
31+
}
32+
33+
test('Search activates inactive tab containing match', async ({ page }) => {
34+
await page.goto(`${BASE}?q=beta-unique-search-term`);
35+
36+
// Mark should be visible (tab activation deferred to pageshow)
37+
const marks = page.locator('mark');
38+
await expect(marks.first()).toBeVisible({ timeout: 5000 });
39+
40+
// Tab Beta (tabset-1-2) should be active in the ungrouped tabset (index 0)
41+
const activeId = await getActiveTabId(page, 0);
42+
expect(activeId).toBe('tabset-1-2');
43+
44+
await expect(marks).toHaveCount(1);
45+
expect(await visibleMarkCount(page)).toBe(1);
46+
});
47+
48+
test('Search keeps active tab when it already has a match', async ({ page }) => {
49+
await page.goto(`${BASE}?q=gamma-both-tabs`);
50+
51+
const marks = page.locator('mark');
52+
await expect(marks.first()).toBeVisible({ timeout: 5000 });
53+
54+
// R tab (tabset-2-1) should stay active — it already has a match
55+
const activeId = await getActiveTabId(page, 1);
56+
expect(activeId).toBe('tabset-2-1');
57+
58+
// 2 marks total (one in each tab), only 1 visible (in active tab)
59+
await expect(marks).toHaveCount(2);
60+
expect(await visibleMarkCount(page)).toBe(1);
61+
});
62+
63+
test('Search highlights outside tabs without changing tab state', async ({ page }) => {
64+
await page.goto(`${BASE}?q=epsilon-no-tabs`);
65+
66+
const marks = page.locator('mark');
67+
await expect(marks.first()).toBeVisible({ timeout: 5000 });
68+
69+
// All tabs should remain at their defaults (first tab active)
70+
expect(await getActiveTabId(page, 0)).toBe('tabset-1-1');
71+
expect(await getActiveTabId(page, 1)).toBe('tabset-2-1');
72+
73+
await expect(marks).toHaveCount(1);
74+
expect(await visibleMarkCount(page)).toBe(1);
75+
});
76+
77+
test('Search activates both outer and inner tabs for nested match', async ({ page }) => {
78+
await page.goto(`${BASE}?q=nested-inner-only-term`);
79+
80+
const marks = page.locator('mark');
81+
await expect(marks.first()).toBeVisible({ timeout: 5000 });
82+
83+
// Outer Tab B (tabset-6-2) and Inner Tab Y (tabset-5-2) should both activate.
84+
// Tabset indices: outer nested = 4, inner nested = 5
85+
expect(await getActiveTabId(page, 4)).toBe('tabset-6-2');
86+
expect(await getActiveTabId(page, 5)).toBe('tabset-5-2');
87+
88+
await expect(marks).toHaveCount(1);
89+
expect(await visibleMarkCount(page)).toBe(1);
90+
});
91+
92+
test('Search activation overrides localStorage tab preference', async ({ page }) => {
93+
// Pre-set localStorage to prefer "R" for the "language" group
94+
await page.goto(`${BASE}`);
95+
await page.evaluate(() => {
96+
localStorage.setItem(
97+
'quarto-persistent-tabsets-data',
98+
JSON.stringify({ language: 'R' })
99+
);
100+
});
101+
102+
// Navigate with search query that matches only in the Python tab
103+
await page.goto(`${BASE}?q=python-only-content`);
104+
105+
const marks = page.locator('mark');
106+
await expect(marks.first()).toBeVisible({ timeout: 5000 });
107+
108+
// Python tab (tabset-3-2) should be active despite localStorage saying "R"
109+
const activeId = await getActiveTabId(page, 2);
110+
expect(activeId).toBe('tabset-3-2');
111+
112+
// Second grouped tabset should remain on R (no search match there)
113+
expect(await getActiveTabId(page, 3)).toBe('tabset-4-1');
114+
115+
await expect(marks).toHaveCount(1);
116+
expect(await visibleMarkCount(page)).toBe(1);
117+
});

0 commit comments

Comments
 (0)