Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions src/__testing__/RelatedDesigns.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import RelatedDesigns from '../custom/CatalogDetail/RelatedDesigns';
import type { Pattern, SuggestedPattern } from '../custom/CustomCatalog/CustomCard';
import { SistentThemeProvider } from '../theme';

jest.mock('react-markdown', () => ({
__esModule: true,
default: ({ children }: { children: React.ReactNode }) => <div>{children}</div>
}));

jest.mock('remark-gfm', () => ({
__esModule: true,
default: () => {}
}));

jest.mock('rehype-raw', () => ({
__esModule: true,
default: () => {}
}));

beforeAll(() => {
global.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
});

const sampleDetail: Pattern = {
id: 'current-id',
userId: 'user-1',
name: 'Current Design',
patternFile: '',
visibility: 'public',
avatarUrl: 'https://example.com/avatar.png',
user: {
firstName: 'Alice',
lastName: 'Smith'
},
viewCount: 0,
downloadCount: 0,
cloneCount: 0,
deploymentCount: 0,
shareCount: 0
};

const renderWithTheme = (ui: React.ReactElement) => {
return render(<SistentThemeProvider>{ui}</SistentThemeProvider>);
};

describe('RelatedDesigns runtime fallback and card handling', () => {
it('renders null when patternsPerUser is undefined', () => {
const { container } = renderWithTheme(
<RelatedDesigns
details={sampleDetail}
type="pattern"
patternsPerUser={undefined}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName="Layer5"
fetchingOrgError={false}
/>
);
expect(container.firstChild).toBeNull();
});

it('renders null when patternsPerUser has no patterns property', () => {
const { container } = renderWithTheme(
<RelatedDesigns
details={sampleDetail}
type="pattern"
patternsPerUser={{}}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName="Layer5"
fetchingOrgError={false}
/>
);
expect(container.firstChild).toBeNull();
});

it('renders null when patterns array is empty or contains only current design', () => {
const { container } = renderWithTheme(
<RelatedDesigns
details={sampleDetail}
type="pattern"
patternsPerUser={{ patterns: [{ id: 'current-id', name: 'Current Design' }] }}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName="Layer5"
fetchingOrgError={false}
/>
);
expect(container.firstChild).toBeNull();
});

it('renders suggested patterns and invokes onSuggestedPatternClick on card click', () => {
const onSuggestedPatternClick = jest.fn();
const suggested: SuggestedPattern = {
id: 'related-1',
name: 'Related Design 1',
downloadCount: 10,
viewCount: 20
};

renderWithTheme(
<RelatedDesigns
details={sampleDetail}
type="pattern"
patternsPerUser={{ patterns: [suggested] }}
onSuggestedPatternClick={onSuggestedPatternClick}
technologySVGPath=""
technologySVGSubpath=""
orgName="Layer5"
fetchingOrgError={false}
/>
);

expect(screen.getByText('Related Design 1')).toBeTruthy();
fireEvent.click(screen.getByText('Related Design 1'));
expect(onSuggestedPatternClick).toHaveBeenCalledTimes(1);
expect(onSuggestedPatternClick).toHaveBeenCalledWith(
expect.objectContaining({ id: 'related-1' })
);
});
});
235 changes: 235 additions & 0 deletions src/__testing__/fixtures/relatedDesignsPatterns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Type-level fixture for Issue #1873: RightPanel / RelatedDesigns patternsPerUser.
// It is compiled by `relatedDesignsPatternsTypes.test.ts` via `tsc --noEmit`.
import type { GetPatternsApiResponse } from '@meshery/schemas/cloudApi';
import React from 'react';
import type { DetailsByType, PatternsPerUser, SuggestedPattern } from '../../custom/CatalogDetail';
import RelatedDesigns from '../../custom/CatalogDetail/RelatedDesigns';
import RightPanel from '../../custom/CatalogDetail/RightPanel';
import type { Pattern } from '../../custom/CustomCatalog/CustomCard';

export type ExercisedPatternsPerUser = PatternsPerUser;

declare const canonicalResponse: GetPatternsApiResponse;
declare const optionalResponse: GetPatternsApiResponse | undefined;
declare const completePattern: Pattern;
declare const detailsByType: DetailsByType;

// 1. RightPanel accepts undefined patternsPerUser.
export const rightPanelWithUndefined: React.ReactElement = (
<RightPanel
details={completePattern}
detailsByType={detailsByType}
type="pattern"
title="Test Pattern"
getUrl={() => ''}
showContentDetails={true}
showVersion={true}
showCaveats={false}
classes={[]}
patternsPerUser={undefined}
handleCopyUrl={() => {}}
onSuggestedPatternClick={() => {}}
useGetUserProfileByIdQuery={() => ({ data: undefined })}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
showShareAction={false}
handleShare={() => {}}
isVisibilityEnabled={false}
handleVisibilityChange={() => {}}
/>
);

