diff --git a/src/__tests__/collection.patternFlyApi.test.ts b/src/__tests__/collection.patternFlyApi.test.ts
index b4683ff0..fa584500 100644
--- a/src/__tests__/collection.patternFlyApi.test.ts
+++ b/src/__tests__/collection.patternFlyApi.test.ts
@@ -179,6 +179,24 @@ describe('parsePayload', () => {
it('parses numeric payloads as non-empty', () => {
expect(parsePayload('42').isEmpty).toBe(false);
});
+
+ it.each([
+ {
+ description: 'length',
+ payload: 'A'.repeat(200),
+ expected: 1
+ },
+ {
+ description: 'code fence',
+ payload: '```ts file="./ButtonBasic.tsx"\n```',
+ expected: 0.95
+ }
+ ])('should calculate qualityScore during payload parsing, $description', ({ payload, expected }) => {
+ // See collection.patternFlyApi.test.ts for quality scoring tests
+ const parsed = parsePayload(payload);
+
+ expect(parsed.qualityScore).toBeLessThanOrEqual(expected);
+ });
});
describe('crawler', () => {
diff --git a/src/__tests__/collection.patternFlyApiHelpers.test.ts b/src/__tests__/collection.patternFlyApiHelpers.test.ts
index 0a6ae48f..1671cd00 100644
--- a/src/__tests__/collection.patternFlyApiHelpers.test.ts
+++ b/src/__tests__/collection.patternFlyApiHelpers.test.ts
@@ -6,8 +6,11 @@ import {
formatSlugToTitle,
getApiFallbackDescription,
getLiveExampleCount,
+ getRawImportCount,
+ getTemplateCount,
+ isContentAggregator,
+ isSubstantialGuide,
hasEmptyFileCodeFence,
- hasLiveExample,
isRawImport,
normalizeSlug
} from '../collection.patternFlyApiHelpers';
@@ -79,50 +82,141 @@ describe('isRawImport', () => {
});
});
-describe('hasLiveExample', () => {
+describe('getRawImportCount', () => {
it.each([
{
- description: 'self-closing LiveExample tag',
- input: '',
+ description: 'plain text with no import',
+ input: 'const a = "./Button.tsx";',
+ expected: 0
+ },
+ {
+ description: 'single raw import',
+ input: "import ButtonRaw from './Button.tsx?raw';",
+ expected: 1
+ },
+ {
+ description: 'multiple raw imports across multiple lines',
+ input: "import Btn1 from './Btn1.tsx?raw';\nimport Btn2 from './Btn2.tsx?raw';\nimport Btn3 from './Btn3.tsx?raw';",
+ expected: 3
+ },
+ {
+ description: 'multiline raw import statement',
+ input: "import {\n Button,\n Card\n} from './components?raw';",
+ expected: 1
+ },
+ {
+ description: 'mixed raw imports and standard imports',
+ input: "import React from 'react';\nimport ExampleRaw from './Example?raw';\nimport { Card } from './Card';",
+ expected: 1
+ }
+ ])('should count raw imports, $description', ({ input, expected }) => {
+ expect(getRawImportCount(input)).toBe(expected);
+ });
+});
+
+describe('getTemplateCount', () => {
+ it.each([
+ {
+ description: 'content with no templates, raw imports',
+ input: 'Pure documentation content without any examples.',
+ expected: { pairedCount: 0, orphanCount: 0, totalUnits: 0 }
+ },
+ {
+ description: 'live example, raw import',
+ input: "import Example from './Example?raw';\n",
+ expected: { pairedCount: 1, orphanCount: 0, totalUnits: 1 }
+ },
+ {
+ description: 'orphaned raw import, no live examples',
+ input: "import Example1 from './Example1?raw';\nimport Example2 from './Example2?raw';",
+ expected: { pairedCount: 0, orphanCount: 2, totalUnits: 2 }
+ },
+ {
+ description: 'orphaned live examples, no raw imports',
+ input: '\n',
+ expected: { pairedCount: 0, orphanCount: 2, totalUnits: 2 }
+ },
+ {
+ description: 'multiple live examples and a single raw import',
+ input: "import Ex1 from './Ex1?raw';\n\n\n",
+ expected: { pairedCount: 1, orphanCount: 2, totalUnits: 3 }
+ }
+ ])('should calculate template counts and pairings, $description', ({ input, expected }) => {
+ expect(getTemplateCount(input)).toEqual(expected);
+ });
+});
+
+describe('isContentAggregator', () => {
+ it.each([
+ {
+ description: 'multiple templates without inlined code blocks',
+ input: '\n',
expected: true
},
{
- description: 'opening LiveExample tag with attributes',
- input: '',
+ description: 'multiple templates where total units is greater than inlinedCodeCount',
+ input: '\n\n\n```tsx\nconst a = \'single block that exceeds 20 characters\';\n```',
expected: true
},
{
- description: 'case-insensitive liveexample tag',
- input: '',
+ description: 'template tag present, lacks prose paragraphs and no inlined code',
+ input: '\nShort line',
expected: true
},
{
- description: 'LiveExample tag with multiline attributes',
- input: '',
+ description: 'multiple paragraphs and inlined code, content is not an aggregator',
+ input: '\n\nThis is the first substantive paragraph detailing how to implement this pattern.\n\nThis is the second substantive paragraph detailing accessibility guidelines and props.\n\n```tsx\nimport React from \'react\';\nexport const Sample = () => ;\n```',
+ expected: false
+ },
+ {
+ description: 'non-markdown/html, content is not an aggregator',
+ input: '{"template": "", "raw": "import x from \'x?raw\'"}',
+ expected: false
+ },
+ {
+ description: 'no templates, content is not an aggregator',
+ input: '# Documentation\n\nThis is a standard document with no template references.',
+ expected: false
+ }
+ ])('should determine if content is an aggregator, $description', ({ input, expected }) => {
+ expect(isContentAggregator(input)).toBe(expected);
+ });
+});
+
+describe('isSubstantialGuide', () => {
+ it.each([
+ {
+ description: 'markdown with templates and multiple paragraphs',
+ input: 'This is paragraph one explaining how to configure the component in depth.\n\nThis is paragraph two explaining proper event handling and keyboard navigation.\n\n',
expected: true
},
{
- description: 'LiveExample tag without attributes',
- input: '',
+ description: 'markdown with templates and an inlined code block',
+ input: '```tsx\nimport React from \'react\';\nexport const App = () => Content;\n```\n',
expected: true
},
{
- description: 'text without LiveExample tag',
- input: 'Regular HTML component
',
+ description: 'markdown with templates and minimum number of words',
+ input: `${'word '.repeat(105)}\n\n`,
+ expected: true
+ },
+ {
+ description: 'aggregator stub with templates but no paragraphs, words < 100, and no inlined code',
+ input: "import Ex1 from './Ex1?raw';\nimport Ex2 from './Ex2?raw';\n\n",
expected: false
},
{
- description: 'extended component name without word boundary match',
- input: '',
+ description: 'substantial document without template units',
+ input: "This is paragraph one explaining how to configure the component in depth.\n\nThis is paragraph two explaining proper event handling and keyboard navigation.\n\n```tsx\nimport React from 'react';\nexport const App = () => Content;\n```",
expected: false
},
{
- description: 'empty string',
- input: '',
+ description: 'non-markdown content type',
+ input: '{"paragraphs": 5, "template": ""}',
expected: false
}
- ])('should detect LiveExample tags, $description', ({ input, expected }) => {
- expect(hasLiveExample(input)).toBe(expected);
+ ])('should determine if content is a substantial guide, $description', ({ input, expected }) => {
+ expect(isSubstantialGuide(input)).toBe(expected);
});
});
@@ -152,6 +246,31 @@ describe('getLiveExampleCount', () => {
description: 'empty string',
input: '',
expected: 0
+ },
+ {
+ description: 'opening LiveExample tag with attributes',
+ input: '',
+ expected: 1
+ },
+ {
+ description: 'LiveExample tag with multiline attributes',
+ input: '',
+ expected: 1
+ },
+ {
+ description: 'LiveExample tag without attributes',
+ input: '',
+ expected: 1
+ },
+ {
+ description: 'text without LiveExample tag',
+ input: 'Regular HTML component
',
+ expected: 0
+ },
+ {
+ description: 'extended component name without word boundary match',
+ input: '',
+ expected: 0
}
])('should count LiveExample occurrences, $description', ({ input, expected }) => {
expect(getLiveExampleCount(input)).toBe(expected);
@@ -335,6 +454,57 @@ describe('calculateContentQualityScore', () => {
])('should calculate quality score, $description', ({ content, options, expected }: any) => {
expect(calculateContentQualityScore(content, options)).toBe(expected);
});
+
+ it.each([
+ {
+ description: 'multiple paragraphs multiple live examples',
+ content: [
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ '\n',
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ '\n',
+ '',
+ ''
+ ].join('\n'),
+ expected: 0.94
+ },
+ {
+ description: 'single paragraph',
+ content: [
+ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
+ '\n',
+ '',
+ ''
+ ].join('\n'),
+ expected: 0.94
+ },
+ {
+ description: 'no paragraphs',
+ content: [
+ 'Lorem ipsum dolor sit amet.',
+ '',
+ ''
+ ].join('\n'),
+ expected: 0.91
+ },
+ {
+ description: 'no paragraphs single example',
+ content: [
+ 'Lorem ipsum dolor sit amet.',
+ ''
+ ].join('\n'),
+ expected: 0.91
+ },
+ {
+ description: 'single example',
+ content: [
+ ''
+ ].join('\n'),
+ expected: 0.91
+ }
+ ])('should cap "LiveExample" penalities for wordy documents, $description', ({ content, expected }) => {
+ expect(calculateContentQualityScore(content)).toBe(expected);
+ });
});
describe('normalizeSlug', () => {
@@ -662,7 +832,7 @@ describe('getApiFallbackDescription', () => {
description: 'examples category',
displayName: 'Button',
category: 'examples',
- expected: 'PatternFly Button examples and demos.'
+ expected: 'PatternFly examples and demos for Button.'
},
{
description: 'doc category',
@@ -680,7 +850,7 @@ describe('getApiFallbackDescription', () => {
description: 'default arguments without parameters',
displayName: undefined,
category: undefined,
- expected: 'PatternFly documentation and guidelines for .'
+ expected: 'PatternFly documentation and guidelines.'
}
])('should provide fallback description, $description', ({ displayName, category, expected }: any) => {
expect(getApiFallbackDescription(displayName, category)).toBe(expected);
@@ -705,7 +875,7 @@ describe('extractApiDescription', () => {
description: 'detailType equals examples returns fallback',
content: '# Title\nValid paragraph line exceeding twenty characters in length.',
context: { displayName: 'Button', detailType: 'examples' },
- expected: 'PatternFly Button examples and demos.'
+ expected: 'PatternFly examples and demos for Button.'
},
{
description: 'markdown content uses first valid paragraph',
@@ -749,6 +919,24 @@ describe('extractApiDescription', () => {
context: { displayName: 'Card', category: 'doc' },
expected: 'PatternFly documentation and guidelines for Card.'
},
+ {
+ description: 'content with purposeful script injections',
+ content: "import React from 'react';\n# Heading\nShort line <",
+ context: { displayName: 'Card', category: 'doc' },
+ expected: 'PatternFly documentation and guidelines for Card.'
+ },
+ {
+ description: 'content with purposeful nested script injections',
+ content: "import React from 'react';\n# Heading\nShort line ipt> console.warn('lorem ipsum')",
+ context: { displayName: 'Card', category: 'doc' },
+ expected: 'PatternFly documentation and guidelines for Card.'
+ },
+ {
+ description: 'content with purposeful unclosed script injections',
+ content: "import React from 'react';\n# Heading\nShort line