packages/plugin-code-blocks/src/output/markdown-code-blocks.spec.ts
import { noop } from 'lodash';
import { DeclarationReflection, ReflectionKind } from 'typedoc';
import { relative, resolve } from '@knodes/typedoc-pluginutils/path';
import { MockPlugin, createMockProjectWithPackage, mockPlugin, restoreFs, setVirtualFs, setupMockMarkdownReplacer, setupMockPageMemo } from '#plugintestbed';
/* eslint-disable @typescript-eslint/no-var-requires */
jest.mock( '../code-sample-file' );
const { DEFAULT_BLOCK_NAME, readCodeSample: readCodeSampleMock } = require( '../code-sample-file' ) as jest.Mocked<typeof import( '../code-sample-file' )>;
/* eslint-enable @typescript-eslint/no-var-requires */
import { MarkdownCodeBlocks } from './markdown-code-blocks';
import { ICodeBlocksPluginThemeMethods } from './theme';
import { CodeBlockPlugin } from '../plugin';
import { EBlockMode, ICodeBlock } from '../types';
class FakeSource {
public static readonly REPO_URL = 'https://example.repo.com';
public static readonly getURL = jest.fn().mockImplementation( ( path, line ) => `${FakeSource.REPO_URL}/${relative( process.cwd(), path )}#L${line}` );
public static readonly getRepository = jest.fn().mockReturnValue( {
getURL: this.getURL,
} );
public readonly getURL = FakeSource.getURL;
public readonly getRepository = FakeSource.getRepository;
}
let themeMethods: ICodeBlocksPluginThemeMethods;
let plugin: MockPlugin<CodeBlockPlugin>;
const rootDir = resolve( __dirname, '../..' );
const markdownReplacerTestbed = setupMockMarkdownReplacer();
const pageMemoTestbed = setupMockPageMemo();
const FILE = 'foo/qux.txt';
const FILE_CONTENT = `Content of ${FILE}`;
beforeEach( () => {
process.chdir( rootDir );
jest.clearAllMocks();
let i = 0;
themeMethods = {
renderCodeBlock: jest.fn().mockImplementation( () => `renderCodeBlock-${i++}` ),
renderInlineCodeBlock: jest.fn().mockImplementation( () => `renderInlineCodeBlock-${i++}` ),
};
markdownReplacerTestbed.captureEventRegistration();
pageMemoTestbed.captureEventRegistration();
setVirtualFs( {
'foo': {
'qux.txt': '',
},
'package.json': '',
} );
plugin = mockPlugin<CodeBlockPlugin>();
plugin.application.converter.removeAllComponents();
new MarkdownCodeBlocks( plugin, themeMethods );
const ref = new DeclarationReflection( 'Foo', ReflectionKind.Class, createMockProjectWithPackage() );
pageMemoTestbed.setCurrentPage( 'foo.html', 'foo.ts', ref );
} );
afterEach( restoreFs );
describe( 'Behavior', () => {
it( 'should not affect text if no code block', () => {
const text = 'Hello world';
expect( markdownReplacerTestbed.runMarkdownReplace( text ) ).toEqual( text );
} );
describe( 'Code block generation', ()=> {
const sourceFile = resolve( rootDir, FILE );
interface IBlockGenerationAssertion{
renderCall: Partial<ICodeBlock>;
blocks: Array<[string, {code: string;startLine: number;endLine: number}]>;
withGitHub: boolean;
}
const defaultBlock = { startLine: 1, endLine: 1, file: FILE, region: DEFAULT_BLOCK_NAME as string };
const regions: IBlockGenerationAssertion['blocks'] = [
[ 'hello', { code: 'Content of foo/qux.txt', startLine: 13, endLine: 24 } ],
[ 'world', { code: 'stuff', startLine: 18, endLine: 42 } ],
];
it.each<[label: string, source: string, assertion: Partial<IBlockGenerationAssertion>]>( [
[ 'Mode ⇒ code block', `{@codeblock ${FILE} default}`, { renderCall: { mode: EBlockMode.DEFAULT }} ],
[ 'Mode ⇒ foldable code block', `{@codeblock ${FILE} expanded}`, { renderCall: { mode: EBlockMode.EXPANDED }} ],
[ 'Mode ⇒ folded code block', `{@codeblock ${FILE} folded}`, { renderCall: { mode: EBlockMode.FOLDED }} ],
[ 'Filename ⇒ default', `{@codeblock ${FILE}}`, { renderCall: { asFile: `./${FILE}` }} ],
[ 'Filename ⇒ explicit', `{@codeblock ${FILE} | hello.txt}`, { renderCall: { asFile: 'hello.txt' }} ],
[ 'Filename ⇒ region', `{@codeblock ${FILE}#hello}`, { renderCall: { asFile: `./${FILE}#13~24` }, blocks: [ regions[0] ] } ],
[ 'Filename ⇒ region ({a,b})', `{@codeblock ${FILE}#{hello,world}}`, { renderCall: { asFile: `./${FILE}#13~42` }, blocks: regions } ],
[ 'Filename ⇒ region (a+b)', `{@codeblock ${FILE}#hello+world}`, { renderCall: { asFile: `./${FILE}#13~42` }, blocks: regions } ],
[ 'Filename ⇒ region + explicit', `{@codeblock ${FILE}#hello | test.txt}`, { renderCall: { asFile: 'test.txt' }, blocks: [ regions[0] ] } ],
[ 'URL ⇒ default', `{@codeblock ${FILE}}`, { withGitHub: true, renderCall: { url: `${FakeSource.REPO_URL}/${FILE}#L1` }} ],
[ 'URL ⇒ region', `{@codeblock ${FILE}#hello}`, { withGitHub: true, renderCall: { url: `${FakeSource.REPO_URL}/${FILE}#L13` }, blocks: [ regions[0] ] } ],
[ 'Filename ⇒ region+mode+name', `{@codeblock ${FILE}#*o* folded | qux}`, { renderCall: { asFile: 'qux', mode: EBlockMode.FOLDED }, blocks: regions } ],
] )( 'Code block "%s"', ( _label, source, { renderCall, blocks, withGitHub } ) => {
if( withGitHub ){
plugin.application.converter.addComponent( 'source', FakeSource as any );
}
readCodeSampleMock.mockReturnValue( new Map( blocks?.map( ( [ name, b ] ) => [
name,
{ ...b, file: FILE, region: name } ] as const ) ?? [[ DEFAULT_BLOCK_NAME, { code: FILE_CONTENT, ...defaultBlock } ]] ) );
const callOut = markdownReplacerTestbed.runMarkdownReplace( source );
expect( themeMethods.renderCodeBlock ).toHaveBeenCalledTimes( 1 );
expect( themeMethods.renderCodeBlock ).toHaveBeenCalledWith( expect.objectContaining( renderCall ) );
if( withGitHub ){
expect( FakeSource.getURL ).toHaveBeenCalledTimes( 2 );
expect( FakeSource.getURL ).toHaveBeenCalledWith( sourceFile, blocks ? blocks[0][1].startLine : 1 );
expect( FakeSource.getRepository ).toHaveBeenCalledTimes( 2 );
expect( FakeSource.getRepository ).toHaveBeenCalledWith( sourceFile );
}
expect( callOut ).toEqual( 'renderCodeBlock-0' );
expect( plugin.logger.error ).not.toHaveBeenCalled();
expect( plugin.logger.warn ).not.toHaveBeenCalled();
} );
it( 'should not alter content if region does not exists', () => {
readCodeSampleMock.mockReturnValue( new Map( [[ DEFAULT_BLOCK_NAME, { code: FILE_CONTENT, ...defaultBlock } ]] ) );
const content = '{@codeblock foo/baz.txt#nope}';
plugin.logger.error.mockImplementation( noop );
expect( markdownReplacerTestbed.runMarkdownReplace( content ) ).toEqual( content );
} );
it( 'should throw if invalid mode', () => {
plugin.logger.error.mockImplementation( noop );
setVirtualFs( { foo: { 'bar.txt': '' }} );
readCodeSampleMock.mockReturnValue( new Map( [[ DEFAULT_BLOCK_NAME, { code: FILE_CONTENT, ...defaultBlock } ]] ) );
const content = '{@codeblock foo/bar.txt asdasd}';
expect( markdownReplacerTestbed.runMarkdownReplace( content ) ).toEqual( content );
expect( plugin.logger.error ).toHaveBeenCalledTimes( 1 );
expect( plugin.logger.error ).toHaveBeenCalledWith( expect.toSatisfy( fn => {
expect( fn() ).toEqual( 'In "foo.ts:1:1" (in expansion of @codeblock): Failed to render code block from Foo: Invalid block mode "asdasd".' );
return true;
} ) );
} );
} );
} );