diff --git a/docs/src/components/workshop/WorkshopExperience.astro b/docs/src/components/workshop/WorkshopExperience.astro index 75fd6ee142e..f61536d1a34 100644 --- a/docs/src/components/workshop/WorkshopExperience.astro +++ b/docs/src/components/workshop/WorkshopExperience.astro @@ -229,8 +229,41 @@ function rewriteGfmTaskLists(html: string) { return result; } +function normalizeJourneyToken(value: string) { + return value.trim().toLowerCase().replace(/[^a-z0-9-]/gu, ''); +} + +const journeyBlockPattern = /([\s\S]*?)/giu; +const journeyOpenPattern = //giu; +const journeyClosePattern = //giu; + +function parseJourneyTokens(value: string) { + return uniqueItems( + value + .split(',') + .map(normalizeJourneyToken) + .filter(Boolean), + ); +} + +function rewriteJourneyBlocks(html: string) { + const wrapped = html.replace( + journeyBlockPattern, + (_match, journeyCsv, body) => { + const journeys = parseJourneyTokens(String(journeyCsv)); + if (journeys.length === 0) return body; + const classes = ['aw-workshop-journey-block', ...journeys.map((journey) => `aw-workshop-journey-block-${journey}`)]; + return `
${body}
`; + }, + ); + + return wrapped + .replace(journeyOpenPattern, '') + .replace(journeyClosePattern, ''); +} + function rewriteWorkshopHtml(html: string) { - return transformTables(rewriteGfmTaskLists(rewriteGfmAlerts(sanitizeWorkshopHtml(html))) + return transformTables(rewriteJourneyBlocks(rewriteGfmTaskLists(rewriteGfmAlerts(sanitizeWorkshopHtml(html)))) .replace(/<(\/?)h([1-4])\b/gu, (_match, slash, level) => { return `<${slash}h${Math.min(Number(level) + 1, 4)}`; @@ -362,6 +395,8 @@ const workshopSteps: WorkshopStep[] = await Promise.all(workshopEntries.map(asyn const initialFlow = buildWorkshopFlow(workshopDefaults.journeyId, workshopDefaults.scenarioId); const initialStepKey = initialFlow[0] ?? workshopSteps[0]?.key ?? ''; const initialStep = workshopSteps.find((item) => item.key === initialStepKey) ?? workshopSteps[0]; +const initialJourney = workshopJourneys.find((item) => item.id === workshopDefaults.journeyId) ?? workshopJourneys[0]; +const initialVisibleJourneyIds = uniqueItems(['all', ...(initialJourney?.contentJourneyIds ?? [])]); // Precompute step counts for entry path cards using the default scenario (estimate shown before scenario is selected). const entryPathStepCounts: Record = Object.fromEntries( @@ -388,6 +423,7 @@ const scenarioStepCountsByJourney: Record> = Obje
@@ -840,6 +876,8 @@ const scenarioStepCountsByJourney: Record> = Obje const journey = manifest.journeys.find((item) => item.id === state.journeyId) || manifest.journeys[0]; const scenario = manifest.scenarios.find((item) => item.id === state.scenarioId) || manifest.scenarios[0]; const stepIndex = visibleFlow.indexOf(activeStep.key); + const visibleJourneyIds = ['all', ...((journey && journey.contentJourneyIds) || [])]; + root.dataset.workshopVisibleJourneys = [...new Set(visibleJourneyIds)].join(' '); if (tunnelPanel) { tunnelPanel.hidden = state.journeyId !== 'vscode' || setupStep !== 'scenario'; } @@ -1307,6 +1345,19 @@ const scenarioStepCountsByJourney: Record> = Obje min-height: 22rem; } + .aw-workshop-step-content :global(.aw-workshop-journey-block) { + display: none; + } + + .aw-workshop-step-content :global(.aw-workshop-journey-block.aw-workshop-journey-block-all), + .aw-workshop[data-workshop-visible-journeys~='ui'] .aw-workshop-step-content :global(.aw-workshop-journey-block.aw-workshop-journey-block-ui), + .aw-workshop[data-workshop-visible-journeys~='terminal'] .aw-workshop-step-content :global(.aw-workshop-journey-block.aw-workshop-journey-block-terminal), + .aw-workshop[data-workshop-visible-journeys~='local'] .aw-workshop-step-content :global(.aw-workshop-journey-block.aw-workshop-journey-block-local), + .aw-workshop[data-workshop-visible-journeys~='codespace'] .aw-workshop-step-content :global(.aw-workshop-journey-block.aw-workshop-journey-block-codespace), + .aw-workshop[data-workshop-visible-journeys~='copilot'] .aw-workshop-step-content :global(.aw-workshop-journey-block.aw-workshop-journey-block-copilot) { + display: contents; + } + .aw-workshop-step-content :global(h2), .aw-workshop-step-content :global(h3), .aw-workshop-step-content :global(h4) { diff --git a/docs/tests/workshop.spec.ts b/docs/tests/workshop.spec.ts index bae97d5b698..3a5bf3ea786 100644 --- a/docs/tests/workshop.spec.ts +++ b/docs/tests/workshop.spec.ts @@ -583,3 +583,143 @@ test.describe('Workshop flow filtering: Copilot scenario-d substitution', () => expect(keys).not.toContain('11c-build-pr-reviewer-ui'); }); }); + +// --------------------------------------------------------------------------- +// Journey block visibility tests — verify that CSS-driven journey section +// visibility works correctly: marker rewriting, attribute state, and +// visibility transitions when switching paths. +// --------------------------------------------------------------------------- + +test.describe('Workshop journey block visibility', () => { + test('data-workshop-visible-journeys contains "all" and journey-specific IDs after starting the tutorial', async ({ page }) => { + // ui-learner maps to the github journey, which has contentJourneyIds: ['ui'] + await startWorkshop(page); + + const visibleJourneys = await page.evaluate(() => + document.querySelector('[data-workshop-root]')?.getAttribute('data-workshop-visible-journeys') ?? '', + ); + // 'all' is always included; 'ui' comes from the github journey's contentJourneyIds + expect(visibleJourneys.split(' ')).toContain('all'); + expect(visibleJourneys.split(' ')).toContain('ui'); + }); + + test('data-workshop-visible-journeys updates when switching to a different entry path', async ({ page }) => { + // Start with ui-learner (github journey → contentJourneyIds: ['ui']) + await startWorkshop(page); + + const initialJourneys = await page.evaluate(() => + document.querySelector('[data-workshop-root]')?.getAttribute('data-workshop-visible-journeys') ?? '', + ); + expect(initialJourneys.split(' ')).toContain('ui'); + + // Switch to the terminal path (contentJourneyIds: ['terminal', 'local']) + await page.getByRole('button', { name: /Change route/i }).click(); + await page.locator('[data-workshop-entry-path="cli-user"]').click(); + await page.locator('[data-workshop-scenario="daily-status"]').click(); + await expect(page.locator('[data-workshop-tutorial]')).toBeVisible(); + + const updatedJourneys = await page.evaluate(() => + document.querySelector('[data-workshop-root]')?.getAttribute('data-workshop-visible-journeys') ?? '', + ); + expect(updatedJourneys.split(' ')).toContain('all'); + expect(updatedJourneys.split(' ')).toContain('terminal'); + // 'ui' should no longer be listed (terminal journey does not map to ui content) + expect(updatedJourneys.split(' ')).not.toContain('ui'); + }); + + test('no raw journey comment markers survive HTML rewriting in step data', async ({ page }) => { + await startWorkshop(page); + + // Any or remaining in rendered HTML + // means rewriteJourneyBlocks did not run or failed. Raw markers must never + // appear in the embedded step-data JSON. + const hasRawMarkers = await page.evaluate(() => { + const node = document.getElementById('aw-workshop-step-data'); + if (!node) return false; + const steps = JSON.parse(node.textContent?.trim() || '[]') as Array<{ html: string }>; + return steps.some((s) => /