diff --git a/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx b/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx index fdc7de9651db..ddef2c0f2de4 100644 --- a/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx +++ b/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { IconButton } from '@storybook/core/components'; import { styled } from '@storybook/core/theming'; import { BottomBarToggleIcon, MenuIcon } from '@storybook/icons'; +import type { API_IndexHash, API_Refs } from '@storybook/types'; import { useStorybookApi, useStorybookState } from '@storybook/core/manager-api'; @@ -17,27 +18,46 @@ interface MobileNavigationProps { showPanel: boolean; } +// Function to combine all indexes +function combineIndexes(rootIndex: API_IndexHash | undefined, refs: API_Refs) { + // Create a copy of the root index to avoid mutation + const combinedIndex = { ...(rootIndex || {}) }; // Use an empty object as fallback + + // Traverse refs and merge each nested index with the root index + Object.values(refs).forEach((ref) => { + if (ref.index) { + Object.assign(combinedIndex, ref.index); + } + }); + + return combinedIndex; +} + /** * Walks the tree from the current story to combine story+component+folder names into a single * string */ const useFullStoryName = () => { - const { index } = useStorybookState(); + const { index, refs } = useStorybookState(); const api = useStorybookApi(); const currentStory = api.getCurrentStoryData(); if (!currentStory) { return ''; } - + const combinedIndex = combineIndexes(index, refs || {}); let fullStoryName = currentStory.renderLabel?.(currentStory, api) || currentStory.name; - // @ts-expect-error (non strict) - let node = index[currentStory.id]; - // @ts-expect-error (non strict) - while ('parent' in node && node.parent && index[node.parent] && fullStoryName.length < 24) { - // @ts-expect-error (non strict) - node = index[node.parent]; + let node = combinedIndex[currentStory.id]; + + while ( + node && + 'parent' in node && + node.parent && + combinedIndex[node.parent] && + fullStoryName.length < 24 + ) { + node = combinedIndex[node.parent]; const parentName = node.renderLabel?.(node, api) || node.name; fullStoryName = `${parentName}/${fullStoryName}`; } diff --git a/code/e2e-tests/composition.spec.ts b/code/e2e-tests/composition.spec.ts index 8e50987675b5..15bc6c4c4ed9 100644 --- a/code/e2e-tests/composition.spec.ts +++ b/code/e2e-tests/composition.spec.ts @@ -11,12 +11,10 @@ test.describe('composition', () => { 'Slow, framework independent test, so only run it on in react-vite/default-ts' ); - test.beforeEach(async ({ page }) => { + test('should filter and render composed stories', async ({ page }) => { await page.goto(storybookUrl); await new SbPage(page, expect).waitUntilLoaded(); - }); - test('should correctly filter composed stories', async ({ page }) => { // Expect that composed Storybooks are visible await expect(page.getByTitle('Storybook 8.0.0')).toBeVisible(); await expect(page.getByTitle('Storybook 7.6.18')).toBeVisible(); @@ -35,10 +33,64 @@ test.describe('composition', () => { // Expect composed stories `to be available in the search await page.getByPlaceholder('Find components').fill('Button'); await expect( - page.getByRole('option', { name: 'Button Storybook 8.0.0 / @blocks / examples' }) + page.getByRole('option', { name: 'Button Storybook 7.6.18 / @blocks / examples' }) ).toBeVisible(); + + const buttonStory = page.getByRole('option', { + name: 'Button Storybook 8.0.0 / @blocks / examples', + }); + await expect(buttonStory).toBeVisible(); + await buttonStory.click(); + + // Note: this could potentially be flaky due to it accessing a hosted Storybook + await expect( + page + .locator('iframe[title="storybook-ref-storybook\\@8\\.0\\.0"]') + .contentFrame() + .getByRole('heading', { name: 'Example button component' }) + ).toBeVisible({ timeout: 15000 }); + }); + + test('should filter and render composed stories on mobile', async ({ page }) => { + page.setViewportSize({ width: 320, height: 800 }); + await page.goto(storybookUrl); + await new SbPage(page, expect).waitUntilLoaded(); + + await page.click('button[title="Open navigation menu"]'); + + // Expect that composed Storybooks are visible + await expect(page.getByTitle('Storybook 8.0.0')).toBeVisible(); + await expect(page.getByTitle('Storybook 7.6.18')).toBeVisible(); + + // Expect composed stories to be available in the sidebar + await page.locator('[id="storybook\\@8\\.0\\.0_components-badge"]').click(); + await expect( + page.locator('[id="storybook\\@8\\.0\\.0_components-badge--default"]') + ).toBeVisible(); + + await page.locator('[id="storybook\\@7\\.6\\.18_components-badge"]').click(); + await expect( + page.locator('[id="storybook\\@7\\.6\\.18_components-badge--default"]') + ).toBeVisible(); + + // Expect composed stories `to be available in the search + await page.getByPlaceholder('Find components').fill('Button'); await expect( page.getByRole('option', { name: 'Button Storybook 7.6.18 / @blocks / examples' }) ).toBeVisible(); + + const buttonStory = page.getByRole('option', { + name: 'Button Storybook 8.0.0 / @blocks / examples', + }); + await expect(buttonStory).toBeVisible(); + await buttonStory.click(); + + // Note: this could potentially be flaky due to it accessing a hosted Storybook + await expect( + page + .locator('iframe[title="storybook-ref-storybook\\@8\\.0\\.0"]') + .contentFrame() + .getByRole('heading', { name: 'Example button component' }) + ).toBeVisible({ timeout: 15000 }); }); });