diff --git a/.github/workflows/docs_functionalities_tests.yml b/.github/workflows/docs_functionalities_tests.yml new file mode 100644 index 000000000..770325762 --- /dev/null +++ b/.github/workflows/docs_functionalities_tests.yml @@ -0,0 +1,60 @@ +name: Docs Functionalities Tests + +permissions: + contents: read + +on: + schedule: + # Run every Sunday at 23:00 UTC (offset from indexability's 22:00 slot) + - cron: "0 23 * * 0" + workflow_dispatch: + inputs: + base_url: + description: "Base URL to test against (e.g. a Netlify deploy preview)" + required: false + default: "https://docs.weaviate.io" + +env: + PYTHON_VERSION: "3.11" + +jobs: + test-functionalities: + name: Test Functionalities + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up test environment + uses: ./.github/actions/setup-test-env + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Playwright browser + run: uv run playwright install --with-deps chromium + + - name: Record test start time + run: echo "TEST_START_TIME=$(date +%s)" >> $GITHUB_ENV + + - name: Run functionalities tests + id: functionalities-tests + continue-on-error: true + env: + DOCS_BASE_URL: ${{ github.event.inputs.base_url || 'https://docs.weaviate.io' }} + # Tell tests/conftest.py to skip its Weaviate docker-compose startup — + # this Playwright-only suite doesn't need a local Weaviate instance. + DOCS_GITHUB_ENV: "true" + run: | + echo "Running Copy-page functionalities tests against ${DOCS_BASE_URL} ..." + uv run pytest -m "functionalities" -v --tb=short --junitxml=pytest-results.xml + + - name: Handle test results + if: always() + uses: ./.github/actions/handle-test-results + with: + test-outcome: ${{ steps.functionalities-tests.outcome == 'success' && 'success' || 'failure' }} + test-type: 'Functionalities' + slack-bot-token: ${{ secrets.TESTING_CI_SLACK_BOT }} + test-results-xml: 'pytest-results.xml' diff --git a/_includes/docs-feedback.mdx b/_includes/docs-feedback.mdx index 440a12f97..98e5edf75 100644 --- a/_includes/docs-feedback.mdx +++ b/_includes/docs-feedback.mdx @@ -1,5 +1,3 @@ -Have a question or feedback? Here's how to reach us. - import CardsSection from "/src/components/CardsSection"; import styles from "/src/components/CardsSection/styles.module.scss"; @@ -30,4 +28,10 @@ export const feedbackCardsData = [ }, ]; +
+ +Have a question or feedback? Here's how to reach us. + + +
diff --git a/package.json b/package.json index fae12bedf..157d2178f 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ "sass": "^1.83.4", "stream-chain": "^2.2.5", "stream-json": "^1.7.5", + "turndown": "^7.2.0", + "turndown-plugin-gfm": "^1.0.2", "uuid": "^13.0.0", "weaviate-agents": "^1.5.0", "weaviate-client": "^3.12.1", diff --git a/pyproject.toml b/pyproject.toml index 132c210cc..c95d3eefa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ dependencies = [ "ijson>=3.3.0", "openai>=1.50.0", "pandas>=2.2.3", + "playwright>=1.40.0", "pytest>=8.3.5", "python-dotenv>=1.1.1", "requests>=2.32.3", diff --git a/pytest.ini b/pytest.ini index 3f173969d..e996fe6f1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,6 @@ [pytest] python_files = test_*.py *_test.py -norecursedirs = _* .github .idea* .pytest_cache .venv venv* blog developers downloads playbook src static +norecursedirs = _* .github .idea* .pytest_cache .venv venv* blog developers downloads node_modules playbook src static markers = pyv4: groups tests by language for v4 client only (python) agents: group tests for Query Agent @@ -14,3 +14,4 @@ markers = indexability: HTML structure tests for docs indexability (no API keys) indexability_agents: AI agent tests for docs indexability (requires API keys) llms_txt: verifies llms.txt code snippets are covered by tested docs scripts + functionalities: browser-based checks of live-site UI features (e.g. Copy page button); requires playwright + chromium diff --git a/src/components/AcademyBadge/index.jsx b/src/components/AcademyBadge/index.jsx index 0b6c8ab6a..14baec012 100644 --- a/src/components/AcademyBadge/index.jsx +++ b/src/components/AcademyBadge/index.jsx @@ -9,10 +9,13 @@ const AcademyBadge = ({ iconOnly = false, className = "" }) => { + // data-copy-exclude marks this badge as UI chrome so the "Copy page" markdown + // export (src/components/ContextualMenu) strips it out via [data-copy-exclude]. return ( {!iconOnly && {compact ? compactText : text}} diff --git a/src/components/CloudOnlyBadge/index.jsx b/src/components/CloudOnlyBadge/index.jsx index 7d7d42028..30cef28be 100644 --- a/src/components/CloudOnlyBadge/index.jsx +++ b/src/components/CloudOnlyBadge/index.jsx @@ -9,10 +9,13 @@ const CloudOnlyBadge = ({ iconOnly = false, className = "" }) => { + // data-copy-exclude marks this badge as UI chrome so the "Copy page" markdown + // export (src/components/ContextualMenu) strips it out via [data-copy-exclude]. return ( {!iconOnly && {compact ? compactText : text}} diff --git a/src/components/ContextualMenu/index.js b/src/components/ContextualMenu/index.js index ecce90f7a..1555b16cb 100644 --- a/src/components/ContextualMenu/index.js +++ b/src/components/ContextualMenu/index.js @@ -11,7 +11,7 @@ export default function ContextualMenu({ promptName = "", }) { const [isOpen, setIsOpen] = useState(false); - const [copyStatus, setCopyStatus] = useState("idle"); // idle, copying, success + const [copyStatus, setCopyStatus] = useState("idle"); // idle, copying, success, error const [selectedLanguage, setSelectedLanguage] = useState( languages[0] || "python", ); @@ -42,9 +42,24 @@ export default function ContextualMenu({ const copyPageAsMarkdown = async () => { setCopyStatus("copying"); try { - // Get the main article content - const articleElement = document.querySelector("article"); - if (!articleElement) { + // turndown touches the DOM, and Docusaurus server-renders this component + // at build time, so it must never be imported at module scope. Load it + // lazily inside this browser-only async handler instead. + const { default: TurndownService } = await import("turndown"); + // Resolve `gfm` robustly whether the named export lands on the module + // namespace or on `.default` (CJS/ESM interop can differ by bundler). + const gfmMod = await import("turndown-plugin-gfm"); + const gfm = gfmMod.gfm ?? gfmMod.default?.gfm ?? gfmMod.default; + + // Scope to the rendered MDX body only. `.theme-doc-markdown` is the inner + // wrapper from @theme/DocItem/Content; the breadcrumbs, the ContextualMenu + // button, the version badge, the mobile TOC and the footer are all + // siblings OUTSIDE it (see src/theme/DocItem/Layout/index.js), so scoping + // here drops all the page chrome. Fall back to
for safety. + const contentRoot = + document.querySelector(".theme-doc-markdown") || + document.querySelector("article"); + if (!contentRoot) { throw new Error("Article content not found"); } @@ -52,19 +67,333 @@ export default function ContextualMenu({ const title = metadata.title || frontMatter.title || "Untitled"; const pageUrl = getCurrentPageUrl(); - // Extract text content from the article - const content = articleElement.innerText; - - // Format as markdown with metadata - const markdown = `# ${title} - -Source: ${pageUrl} - ---- - -${content}`; - - await navigator.clipboard.writeText(markdown); + // Work on a clone so the live page is never mutated. + const clone = contentRoot.cloneNode(true); + + // --- Pre-strip normalizations (MUST run before the chrome strip below) --- + + // KaTeX math: replace each rendered .katex with its source LaTeX. KaTeX + // emits accessible MathML (with the real LaTeX in + // ) plus a visual .katex-html that + // is aria-hidden. If the strip drops .katex-html, the leftover MathML + // glyphs + annotation concatenate into garbage ("2ksim2^{ksim}"). Swap the + // whole node for `$latex$` (or `$$latex$$` inside a .katex-display). + clone.querySelectorAll(".katex").forEach((k) => { + const annotation = k.querySelector( + 'annotation[encoding="application/x-tex"]', + ); + const latex = annotation && (annotation.textContent || "").trim(); + if (!latex) return; + const display = k.closest(".katex-display"); + (display || k).replaceWith( + document.createTextNode(display ? `$$${latex}$$` : `$${latex}$`), + ); + }); + + // DEFAULT Docusaurus tabs (role="tablist" + [hidden] panels): the strip + // below would DROP every non-selected panel, losing real content (e.g. a + // docker-compose "With authentication" variant). Instead surface ALL + // panels — label each with an

and un-hide it — then remove the + // tablist. This targets ONLY default tabs; our custom CODE tabs use + //