From 16361d0e594d99176c6e843669792ab429c7fdb7 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 17 Feb 2026 16:10:45 +0100 Subject: [PATCH 1/6] Add .gitignore for search-highlight test fixture --- tests/docs/playwright/html/search-highlight/.gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tests/docs/playwright/html/search-highlight/.gitignore diff --git a/tests/docs/playwright/html/search-highlight/.gitignore b/tests/docs/playwright/html/search-highlight/.gitignore new file mode 100644 index 0000000000..91e4dc52c8 --- /dev/null +++ b/tests/docs/playwright/html/search-highlight/.gitignore @@ -0,0 +1,3 @@ +/.quarto/ +/_site/ +**/*.quarto_ipynb From b2b2c34f641ec54f90c5473396f6224c06d05fb5 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 17 Feb 2026 16:23:58 +0100 Subject: [PATCH 2/6] Add test verifying search highlight listeners register after delay Complements the persistence test by confirming that quarto-hrChanged does clear marks after the 1000ms registration delay elapses. Co-Authored-By: Claude Opus 4.6 --- .../tests/html-search-highlight.spec.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/integration/playwright/tests/html-search-highlight.spec.ts diff --git a/tests/integration/playwright/tests/html-search-highlight.spec.ts b/tests/integration/playwright/tests/html-search-highlight.spec.ts new file mode 100644 index 0000000000..0f4272043f --- /dev/null +++ b/tests/integration/playwright/tests/html-search-highlight.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from "@playwright/test"; + +test('Search highlights persist after page load', async ({ page }) => { + await page.goto('./html/search-highlight/_site/index.html?q=special'); + const marks = page.locator('mark'); + + // Marks should exist after page load + await expect(marks.first()).toBeVisible({ timeout: 5000 }); + + // Simulate the layout-triggered quarto-hrChanged event that clears marks + // prematurely without the fix (#14047). With the fix, the listener is + // registered after a delay, so early events like this are ignored. + await page.evaluate(() => { + window.dispatchEvent(new CustomEvent('quarto-hrChanged')); + }); + + // Marks should still be visible after the simulated layout event + await expect(marks.first()).toBeVisible(); + await expect(marks.first()).toContainText(/special/i); +}); + +test('Search highlights are cleared by scroll after delay', async ({ page }) => { + await page.goto('./html/search-highlight/_site/index.html?q=special'); + const marks = page.locator('mark'); + + // Marks should exist after page load + await expect(marks.first()).toBeVisible({ timeout: 5000 }); + + // Wait for the delayed listener registration (1000ms in quarto-search.js) + await page.waitForTimeout(1500); + + // Now quarto-hrChanged should clear marks (listener is registered) + await page.evaluate(() => { + window.dispatchEvent(new CustomEvent('quarto-hrChanged')); + }); + + await expect(marks).toHaveCount(0); +}); From 2c83a8439847a0f47ec3663b5313b53e8d169b6e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Tue, 17 Feb 2026 17:10:34 +0100 Subject: [PATCH 3/6] Add changelog entry for search highlight fix --- news/changelog-1.9.md | 1 + 1 file changed, 1 insertion(+) diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 0f61d281a9..5bf255fd03 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -119,6 +119,7 @@ All changes included in 1.9: - ([#13932](https://github.com/quarto-dev/quarto-cli/pull/13932)): Add `llms-txt: true` option to generate LLM-friendly content for websites. Creates `.llms.md` markdown files alongside HTML pages and a root `llms.txt` index file following the [llms.txt](https://llmstxt.org/) specification. - ([#13951](https://github.com/quarto-dev/quarto-cli/issues/13951)): Fix `image-lazy-loading` not applying `loading="lazy"` attribute to auto-detected listing images. - ([#14003](https://github.com/quarto-dev/quarto-cli/pull/14003)): Add text fragments to search result links so browsers scroll to and highlight the matched text on the target page. +- ([#14047](https://github.com/quarto-dev/quarto-cli/issues/14047)): Fix search highlights cleared before user can see them when landing on a page with `?q=` search parameter. ### `book` From a5434e6f56fc88f40cdf440ca60c0137db9581eb Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 17:40:06 +0100 Subject: [PATCH 4/6] Add test coverage for search highlight persistence (#14047, #9802) PR #13442 removed scroll-based highlight clearing. This commit adds Playwright tests verifying: scroll events never clear marks, query-change clearing still works, and no marks appear without ?q= parameter. Restores test fixture files dropped during rebase (source files were in the skipped JS fix commit). Updates changelog to credit @jtbayly and reference both #9802 and #14047. Co-Authored-By: Claude Opus 4.6 --- news/changelog-1.9.md | 2 +- tests/docs/playwright/html/.gitignore | 4 +- .../html/search-highlight/_quarto.yml | 11 ++++ .../html/search-highlight/index.qmd | 7 +++ .../tests/html-search-highlight.spec.ts | 53 ++++++++++++------- 5 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 tests/docs/playwright/html/search-highlight/_quarto.yml create mode 100644 tests/docs/playwright/html/search-highlight/index.qmd diff --git a/news/changelog-1.9.md b/news/changelog-1.9.md index 5bf255fd03..cbf080fd90 100644 --- a/news/changelog-1.9.md +++ b/news/changelog-1.9.md @@ -119,7 +119,7 @@ All changes included in 1.9: - ([#13932](https://github.com/quarto-dev/quarto-cli/pull/13932)): Add `llms-txt: true` option to generate LLM-friendly content for websites. Creates `.llms.md` markdown files alongside HTML pages and a root `llms.txt` index file following the [llms.txt](https://llmstxt.org/) specification. - ([#13951](https://github.com/quarto-dev/quarto-cli/issues/13951)): Fix `image-lazy-loading` not applying `loading="lazy"` attribute to auto-detected listing images. - ([#14003](https://github.com/quarto-dev/quarto-cli/pull/14003)): Add text fragments to search result links so browsers scroll to and highlight the matched text on the target page. -- ([#14047](https://github.com/quarto-dev/quarto-cli/issues/14047)): Fix search highlights cleared before user can see them when landing on a page with `?q=` search parameter. +- ([#9802](https://github.com/quarto-dev/quarto-cli/issues/9802), [#14047](https://github.com/quarto-dev/quarto-cli/issues/14047)): Fix search term highlighting disappearing on page scroll or layout events when navigating from search results. (author: @jtbayly, [#13442](https://github.com/quarto-dev/quarto-cli/pull/13442)) ### `book` diff --git a/tests/docs/playwright/html/.gitignore b/tests/docs/playwright/html/.gitignore index 2cca5373e9..76e53aa71b 100644 --- a/tests/docs/playwright/html/.gitignore +++ b/tests/docs/playwright/html/.gitignore @@ -1,2 +1,4 @@ *_files/ -*.html \ No newline at end of file +*.html +/.quarto/ +**/*.quarto_ipynb diff --git a/tests/docs/playwright/html/search-highlight/_quarto.yml b/tests/docs/playwright/html/search-highlight/_quarto.yml new file mode 100644 index 0000000000..5230178921 --- /dev/null +++ b/tests/docs/playwright/html/search-highlight/_quarto.yml @@ -0,0 +1,11 @@ +project: + type: website + +website: + title: "Search Highlight Test" + navbar: + left: + - href: index.qmd + text: Home + +format: html diff --git a/tests/docs/playwright/html/search-highlight/index.qmd b/tests/docs/playwright/html/search-highlight/index.qmd new file mode 100644 index 0000000000..42d90d0b6b --- /dev/null +++ b/tests/docs/playwright/html/search-highlight/index.qmd @@ -0,0 +1,7 @@ +--- +title: "Search Highlight Test" +--- + +This page contains a special keyword that we use for testing search highlighting. + +The word special appears multiple times on this page to ensure search highlighting works correctly. diff --git a/tests/integration/playwright/tests/html-search-highlight.spec.ts b/tests/integration/playwright/tests/html-search-highlight.spec.ts index 0f4272043f..93a7b7638f 100644 --- a/tests/integration/playwright/tests/html-search-highlight.spec.ts +++ b/tests/integration/playwright/tests/html-search-highlight.spec.ts @@ -1,38 +1,53 @@ import { test, expect } from "@playwright/test"; -test('Search highlights persist after page load', async ({ page }) => { - await page.goto('./html/search-highlight/_site/index.html?q=special'); +const BASE = './html/search-highlight/_site/index.html'; + +test('Search highlights not cleared by scroll events', async ({ page }) => { + await page.goto(`${BASE}?q=special`); const marks = page.locator('mark'); - // Marks should exist after page load await expect(marks.first()).toBeVisible({ timeout: 5000 }); + const initialCount = await marks.count(); + expect(initialCount).toBeGreaterThanOrEqual(2); - // Simulate the layout-triggered quarto-hrChanged event that clears marks - // prematurely without the fix (#14047). With the fix, the listener is - // registered after a delay, so early events like this are ignored. + // Dispatch layout events immediately (previously cleared marks via quarto-hrChanged) await page.evaluate(() => { window.dispatchEvent(new CustomEvent('quarto-hrChanged')); + window.dispatchEvent(new CustomEvent('quarto-sectionChanged')); }); + await expect(marks).toHaveCount(initialCount); - // Marks should still be visible after the simulated layout event - await expect(marks.first()).toBeVisible(); - await expect(marks.first()).toContainText(/special/i); + // Wait and dispatch again — marks should persist at any time + await page.waitForTimeout(1500); + await page.evaluate(() => { + window.dispatchEvent(new CustomEvent('quarto-hrChanged')); + window.dispatchEvent(new CustomEvent('quarto-sectionChanged')); + }); + await expect(marks).toHaveCount(initialCount); }); -test('Search highlights are cleared by scroll after delay', async ({ page }) => { - await page.goto('./html/search-highlight/_site/index.html?q=special'); +test('Search highlights cleared when query changes', async ({ page }) => { + await page.goto(`${BASE}?q=special`); const marks = page.locator('mark'); - // Marks should exist after page load await expect(marks.first()).toBeVisible({ timeout: 5000 }); - // Wait for the delayed listener registration (1000ms in quarto-search.js) - await page.waitForTimeout(1500); + // Open the detached search overlay + await page.locator('.aa-DetachedSearchButton').click(); + const input = page.locator('.aa-Input'); + await expect(input).toBeVisible({ timeout: 2000 }); - // Now quarto-hrChanged should clear marks (listener is registered) - await page.evaluate(() => { - window.dispatchEvent(new CustomEvent('quarto-hrChanged')); - }); + // Type a different query — triggers onStateChange which clears marks + await input.fill('different'); + await expect(page.locator('main mark')).toHaveCount(0, { timeout: 2000 }); +}); + +test('No highlights without search query', async ({ page }) => { + await page.goto(BASE); + + // Wait for page to fully load + await expect(page.locator('main')).toBeVisible(); - await expect(marks).toHaveCount(0); + // No marks should exist without ?q= parameter + await expect(page.locator('mark')).toHaveCount(0); }); From 417faf09996d61c7b1de00007e5cad947cadd2c7 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 18:10:56 +0100 Subject: [PATCH 5/6] Simplify scroll persistence test to use actual scrolling The previous test dispatched quarto-hrChanged/quarto-sectionChanged custom events, which were leftovers from when search code listened to those events. Since #13442 removed those listeners entirely, dispatching those events tested nothing. Replace with actual scroll behavior which is the real user scenario from #14047. Co-Authored-By: Claude Opus 4.6 --- .../tests/html-search-highlight.spec.ts | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tests/integration/playwright/tests/html-search-highlight.spec.ts b/tests/integration/playwright/tests/html-search-highlight.spec.ts index 93a7b7638f..29dcaac70b 100644 --- a/tests/integration/playwright/tests/html-search-highlight.spec.ts +++ b/tests/integration/playwright/tests/html-search-highlight.spec.ts @@ -2,7 +2,7 @@ import { test, expect } from "@playwright/test"; const BASE = './html/search-highlight/_site/index.html'; -test('Search highlights not cleared by scroll events', async ({ page }) => { +test('Search highlights persist after scrolling', async ({ page }) => { await page.goto(`${BASE}?q=special`); const marks = page.locator('mark'); @@ -10,19 +10,9 @@ test('Search highlights not cleared by scroll events', async ({ page }) => { const initialCount = await marks.count(); expect(initialCount).toBeGreaterThanOrEqual(2); - // Dispatch layout events immediately (previously cleared marks via quarto-hrChanged) - await page.evaluate(() => { - window.dispatchEvent(new CustomEvent('quarto-hrChanged')); - window.dispatchEvent(new CustomEvent('quarto-sectionChanged')); - }); - await expect(marks).toHaveCount(initialCount); - - // Wait and dispatch again — marks should persist at any time - await page.waitForTimeout(1500); - await page.evaluate(() => { - window.dispatchEvent(new CustomEvent('quarto-hrChanged')); - window.dispatchEvent(new CustomEvent('quarto-sectionChanged')); - }); + // Scroll the page — marks should not be cleared + await page.evaluate(() => window.scrollBy(0, 300)); + await page.waitForTimeout(500); await expect(marks).toHaveCount(initialCount); }); From 440b6547d7c4a22fd88f5aea159f94f024b5adfa Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 20 Feb 2026 18:15:56 +0100 Subject: [PATCH 6/6] Use role-based locators instead of Algolia CSS classes Replace .aa-DetachedSearchButton and .aa-Input with ARIA role locators (getByRole('button'), getByRole('searchbox')). These are resilient to Algolia autocomplete class name changes and follow Playwright best practices used elsewhere in the test suite. Co-Authored-By: Claude Opus 4.6 --- .../playwright/tests/html-search-highlight.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/playwright/tests/html-search-highlight.spec.ts b/tests/integration/playwright/tests/html-search-highlight.spec.ts index 29dcaac70b..5b326751f8 100644 --- a/tests/integration/playwright/tests/html-search-highlight.spec.ts +++ b/tests/integration/playwright/tests/html-search-highlight.spec.ts @@ -22,12 +22,12 @@ test('Search highlights cleared when query changes', async ({ page }) => { await expect(marks.first()).toBeVisible({ timeout: 5000 }); - // Open the detached search overlay - await page.locator('.aa-DetachedSearchButton').click(); - const input = page.locator('.aa-Input'); + // Open the search overlay and type a different query + await page.locator('#quarto-search').getByRole('button').click(); + const input = page.getByRole('searchbox'); await expect(input).toBeVisible({ timeout: 2000 }); - // Type a different query — triggers onStateChange which clears marks + // Typing a different query triggers onStateChange which clears marks await input.fill('different'); await expect(page.locator('main mark')).toHaveCount(0, { timeout: 2000 }); });