diff --git a/.vscode/markdoc.code-snippets b/.vscode/markdoc.code-snippets index 658ce3a63bc..5d669b83128 100644 --- a/.vscode/markdoc.code-snippets +++ b/.vscode/markdoc.code-snippets @@ -189,6 +189,13 @@ "description": "Markdoc underline tag" }, + "UI element": { + "scope": "markdoc", + "prefix": ";;ui", + "body": "{% ui %}${1:UI element name}{% /ui %}", + "description": "Markdoc ui tag for rendering references to UI elements" + }, + "Superscript": { "scope": "markdoc", "prefix": ";;sup", diff --git a/content/.gitignore b/content/.gitignore index b45e0256453..9bba0346b29 100644 --- a/content/.gitignore +++ b/content/.gitignore @@ -72,6 +72,9 @@ /en/dd_e2e/cdocs/integration/content_filtering.md /en/dd_e2e/cdocs/integration/headings_and_toc.md /en/dd_e2e/cdocs/integration/sticky_data.md +/en/agent/tooltip_test.md /en/dd_e2e/cdocs/integration/conditionally_displayed_filters/hide_if.md /en/dd_e2e/cdocs/integration/conditionally_displayed_filters/show_if.md /en/dd_e2e/cdocs/integration/dynamic_options.md +/en/dd_e2e/cdocs/components/glossary_tooltip.md +/en/dd_e2e/cdocs/components/ui.md diff --git a/content/en/dd_e2e/cdocs/_index.mdoc.md b/content/en/dd_e2e/cdocs/_index.mdoc.md index c1cdfa3c77b..50a26c5f772 100644 --- a/content/en/dd_e2e/cdocs/_index.mdoc.md +++ b/content/en/dd_e2e/cdocs/_index.mdoc.md @@ -82,6 +82,9 @@ This folder contains a collection of pages used for e2e tests. These pages are n * [Tooltip](/dd_e2e/cdocs/components/tooltip) * Variations of the tooltip component render as expected on initial page load. --- +* [UI](/dd_e2e/cdocs/components/ui) +* Variations of the ui component render as expected on initial page load. +--- * [Underline](/dd_e2e/cdocs/components/underline) * Variations of the underline component render as expected on initial page load. --- diff --git a/content/en/dd_e2e/cdocs/components/glossary_tooltip.mdoc.md b/content/en/dd_e2e/cdocs/components/glossary_tooltip.mdoc.md new file mode 100644 index 00000000000..dd1bbb421fa --- /dev/null +++ b/content/en/dd_e2e/cdocs/components/glossary_tooltip.mdoc.md @@ -0,0 +1,20 @@ +--- +title: Glossary tooltip +draft: true +--- + +## Case tests + +Default case: {% glossary-tooltip term="trace_context_propagation" /%} + +Title case: {% glossary-tooltip term="trace_context_propagation" case="title" /%} + +Title case (no short definition): {% glossary-tooltip term="snmp" case="title" /%} + +Sentence case: {% glossary-tooltip term="trace_context_propagation" case="sentence" /%} + +Sentence case (no short definition): {% glossary-tooltip term="snmp" case="sentence" /%} + +Lower case: {% glossary-tooltip term="trace_context_propagation" case="lower" /%} + +Upper case: {% glossary-tooltip term="trace_context_propagation" case="upper" /%} \ No newline at end of file diff --git a/content/en/dd_e2e/cdocs/components/ui.mdoc.md b/content/en/dd_e2e/cdocs/components/ui.mdoc.md new file mode 100644 index 00000000000..c3c75fbc386 --- /dev/null +++ b/content/en/dd_e2e/cdocs/components/ui.mdoc.md @@ -0,0 +1,23 @@ +--- +title: UI +draft: true +--- + +## Overview + +This page contains examples of the ui component. + + +## Examples + +### Button label + +Click {% ui %}Save{% /ui %} to save your changes. + +### Menu navigation + +Open the {% ui %}File{% /ui %} menu and select {% ui %}Open{% /ui %}. + +### UI element in a sentence + +To enable notifications, navigate to the {% ui %}Settings{% /ui %} page and toggle {% ui %}Notifications{% /ui %} on. diff --git a/content/en/dd_e2e/cdocs/integration/dynamic_options.mdoc.md b/content/en/dd_e2e/cdocs/integration/dynamic_options.mdoc.md index 80a8f7c0b70..578163470d4 100644 --- a/content/en/dd_e2e/cdocs/integration/dynamic_options.mdoc.md +++ b/content/en/dd_e2e/cdocs/integration/dynamic_options.mdoc.md @@ -5,7 +5,6 @@ content_filters: - trait_id: platform option_group_id: dd_e2e_platform_options - trait_id: prog_lang - label: language option_group_id: dd_e2e__prog_lang_options --- diff --git a/content/en/glossary/terms/extract_(ETL).md b/content/en/glossary/terms/extract_etl.md similarity index 95% rename from content/en/glossary/terms/extract_(ETL).md rename to content/en/glossary/terms/extract_etl.md index 61dc9380866..247e516b468 100644 --- a/content/en/glossary/terms/extract_(ETL).md +++ b/content/en/glossary/terms/extract_etl.md @@ -1,6 +1,5 @@ --- id: extract_etl title: extract, transform, and load (ETL) -related_terms: --- An established system where you pull data, transform the data, and load the output into a data warehouse. For more information, see the blog post. \ No newline at end of file diff --git a/content/es/glossary/terms/extract_(ETL).md b/content/es/glossary/terms/extract_(ETL).md index a7c18570f4c..e67c79cf5cb 100644 --- a/content/es/glossary/terms/extract_(ETL).md +++ b/content/es/glossary/terms/extract_(ETL).md @@ -1,6 +1,5 @@ --- id: extract_etl -related_terms: null title: extracción, transformación y carga (ETL) --- Un sistema establecido en el que se extraen datos, se transforman y se carga el resultado en un almacén de datos. Para obtener más información, consulta la entrada del blog. \ No newline at end of file diff --git a/content/fr/glossary/terms/extract_(ETL).md b/content/fr/glossary/terms/extract_(ETL).md index c63e1d89304..dea7c6ac5ab 100644 --- a/content/fr/glossary/terms/extract_(ETL).md +++ b/content/fr/glossary/terms/extract_(ETL).md @@ -1,6 +1,5 @@ --- id: extract_etl -related_terms: null title: extract, transform, and load (ETL) --- Un système établi dans lequel vous récupérez des données, les transformez et chargez le résultat dans un entrepôt de données. Pour plus d'informations, consultez l'article de blog à ce sujet. \ No newline at end of file diff --git a/content/ja/glossary/terms/extract_(ETL).md b/content/ja/glossary/terms/extract_(ETL).md index adf41b6e94f..7cdb31df18d 100644 --- a/content/ja/glossary/terms/extract_(ETL).md +++ b/content/ja/glossary/terms/extract_(ETL).md @@ -1,6 +1,5 @@ --- id: extract_etl -related_terms: null title: 抽出、変換、書き出し (ETL) --- データを抽出し、変換し、その結果をデータウェアハウスにロードする確立されたシステム。詳しくは、ブログ記事を参照してください。 \ No newline at end of file diff --git a/e2e/author-console/cdocs-author-console.spec.ts b/e2e/author-console/cdocs-author-console.spec.ts new file mode 100644 index 00000000000..d0123e3be97 --- /dev/null +++ b/e2e/author-console/cdocs-author-console.spec.ts @@ -0,0 +1,409 @@ +import { test, expect, type Locator, type Page } from '@playwright/test'; +import errorsOverlay from './fixtures/errors-overlay.json'; +import { + freezeClock, + patchConsoleData, + selectAutocompleteOption, + waitForConsole +} from './helpers'; + +const CONSOLE_URL = '/cdocs/console/'; +const FROZEN_NOW = '2026-04-20T12:00:00Z'; +const FROZEN_BUILD_TIMESTAMP = Date.parse('2026-04-20T11:59:30Z'); + +/** + * Shared setup: freeze the clock, patch data.json to pin the build timestamp, + * then navigate. Routes / init scripts must be registered before goto. + */ +async function setupConsole( + page: Page, + opts: { errorsByFilePath?: Record; tab?: string } = {} +) { + await freezeClock(page, FROZEN_NOW); + await patchConsoleData(page, { + timestamp: FROZEN_BUILD_TIMESTAMP, + errorsByFilePath: opts.errorsByFilePath + }); + const url = opts.tab ? `${CONSOLE_URL}?tab=${opts.tab}` : CONSOLE_URL; + await page.goto(url); + await waitForConsole(page); +} + +/** Locate the MUI Accordion that contains a summary button with `title` text. */ +function accordionByTitle(page: Page, title: string): Locator { + return page + .getByRole('button', { name: title, exact: true }) + .locator('xpath=ancestor::*[contains(@class, "MuiAccordion-root")][1]'); +} + +// --- Build errors tab --- + +test.describe('Build errors tab', () => { + test('no-errors state shows success alert', async ({ page }) => { + await setupConsole(page, { tab: 'build-errors' }); + await expect(page.getByText('The latest cdocs build has no errors.')).toBeVisible(); + await expect(page.getByText('Compilation errors')).toBeHidden(); + }); + + test('no-errors state snapshot', async ({ page }) => { + await setupConsole(page, { tab: 'build-errors' }); + await expect(page).toHaveScreenshot('build-errors-no-errors.png'); + }); + + test('errors state shows alert and compilation errors heading', async ({ page }) => { + await setupConsole(page, { tab: 'build-errors', errorsByFilePath: errorsOverlay }); + await expect(page.getByText('The latest cdocs build has errors.')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Compilation errors' })).toBeVisible(); + }); + + test('errors state renders one Paper per file with VS Code links', async ({ page }) => { + await setupConsole(page, { tab: 'build-errors', errorsByFilePath: errorsOverlay }); + for (const filePath of Object.keys(errorsOverlay)) { + const filename = filePath.split('/').pop()!; + await expect(page.getByRole('heading', { level: 3, name: filename })).toBeVisible(); + await expect(page.locator(`a[href^="vscode://file/${filePath}"]`).first()).toBeVisible(); + } + }); + + test('errors state renders each error message as a row', async ({ page }) => { + await setupConsole(page, { tab: 'build-errors', errorsByFilePath: errorsOverlay }); + const expectedMessages = Object.values(errorsOverlay).flatMap((errors) => + errors.map((e) => e.message) + ); + for (const msg of expectedMessages) { + await expect(page.getByRole('cell', { name: msg })).toBeVisible(); + } + }); + + test('errors state snapshot', async ({ page }) => { + await setupConsole(page, { tab: 'build-errors', errorsByFilePath: errorsOverlay }); + await expect(page).toHaveScreenshot('build-errors-with-errors.png'); + }); +}); + +// --- Quick filter tab --- + +test.describe('Quick filter tab', () => { + const traitInput = (page: Page) => + page.getByPlaceholder('Type here to search available traits'); + const optionGroupInput = (page: Page) => + page.getByPlaceholder('Type here to search available option groups'); + + test.beforeEach(async ({ page }) => { + await setupConsole(page, { tab: 'quick-filter' }); + }); + + test('initial render has no generated markup', async ({ page }) => { + await expect(page.getByRole('heading', { name: 'Filter configuration' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Generated markup' })).toBeHidden(); + }); + + test('initial snapshot', async ({ page }) => { + await expect(page).toHaveScreenshot('quick-filter-initial.png'); + }); + + test('selecting only a trait does not generate markup', async ({ page }) => { + await selectAutocompleteOption(traitInput(page), 'database', /\(id: `database`/); + await expect(page.getByRole('heading', { name: 'Generated markup' })).toBeHidden(); + }); + + test('selecting a trait and option group generates frontmatter and if blocks', async ({ + page + }) => { + await selectAutocompleteOption(traitInput(page), 'database', /\(id: `database`/); + await selectAutocompleteOption( + optionGroupInput(page), + 'dd_e2e_database_options', + /dd_e2e_database_options/ + ); + + await expect(page.getByRole('heading', { name: 'Generated markup' })).toBeVisible(); + + // First code block is the yaml frontmatter, second is the if-blocks example. + const codeBlocks = page.locator('pre'); + await expect(codeBlocks.nth(0)).toContainText('trait_id: database'); + await expect(codeBlocks.nth(0)).toContainText('option_group_id: dd_e2e_database_options'); + + await expect(codeBlocks.nth(1)).toContainText('equals($database, "postgres")'); + await expect(codeBlocks.nth(1)).toContainText('equals($database, "mysql")'); + await expect(codeBlocks.nth(1)).toContainText('equals($database, "mongo_db")'); + }); + + test('snapshot after lookup completes', async ({ page }) => { + await selectAutocompleteOption(traitInput(page), 'database', /\(id: `database`/); + await selectAutocompleteOption( + optionGroupInput(page), + 'dd_e2e_database_options', + /dd_e2e_database_options/ + ); + await expect(page.getByRole('heading', { name: 'Generated markup' })).toBeVisible(); + + await expect(page).toHaveScreenshot('quick-filter-after-lookup.png'); + }); +}); + +// --- Page wizard tab --- + +test.describe('Page wizard tab', () => { + test.beforeEach(async ({ page }) => { + await setupConsole(page, { tab: 'page-wizard' }); + }); + + test('initial render has only Add filter button', async ({ page }) => { + await expect(page.getByRole('heading', { level: 1, name: 'Filters' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Add filter' })).toBeVisible(); + await expect(page.getByText('No filters added yet.')).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Setup instructions' })).toBeHidden(); + }); + + test('initial snapshot', async ({ page }) => { + await expect(page).toHaveScreenshot('page-wizard-initial.png'); + }); + + test.describe('existing-config path', () => { + async function addExistingFilter(page: Page) { + await page.getByRole('button', { name: 'Add filter' }).click(); + await selectAutocompleteOption( + page.getByPlaceholder('Type here to search existing traits'), + 'prog_lang', + /\(id: `prog_lang`/ + ); + await selectAutocompleteOption( + page.getByPlaceholder('Type here to search existing option groups'), + 'dd_e2e_backend_prog_lang_options', + /dd_e2e_backend_prog_lang_options/ + ); + // Filter-level Save is the last "Save" in the DOM (outside any accordion). + const filterLevelSave = page.getByRole('button', { name: 'Save', exact: true }).last(); + await expect(filterLevelSave).toBeEnabled(); + await filterLevelSave.click(); + await expect(page.getByRole('heading', { name: 'Setup instructions' })).toBeVisible(); + } + + test('pick existing trait and existing option group', async ({ page }) => { + await addExistingFilter(page); + + // Filter row summary shows the option labels joined. + await expect(page.getByText(/JavaScript, Python, Ruby, Go, Java/)).toBeVisible(); + + // No new config needed → YamlConfigInstructions returns null. + await expect( + page.getByRole('heading', { + name: 'Add the required YAML configuration to the docs site' + }) + ).toBeHidden(); + + // mdoc template references the filter. + const mdocCode = page.locator('pre').filter({ hasText: 'trait_id: prog_lang' }); + await expect(mdocCode).toBeVisible(); + await expect(mdocCode).toContainText('option_group_id: dd_e2e_backend_prog_lang_options'); + }); + + test('filters section snapshot', async ({ page }) => { + await addExistingFilter(page); + const filtersSection = page + .getByRole('heading', { level: 1, name: 'Filters' }) + .locator('xpath=..'); + await expect(filtersSection).toHaveScreenshot( + 'page-wizard-existing-filters-section.png' + ); + }); + + test('setup instructions snapshot', async ({ page }) => { + await addExistingFilter(page); + const setupSection = page + .getByRole('heading', { name: 'Setup instructions' }) + .locator('xpath=..'); + await expect(setupSection).toHaveScreenshot( + 'page-wizard-existing-setup-instructions.png' + ); + }); + }); + + test.describe('new-config path (mirrors twoNewFilters.test.ts)', () => { + const monsterMovieSpec: NewFilterSpec = { + traitId: 'favorite_monster_movie', + traitLabel: 'Favorite monster movie', + optionGroupId: 'favorite_monster_movie_options', + options: [ + { id: 'jaws', label: 'Jaws' }, + { id: 'godzilla', label: 'Godzilla' } + ] + }; + const movieCandySpec: NewFilterSpec = { + traitId: 'favorite_movie_candy', + traitLabel: 'Favorite movie candy', + optionGroupId: 'favorite_movie_candy_options', + options: [ + { id: 'twizzlers', label: 'Twizzlers' }, + { id: 'skittles', label: 'Skittles' } + ] + }; + + async function buildBothFilters(page: Page) { + await buildNewFilter(page, monsterMovieSpec); + await buildNewFilter(page, movieCandySpec); + await expect(page.getByRole('heading', { name: 'Setup instructions' })).toBeVisible(); + } + + // Each YAML code block follows its h3 — scope by the heading for clarity. + const codeAfter = (page: Page, headingName: string) => + page + .getByRole('heading', { name: headingName }) + .locator('xpath=following-sibling::pre[1]'); + + test('filter rows, YAML, and mdoc template all render', async ({ page }) => { + await buildBothFilters(page); + + await expect(page.getByText(/Favorite monster movie: Jaws, Godzilla/)).toBeVisible(); + await expect(page.getByText(/Favorite movie candy: Twizzlers, Skittles/)).toBeVisible(); + + const traitsYaml = codeAfter(page, 'Add new traits'); + await expect(traitsYaml).toContainText('- id: favorite_monster_movie'); + await expect(traitsYaml).toContainText('- id: favorite_movie_candy'); + + const optionsYaml = codeAfter(page, 'Add new options'); + await expect(optionsYaml).toContainText('- label: Jaws'); + await expect(optionsYaml).toContainText('- label: Godzilla'); + await expect(optionsYaml).toContainText('- label: Twizzlers'); + await expect(optionsYaml).toContainText('- label: Skittles'); + + const optionGroupsYaml = codeAfter(page, 'Add new option groups'); + await expect(optionGroupsYaml).toContainText('favorite_monster_movie_options:'); + await expect(optionGroupsYaml).toContainText('favorite_movie_candy_options:'); + // Each group's first option is marked default: true + await expect(optionGroupsYaml).toContainText('default: true'); + + const mdocCode = codeAfter(page, 'Create the page'); + await expect(mdocCode).toContainText('trait_id: favorite_monster_movie'); + await expect(mdocCode).toContainText('trait_id: favorite_movie_candy'); + }); + + test('filled-out filter form snapshot (before final submit)', async ({ page }) => { + await buildNewFilter(page, monsterMovieSpec, { skipFilterSave: true }); + + // The filter row summary shows the filled-in data before we submit. + await expect( + page.getByText(/Favorite monster movie: Jaws, Godzilla/) + ).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Setup instructions' })).toBeHidden(); + + const filtersSection = page + .getByRole('heading', { level: 1, name: 'Filters' }) + .locator('xpath=..'); + await expect(filtersSection).toHaveScreenshot( + 'page-wizard-new-filter-filled-out.png' + ); + }); + + test('YAML traits section snapshot', async ({ page }) => { + await buildBothFilters(page); + await expect(codeAfter(page, 'Add new traits')).toHaveScreenshot( + 'page-wizard-new-yaml-traits.png' + ); + }); + + test('YAML options section snapshot', async ({ page }) => { + await buildBothFilters(page); + await expect(codeAfter(page, 'Add new options')).toHaveScreenshot( + 'page-wizard-new-yaml-options.png' + ); + }); + + test('YAML option groups section snapshot', async ({ page }) => { + await buildBothFilters(page); + await expect(codeAfter(page, 'Add new option groups')).toHaveScreenshot( + 'page-wizard-new-yaml-option-groups.png' + ); + }); + + test('mdoc template snapshot', async ({ page }) => { + await buildBothFilters(page); + await expect(codeAfter(page, 'Create the page')).toHaveScreenshot( + 'page-wizard-new-mdoc-template.png' + ); + }); + }); +}); + +// --- Cross-tab --- + +test.describe('Tab navigation', () => { + test('clicking tabs updates the URL query param', async ({ page }) => { + await setupConsole(page); + + await page.getByRole('tab', { name: 'Quick filter' }).click(); + await expect(page).toHaveURL(/[?&]tab=quick-filter/); + + await page.getByRole('tab', { name: 'Page wizard' }).click(); + await expect(page).toHaveURL(/[?&]tab=page-wizard/); + + await page.getByRole('tab', { name: 'Build errors' }).click(); + await expect(page).toHaveURL(/[?&]tab=build-errors/); + }); + + test('loading with ?tab=page-wizard selects the Page wizard tab', async ({ page }) => { + await setupConsole(page, { tab: 'page-wizard' }); + await expect(page.getByRole('tab', { name: 'Page wizard' })).toHaveAttribute( + 'aria-selected', + 'true' + ); + }); +}); + +// --- Builder helper for new-config filters --- + +interface NewFilterSpec { + traitId: string; + traitLabel: string; + optionGroupId: string; + options: { id: string; label: string }[]; +} + +/** + * Build one filter entirely from new config: new trait, new option group + * with new options. Mirrors the UI path exercised in twoNewFilters.test.ts. + * + * When `skipFilterSave` is set, stops after the option group is saved — + * leaves the filter row summary visible alongside an enabled filter-level + * Save button so callers can snapshot the pre-submit state. + */ +async function buildNewFilter( + page: Page, + spec: NewFilterSpec, + { skipFilterSave = false }: { skipFilterSave?: boolean } = {} +) { + await page.getByRole('button', { name: 'Add filter' }).click(); + + // --- New trait --- + const traitAccordion = accordionByTitle(page, 'Add a new trait'); + await traitAccordion.getByRole('button', { name: 'Add a new trait' }).click(); + await page.getByPlaceholder('e.g., prog_lang').fill(spec.traitId); + await page.getByPlaceholder('e.g., Programming language').fill(spec.traitLabel); + const traitSaveButton = traitAccordion.getByRole('button', { name: 'Save', exact: true }); + await expect(traitSaveButton).toBeEnabled(); + await traitSaveButton.click(); + + // --- New option group --- + const groupAccordion = accordionByTitle(page, 'Add a new option group'); + await groupAccordion.getByRole('button', { name: 'Add a new option group' }).click(); + await page.getByPlaceholder('e.g., rum_sdk_platform_options').fill(spec.optionGroupId); + + for (const option of spec.options) { + const optionAccordion = accordionByTitle(page, 'Add a new option'); + await optionAccordion.getByRole('button', { name: 'Add a new option' }).click(); + await page.getByPlaceholder('e.g., amazon_ec2').fill(option.id); + await page.getByPlaceholder('e.g., Amazon EC2').fill(option.label); + await optionAccordion.getByRole('button', { name: 'Add option to selection' }).click(); + } + + const groupSaveButton = groupAccordion.getByRole('button', { name: 'Save', exact: true }); + await expect(groupSaveButton).toBeEnabled(); + await groupSaveButton.click(); + + // --- Filter-level Save (outside any accordion — last Save in DOM) --- + const filterLevelSave = page.getByRole('button', { name: 'Save', exact: true }).last(); + await expect(filterLevelSave).toBeEnabled(); + if (skipFilterSave) return; + await filterLevelSave.click(); +} diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/build-errors-no-errors-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/build-errors-no-errors-chromium-darwin.png new file mode 100644 index 00000000000..d3225d31752 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/build-errors-no-errors-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/build-errors-with-errors-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/build-errors-with-errors-chromium-darwin.png new file mode 100644 index 00000000000..6b7eed7cdef Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/build-errors-with-errors-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-existing-filters-section-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-existing-filters-section-chromium-darwin.png new file mode 100644 index 00000000000..a6f70750daf Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-existing-filters-section-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-existing-setup-instructions-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-existing-setup-instructions-chromium-darwin.png new file mode 100644 index 00000000000..03c72a69902 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-existing-setup-instructions-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-initial-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-initial-chromium-darwin.png new file mode 100644 index 00000000000..20460f90c9a Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-initial-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-filter-filled-out-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-filter-filled-out-chromium-darwin.png new file mode 100644 index 00000000000..03f30e479c7 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-filter-filled-out-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-mdoc-template-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-mdoc-template-chromium-darwin.png new file mode 100644 index 00000000000..6e406bbc1b4 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-mdoc-template-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-option-groups-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-option-groups-chromium-darwin.png new file mode 100644 index 00000000000..024650a46bd Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-option-groups-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-options-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-options-chromium-darwin.png new file mode 100644 index 00000000000..cc1b250e05f Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-options-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-traits-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-traits-chromium-darwin.png new file mode 100644 index 00000000000..7bfa562fd13 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/page-wizard-new-yaml-traits-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/quick-filter-after-lookup-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/quick-filter-after-lookup-chromium-darwin.png new file mode 100644 index 00000000000..a651d11c6a3 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/quick-filter-after-lookup-chromium-darwin.png differ diff --git a/e2e/author-console/cdocs-author-console.spec.ts-snapshots/quick-filter-initial-chromium-darwin.png b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/quick-filter-initial-chromium-darwin.png new file mode 100644 index 00000000000..f9d854f6331 Binary files /dev/null and b/e2e/author-console/cdocs-author-console.spec.ts-snapshots/quick-filter-initial-chromium-darwin.png differ diff --git a/e2e/author-console/fixtures/errors-overlay.json b/e2e/author-console/fixtures/errors-overlay.json new file mode 100644 index 00000000000..091f4a572ea --- /dev/null +++ b/e2e/author-console/fixtures/errors-overlay.json @@ -0,0 +1,22 @@ +{ + "/Users/tester/repo/content/en/example/page_one.mdoc.md": [ + { + "message": "Filter references unknown option group `fake_options`.", + "data": { + "lines": [12], + "searchTerm": "fake_options" + } + }, + { + "message": "Trait `nonexistent_trait` is not defined in the customization config.", + "data": { + "lines": [15, 18] + } + } + ], + "/Users/tester/repo/content/en/example/page_two.mdoc.md": [ + { + "message": "Duplicate `content_filters` entry for trait `prog_lang`." + } + ] +} diff --git a/e2e/author-console/helpers.ts b/e2e/author-console/helpers.ts new file mode 100644 index 00000000000..61f28a97a62 --- /dev/null +++ b/e2e/author-console/helpers.ts @@ -0,0 +1,80 @@ +import { expect, type Locator, type Page } from '@playwright/test'; + +// --- Route patching --- + +/** + * Intercept the console's `data.json` request, fetch the live response, + * and overwrite just the bits the tests depend on. Leaves the real + * customizationConfig untouched so dropdown contents track the live build. + */ +export async function patchConsoleData( + page: Page, + overrides: { + timestamp: number; + errorsByFilePath?: Record; + } +) { + await page.route('**/cdocs/console/data.json', async (route) => { + const response = await route.fetch(); + const json = await response.json(); + json.timestamp = overrides.timestamp; + if (overrides.errorsByFilePath) { + json.errorsByFilePath = overrides.errorsByFilePath; + } + await route.fulfill({ response, json }); + }); +} + +/** + * Freeze `Date.now()` (and `new Date()` called with no args) to a fixed + * instant so `ReactTimeAgo` renders deterministic text like "1 minute ago" + * regardless of wall-clock drift between the patched `timestamp` and the + * moment the test actually runs. + */ +export async function freezeClock(page: Page, iso: string) { + const fixedNow = Date.parse(iso); + await page.addInitScript((now: number) => { + const RealDate = Date; + class FrozenDate extends RealDate { + constructor(...args: ConstructorParameters) { + if (args.length === 0) { + super(now); + } else { + // @ts-expect-error — forwarding variadic args to Date + super(...args); + } + } + static now() { + return now; + } + } + // @ts-expect-error — replacing the global + globalThis.Date = FrozenDate; + }, fixedNow); +} + +// --- MUI Autocomplete --- + +/** + * Select an option from a MUI Autocomplete by typing a substring of its label. + * Works for both strict (single-value) and flexible (create-new) selectors + * since all of the console's dropdowns share the same Autocomplete base. + */ +export async function selectAutocompleteOption( + input: Locator, + search: string, + optionText: string | RegExp +) { + await input.click(); + await input.fill(search); + const option = input.page().getByRole('option', { name: optionText }).first(); + await expect(option).toBeVisible(); + await option.click(); +} + +// --- Page chrome --- + +/** Wait for the SPA to finish mounting — `

` only renders after fetch. */ +export async function waitForConsole(page: Page) { + await page.waitForSelector('h1#title'); +} diff --git a/e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts b/e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts new file mode 100644 index 00000000000..28fd23dfe0c --- /dev/null +++ b/e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts @@ -0,0 +1,76 @@ +import { test, expect, type Page, type Locator } from '@playwright/test'; +import { hideOverlays } from '../../helpers'; + +const PAGE_URL = '/dd_e2e/cdocs/components/glossary_tooltip/'; +const CONTENT_AREA = '#mainContent'; + +function paragraph(page: Page, label: string): Locator { + return page.locator('p', { hasText: label }); +} + +test.describe('Cdocs glossary-tooltip component', () => { + test.beforeEach(async ({ page }) => { + await page.goto(PAGE_URL); + await page.waitForSelector(CONTENT_AREA); + await hideOverlays(page); + }); + + test('page renders as expected', async ({ page }) => { + await expect(page.locator(CONTENT_AREA)).toHaveScreenshot('glossary-tooltip-initial.png'); + }); + + test('default case renders glossary title unchanged', async ({ page }) => { + const trigger = paragraph(page, 'Default case:').locator('.tooltip-trigger'); + await expect(trigger).toHaveText('trace context propagation'); + }); + + test('casing is applied to the tooltip trigger text', async ({ page }) => { + const expected: [string, string][] = [ + ['Title case:', 'Trace Context Propagation'], + ['Sentence case:', 'Trace context propagation'], + ['Lower case:', 'trace context propagation'], + ['Upper case:', 'TRACE CONTEXT PROPAGATION'] + ]; + + for (const [label, text] of expected) { + const trigger = paragraph(page, label).locator('.tooltip-trigger'); + await expect(trigger).toHaveText(text); + } + }); + + test('no-short-definition renders title as plain text, no tooltip wrapper', async ({ page }) => { + const expected: [string, string][] = [ + ['Title case (no short definition):', 'Simple Network Management Protocol (snmp)'], + ['Sentence case (no short definition):', 'Simple network management protocol (snmp)'] + ]; + + for (const [label, text] of expected) { + const para = paragraph(page, label); + await expect(para).toContainText(text); + await expect(para.locator('.tooltip-container')).toHaveCount(0); + } + }); + + test('hover reveals the tooltip popup with the short definition', async ({ page }) => { + const trigger = paragraph(page, 'Default case:').locator('.tooltip-trigger'); + + await trigger.hover(); + // tooltip.js lifts the popup to on hover (escapes ancestor overflow:hidden). + const popup = page.locator('body > .tooltip-content.show'); + await expect(popup).toBeVisible(); + await expect(popup).toContainText('Trace context propagation is the method of passing trace identifiers'); + }); + + test('tooltip contains a working link to the glossary entry', async ({ page }) => { + const para = paragraph(page, 'Default case:'); + const link = para.locator('a.tooltip-full-link'); + + await expect(link).toHaveAttribute('href', /\/glossary\/#trace_context_propagation$/); + await expect(link).toHaveText('Glossary'); + + await para.locator('.tooltip-trigger').hover(); + await page.locator('body > .tooltip-content.show a.tooltip-full-link').click(); + await expect(page).toHaveURL(/\/glossary\/#trace_context_propagation$/); + }); + +}); diff --git a/e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts-snapshots/glossary-tooltip-initial-chromium-darwin.png b/e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts-snapshots/glossary-tooltip-initial-chromium-darwin.png new file mode 100644 index 00000000000..06d320e0c3a Binary files /dev/null and b/e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts-snapshots/glossary-tooltip-initial-chromium-darwin.png differ diff --git a/e2e/components/ui/cdocs-ui.spec.ts b/e2e/components/ui/cdocs-ui.spec.ts new file mode 100644 index 00000000000..efac19243f0 --- /dev/null +++ b/e2e/components/ui/cdocs-ui.spec.ts @@ -0,0 +1,17 @@ +import { test, expect } from '@playwright/test'; +import { hideOverlays } from '../../helpers'; + +const PAGE_URL = '/dd_e2e/cdocs/components/ui/'; +const CONTENT_AREA = '#mainContent'; + +test.describe('Cdocs ui component', () => { + test.beforeEach(async ({ page }) => { + await page.goto(PAGE_URL); + await page.waitForSelector(CONTENT_AREA); + await hideOverlays(page); + }); + + test('page renders as expected', async ({ page }) => { + await expect(page.locator(CONTENT_AREA)).toHaveScreenshot('ui-initial.png'); + }); +}); diff --git a/e2e/components/ui/cdocs-ui.spec.ts-snapshots/ui-initial-chromium-darwin.png b/e2e/components/ui/cdocs-ui.spec.ts-snapshots/ui-initial-chromium-darwin.png new file mode 100644 index 00000000000..8391d2bd308 Binary files /dev/null and b/e2e/components/ui/cdocs-ui.spec.ts-snapshots/ui-initial-chromium-darwin.png differ diff --git a/e2e/plans/author-console.md b/e2e/plans/author-console.md new file mode 100644 index 00000000000..b490403512b --- /dev/null +++ b/e2e/plans/author-console.md @@ -0,0 +1,199 @@ +# Author console e2e testing + +## Prompt + +When the local docs build is running, the Cdocs author console is available at `http://localhost:1313/cdocs/console/`. No Playwright tests currently exist for the console, so we should add them. + +The code for the console, including recently added features, can be found at this path: `~/dd/corp-node-packages/packages/cdocs-author-console`. + +Add e2e tests for each of the tabs in the console, verifying that the build has no errors and that the configuration building/lookup features work as expected. In the tests, when you need to refer to an existing option group, use one of the groups set up specifically for e2e testing, since other option groups can change. It's okay to use any traits and options available, as those usually persist. + +In the Page Wizard, test both the use of existing config, and the creation of fully new config. You can draw from `~/dd/corp-node-packages/packages/cdocs-author-console/test/twoNewFilters.test.ts` to derive test inputs for the author console UI. + +Take snapshots of each other console tab at load, and after the task has been completed where relevant (for example, after a Quick Lookup has been completed in the UI). In cases when the snapshot would be quite large, as is the case with the Page Wizard, snapshot areas of the page separately. For example, the form has distinct input areas, and the resulting instructions also have distinct sections that can be snapshotted. + +## Claude's plan + +### Context + +The Cdocs author console is a standalone React SPA (Vite build, MUI components) served by Hugo as a static asset from [static/cdocs/console/index.html](static/cdocs/console/index.html). On page load it fetches [static/cdocs/console/data.json](static/cdocs/console/data.json) — which `make build-cdocs` regenerates — and renders three tabs: + +1. **Build errors** — success alert, or `ErrorsReport` grouped by file path. +2. **Quick filter** — pair an *existing* trait + *existing* option group; renders generated frontmatter and example `if` blocks. Source: [QuickFilterBuilder.tsx](../../../../dd/corp-node-packages/packages/cdocs-author-console/src/components/QuickFilterBuilder.tsx). +3. **Page wizard** — build one or more filters using a mix of existing and brand-new traits/option groups/options, then render `SetupInstructions` (YAML config blocks + mdoc markup template). Source: [PageWizard.tsx](../../../../dd/corp-node-packages/packages/cdocs-author-console/src/components/PageWizard.tsx). + +The tab is reflected in the URL as `?tab=build-errors|quick-filter|page-wizard`, so tests can deep-link instead of clicking. + +Fixture option groups that were created specifically for e2e testing (won't churn) live in [customization_config/en/option_groups/dd_e2e_testing.yaml](customization_config/en/option_groups/dd_e2e_testing.yaml): + +``` +dd_e2e_mobile_prog_lang_options, dd_e2e_backend_prog_lang_options, +dd_e2e_database_options, dd_e2e_postgres_host_options, dd_e2e_mysql_host_options, +dd_e2e_mongo_db_host_options, dd_e2e_platform_options, +dd_e2e_ios_prog_lang_options, dd_e2e_android_prog_lang_options +``` + +Traits are not e2e-owned; per the prompt we can use any available trait (e.g. `prog_lang`, `database`, `platform`), since `data.json` is populated from the full [customization_config/en/traits/](customization_config/en/traits/) tree and these are stable. + +### Key obstacles to plan around + +1. **The last-build timestamp is dynamic.** `App.tsx` renders `` in the h1 and colors it red after 5 minutes. Direct snapshots drift every run. +2. **Production `data.json` has no errors.** We can't assert on the error state without mocking. +3. **`data.json` contents change** when `make build-cdocs` runs, so autocomplete dropdown contents for traits/options also drift. +4. **MUI Autocomplete** doesn't expose options until you click (or focus + ArrowDown). Searching by typing is the most reliable interaction. + +Solution for (1)–(2): intercept `data.json` with `page.route()` in `beforeEach` and **patch the live response** rather than replace it. Fetch the real JSON (`await route.fetch()`), then: + +- Overwrite `timestamp` with a fixed value (pins the `ReactTimeAgo` output). +- For the error-state test only, splice a couple of sample entries into `errorsByFilePath`. + +Leave `customizationConfig` untouched. This means the config drifts with the live build — that's fine for tests written against `dd_e2e_*` option groups (stable by design) and against traits like `prog_lang` / `database` / `platform` (long-lived). If one of those is ever renamed, the test fails loudly and we update the single constant at the top of the spec — cheaper than maintaining a frozen blob. + +The patch helper lives in `e2e/author-console/helpers.ts`; the only checked-in fixture is a tiny `errors-overlay.json` containing just the `errorsByFilePath` object to splice in. + +### Layout + +New directory (mirrors existing `e2e/components/*/` and `e2e/integration/*/` layout): + +``` +e2e/author-console/ + cdocs-author-console.spec.ts + cdocs-author-console.spec.ts-snapshots/ + fixtures/ + errors-overlay.json // just the errorsByFilePath blob to splice in + helpers.ts // Autocomplete + route patch helpers +``` + +`e2e/author-console/helpers.ts` holds console-specific helpers; [e2e/helpers.ts](e2e/helpers.ts) is for Hugo-rendered cdoc pages (pills, toggleables) and doesn't belong here. + +### Fixtures + +- **`errors-overlay.json`**: just an `errorsByFilePath` object with two sample paths, each mapped to a `CdocsCoreError[]`. Inspect `node_modules/cdocs-data/dist/...` (or `cdocs-data`'s published types) for the required shape before writing — don't guess fields. This is the only hand-maintained JSON. + +### Helpers to write + +`e2e/author-console/helpers.ts`: + +- `patchConsoleData(page, { timestamp, errorsByFilePath? })` — registers `page.route('**/data.json', async route => { const r = await route.fetch(); const json = await r.json(); json.timestamp = timestamp; if (errorsByFilePath) json.errorsByFilePath = errorsByFilePath; await route.fulfill({ response: r, json }); })`. Leaves `customizationConfig` as-is so the live build feeds the app. +- `freezeClock(page, iso)` — `await page.addInitScript` overriding `Date.now` and `new Date()` to return a fixed time. Required so `ReactTimeAgo` renders a deterministic string ("1 minute ago") regardless of wall-clock drift between the patched timestamp and the test run. +- `selectAutocomplete(page, locator, optionLabelSubstring)` — clicks the input, types the substring, waits for `role=listbox`, clicks the matching `role=option`. Covers `StrictTraitSelector`, `StrictOptionGroupSelector`, `FlexibleTraitSelector`, `FlexibleOptionGroupSelector`. +- `openAccordion(page, summaryText)` / `clickAccordionButton(page, summaryText, buttonText)` — thin wrappers that find the accordion by its summary text (e.g. `"Add a new trait"`) and click inside it. MUI wraps accordions in a way that `getByRole` alone is awkward. +- `screenshotSection(page, locator, name, opts?)` — wraps `expect(locator).toHaveScreenshot(name, opts)` with a default `mask: [page.locator('#title span')]` so the last-build line is never part of the image even if a test forgets `freezeClock`. + +### Tests + +Pattern-match on [e2e/components/tabs/cdocs-tabs.spec.ts](e2e/components/tabs/cdocs-tabs.spec.ts): snapshot + targeted assertions, one behavior per test. + +**Shared setup** (`test.beforeEach`): + +```ts +await freezeClock(page, '2026-04-20T12:00:00Z'); +await patchConsoleData(page, { timestamp: Date.parse('2026-04-20T11:59:30Z') }); +await page.goto('/cdocs/console/'); +await page.waitForSelector('h1#title'); +``` + +The error-state test re-registers the route before `page.goto` with `patchConsoleData(page, { timestamp: ..., errorsByFilePath: require('./fixtures/errors-overlay.json') })`. + +#### Build errors tab + +1. **No-errors state snapshot** (default fixture) — navigate to `?tab=build-errors`, snapshot `main` (or the tab panel). Assert the success alert text `The latest cdocs build has no errors.` is visible. +2. **Errors state snapshot** — override with `data-with-errors.json`, navigate, snapshot. Assert: + - The error alert title `The latest cdocs build has errors.` is visible. + - `h2` "Compilation errors" is visible. + - For each fixture file path, a `Paper` contains an `

` with the filename, a "copy full path" link, and an "open file in VS Code" link with `href` starting `vscode://file/`. + - The errors table renders one row per `CdocsCoreError`. + +#### Quick filter tab + +3. **Initial snapshot** — navigate to `?tab=quick-filter`, snapshot the tab panel. Assert no `

Generated markup

` yet. +4. **Trait-only selection does not generate markup** — pick a trait (e.g. `database`). Assert the form still shows no "Generated markup" heading. +5. **Full lookup generates markup** — pick trait `database`, then option group `dd_e2e_database_options`. Assert: + - `

Generated markup

` appears. + - The frontmatter `` block contains `trait_id: database` and `option_group_id: dd_e2e_database_options`. + - The `if` blocks `` block contains `eq($database, "postgres")` (first option + default) and `eq($database, "mysql")`. +6. **Post-lookup snapshot** — snapshot the full tab panel after step 5 completes, to cover both the generated YAML and the example `if` blocks. + +#### Page wizard tab — existing config path + +7. **Initial snapshot** — navigate to `?tab=page-wizard`, snapshot the tab panel. Assert the intro copy and that only an "Add filter" button is visible. +8. **Add filter using existing trait + existing option group**: + - Click "Add filter". + - In `FlexibleTraitSelector`, select existing trait `prog_lang` via the top Autocomplete (do not open the "Add a new trait" accordion). + - In `FlexibleOptionGroupSelector`, select `dd_e2e_backend_prog_lang_options`. + - Click "Save". + - Assert the filter row summary reads something like `Programming Language: JavaScript, Python, Ruby, Go, Java`. + - Assert `

Setup instructions

` renders. + - Because no new config is required, assert `

Add the required YAML configuration to the docs site

` is **absent** (YamlConfigInstructions returns null). + - Snapshot **the "Filters" area** and **the "Create the page" mdoc template** as separate images (one per logical section, to keep diffs small). + +#### Page wizard tab — all-new config path (mirrors `twoNewFilters.test.ts`) + +9. **Build a brand-new filter: `favorite_monster_movie`**. + - Click "Add filter". + - Open the "Add a new trait" accordion. Fill ID=`favorite_monster_movie`, label=`Favorite monster movie`, type=`Text`, notes blank. Click "Save". + - Open the "Add a new option group" accordion. Fill ID=`favorite_monster_movie_options`. + - Open the inner "Add a new option" accordion. Create `jaws` / `Jaws`, save. Then `godzilla` / `Godzilla`, save. + - The autocomplete multi-select should now show both options; click outside to dismiss, then click the outer "Save" on the option group accordion. + - Click the filter-level "Save". +10. **Build a second brand-new filter: `favorite_movie_candy`** (`twizzlers` default + `skittles`). +11. **Assertions after both filters are saved**: + - Two filter rows are listed, with summaries in order. + - `

Setup instructions

` renders. + - `YamlConfigInstructions` renders three sub-sections: **Add new traits** (yaml contains `favorite_monster_movie:` and `favorite_movie_candy:`), **Add new options** (contains `jaws:`, `godzilla:`, `twizzlers:`, `skittles:`), **Add new option groups** (contains both `_options` keys, with `default: true` on the first option of each). + - The mdoc template (`CdocMarkupTemplate`) contains both `trait_id: favorite_monster_movie` and `trait_id: favorite_movie_candy`. +12. **Section snapshots (per prompt guidance — avoid one huge image)**: + - Form input area (Filters + any open filter builder) — taken mid-way through step 9 while the accordions are open, to exercise the form layout. + - Filters list after both filters saved. + - YAML config instructions — one snapshot per sub-section (`traits`, `options`, `option groups`) targeted by the `

` and its immediate sibling `
`.
+    - Mdoc template section.
+
+#### Cross-tab
+
+13. **URL ↔ tab sync** — click through the three tabs; assert the URL query param updates to `build-errors`, `quick-filter`, `page-wizard` respectively. Directly navigating to `?tab=page-wizard` selects the correct tab on load.
+
+### Locator conventions
+
+- Prefer role-based locators (`getByRole('tab', { name: 'Quick filter' })`, `getByRole('button', { name: 'Add filter' })`) and text-scoped queries (`locator('p', { hasText: 'Filter configuration' })`).
+- Do **not** use `.nth()` — the page is dense and ordering shifts as state changes.
+- For YAML/generated code, assert on text content (`toContainText`) rather than matching the whole block — the syntax highlighter wraps tokens in spans.
+
+### Running
+
+Assumes the docs site is already running on `localhost:1313` — the user starts it (the `yarn start` build takes a while and is not a CI-style one-shot). The same convention as every existing spec in `e2e/`.
+
+```bash
+# First run — generate snapshots
+npx playwright test e2e/author-console --update-snapshots
+
+# Normal run
+npx playwright test e2e/author-console
+
+# Confirm stability (re-run same spec twice, no diffs)
+npx playwright test e2e/author-console
+```
+
+### Files to create
+
+- `e2e/author-console/cdocs-author-console.spec.ts`
+- `e2e/author-console/helpers.ts`
+- `e2e/author-console/fixtures/data-no-errors.json`
+- `e2e/author-console/fixtures/data-with-errors.json`
+- `e2e/author-console/cdocs-author-console.spec.ts-snapshots/*.png` (generated)
+
+No source files in this repo need to change. No changes to `playwright.config.ts` — `testDir: './e2e'` already picks up the new subdirectory.
+
+### Verification
+
+Ask the user to confirm the site is running on `localhost:1313` before running tests. Then:
+
+1. `npx playwright test e2e/author-console` passes on a clean run.
+2. Re-run the same command — all snapshots stable, no flakes.
+3. Confirm the error-state test still passes even when the live `data.json` has no errors (proves the route patch is effective).
+
+### Out of scope / follow-ups
+
+- No test for the `data.json` being absent / malformed (the app renders `'No Cdocs build data found.'`). Easy to add later if needed.
+- No assertion on the syntax-highlighter styling of the YAML / text blocks — we only check text content.
+- No test for the "copy to clipboard" behavior (clicking a `Code` block or `copy full path` link). Requires a clipboard permissions workaround in Playwright and isn't what the prompt asks for.
+- If the console grows a keyboard-nav story, add tests modeled on the one in [cdocs-content-filtering.spec.ts](e2e/integration/content-filtering/cdocs-content-filtering.spec.ts).
\ No newline at end of file
diff --git a/e2e/plans/glossary-tooltip.md b/e2e/plans/glossary-tooltip.md
new file mode 100644
index 00000000000..5a2629912a2
--- /dev/null
+++ b/e2e/plans/glossary-tooltip.md
@@ -0,0 +1,116 @@
+# Glossary tooltip test prompt
+
+Turn the mdoc at `content/en/agent/tooltip_test.mdoc.md` into a playwright test.
+
+Move the page to the relevant directory, creating the relevant subdirectories if necessary.
+
+The page should test the glossary tooltip for each scenario in the mdoc file:
+
+- "Default case" should not apply casing to the title. The glossary tooltip should show on hover.
+- "Title case" should apply casing to the title. The glossary tooltip should show on hover.
+- "Title case (no short definition)" should apply title casing to the title. The glossary tooltip should not render.
+- "Sentence case" should apply sentence casing to the title. The glossary tooltip should show on hover.
+- "Sentence case (no short definition)" should apply sentence casing to the title. The glossary tooltip should not render. 
+- "Lower case" should apply lower casing to the title. The glossary tooltip should show on hover.
+- "Upper case" should apply upper casing to the title. The glossary tooltip should show on hover.
+- For the fallback option, the glossary tooltip should not render and the display text should match the tooltip ID.
+- Also test the glossary tooltip. It should include a working link to the glossary entry.
+
+When you're finished, run the tests to verify that they're passing 
+and that the snapshots are stable.
+
+## Plan
+
+### Context
+
+The `glossary-tooltip` Markdoc tag was added in `cdocs-hugo-integration@2.17.0` (branch `heston/cdocs-glossary`, commit `a0dda411dc`). A draft mdoc test page exists at `content/en/agent/tooltip_test.mdoc.md` but it isn't wired into Playwright. This plan moves that page under the e2e content tree and adds a Playwright spec that exercises each casing scenario and verifies the tooltip popup + glossary link.
+
+#### Scenarios actually exercised (after clarifications)
+
+The tag's behavior, per `node_modules/cdocs-hugo-integration/dist/markdocCustomization/tags/glossaryTooltip.js`:
+- Term found + `short_definition` present → full tooltip (button + popup + glossary link), with casing applied.
+- Term found + no `short_definition` → renders the glossary `title` as plain text (no tooltip wrapper), with casing applied.
+- Term not found + `lang === "en"` → **throws at Markdoc compile time**. This path is unreachable from an English e2e page, so we are not covering it in this PR (per user decision).
+
+The original prompt's "fallback … display text should match the tooltip ID" description doesn't match the code: when no `short_definition` exists, the rendered text is the glossary `title`, which only equals the ID by coincidence for `anomaly` (title: `anomaly`) and differs for `snmp` (title: `Simple Network Management Protocol (SNMP)`). The spec asserts against the actual title.
+
+### Steps
+
+#### 1. Move the test page
+
+Move (via `git mv`) and rename for consistency with existing cdocs e2e pages:
+
+- `content/en/agent/tooltip_test.mdoc.md` → `content/en/dd_e2e/cdocs/components/glossary_tooltip.mdoc.md`
+
+Update the frontmatter title from `Glossary tooltip Test Page` to `Glossary tooltip`. Leave `draft: true`. Drop the stale "I made this page before we introduced Playwright" note.
+
+The mdoc content stays byte-identical otherwise — every existing line maps to a test assertion below. No new scenarios are added.
+
+Resulting URL: `/dd_e2e/cdocs/components/glossary_tooltip/`.
+
+The `.md` compiled sibling is generated by `make build-cdocs` and is checked in (repo convention — see `content/en/dd_e2e/cdocs/components/tooltip.md`).
+
+#### 2. Add the Playwright spec
+
+Create `e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts`.
+
+Pattern-match on `e2e/components/tabs/cdocs-tabs.spec.ts` (snapshot + targeted assertions) rather than the minimal `cdocs-tooltip.spec.ts` — casing and hover behavior warrant DOM-level checks, not just a snapshot.
+
+Imports and setup mirror `cdocs-tooltip.spec.ts` (goto, wait for `#mainContent`, `hideOverlays`).
+
+Tests (keep small and focused — one behavior per test):
+
+1. **Initial page snapshot** — `await expect(page.locator('#mainContent')).toHaveScreenshot('glossary-tooltip-initial.png')`. Covers all casing + no-short-def layouts in one image.
+2. **Default case renders glossary title verbatim** — assert the button inside the "Default case:" paragraph has text `new` (matches `content/en/glossary/terms/new.md` title).
+3. **Casing is applied to the button text** — for each of `title`, `sentence`, `lower`, `upper`, find the corresponding `.tooltip-trigger` and assert its text:
+   - title: `New`
+   - sentence: `New`
+   - lower: `new`
+   - upper: `NEW`
+4. **No-short-definition renders title as plain text, no tooltip container** — in the "Title case (no short definition)" and "Sentence case (no short definition)" paragraphs, assert:
+   - No `.tooltip-container` descendant.
+   - Visible text matches the SNMP title with casing applied:
+     - title case: `Simple Network Management Protocol (snmp)` — `toTitleCase` lowercases first, then capitalizes the first character of each space-split word. `(SNMP)` becomes `(snmp)` because the `(` is the first char and `s` stays lowercase.
+     - sentence case: `Simple network management protocol (snmp)`
+5. **Hover shows the tooltip popup** — hover over the default-case `.tooltip-trigger`. `tooltip.js` moves the popup to `` on hover (to escape ancestor `overflow: hidden`), so assert against `body > .tooltip-content.show` — a paragraph-scoped locator stops working after hover. Verify it contains the `short_definition` text from `new.md`.
+6. **Tooltip contains a working glossary link** — assert `a.tooltip-full-link` has an href ending in `/glossary/#new` (Hugo rewrites to absolute URLs at serve time, so use a regex tail-match rather than exact string) and text `Glossary`. Hover, click the link, and assert `page.url()` ends with `/glossary/#new`.
+
+Locators: use paragraph-scoped queries (e.g., `page.locator('p', { hasText: 'Default case:' })`) to keep tests resilient to ordering changes. Do NOT use `nth()`.
+
+#### 3. Generate snapshots and run
+
+```bash
+npx playwright test e2e/components/glossary-tooltip --update-snapshots
+npx playwright test e2e/components/glossary-tooltip
+npx playwright test e2e/components/glossary-tooltip  # second run to confirm stability
+```
+
+Requires `yarn start` (or `yarn run start` if cdocs is already compiled) running on `localhost:1313`.
+
+### Files to create / modify
+
+- **Move**: `content/en/agent/tooltip_test.mdoc.md` → `content/en/dd_e2e/cdocs/components/glossary_tooltip.mdoc.md` (via `git mv`, then edit frontmatter).
+- **Regenerate**: `content/en/dd_e2e/cdocs/components/glossary_tooltip.md` (auto-produced by `make build-cdocs`; commit it).
+- **Create**: `e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts`.
+- **Create**: `e2e/components/glossary-tooltip/cdocs-glossary-tooltip.spec.ts-snapshots/glossary-tooltip-initial-chromium-darwin.png` (generated).
+
+### Reused code / references
+
+- `e2e/helpers.ts` — `hideOverlays` for stable snapshots.
+- `e2e/components/tabs/cdocs-tabs.spec.ts` — structural template.
+- `assets/scripts/components/tooltip.js` — shows that `showTooltip` appends the popup to `document.body`.
+- `node_modules/cdocs-hugo-integration/dist/markdocCustomization/tags/glossaryTooltip.js` — source of truth for behavior the assertions mirror.
+- `node_modules/cdocs-hugo-integration/dist/markdocCustomization/utils/formatStrings.js` — `toTitleCase` / `toSentenceCase` implementations.
+- `content/en/glossary/terms/{new,snmp,anomaly}.md` — fixture data the assertions depend on.
+
+### Verification
+
+1. `make build-cdocs` completes without errors (confirms the mdoc compiles and no "term not found" throw).
+2. `yarn start` serves the page at `/dd_e2e/cdocs/components/glossary_tooltip/`; visually check hover behavior in a browser.
+3. `npx playwright test e2e/components/glossary-tooltip` passes.
+4. Re-run the same command — all assertions still pass and no snapshot diff surfaces.
+
+### Caveats / follow-ups
+
+- **Not-found path uncovered.** English throws at compile; the `return term` branch in `glossaryTooltip.js` is only reachable for non-English pages. If coverage matters, either relax the English throw in the cdocs tag or add a parallel non-English test page — neither is in scope here.
+- **Casing on SNMP looks wrong.** `toTitleCase` turns `Simple Network Management Protocol (SNMP)` into `Simple Network Management Protocol (snmp)` — it loses the caps on `SNMP` entirely. The spec asserts the actual output, but this is worth flagging to whoever owns the tag — it may be a latent bug.
\ No newline at end of file
diff --git a/package.json b/package.json
index a7f9c3200ed..5118d645410 100644
--- a/package.json
+++ b/package.json
@@ -53,7 +53,7 @@
         "@popperjs/core": "^2.11.8",
         "alpinejs": "^3.13.7",
         "bootstrap": "^5.2",
-        "cdocs-hugo-integration": "https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v2.16.0.tgz",
+        "cdocs-hugo-integration": "https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v3.1.0.tgz",
         "del": "4.1.1",
         "fancy-log": "^1.3.3",
         "geo-locate": "https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/geo-locate-v1.0.2.tgz",
diff --git a/yarn.lock b/yarn.lock
index 26ff30a92c7..f55dbe8157d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6647,9 +6647,9 @@ __metadata:
   languageName: node
   linkType: hard
 
-"cdocs-hugo-integration@https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v2.16.0.tgz":
-  version: 2.16.0
-  resolution: "cdocs-hugo-integration@https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v2.16.0.tgz"
+"cdocs-hugo-integration@https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v3.1.0.tgz":
+  version: 3.1.0
+  resolution: "cdocs-hugo-integration@https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v3.1.0.tgz"
   dependencies:
     "@prettier/sync": "npm:^0.5.2"
     "@types/markdown-it": "npm:^14.1.2"
@@ -6674,7 +6674,7 @@ __metadata:
     vite: "npm:^5.4.10"
     vite-plugin-singlefile: "npm:^2.0.2"
     zod: "npm:^4.1.12"
-  checksum: 10/7ff9efbc77a5911c02c305d624e5adb5ba9fe1c5dff8cfccec6e133755eefeb1a75e626e82cf0608100dc50d71196013db935a680536807977ee2007a3a54ffb
+  checksum: 10/d5095b9a606439d6ff006cd3d0503b09216985bb496999e670d433ab8dd06550e84920bdd76031527c3ed6127327420f602b018614cf92b1407b35cdaffdeb73
   languageName: node
   linkType: hard
 
@@ -7725,7 +7725,7 @@ __metadata:
     acorn: "npm:^7.4.1"
     alpinejs: "npm:^3.13.7"
     bootstrap: "npm:^5.2"
-    cdocs-hugo-integration: "https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v2.16.0.tgz"
+    cdocs-hugo-integration: "https://s3.amazonaws.com/origin-static-assets/corp-node-packages/master/cdocs-hugo-integration-v3.1.0.tgz"
     cross-env: "npm:^5.2.1"
     del: "npm:4.1.1"
     eslint: "npm:^6.8.0"