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