diff --git a/docs/RECOVERY.md b/docs/RECOVERY.md index 1ffc4b15b2..2fc26450f2 100644 --- a/docs/RECOVERY.md +++ b/docs/RECOVERY.md @@ -124,7 +124,7 @@ When reporting missing records, share the failing view, running Pulse version, affected provider, time range, expected reading and relevant redacted error. Use consistent placeholders for private hostnames, datasets and IDs. Do not share full backup inventories, request headers, tokens, cookies or recovery -keys. See [safe issue reporting](TROUBLESHOOTING.md#getting-help). +keys. See [safe issue reporting](TROUBLESHOOTING.md#-getting-help). ## API reference diff --git a/frontend-modern/browser-tests/recovery-safe-reporting.cjs b/frontend-modern/browser-tests/recovery-safe-reporting.cjs new file mode 100644 index 0000000000..582d449a13 --- /dev/null +++ b/frontend-modern/browser-tests/recovery-safe-reporting.cjs @@ -0,0 +1,109 @@ +// Shipped Recovery -> Troubleshooting navigation, with the production Docs page. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium, webkit } = require('playwright'); + +(async () => { + const root = '/workspace/frontend-modern'; + process.chdir(root); + const artifacts = path.join(root, 'node_modules', 'recovery-safe-reporting-browser'); + fs.mkdirSync(artifacts, { recursive: true }); + const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); + const server = await createServer({ + root, + configFile: path.join(root, 'vite.config.ts'), + cacheDir: path.join(root, 'node_modules', '.vite-recovery-safe-reporting'), + server: { host: '127.0.0.1', port: 5249, strictPort: true }, + optimizeDeps: { noDiscovery: true, include: [] }, + }); + let browser; + const results = []; + try { + await server.listen(); + for (const [engine, width, dark] of [ + ['chromium', 1280, false], + ['webkit', 390, true], + ]) { + browser = await { chromium, webkit }[engine].launch( + engine === 'chromium' + ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } + : { headless: true }, + ); + const page = await browser.newPage({ + viewport: { width, height: 900 }, + ...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}), + }); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()); + }); + await page.goto('http://127.0.0.1:5249/browser-tests/docs-fragment-navigation.html', { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + // The fixture mounts the actual /docs router. Navigate without mocking assets. + await page.evaluate(() => { + history.pushState({}, '', '/docs/RECOVERY'); + dispatchEvent(new PopStateEvent('popstate')); + }); + const link = page.getByRole('link', { name: 'safe issue reporting', exact: true }); + await link.waitFor(); + if (dark) await page.evaluate(() => document.documentElement.classList.add('dark')); + await link.focus(); + await page.keyboard.press('Enter'); + const verifyDestination = async () => { + await page.waitForFunction(() => { + const heading = document.getElementById('-getting-help'); + if (!heading) return false; + const rect = heading.getBoundingClientRect(); + return document.activeElement === heading && rect.top >= 0 && rect.top < innerHeight; + }); + assert.ok(page.url().endsWith('/docs/TROUBLESHOOTING#-getting-help')); + assert.equal(await page.locator('h2#-getting-help').textContent(), '🆘 Getting Help'); + const article = await page.locator('article').innerText(); + for (const text of [ + 'Export for GitHub (sanitized)', + 'Review before posting', + 'Never post bootstrap/recovery', + 'Do not refile information you have already supplied', + ]) + assert.ok(article.includes(text), text); + assert.ok( + await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), + ); + }; + await verifyDestination(); + await page.screenshot({ path: path.join(artifacts, `${engine}-safe-reporting.png`) }); + assert.deepEqual(errors, []); + results.push({ + engine, + version: browser.version(), + width, + dark, + keyboardNavigation: true, + targetFocusedAndVisible: true, + safeGuidance: true, + noHorizontalOverflow: true, + errors, + }); + await browser.close(); + browser = undefined; + } + const result = { + result: 'passed', + playwright: require('playwright/package.json').version, + scope: 'shipped help navigation, not native recovery or release availability', + results, + }; + fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n'); + console.log(JSON.stringify(result)); + } finally { + if (browser) await browser.close(); + await server.close(); + } +})().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/frontend-modern/public/docs/RECOVERY.md b/frontend-modern/public/docs/RECOVERY.md index 1ffc4b15b2..2fc26450f2 100644 --- a/frontend-modern/public/docs/RECOVERY.md +++ b/frontend-modern/public/docs/RECOVERY.md @@ -124,7 +124,7 @@ When reporting missing records, share the failing view, running Pulse version, affected provider, time range, expected reading and relevant redacted error. Use consistent placeholders for private hostnames, datasets and IDs. Do not share full backup inventories, request headers, tokens, cookies or recovery -keys. See [safe issue reporting](TROUBLESHOOTING.md#getting-help). +keys. See [safe issue reporting](TROUBLESHOOTING.md#-getting-help). ## API reference diff --git a/frontend-modern/src/utils/__tests__/recoveryHelp.test.ts b/frontend-modern/src/utils/__tests__/recoveryHelp.test.ts index 6af454d0ad..577dba9042 100644 --- a/frontend-modern/src/utils/__tests__/recoveryHelp.test.ts +++ b/frontend-modern/src/utils/__tests__/recoveryHelp.test.ts @@ -42,6 +42,24 @@ describe('recovery help', () => { expect(target.querySelector('#backup-safety')?.textContent).toBe('Backup safety'); }); + it('resolves safe issue reporting to the actual troubleshooting heading', () => { + const rendered = document.createElement('article'); + rendered.innerHTML = renderDocMarkdown(guide, 'RECOVERY'); + const link = rendered.querySelector('a[href^="/docs/TROUBLESHOOTING#"]'); + expect(link?.textContent).toBe('safe issue reporting'); + expect(link?.hasAttribute('data-doc-link')).toBe(true); + const destination = new URL(link!.getAttribute('href')!, 'https://pulse.example.invalid'); + expect(destination.pathname).toBe('/docs/TROUBLESHOOTING'); + expect(destination.hash).not.toBe(''); + + const target = document.createElement('article'); + target.innerHTML = renderDocMarkdown(read('docs/TROUBLESHOOTING.md'), 'TROUBLESHOOTING'); + const heading = [...target.querySelectorAll('h2')].find( + (element) => element.id === decodeURIComponent(destination.hash.slice(1)), + ); + expect(heading?.textContent).toBe('🆘 Getting Help'); + }); + it('documents actual point filters and an example the handlers consume', () => { const handlers = read('internal/api/recovery_handlers.go'); const model = read('internal/recovery/model/types.go');