// 2. RightPanel accepts the canonical GetPatternsApiResponse without a cast.
export const rightPanelWithCanonical: React.ReactElement = (
<RightPanel
details={completePattern}
detailsByType={detailsByType}
type="pattern"
title="Test Pattern"
getUrl={() => ''}
showContentDetails={true}
showVersion={true}
showCaveats={false}
classes={[]}
patternsPerUser={canonicalResponse}
handleCopyUrl={() => {}}
onSuggestedPatternClick={() => {}}
useGetUserProfileByIdQuery={() => ({ data: undefined })}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
showShareAction={false}
handleShare={() => {}}
isVisibilityEnabled={false}
handleVisibilityChange={() => {}}
/>
);

// 3. RightPanel accepts optional response (GetPatternsApiResponse | undefined).
export const rightPanelWithOptionalResponse: React.ReactElement = (
<RightPanel
details={completePattern}
detailsByType={detailsByType}
type="pattern"
title="Test Pattern"
getUrl={() => ''}
showContentDetails={true}
showVersion={true}
showCaveats={false}
classes={[]}
patternsPerUser={optionalResponse}
handleCopyUrl={() => {}}
onSuggestedPatternClick={() => {}}
useGetUserProfileByIdQuery={() => ({ data: undefined })}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
showShareAction={false}
handleShare={() => {}}
isVisibilityEnabled={false}
handleVisibilityChange={() => {}}
/>
);

// 4. RightPanel accepts omitted patternsPerUser (optional prop).
export const rightPanelWithOmittedProp: React.ReactElement = (
<RightPanel
details={completePattern}
detailsByType={detailsByType}
type="pattern"
title="Test Pattern"
getUrl={() => ''}
showContentDetails={true}
showVersion={true}
showCaveats={false}
classes={[]}
handleCopyUrl={() => {}}
onSuggestedPatternClick={() => {}}
useGetUserProfileByIdQuery={() => ({ data: undefined })}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
showShareAction={false}
handleShare={() => {}}
isVisibilityEnabled={false}
handleVisibilityChange={() => {}}
/>
);

// 5. RightPanel accepts response with missing patterns property.
export const rightPanelWithMissingPatterns: React.ReactElement = (
<RightPanel
details={completePattern}
detailsByType={detailsByType}
type="pattern"
title="Test Pattern"
getUrl={() => ''}
showContentDetails={true}
showVersion={true}
showCaveats={false}
classes={[]}
patternsPerUser={{}}
handleCopyUrl={() => {}}
onSuggestedPatternClick={() => {}}
useGetUserProfileByIdQuery={() => ({ data: undefined })}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
showShareAction={false}
handleShare={() => {}}
isVisibilityEnabled={false}
handleVisibilityChange={() => {}}
/>
);

// 6. RightPanel accepts existing consumer passing complete Pattern[].
export const rightPanelWithCompletePattern: React.ReactElement = (
<RightPanel
details={completePattern}
detailsByType={detailsByType}
type="pattern"
title="Test Pattern"
getUrl={() => ''}
showContentDetails={true}
showVersion={true}
showCaveats={false}
classes={[]}
patternsPerUser={{ patterns: [completePattern] }}
handleCopyUrl={() => {}}
onSuggestedPatternClick={(pattern: SuggestedPattern) => {
// SuggestedPattern does not require avatarUrl or user
void pattern.id;
void pattern.name;
}}
useGetUserProfileByIdQuery={() => ({ data: undefined })}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
showShareAction={false}
handleShare={() => {}}
isVisibilityEnabled={false}
handleVisibilityChange={() => {}}
/>
);

// 7. RelatedDesigns directly accepts undefined, canonical, and complete Pattern.
export const relatedDesignsWithCanonical: React.ReactElement = (
<RelatedDesigns
details={completePattern}
type="pattern"
patternsPerUser={canonicalResponse}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
/>
);

export const relatedDesignsWithUndefined: React.ReactElement = (
<RelatedDesigns
details={completePattern}
type="pattern"
patternsPerUser={undefined}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
/>
);

export const relatedDesignsWithCompletePattern: React.ReactElement = (
<RelatedDesigns
details={completePattern}
type="pattern"
patternsPerUser={{ patterns: [completePattern] }}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
/>
);

// 8. Self-check: verifying that patternsPerUser rejects incompatible types.
export const rejectsIncompatiblePatterns: React.ReactElement = (
<RelatedDesigns
details={completePattern}
type="pattern"
// @ts-expect-error - invalid object cannot be assigned to PatternsPerUser
patternsPerUser={{ patterns: [{ invalidField: 123 }] }}
onSuggestedPatternClick={() => {}}
technologySVGPath=""
technologySVGSubpath=""
orgName=""
fetchingOrgError={false}
/>
);
7 changes: 7 additions & 0 deletions src/__testing__/fixtures/tsconfig.relatedDesignsPatterns.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["./relatedDesignsPatterns.tsx"]
}
Loading
Loading