1+ import fs from 'fs' ;
2+ import path from 'path' ;
3+ import { BlogPostFrontmatter } from '@/types' ;
4+
5+ describe ( 'Blog Posts Content Validation' , ( ) => {
6+ const postsDirectory = path . join ( process . cwd ( ) , 'src/content/posts' ) ;
7+
8+ // Helper function to get all markdown files
9+ const getMarkdownFiles = ( ) : string [ ] => {
10+ if ( ! fs . existsSync ( postsDirectory ) ) {
11+ return [ ] ;
12+ }
13+ return fs . readdirSync ( postsDirectory )
14+ . filter ( file => file . endsWith ( '.md' ) )
15+ . map ( file => path . join ( postsDirectory , file ) ) ;
16+ } ;
17+
18+ // Helper function to extract frontmatter from markdown
19+ const extractFrontmatter = ( content : string ) : BlogPostFrontmatter => {
20+ const frontmatterMatch = content . match ( / ^ - - - \n ( [ \s \S ] * ?) \n - - - / ) ;
21+ if ( ! frontmatterMatch ) {
22+ throw new Error ( 'No frontmatter found' ) ;
23+ }
24+
25+ const frontmatterText = frontmatterMatch [ 1 ] ;
26+ const frontmatter : any = { } ;
27+
28+ frontmatterText . split ( '\n' ) . forEach ( line => {
29+ const [ key , ...valueParts ] = line . split ( ':' ) ;
30+ if ( key && valueParts . length > 0 ) {
31+ const value = valueParts . join ( ':' ) . trim ( ) ;
32+
33+ if ( key === 'tags' ) {
34+ // Parse tags array
35+ frontmatter [ key ] = value . replace ( / [ \[ \] ] / g, '' ) . split ( ',' ) . map ( tag => tag . trim ( ) ) ;
36+ } else if ( key === 'date' ) {
37+ frontmatter [ key ] = value ;
38+ } else if ( key === 'readingTime' || key === 'published' ) {
39+ frontmatter [ key ] = key === 'published' ? value === 'true' : parseInt ( value ) ;
40+ } else {
41+ frontmatter [ key ] = value ;
42+ }
43+ }
44+ } ) ;
45+
46+ return frontmatter as BlogPostFrontmatter ;
47+ } ;
48+
49+ // Helper function to calculate reading time
50+ const calculateReadingTime = ( content : string ) : number => {
51+ const words = content . split ( / \s + / ) . length ;
52+ return Math . ceil ( words / 200 ) ; // 200 words per minute
53+ } ;
54+
55+ describe ( 'Frontmatter Validation' , ( ) => {
56+ it ( 'should have valid frontmatter for all blog posts' , ( ) => {
57+ const markdownFiles = getMarkdownFiles ( ) ;
58+
59+ if ( markdownFiles . length === 0 ) {
60+ // This test will fail initially as no posts exist yet
61+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
62+ }
63+
64+ markdownFiles . forEach ( filePath => {
65+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
66+ const frontmatter = extractFrontmatter ( content ) ;
67+
68+ // Validate required fields
69+ expect ( frontmatter . title ) . toBeDefined ( ) ;
70+ expect ( frontmatter . title . length ) . toBeGreaterThan ( 0 ) ;
71+ expect ( frontmatter . date ) . toBeDefined ( ) ;
72+ expect ( frontmatter . excerpt ) . toBeDefined ( ) ;
73+ expect ( frontmatter . excerpt . length ) . toBeGreaterThan ( 0 ) ;
74+ expect ( frontmatter . tags ) . toBeDefined ( ) ;
75+ expect ( Array . isArray ( frontmatter . tags ) ) . toBe ( true ) ;
76+ expect ( frontmatter . category ) . toBeDefined ( ) ;
77+ expect ( frontmatter . readingTime ) . toBeDefined ( ) ;
78+ expect ( typeof frontmatter . readingTime ) . toBe ( 'number' ) ;
79+ expect ( frontmatter . published ) . toBeDefined ( ) ;
80+ expect ( typeof frontmatter . published ) . toBe ( 'boolean' ) ;
81+ } ) ;
82+ } ) ;
83+
84+ it ( 'should have valid date format in frontmatter' , ( ) => {
85+ const markdownFiles = getMarkdownFiles ( ) ;
86+
87+ if ( markdownFiles . length === 0 ) {
88+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
89+ }
90+
91+ markdownFiles . forEach ( filePath => {
92+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
93+ const frontmatter = extractFrontmatter ( content ) ;
94+
95+ // Validate date format (YYYY-MM-DD)
96+ expect ( frontmatter . date ) . toMatch ( / ^ \d { 4 } - \d { 2 } - \d { 2 } $ / ) ;
97+ expect ( ( ) => new Date ( frontmatter . date ) ) . not . toThrow ( ) ;
98+ } ) ;
99+ } ) ;
100+ } ) ;
101+
102+ describe ( 'Markdown Content Validation' , ( ) => {
103+ it ( 'should have properly formatted markdown content' , ( ) => {
104+ const markdownFiles = getMarkdownFiles ( ) ;
105+
106+ if ( markdownFiles . length === 0 ) {
107+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
108+ }
109+
110+ markdownFiles . forEach ( filePath => {
111+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
112+
113+ // Remove frontmatter to get content only
114+ const contentOnly = content . replace ( / ^ - - - \n [ \s \S ] * ?\n - - - \n / , '' ) ;
115+
116+ // Should have content after frontmatter
117+ expect ( contentOnly . trim ( ) . length ) . toBeGreaterThan ( 0 ) ;
118+
119+ // Should have at least one heading
120+ expect ( contentOnly ) . toMatch ( / ^ # \s + / m) ;
121+
122+ // Should have proper paragraph spacing
123+ expect ( contentOnly ) . toMatch ( / \n \n / ) ;
124+ } ) ;
125+ } ) ;
126+
127+ it ( 'should have code blocks with proper syntax highlighting' , ( ) => {
128+ const markdownFiles = getMarkdownFiles ( ) ;
129+
130+ if ( markdownFiles . length === 0 ) {
131+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
132+ }
133+
134+ markdownFiles . forEach ( filePath => {
135+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
136+ const contentOnly = content . replace ( / ^ - - - \n [ \s \S ] * ?\n - - - \n / , '' ) ;
137+
138+ // Should have at least one code block
139+ expect ( contentOnly ) . toMatch ( / ` ` ` \w * \n [ \s \S ] * ?\n ` ` ` / ) ;
140+
141+ // Code blocks should have language specification
142+ const codeBlocks = contentOnly . match ( / ` ` ` ( \w + ) \n [ \s \S ] * ?\n ` ` ` / g) ;
143+ if ( codeBlocks ) {
144+ codeBlocks . forEach ( block => {
145+ expect ( block ) . toMatch ( / ` ` ` \w + \n / ) ;
146+ } ) ;
147+ }
148+ } ) ;
149+ } ) ;
150+ } ) ;
151+
152+ describe ( 'Reading Time Calculation' , ( ) => {
153+ it ( 'should have accurate reading time calculation' , ( ) => {
154+ const markdownFiles = getMarkdownFiles ( ) ;
155+
156+ if ( markdownFiles . length === 0 ) {
157+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
158+ }
159+
160+ markdownFiles . forEach ( filePath => {
161+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
162+ const frontmatter = extractFrontmatter ( content ) ;
163+ const contentOnly = content . replace ( / ^ - - - \n [ \s \S ] * ?\n - - - \n / , '' ) ;
164+
165+ const calculatedReadingTime = calculateReadingTime ( contentOnly ) ;
166+ const frontmatterReadingTime = frontmatter . readingTime ;
167+
168+ // Reading time should be within 1 minute of calculated time
169+ expect ( Math . abs ( calculatedReadingTime - frontmatterReadingTime ) ) . toBeLessThanOrEqual ( 1 ) ;
170+ } ) ;
171+ } ) ;
172+ } ) ;
173+
174+ describe ( 'Word Count Validation' , ( ) => {
175+ it ( 'should have appropriate word count (800-1200 words)' , ( ) => {
176+ const markdownFiles = getMarkdownFiles ( ) ;
177+
178+ if ( markdownFiles . length === 0 ) {
179+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
180+ }
181+
182+ markdownFiles . forEach ( filePath => {
183+ const content = fs . readFileSync ( filePath , 'utf-8' ) ;
184+ const contentOnly = content . replace ( / ^ - - - \n [ \s \S ] * ?\n - - - \n / , '' ) ;
185+
186+ const wordCount = contentOnly . split ( / \s + / ) . length ;
187+
188+ // Should be between 800-1200 words
189+ expect ( wordCount ) . toBeGreaterThanOrEqual ( 800 ) ;
190+ expect ( wordCount ) . toBeLessThanOrEqual ( 1200 ) ;
191+ } ) ;
192+ } ) ;
193+ } ) ;
194+
195+ describe ( 'Slug Naming Convention' , ( ) => {
196+ it ( 'should follow snake_case naming convention' , ( ) => {
197+ const markdownFiles = getMarkdownFiles ( ) ;
198+
199+ if ( markdownFiles . length === 0 ) {
200+ throw new Error ( 'No blog posts found in content/posts directory' ) ;
201+ }
202+
203+ markdownFiles . forEach ( filePath => {
204+ const fileName = path . basename ( filePath , '.md' ) ;
205+
206+ // Should be snake_case format
207+ expect ( fileName ) . toMatch ( / ^ [ a - z 0 - 9 _ ] + $ / ) ;
208+ expect ( fileName ) . not . toMatch ( / [ A - Z ] / ) ;
209+ expect ( fileName ) . not . toMatch ( / ^ - | - $ / ) ;
210+ } ) ;
211+ } ) ;
212+ } ) ;
213+ } ) ;
0 commit comments